about summary refs log tree commit diff
path: root/tools/schedlatency/main.go
blob: 052202624f33f8b832228b68d382dc02efa14819 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"strconv"
	"strings"
	"time"
)

type SchedStat struct {
	Pid         int     `json:"pid"`
	RunTicks    int     `json:"run_ticks"`
	WaitTicks   int     `json:"wait_ticks"`
	SlicesRan   int     `json:"ran_slices"`
	AverageRun  float64 `json:"avg_run"`
	AverageWait float64 `json:"avg_wait"`
}

func usage() {
	fmt.Fprintf(os.Stderr, "usage: %s <pid>\n", os.Args[0])
}

func main() {
	if len(os.Args) == 1 {
		usage()
		os.Exit(1)
	}

	input := os.Args[1]
	pid, err := strconv.Atoi(input)
	if err != nil {
		fmt.Fprintf(os.Stderr, "failed to convert %s to a PID: %v", input, err)
		os.Exit(1)
	}

	p := Proc{
		PID: pid,
	}
	oran := 0
	owait_ticks := 0
	orun_ticks := 0
	for {
		stat, err := p.SchedStat()
		if err != nil {
			fmt.Fprintf(os.Stderr, "failed to get schedstat for %d: %v\n", p.PID, err)
			os.Exit(1)
		}
		diff := stat.SlicesRan - oran
		var avgrun, avgwait float64

		if diff > 0 {
			avgrun = float64((stat.RunTicks - orun_ticks) / diff)
			avgwait = float64((stat.WaitTicks - owait_ticks) / diff)
		} else {
			avgrun = 0
			avgwait = 0
		}

		stat.AverageRun = avgrun
		stat.AverageWait = avgwait

		out, err := json.Marshal(stat)
		if err != nil {
			fmt.Fprintln(err)
			os.Exit(1)
		}
		fmt.Println(string(out))
		oran = stat.SlicesRan
		orun_ticks = stat.RunTicks
		owait_ticks = stat.WaitTicks
		time.Sleep(5 * time.Second)
	}
}

// This the the path that contains the scheduler statistics.
// Note that they are not populated unless the value for
// /proc/sys/kernel/sched_schedstats is 1
const procSchedStat = "/proc/schedstat"

var idleness = []string{"idle", "busy", "newlyIdle"}

type ProcSchedStat struct {
	RunTicks    int     `json:"run_ticks"`
	WaitTicks   int     `json:"wait_ticks"`
	SlicesRan   int     `json:"ran_slices"`
	AverageRun  float64 `json:"avg_run"`
	AverageWait float64 `json:"avg_wait"`
}

// SchedCPUStat contains the load balancer statistics for a CPU.
type SchedCPUStat struct {
	YieldCount       uint64                 `json:"yield_count"`
	SchedulerCount   uint64                 `json:"sched_count"`
	SchedulerGoIdle  uint64                 `json:"sched_go_idle"`
	TryToWakeUp      uint64                 `json:"try_to_wake"`
	TryToWakeUpLocal uint64                 `json:"try_to_wake_local"`
	Running          uint64                 `json:"running"`
	Waiting          uint64                 `json:"waiting"`
	Slices           uint64                 `json:"slices"`
	Domains          map[string]SchedDomain `json:"domains"`
}

// SchedLoadBalance contains the load balancer statistics for a domain
// in a given domain.
type SchedLoadBalance struct {
	LBCount       uint64 `json:"lb_count"`
	LBBalanced    uint64 `json:"lb_balanced"`
	LBFailed      uint64 `json:"lb_failed"`
	LBImbalanced  uint64 `json:"lb_imbalanced"`
	LBGained      uint64 `json:"lb_gained"`
	LBHotGain     uint64 `json:"lb_hot_gain"`
	LBNoBusyQueue uint64 `json:"lb_no_busy_queue"`
	LBNoBusyGroup uint64 `json:"lb_no_busy_group"`
}

// SchedDomain contains the statistics for a domain.
type SchedDomain struct {
	LoadBalancers           map[string]SchedLoadBalance `json:"lbs"`
	ActiveLoadBalanceCount  uint64                      `json:"active_lb_count"`
	ActiveLoadBalanceFailed uint64                      `json:"active_lb_failed"`
	ActiveLoadBalancePushed uint64                      `json:"active_lb_pushed"`
	TryToWakeUpRemote       uint64                      `json:"try_to_wake_up_remote"`
	TryToWakeUpMoveAffine   uint64                      `json:"try_to_wake_up_move_affine"`
	TryToWakeUpMoveBalance  uint64                      `json:"try_to_wake_up_move_balance"`
}

// Proc provides information about a running process.
type Proc struct {
	// The process ID.
	PID int
}

// SchedStat returns scheduler statistics for the process.
// The information available are:
// 1. time spent on the cpu
// 2. time spent waiting on a runqueue
// 3. # of timeslices run on this cpu
//
func (p Proc) SchedStat() (ProcSchedStat, error) {
	path := fmt.Sprintf("/proc/%d/schedstat", p.PID)
	b, err := ioutil.ReadFile(path)
	if err != nil {
		return ProcSchedStat{}, err
	}
	content := string(b)
	stats := strings.Fields(content)

	run_ticks, err := strconv.Atoi(stats[0])
	if err != nil {
		return ProcSchedStat{}, err
	}

	wait_ticks, err := strconv.Atoi(stats[1])
	if err != nil {
		return ProcSchedStat{}, err
	}

	nran, err := strconv.Atoi(stats[2])
	if err != nil {
		return ProcSchedStat{}, err
	}

	stat := ProcSchedStat{
		RunTicks:  run_ticks,
		WaitTicks: wait_ticks,
		SlicesRan: nran,
	}
	return stat, nil
}

// ReadSchedstat returns statistics from the scheduler.
// Information about the statistics can be found at
// https://www.kernel.org/doc/html/latest/scheduler/sched-stats.html.
func ReadSchedStat() (map[string]SchedCPUStat, error) {
	b, err := ioutil.ReadFile(procSchedStat)
	if err != nil {
		return nil, fmt.Errorf("procfs: failed to open %s: %v", procSchedStat, err)
	}
	content := string(b)

	cpus := map[string]SchedCPUStat{}

	lines := strings.Split(content, "\n")

	var currentCpu string

	// The first line is the version of the stats
	// TODO(fcuny): we should check which version is used, because the
	// format changes.
	for _, line := range lines[2:] {
		// The format is as follow:
		// cpu<N> 1 2 3 4 5 6 7 8 9
		// domain<N> <cpumask> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
		if strings.HasPrefix(line, "cpu") {
			// meaning of the fields: https://www.kernel.org/doc/html/latest/scheduler/sched-stats.html#cpu-statistics
			fields := strings.Fields(line)
			cpuStat := SchedCPUStat{
				YieldCount:       convertField(fields[1]),
				SchedulerCount:   convertField(fields[3]),
				SchedulerGoIdle:  convertField(fields[4]),
				TryToWakeUp:      convertField(fields[5]),
				TryToWakeUpLocal: convertField(fields[6]),
				Running:          convertField(fields[7]),
				Waiting:          convertField(fields[8]),
				Slices:           convertField(fields[9]),
				Domains:          map[string]SchedDomain{},
			}
			currentCpu = fields[0]
			cpus[currentCpu] = cpuStat
		} else if strings.HasPrefix(line, "domain") {
			// meaning of the fields: https://www.kernel.org/doc/html/latest/scheduler/sched-stats.html#domain-statistics
			fields := strings.Fields(line)
			i := 2
			lbs := map[string]SchedLoadBalance{}
			for _, idle := range idleness {
				lb := SchedLoadBalance{
					LBCount:       convertField(fields[i]),
					LBBalanced:    convertField(fields[i+1]),
					LBFailed:      convertField(fields[i+2]),
					LBImbalanced:  convertField(fields[i+3]),
					LBGained:      convertField(fields[i+4]),
					LBHotGain:     convertField(fields[i+5]),
					LBNoBusyQueue: convertField(fields[i+6]),
					LBNoBusyGroup: convertField(fields[i+7]),
				}
				i = i + 8
				lbs[idle] = lb
			}
			domain := SchedDomain{
				LoadBalancers:           lbs,
				ActiveLoadBalanceCount:  convertField(fields[26]),
				ActiveLoadBalanceFailed: convertField(fields[27]),
				ActiveLoadBalancePushed: convertField(fields[28]),
				TryToWakeUpRemote:       convertField(fields[35]),
				TryToWakeUpMoveAffine:   convertField(fields[36]),
				TryToWakeUpMoveBalance:  convertField(fields[37]),
			}
			c := cpus[currentCpu]
			c.Domains[fields[0]] = domain
			cpus[currentCpu] = c
		}
	}
	return cpus, nil
}

func convertField(field string) uint64 {
	val, err := strconv.ParseUint(field, 10, 64)
	if err != nil {
		return 0
	}
	return val
}