xtc/docs/RELIABILITY.md

151 lines
9.5 KiB
Markdown
Raw Normal View History

2026-11-06 15:20:20 +01:00
# Engine Correctness and Reliability
2026-11-06 14:12:22 +01:00
The existing engine was audited rather than extended with musical features. The
review covers graph edits/compilation, offline DSP, plan publication, JACK
callbacks/lifecycle, audio/MIDI source plans and the recorder handoff.
2026-11-06 15:20:20 +01:00
---
## Ownership and Synchronization
| Resource | Owner and lifetime |
| --- | --- |
| Editable graph, project, history and asset paths | One serialized control thread (the GUI thread in the application). Borrowed model pointers expire on mutation/destruction. Never traverse these objects in the callback. |
| Compiled graph routing/specifications | Copied during compilation; independent of the editable graph. Immutable after publication. All nodes, including disconnected nodes, are scheduled; only explicit outputs (or compatibility terminals) are mixed. |
| DSP buffers, native instances, phases, meter peaks and graph-local clock | One processing consumer. Reset and meter reads require that owner or quiescence; these are not atomic GUI snapshots. Native callback code must remain loaded until all instances are destroyed. |
| Pending graph | Single control producer publishes with release; the consumer acquires it at a block boundary. The revision is read before releasing the pending slot, preventing producer reuse of its metadata. A full pending slot rejects submission. |
| Active graph | Consumer only until the consumer is stopped/joined. No producer access to mutable DSP state. |
| Retired graph | Consumer releases only after its final access. Control acquires/exchanges the slot and destroys the plan. If the slot is occupied, adoption waits while processing the current graph continues. The callback never destroys plans. |
| Realtime status | Individual atomic counters/revisions; a status read is not a transaction spanning every field. Required pointer/integer atomics must be always lock-free. |
| JACK client, ports and backend control fields | Serialized control thread. JACK owns callback scheduling. Successful deactivate establishes processing quiescence; successful close synchronizes callbacks before freeing context. Failed deactivate/close preserves ownership for retry. Server-loss notification alone is not permission to free context. |
| Preloaded audio/MIDI plans and source buffers | Backend owns them; prepare/replace while stopped, callback alone renders while running. MIDI voices/panic state follow the same rule; panic requests use atomics. |
| Recorder | Caller owns it; backend borrows it. Attach/detach only while stopped. Audio is sole ring producer, control is sole drain/file writer. Acquire/release indices protect samples; errors are sticky. Detach/quiesce before finish/destroy. |
| Widgets and GUI timers | GUI thread only; status polling and retired-plan collection occur there, never through FLTK calls from audio. |
The public realtime API is SPSC: multiple producers, simultaneous process calls,
collection from arbitrary threads, and destruction during processing are not
supported. Atomics do not remove those ownership requirements.
2026-11-06 15:20:20 +01:00
---
## Bounded Callback Work
Compilation, topological sorting, asset I/O, allocation and native preparation
are control-thread operations. Graph processing traverses at most 256 nodes,
65536 frames per accepted block and the precomputed edges (at most 256 squared
storage entries). Graph edits validate port direction/type/layout and reject
cycles transactionally using Kahn ordering; fixed public mono ports have no
implicit event/audio conversions. Stereo uses two mono graph passes, independent
native instances and one oscillator phase advance per frame.
Audio regions, MIDI spans, voices, events and automation are bounded by public
project/MIDI capacities. Rendering scans bounded region/note arrays; automation
uses binary searches in prepared lanes. JACK MIDI accepts at most 256 events per
block; overflow requests panic and reports drops. Recorder push performs at most
one ring-capacity-sized copy and never drains a file. No callback path waits for
a producer, collector, GUI or disk. Realtime pointer handoff has no retry loop.
Oversized JACK blocks are silenced without graph/source processing. Clearing
backend-provided output and computing peaks still costs the server's frame
count; the server's buffer contract is trusted. Public render APIs likewise
require caller-provided valid buffers. Lock-free does not promise a hardware
latency bound for atomic instructions, libc memory functions or libm. Native
callbacks are trusted: the host cannot prevent their allocation, blocking,
crashes or excessive runtime. Maximum supported capacities are safety bounds,
not a promise that every workload meets its audio deadline.
2026-11-06 15:20:20 +01:00
---
## Findings and Regression Coverage
Graph rendering previously mixed the left output before reading right-channel
2026-11-06 15:20:20 +01:00
sources. With external input equal to the left output, the right pass could read
already processed samples (gain applied twice). Both passes now finish source
reads before either output is written. Tests check mono input aliasing and
cross-channel source aliasing against exact expected samples. Output buffers
must not overlap each other. A redundant assignment in missing-asset preparation
was also removed after Clang's dead-store diagnostic; behavior is unchanged.
`test_reliability` uses GNU-compatible linker allocation interposition in the
test executable only. It rejects malloc/calloc/realloc/free during rendering,
fails each preparation allocation in turn until compilation succeeds, verifies
that failed replacement preserves output and revisions, and counts both stereo
native instances through retirement, collection and destruction of a pending
plan. It repeats this lifecycle 100 times. A maximum 256-node reverse-insertion
chain checks dependency scheduling, long-cycle rejection/rollback and compiled
2026-11-06 15:20:20 +01:00
snapshot independence. The pthread handoff test submits 2000 native graphs while
processing, checks whole-block output/revision consistency, asserts that
prepare/destruction run on the control thread, and balances all native lifetimes
after joining the consumer.
Existing backend fault tests cover repeated start/stop, partial registration,
2026-11-06 15:20:20 +01:00
callback setup, open/activate/deactivate/close failures and retries, server
loss, rate/buffer changes, missing buffers, MIDI disconnect/panic, and recorder
detach. Other suites cover deterministic variable-block DSP, finite-output
handling, project errors, record-ring saturation and concurrent draining, and
crash/write failure preservation. No critical realtime safety defect remains
known within these contracts; these tests are evidence for tested paths, not a
formal proof.
2026-11-06 15:20:20 +01:00
---
## Reproducing the Audit
From the repository root:
```sh
make BUILD_DIR=build/audit all gui test-reliability test-recovery
make BUILD_DIR=build/audit test-sanitize
make analyze
make BUILD_DIR=build/audit test-gui test-gui-jack test-jack test-jack-audio test-jack-midi
make BUILD_DIR=build/nojack PKG_CONFIG=false all test-reliability gui
```
2026-11-06 15:20:20 +01:00
`test-sanitize` needs installed Clang and runs core, allocation-failure and
recovery tests with ASan/UBSan and Linux's default ASan leak checking. `analyze`
uses Clang's path-sensitive analyzer on every core translation unit, including
JACK when its development files are available, and fails on diagnostics. Python
and Clang are development tools, not new runtime dependencies. The allocation
guard requires a GNU-compatible linker. Live tests require an existing
JACK-compatible server; GUI tests use installed FLTK and Xvfb. No target
installs dependencies or changes server/system configuration.
2026-11-06 15:20:20 +01:00
Verification on GCC 16.2.1 / Clang 23.1.3 passed normal and no-JACK builds, all
core tests, ASan/UBSan/leak checks, Clang static analysis, GUI/mock and live
JACK integrations, plus GCC TSan on the pthread native-publication and recorder
concurrency tests. The GUI also passed ASan/UBSan with the existing
2026-11-06 15:20:20 +01:00
`tests/integration/lsan.supp` fontconfig-only suppression; core tests use no
leak suppression. The live graph test observed both replacement gains, ten
restart cycles and zero xruns during its five-second software run.
Physical audio devices, hard realtime scheduling, multi-hour endurance and
arbitrary third-party native callbacks remain unqualified. Sanitizers change
timing and should not be used to measure production realtime performance. See
PERFORMANCE.md for workload measurements, RECOVERY.md for process-crash limits,
and NATIVE_API.md for the trusted extension boundary.
2026-11-06 15:20:20 +01:00
---
## Transition Ownership
Source fade configuration is written before publication and immutable during
rendering. The master ramp and stop-observed flag belong to the audio consumer
while active; start initializes them only while quiescent. Control publishes an
atomic stop request; audio publishes completion after the last ramp sample.
Collection acquires completion and deactivates the backend before resetting
voices/flags. A completed callback continues silence/holds position until that
deactivation. No callback waits or calls JACK control functions. Immediate
stop/close handles faults or missing callbacks. See AUDIO_QUALITY.md.
2026-11-06 15:20:20 +01:00
---
2026-11-06 15:20:20 +01:00
## Repeatable Development Checks
`make check-development` combines normal tests, capacity/endurance,
deterministic benchmark-gate tests, static analysis and ASan/UBSan (including
scale and soak). `make test-stress` repeats project/backend/publication faults
under CPU contention; `make test-stress-plugins` adds the original LADSPA
failure fixture. The reliability callback guard now also rejects common host
file I/O and blocking mutex/condition calls. It audits directly linked host
code, not arbitrary dynamic-plugin internals. See PERFORMANCE.md for exact
workloads, metrics, profiles and qualification limits.