Add disk-load: no tool said a machine's iowait, disk load or who does the I/O (hq issue 312)
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CommandTimeout bounds the one command this module runs (the container runtime's list of names).
|
||||
const CommandTimeout = 5 * time.Second
|
||||
|
||||
// StatfsTimeout bounds the look at one mount: a network filesystem whose server is gone blocks statfs
|
||||
// for as long as it likes, and must cost the answer one line, never the whole call.
|
||||
const StatfsTimeout = 2 * time.Second
|
||||
|
||||
// Runner runs one command and answers its standard output. It is injected so that the tools are tested
|
||||
// against recorded answers rather than this machine's daemons.
|
||||
type Runner func(ctx context.Context, name string, args ...string) (string, error)
|
||||
|
||||
// ExecRunner runs a command on the machine, bounded by CommandTimeout.
|
||||
func ExecRunner(ctx context.Context, name string, args ...string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, CommandTimeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
said := strings.TrimSpace(stderr.String())
|
||||
if said != "" {
|
||||
return "", fmt.Errorf("%s: %w: %s", name, err, said)
|
||||
}
|
||||
return "", fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
return stdout.String(), nil
|
||||
}
|
||||
|
||||
// FSStat is what statfs says of one mounted filesystem, in bytes and inodes.
|
||||
type FSStat struct {
|
||||
Size, Free, Avail uint64
|
||||
Files, FilesFree uint64
|
||||
}
|
||||
|
||||
// Machine is what the module reads: a filesystem root (the real one, or a test's tree standing in for
|
||||
// /proc and /sys), a way to run a command, a way to wait out the sampling window, and statfs.
|
||||
type Machine struct {
|
||||
Root string
|
||||
Run Runner
|
||||
Wait func(time.Duration)
|
||||
Statfs func(path string) (FSStat, error)
|
||||
}
|
||||
|
||||
// Here is the machine this process runs on.
|
||||
func Here() *Machine {
|
||||
return &Machine{Root: "/", Run: ExecRunner, Wait: time.Sleep, Statfs: statfs}
|
||||
}
|
||||
|
||||
func statfs(path string) (FSStat, error) {
|
||||
type answer struct {
|
||||
s FSStat
|
||||
err error
|
||||
}
|
||||
done := make(chan answer, 1)
|
||||
go func() {
|
||||
var s syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &s); err != nil {
|
||||
done <- answer{err: err}
|
||||
return
|
||||
}
|
||||
bs := uint64(s.Bsize)
|
||||
done <- answer{s: FSStat{Size: s.Blocks * bs, Free: s.Bfree * bs, Avail: s.Bavail * bs, Files: s.Files, FilesFree: s.Ffree}}
|
||||
}()
|
||||
select {
|
||||
case a := <-done:
|
||||
return a.s, a.err
|
||||
case <-time.After(StatfsTimeout):
|
||||
return FSStat{}, fmt.Errorf("statfs did not answer within %s", StatfsTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Machine) path(p string) string { return filepath.Join(m.Root, p) }
|
||||
|
||||
// read is a file's content; "" when it cannot be read.
|
||||
func (m *Machine) read(p string) string {
|
||||
b, err := os.ReadFile(m.path(p))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (m *Machine) exists(p string) bool {
|
||||
_, err := os.Stat(m.path(p))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// names lists the entries of a directory, sorted; none when it cannot be read.
|
||||
func (m *Machine) names(dir string) []string {
|
||||
entries, err := os.ReadDir(m.path(dir))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
out = append(out, e.Name())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// round1 rounds to one decimal, for what a person reads.
|
||||
func round1(f float64) float64 {
|
||||
if f < 0 {
|
||||
return -float64(int64(-f*10+0.5)) / 10
|
||||
}
|
||||
return float64(int64(f*10+0.5)) / 10
|
||||
}
|
||||
|
||||
const mib = 1 << 20
|
||||
const gib = 1 << 30
|
||||
Reference in New Issue
Block a user