Phase 19: measure graph scaling and guard capacity and contention behavior
This commit is contained in:
parent
1369e4d463
commit
6b85f28add
9 changed files with 639 additions and 3 deletions
22
Makefile
22
Makefile
|
|
@ -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)"
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
349
docs/performance-baseline.json
Normal file
349
docs/performance-baseline.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
60
tests/integration/benchmark_graph.c
Normal file
60
tests/integration/benchmark_graph.c
Normal 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;
|
||||||
|
}
|
||||||
42
tests/integration/test_scale.c
Normal file
42
tests/integration/test_scale.c
Normal 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
60
tools/benchmark.py
Normal 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
33
tools/stress.py
Normal 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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue