diff options
author | Franck Cuny <franck@fcuny.net> | 2024-03-06 06:29:24 -0800 |
---|---|---|
committer | Franck Cuny <franck@fcuny.net> | 2024-03-06 06:29:24 -0800 |
commit | 1e4a5aa09c1c8f43722c9c260f011398799a8e8f (patch) | |
tree | cd73e0fb8ba53bd21cee6ccf2dcc85639bbbb93f /packages/schedlatency | |
parent | set correct git email in the profiles (diff) | |
download | world-1e4a5aa09c1c8f43722c9c260f011398799a8e8f.tar.gz |
rename `tools` to `packages` to follow convention
The convention is to use `pkgs` or `packages` for overlays and definition of custom packages. Since I'm already using `pkg` for go, I prefer to use `packages` for my scripts.
Diffstat (limited to 'packages/schedlatency')
-rw-r--r-- | packages/schedlatency/go.mod | 3 | ||||
-rw-r--r-- | packages/schedlatency/main.go | 254 |
2 files changed, 257 insertions, 0 deletions
diff --git a/packages/schedlatency/go.mod b/packages/schedlatency/go.mod new file mode 100644 index 0000000..9a073ac --- /dev/null +++ b/packages/schedlatency/go.mod @@ -0,0 +1,3 @@ +module golang.fcuny.net/schedlatency + +go 1.17 diff --git a/packages/schedlatency/main.go b/packages/schedlatency/main.go new file mode 100644 index 0000000..7dd709e --- /dev/null +++ b/packages/schedlatency/main.go @@ -0,0 +1,254 @@ +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 +} |