1use std::path::Path;
4
5use sva_engine::{
6 Alias, AliasBand, Answer, Arguments, BandCrest, BandTrack, Bands, Binding, Buffer, CacheStats,
7 Cost, Crest, Detail, EnvelopeFrame, FormantFrame, Horizon, Label, LedgerEntry, Loudness,
8 LoudnessFrame, Outcome, Output, PayloadKind, Source, SpectralSum, Spectrum, StereoFrame,
9 StereoImage, Tier,
10};
11
12use crate::json::{NONE, capped, escape, list, num};
13
14pub const SAMPLE_LIMIT: usize = 4096;
16
17pub struct CacheReport {
19 pub dir: String,
20 pub stats: CacheStats,
21 pub held_bytes: u64,
22 pub max_bytes: u64,
23 pub evicted_bytes: u64,
24 pub faults: u64,
25}
26
27fn maybe(v: Option<f64>) -> String {
28 v.map(num).unwrap_or_else(|| NONE.to_string())
29}
30
31fn counted(v: Option<usize>) -> String {
32 v.map_or_else(|| NONE.to_string(), |c| c.to_string())
33}
34
35fn ledger_json(entries: &[LedgerEntry], skim: bool) -> String {
36 list(entries, |e| {
37 let clipped = e
38 .clipped
39 .map_or_else(|| NONE.to_string(), |c| c.to_string());
40 let channel = counted(e.channel);
41 if skim {
42 return format!(
43 "\n {{ \"node\": \"{}\", \"channel\": {channel}, \"rms\": {}, \"peak\": {}, \"clipped\": {clipped} }}",
44 escape(&e.node),
45 num(e.rms),
46 num(e.peak)
47 );
48 }
49 format!(
50 "\n {{ \"node\": \"{}\", \"channel\": {channel}, \"depth\": {}, \"unit\": \"{}\", \
51 \"control\": {}, \"rms\": {}, \"peak\": {}, \"min\": {}, \"max\": {}, \
52 \"share\": {}, \"clipped\": {clipped} }}",
53 escape(&e.node),
54 e.depth,
55 e.kind.unit().name(),
56 !e.kind.is_audio(),
57 num(e.rms),
58 num(e.peak),
59 num(e.min),
60 num(e.max),
61 maybe(e.share)
62 )
63 })
64}
65
66fn spectrum_json(s: &Spectrum) -> String {
67 format!(
68 "{{ \"frame_size\": {}, \"frames\": {}, \"resolution_hz\": {}, \"rms\": {}, \
69 \"centroid_hz\": {}, \"rolloff85_hz\": {}, \"peaks\": {}, \"bands\": {} }}",
70 s.frame_size,
71 s.frames,
72 num(s.resolution_hz),
73 num(s.rms),
74 num(s.centroid_hz),
75 num(s.rolloff85_hz),
76 list(&s.peaks, |p| format!(
77 "{{ \"hz\": {}, \"db\": {} }}",
78 num(p.hz),
79 num(p.db)
80 )),
81 list(&s.bands, |b| format!(
82 "\n {{ \"lo_hz\": {}, \"hi_hz\": {}, \"db\": {} }}",
83 num(b.lo_hz),
84 num(b.hi_hz),
85 num(b.db)
86 ))
87 )
88}
89
90fn stereo_json(image: &StereoImage) -> String {
91 let frame = |f: &StereoFrame| {
92 format!(
93 "\n {{ \"t\": {}, \"correlation\": {}, \"mid_rms\": {}, \"side_rms\": {}, \
94 \"width\": {}, \"balance_db\": {}, \"mono_db\": {} }}",
95 num(f.t_secs),
96 num(f.correlation),
97 num(f.mid_rms),
98 num(f.side_rms),
99 num(f.width),
100 num(f.balance_db),
101 num(f.mono_db)
102 )
103 };
104 format!(
105 "{{ \"channels\": {}, \"overall\": {}, \"frames\": {} }}",
106 image.channels,
107 frame(&image.overall),
108 list(&image.frames, frame)
109 )
110}
111
112fn formants_json(f: &FormantFrame) -> String {
113 format!(
114 "\n {{ \"t\": {}, \"order\": {}, \"energy\": {}, \"residual\": {}, \"formants\": {} }}",
115 num(f.t_secs),
116 f.order,
117 num(f.energy),
118 num(f.residual),
119 list(&f.formants, |v| format!(
120 "{{ \"hz\": {}, \"bandwidth_hz\": {}, \"db\": {} }}",
121 num(v.hz),
122 num(v.bandwidth_hz),
123 num(v.db)
124 ))
125 )
126}
127
128fn band_json(b: &BandTrack, start_secs: f64, rate_hz: f64, limit: Option<usize>) -> String {
129 let shown = limit.map_or(b.rms.len(), |cap| b.rms.len().min(cap));
130 format!(
131 "\n {{ \"centre_hz\": {}, \"cam\": {}, \"erb_hz\": {}, \"q\": {}, \"peak\": {}, \
132 \"time_to_peak_secs\": {}, \"rise_10_90_secs\": {}, \
133 \"floor\": {{ \"peak\": {}, \"time_to_peak_secs\": {}, \"rise_10_90_secs\": {} }}, \
134 \"rms\": {} }}",
135 num(b.centre_hz),
136 num(b.cam),
137 num(b.erb_hz),
138 num(b.q),
139 num(b.peak),
140 maybe(b.time_to_peak_secs),
141 maybe(b.rise_10_90_secs),
142 num(b.floor.peak),
143 num(b.floor.time_to_peak_secs),
144 maybe(b.floor.rise_10_90_secs),
145 capped(
146 &b.rms,
147 shown,
148 |n| start_secs + n as f64 / rate_hz,
149 |v| num(*v),
150 )
151 )
152}
153
154fn bands_json(b: &Bands, limit: Option<usize>) -> String {
155 format!(
156 "{{ \"rate_hz\": {}, \"start_secs\": {}, \"bands\": {} }}",
157 num(b.rate_hz),
158 num(b.start_secs),
159 list(&b.bands, |band| band_json(
160 band,
161 b.start_secs,
162 b.rate_hz,
163 limit
164 ))
165 )
166}
167
168fn loudness_json(l: &Loudness, limit: Option<usize>) -> String {
169 let frames = |fs: &[LoudnessFrame]| {
170 let shown = limit.map_or(fs.len(), |cap| fs.len().min(cap));
171 capped(
172 fs,
173 shown,
174 |n| fs[n].t,
175 |f| format!("\n {{ \"t\": {}, \"lufs\": {} }}", num(f.t), num(f.lufs)),
176 )
177 };
178 format!(
179 "{{ \"integrated_lufs\": {}, \"range_lu\": {}, \"momentary_max_lufs\": {}, \
180 \"short_term_max_lufs\": {}, \"sample_peak\": {}, \"sample_peak_dbfs\": {}, \
181 \"peak_note\": \"{}\", \"momentary\": {}, \"short_term\": {} }}",
182 maybe(l.integrated_lufs),
183 maybe(l.range_lu),
184 maybe(l.momentary_max_lufs),
185 maybe(l.short_term_max_lufs),
186 num(l.sample_peak),
187 maybe(l.sample_peak_dbfs),
188 escape(l.peak_note),
189 frames(&l.momentary),
190 frames(&l.short_term)
191 )
192}
193
194fn crest_json(c: &Crest) -> String {
195 let band = |b: &BandCrest| {
196 format!(
197 "\n {{ \"lo_hz\": {}, \"hi_hz\": {}, \"centre_hz\": {}, \"peak\": {}, \
198 \"rms\": {}, \"crest_db\": {}, \"counted\": {} }}",
199 num(b.lo_hz),
200 num(b.hi_hz),
201 num(b.centre_hz),
202 num(b.peak),
203 num(b.rms),
204 num(b.crest_db),
205 b.counted
206 )
207 };
208 format!(
209 "{{ \"broadband_crest_db\": {}, \"spread_db\": {}, \"widest_band_hz\": {}, \
210 \"tightest_band_hz\": {}, \"counted_under_db\": {}, \"bands\": {} }}",
211 num(c.broadband_crest_db),
212 maybe(c.spread_db),
213 maybe(c.widest_band_hz),
214 maybe(c.tightest_band_hz),
215 num(c.counted_under_db),
216 list(&c.bands, band)
217 )
218}
219
220fn alias_json(a: &Alias) -> String {
223 let band = |b: &AliasBand| {
224 format!(
225 "\n {{ \"lo_hz\": {}, \"hi_hz\": {}, \"signal_db\": {}, \"alias_db\": {}, \
226 \"nmr_db\": {} }}",
227 num(b.lo_hz),
228 num(b.hi_hz),
229 num(b.signal_db),
230 num(b.alias_db),
231 num(b.nmr_db)
232 )
233 };
234 format!(
235 "{{ \"oversample\": {}, \"sample_rate\": {}, \"frame_size\": {}, \"frames\": {}, \
236 \"scored_frames\": {}, \"playback_db_spl\": {}, \"asr_db\": {}, \"nmr_db\": {}, \
237 \"nmr_peak_db\": {}, \"peak_at_secs\": {}, \"audible\": {}, \"rate_dependent\": {}, \
238 \"instances\": {}, \"bands\": {} }}",
239 a.oversample,
240 num(a.sample_rate),
241 a.frame_size,
242 a.frames,
243 a.scored_frames,
244 num(a.playback_db_spl),
245 num(a.asr_db),
246 num(a.nmr_db),
247 num(a.nmr_peak_db),
248 num(a.peak_at_secs),
249 a.audible,
250 a.rate_dependent,
251 a.instances,
252 list(&a.bands, band)
253 )
254}
255
256fn binding_json(b: &Binding) -> String {
257 format!(
258 "\n {{ \"name\": \"{}\", \"source\": \"{}\" }}",
259 escape(&b.name),
260 escape(&b.source)
261 )
262}
263
264fn arguments_json(a: &Arguments) -> String {
265 let at = |s: sva_ast::ByteSpan| format!("{{ \"start\": {}, \"end\": {} }}", s.start, s.end);
266 let calls = list(&a.calls, |c| {
267 let arguments = list(&c.arguments, |x| {
268 format!(
269 "{{ \"name\": \"{}\", \"value\": {}, \"written\": {} }}",
270 escape(&x.name),
271 num(x.value),
272 x.written
273 )
274 });
275 format!(
276 "\n {{ \"name\": \"{}\", \"at\": {}, \"arguments\": {arguments} }}",
277 escape(&c.name),
278 at(c.at)
279 )
280 });
281 let chosen = list(&a.chosen, |c| {
282 format!(
283 "\n {{ \"name\": \"{}\", \"at\": {}, \"operands\": {}, \"chosen\": {} }}",
284 escape(&c.name),
285 at(c.at),
286 list(&c.operands, |v| num(*v)),
287 c.chosen
288 )
289 });
290 format!(
291 "\n {{ \"node\": \"{}\", \"calls\": {calls}, \"chosen\": {chosen} }}",
292 escape(&a.node)
293 )
294}
295
296fn samples_json(b: &Buffer, limit: Option<usize>) -> String {
298 let component = |c: usize| {
299 let plane = b.plane(c);
300 let shown = limit.map_or(plane.len(), |cap| plane.len().min(cap));
301 format!(
302 "\n {{ \"channel\": {c}, \"values\": {} }}",
303 capped(
304 plane,
305 shown,
306 |n| b.origin_secs + n as f64 / f64::from(b.rate),
307 |v| num(*v)
308 )
309 )
310 };
311 let components: Vec<usize> = (0..b.width).collect();
312 format!(
313 "{{ \"rate\": {}, \"origin_secs\": {}, \"width\": {}, \"components\": {} }}",
314 b.rate,
315 num(b.origin_secs),
316 b.width,
317 list(&components, |c| component(*c))
318 )
319}
320
321fn symbolic_json(n: &SpectralSum) -> String {
322 let terms: Vec<String> = n.atoms().map(sva_engine::sketch_atom).collect();
323 format!(
324 "{{ \"var\": \"{}\", \"lanes\": {}, \"terms\": {} }}",
325 match n.var {
326 sva_engine::Var::T => "t",
327 sva_engine::Var::F => "f",
328 },
329 n.lanes.len(),
330 crate::json::strings(&terms)
331 )
332}
333
334fn flops_json(tree: &sva_engine::FlopTree) -> String {
336 format!(
337 "{{ \"total\": {}, \"budget\": {}, \"rows\": {} }}",
338 tree.total,
339 tree.budget,
340 list(&tree.rows, |r: &sva_engine::FlopRow| format!(
341 "\n {{ \"depth\": {}, \"node\": \"{}\", \"own\": {}, \"subtree\": {}, \
342 \"percent\": {}, \"route\": \"{}\", \"shared\": {} }}",
343 r.depth,
344 escape(&r.node),
345 r.own,
346 r.subtree,
347 num(r.percent),
348 escape(r.route),
349 r.shared
350 ))
351 )
352}
353
354pub fn value_json(output: &Output, limit: Option<usize>, skim: bool) -> String {
357 match output {
358 Output::Lines(lines) => list(lines, line_json),
359 Output::Atoms(sketches) => crate::json::strings(sketches),
360 Output::Symbolic(sum) => symbolic_json(sum),
361 Output::Samples(buffer) => samples_json(buffer, limit),
362 Output::Ledger(entries) => ledger_json(entries, skim),
363 Output::Spectrum(s) => spectrum_json(s),
364 Output::Stereo(image) => stereo_json(image),
365 Output::Bands(b) => bands_json(b, limit),
366 Output::Loudness(l) => loudness_json(l, limit),
367 Output::Crest(c) => crest_json(c),
368 Output::Alias(a) => alias_json(a),
369 Output::Bindings(b) => list(b, binding_json),
370 Output::Arguments(a) => list(a, arguments_json),
371 Output::Flops(tree) => flops_json(tree),
372 Output::Envelope(frames) => list(frames, |f: &EnvelopeFrame| {
373 format!(
374 "\n {{ \"t\": {}, \"rms\": {}, \"peak\": {} }}",
375 num(f.t_secs),
376 num(f.rms),
377 num(f.peak)
378 )
379 }),
380 Output::Pitch(frames) => list(frames, |f| {
381 format!(
382 "\n {{ \"t\": {}, \"notes\": {} }}",
383 num(f.t_secs),
384 list(&f.notes, |n| format!(
385 "{{ \"note\": \"{}\", \"hz\": {}, \"cents\": {}, \"db\": {}, \
386 \"harmonic_of\": {} }}",
387 escape(&n.name),
388 num(n.hz),
389 num(n.cents),
390 num(n.db),
391 maybe(n.harmonic_of)
392 ))
393 )
394 }),
395 Output::Formants(frames) => list(frames, formants_json),
396 }
397}
398
399fn line_json(l: &sva_engine::Line) -> String {
400 format!(
401 "\n {{ \"hz\": {}, \"re\": {}, \"im\": {}, \"db\": {} }}",
402 num(l.hz),
403 num(l.amp.re),
404 num(l.amp.im),
405 num(20.0 * l.amp.abs().log10())
406 )
407}
408
409pub fn answer_json(answer: &Answer, limit: Option<usize>, skim: bool) -> String {
413 format!(
414 "{{ \"source\": \"{}\", \"profile\": \"{}\", \"rate\": {}, \"value\": {}, \
415 \"dropped\": {}, \"tail_db\": {} }}",
416 match answer.source {
417 Source::Exact => "exact",
418 Source::Measured => "measured",
419 },
420 escape(answer.profile),
421 answer
422 .rate
423 .map_or_else(|| NONE.to_string(), |r| r.to_string()),
424 value_json(&answer.value, limit, skim),
425 list(&answer.dropped, line_json),
426 maybe(answer.tail_db)
427 )
428}
429
430fn detail_json(detail: &Detail) -> String {
432 match detail {
433 Detail::Lines {
434 placed,
435 summed,
436 dropped,
437 dropped_more,
438 terms,
439 tail_db,
440 ..
441 } => format!(
442 ", \"placed\": {placed}, \"summed\": {summed}, \"dropped\": {}, \
443 \"dropped_more\": {dropped_more}, \"terms\": {}, \"tail_db\": {}",
444 list(dropped, |d| format!(
445 "{{ \"hz\": {}, \"db\": {} }}",
446 num(d.hz),
447 num(d.db)
448 )),
449 counted(*terms),
450 maybe(*tail_db)
451 ),
452 Detail::Cropped { tail_db, .. } => format!(", \"tail_db\": {}", maybe(*tail_db)),
453 Detail::Point { alias_db, .. } => format!(", \"alias_db\": {}", maybe(*alias_db)),
454 Detail::Spectrum { wrap_db, .. } => format!(", \"wrap_db\": {}", num(*wrap_db)),
455 Detail::Roundtrip { edited, .. } => format!(", \"edited\": {edited}"),
456 Detail::Continuous { .. } | Detail::Reading { .. } => String::new(),
457 Detail::Added { parts } => format!(
458 ", \"addends\": {}",
459 list(parts, |part| format!(
460 "{{ \"rule\": \"{}\"{} }}",
461 part.rule().as_str(),
462 detail_json(part)
463 ))
464 ),
465 }
466}
467
468pub fn label_json(label: &Label) -> String {
471 let detail = detail_json(&label.detail);
472 let cost = match label.cost {
473 Some(Cost { flops, budget }) => format!(", \"flops\": {flops}, \"flop_budget\": {budget}"),
474 None => format!(", \"flops\": {NONE}, \"flop_budget\": {NONE}"),
475 };
476 format!(
477 "{{ \"source\": \"{}\", \"profile\": \"{}\", \"rate\": {}, \"rule\": \"{}\"{detail}{cost} }}",
478 match label.source {
479 Source::Exact => "exact",
480 Source::Measured => "measured",
481 },
482 escape(label.profile),
483 label.rate,
484 escape(label.rule().as_str())
485 )
486}
487
488fn cache_json(cache: Option<&CacheReport>) -> String {
489 match cache {
490 Some(c) => format!(
491 "{{ \"dir\": \"{}\", \"held_bytes\": {}, \"max_bytes\": {}, \
492 \"evicted_bytes\": {}, \"faults\": {}, \"stats\": {} }}",
493 escape(&c.dir),
494 c.held_bytes,
495 c.max_bytes,
496 c.evicted_bytes,
497 c.faults,
498 stats_json(&c.stats)
499 ),
500 None => NONE.to_string(),
501 }
502}
503
504pub fn stats_json(stats: &CacheStats) -> String {
507 let lookups = list(&stats.lookups, |l| {
508 let (outcome, tier) = match l.outcome {
509 Outcome::Hit(tier) => ("hit", format!(", \"tier\": \"{}\"", tier_name(tier))),
510 Outcome::ComputedStored => ("computed_stored", String::new()),
511 Outcome::ComputedNotStored => ("computed_not_stored", String::new()),
512 Outcome::ComputedSlotted => ("computed_slotted", String::new()),
513 Outcome::ComputedReplaced => ("computed_replaced", String::new()),
514 };
515 format!(
516 "{{ \"node\": \"{}\", \"key\": \"{}\", \"kind\": \"{}\", \"outcome\": \"{outcome}\"{tier} }}",
517 escape(&l.node),
518 l.key,
519 match l.kind {
520 PayloadKind::Samples => "samples",
521 PayloadKind::Frames => "frames",
522 PayloadKind::Symbolic => "symbolic",
523 }
524 )
525 });
526 format!(
527 "{{ \"nodes\": {}, \"hits\": {{ \"memory\": {}, \"persistent\": {}, \"volatile\": {} }}, \
528 \"computed\": {}, \"stored\": {}, \"slotted\": {}, \"replaced\": {}, \
529 \"lookups\": {lookups} }}",
530 stats.nodes(),
531 stats.hits_in(Tier::Memory),
532 stats.hits_in(Tier::Persistent),
533 stats.hits_in(Tier::Volatile),
534 stats.computed(),
535 stats.stored(),
536 stats.slotted(),
537 stats.replaced()
538 )
539}
540
541fn tier_name(tier: Tier) -> &'static str {
542 match tier {
543 Tier::Memory => "memory",
544 Tier::Persistent => "persistent",
545 Tier::Volatile => "volatile",
546 }
547}
548
549pub struct Report<'a> {
551 pub target: &'a str,
552 pub rate: u32,
553 pub horizon: Horizon,
554 pub profile: &'a str,
555 pub label: Option<&'a Label>,
556 pub written: &'a [(String, &'a Path)],
557 pub cache: Option<&'a CacheReport>,
558 pub answers: &'a [(String, Answer)],
559 pub analyses: &'a [(String, String)],
562 pub limit: Option<usize>,
563 pub skim: bool,
564}
565
566pub fn query_data(report: &Report) -> String {
568 let written = list(report.written, |(name, path)| {
569 format!(
570 "{{ \"as\": \"{}\", \"path\": \"{}\" }}",
571 escape(name),
572 escape(&path.display().to_string())
573 )
574 });
575 let label = report
576 .label
577 .map(label_json)
578 .unwrap_or_else(|| NONE.to_string());
579 let reads = report
580 .answers
581 .iter()
582 .map(|(name, answer)| {
583 format!(
584 "\"{}\": {}",
585 escape(name),
586 answer_json(answer, report.limit, report.skim)
587 )
588 })
589 .chain(report.analyses.iter().map(|(name, value)| {
590 format!(
591 "\"{}\": {{ \"source\": \"measured\", \"profile\": \"{}\", \"rate\": {}, \
592 \"value\": {value} }}",
593 escape(name),
594 escape(report.profile),
595 report.rate
596 )
597 }))
598 .collect::<Vec<_>>()
599 .join(",\n ");
600 let tail = match reads.is_empty() {
601 true => String::new(),
602 false => format!(",\n {reads}"),
603 };
604 format!(
605 "{{\n \"target\": \"{}\",\n \"sample_rate\": {},\n \"profile\": \"{}\",\n \
606 \"window\": {{ \"start_secs\": {}, \"end_secs\": {} }},\n \"label\": {label},\n \
607 \"written\": {written},\n \"cache\": {}{tail}\n}}",
608 escape(report.target),
609 report.rate,
610 escape(report.profile),
611 num(report.horizon.start_secs),
612 num(report.horizon.end_secs),
613 cache_json(report.cache)
614 )
615}