lib/machine/src/profiling/economics.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const bench = @import("bench");
  2 const coz = @import("coz");
  3 const machine = @import("machine");
  4 const os = @import("os");
  5 const std = @import("std");
  6 const sys = @import("sys");
  7 const profiling = @import("root.zig");
  8 
  9 const roots = machine.checkpoint.roots;
 10 const page_bytes = roots.page_bytes;
 11 const configured_ram_bytes = machine.InstanceRamBytes;
 12 const root_store_bytes: u64 = 96 * 1024 * 1024;
 13 const root_entry_limit: usize = 131_072;
 14 const root_lookup_limit: usize = 262_144;
 15 const maximum_dirty_bytes: usize = 4 * 1024 * 1024;
 16 const maximum_dirty_pages: usize = maximum_dirty_bytes / page_bytes;
 17 const maximum_fanout: usize = 16;
 18 const branch_page_limit: usize = 128;
 19 const allocated_block_bytes: u64 = 512;
 20 
 21 pub const CacheState = enum {
 22     cold,
 23     warm,
 24     hot,
 25 };
 26 
 27 pub const Scenario = struct {
 28     working_set_bytes: usize,
 29     dirty_bytes: usize,
 30     delta_depth: u8,
 31     branch_fanout: u8,
 32     cache_state: CacheState,
 33 };
 34 
 35 const baseline: Scenario = .{
 36     .working_set_bytes = 64 * 1024,
 37     .dirty_bytes = page_bytes,
 38     .delta_depth = 1,
 39     .branch_fanout = 1,
 40     .cache_state = .hot,
 41 };
 42 
 43 pub const scenarios = [_]Scenario{
 44     baseline,
 45     withWorkingSet(0),
 46     withWorkingSet(4 * 1024 * 1024),
 47     withWorkingSet(configured_ram_bytes),
 48     withDirtyBytes(0),
 49     withDirtyBytes(256 * 1024),
 50     withDirtyBytes(maximum_dirty_bytes),
 51     withDepth(0),
 52     withDepth(8),
 53     withDepth(32),
 54     withFanout(2),
 55     withFanout(8),
 56     withFanout(maximum_fanout),
 57     withCache(.cold),
 58     withCache(.warm),
 59 };
 60 
 61 pub const Phase = enum {
 62     checkpoint_pause,
 63     hot_capture,
 64     delta_binding,
 65     root_sync,
 66     logical_fork_latency,
 67     restore_to_first_boundary,
 68     compaction_cost,
 69 };
 70 
 71 pub const phases = std.meta.tags(Phase).*;
 72 
 73 pub const ResourceSamples = struct {
 74     pss_bytes: [profiling.fixture.sample_count]u64,
 75     pss_growth_bytes: [profiling.fixture.sample_count]u64,
 76     private_dirty_pages: [profiling.fixture.sample_count]u64,
 77     private_dirty_growth_pages: [profiling.fixture.sample_count]u64,
 78     retained_bytes: [profiling.fixture.sample_count]u64,
 79     allocated_bytes: [profiling.fixture.sample_count]u64,
 80 };
 81 
 82 const BranchIndices = [branch_page_limit]u16;
 83 const BranchPages = [branch_page_limit * page_bytes]u8;
 84 const BranchAuthentication =
 85     [roots.branch.authentication_word_count]u64;
 86 const BranchDigests = [roots.page_count]os.abi.Digest;
 87 const RootEntries = [root_entry_limit]profiling.store.Entry;
 88 const RootLookup = [root_lookup_limit]u32;
 89 const CheckpointRam = [machine.CheckpointRamBytes]u8;
 90 const DirtyPages = [maximum_dirty_bytes]u8;
 91 
 92 const Pending = struct {
 93     scenario_index: usize,
 94     phase: Phase,
 95     before: profiling.memory.Footprint,
 96     complete: bool = false,
 97 };
 98 
 99 var root_entries: RootEntries = undefined;
100 var root_lookup: RootLookup = undefined;
101 var active_store: ?profiling.store.Store = null;
102 var active_root: roots.ManifestRoot = undefined;
103 var checkpoint_storage: [2]machine.CheckpointStorage = undefined;
104 var checkpoint_ram: [2]CheckpointRam align(page_bytes) = undefined;
105 var checkpoint_values: [2]machine.Checkpoint = undefined;
106 var current_ram: CheckpointRam align(page_bytes) = undefined;
107 var active_parent: *const machine.Checkpoint = undefined;
108 var active_snapshot: ?machine.checkpoint.hot.Snapshot = null;
109 var dirty_indices: [maximum_dirty_pages]u16 = undefined;
110 var dirty_pages: DirtyPages align(page_bytes) = undefined;
111 var branch_indices: [maximum_fanout]BranchIndices align(page_bytes) = undefined;
112 var branch_pages: [maximum_fanout]BranchPages align(page_bytes) = undefined;
113 var branch_authenticated: [maximum_fanout]BranchAuthentication align(page_bytes) =
114     undefined;
115 var branch_digests: [maximum_fanout]BranchDigests align(page_bytes) = undefined;
116 var active_branches: [maximum_fanout]?roots.branch.Branch = @splat(null);
117 var instance_storage: [maximum_fanout]machine.InstanceStorage = undefined;
118 var active_instances: [maximum_fanout]?machine.Instance = @splat(null);
119 var resource_samples: [scenarios.len][phases.len]ResourceSamples = undefined;
120 var resource_counts: [scenarios.len][phases.len]u8 = @splat(@splat(0));
121 var pending: ?Pending = null;
122 
123 pub fn add(suite: *bench.Suite) !void {
124     std.debug.assert(pending == null);
125     resource_counts = @splat(@splat(0));
126     inline for (scenarios, 0..) |_, scenario_index| {
127         inline for (phases) |phase| {
128             const functions = Functions(scenario_index, phase);
129             try suite.add(
130                 benchmarkName(scenario_index, phase),
131                 functions.run,
132                 .{
133                     .prepare = functions.prepare,
134                     .teardown = functions.teardown,
135                 },
136             );
137         }
138     }
139 }
140 
141 pub fn samples(
142     scenario_index: usize,
143     phase: Phase,
144 ) *const ResourceSamples {
145     std.debug.assert(scenario_index < scenarios.len);
146     std.debug.assert(
147         resource_counts[scenario_index][@backingInt(phase)] ==
148             profiling.fixture.sample_count,
149     );
150     return &resource_samples[scenario_index][@backingInt(phase)];
151 }
152 
153 pub fn benchmarkName(
154     comptime scenario_index: usize,
155     comptime phase: Phase,
156 ) []const u8 {
157     const scenario = scenarios[scenario_index];
158     return std.fmt.comptimePrint(
159         "machine.reference.{s}.ram{d}.ws{d}.dirty{d}.depth{d}.fanout{d}.cache{s}",
160         .{
161             @tagName(phase),
162             configured_ram_bytes,
163             scenario.working_set_bytes,
164             scenario.dirty_bytes,
165             scenario.delta_depth,
166             scenario.branch_fanout,
167             @tagName(scenario.cache_state),
168         },
169     );
170 }
171 
172 fn withWorkingSet(bytes: usize) Scenario {
173     var result = baseline;
174     result.working_set_bytes = bytes;
175     return result;
176 }
177 
178 fn withDirtyBytes(bytes: usize) Scenario {
179     var result = baseline;
180     result.dirty_bytes = bytes;
181     return result;
182 }
183 
184 fn withDepth(depth: u8) Scenario {
185     var result = baseline;
186     result.delta_depth = depth;
187     return result;
188 }
189 
190 fn withFanout(fanout: u8) Scenario {
191     var result = baseline;
192     result.branch_fanout = fanout;
193     return result;
194 }
195 
196 fn withCache(cache_state: CacheState) Scenario {
197     var result = baseline;
198     result.cache_state = cache_state;
199     return result;
200 }
201 
202 fn Functions(comptime scenario_index: usize, comptime phase: Phase) type {
203     return struct {
204         fn prepare(_: std.mem.Allocator) void {
205             prepareSample(scenario_index, phase) catch |failure|
206                 fail(scenario_index, phase, "prepare", failure);
207         }
208 
209         fn run(_: std.mem.Allocator) void {
210             const measured = coz.scope(benchmarkName(scenario_index, phase));
211             runPhase(scenario_index, phase) catch |failure|
212                 fail(scenario_index, phase, "measure", failure);
213             measured.end();
214             coz.progressNamed(benchmarkName(scenario_index, phase) ++ ".complete");
215         }
216 
217         fn teardown() void {
218             finishPending() catch |failure|
219                 fail(scenario_index, phase, "observe", failure);
220             cleanup();
221         }
222     };
223 }
224 
225 fn prepareSample(scenario_index: usize, phase: Phase) !void {
226     try finishPending();
227     cleanup();
228     const scenario = scenarios[scenario_index];
229     try setup(scenario);
230     try conditionCache(scenario);
231     try prepareMeasuredPhase(scenario, phase);
232     pending = .{
233         .scenario_index = scenario_index,
234         .phase = phase,
235         .before = try footprint(),
236     };
237 }
238 
239 fn setup(scenario: Scenario) !void {
240     std.debug.assert(scenario.dirty_bytes <= maximum_dirty_bytes);
241     std.debug.assert(scenario.dirty_bytes % page_bytes == 0);
242     std.debug.assert(scenario.working_set_bytes <= configured_ram_bytes);
243     std.debug.assert(scenario.working_set_bytes % page_bytes == 0);
244     std.debug.assert(scenario.delta_depth < roots.chain_limit);
245     std.debug.assert(scenario.branch_fanout > 0);
246     std.debug.assert(scenario.branch_fanout <= maximum_fanout);
247 
248     var parent = try profiling.fixture.prepareReferenceCheckpoint();
249     const contents = try machine.checkpoint.inspect(parent);
250     active_store = try profiling.store.Store.init(
251         try profiling.fixture.economicsRootFile(),
252         &root_entries,
253         &root_lookup,
254         contents.material.receipt.block_root,
255         root_store_bytes,
256     );
257     const store = try rootStore();
258     var binding = try roots.bind(store.storage(), parent);
259     for (0..scenario.delta_depth) |depth| {
260         @memcpy(current_ram[0..], parent.ram);
261         dirtyRam(
262             current_ram[0..],
263             scenario.dirty_bytes,
264             @intCast(depth + 1),
265         );
266         const snapshot = try machine.checkpoint.hot.capture(
267             parent,
268             contents.material,
269             &current_ram,
270             try hotStorage(scenario.dirty_bytes),
271         );
272         if (snapshot.dirtyPageCount() != dirtyPageCount(scenario.dirty_bytes)) {
273             return error.EconomicsDirtyPageCountMismatch;
274         }
275         binding = try roots.bindDelta(store.storage(), binding.root, &snapshot);
276         const slot = depth % checkpoint_storage.len;
277         checkpoint_storage[slot] = machine.CheckpointStorage.init();
278         checkpoint_values[slot] = try machine.checkpoint.publish(
279             contents.material,
280             &checkpoint_storage[slot],
281             &current_ram,
282             @alignCast(checkpoint_ram[slot][0..]),
283         );
284         parent = &checkpoint_values[slot];
285     }
286     active_parent = parent;
287     active_root = binding.root;
288     @memcpy(current_ram[0..], parent.ram);
289     dirtyRam(
290         current_ram[0..],
291         scenario.dirty_bytes,
292         scenario.delta_depth + 1,
293     );
294     try store.sync();
295 }
296 
297 fn conditionCache(scenario: Scenario) !void {
298     const store = try rootStore();
299     switch (scenario.cache_state) {
300         .cold => try store.discardCache(),
301         .warm => _ = try roots.reopen(store.storage(), active_root),
302         .hot => {
303             _ = try roots.reopen(store.storage(), active_root);
304             var branch = try roots.branch.restore(
305                 store.storage(),
306                 active_root,
307                 try branchStorage(0),
308             );
309             try touchBranch(&branch, scenario.working_set_bytes);
310         },
311     }
312 }
313 
314 fn runPhase(scenario_index: usize, phase: Phase) !void {
315     const scenario = scenarios[scenario_index];
316     switch (phase) {
317         .checkpoint_pause => try checkpointPause(scenario),
318         .hot_capture => active_snapshot = try captureHot(scenario),
319         .delta_binding => try bindActiveSnapshot(),
320         .root_sync => try (try rootStore()).sync(),
321         .logical_fork_latency => try logicalFork(scenario),
322         .restore_to_first_boundary => try restoreToFirstBoundary(scenario),
323         .compaction_cost => try compactAndCollect(),
324     }
325     const state = if (pending) |*value| value else return error.EconomicsSampleMissing;
326     std.debug.assert(state.scenario_index == scenario_index);
327     std.debug.assert(state.phase == phase);
328     state.complete = true;
329 }
330 
331 fn prepareMeasuredPhase(scenario: Scenario, phase: Phase) !void {
332     switch (phase) {
333         .delta_binding => active_snapshot = try captureHot(scenario),
334         .root_sync => {
335             active_snapshot = try captureHot(scenario);
336             try bindActiveSnapshot();
337         },
338         else => {},
339     }
340 }
341 
342 fn checkpointPause(scenario: Scenario) !void {
343     active_snapshot = try captureHot(scenario);
344     try bindActiveSnapshot();
345     try (try rootStore()).sync();
346 }
347 
348 fn captureHot(scenario: Scenario) !machine.checkpoint.hot.Snapshot {
349     const material = (try machine.checkpoint.inspect(active_parent)).material;
350     return machine.checkpoint.hot.capture(
351         active_parent,
352         material,
353         &current_ram,
354         try hotStorage(scenario.dirty_bytes),
355     );
356 }
357 
358 fn bindActiveSnapshot() !void {
359     const snapshot = try currentSnapshot();
360     const binding = try roots.bindDelta(
361         (try rootStore()).storage(),
362         active_root,
363         snapshot,
364     );
365     active_root = binding.root;
366 }
367 
368 fn logicalFork(scenario: Scenario) !void {
369     const store = try rootStore();
370     for (0..scenario.branch_fanout) |index| {
371         active_branches[index] = try roots.branch.restore(
372             store.storage(),
373             active_root,
374             try branchStorage(index),
375         );
376     }
377 }
378 
379 fn restoreToFirstBoundary(scenario: Scenario) !void {
380     const store = try rootStore();
381     const input: machine.InstanceSharedRestoreInput = .{
382         .expected_root = active_root,
383         .profile = profiling.fixture.referenceProfile(),
384         .execution_manifest = profiling.fixture.referenceExecutionManifest(),
385         .fence = profiling.fixture.referenceFence(),
386     };
387     for (0..scenario.branch_fanout) |index| {
388         instance_storage[index] = machine.InstanceStorage.init();
389         active_instances[index] = switch (machine.Instance.restoreShared(
390             &instance_storage[index],
391             store.storage(),
392             try branchStorage(index),
393             input,
394         )) {
395             .ready => |value| value,
396             .unavailable => return error.ReferenceBackendUnavailable,
397             .rejected => |failure| return failure,
398         };
399         const instance = &(active_instances[index] orelse unreachable);
400         switch (try instance.run()) {
401             .doorbell => |doorbell| if (doorbell.code != .ready) {
402                 return error.UnexpectedMachineBoundary;
403             },
404             else => return error.UnexpectedMachineExit,
405         }
406         var events: machine.instance.EventBatch = undefined;
407         try instance.takeEvents(&events);
408         std.mem.doNotOptimizeAway(events);
409     }
410 }
411 
412 fn compactAndCollect() !void {
413     const store = try rootStore();
414     const binding = try roots.maintenance.compact(store.storage(), active_root);
415     active_root = binding.root;
416     try store.clearSeeds();
417     try store.seedRoot(.retained_receipt, active_root);
418     const report = try roots.maintenance.collect(
419         store.storage(),
420         try store.collectionCapacity(),
421     );
422     if (report.retained_roots != 1 or report.retained_blocks != 1) {
423         return error.EconomicsCollectionRetentionMismatch;
424     }
425     try store.sync();
426 }
427 
428 fn finishPending() !void {
429     const state = pending orelse return;
430     if (!state.complete) return error.EconomicsSampleIncomplete;
431     try verifyMeasuredPhase(scenarios[state.scenario_index], state.phase);
432     try touchMeasuredWorkingSet(scenarios[state.scenario_index], state.phase);
433     const after = try footprint();
434     const sample_index = resource_counts[state.scenario_index][
435         @backingInt(
436             state.phase,
437         )
438     ];
439     if (sample_index == profiling.fixture.sample_count) {
440         return error.EconomicsSampleCapacityExceeded;
441     }
442     const output = &resource_samples[state.scenario_index][
443         @backingInt(
444             state.phase,
445         )
446     ];
447     output.pss_bytes[sample_index] = after.pss_bytes;
448     output.pss_growth_bytes[sample_index] =
449         after.pss_bytes -| state.before.pss_bytes;
450     output.private_dirty_pages[sample_index] =
451         after.private_dirty_bytes / page_bytes;
452     output.private_dirty_growth_pages[sample_index] =
453         (after.private_dirty_bytes -| state.before.private_dirty_bytes) /
454         page_bytes;
455     output.retained_bytes[sample_index] = try retainedBytes();
456     output.allocated_bytes[sample_index] = try allocatedBytes();
457     resource_counts[state.scenario_index][@backingInt(state.phase)] += 1;
458     pending = null;
459 }
460 
461 fn verifyMeasuredPhase(scenario: Scenario, phase: Phase) !void {
462     switch (phase) {
463         .checkpoint_pause, .delta_binding, .root_sync => try verifyActiveSnapshot(scenario),
464         .hot_capture => {
465             try verifyActiveSnapshot(scenario);
466             std.mem.doNotOptimizeAway(try (try currentSnapshot()).identity());
467         },
468         else => {},
469     }
470 }
471 
472 fn verifyActiveSnapshot(scenario: Scenario) !void {
473     const snapshot = try currentSnapshot();
474     if (snapshot.dirtyPageCount() != dirtyPageCount(scenario.dirty_bytes)) {
475         return error.EconomicsDirtyPageCountMismatch;
476     }
477 }
478 
479 fn currentSnapshot() !*machine.checkpoint.hot.Snapshot {
480     if (active_snapshot) |*snapshot| return snapshot;
481     return error.EconomicsSnapshotMissing;
482 }
483 
484 fn touchMeasuredWorkingSet(scenario: Scenario, phase: Phase) !void {
485     switch (phase) {
486         .logical_fork_latency => for (active_branches[0..scenario.branch_fanout]) |*slot| {
487             const branch = &(slot.* orelse return error.EconomicsBranchMissing);
488             try touchBranch(branch, scenario.working_set_bytes);
489         },
490         .restore_to_first_boundary => for (active_instances[0..scenario.branch_fanout]) |*slot| {
491             const instance = &(slot.* orelse return error.EconomicsInstanceMissing);
492             try touchInstance(instance, scenario.working_set_bytes);
493         },
494         .compaction_cost => _ = try roots.reopen(
495             (try rootStore()).storage(),
496             active_root,
497         ),
498         .checkpoint_pause, .hot_capture, .delta_binding, .root_sync => {},
499     }
500 }
501 
502 fn touchBranch(branch: *roots.branch.Branch, byte_count: usize) !void {
503     const page_count = byte_count / page_bytes;
504     var page: [page_bytes]u8 align(page_bytes) = undefined;
505     for (0..page_count) |page_index| {
506         try branch.readPage(@intCast(page_index), &page);
507         std.mem.doNotOptimizeAway(&page);
508     }
509 }
510 
511 fn touchInstance(instance: *const machine.Instance, byte_count: usize) !void {
512     const page_count = byte_count / page_bytes;
513     var page: [page_bytes]u8 align(page_bytes) = undefined;
514     for (0..page_count) |page_index| {
515         try instance.readMemory(page_index * page_bytes, &page);
516         std.mem.doNotOptimizeAway(&page);
517     }
518 }
519 
520 fn retainedBytes() !u64 {
521     const store = try rootStore();
522     var bytes = store.retainedBytes();
523     for (&active_branches) |*slot| {
524         if (slot.*) |*branch| {
525             bytes = try std.math.add(u64, bytes, branch.residentBytes());
526         }
527     }
528     for (&active_instances) |*slot| {
529         if (slot.*) |*instance| {
530             bytes = try std.math.add(
531                 u64,
532                 bytes,
533                 try instance.residentMemoryBytes(),
534             );
535         }
536     }
537     return bytes;
538 }
539 
540 fn allocatedBytes() !u64 {
541     return switch ((try rootStore()).allocatedBlocks()) {
542         .supported => |blocks| try std.math.mul(
543             u64,
544             blocks,
545             allocated_block_bytes,
546         ),
547         .unsupported => return error.AllocatedBytesUnavailable,
548     };
549 }
550 
551 fn footprint() !profiling.memory.Footprint {
552     return switch (try profiling.memory.read()) {
553         .supported => |value| value,
554         .unsupported => return error.MemoryFootprintUnavailable,
555     };
556 }
557 
558 fn hotStorage(dirty_bytes_count: usize) !machine.checkpoint.hot.Storage {
559     const dirty_page_count = dirty_bytes_count / page_bytes;
560     return machine.checkpoint.hot.Storage.init(
561         dirty_indices[0..dirty_page_count],
562         dirty_pages[0..dirty_bytes_count],
563     );
564 }
565 
566 fn dirtyPageCount(dirty_bytes_count: usize) u16 {
567     std.debug.assert(dirty_bytes_count % page_bytes == 0);
568     return @intCast(dirty_bytes_count / page_bytes);
569 }
570 
571 fn branchStorage(index: usize) !roots.branch.Storage {
572     std.debug.assert(index < maximum_fanout);
573     return roots.branch.Storage.init(
574         &branch_indices[index],
575         @alignCast(branch_pages[index][0..]),
576         &branch_authenticated[index],
577         &branch_digests[index],
578     );
579 }
580 
581 fn dirtyRam(ram: []align(page_bytes) u8, dirty_bytes_count: usize, turn: u8) void {
582     std.debug.assert(dirty_bytes_count <= ram.len);
583     if (dirty_bytes_count == 0) return;
584     const start = ram.len - dirty_bytes_count;
585     @memset(ram[start..], 0x80 | (turn & 0x3f));
586 }
587 
588 fn rootStore() !*profiling.store.Store {
589     return if (active_store) |*store| store else error.EconomicsRootStoreMissing;
590 }
591 
592 fn cleanup() void {
593     active_snapshot = null;
594     for (&active_instances) |*slot| {
595         if (slot.*) |*instance| instance.deinit();
596         slot.* = null;
597     }
598     active_branches = @splat(null);
599     if (active_store) |*store| store.deinit();
600     active_store = null;
601     profiling.fixture.reset();
602     discardScratch();
603 }
604 
605 fn discardScratch() void {
606     sys.memory.discard(@alignCast(std.mem.asBytes(&checkpoint_ram))) catch {};
607     sys.memory.discard(@alignCast(std.mem.asBytes(&current_ram))) catch {};
608     sys.memory.discard(@alignCast(std.mem.asBytes(&dirty_pages))) catch {};
609     sys.memory.discard(@alignCast(std.mem.asBytes(&branch_indices))) catch {};
610     sys.memory.discard(@alignCast(std.mem.asBytes(&branch_pages))) catch {};
611     sys.memory.discard(@alignCast(std.mem.asBytes(&branch_authenticated))) catch {};
612     sys.memory.discard(@alignCast(std.mem.asBytes(&branch_digests))) catch {};
613 }
614 
615 fn fail(
616     comptime scenario_index: usize,
617     comptime phase: Phase,
618     stage: []const u8,
619     failure: anyerror,
620 ) noreturn {
621     bench.stderr(
622         "machine reference economics {s} failed during {s}: {s}\n",
623         .{ benchmarkName(scenario_index, phase), stage, @errorName(failure) },
624     );
625     @panic("machine checkpoint economics failed");
626 }
627 
628 comptime {
629     std.debug.assert(configured_ram_bytes == 64 * 1024 * 1024);
630     std.debug.assert(root_entry_limit <= root_lookup_limit / 2);
631     std.debug.assert(root_store_bytes >= roots.publication_capacity.bytes);
632     std.debug.assert(maximum_dirty_bytes % page_bytes == 0);
633     std.debug.assert(branch_page_limit >= 32);
634     std.debug.assert(maximum_fanout <= std.math.maxInt(u8));
635     for (scenarios) |scenario| {
636         std.debug.assert(scenario.working_set_bytes <= configured_ram_bytes);
637         std.debug.assert(scenario.working_set_bytes % page_bytes == 0);
638         std.debug.assert(scenario.dirty_bytes <= maximum_dirty_bytes);
639         std.debug.assert(scenario.dirty_bytes % page_bytes == 0);
640         std.debug.assert(scenario.delta_depth < roots.chain_limit);
641         std.debug.assert(scenario.branch_fanout > 0);
642         std.debug.assert(scenario.branch_fanout <= maximum_fanout);
643     }
644 }