about summary refs log tree commit diff
path: root/packages/schedlatency
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2024-03-06 06:29:24 -0800
committerFranck Cuny <franck@fcuny.net>2024-03-06 06:29:24 -0800
commit1e4a5aa09c1c8f43722c9c260f011398799a8e8f (patch)
treecd73e0fb8ba53bd21cee6ccf2dcc85639bbbb93f /packages/schedlatency
parentset correct git email in the profiles (diff)
downloadworld-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.mod3
-rw-r--r--packages/schedlatency/main.go254
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
+}