Phase 19: measure graph scaling and guard capacity and contention behavior

This commit is contained in:
Johannes Findeisen 2026-10-27 06:35:15 +01:00
commit 6b85f28add
9 changed files with 639 additions and 3 deletions

View file

@ -344,3 +344,25 @@ test-portability: $(BUILD_DIR)/test_portability
./$(BUILD_DIR)/test_portability ./$(BUILD_DIR)/test_portability
test: test-portability test: test-portability
-include $(BUILD_DIR)/tests/integration/test_portability.d -include $(BUILD_DIR)/tests/integration/test_portability.d
$(BUILD_DIR)/benchmark_graph: $(BUILD_DIR)/tests/integration/benchmark_graph.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
.PHONY: benchmark
benchmark: $(BUILD_DIR)/benchmark_graph
$(PYTHON) tools/benchmark.py --binary "$(BUILD_DIR)/benchmark_graph" --output "$(BUILD_DIR)/performance.json" --compiler "$(CC)" --flags "$(CFLAGS)"
-include $(BUILD_DIR)/tests/integration/benchmark_graph.d
$(BUILD_DIR)/test_scale: $(BUILD_DIR)/tests/integration/test_scale.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
.PHONY: test-scale test-stress
test-scale: $(BUILD_DIR)/test_scale
./$(BUILD_DIR)/test_scale
test-stress: $(BUILD_DIR)/test_realtime $(BUILD_DIR)/test_reliability $(BUILD_DIR)/test_scale $(if $(GUI_AVAILABLE),$(BUILD_DIR)/test_gui)
$(PYTHON) tools/stress.py --build-dir "$(BUILD_DIR)" $(if $(GUI_AVAILABLE),--gui)
-include $(BUILD_DIR)/tests/integration/test_scale.d
BENCHMARK_BASELINE ?= docs/performance-baseline.json
BENCHMARK_MAX_REGRESSION ?= 1.5
.PHONY: benchmark-compare
benchmark-compare: $(BUILD_DIR)/benchmark_graph
$(PYTHON) tools/benchmark.py --binary "$(BUILD_DIR)/benchmark_graph" --output "$(BUILD_DIR)/performance.json" --compiler "$(CC)" --flags "$(CFLAGS)" --baseline "$(BENCHMARK_BASELINE)" --max-regression "$(BENCHMARK_MAX_REGRESSION)"

View file

@ -33,3 +33,59 @@ the API cannot enforce an extension's memory budget. There is no CPU overload
scheduler, disk streaming, latency compensation or callback allocator audit of scheduler, disk streaming, latency compensation or callback allocator audit of
arbitrary third-party code. High-density interactive projects, physical devices, arbitrary third-party code. High-density interactive projects, physical devices,
long-duration stereo/recording and live server termination remain unqualified. long-duration stereo/recording and live server termination remain unqualified.
## Phase 19 matrix, capacity and contention checks
`make benchmark` records 24 warmed graph cases: 2/16/64/256 nodes, mono/stereo,
44.1k/31, 48k/128 and 96k/1024. Each reports control-thread compile time separately
from 300 process calls, mean/p99/max, block deadline, mean load, and paired graph
buffer bytes. Timing, sorting, allocation and logging happen outside processing.
Metered gain chains test graph scaling; they do not represent expensive plugins.
The checked-in `performance-baseline.json` records CPU/compiler/flags/kernel and
all measurements. Copy a new run only after reviewing changes and conditions.
```sh
make benchmark test-performance test-scale test-stress
make benchmark-compare BENCHMARK_MAX_REGRESSION=1.5
# Select a baseline from this same host/compiler/flags:
make benchmark-compare BENCHMARK_BASELINE=/path/to/performance.json
```
Comparison rejects mismatched CPU/architecture/compiler/flags or missing cases.
It fails if mean time exceeds both the ratio budget and a 1 microsecond absolute
noise allowance. p99/max are reported for investigation, not universally gated.
Repeat surprising results under comparable load; do not silently replace the
baseline to make a regression disappear. Scheduling, clocks, thermal state and
shared-machine contention still affect results. No real-time scheduling/pinning
or external profiler is required. CPU-pressure runs are correctness checks, not
comparable timing baselines. Tests/benchmark outputs live below BUILD_DIR.
Initial matrix (Threadripper 1920X, GCC 16.2.1, -O2, 300 blocks): 256-node stereo
48k/128 mean 353.214 us, p99 396.495 us, load .132; 96k/1024 mean 2478.520 us,
p99 3092.434 us, load .232. The full JSON is the evidence. These met the local
investigation target of mean load <.5 for ordinary graph chains; this is a
host-specific development target, not a supported maximum CPU or latency promise.
The mixed 64-node/4096-note workload measured 340–357 us before removing unused
live-controller processing and 313 us after (mean over 2000 blocks); p99 varied
with system load. The measured hot-path change skips live voices/controller ramps
when neither is active and avoids finished-ramp calls. Exact CC/odd-block tests
continue to pass. No other graph optimization was justified by this measurement.
`test-scale` constructs all 64 tracks, 1024 clips, 1024 automation points and
4096 notes, compiles a standalone 256-node metered graph, renders stereo odd-sized
blocks and rejects capacity excesses. Project/GUI graphs remain capped at 64
nodes. `test-stress` runs publication, allocation/retirement, capacity and optional
GUI/timer/redraw tests while two ordinary CPU workers compete; subprocesses have
timeouts and workers are always terminated. Existing callback guards verify no
host allocation/free/loading/unloading. This does not qualify arbitrary plugins.
Callback work is bounded by documented node/frame/clip/note/event/point capacities.
Exceeding model/plan/mailbox capacities returns CAPACITY transactionally; invalid
or overlarge processing buffers are silenced and reported, JACK xruns are counted.
There is no CPU scheduler or preemption inside a DSP callback: an expensive trusted
plugin can miss a deadline or hang. Automated LADSPA lanes currently use run(1)
and bounded per-sample lane searches and may be costly; they are excluded from
the gain-chain baseline. Large WAV memory limits remain those in AUDIO_FILES.md.
Physical-device endurance, CPU-overload sound quality and a full GUI latency
benchmark remain unqualified.

View file

@ -416,6 +416,14 @@ path bounds, post-rename sync errors and physical power-loss limits are explicit
- Overload behavior is defined and does not cause unbounded work in the callback. - Overload behavior is defined and does not cause unbounded work in the callback.
- Significant performance regressions can be detected through repeatable tests. - Significant performance regressions can be detected through repeatable tests.
Implemented: 24-case node/channel/rate/block matrix with separate compilation
and warmed process timings, recorded machine/compiler baseline and opt-in
regression budgets; maximum track/clip/note/automation/meter capacity checks and
publication/GUI tests under CPU contention. Measurements justified skipping
unused live-controller processing; exact output tests remain passing. See
PERFORMANCE.md and performance-baseline.json for results, thresholds and overload
policy. Arbitrary plugin costs and physical realtime/endurance are unqualified.
## Phase 20 — Release Engineering, Packaging, and Maintenance ## Phase 20 — Release Engineering, Packaging, and Maintenance
**Goal:** Make the application straightforward to build, install, use, diagnose, and maintain. **Goal:** Make the application straightforward to build, install, use, diagnose, and maintain.

View file

@ -0,0 +1,349 @@
{
"schema": 1,
"context": {
"cpu": "AMD Ryzen Threadripper 1920X 12-Core Processor",
"architecture": "x86_64",
"compiler": "cc (GCC) 16.2.1 20260810",
"flags": "-O2 -g -std=c99 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Iinclude",
"kernel": "7.2.11-artix1-1",
"scheduler": "ordinary process; no realtime scheduling"
},
"cases": [
{
"nodes": 2,
"channels": 1,
"rate": 44100,
"frames": 31,
"blocks": 300,
"compile_ms": 0.003566,
"mean_us": 0.348454,
"p99_us": 0.351007,
"max_us": 0.390995,
"deadline_us": 702.947846,
"load": 0.000496,
"graph_buffer_bytes": 496
},
{
"nodes": 2,
"channels": 1,
"rate": 48000,
"frames": 128,
"blocks": 300,
"compile_ms": 0.010079,
"mean_us": 2.3219,
"p99_us": 3.858004,
"max_us": 4.318004,
"deadline_us": 2666.666667,
"load": 0.000871,
"graph_buffer_bytes": 2048
},
{
"nodes": 2,
"channels": 1,
"rate": 96000,
"frames": 1024,
"blocks": 300,
"compile_ms": 0.010961,
"mean_us": 12.680954,
"p99_us": 18.045001,
"max_us": 21.059997,
"deadline_us": 10666.666667,
"load": 0.001189,
"graph_buffer_bytes": 16384
},
{
"nodes": 2,
"channels": 2,
"rate": 44100,
"frames": 31,
"blocks": 300,
"compile_ms": 0.006352,
"mean_us": 1.5425,
"p99_us": 1.793,
"max_us": 1.813998,
"deadline_us": 702.947846,
"load": 0.002194,
"graph_buffer_bytes": 496
},
{
"nodes": 2,
"channels": 2,
"rate": 48000,
"frames": 128,
"blocks": 300,
"compile_ms": 0.007594,
"mean_us": 2.917503,
"p99_us": 5.650989,
"max_us": 6.111994,
"deadline_us": 2666.666667,
"load": 0.001094,
"graph_buffer_bytes": 2048
},
{
"nodes": 2,
"channels": 2,
"rate": 96000,
"frames": 1024,
"blocks": 300,
"compile_ms": 0.01585,
"mean_us": 22.3441,
"p99_us": 36.149009,
"max_us": 44.774992,
"deadline_us": 10666.666667,
"load": 0.002095,
"graph_buffer_bytes": 16384
},
{
"nodes": 16,
"channels": 1,
"rate": 44100,
"frames": 31,
"blocks": 300,
"compile_ms": 0.017393,
"mean_us": 3.149973,
"p99_us": 3.296009,
"max_us": 11.472002,
"deadline_us": 702.947846,
"load": 0.004481,
"graph_buffer_bytes": 3968
},
{
"nodes": 16,
"channels": 1,
"rate": 48000,
"frames": 128,
"blocks": 300,
"compile_ms": 0.030107,
"mean_us": 12.434426,
"p99_us": 16.982012,
"max_us": 18.214996,
"deadline_us": 2666.666667,
"load": 0.004663,
"graph_buffer_bytes": 16384
},
{
"nodes": 16,
"channels": 1,
"rate": 96000,
"frames": 1024,
"blocks": 300,
"compile_ms": 0.017924,
"mean_us": 68.742836,
"p99_us": 95.873009,
"max_us": 109.757995,
"deadline_us": 10666.666667,
"load": 0.006445,
"graph_buffer_bytes": 131072
},
{
"nodes": 16,
"channels": 2,
"rate": 44100,
"frames": 31,
"blocks": 300,
"compile_ms": 0.01581,
"mean_us": 6.329124,
"p99_us": 6.501999,
"max_us": 12.534001,
"deadline_us": 702.947846,
"load": 0.009004,
"graph_buffer_bytes": 3968
},
{
"nodes": 16,
"channels": 2,
"rate": 48000,
"frames": 128,
"blocks": 300,
"compile_ms": 0.042171,
"mean_us": 37.250633,
"p99_us": 51.957992,
"max_us": 54.725009,
"deadline_us": 2666.666667,
"load": 0.013969,
"graph_buffer_bytes": 16384
},
{
"nodes": 16,
"channels": 2,
"rate": 96000,
"frames": 1024,
"blocks": 300,
"compile_ms": 0.017924,
"mean_us": 167.51364,
"p99_us": 332.022013,
"max_us": 387.477005,
"deadline_us": 10666.666667,
"load": 0.015704,
"graph_buffer_bytes": 131072
},
{
"nodes": 64,
"channels": 1,
"rate": 44100,
"frames": 31,
"blocks": 300,
"compile_ms": 0.081966,
"mean_us": 10.984623,
"p99_us": 19.296,
"max_us": 20.708991,
"deadline_us": 702.947846,
"load": 0.015627,
"graph_buffer_bytes": 15872
},
{
"nodes": 64,
"channels": 1,
"rate": 48000,
"frames": 128,
"blocks": 300,
"compile_ms": 0.147651,
"mean_us": 64.12053,
"p99_us": 92.806993,
"max_us": 93.618,
"deadline_us": 2666.666667,
"load": 0.024045,
"graph_buffer_bytes": 65536
},
{
"nodes": 64,
"channels": 1,
"rate": 96000,
"frames": 1024,
"blocks": 300,
"compile_ms": 0.06297,
"mean_us": 302.05437,
"p99_us": 394.601011,
"max_us": 396.89499,
"deadline_us": 10666.666667,
"load": 0.028318,
"graph_buffer_bytes": 524288
},
{
"nodes": 64,
"channels": 2,
"rate": 44100,
"frames": 31,
"blocks": 300,
"compile_ms": 0.123665,
"mean_us": 38.359343,
"p99_us": 52.951,
"max_us": 53.622003,
"deadline_us": 702.947846,
"load": 0.054569,
"graph_buffer_bytes": 15872
},
{
"nodes": 64,
"channels": 2,
"rate": 48000,
"frames": 128,
"blocks": 300,
"compile_ms": 0.133965,
"mean_us": 91.372266,
"p99_us": 175.463996,
"max_us": 177.548005,
"deadline_us": 2666.666667,
"load": 0.034265,
"graph_buffer_bytes": 65536
},
{
"nodes": 64,
"channels": 2,
"rate": 96000,
"frames": 1024,
"blocks": 300,
"compile_ms": 0.063621,
"mean_us": 620.865637,
"p99_us": 1065.186996,
"max_us": 1085.204,
"deadline_us": 10666.666667,
"load": 0.058206,
"graph_buffer_bytes": 524288
},
{
"nodes": 256,
"channels": 1,
"rate": 44100,
"frames": 31,
"blocks": 300,
"compile_ms": 0.594482,
"mean_us": 61.93558,
"p99_us": 91.194001,
"max_us": 98.316988,
"deadline_us": 702.947846,
"load": 0.088108,
"graph_buffer_bytes": 63488
},
{
"nodes": 256,
"channels": 1,
"rate": 48000,
"frames": 128,
"blocks": 300,
"compile_ms": 0.289451,
"mean_us": 142.010174,
"p99_us": 196.253997,
"max_us": 197.596004,
"deadline_us": 2666.666667,
"load": 0.053254,
"graph_buffer_bytes": 262144
},
{
"nodes": 256,
"channels": 1,
"rate": 96000,
"frames": 1024,
"blocks": 300,
"compile_ms": 0.311192,
"mean_us": 1379.590334,
"p99_us": 1909.183004,
"max_us": 2125.945,
"deadline_us": 10666.666667,
"load": 0.129337,
"graph_buffer_bytes": 2097152
},
{
"nodes": 256,
"channels": 2,
"rate": 44100,
"frames": 31,
"blocks": 300,
"compile_ms": 0.304259,
"mean_us": 81.809496,
"p99_us": 114.608003,
"max_us": 127.602994,
"deadline_us": 702.947846,
"load": 0.116381,
"graph_buffer_bytes": 63488
},
{
"nodes": 256,
"channels": 2,
"rate": 48000,
"frames": 128,
"blocks": 300,
"compile_ms": 0.541971,
"mean_us": 353.213863,
"p99_us": 396.495001,
"max_us": 401.252997,
"deadline_us": 2666.666667,
"load": 0.132455,
"graph_buffer_bytes": 262144
},
{
"nodes": 256,
"channels": 2,
"rate": 96000,
"frames": 1024,
"blocks": 300,
"compile_ms": 0.518307,
"mean_us": 2478.520283,
"p99_us": 3092.433995,
"max_us": 4376.808007,
"deadline_us": 10666.666667,
"load": 0.232361,
"graph_buffer_bytes": 2097152
}
]
}

View file

@ -339,12 +339,18 @@ static void segment(xtc_midi_plan *p, uint64_t position, float *out, float *righ
right[j] += sample * xtc_mix_level(&p->mix, s->track, position + j, 1); right[j] += sample * xtc_mix_level(&p->mix, s->track, position + j, 1);
} }
} }
{
int live = 0; unsigned int c;
for (i = 0; i < XTC_MIDI_VOICES; ++i) if (p->voices[i].active) live = 1;
for (c = 0; c < 16; ++c) if (p->volume[c].remaining || p->pan[c].remaining) live = 1;
if (!live) return; /* Scheduled-only projects need no live-controller work. */
}
for (j = 0; j < frames; ++j) { for (j = 0; j < frames; ++j) {
float levels[16], pans[16]; unsigned int c; float levels[16], pans[16]; unsigned int c;
for (c = 0; c < 16; ++c) { for (c = 0; c < 16; ++c) {
levels[c] = pans[c] = 1; levels[c] = p->volume[c].current; pans[c] = p->pan[c].current;
(void)xtc_gain_ramp_process(p->volume+c,levels+c,NULL,1); if (p->volume[c].remaining) { levels[c] = 1; (void)xtc_gain_ramp_process(p->volume+c,levels+c,NULL,1); }
(void)xtc_gain_ramp_process(p->pan+c,pans+c,NULL,1); if (p->pan[c].remaining) { pans[c] = 1; (void)xtc_gain_ramp_process(p->pan+c,pans+c,NULL,1); }
} }
for (i = 0; i < XTC_MIDI_VOICES; ++i) { for (i = 0; i < XTC_MIDI_VOICES; ++i) {
voice *v = p->voices+i; voice *v = p->voices+i;

View file

@ -0,0 +1,60 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_engine.h"
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
static double now(void)
{ struct timespec t; if (clock_gettime(CLOCK_MONOTONIC,&t)) abort(); return (double)t.tv_sec+(double)t.tv_nsec/1e9; }
static int compare(const void *a,const void *b)
{ double x = *(const double *)a,y = *(const double *)b; return x < y ? -1 : x > y; }
static size_t number(const char *s,size_t maximum)
{
char *end; unsigned long n;
errno = 0; n = strtoul(s,&end,10);
if (errno || !s[0] || *end || n < 1 || n > maximum) return 0;
return (size_t)n;
}
int main(int argc,char **argv)
{
size_t nodes,channels,rate,frames,blocks,i,j,id = 0,previous = 0;
xtc_graph *g = NULL; xtc_plan *p = NULL;
float *in = NULL,*left = NULL,*right = NULL;
double *times = NULL,begin,compile,total = 0;
int status = 1;
if (argc != 6) return 1;
nodes = number(argv[1],256); channels = number(argv[2],2); rate = number(argv[3],768000);
frames = number(argv[4],65536); blocks = number(argv[5],100000);
if (nodes < 2 || !channels || !rate || !frames || !blocks) return 1;
in = calloc(frames,sizeof(float)); left = calloc(frames,sizeof(float)); right = calloc(frames,sizeof(float));
times = calloc(blocks,sizeof(double));
if (!in || !left || !right || !times || xtc_graph_create(nodes,&g) != XTC_OK) goto done;
for (i = 0; i < nodes; ++i) {
xtc_result r = i+1 == nodes ? xtc_graph_add_output(g,&id) :
i+2 == nodes ? xtc_graph_add_meter(g,&id) : xtc_graph_add_gain(g,.999f,&id);
if (r != XTC_OK || (i && xtc_graph_connect(g,previous,id) != XTC_OK)) goto done;
previous = id;
}
begin = now();
if (xtc_graph_compile(g,(double)rate,frames,&p) != XTC_OK) goto done;
compile = now()-begin;
for (i = 0; i < frames; ++i) in[i] = (float)sin((double)i*.1)*.1f;
/* Warm caches outside the sample, then measure only the process call. */
for (i = 0; i < 20; ++i) if (xtc_plan_process_stereo(p,in,NULL,NULL,NULL,NULL,left,channels == 2 ? right : NULL,frames) != XTC_OK) goto done;
for (i = 0; i < blocks; ++i) {
begin = now();
if (xtc_plan_process_stereo(p,in,NULL,NULL,NULL,NULL,left,channels == 2 ? right : NULL,frames) != XTC_OK) goto done;
times[i] = now()-begin; total += times[i];
for (j = 0; j < frames; ++j) if (!isfinite(left[j]) || (channels == 2 && !isfinite(right[j]))) goto done;
}
qsort(times,blocks,sizeof(double),compare);
printf("{\"nodes\":%zu,\"channels\":%zu,\"rate\":%zu,\"frames\":%zu,\"blocks\":%zu,"
"\"compile_ms\":%.6f,\"mean_us\":%.6f,\"p99_us\":%.6f,\"max_us\":%.6f,\"deadline_us\":%.6f,"
"\"load\":%.6f,\"graph_buffer_bytes\":%zu}\n",nodes,channels,rate,frames,blocks,compile*1e3,total/(double)blocks*1e6,
times[blocks*99/100]*1e6,times[blocks-1]*1e6,(double)frames/(double)rate*1e6,
total/(double)blocks/((double)frames/(double)rate),nodes*frames*2*sizeof(float));
status = 0;
done:
free(in); free(left); free(right); free(times); xtc_plan_destroy(p); xtc_graph_destroy(g); return status;
}

View file

@ -0,0 +1,42 @@
#include "xtc/xtc_midi.h"
#include "xtc/xtc_audio.h"
#include "xtc/xtc_mixer.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
int main(void)
{
xtc_project *p = NULL; xtc_midi_plan *midi = NULL; xtc_audio_plan *audio = NULL;
xtc_graph *graph = NULL; xtc_plan *plan = NULL;
uint64_t tracks[XTC_PROJECT_MAX_TRACKS],clips[XTC_PROJECT_MAX_CLIPS],id;
size_t i,j,previous = 0,node; float left[31],right[31],out[31],other[31];
assert(xtc_project_create(&p) == XTC_OK);
for (i = 0; i < XTC_PROJECT_MAX_TRACKS; ++i) assert(xtc_project_add_track(p,"Scale",tracks+i) == XTC_OK);
assert(xtc_project_add_track(p,"Excess",&id) == XTC_ERR_CAPACITY);
for (i = 0; i < XTC_PROJECT_MAX_CLIPS; ++i) assert(xtc_project_add_midi_clip(p,tracks[i%64],i*4,4,clips+i) == XTC_OK);
assert(xtc_project_add_midi_clip(p,tracks[0],0,4,&id) == XTC_ERR_CAPACITY);
for (i = 0; i < XTC_PROJECT_MAX_NOTES; ++i) assert(xtc_project_add_note(p,clips[i%1024],i/1024,1,60,100,0,&id) == XTC_OK);
assert(xtc_project_add_note(p,clips[0],0,1,60,100,0,&id) == XTC_ERR_CAPACITY);
for (i = 0; i < XTC_MAX_AUTOMATION_POINTS; ++i) {
xtc_automation_point a = {tracks[i%64],i/64,XTC_AUTOMATION_GAIN,.1f};
assert(xtc_project_set_automation(p,&a) == XTC_OK);
}
{ xtc_automation_point a = {tracks[0],1000,XTC_AUTOMATION_GAIN,1}; assert(xtc_project_set_automation(p,&a) == XTC_ERR_CAPACITY); }
assert(xtc_graph_create(256,&graph) == XTC_OK && xtc_graph_add_instrument(graph,&previous) == XTC_OK);
for (i = 1; i < 256; ++i) {
assert((i == 255 ? xtc_graph_add_output(graph,&node) : xtc_graph_add_meter(graph,&node)) == XTC_OK);
assert(xtc_graph_connect(graph,previous,node) == XTC_OK); previous = node;
}
assert(xtc_graph_add_gain(graph,1,&node) == XTC_ERR_CAPACITY);
assert(xtc_graph_compile(graph,96000,31,&plan) == XTC_OK);
assert(xtc_midi_plan_create(p,96000,&midi) == XTC_OK && xtc_audio_plan_create(p,96000,&audio) == XTC_OK);
for (i = 0; i < 100; ++i) {
assert(xtc_midi_plan_render_stereo(midi,i*31,NULL,0,left,right,31) == XTC_OK);
assert(xtc_plan_process_stereo(plan,NULL,NULL,NULL,left,right,out,other,31) == XTC_OK);
for (j = 0; j < 31; ++j) assert(isfinite(out[j]) && out[j] == other[j]);
}
xtc_audio_plan_destroy(audio); xtc_midi_plan_destroy(midi); xtc_plan_destroy(plan);
xtc_graph_destroy(graph); xtc_project_destroy(p);
puts("64 tracks, 1024 clips/points, 4096 notes, 256 metered nodes and capacity failures passed.");
return 0;
}

60
tools/benchmark.py Normal file
View file

@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Record graph timing matrix; optionally compare a matching host baseline."""
import argparse
import json
from pathlib import Path
import platform
import shlex
import subprocess
import sys
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--binary', type=Path, required=True)
p.add_argument('--output', type=Path, required=True)
p.add_argument('--baseline', type=Path)
p.add_argument('--max-regression', type=float, default=1.5)
p.add_argument('--noise-us', type=float, default=1.0)
p.add_argument('--blocks', type=int, default=300)
p.add_argument('--compiler', default='cc')
p.add_argument('--flags', default='-O2 -g')
a = p.parse_args()
if not 10 <= a.blocks <= 100000 or not 1 <= a.max_regression <= 100 or not 0 <= a.noise_us <= 100:
p.error('blocks must be 10..100000 and regression factor 1..100')
cpu = next((line.split(':', 1)[1].strip() for line in Path('/proc/cpuinfo').read_text().splitlines()
if line.startswith('model name')), platform.machine())
compiler = subprocess.check_output(shlex.split(a.compiler) + ['--version'], text=True).splitlines()[0]
context = dict(cpu=cpu, architecture=platform.machine(), compiler=compiler, flags=a.flags,
kernel=platform.release(), scheduler='ordinary process; no realtime scheduling')
cases = []
for nodes in (2, 16, 64, 256):
for channels in (1, 2):
for rate, frames in ((44100, 31), (48000, 128), (96000, 1024)):
result = subprocess.check_output([str(a.binary.resolve()), *map(str, (nodes, channels, rate, frames, a.blocks))], text=True)
case = json.loads(result)
cases.append(case)
print(f'{nodes:3} nodes {channels}ch {rate}/{frames}: compile {case["compile_ms"]:.3f} ms, '
f'mean {case["mean_us"]:.3f} us, p99 {case["p99_us"]:.3f} us, load {case["load"]:.3f}')
report = dict(schema=1, context=context, cases=cases)
a.output.parent.mkdir(parents=True, exist_ok=True)
a.output.write_text(json.dumps(report, indent=2) + '\n')
if a.baseline:
baseline = json.loads(a.baseline.read_text())
if baseline.get('schema') != 1 or any(baseline['context'][k] != context[k] for k in ('cpu', 'architecture', 'compiler', 'flags')):
sys.exit('Baseline host/compiler/flags mismatch; record a baseline for this configuration.')
key = lambda c: (c['nodes'], c['channels'], c['rate'], c['frames'])
old = {key(c): c for c in baseline['cases']}
failed = []
for c in cases:
if key(c) not in old:
failed.append(f'missing baseline case {key(c)}')
elif c['mean_us'] > max(old[key(c)]['mean_us'] * a.max_regression, old[key(c)]['mean_us'] + a.noise_us):
failed.append(f'{key(c)}: mean {c["mean_us"]:.3f} us exceeds {a.max_regression}x baseline')
if failed:
sys.exit('\n'.join(failed))
print('Mean processing times satisfy the requested regression budget.')
if __name__ == '__main__':
main()

33
tools/stress.py Normal file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Run existing publication/GUI tests while two ordinary CPU workers contend."""
import argparse
from pathlib import Path
import subprocess
import sys
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--build-dir', type=Path, required=True)
p.add_argument('--gui', action='store_true')
a = p.parse_args()
workers = []
try:
for _ in range(2):
workers.append(subprocess.Popen([sys.executable, '-c', 'x=1\nwhile True: x=(x*1664525+1013904223)&0xffffffff']))
for name in ('test_realtime', 'test_reliability', 'test_scale'):
subprocess.run([str((a.build_dir / name).resolve())], check=True, timeout=120)
if a.gui:
# The GUI suite pumps timers and forces redraw during engine/edit tests.
subprocess.run(['xvfb-run', '-a', '-s', '-screen 0 1280x800x24',
str((a.build_dir / 'test_gui').resolve())], check=True, timeout=120)
finally:
for w in workers:
w.terminate()
for w in workers:
w.wait(timeout=10)
print('Publication, capacity and optional GUI/timer tests passed under two CPU workers.')
if __name__ == '__main__':
main()