package buildstep import ( "encoding/json" "fmt" "math" "os" "runtime" "strconv" "strings" "git.novox.be/novox/mesh-sdk/go/buildcpu" "git.novox.be/novox/mesh-sdk/go/delivery" ) // **A build seat's capacity follows the CPU its node gives builds** (issue 1806's follow-up; issue 1890, 2026-10-26 // 19:41: under the 50 % cap shanks' mesh-build-go kept capacity 3, three full mesh-catalog checks shared 12 cores, and // each step ran past the 30-minute step bound with no test run). The configured capacity is an upper bound; the node's // build slice bounds it again by the cores a step needs: floor(the slice's quota in cores / cores-per-step), one at // least. The quota is read live from the cgroup at every judgement, so a changed cap is followed at the next take // without a restart, and the result is said (the seat's current verb, the status tools, `capacity.` in the pauses // state that the delivery view's build_usage reads). // CoresPerStepSetting is a build module's setting of the processors one of its steps needs: a number above zero, the // same on every node (a step's need is the module's, not the node's). Each build module declares it with its own default // (DefaultCoresPerStep). const CoresPerStepSetting = "cores-per-step" // DefaultCoresPerStep are the build modules' defaults by language, from measured steps (2026-10-26): a full mesh-catalog // check on mesh-build-go takes what it is given (three at once took 965 % of shanks' 1200 % slice, about 4 cores each, // and ran past the 30-minute step bound), so a Go step is given 6, half again what starved; a Node step (tsc, esbuild, a // test runner: median 15 s) and an image build (buildah, its RUN lines mostly fetches) need 2. var DefaultCoresPerStep = map[string]float64{delivery.AskGo: 6, delivery.AskNode: 2, delivery.AskImage: 2} // Quota is the build slice's CPU quota as this process's cgroup holds it, in percent of one processor (1200 is 12 // cores), and where it was read: the cgroup's cpu.max (buildcpu.ReadInForce, the tighter of its unit's and its slice's), // else the share the composer gave (buildcpu.ShareWord) over the machine's processors, else 0 and why none is known. func Quota() (percent int, from string) { f := buildcpu.ReadInForce() if f.Quota > 0 { path := f.Slice.Path if path == "" { path = f.Cgroup } return f.Quota, "the cpu.max of " + path } if n, err := strconv.Atoi(strings.TrimSpace(os.Getenv(buildcpu.ShareWord))); err == nil && n >= 1 && n <= 100 { return buildcpu.Quota(n, runtime.NumCPU()), fmt.Sprintf("the share %d%% the composer gave (%s), of %d processors; no cpu.max is read", n, buildcpu.ShareWord, runtime.NumCPU()) } return 0, "no CPU quota: neither a cpu.max on its cgroup nor a share from the composer" } // EffectiveCapacity is the capacity a seat's holder judges by, from its configured capacity, its node's quota (percent of // one processor, 0 when none is known) and the cores one of its steps needs. Without a quota it is the configured one; // with one, min(configured, floor(quota cores / coresPerStep)), and one at least while the configured one is above // zero. A coresPerStep of zero or less is taken as 1. func EffectiveCapacity(configured, quotaPercent int, quotaFrom string, coresPerStep float64) delivery.Capacity { if coresPerStep <= 0 || math.IsNaN(coresPerStep) || math.IsInf(coresPerStep, 0) { coresPerStep = 1 } c := delivery.Capacity{Configured: configured, Effective: configured, QuotaFrom: quotaFrom, CoresPerStep: coresPerStep} switch { case configured <= 0: c.Effective = 0 c.Why = fmt.Sprintf("its configured capacity is %d, so it pulls nothing", configured) return c case quotaPercent <= 0: c.Why = fmt.Sprintf("its configured capacity %d; %s, so the CPU bounds nothing", configured, quotaFrom) return c } c.QuotaCores = float64(quotaPercent) / 100 c.ByCPU = int(math.Floor(c.QuotaCores/coresPerStep + 1e-9)) if c.ByCPU < 1 { c.ByCPU = 1 } byCPU := fmt.Sprintf("%g cores of the build slice (%s) / %g cores a step = %d", c.QuotaCores, quotaFrom, coresPerStep, c.ByCPU) if c.ByCPU < configured { c.Effective = c.ByCPU c.Why = fmt.Sprintf("%d, bounded by the CPU: %s, under its configured %d", c.Effective, byCPU, configured) } else { c.Why = fmt.Sprintf("%d, its configured capacity; the CPU allows %s", configured, byCPU) } return c } // NodeCoresPerStep is the module's cores-per-step, read from its settings file at path at every call so a change is // followed at the next judgement; fallback when there is no path, no value or one that is not a number above zero, said // once per distinct fault through logf. func NodeCoresPerStep(path string, fallback float64, logf func(string, ...any)) func() float64 { said := "" say := func(why string) { if logf != nil && why != said { said = why logf("%s, so a step is taken to need %g cores", why, fallback) } } return func() float64 { if path == "" { return fallback } raw, err := os.ReadFile(path) if err != nil { say(fmt.Sprintf("the setting %s cannot be read: %v", CoresPerStepSetting, err)) return fallback } var s map[string]json.RawMessage if err := json.Unmarshal(raw, &s); err != nil { say(fmt.Sprintf("the settings file %s is not JSON: %v", path, err)) return fallback } v, ok := s[CoresPerStepSetting] if !ok { return fallback // not declared yet (a module before its manifest names it): the default, nothing to say } n, err := coresOf(v) if err != nil { say(fmt.Sprintf("the setting %s %s: %v", CoresPerStepSetting, v, err)) return fallback } said = "" return n } } // coresOf reads a cores-per-step value: a JSON number or a string holding one, above zero. func coresOf(v json.RawMessage) (float64, error) { var n float64 if err := json.Unmarshal(v, &n); err != nil { var s string if err := json.Unmarshal(v, &s); err != nil { return 0, fmt.Errorf("is neither a number nor a string") } if n, err = strconv.ParseFloat(strings.TrimSpace(s), 64); err != nil { return 0, fmt.Errorf("is not a number") } } if n <= 0 || math.IsNaN(n) || math.IsInf(n, 0) { return 0, fmt.Errorf("is not above zero") } return n, nil }