|
| 1 | +//! Honest measurement of the audio-render thread pool's parallel |
| 2 | +//! scaling. |
| 3 | +//! |
| 4 | +//! Runs the same render scenario at varying audio thread counts and |
| 5 | +//! reports wall-clock time per iteration. Pair with `/usr/bin/time -l` |
| 6 | +//! to see total CPU time (user+sys) vs wall time — the ratio reveals |
| 7 | +//! whether parallelism is delivering real wall-clock gains or just |
| 8 | +//! burning more cores for the same total work. |
| 9 | +//! |
| 10 | +//! Usage: |
| 11 | +//! # voice_count thread_count |
| 12 | +//! /usr/bin/time -l cargo run --release --example parallel_scaling -p weresocool_core -- 100 1 |
| 13 | +//! /usr/bin/time -l cargo run --release --example parallel_scaling -p weresocool_core -- 100 4 |
| 14 | +//! /usr/bin/time -l cargo run --release --example parallel_scaling -p weresocool_core -- 100 8 |
| 15 | +//! /usr/bin/time -l cargo run --release --example parallel_scaling -p weresocool_core -- 100 16 |
| 16 | +//! |
| 17 | +//! Both args are optional: defaults are 100 voices, current setting's |
| 18 | +//! audio_thread_count. Thread count is configured by writing it into |
| 19 | +//! Settings BEFORE `Settings::init` so the audio pool picks it up on |
| 20 | +//! first use. |
| 21 | +
|
| 22 | +use std::time::Instant; |
| 23 | +use weresocool_core::manager::{RenderManager, RenderManagerSettings}; |
| 24 | +use weresocool_instrument::renderable::{render_voice::RenderVoice, RenderOp}; |
| 25 | +use weresocool_instrument::Offset; |
| 26 | +use weresocool_shared::Settings; |
| 27 | + |
| 28 | +fn make_ops(num_ops: usize, samples_per_op: usize, sample_rate: f64) -> Vec<RenderOp> { |
| 29 | + let mut ops = Vec::with_capacity(num_ops); |
| 30 | + for i in 0..num_ops { |
| 31 | + let f = 220.0 + (i % 8) as f64 * 15.0; |
| 32 | + let g = (0.2, 0.2); |
| 33 | + let p = 0.0; |
| 34 | + let l = samples_per_op as f64 / sample_rate; |
| 35 | + let mut op = RenderOp::init_fglps(f, g, l, p, samples_per_op); |
| 36 | + op.index = 0; |
| 37 | + op.total_samples = samples_per_op; |
| 38 | + ops.push(op); |
| 39 | + } |
| 40 | + ops |
| 41 | +} |
| 42 | + |
| 43 | +fn render_once(voices: usize, ops_per_voice: usize, reads: usize, buffer: usize, sample_rate: f64) { |
| 44 | + let settings = RenderManagerSettings { sample_rate, buffer_size: buffer }; |
| 45 | + let mut rm = RenderManager::init(None, None, false, Some(settings)); |
| 46 | + |
| 47 | + let mut all_voices: Vec<RenderVoice> = Vec::with_capacity(voices); |
| 48 | + for v in 0..voices { |
| 49 | + let mut ops = make_ops(ops_per_voice, buffer * reads, sample_rate); |
| 50 | + for (event, op) in ops.iter_mut().enumerate() { |
| 51 | + op.voice = v; |
| 52 | + op.event = event; |
| 53 | + } |
| 54 | + all_voices.push(RenderVoice::init(&ops)); |
| 55 | + } |
| 56 | + rm.push_render(all_voices, false); |
| 57 | + |
| 58 | + let mut produced = 0; |
| 59 | + while produced < reads { |
| 60 | + match rm.read(buffer, Offset::default()) { |
| 61 | + Some((sw, _ramp, _ops)) => { |
| 62 | + std::hint::black_box(sw); |
| 63 | + produced += 1; |
| 64 | + } |
| 65 | + None => break, |
| 66 | + } |
| 67 | + } |
| 68 | +} |
| 69 | + |
| 70 | +fn main() { |
| 71 | + let voices: usize = std::env::args() |
| 72 | + .nth(1) |
| 73 | + .and_then(|s| s.parse().ok()) |
| 74 | + .unwrap_or(100); |
| 75 | + let thread_count: Option<usize> = std::env::args().nth(2).and_then(|s| s.parse().ok()); |
| 76 | + |
| 77 | + let buffer = 1024usize; |
| 78 | + let sample_rate = 48_000.0; |
| 79 | + |
| 80 | + // Configure Settings BEFORE the audio pool is first touched. The |
| 81 | + // audio pool reads `audio_thread_count` once at init. |
| 82 | + let mut s = weresocool_shared::default_settings(); |
| 83 | + s.sample_rate = sample_rate; |
| 84 | + s.buffer_size = buffer; |
| 85 | + if let Some(n) = thread_count { |
| 86 | + s.audio_thread_count = n; |
| 87 | + // For the sweep, drop the voice-count threshold so even small |
| 88 | + // voice counts go through the pool — otherwise low-voice-count |
| 89 | + // / low-thread-count rows wouldn't exercise parallelism. |
| 90 | + s.parallel_voice_threshold = 1; |
| 91 | + } |
| 92 | + s.set(); |
| 93 | + |
| 94 | + let ops_per_voice = 4; |
| 95 | + let reads = 16; |
| 96 | + let iterations = 50; |
| 97 | + |
| 98 | + println!( |
| 99 | + "voices={voices} ops_per_voice={ops_per_voice} reads={reads} iters={iterations}" |
| 100 | + ); |
| 101 | + println!( |
| 102 | + "audio_thread_count={} parallel_voice_threshold={}", |
| 103 | + Settings::global().audio_thread_count, |
| 104 | + Settings::global().parallel_voice_threshold |
| 105 | + ); |
| 106 | + |
| 107 | + // Warmup |
| 108 | + for _ in 0..5 { |
| 109 | + render_once(voices, ops_per_voice, reads, buffer, sample_rate); |
| 110 | + } |
| 111 | + |
| 112 | + let start = Instant::now(); |
| 113 | + for _ in 0..iterations { |
| 114 | + render_once(voices, ops_per_voice, reads, buffer, sample_rate); |
| 115 | + } |
| 116 | + let elapsed = start.elapsed(); |
| 117 | + |
| 118 | + let per_iter_ms = elapsed.as_secs_f64() * 1000.0 / iterations as f64; |
| 119 | + println!( |
| 120 | + "wall total: {:.3} s per iteration: {:.3} ms", |
| 121 | + elapsed.as_secs_f64(), |
| 122 | + per_iter_ms |
| 123 | + ); |
| 124 | +} |
0 commit comments