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
}