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 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}