provisioner_compose took 2.6-13.6 s per node with nothing changed, CPU bound. The node is now composed at its wanted and its newest builds over one read (frombus.PlansHeldTwice, whose composer resolves each machine once), its own plans, provisions and refusals and the providers' entries are read from the watched copy, its own writes taken in at once, and a provider's manifest once by digest: no read by key per composition is left. The result says where the time went, phase by phase, and a slow composition that changed nothing says it in the log. BenchmarkComposeNode times it over a mesh captured with the read tools (MESH_COMPOSE_CAPTURE): novox 11.7 s to 0.49 s, ace 0.49 s, g14 and shanks 0.39 s; plans and provisions identical to before over nine compositions of the four nodes. Needs mesh-sdk feat/compose-once (5d26c65); go.mod still pins the old mesh-sdk until the group pins its head. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
226 lines
7.0 KiB
Go
226 lines
7.0 KiB
Go
package main
|
|
|
|
// BenchmarkComposeNode times ComposeNode of an unchanged node over a mesh captured from the bus (issue 1954): the
|
|
// placement, wants, haves, the indexer's modules and manifests, the builder's builds, mesh-config's resolved values and
|
|
// the templates archives, read with the read tools (mesh-placement.placement, mesh-delivery-view.assigner_wants,
|
|
// node_haves, indexer_modules, builder_build, mesh-config.config) and the manifests and archives by digest. The capture
|
|
// is not kept in this repository (a few megabytes, and the mesh's own addresses and keys): the benchmark reads the
|
|
// directory MESH_COMPOSE_CAPTURE names and is skipped without it. MESH_COMPOSE_NODE names the node (novox when unset).
|
|
//
|
|
// The capture's layout: placement.json, wants.json, haves.json, modules.json and config.json as those tools answer,
|
|
// builds/<module>.<hex>.json as builder_build answers, manifests/<digest> and archives/<hex> as their bytes.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
|
|
"git.novox.be/novox/mesh-sdk/go/compose/fromruntime"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// countingStore counts the holder's reads of its own states (and the providers' entries), each a bus request live.
|
|
type countingStore struct {
|
|
Store
|
|
gets, keys atomic.Int64
|
|
}
|
|
|
|
func (c *countingStore) Get(state, key string) (*Entry, error) {
|
|
c.gets.Add(1)
|
|
return c.Store.Get(state, key)
|
|
}
|
|
|
|
func (c *countingStore) Keys(state string) ([]string, error) {
|
|
c.keys.Add(1)
|
|
return c.Store.Keys(state)
|
|
}
|
|
|
|
// captured is the captured mesh as the states the holder reads, and the archives by digest.
|
|
func captured(b *testing.B, dir string) (map[string]map[string][]byte, map[string][]byte) {
|
|
b.Helper()
|
|
read := func(name string, into any) {
|
|
raw, err := os.ReadFile(filepath.Join(dir, name))
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
if err := json.Unmarshal(raw, into); err != nil {
|
|
b.Fatalf("%s: %v", name, err)
|
|
}
|
|
}
|
|
states := map[string]map[string][]byte{}
|
|
put := func(state, key string, raw []byte) {
|
|
if states[state] == nil {
|
|
states[state] = map[string][]byte{}
|
|
}
|
|
states[state][key] = raw
|
|
}
|
|
var placement struct {
|
|
Keys map[string]struct {
|
|
Placement json.RawMessage `json:"placement"`
|
|
} `json:"keys"`
|
|
}
|
|
read("placement.json", &placement)
|
|
for node, k := range placement.Keys {
|
|
var p delivery.Placement
|
|
if err := json.Unmarshal(k.Placement, &p); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
p.Node = node
|
|
raw, err := delivery.EncodePlacement(p)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
put(PlacementRead, "placement."+node, raw)
|
|
}
|
|
var wants struct {
|
|
Wants map[string]json.RawMessage `json:"wants"`
|
|
}
|
|
read("wants.json", &wants)
|
|
for k, v := range wants.Wants {
|
|
put(WantsRead, k, v)
|
|
}
|
|
var haves struct {
|
|
Keys map[string]json.RawMessage `json:"keys"`
|
|
}
|
|
read("haves.json", &haves)
|
|
for k, v := range haves.Keys {
|
|
put(HaveRead, k, v)
|
|
}
|
|
var modules struct {
|
|
Modules map[string]json.RawMessage `json:"modules"`
|
|
}
|
|
read("modules.json", &modules)
|
|
for m, v := range modules.Modules {
|
|
key, err := delivery.ModulesKey(m)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
put(ModulesRead, key, v)
|
|
}
|
|
var config struct {
|
|
Resolved []struct {
|
|
Key, Module, Node, Source string
|
|
Value any
|
|
} `json:"resolved"`
|
|
}
|
|
read("config.json", &config)
|
|
for _, r := range config.Resolved {
|
|
raw, _ := json.Marshal(map[string]any{"value": r.Value, "source": r.Source})
|
|
put(ResolvedRead, r.Node+"."+r.Module+"."+r.Key, raw)
|
|
}
|
|
manifests, _ := filepath.Glob(filepath.Join(dir, "manifests", "*"))
|
|
for _, f := range manifests {
|
|
raw, err := os.ReadFile(f)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
digest := filepath.Base(f)
|
|
v, err := delivery.EncodeManifest(delivery.Manifest{Module: "captured", JSON: string(raw)})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
key, err := delivery.ManifestsKey(digest)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
put(ManifestsRead, key, v)
|
|
}
|
|
builds, _ := filepath.Glob(filepath.Join(dir, "builds", "*.json"))
|
|
for _, f := range builds {
|
|
var bb struct {
|
|
Module string `json:"module"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
Build json.RawMessage `json:"build"`
|
|
}
|
|
read(strings.TrimPrefix(f, dir+"/"), &bb)
|
|
key, err := delivery.BuilderBuildKey(bb.Module, bb.Fingerprint)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
put(BuildsRead, key, bb.Build)
|
|
}
|
|
archives := map[string][]byte{}
|
|
files, _ := filepath.Glob(filepath.Join(dir, "archives", "*"))
|
|
for _, f := range files {
|
|
raw, err := os.ReadFile(f)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
archives[filepath.Base(f)] = raw
|
|
}
|
|
return states, archives
|
|
}
|
|
|
|
func BenchmarkComposeNode(b *testing.B) {
|
|
dir := os.Getenv("MESH_COMPOSE_CAPTURE")
|
|
if dir == "" {
|
|
b.Skip("MESH_COMPOSE_CAPTURE names no captured mesh")
|
|
}
|
|
node := os.Getenv("MESH_COMPOSE_NODE")
|
|
if node == "" {
|
|
node = "novox"
|
|
}
|
|
states, archives := captured(b, dir)
|
|
ws := stdio.WatchedStatesOver(func(state string, handle func(stdio.StateChange) error) error {
|
|
for k, v := range states[state] {
|
|
if err := handle(stdio.StateChange{State: state, Key: k, Op: stdio.OpPut, Value: v, Current: true}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}, func(state, key string) ([]byte, bool, error) {
|
|
v, ok := states[state][key]
|
|
return v, ok, nil
|
|
})
|
|
fetch := fromruntime.KeptFetch(func(_ context.Context, url string) ([]byte, error) {
|
|
_, hex, _ := strings.Cut(url, "sha256:")
|
|
raw, ok := archives[hex]
|
|
if !ok {
|
|
return nil, os.ErrNotExist
|
|
}
|
|
return raw, nil
|
|
})
|
|
own := &countingStore{Store: newStore()}
|
|
h := &Holder{Store: Watching(own, ws), Compose: composerOver(frombus.StatesReader(fromruntime.Watched{WatchedStates: ws}, fetch)),
|
|
Now: time.Now, Logf: func(string, ...any) {}}
|
|
// Composed twice before it is timed: a provision's state follows the states of those it depends on, which the first
|
|
// composition writes, so the second may still change a key; from then on nothing changes.
|
|
first, err := h.ComposeNode(context.Background(), node)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
if second, err := h.ComposeNode(context.Background(), node); err != nil {
|
|
b.Fatal(err)
|
|
} else if len(second.Changed) > 0 {
|
|
b.Logf("the second composition of %s changed %v", node, second.Changed)
|
|
}
|
|
b.Logf("first composition of %s: %d plans written, %d held, %d problems, %d keys changed", node, len(first.Plans),
|
|
len(first.Held), len(first.Problems), len(first.Changed))
|
|
for _, p := range first.Problems {
|
|
b.Logf("problem: %s", p)
|
|
}
|
|
own.gets.Store(0)
|
|
own.keys.Store(0)
|
|
b.ResetTimer()
|
|
var c Composed
|
|
for b.Loop() {
|
|
c, err = h.ComposeNode(context.Background(), node)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
if len(c.Plans)+len(c.Gone)+len(c.Changed) > 0 {
|
|
b.Fatalf("an unchanged node composed changes: %+v", c)
|
|
}
|
|
}
|
|
b.Logf("the last composition of %s: %s", node, TookSaid(c.Took))
|
|
b.ReportMetric(float64(own.gets.Load())/float64(b.N), "own-gets/op")
|
|
b.ReportMetric(float64(own.keys.Load())/float64(b.N), "own-keys/op")
|
|
}
|