feat: add stress test for audio engine real-time safety
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
289
stress_test.c
Normal file
289
stress_test.c
Normal file
@@ -0,0 +1,289 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <stdatomic.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include "engine.h"
|
||||
|
||||
static volatile int keep_running = 1;
|
||||
|
||||
static void handle_sigint(int sig) {
|
||||
(void)sig;
|
||||
keep_running = 0;
|
||||
}
|
||||
|
||||
static Engine *create_stress_engine(void) {
|
||||
Engine *engine = (Engine *)calloc(1, sizeof(Engine));
|
||||
assert(engine != NULL);
|
||||
|
||||
engine->control_channel = 0;
|
||||
engine->sample_rate = 48000;
|
||||
engine->quantize_mode = QUANTIZE_OFF;
|
||||
engine->quantize_threshold = 0;
|
||||
engine->queued_triggers = NULL;
|
||||
|
||||
command_queue_init(&engine->command_queue);
|
||||
|
||||
atomic_store(&engine->quantize_mode_atomic, (int)QUANTIZE_OFF);
|
||||
atomic_store(&engine->quantize_threshold_atomic, 0);
|
||||
|
||||
engine->transport = (Transport *)calloc(1, sizeof(Transport));
|
||||
assert(engine->transport != NULL);
|
||||
transport_init(engine->transport, 48000);
|
||||
|
||||
for (int i = 0; i < MAX_CLIPS; i++) {
|
||||
engine->clips[i].state = CLIP_EMPTY;
|
||||
engine->clips[i].buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float));
|
||||
assert(engine->clips[i].buffer != NULL);
|
||||
engine->clips[i].buffer_size = 0;
|
||||
engine->clips[i].write_position = 0;
|
||||
engine->clips[i].read_position = 0;
|
||||
}
|
||||
|
||||
return engine;
|
||||
}
|
||||
|
||||
static void destroy_stress_engine(Engine *engine) {
|
||||
if (engine) {
|
||||
engine->queued_triggers = NULL; // pool, no free
|
||||
if (engine->transport) {
|
||||
transport_cleanup(engine->transport);
|
||||
free(engine->transport);
|
||||
}
|
||||
for (int i = 0; i < MAX_CLIPS; i++) {
|
||||
free(engine->clips[i].buffer);
|
||||
}
|
||||
free(engine);
|
||||
}
|
||||
}
|
||||
|
||||
// Stress test 1: Rapid clip triggers via command queue
|
||||
static void stress_command_queue(void) {
|
||||
printf("Stress test 1: Rapid command queue submission...\n");
|
||||
Engine *engine = create_stress_engine();
|
||||
|
||||
int num_ops = 10000;
|
||||
for (int i = 0; i < num_ops; i++) {
|
||||
int clip_idx = i % MAX_CLIPS;
|
||||
int ret = engine_submit_command(engine, CMD_TRIGGER_CLIP, clip_idx, 0);
|
||||
assert(ret == 0); // should not drop
|
||||
}
|
||||
|
||||
// Process all commands
|
||||
engine_process_commands(engine);
|
||||
|
||||
// Verify no commands left
|
||||
CommandQueue *q = &engine->command_queue;
|
||||
unsigned int write = atomic_load(&q->write_index);
|
||||
unsigned int read = atomic_load(&q->read_index);
|
||||
assert(write == read);
|
||||
|
||||
destroy_stress_engine(engine);
|
||||
printf(" PASSED (%d commands processed)\n", num_ops);
|
||||
}
|
||||
|
||||
// Stress test 2: Mix of clip and scene triggers
|
||||
static void stress_mixed_triggers(void) {
|
||||
printf("Stress test 2: Mixed clip/scene triggers...\n");
|
||||
Engine *engine = create_stress_engine();
|
||||
|
||||
int num_ops = 5000;
|
||||
for (int i = 0; i < num_ops; i++) {
|
||||
if (i % 3 == 0) {
|
||||
int scene_idx = (i / 3) % MAX_SCENES;
|
||||
engine_submit_command(engine, CMD_TRIGGER_SCENE, scene_idx, 0);
|
||||
} else {
|
||||
int clip_idx = i % MAX_CLIPS;
|
||||
engine_submit_command(engine, CMD_TRIGGER_CLIP, clip_idx, 0);
|
||||
}
|
||||
}
|
||||
|
||||
engine_process_commands(engine);
|
||||
|
||||
// Verify queue drained
|
||||
CommandQueue *q = &engine->command_queue;
|
||||
assert(atomic_load(&q->write_index) == atomic_load(&q->read_index));
|
||||
|
||||
destroy_stress_engine(engine);
|
||||
printf(" PASSED\n");
|
||||
}
|
||||
|
||||
// Stress test 3: Queue overflow (should not crash)
|
||||
static void stress_queue_overflow(void) {
|
||||
printf("Stress test 3: Queue overflow (should drop gracefully)...\n");
|
||||
Engine *engine = create_stress_engine();
|
||||
|
||||
// Submit more than MAX_QUEUED_COMMANDS
|
||||
int num_ops = MAX_QUEUED_COMMANDS * 2;
|
||||
int dropped = 0;
|
||||
for (int i = 0; i < num_ops; i++) {
|
||||
int ret = engine_submit_command(engine, CMD_TRIGGER_CLIP, i % MAX_CLIPS, 0);
|
||||
if (ret != 0) dropped++;
|
||||
}
|
||||
|
||||
assert(dropped > 0); // some should have been dropped
|
||||
printf(" Dropped %d commands (expected)\n", dropped);
|
||||
|
||||
// Process what we can
|
||||
engine_process_commands(engine);
|
||||
|
||||
destroy_stress_engine(engine);
|
||||
printf(" PASSED\n");
|
||||
}
|
||||
|
||||
// Stress test 4: Trigger pool exhaustion (simulate many MIDI notes)
|
||||
static void stress_trigger_pool(void) {
|
||||
printf("Stress test 4: Trigger pool exhaustion...\n");
|
||||
Engine *engine = create_stress_engine();
|
||||
|
||||
// Set transport playing so queue_trigger is used
|
||||
engine->transport->state = TRANSPORT_PLAYING;
|
||||
atomic_store(&engine->transport->state_atomic, TRANSPORT_PLAYING);
|
||||
engine->quantize_mode = QUANTIZE_BEAT;
|
||||
atomic_store(&engine->quantize_mode_atomic, (int)QUANTIZE_BEAT);
|
||||
|
||||
// Queue more triggers than pool size
|
||||
int num_triggers = QUEUED_TRIGGER_POOL_SIZE * 2;
|
||||
for (int i = 0; i < num_triggers; i++) {
|
||||
queue_trigger(engine, i % MAX_CLIPS, false, 100);
|
||||
}
|
||||
|
||||
// Process queued triggers (simulate reaching quantize boundary)
|
||||
// We need to call process_queued_triggers directly (it's static, but we can
|
||||
// simulate by calling engine_process_commands which doesn't process queued triggers).
|
||||
// Instead, we'll just verify the pool didn't crash and that some triggers were queued.
|
||||
// The pool head should be at most QUEUED_TRIGGER_POOL_SIZE.
|
||||
// We can't access the static pool head, but we can check that the linked list
|
||||
// has at most QUEUED_TRIGGER_POOL_SIZE nodes.
|
||||
int count = 0;
|
||||
QueuedTrigger *qt = engine->queued_triggers;
|
||||
while (qt) {
|
||||
count++;
|
||||
qt = qt->next;
|
||||
}
|
||||
assert(count <= QUEUED_TRIGGER_POOL_SIZE);
|
||||
printf(" Queued %d triggers (pool size %d)\n", count, QUEUED_TRIGGER_POOL_SIZE);
|
||||
|
||||
// Clean up (just set to NULL, pool is static)
|
||||
engine->queued_triggers = NULL;
|
||||
|
||||
destroy_stress_engine(engine);
|
||||
printf(" PASSED\n");
|
||||
}
|
||||
|
||||
// Stress test 5: Undo/Redo stress
|
||||
static void stress_undo_redo(void) {
|
||||
printf("Stress test 5: Undo/Redo stress...\n");
|
||||
Engine *engine = create_stress_engine();
|
||||
|
||||
int num_ops = 1000;
|
||||
for (int i = 0; i < num_ops; i++) {
|
||||
int clip_idx = i % MAX_CLIPS;
|
||||
engine_submit_command(engine, CMD_TRIGGER_CLIP, clip_idx, 0);
|
||||
engine_process_commands(engine);
|
||||
|
||||
// Undo every other operation
|
||||
if (i % 2 == 0) {
|
||||
engine_submit_command(engine, CMD_UNDO, 0, 0);
|
||||
engine_process_commands(engine);
|
||||
}
|
||||
}
|
||||
|
||||
// Redo some
|
||||
for (int i = 0; i < 100; i++) {
|
||||
engine_submit_command(engine, CMD_REDO, 0, 0);
|
||||
engine_process_commands(engine);
|
||||
}
|
||||
|
||||
destroy_stress_engine(engine);
|
||||
printf(" PASSED\n");
|
||||
}
|
||||
|
||||
// Stress test 6: Transport state changes
|
||||
static void stress_transport(void) {
|
||||
printf("Stress test 6: Transport state changes...\n");
|
||||
Engine *engine = create_stress_engine();
|
||||
|
||||
int num_ops = 5000;
|
||||
for (int i = 0; i < num_ops; i++) {
|
||||
switch (i % 5) {
|
||||
case 0:
|
||||
engine_submit_command(engine, CMD_TRANSPORT_PLAY, 0, 0);
|
||||
break;
|
||||
case 1:
|
||||
engine_submit_command(engine, CMD_TRANSPORT_PAUSE, 0, 0);
|
||||
break;
|
||||
case 2:
|
||||
engine_submit_command(engine, CMD_TRANSPORT_STOP, 0, 0);
|
||||
break;
|
||||
case 3:
|
||||
engine_submit_command(engine, CMD_TRANSPORT_TOGGLE_PLAY, 0, 0);
|
||||
break;
|
||||
case 4:
|
||||
engine_submit_command(engine, CMD_SET_CLOCK_SOURCE, (int)CLOCK_SOURCE_MIDI, 0);
|
||||
break;
|
||||
}
|
||||
engine_process_commands(engine);
|
||||
}
|
||||
|
||||
destroy_stress_engine(engine);
|
||||
printf(" PASSED\n");
|
||||
}
|
||||
|
||||
// Stress test 7: Quantize mode changes
|
||||
static void stress_quantize(void) {
|
||||
printf("Stress test 7: Quantize mode changes...\n");
|
||||
Engine *engine = create_stress_engine();
|
||||
|
||||
int num_ops = 5000;
|
||||
for (int i = 0; i < num_ops; i++) {
|
||||
QuantizeMode mode = (QuantizeMode)(i % 3);
|
||||
engine_submit_command(engine, CMD_SET_QUANTIZE_MODE, (int)mode, 0);
|
||||
engine_submit_command(engine, CMD_SET_QUANTIZE_THRESHOLD, 0, (jack_nframes_t)(i * 100));
|
||||
engine_process_commands(engine);
|
||||
}
|
||||
|
||||
destroy_stress_engine(engine);
|
||||
printf(" PASSED\n");
|
||||
}
|
||||
|
||||
// Stress test 8: Reset clips while triggering
|
||||
static void stress_reset_while_triggering(void) {
|
||||
printf("Stress test 8: Reset clips while triggering...\n");
|
||||
Engine *engine = create_stress_engine();
|
||||
|
||||
int num_ops = 2000;
|
||||
for (int i = 0; i < num_ops; i++) {
|
||||
int clip_idx = i % MAX_CLIPS;
|
||||
engine_submit_command(engine, CMD_TRIGGER_CLIP, clip_idx, 0);
|
||||
engine_process_commands(engine);
|
||||
|
||||
if (i % 10 == 0) {
|
||||
engine_submit_command(engine, CMD_RESET_CLIP, clip_idx, 0);
|
||||
engine_process_commands(engine);
|
||||
}
|
||||
}
|
||||
|
||||
destroy_stress_engine(engine);
|
||||
printf(" PASSED\n");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
signal(SIGINT, handle_sigint);
|
||||
|
||||
printf("Running JACK Looper stress tests...\n\n");
|
||||
|
||||
stress_command_queue();
|
||||
stress_mixed_triggers();
|
||||
stress_queue_overflow();
|
||||
stress_trigger_pool();
|
||||
stress_undo_redo();
|
||||
stress_transport();
|
||||
stress_quantize();
|
||||
stress_reset_while_triggering();
|
||||
|
||||
printf("\nAll stress tests passed!\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user