Build modules: a per-node processor cap, and each step's threads within it (issue 1806)
The operator's desktop slows while it builds (shanks at 92%, the Go build runtime at 1650% of 24 processors). The build modules declare cpu-cap (default shanks=50,g14=50,ace=75), which the composer turns into the runtime's quota at idle priority; each step's go, node and buildah commands get GOMAXPROCS and UV_THREADPOOL_SIZE for their part of it, and the status tools show the limits and what the kernel holds the runtime to.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-sdk/go/buildcpu"
|
||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||
)
|
||||
|
||||
// The processors a step may take (novox/hq issue 1806). The kernel holds the module's runtime to idle priority and to
|
||||
// its node's share of the processors (the setting cpu-cap, written into its unit by the node-engine); inside that
|
||||
// bound a step starts no more threads than its part of it, so the steps running at once do not wait in the kernel's
|
||||
// throttle.
|
||||
|
||||
// cpuLimits is what one step is held to now: the cap from the settings file, the machine's processors and the node's
|
||||
// capacity, read at every call like the capacity, so a change takes effect at the next step.
|
||||
func (m *Module) cpuLimits(say func(string, ...any)) buildcpu.Limits {
|
||||
slots := 1
|
||||
switch {
|
||||
case m.Capacity != nil:
|
||||
slots = m.Capacity()
|
||||
case m.Settings != "":
|
||||
slots = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, nil)()
|
||||
}
|
||||
if slots < 1 {
|
||||
slots = 1
|
||||
}
|
||||
return buildstep.NodeLimits(m.Settings, m.Node, slots, say)
|
||||
}
|
||||
|
||||
// threadEnv is the environment every command of a step is given for its threads: GOMAXPROCS for esbuild and every Go
|
||||
// program, UV_THREADPOOL_SIZE for Node's worker pool.
|
||||
func (m *Module) threadEnv() []string {
|
||||
return buildcpu.Env(m.cpuLimits(nil).Threads)
|
||||
}
|
||||
|
||||
// cpuStatus is what the status tool says of it: the limits a step is given, and what the kernel holds this runtime
|
||||
// to, read from its cgroup rather than assumed.
|
||||
func (m *Module) cpuStatus() map[string]any {
|
||||
return map[string]any{"limits": m.cpuLimits(m.Say), "in-force": buildcpu.ReadInForce()}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -245,6 +246,11 @@ func TestNoCommandOfTheStepSeesATokenOfTheProcess(t *testing.T) {
|
||||
if len(c.env) < 3 || !strings.HasPrefix(c.env[0], "PATH=") || !strings.HasPrefix(c.env[1], "HOME=") || c.env[2] != "LC_ALL=C" {
|
||||
t.Errorf("%s was given %v, want PATH, HOME and the locale first", c.name, c.env)
|
||||
}
|
||||
// And its threads, its part of the node's processor cap (issue 1806).
|
||||
if !slices.ContainsFunc(c.env, func(e string) bool { return strings.HasPrefix(e, "GOMAXPROCS=") }) ||
|
||||
!slices.ContainsFunc(c.env, func(e string) bool { return strings.HasPrefix(e, "UV_THREADPOOL_SIZE=") }) {
|
||||
t.Errorf("%s was given %v, without its threads", c.name, c.env)
|
||||
}
|
||||
}
|
||||
// The runner main wires: what it hands a command is exactly what the step gave.
|
||||
ran := forgekey.ExecRunnerClean(10*time.Second)(context.Background(), append(m.baseEnv(), "ONLY=this"), "env")
|
||||
|
||||
@@ -35,7 +35,7 @@ func (m *Module) isolation(root string, write bool) buildstep.Isolation {
|
||||
ReadOnly: []string{m.toolchainDir()},
|
||||
Caches: []string{m.npmCache()},
|
||||
WriteCaches: write,
|
||||
Env: []string{"PATH=" + pathOrDefault(), "LC_ALL=C"},
|
||||
Env: append([]string{"PATH=" + pathOrDefault(), "LC_ALL=C"}, m.threadEnv()...),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,8 @@ func (m *Module) Status(ctx context.Context) map[string]any {
|
||||
err := m.Check(ctx)
|
||||
installed, _ := m.Probe(ctx)
|
||||
r := m.recipe()
|
||||
out := map[string]any{"node": m.Node, "recipe": r.Version(), "installed": installed, "refusing": err != nil}
|
||||
out := map[string]any{"node": m.Node, "recipe": r.Version(), "installed": installed, "refusing": err != nil,
|
||||
"cpu": m.cpuStatus()}
|
||||
if m.Toolchain != nil {
|
||||
out["toolchain-dir"] = m.Toolchain.Paths.Dir(r.Version())
|
||||
out["toolchain-installed"] = m.Toolchain.Present(r.Version())
|
||||
@@ -227,7 +228,7 @@ const defaultPath = "/usr/local/bin:/usr/bin:/bin"
|
||||
// files and git configuration) and a fixed locale. A command is given these and its own words, never this process's
|
||||
// environment, which holds the module's bus credentials (ADR 0466 §4).
|
||||
func (m *Module) baseEnv() []string {
|
||||
return []string{"PATH=" + pathOrDefault(), "HOME=" + m.Toolchain.homeOrEmpty(), "LC_ALL=C"}
|
||||
return append([]string{"PATH=" + pathOrDefault(), "HOME=" + m.Toolchain.homeOrEmpty(), "LC_ALL=C"}, m.threadEnv()...)
|
||||
}
|
||||
|
||||
func pathOrDefault() string {
|
||||
|
||||
Reference in New Issue
Block a user