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 ¤t_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 ¤t_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 ¤t_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(¤t_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 }