Phase 29: add project benchmarks, endurance stress and callback safety checks
This commit is contained in:
parent
ca6c869b0b
commit
afc049e2c8
14 changed files with 1032 additions and 43 deletions
|
|
@ -5,8 +5,11 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include <sys/resource.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 double cpu_now(void)
|
||||
{ struct timespec t; if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID,&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)
|
||||
|
|
@ -21,7 +24,9 @@ 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;
|
||||
double *times = NULL,begin,compile,total = 0,cpu_begin,cpu;
|
||||
size_t misses = 0;
|
||||
struct rusage usage;
|
||||
int status = 1;
|
||||
if (argc != 6) return 1;
|
||||
nodes = number(argv[1],256); channels = number(argv[2],2); rate = number(argv[3],768000);
|
||||
|
|
@ -42,18 +47,22 @@ int main(int argc,char **argv)
|
|||
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;
|
||||
cpu_begin = cpu_now();
|
||||
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];
|
||||
if (times[i] > (double)frames/(double)rate) ++misses;
|
||||
for (j = 0; j < frames; ++j) if (!isfinite(left[j]) || (channels == 2 && !isfinite(right[j]))) goto done;
|
||||
}
|
||||
cpu = cpu_now()-cpu_begin;
|
||||
if (getrusage(RUSAGE_SELF,&usage)) 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,
|
||||
"\"load\":%.6f,\"graph_buffer_bytes\":%zu,\"deadline_misses\":%zu,\"process_cpu_ms\":%.6f,\"peak_rss_kib\":%ld}\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));
|
||||
total/(double)blocks/((double)frames/(double)rate),nodes*frames*2*sizeof(float),misses,cpu*1e3,usage.ru_maxrss);
|
||||
status = 0;
|
||||
done:
|
||||
free(in); free(left); free(right); free(times); xtc_plan_destroy(p); xtc_graph_destroy(g); return status;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include <sys/resource.h>
|
||||
#include <inttypes.h>
|
||||
#define REQUIRE(c) \
|
||||
do { \
|
||||
if (!(c)) { \
|
||||
|
|
@ -19,6 +21,12 @@ static double now(void)
|
|||
clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
return (double)t.tv_sec + (double)t.tv_nsec / 1e9;
|
||||
}
|
||||
static double cpu_now(void)
|
||||
{
|
||||
struct timespec t;
|
||||
if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &t)) abort();
|
||||
return (double)t.tv_sec + (double)t.tv_nsec / 1e9;
|
||||
}
|
||||
static int compare(const void *left, const void *right)
|
||||
{
|
||||
double a = *(const double *)left, b = *(const double *)right;
|
||||
|
|
@ -32,39 +40,52 @@ int main(int argc, char **argv)
|
|||
xtc_midi_plan *midi = NULL;
|
||||
long blocks = 2000;
|
||||
char *end;
|
||||
size_t i, j, source, previous, node;
|
||||
size_t i, j, source, previous, node, tracks = 16, frames = 128, misses = 0;
|
||||
size_t point_count, note_count;
|
||||
uint64_t position = 0, wraps = 0;
|
||||
struct rusage usage;
|
||||
uint64_t track, clip, note, future;
|
||||
float left[128], right[128], out_left[128], out_right[128];
|
||||
double *times = NULL, total = 0, begin, prepare_ms;
|
||||
float left[1024], right[1024], out_left[1024], out_right[1024];
|
||||
double *times = NULL, total = 0, begin, prepare_ms, cpu_begin, cpu;
|
||||
int result = EXIT_FAILURE;
|
||||
if (argc > 2)
|
||||
if (argc != 1 && argc != 2 && argc != 4)
|
||||
return EXIT_FAILURE;
|
||||
if (argc == 2) {
|
||||
if (argc >= 2) {
|
||||
blocks = strtol(argv[1], &end, 10);
|
||||
if (!*argv[1] || *end || blocks < 1 || blocks > 100000)
|
||||
if (!*argv[1] || *end || blocks < 1 || blocks > 10000000)
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (argc == 4) {
|
||||
long value = strtol(argv[2], &end, 10);
|
||||
if (!*argv[2] || *end || value < 1 || value > 64) return EXIT_FAILURE;
|
||||
tracks = (size_t)value;
|
||||
value = strtol(argv[3], &end, 10);
|
||||
if (!*argv[3] || *end || value < 1 || value > 1024) return EXIT_FAILURE;
|
||||
frames = (size_t)value;
|
||||
}
|
||||
times = calloc((size_t)blocks, sizeof(*times));
|
||||
REQUIRE(times);
|
||||
begin = now();
|
||||
REQUIRE(xtc_project_create(&project) == XTC_OK);
|
||||
for (i = 0; i < 16; ++i) {
|
||||
for (i = 0; i < tracks; ++i) {
|
||||
xtc_automation_point point;
|
||||
REQUIRE(xtc_project_add_track(project, "Performance", &track) == XTC_OK);
|
||||
REQUIRE(xtc_project_add_midi_clip(project, track, 0, 384000, &clip) == XTC_OK);
|
||||
REQUIRE(xtc_project_set_mix(project, track, 0.1f, i % 2 ? 0.5f : -0.5f, 0, 0) == XTC_OK);
|
||||
for (j = 0; j < 2; ++j)
|
||||
REQUIRE(xtc_project_add_note(project, clip, 0, 384000, 48 + (unsigned int)i, 100, 0,
|
||||
REQUIRE(xtc_project_add_note(project, clip, i < 16 ? 0 : 380000 + i * 2, i < 16 ? 370000 : 1, 48 + (unsigned int)i, 100, 0,
|
||||
¬e) == XTC_OK);
|
||||
point = (xtc_automation_point){track, 0, XTC_AUTOMATION_GAIN, 0.1f,0};
|
||||
REQUIRE(xtc_project_set_automation(project, &point) == XTC_OK);
|
||||
point.tick = 384000;
|
||||
point.value = 0.2f;
|
||||
REQUIRE(xtc_project_set_automation(project, &point) == XTC_OK);
|
||||
for (j = 1; j < 16; ++j) {
|
||||
point.tick = j * 25600;
|
||||
point.value = j % 2 ? 0.2f : 0.1f;
|
||||
REQUIRE(xtc_project_set_automation(project, &point) == XTC_OK);
|
||||
}
|
||||
}
|
||||
REQUIRE(xtc_project_add_midi_clip(project, track, 1000000, 10000, &future) == XTC_OK);
|
||||
for (i = 32; i < XTC_PROJECT_MAX_NOTES; ++i)
|
||||
REQUIRE(xtc_project_add_note(project, future, (i - 32) * 2, 1, 60, 100, 0, ¬e) ==
|
||||
for (i = tracks * 2; i < tracks * 64; ++i)
|
||||
REQUIRE(xtc_project_add_note(project, future, (i - tracks * 2) * 2, 1, 60, 100, 0, ¬e) ==
|
||||
XTC_OK);
|
||||
REQUIRE(xtc_graph_create(64, &graph) == XTC_OK &&
|
||||
xtc_graph_add_instrument(graph, &source) == XTC_OK);
|
||||
|
|
@ -79,26 +100,40 @@ int main(int argc, char **argv)
|
|||
previous = node;
|
||||
REQUIRE(xtc_graph_add_output(graph, &node) == XTC_OK &&
|
||||
xtc_graph_connect(graph, previous, node) == XTC_OK);
|
||||
REQUIRE(xtc_graph_compile(graph, 48000, 128, &plan) == XTC_OK &&
|
||||
REQUIRE(xtc_graph_compile(graph, 48000, frames, &plan) == XTC_OK &&
|
||||
xtc_midi_plan_create(project, 48000, &midi) == XTC_OK);
|
||||
prepare_ms = (now() - begin) * 1000;
|
||||
REQUIRE(xtc_project_note_count(project, ¬e_count) == XTC_OK);
|
||||
REQUIRE(xtc_project_automation_count(project, &point_count) == XTC_OK);
|
||||
cpu_begin = cpu_now();
|
||||
for (i = 0; i < (size_t)blocks; ++i) {
|
||||
begin = now();
|
||||
REQUIRE(xtc_midi_plan_render_stereo(midi, i * 128, NULL, 0, left, right, 128) == XTC_OK);
|
||||
REQUIRE(xtc_midi_plan_render_stereo(midi, position, NULL, 0, left, right, frames) == XTC_OK);
|
||||
REQUIRE(xtc_plan_process_stereo(plan, NULL, NULL, NULL, left, right, out_left, out_right,
|
||||
128) == XTC_OK);
|
||||
frames) == XTC_OK);
|
||||
times[i] = now() - begin;
|
||||
total += times[i];
|
||||
for (j = 0; j < 128; ++j)
|
||||
if (times[i] > (double)frames / 48000) ++misses;
|
||||
position += frames;
|
||||
/* Revisit the active arrangement in long offline runs. Explicit sample
|
||||
* positions exercise discontinuous seek/wrap handling in the MIDI plan. */
|
||||
if (position >= 96000) { position = 0; ++wraps; }
|
||||
|
||||
for (j = 0; j < frames; ++j)
|
||||
REQUIRE(isfinite(out_left[j]) && isfinite(out_right[j]));
|
||||
}
|
||||
cpu = cpu_now() - cpu_begin;
|
||||
REQUIRE(getrusage(RUSAGE_SELF, &usage) == 0);
|
||||
qsort(times, (size_t)blocks, sizeof(*times), compare);
|
||||
printf("Offline 48kHz/128, 64 nodes, 4096 notes (32 active), 16 automated tracks; %ld blocks\n",
|
||||
blocks);
|
||||
printf("Prepare %.3f ms; block mean %.3f us, p99 %.3f us, max %.3f us; deadline 2666.667 us; "
|
||||
"CPU/audio %.4f\n",
|
||||
prepare_ms, total / (double)blocks * 1e6, times[(size_t)blocks * 99 / 100] * 1e6,
|
||||
times[blocks - 1] * 1e6, total / ((double)blocks * 128 / 48000));
|
||||
printf("{\"tracks\":%zu,\"notes\":%zu,\"points\":%zu,\"nodes\":64,"
|
||||
"\"rate\":48000,\"frames\":%zu,\"blocks\":%ld,\"wraps\":%" PRIu64 ","
|
||||
"\"prepare_ms\":%.6f,\"mean_us\":%.6f,\"p99_us\":%.6f,\"max_us\":%.6f,"
|
||||
"\"deadline_us\":%.6f,\"deadline_misses\":%zu,\"process_cpu_ms\":%.6f,"
|
||||
"\"cpu_audio_ratio\":%.6f,\"peak_rss_kib\":%ld}\n",
|
||||
tracks, note_count, point_count, frames, blocks, wraps, prepare_ms,
|
||||
total / (double)blocks * 1e6, times[(size_t)blocks * 99 / 100] * 1e6,
|
||||
times[blocks - 1] * 1e6, (double)frames / 48000 * 1e6, misses, cpu * 1e3,
|
||||
cpu / ((double)blocks * (double)frames / 48000), usage.ru_maxrss);
|
||||
result = EXIT_SUCCESS;
|
||||
cleanup:
|
||||
free(times);
|
||||
|
|
|
|||
|
|
@ -6,12 +6,37 @@
|
|||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
|
||||
void *__real_malloc(size_t size);
|
||||
void *__real_calloc(size_t count, size_t size);
|
||||
void *__real_realloc(void *pointer, size_t size);
|
||||
void __real_free(void *pointer);
|
||||
static int processing, fail_after = -1;
|
||||
/* Catch direct host I/O and blocking synchronization in guarded callbacks.
|
||||
* Dynamic dependencies have their own symbols and are outside this audit. */
|
||||
FILE *__real_fopen(const char *path, const char *mode);
|
||||
size_t __real_fread(void *buffer, size_t size, size_t count, FILE *file);
|
||||
size_t __real_fwrite(const void *buffer, size_t size, size_t count, FILE *file);
|
||||
ssize_t __real_read(int fd, void *buffer, size_t size);
|
||||
ssize_t __real_write(int fd, const void *buffer, size_t size);
|
||||
int __real_pthread_mutex_lock(pthread_mutex_t *mutex);
|
||||
int __real_pthread_cond_wait(pthread_cond_t *condition, pthread_mutex_t *mutex);
|
||||
FILE *__wrap_fopen(const char *path, const char *mode)
|
||||
{ assert(!processing); return __real_fopen(path, mode); }
|
||||
size_t __wrap_fread(void *buffer, size_t size, size_t count, FILE *file)
|
||||
{ assert(!processing); return __real_fread(buffer, size, count, file); }
|
||||
size_t __wrap_fwrite(const void *buffer, size_t size, size_t count, FILE *file)
|
||||
{ assert(!processing); return __real_fwrite(buffer, size, count, file); }
|
||||
ssize_t __wrap_read(int fd, void *buffer, size_t size)
|
||||
{ assert(!processing); return __real_read(fd, buffer, size); }
|
||||
ssize_t __wrap_write(int fd, const void *buffer, size_t size)
|
||||
{ assert(!processing); return __real_write(fd, buffer, size); }
|
||||
int __wrap_pthread_mutex_lock(pthread_mutex_t *mutex)
|
||||
{ assert(!processing); return __real_pthread_mutex_lock(mutex); }
|
||||
int __wrap_pthread_cond_wait(pthread_cond_t *condition, pthread_mutex_t *mutex)
|
||||
{ assert(!processing); return __real_pthread_cond_wait(condition, mutex); }
|
||||
static unsigned int prepared, destroyed;
|
||||
static int fail_allocation(void)
|
||||
{
|
||||
|
|
|
|||
56
tests/integration/test_soak.c
Normal file
56
tests/integration/test_soak.c
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
/* Accelerated virtual playback: synchronous consumer, no hardware/server. */
|
||||
#include "xtc/xtc_realtime.h"
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
xtc_graph *graph = NULL;
|
||||
xtc_realtime *engine = NULL;
|
||||
xtc_realtime_status status;
|
||||
size_t gain, output, i, j, frames;
|
||||
unsigned long blocks = 1000000, iterations = 0;
|
||||
uint64_t samples = 0;
|
||||
float input[1024], left[1024], right[1024];
|
||||
if (argc > 2) return 1;
|
||||
if (argc == 2) {
|
||||
char *end;
|
||||
blocks = strtoul(argv[1], &end, 10);
|
||||
if (!*argv[1] || *end || blocks < 100 || blocks > 100000000) return 1;
|
||||
}
|
||||
assert(xtc_graph_create(2, &graph) == XTC_OK);
|
||||
assert(xtc_graph_add_gain(graph, 0.5f, &gain) == XTC_OK);
|
||||
assert(xtc_graph_add_output(graph, &output) == XTC_OK);
|
||||
assert(xtc_graph_connect(graph, gain, output) == XTC_OK);
|
||||
assert(xtc_realtime_create(48000, 1024, &engine) == XTC_OK);
|
||||
assert(xtc_realtime_submit(engine, graph) == XTC_OK);
|
||||
for (j = 0; j < 1024; ++j) input[j] = j % 2 ? -0.5f : 0.25f;
|
||||
for (i = 0; i < blocks; ++i) {
|
||||
static const size_t sizes[] = {31, 128, 1024};
|
||||
frames = sizes[i % 3];
|
||||
if (i && i % 100 == 0) {
|
||||
/* Quiescent control work models stop, seek/wrap and graph restart.
|
||||
* No hardware lifecycle claims are made by this test. */
|
||||
xtc_realtime_collect(engine);
|
||||
assert(xtc_realtime_position(engine, (i % 200) ? 48000 : 0) == XTC_OK);
|
||||
assert(xtc_realtime_submit(engine, graph) == XTC_OK);
|
||||
++iterations;
|
||||
}
|
||||
assert(xtc_realtime_process_stereo(engine, input, NULL, NULL, NULL, NULL,
|
||||
left, right, frames) == XTC_OK);
|
||||
for (j = 0; j < frames; ++j)
|
||||
assert(isfinite(left[j]) && left[j] == input[j] * 0.5f && right[j] == left[j]);
|
||||
samples += frames;
|
||||
}
|
||||
assert(xtc_realtime_get_status(engine, &status) == XTC_OK);
|
||||
assert(status.last_result == XTC_OK && status.processed_blocks == blocks);
|
||||
assert(status.active_revision == status.submitted_revision);
|
||||
xtc_realtime_collect(engine);
|
||||
xtc_realtime_destroy(engine);
|
||||
xtc_graph_destroy(graph);
|
||||
printf("Virtual playback: %lu blocks, %.3f audio hours, %lu quiescent seek/rebuild cycles passed.\n",
|
||||
blocks, (double)samples / 48000 / 3600, iterations);
|
||||
return 0;
|
||||
}
|
||||
51
tests/tools/test_benchmark.py
Normal file
51
tests/tools/test_benchmark.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Verify benchmark gates without depending on noisy real-machine timings."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='xtc-benchmark-') as directory:
|
||||
root = Path(directory)
|
||||
binary = root / 'measurement'
|
||||
binary.write_text('''#!/usr/bin/env python3
|
||||
import json, sys
|
||||
nodes, channels, rate, frames, blocks = map(int, sys.argv[1:])
|
||||
print(json.dumps(dict(nodes=nodes, channels=channels, rate=rate, frames=frames,
|
||||
blocks=blocks, mean_us=10, p99_us=12, deadline_misses=0,
|
||||
process_cpu_ms=1, peak_rss_kib=1024)))
|
||||
''')
|
||||
binary.chmod(0o700)
|
||||
report, baseline = root / 'report.json', root / 'baseline.json'
|
||||
command = [sys.executable, 'tools/benchmark.py', '--binary', str(binary),
|
||||
'--output', str(report)]
|
||||
|
||||
def run(arguments, success, message=None):
|
||||
result = subprocess.run(arguments, capture_output=True, text=True, timeout=30)
|
||||
assert (result.returncode == 0) == success, result.stdout + result.stderr
|
||||
if message:
|
||||
assert message in result.stdout + result.stderr
|
||||
|
||||
run(command, True)
|
||||
original = json.loads(report.read_text())
|
||||
baseline.write_text(json.dumps(original))
|
||||
compare = command + ['--baseline', str(baseline)]
|
||||
run(compare, True)
|
||||
changed = json.loads(json.dumps(original))
|
||||
changed['cases'][0]['mean_us'] = 1
|
||||
baseline.write_text(json.dumps(changed))
|
||||
run(compare, False, 'exceeds')
|
||||
changed = json.loads(json.dumps(original))
|
||||
changed['cases'].pop()
|
||||
baseline.write_text(json.dumps(changed))
|
||||
run(compare, False, 'missing baseline case')
|
||||
changed = json.loads(json.dumps(original))
|
||||
changed['context']['flags'] = 'incompatible'
|
||||
baseline.write_text(json.dumps(changed))
|
||||
run(compare, False, 'mismatch')
|
||||
changed = json.loads(json.dumps(original))
|
||||
changed['context']['workload'] = 'project'
|
||||
baseline.write_text(json.dumps(changed))
|
||||
run(compare, False, 'workload mismatch')
|
||||
print('Benchmark matching, regression, missing-case and context gates passed.')
|
||||
Loading…
Add table
Add a link
Reference in a new issue