Skip to main content

processing_benchmark/
processing_benchmark.rs

1use std::alloc::{GlobalAlloc, Layout, System};
2use std::env;
3use std::fs::{self, File};
4use std::hint::black_box;
5use std::io::{self, Read, Write};
6use std::path::PathBuf;
7use std::process::Command;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicIsize, Ordering};
10use std::time::Instant;
11
12use zirium::dialect::DialectRegistry;
13use zirium::parser::{ParseLimits, ParsedFile};
14use zirium::printer::PrintLayout;
15use zirium::semantic::{
16    AttributeSpec, AttributeValue, LoweringMode, RetentionProfile,
17    lower_with_dialect_registry_and_retention,
18};
19
20const MIB: usize = 1024 * 1024;
21const SEED: u64 = 0x5a49_5249_554d_0028;
22
23struct CountingAllocator;
24static LIVE: AtomicIsize = AtomicIsize::new(0);
25static PEAK: AtomicIsize = AtomicIsize::new(0);
26
27unsafe impl GlobalAlloc for CountingAllocator {
28    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
29        let pointer = unsafe { System.alloc(layout) };
30        if !pointer.is_null() {
31            record(layout.size() as isize);
32        }
33        pointer
34    }
35    unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
36        LIVE.fetch_sub(layout.size() as isize, Ordering::Relaxed);
37        unsafe { System.dealloc(pointer, layout) }
38    }
39    unsafe fn realloc(&self, pointer: *mut u8, old: Layout, size: usize) -> *mut u8 {
40        let result = unsafe { System.realloc(pointer, old, size) };
41        if !result.is_null() {
42            record(size as isize - old.size() as isize);
43        }
44        result
45    }
46}
47
48#[global_allocator]
49static ALLOCATOR: CountingAllocator = CountingAllocator;
50
51fn record(delta: isize) {
52    let live = LIVE.fetch_add(delta, Ordering::Relaxed) + delta;
53    PEAK.fetch_max(live, Ordering::Relaxed);
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57enum Shape {
58    Primary,
59    BlockRich,
60    Trivia,
61    Nested,
62    Payload,
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66enum Stage {
67    Parse,
68    Traverse,
69    SyntaxPayload,
70    SyntaxOperationPayload,
71    SyntaxOperationTraverse,
72    OperationPayload,
73    OperationTraverse,
74    Lower,
75    Verify,
76    Canonical,
77    UseIndex,
78    SymbolIndex,
79    DominanceIndex,
80    Editor,
81    Preserving,
82    DirtyPreserving,
83}
84
85struct Args {
86    bytes: usize,
87    shape: Shape,
88    depth: Option<usize>,
89    stages: Vec<Stage>,
90    warmups: usize,
91    runs: usize,
92    smoke: bool,
93}
94
95#[derive(Clone)]
96struct Measurement {
97    ns: u128,
98    peak: usize,
99    output: usize,
100    checksum: u64,
101    stats: String,
102}
103
104#[derive(Default)]
105struct ByteCounter(usize);
106
107impl Write for ByteCounter {
108    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
109        self.0 += bytes.len();
110        Ok(bytes.len())
111    }
112    fn flush(&mut self) -> io::Result<()> {
113        Ok(())
114    }
115}
116
117fn main() {
118    if env::args().nth(1).as_deref() == Some("--report") {
119        report();
120        return;
121    }
122    let args = parse_args();
123    print_environment(&args);
124    let path = temp_path();
125    let source = generate(args.shape, args.bytes, args.depth);
126    assert_eq!(source.len(), args.bytes);
127    fs::write(&path, &source).expect("write temporary fixture");
128    drop(source);
129    println!(
130        "fixture_storage=temporary generated_bytes={} seed=0x{SEED:016x}",
131        args.bytes
132    );
133    for stage in args.stages {
134        for _ in 0..args.warmups {
135            black_box(measure(stage, args.shape, args.depth, &path));
136        }
137        let mut samples = (0..args.runs)
138            .map(|_| measure(stage, args.shape, args.depth, &path))
139            .collect::<Vec<_>>();
140        samples.sort_by_key(|sample| sample.ns);
141        let sample = samples[samples.len() / 2].clone();
142        let mib_s = args.bytes as f64 / MIB as f64 / (sample.ns as f64 / 1e9);
143        println!(
144            "measurement stage={} shape={} depth={} input_bytes={} median_ns={} throughput_mib_s={mib_s:.3} peak_live_bytes={} output_bytes={} checksum={} {}",
145            stage.name(),
146            args.shape.name(),
147            args.depth
148                .map_or_else(|| "none".into(), |depth| depth.to_string()),
149            args.bytes,
150            sample.ns,
151            sample.peak,
152            sample.output,
153            sample.checksum,
154            sample.stats
155        );
156    }
157    fs::remove_file(path).expect("remove temporary fixture");
158}
159
160fn parse_args() -> Args {
161    let mut bytes = 10 * MIB;
162    let mut shape = Shape::Primary;
163    let mut depth = None;
164    let mut stages = vec![Stage::Parse];
165    let mut warmups = 1;
166    let mut runs = 3;
167    let mut smoke = false;
168    let mut it = env::args().skip(1);
169    while let Some(arg) = it.next() {
170        match arg.as_str() {
171            "--smoke" => {
172                smoke = true;
173                bytes = 64 * 1024;
174                warmups = 1;
175                runs = 1;
176            }
177            "--size-mib" => {
178                bytes = it
179                    .next()
180                    .expect("size")
181                    .parse::<usize>()
182                    .expect("integer size")
183                    * MIB
184            }
185            "--size-kib" => {
186                bytes = it
187                    .next()
188                    .expect("size")
189                    .parse::<usize>()
190                    .expect("integer size")
191                    * 1024
192            }
193            "--shape" => shape = Shape::parse(&it.next().expect("shape")),
194            "--depth" => depth = Some(it.next().expect("depth").parse().expect("integer depth")),
195            "--stage" => {
196                stages = it
197                    .next()
198                    .expect("stage")
199                    .split(',')
200                    .map(Stage::parse)
201                    .collect()
202            }
203            "--runs" => runs = it.next().expect("runs").parse().expect("integer runs"),
204            "--warmups" => {
205                warmups = it
206                    .next()
207                    .expect("warmups")
208                    .parse()
209                    .expect("integer warmups")
210            }
211            _ => panic!("unknown argument {arg}"),
212        }
213    }
214    assert!(runs > 0);
215    assert_eq!(
216        shape == Shape::Nested,
217        depth.is_some(),
218        "--depth is required exactly when --shape nested is selected"
219    );
220    assert!(depth != Some(0), "depth must be positive");
221    Args {
222        bytes,
223        shape,
224        depth,
225        stages,
226        warmups,
227        runs,
228        smoke,
229    }
230}
231
232fn measure(stage: Stage, shape: Shape, depth: Option<usize>, path: &PathBuf) -> Measurement {
233    let registry = DialectRegistry::baseline();
234    let mut limits = ParseLimits::default();
235    if let Some(depth) = depth {
236        limits.max_delimiter_depth = limits.max_delimiter_depth.max(depth.saturating_add(8));
237    }
238    let mut bytes = Vec::new();
239    File::open(path).unwrap().read_to_end(&mut bytes).unwrap();
240    let mut parsed = if stage == Stage::Parse {
241        None
242    } else {
243        Some(
244            ParsedFile::parse_with_limits_and_registry(
245                Arc::<[u8]>::from(bytes.clone()),
246                limits,
247                registry,
248            )
249            .unwrap(),
250        )
251    };
252    let retention = if shape == Shape::Primary
253        && matches!(
254            stage,
255            Stage::Lower | Stage::Preserving | Stage::DirtyPreserving
256        ) {
257        RetentionProfile::Hybrid
258    } else {
259        RetentionProfile::SemanticOnly
260    };
261    let mut document = if matches!(
262        stage,
263        Stage::Lower
264            | Stage::Parse
265            | Stage::Traverse
266            | Stage::SyntaxPayload
267            | Stage::SyntaxOperationPayload
268            | Stage::SyntaxOperationTraverse
269    ) {
270        None
271    } else {
272        Some(
273            lower_with_dialect_registry_and_retention(
274                parsed.as_ref().unwrap(),
275                LoweringMode::Strict,
276                retention,
277                registry,
278            )
279            .document
280            .expect("strict lowering"),
281        )
282    };
283    if stage == Stage::DirtyPreserving {
284        let document = document.as_mut().unwrap();
285        let operations = document
286            .operations()
287            .filter(|operation| {
288                document
289                    .operation_regions(*operation)
290                    .unwrap_or(&[])
291                    .is_empty()
292            })
293            .collect::<Vec<_>>();
294        let mut editor = document.edit(registry).unwrap();
295        for operation in operations {
296            editor
297                .set_attribute(
298                    operation,
299                    AttributeSpec {
300                        name: "bench_dirty".into(),
301                        spelling: "\"yes\"".into(),
302                        value: AttributeValue::String("\"yes\"".into()),
303                    },
304                )
305                .unwrap();
306        }
307        editor.commit().unwrap();
308    }
309    let baseline = LIVE.load(Ordering::Relaxed);
310    PEAK.store(baseline, Ordering::Relaxed);
311    let started = Instant::now();
312    let mut output = 0;
313    let mut checksum = 0_u64;
314    match stage {
315        Stage::Parse => {
316            parsed = Some(
317                ParsedFile::parse_with_limits_and_registry(
318                    Arc::<[u8]>::from(bytes),
319                    limits,
320                    registry,
321                )
322                .unwrap(),
323            )
324        }
325        Stage::Traverse => {
326            for node in parsed
327                .as_ref()
328                .unwrap()
329                .syntax()
330                .tree()
331                .subtree(parsed.as_ref().unwrap().syntax().tree().root())
332                .unwrap()
333            {
334                let tree = parsed.as_ref().unwrap().syntax().tree();
335                checksum = checksum
336                    .wrapping_mul(0x100000001b3)
337                    .wrapping_add(tree.kind(node).unwrap() as u64)
338                    .wrapping_add(tree.token_indices(node).unwrap().len() as u64);
339            }
340        }
341        Stage::SyntaxPayload => {
342            let tree = parsed.as_ref().unwrap().syntax().tree();
343            let mut node_kind = Vec::with_capacity(tree.node_count() * 2);
344            let mut node_start = Vec::with_capacity(tree.node_count() * 4);
345            let mut node_end = Vec::with_capacity(tree.node_count() * 4);
346            let mut node_subtree_end = Vec::with_capacity(tree.node_count() * 4);
347            let mut node_flags = Vec::with_capacity(tree.node_count());
348            for index in 0..tree.node_count() {
349                let id = tree.node(index).unwrap();
350                node_kind.extend_from_slice(&(tree.kind(id).unwrap() as u16).to_ne_bytes());
351                node_start.extend_from_slice(
352                    &tree
353                        .text_range(id)
354                        .map_or(u32::MAX, |r| r.start())
355                        .to_ne_bytes(),
356                );
357                node_end.extend_from_slice(
358                    &tree
359                        .text_range(id)
360                        .map_or(u32::MAX, |r| r.end())
361                        .to_ne_bytes(),
362                );
363                node_subtree_end
364                    .extend_from_slice(&(tree.subtree_end(id).unwrap() as u32).to_ne_bytes());
365                node_flags.push(u8::from(tree.has_error(id).unwrap()));
366            }
367            let mut token_kind = Vec::with_capacity(tree.token_count() * 2);
368            let mut token_start = Vec::with_capacity(tree.token_count() * 4);
369            let mut token_end = Vec::with_capacity(tree.token_count() * 4);
370            for index in 0..tree.token_count() {
371                let token = tree.token(index).unwrap();
372                token_kind.extend_from_slice(&(token.kind() as u16).to_ne_bytes());
373                token_start.extend_from_slice(&token.range().start().to_ne_bytes());
374                token_end.extend_from_slice(&token.range().end().to_ne_bytes());
375            }
376            output = node_kind.len()
377                + node_start.len()
378                + node_end.len()
379                + node_subtree_end.len()
380                + node_flags.len()
381                + token_kind.len()
382                + token_start.len()
383                + token_end.len();
384            checksum = node_kind
385                .iter()
386                .chain(&token_kind)
387                .fold(0_u64, |sum, byte| sum.wrapping_add(*byte as u64));
388            black_box((
389                node_kind,
390                node_start,
391                node_end,
392                node_subtree_end,
393                node_flags,
394                token_kind,
395                token_start,
396                token_end,
397            ));
398        }
399        Stage::SyntaxOperationTraverse => {
400            for operation in parsed.as_ref().unwrap().syntax().file().operations() {
401                checksum = checksum
402                    .wrapping_add(operation.id().index() as u64)
403                    .wrapping_add(operation.results().count() as u64)
404                    .wrapping_add(operation.operands().count() as u64)
405                    .wrapping_add(operation.successors().count() as u64)
406                    .wrapping_add(operation.regions().count() as u64);
407            }
408        }
409        Stage::SyntaxOperationPayload => {
410            let operations = parsed
411                .as_ref()
412                .unwrap()
413                .syntax()
414                .file()
415                .operations()
416                .collect::<Vec<_>>();
417            let result_count = operations.iter().map(|op| op.results().count()).sum();
418            let operand_count = operations.iter().map(|op| op.operands().count()).sum();
419            let successor_count = operations.iter().map(|op| op.successors().count()).sum();
420            let region_count = operations.iter().map(|op| op.regions().count()).sum();
421            let mut columns = [
422                Vec::with_capacity(operations.len()),
423                Vec::with_capacity(operations.len() + 1),
424                Vec::with_capacity(result_count),
425                Vec::with_capacity(operations.len() + 1),
426                Vec::with_capacity(operand_count),
427                Vec::with_capacity(operations.len() + 1),
428                Vec::with_capacity(successor_count),
429                Vec::with_capacity(operations.len() + 1),
430                Vec::with_capacity(region_count),
431            ];
432            for index in [1, 3, 5, 7] {
433                columns[index].push(0);
434            }
435            for operation in operations {
436                columns[0].push(operation.id().index() as u32);
437                columns[2].extend(operation.results().map(|node| node.id().index() as u32));
438                let end = columns[2].len() as u32;
439                columns[1].push(end);
440                columns[4].extend(operation.operands().map(|node| node.id().index() as u32));
441                let end = columns[4].len() as u32;
442                columns[3].push(end);
443                columns[6].extend(operation.successors().map(|node| node.id().index() as u32));
444                let end = columns[6].len() as u32;
445                columns[5].push(end);
446                columns[8].extend(operation.regions().map(|node| node.id().index() as u32));
447                let end = columns[8].len() as u32;
448                columns[7].push(end);
449            }
450            checksum = columns
451                .iter()
452                .flatten()
453                .fold(0_u64, |sum, value| sum.wrapping_add(*value as u64));
454            output = columns
455                .iter()
456                .map(|column| column.len() * size_of::<u32>())
457                .sum();
458            black_box(columns);
459        }
460        Stage::Lower => {
461            document = Some(
462                lower_with_dialect_registry_and_retention(
463                    parsed.as_ref().unwrap(),
464                    LoweringMode::Strict,
465                    retention,
466                    registry,
467                )
468                .document
469                .expect("strict lowering"),
470            )
471        }
472        Stage::OperationPayload => {
473            let doc = document.as_ref().unwrap();
474            let mut names = std::collections::HashMap::<u32, u32>::new();
475            let mut distinct = Vec::new();
476            let mut columns = Vec::<u32>::new();
477            for id in doc.operations() {
478                let stored = doc.operation_name_index(id).unwrap();
479                let next = distinct.len() as u32;
480                let code = *names.entry(stored).or_insert_with(|| {
481                    distinct.push(stored);
482                    next
483                });
484                let range = doc.operation_source_range(id).unwrap();
485                columns.extend([code, range.start(), range.end()]);
486                checksum = checksum.wrapping_add(code as u64);
487            }
488            let name_bytes: usize = distinct
489                .iter()
490                .map(|&index| doc.string_at(index).unwrap().len())
491                .sum();
492            output = columns.len() * std::mem::size_of::<u32>()
493                + doc.operations().count()
494                + (distinct.len() + 1) * std::mem::size_of::<u32>()
495                + name_bytes;
496            black_box(columns);
497        }
498        Stage::OperationTraverse => {
499            let doc = document.as_ref().unwrap();
500            for id in doc.operations() {
501                checksum = checksum
502                    .wrapping_add(doc.operation_name_index(id).unwrap() as u64)
503                    .wrapping_add(doc.operation_source_range(id).unwrap().start() as u64);
504            }
505        }
506        Stage::Verify => document
507            .as_ref()
508            .unwrap()
509            .verify_semantics(registry)
510            .unwrap(),
511        Stage::Canonical => {
512            let mut sink = ByteCounter::default();
513            document
514                .as_ref()
515                .unwrap()
516                .write_canonical(&mut sink, PrintLayout::Compact)
517                .unwrap();
518            output = sink.0;
519        }
520        Stage::UseIndex => {
521            let doc = document.as_ref().unwrap();
522            if let Some((_op, value)) = doc.operations().find_map(|op| {
523                doc.operation(op)
524                    .unwrap()
525                    .result(op, 0)
526                    .map(|value| (op, value))
527            }) {
528                black_box(doc.uses(value));
529            }
530        }
531        Stage::SymbolIndex => {
532            black_box(
533                document
534                    .as_ref()
535                    .unwrap()
536                    .symbol_index_diagnostics(registry),
537            );
538        }
539        Stage::DominanceIndex => {
540            let doc = document.as_ref().unwrap();
541            let ops = doc.operations().collect::<Vec<_>>();
542            if let (Some((_, value)), Some(last)) = (
543                ops.iter().find_map(|op| {
544                    doc.operation(*op)
545                        .unwrap()
546                        .result(*op, 0)
547                        .map(|value| (*op, value))
548                }),
549                ops.last(),
550            ) {
551                black_box(doc.dominates(value, *last, registry));
552            }
553        }
554        Stage::Editor => {
555            let doc = document.as_mut().unwrap();
556            let mut editor = doc.edit(registry).unwrap();
557            if shape == Shape::BlockRich {
558                black_box(editor.compact_pools());
559            } else {
560                let leaf = editor
561                    .document()
562                    .operations()
563                    .find(|op| editor.document().operation_name(*op) == Some("bench.op"))
564                    .expect("editable leaf");
565                editor.erase(leaf).unwrap();
566            }
567            editor.commit().unwrap();
568        }
569        Stage::Preserving | Stage::DirtyPreserving => {
570            let doc = document.as_ref().unwrap();
571            let mut sink = ByteCounter::default();
572            doc.write_preserving(&mut sink, PrintLayout::Compact)
573                .unwrap();
574            output = sink.0;
575        }
576    }
577    let elapsed = started.elapsed();
578    let peak = PEAK.load(Ordering::Relaxed).saturating_sub(baseline) as usize;
579    let stats = document.as_ref().map(|doc| { let s=doc.statistics(); format!("operations={} regions={} blocks={} pooled_entries={} direct_owned_bytes={} document_index_bytes={} retained_source_bytes={} retained_cst_nodes={} retained_cst_bytes={} source_storage_shared={} cst_storage_shared={} retained_mapping_entries={} payload_blob_bytes={} use_index_entries={} symbol_index_entries={} dominance_index_entries={}",s.operations,s.regions,s.blocks,s.pooled_list_entries,s.direct_owned_bytes,s.document_index_bytes,s.retained_source_bytes,s.retained_cst_nodes,s.retained_cst_bytes,s.source_storage_shared,s.cst_storage_shared,s.retained_mapping_entries,s.payload_blob_bytes,s.use_index_entries,s.symbol_index_entries,s.dominance_index_entries) }).unwrap_or_else(|| { let tree=parsed.as_ref().map(|p|p.syntax().tree()); format!("cst_nodes={} cst_tokens={} exact_cst_retained_bytes={}",tree.map_or(0,|t|t.node_count()),tree.map_or(0,|t|t.token_count()),tree.map_or(0,|t|t.exact_retained_bytes())) });
580    black_box(checksum);
581    Measurement {
582        ns: elapsed.as_nanos(),
583        peak,
584        output,
585        checksum,
586        stats,
587    }
588}
589
590fn generate(shape: Shape, bytes: usize, depth: Option<usize>) -> Vec<u8> {
591    if shape == Shape::Primary {
592        return primary_fixture(bytes);
593    }
594    if shape == Shape::BlockRich {
595        return block_rich_fixture(bytes);
596    }
597    if shape == Shape::Nested {
598        return nested_fixture(bytes, depth.expect("validated nested depth"));
599    }
600    if shape == Shape::Payload {
601        return payload_fixture(bytes);
602    }
603    let mut out = match shape {
604        Shape::Primary => b"\"bench.container\"() ({\n^bb:\n%seed = \"bench.source\"() : () -> i32\n\"bench.use\"(%seed) : (i32) -> ()\n".to_vec(),
605        Shape::BlockRich => b"builtin.module { func.func @bench() {\n^entry:\n".to_vec(),
606        Shape::Trivia => b"builtin.module {\n".to_vec(),
607        Shape::Nested => b"builtin.module {\n".to_vec(),
608        Shape::Payload => b"builtin.module {\n".to_vec(),
609    };
610    let close: &[u8] = match shape {
611        Shape::Primary => b"}) : () -> ()\n",
612        Shape::BlockRich => b"func.return\n} }\n",
613        Shape::Trivia | Shape::Nested | Shape::Payload => b"}\n",
614    };
615    let line: &[u8] = match shape {
616        Shape::Primary | Shape::BlockRich => b"\"bench.op\"() {tag = \"zirium\"} : () -> () // deterministic primary operation padding xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n",
617        Shape::Trivia => b"// deterministic trivia xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\n",
618        Shape::Nested => b"\"bench.region\"() ({ \"bench.op\"() : () -> () }) : () -> ()\n",
619        Shape::Payload => b"\"bench.payload\"() {value = #vendor.attr<\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\">} : () -> ()\n",
620    };
621    assert!(bytes >= out.len() + close.len());
622    while out.len() + close.len() + line.len() <= bytes {
623        out.extend_from_slice(line);
624    }
625    let padding = bytes - out.len() - close.len();
626    if padding > 0 {
627        out.extend(std::iter::repeat_n(b' ', padding));
628    }
629    out.extend_from_slice(close);
630    out
631}
632
633fn primary_fixture(bytes: usize) -> Vec<u8> {
634    let prefix = b"\"bench.container\"() ({\n^bb:\n%seed = \"bench.source\"() : () -> i32\n\"bench.use\"(%seed) : (i32) -> ()\n";
635    let suffix = b"}) : () -> ()\n";
636    let mut line =
637        b"\"bench.op\"() {tag = \"zirium\"} : () -> () // deterministic operation ".to_vec();
638    line.extend(std::iter::repeat_n(b'x', 440));
639    line.push(b'\n');
640    let mut out = Vec::with_capacity(bytes);
641    out.extend_from_slice(prefix);
642    while out.len() + line.len() + suffix.len() <= bytes {
643        out.extend_from_slice(&line);
644    }
645    out.extend(std::iter::repeat_n(b' ', bytes - out.len() - suffix.len()));
646    out.extend_from_slice(suffix);
647    out
648}
649
650fn nested_fixture(bytes: usize, depth: usize) -> Vec<u8> {
651    let open = b"\"bench.region\"() ({\n";
652    let close = b"}) : () -> ()\n";
653    let mut out = Vec::with_capacity(bytes);
654    for _ in 0..depth {
655        out.extend_from_slice(open);
656    }
657    out.extend_from_slice(b"\"bench.op\"() : () -> ()\n");
658    let closing = close.len() * depth;
659    assert!(
660        out.len() + closing <= bytes,
661        "requested depth does not fit fixture size"
662    );
663    out.extend(std::iter::repeat_n(b' ', bytes - out.len() - closing));
664    for _ in 0..depth {
665        out.extend_from_slice(close);
666    }
667    out
668}
669
670fn block_rich_fixture(bytes: usize) -> Vec<u8> {
671    let mut out = b"builtin.module {\n".to_vec();
672    let close = b"}\n";
673    let mut index = 0;
674    loop {
675        let function = format!(
676            "func.func @f{index}() {{\n^entry:\n%value = arith.constant 1 : i32\ncf.br ^middle\n^middle:\n\"bench.use\"(%value) : (i32) -> ()\ncf.br ^exit\n^exit:\nfunc.return\n}}\n"
677        );
678        if out.len() + function.len() + close.len() > bytes {
679            break;
680        }
681        out.extend_from_slice(function.as_bytes());
682        index += 1;
683    }
684    out.extend(std::iter::repeat_n(b' ', bytes - out.len() - close.len()));
685    out.extend_from_slice(close);
686    out
687}
688
689fn payload_fixture(bytes: usize) -> Vec<u8> {
690    let prefix = b"\"bench.payload\"() {value = #vendor.attr<\"";
691    let suffix = b"\">} : () -> ()\n";
692    assert!(bytes >= prefix.len() + suffix.len());
693    let mut out = Vec::with_capacity(bytes);
694    out.extend_from_slice(prefix);
695    out.extend(std::iter::repeat_n(
696        b'x',
697        bytes - prefix.len() - suffix.len(),
698    ));
699    out.extend_from_slice(suffix);
700    out
701}
702
703fn temp_path() -> PathBuf {
704    env::temp_dir().join(format!(
705        "zirium-processing-{}-{}.mlir",
706        std::process::id(),
707        SEED
708    ))
709}
710
711fn print_environment(args: &Args) {
712    println!(
713        "benchmark=processing profile=release warmups={} measured_runs={} smoke={} shape={} depth={} requested_bytes={}",
714        args.warmups,
715        args.runs,
716        args.smoke,
717        args.shape.name(),
718        args.depth
719            .map_or_else(|| "none".into(), |depth| depth.to_string()),
720        args.bytes
721    );
722    println!(
723        "rustc={} target={} os={} cpu={}",
724        command("rustc", &["-V"]),
725        command("rustc", &["-vV"])
726            .lines()
727            .find_map(|l| l.strip_prefix("host: "))
728            .unwrap_or("unknown"),
729        command("uname", &["-srv"]),
730        if cfg!(target_os = "macos") {
731            command("sysctl", &["-n", "machdep.cpu.brand_string"])
732        } else {
733            "unknown".into()
734        }
735    );
736}
737
738fn command(program: &str, args: &[&str]) -> String {
739    Command::new(program)
740        .args(args)
741        .output()
742        .ok()
743        .filter(|o| o.status.success())
744        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_owned())
745        .unwrap_or_else(|| "unknown".into())
746}
747
748impl Shape {
749    fn parse(s: &str) -> Self {
750        match s {
751            "primary" => Self::Primary,
752            "block-rich" => Self::BlockRich,
753            "trivia" => Self::Trivia,
754            "nested" => Self::Nested,
755            "payload" => Self::Payload,
756            _ => panic!("unknown shape {s}"),
757        }
758    }
759    fn name(self) -> &'static str {
760        match self {
761            Self::Primary => "primary",
762            Self::BlockRich => "block-rich",
763            Self::Trivia => "trivia",
764            Self::Nested => "nested",
765            Self::Payload => "payload",
766        }
767    }
768}
769impl Stage {
770    fn parse(s: &str) -> Self {
771        match s {
772            "parse" => Self::Parse,
773            "traverse" => Self::Traverse,
774            "syntax-payload" => Self::SyntaxPayload,
775            "syntax-operation-payload" => Self::SyntaxOperationPayload,
776            "syntax-operation-traverse" => Self::SyntaxOperationTraverse,
777            "operation-payload" => Self::OperationPayload,
778            "operation-traverse" => Self::OperationTraverse,
779            "lower" => Self::Lower,
780            "verify" => Self::Verify,
781            "canonical" => Self::Canonical,
782            "use-index" => Self::UseIndex,
783            "symbol-index" => Self::SymbolIndex,
784            "dominance-index" => Self::DominanceIndex,
785            "editor" => Self::Editor,
786            "preserving" => Self::Preserving,
787            "dirty-preserving" => Self::DirtyPreserving,
788            "all" => panic!("use comma-separated stage names"),
789            _ => panic!("unknown stage {s}"),
790        }
791    }
792    fn name(self) -> &'static str {
793        match self {
794            Self::Parse => "parse",
795            Self::Traverse => "traverse",
796            Self::SyntaxPayload => "syntax-payload",
797            Self::SyntaxOperationPayload => "syntax-operation-payload",
798            Self::SyntaxOperationTraverse => "syntax-operation-traverse",
799            Self::OperationPayload => "operation-payload",
800            Self::OperationTraverse => "operation-traverse",
801            Self::Lower => "lower",
802            Self::Verify => "verify",
803            Self::Canonical => "canonical",
804            Self::UseIndex => "use-index",
805            Self::SymbolIndex => "symbol-index",
806            Self::DominanceIndex => "dominance-index",
807            Self::Editor => "editor",
808            Self::Preserving => "preserving",
809            Self::DirtyPreserving => "dirty-preserving",
810        }
811    }
812}
813
814fn report() {
815    let mut input = String::new();
816    io::stdin().read_to_string(&mut input).unwrap();
817    println!(
818        "projection_target_mib=500 input_policy=direct_measurement_permitted_for_bounded_stages fit_points_mib=1,10,25,50 held_out_points_mib=75,100 held_out_error_limit=10_percent slope_intervals_mib=25-50,50-75,75-100 slope_deviation_limit=10_percent_of_median signal_floor=10_mib_latency>=0.005_seconds"
819    );
820    for stage in [
821        "parse",
822        "traverse",
823        "syntax-payload",
824        "operation-payload",
825        "operation-traverse",
826        "lower",
827        "verify",
828        "canonical",
829        "use-index",
830        "symbol-index",
831        "dominance-index",
832        "editor",
833        "preserving",
834        "dirty-preserving",
835    ] {
836        let mut points = Vec::new();
837        for line in input.lines().filter(|l| {
838            l.starts_with("measurement ")
839                && l.contains(&format!("stage={stage} "))
840                && l.contains("shape=primary ")
841        }) {
842            let get = |key: &str| {
843                line.split_whitespace()
844                    .find_map(|p| p.strip_prefix(&format!("{key}=")))
845                    .unwrap()
846                    .parse::<f64>()
847                    .unwrap()
848            };
849            points.push((
850                get("input_bytes") / MIB as f64,
851                get("median_ns") / 1e9,
852                get("peak_live_bytes") / MIB as f64,
853            ));
854        }
855        if points.len() != 6 {
856            println!(
857                "projection stage={stage} label=projected status=not-credible reason=requires_exactly_six_primary_points observed_points={}",
858                points.len()
859            );
860            continue;
861        }
862        points.sort_by(|a, b| a.0.total_cmp(&b.0));
863        if points.iter().map(|p| p.0 as usize).collect::<Vec<_>>() != [1, 10, 25, 50, 75, 100] {
864            println!(
865                "projection stage={stage} label=projected status=not-credible reason=requires_1_10_25_50_75_100_mib_points"
866            );
867            continue;
868        }
869        let fitted = &points[..4];
870        let (ta, tb) = fit(fitted, |p| p.1);
871        let (ma, mb) = fit(fitted, |p| p.2);
872        let time_errors = held_out_errors(&points[4..], ta, tb, |p| p.1);
873        let peak_errors = held_out_errors(&points[4..], ma, mb, |p| p.2);
874        let time_slopes = interval_slopes(&points[2..], |p| p.1);
875        let peak_slopes = interval_slopes(&points[2..], |p| p.2);
876        let time_stable = slopes_stable(time_slopes);
877        let peak_stable = slopes_stable(peak_slopes);
878        println!(
879            "fit stage={stage} trained_on_mib=1,10,25,50 time_formula_seconds={ta:.9}+{tb:.9}*MiB peak_formula_mib={ma:.6}+{mb:.6}*MiB held_out_time_error_percent={:.3},{:.3} held_out_peak_error_percent={:.3},{:.3} time_slopes_per_mib={:.9},{:.9},{:.9} peak_slopes_per_mib={:.6},{:.6},{:.6} time_slope_stable={} peak_slope_stable={}",
880            time_errors[0] * 100.0,
881            time_errors[1] * 100.0,
882            peak_errors[0] * 100.0,
883            peak_errors[1] * 100.0,
884            time_slopes[0],
885            time_slopes[1],
886            time_slopes[2],
887            peak_slopes[0],
888            peak_slopes[1],
889            peak_slopes[2],
890            time_stable,
891            peak_stable
892        );
893        let held_out_credible = time_errors
894            .iter()
895            .chain(&peak_errors)
896            .all(|error| *error <= 0.10);
897        // Sub-millisecond stages are dominated by timer and allocator noise at
898        // this scale. Retain the established 10 MiB signal floor
899        // before allowing a claim about a 500 MiB continuation.
900        let credible = held_out_credible && time_stable && peak_stable && points[1].1 >= 0.005;
901        if credible {
902            println!(
903                "projection stage={stage} label=projected status=credible target_mib=500 estimated_seconds={:.3} estimated_peak_mib={:.1} assumptions=same_fixture_mix_allocator_hardware_release_build_and_no_paging_cliff_beyond_100_mib",
904                ta + tb * 500.0,
905                ma + mb * 500.0
906            );
907        } else {
908            let reason = if points[1].1 < 0.005 {
909                "10_mib_latency_below_5ms_signal_floor"
910            } else if !held_out_credible {
911                "held_out_error_exceeds_10_percent"
912            } else {
913                "slope_deviation_exceeds_10_percent_of_median"
914            };
915            println!(
916                "projection stage={stage} label=projected status=not-credible reason={reason} assumptions=same_fixture_mix_allocator_hardware_release_build_and_no_paging_cliff_beyond_100_mib"
917            );
918        }
919    }
920}
921
922fn fit<F: Fn(&(f64, f64, f64)) -> f64>(points: &[(f64, f64, f64)], y: F) -> (f64, f64) {
923    let count = points.len() as f64;
924    let xm = points.iter().map(|p| p.0).sum::<f64>() / count;
925    let ym = points.iter().map(&y).sum::<f64>() / count;
926    let b = points.iter().map(|p| (p.0 - xm) * (y(p) - ym)).sum::<f64>()
927        / points.iter().map(|p| (p.0 - xm).powi(2)).sum::<f64>();
928    let a = ym - b * xm;
929    (a, b)
930}
931
932fn held_out_errors<F: Fn(&(f64, f64, f64)) -> f64>(
933    points: &[(f64, f64, f64)],
934    a: f64,
935    b: f64,
936    y: F,
937) -> [f64; 2] {
938    std::array::from_fn(|i| {
939        ((a + b * points[i].0) - y(&points[i])).abs() / y(&points[i]).abs().max(1e-12)
940    })
941}
942
943fn interval_slopes<F: Fn(&(f64, f64, f64)) -> f64>(points: &[(f64, f64, f64)], y: F) -> [f64; 3] {
944    std::array::from_fn(|i| (y(&points[i + 1]) - y(&points[i])) / (points[i + 1].0 - points[i].0))
945}
946
947fn slopes_stable(mut slopes: [f64; 3]) -> bool {
948    slopes.sort_by(f64::total_cmp);
949    let median = slopes[1];
950    slopes
951        .into_iter()
952        .all(|slope| (slope - median).abs() <= median.abs().max(1e-12) * 0.10)
953}