lib/tldr/src/profiling/suite/planning.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const tldr = @import("tldr");
  3 
  4 const incremental = tldr.incremental;
  5 const trace = tldr.trace;
  6 const Allocator = std.mem.Allocator;
  7 
  8 pub const Workload = struct {
  9     allocator: Allocator,
 10     state: incremental.PreparedState,
 11     replacements: []const incremental.ReplacementContribution,
 12     image: []u8,
 13     expected_decision: incremental.PatchDecision,
 14     input_changes: incremental.InputChanges = .{},
 15     candidate_manifest: incremental.Manifest = incremental.Manifest.empty(),
 16     candidate_image: []u8 = &.{},
 17     inputs: []const tldr.Input = &.{},
 18     expected_replacements: usize = 0,
 19     accepted_relink_plan: incremental.RelinkPlan = .{ .decision = .in_place },
 20     manifest_binary: []const u8 = &.{},
 21     expected_manifest_contributions: usize = 0,
 22 };
 23 
 24 pub const Row = enum {
 25     ordered_64,
 26     ordered_256,
 27     ordered_1024,
 28     disordered_256,
 29     archive_churn_256,
 30     section_growth_256,
 31     relocation_churn_256,
 32     symbol_cascade_256,
 33     candidate_extract_sparse_256,
 34     candidate_extract_dense_256,
 35     candidate_update_discarded_256,
 36     manifest_decode_256,
 37     manifest_decode_1024,
 38 };
 39 
 40 const Pattern = enum {
 41     ordered,
 42     disordered,
 43     archive_churn,
 44     section_growth,
 45     relocation_churn,
 46     symbol_cascade,
 47 };
 48 
 49 const row_count = @typeInfo(Row).@"enum".field_names.len;
 50 
 51 pub const Set = struct {
 52     rows: [row_count]Workload,
 53 };
 54 
 55 var active: ?Set = null;
 56 
 57 pub fn install(workloads: Set) void {
 58     active = workloads;
 59 }
 60 
 61 pub fn clear() void {
 62     active = null;
 63 }
 64 
 65 pub fn planLinearOrdered64(_: Allocator) void {
 66     planLinear(.ordered_64);
 67 }
 68 
 69 pub fn planIndexedOrdered64(_: Allocator) void {
 70     planIndexed(.ordered_64);
 71 }
 72 
 73 pub fn planLinearOrdered256(_: Allocator) void {
 74     planLinear(.ordered_256);
 75 }
 76 
 77 pub fn planIndexedOrdered256(_: Allocator) void {
 78     planIndexed(.ordered_256);
 79 }
 80 
 81 pub fn planLinearOrdered1024(_: Allocator) void {
 82     planLinear(.ordered_1024);
 83 }
 84 
 85 pub fn planIndexedOrdered1024(_: Allocator) void {
 86     planIndexed(.ordered_1024);
 87 }
 88 
 89 pub fn planLinearDisordered256(_: Allocator) void {
 90     planLinear(.disordered_256);
 91 }
 92 
 93 pub fn planIndexedDisordered256(_: Allocator) void {
 94     planIndexed(.disordered_256);
 95 }
 96 
 97 pub fn planLinearArchiveChurn256(_: Allocator) void {
 98     planLinear(.archive_churn_256);
 99 }
100 
101 pub fn planIndexedArchiveChurn256(_: Allocator) void {
102     planIndexed(.archive_churn_256);
103 }
104 
105 pub fn planLinearSectionGrowth256(_: Allocator) void {
106     planLinear(.section_growth_256);
107 }
108 
109 pub fn planIndexedSectionGrowth256(_: Allocator) void {
110     planIndexed(.section_growth_256);
111 }
112 
113 pub fn planLinearRelocationChurn256(_: Allocator) void {
114     planLinear(.relocation_churn_256);
115 }
116 
117 pub fn planIndexedRelocationChurn256(_: Allocator) void {
118     planIndexed(.relocation_churn_256);
119 }
120 
121 pub fn planLinearSymbolCascade256(_: Allocator) void {
122     planLinear(.symbol_cascade_256);
123 }
124 
125 pub fn planIndexedSymbolCascade256(_: Allocator) void {
126     planIndexed(.symbol_cascade_256);
127 }
128 
129 pub fn applyLinearArchiveChurn256(_: Allocator) void {
130     applyLinear(.archive_churn_256);
131 }
132 
133 pub fn applyIndexedArchiveChurn256(_: Allocator) void {
134     applyIndexed(.archive_churn_256);
135 }
136 
137 pub fn applyLinearSectionGrowth256(_: Allocator) void {
138     applyLinear(.section_growth_256);
139 }
140 
141 pub fn applyIndexedSectionGrowth256(_: Allocator) void {
142     applyIndexed(.section_growth_256);
143 }
144 
145 pub fn applyLinearSymbolCascade256(_: Allocator) void {
146     applyLinear(.symbol_cascade_256);
147 }
148 
149 pub fn applyIndexedSymbolCascade256(_: Allocator) void {
150     applyIndexed(.symbol_cascade_256);
151 }
152 
153 pub fn extractCandidateSparse256(allocator: Allocator) void {
154     extractCandidate(.candidate_extract_sparse_256, allocator);
155 }
156 
157 pub fn extractCandidateDense256(allocator: Allocator) void {
158     extractCandidate(.candidate_extract_dense_256, allocator);
159 }
160 
161 pub fn updateCandidateDiscarded256(_: Allocator) void {
162     const phase = trace.product(.manifest_recording);
163     defer phase.end();
164     const workload = activeMutableWorkload(.candidate_update_discarded_256);
165     workload.state.updateManifestForAcceptedCandidateRelinkFromInputChanges(
166         workload.allocator,
167         workload.inputs,
168         workload.input_changes,
169         workload.replacements,
170         workload.candidate_manifest,
171         workload.accepted_relink_plan,
172     ) catch @panic("accepted candidate relink manifest update failed");
173     std.mem.doNotOptimizeAway(workload.state.manifest.discarded_contributions.len);
174     std.mem.doNotOptimizeAway(workload.state.input_contributions.len);
175 }
176 
177 pub fn decodeBinaryManifest256(allocator: Allocator) void {
178     decodeManifest(.manifest_decode_256, allocator);
179 }
180 
181 pub fn decodeBinaryManifest1024(allocator: Allocator) void {
182     decodeManifest(.manifest_decode_1024, allocator);
183 }
184 
185 fn planLinear(row: Row) void {
186     const phase = trace.product(.manifest_recording);
187     defer phase.end();
188     const workload = activeWorkload(row);
189     const plan = workload.state.manifest.planContributionReplacement(workload.replacements);
190     requireExpectedDecision(workload, plan);
191     std.mem.doNotOptimizeAway(plan.decision);
192     std.mem.doNotOptimizeAway(plan.blocker);
193 }
194 
195 fn planIndexed(row: Row) void {
196     const phase = trace.product(.manifest_recording);
197     defer phase.end();
198     const workload = activeWorkload(row);
199     const plan = workload.state.planContributionReplacement(workload.replacements);
200     requireExpectedDecision(workload, plan);
201     std.mem.doNotOptimizeAway(plan.decision);
202     std.mem.doNotOptimizeAway(plan.blocker);
203 }
204 
205 fn applyLinear(row: Row) void {
206     const phase = trace.product(.output_writing);
207     defer phase.end();
208     const workload = activeWorkload(row);
209     const application = workload.state.manifest.applyContributionReplacement(workload.image, workload.replacements) catch
210         @panic("incremental linear patch application failed");
211     requireExpectedDecision(workload, application.plan);
212     std.mem.doNotOptimizeAway(application.contributions_written);
213     std.mem.doNotOptimizeAway(application.bytes_written);
214     std.mem.doNotOptimizeAway(application.zero_fill_bytes);
215 }
216 
217 fn applyIndexed(row: Row) void {
218     const phase = trace.product(.output_writing);
219     defer phase.end();
220     const workload = activeWorkload(row);
221     const application = workload.state.applyContributionReplacement(workload.image, workload.replacements) catch
222         @panic("incremental indexed patch application failed");
223     requireExpectedDecision(workload, application.plan);
224     std.mem.doNotOptimizeAway(application.contributions_written);
225     std.mem.doNotOptimizeAway(application.bytes_written);
226     std.mem.doNotOptimizeAway(application.zero_fill_bytes);
227 }
228 
229 fn extractCandidate(row: Row, allocator: Allocator) void {
230     const phase = trace.product(.manifest_recording);
231     defer phase.end();
232     const workload = activeWorkload(row);
233     const replacements = incremental.replacementContributionsFromImage(
234         allocator,
235         workload.candidate_image,
236         workload.candidate_manifest,
237         workload.input_changes,
238     ) catch @panic("candidate replacement extraction failed");
239     defer if (replacements.len != 0) allocator.free(replacements);
240     if (replacements.len != workload.expected_replacements) @panic("candidate replacement extraction count changed");
241     std.mem.doNotOptimizeAway(replacements.len);
242 }
243 
244 fn decodeManifest(row: Row, allocator: Allocator) void {
245     const phase = trace.product(.manifest_recording);
246     defer phase.end();
247     const workload = activeWorkload(row);
248     const manifest_bytes = allocator.dupe(u8, workload.manifest_binary) catch @panic("incremental manifest bytes failed");
249     defer allocator.free(manifest_bytes);
250     var manifest = incremental.Manifest.fromBinary(allocator, manifest_bytes) catch @panic("incremental manifest decode failed");
251     defer manifest.deinit(allocator);
252     if (manifest.contributions.len != workload.expected_manifest_contributions) @panic("incremental manifest decode count changed");
253     std.mem.doNotOptimizeAway(manifest.inputs.len);
254     std.mem.doNotOptimizeAway(manifest.sections.len);
255     std.mem.doNotOptimizeAway(manifest.contributions.len);
256     std.mem.doNotOptimizeAway(manifest.discarded_contributions.len);
257 }
258 
259 fn activeWorkload(row: Row) *const Workload {
260     if (active) |*workloads| return &workloads.rows[@backingInt(row)];
261     @panic("incremental-planning workloads are not installed");
262 }
263 
264 fn activeMutableWorkload(row: Row) *Workload {
265     if (active) |*workloads| return &workloads.rows[@backingInt(row)];
266     @panic("incremental-planning workloads are not installed");
267 }
268 
269 fn requireExpectedDecision(workload: *const Workload, plan: incremental.PatchPlan) void {
270     if (plan.decision != workload.expected_decision) @panic("incremental plan returned unexpected decision");
271 }
272 
273 pub fn buildSet(allocator: Allocator) !Set {
274     return .{ .rows = .{
275         try build(allocator, 64, .ordered),
276         try build(allocator, 256, .ordered),
277         try build(allocator, 1024, .ordered),
278         try build(allocator, 256, .disordered),
279         try build(allocator, 256, .archive_churn),
280         try build(allocator, 256, .section_growth),
281         try build(allocator, 256, .relocation_churn),
282         try build(allocator, 256, .symbol_cascade),
283         try buildCandidateExtraction(allocator, 256, 8),
284         try buildCandidateExtraction(allocator, 256, 128),
285         try buildCandidateDiscardedUpdate(allocator, 256),
286         try buildManifestDecode(allocator, 256),
287         try buildManifestDecode(allocator, 1024),
288     } };
289 }
290 
291 fn build(allocator: Allocator, contribution_count: usize, pattern: Pattern) !Workload {
292     var builder = try incremental.Builder.init(allocator, .{});
293     defer builder.deinit();
294 
295     const base = try allocator.alloc(incremental.ReplacementContribution, contribution_count);
296     defer allocator.free(base);
297     const replacements = try allocator.alloc(incremental.ReplacementContribution, contribution_count);
298     errdefer allocator.free(replacements);
299 
300     var index: usize = 0;
301     while (index < contribution_count) : (index += 1) {
302         const input_name = try inputName(allocator, pattern, index);
303         const contribution_name = try contributionName(allocator, pattern, index);
304         const contribution_kind = contributionKind(pattern);
305         const output_section_name = outputSectionName(pattern);
306         const size = recordedSize(pattern);
307         const reserve = reservedSize(pattern);
308         const alignment = recordedAlignment(pattern);
309         try builder.addContribution(
310             input_name,
311             index,
312             contribution_kind,
313             contribution_name,
314             @intCast(index),
315             output_section_name,
316             0x401000 + index * 64,
317             0x1000 + index * 64,
318             size,
319             reserve,
320             alignment,
321         );
322         base[index] = .{
323             .input_name = input_name,
324             .input_index = index,
325             .kind = contribution_kind,
326             .name = contribution_name,
327             .ordinal = @intCast(index),
328             .size = replacementSize(pattern),
329             .alignment = replacementAlignment(pattern),
330             .payload = try replacementPayload(allocator, pattern, index, replacementSize(pattern)),
331         };
332     }
333 
334     index = 0;
335     while (index < contribution_count) : (index += 1) {
336         replacements[index] = base[replacementSourceIndex(pattern, index, contribution_count)];
337     }
338     if (pattern == .archive_churn) {
339         replacements[contribution_count - 1] = .{
340             .input_name = try std.fmt.allocPrint(allocator, "libhot.a(member_{d}.o)", .{contribution_count}),
341             .input_index = contribution_count,
342             .kind = .section,
343             .name = try std.fmt.allocPrint(allocator, ".text.member.{d}", .{contribution_count}),
344             .ordinal = @intCast(contribution_count),
345             .size = 32,
346             .alignment = 16,
347             .payload = try replacementPayload(allocator, pattern, contribution_count, 32),
348         };
349     }
350     if (pattern == .symbol_cascade) {
351         replacements[contribution_count - 1] = .{
352             .input_name = try std.fmt.allocPrint(allocator, "cascade_{d}.o", .{contribution_count}),
353             .input_index = contribution_count,
354             .kind = .common_symbol,
355             .name = try std.fmt.allocPrint(allocator, "cascade_symbol_{d}", .{contribution_count}),
356             .ordinal = @intCast(contribution_count),
357             .size = 16,
358             .alignment = 8,
359             .payload = try replacementPayload(allocator, pattern, contribution_count, 16),
360         };
361     }
362 
363     var manifest = try builder.finish();
364     var manifest_moved = false;
365     errdefer if (!manifest_moved) manifest.deinit(allocator);
366 
367     manifest_moved = true;
368     var state = try incremental.PreparedState.fromOwnedManifest(allocator, manifest);
369     errdefer state.deinit(allocator);
370     try state.ensureReplacementIndex(allocator, replacements);
371 
372     return .{
373         .allocator = allocator,
374         .state = state,
375         .replacements = replacements,
376         .image = try outputImage(allocator, contribution_count),
377         .expected_decision = expectedDecision(pattern),
378     };
379 }
380 
381 fn buildManifestDecode(allocator: Allocator, contribution_count: usize) !Workload {
382     var builder = try incremental.Builder.init(allocator, .{ .incremental_mode = .prepare, .gc_sections = true, .icf = .all, .strip_debug = false });
383     defer builder.deinit();
384 
385     try builder.addSection(".text", 0x401000, 0x1000, contribution_count * 48, contribution_count * 64, 16);
386     try builder.addSection(".rodata", 0x501000, 0x9000, contribution_count * 32, contribution_count * 32, 8);
387     try builder.addSection(".tdata", 0x601000, 0xd000, contribution_count * 16, contribution_count * 16, 8);
388     try builder.addSection(".tbss", 0x701000, 0, contribution_count * 16, contribution_count * 16, 8);
389 
390     var index: usize = 0;
391     while (index < contribution_count) : (index += 1) {
392         const input_name = try std.fmt.allocPrint(allocator, "decode_{d}.o", .{index});
393         try builder.addInput(.{ .name = input_name, .bytes = "decode-input" });
394         const output_section_name = manifestDecodeSectionName(index);
395         const contribution_name = try std.fmt.allocPrint(allocator, "{s}.decode.{d}", .{ output_section_name, index });
396         const file_size: u64 = if (std.mem.eql(u8, output_section_name, ".tbss")) 0 else manifestDecodeSize(index);
397         try builder.addContributionWithFileSize(
398             input_name,
399             index,
400             .section,
401             contribution_name,
402             @intCast(index),
403             output_section_name,
404             0x401000 + index * 64,
405             0x1000 + index * 64,
406             manifestDecodeSize(index),
407             file_size,
408             64,
409             manifestDecodeAlignment(index),
410         );
411         if (index % 8 == 0) {
412             const discarded_name = try std.fmt.allocPrint(allocator, ".text.dead.decode.{d}", .{index});
413             try builder.addDiscardedContribution(input_name, index, discarded_name, @intCast(index), .identical_code_folded, 24, 16);
414         }
415     }
416 
417     var manifest = try builder.finish();
418     var manifest_moved = false;
419     errdefer if (!manifest_moved) manifest.deinit(allocator);
420 
421     const manifest_binary = try manifest.formatBinaryAlloc(allocator);
422     errdefer allocator.free(manifest_binary);
423 
424     manifest_moved = true;
425     var state = try incremental.PreparedState.fromOwnedManifest(allocator, manifest);
426     errdefer state.deinit(allocator);
427 
428     return .{
429         .allocator = allocator,
430         .state = state,
431         .replacements = &.{},
432         .image = &.{},
433         .expected_decision = .in_place,
434         .manifest_binary = manifest_binary,
435         .expected_manifest_contributions = contribution_count,
436     };
437 }
438 
439 fn manifestDecodeSectionName(index: usize) []const u8 {
440     return switch (index % 4) {
441         0 => ".text",
442         1 => ".rodata",
443         2 => ".tdata",
444         else => ".tbss",
445     };
446 }
447 
448 fn manifestDecodeSize(index: usize) u64 {
449     return switch (index % 4) {
450         0 => 48,
451         1 => 32,
452         else => 16,
453     };
454 }
455 
456 fn manifestDecodeAlignment(index: usize) u64 {
457     return switch (index % 4) {
458         0 => 16,
459         1 => 8,
460         else => 4,
461     };
462 }
463 
464 fn buildCandidateExtraction(allocator: Allocator, contribution_count: usize, changed_count: usize) !Workload {
465     var builder = try incremental.Builder.init(allocator, .{});
466     defer builder.deinit();
467     var candidate_builder = try incremental.Builder.init(allocator, .{});
468     defer candidate_builder.deinit();
469 
470     var changes = try allocator.alloc(incremental.InputChange, contribution_count);
471     var summary: incremental.InputChangeSummary = .{};
472     var index: usize = 0;
473     while (index < contribution_count) : (index += 1) {
474         const name = try std.fmt.allocPrint(allocator, "candidate_{d}.o", .{index});
475         try builder.addInput(.{ .name = name, .bytes = "old" });
476         try candidate_builder.addInput(.{ .name = name, .bytes = if (index < changed_count) "new" else "old" });
477         const contribution_name = try std.fmt.allocPrint(allocator, ".text.candidate.{d}", .{index});
478         try builder.addContribution(
479             name,
480             index,
481             .section,
482             contribution_name,
483             @intCast(index),
484             ".text",
485             0x401000 + index * 64,
486             0x1000 + index * 64,
487             32,
488             64,
489             16,
490         );
491         try candidate_builder.addContribution(
492             name,
493             index,
494             .section,
495             contribution_name,
496             @intCast(index),
497             ".text",
498             0x401000 + index * 64,
499             0x1000 + index * 64,
500             48,
501             64,
502             16,
503         );
504         changes[index] = .{
505             .kind = if (index < changed_count) .changed else .unchanged,
506             .recorded_index = index,
507             .current_index = index,
508         };
509         if (index < changed_count) {
510             summary.changed += 1;
511         } else {
512             summary.unchanged += 1;
513         }
514     }
515 
516     var manifest = try builder.finish();
517     var manifest_moved = false;
518     errdefer if (!manifest_moved) manifest.deinit(allocator);
519     manifest_moved = true;
520     var state = try incremental.PreparedState.fromOwnedManifest(allocator, manifest);
521     errdefer state.deinit(allocator);
522 
523     return .{
524         .allocator = allocator,
525         .state = state,
526         .replacements = &.{},
527         .image = try outputImage(allocator, contribution_count),
528         .expected_decision = .in_place,
529         .input_changes = .{
530             .changes = changes,
531             .summary = summary,
532         },
533         .candidate_manifest = try candidate_builder.finish(),
534         .candidate_image = try outputImage(allocator, contribution_count),
535         .expected_replacements = changed_count,
536     };
537 }
538 
539 fn buildCandidateDiscardedUpdate(allocator: Allocator, input_count: usize) !Workload {
540     var builder = try incremental.Builder.init(allocator, .{});
541     defer builder.deinit();
542     var candidate_builder = try incremental.Builder.init(allocator, .{});
543     defer candidate_builder.deinit();
544 
545     const inputs = try allocator.alloc(tldr.Input, input_count);
546     var changes = try allocator.alloc(incremental.InputChange, input_count);
547     var index: usize = 0;
548     while (index < input_count) : (index += 1) {
549         const name = try std.fmt.allocPrint(allocator, "discarded_{d}.o", .{index});
550         try builder.addInput(.{ .name = name, .bytes = "old" });
551         inputs[index] = .{ .name = name, .bytes = "new" };
552         try candidate_builder.addInput(inputs[index]);
553         const old_name = try std.fmt.allocPrint(allocator, ".text.dead.{d}", .{index});
554         const new_name = try std.fmt.allocPrint(allocator, ".text.folded.{d}", .{index});
555         try builder.addDiscardedContribution(name, index, old_name, @intCast(index), .discarded, 24, 16);
556         try candidate_builder.addDiscardedContribution(name, index, new_name, @intCast(index), .identical_code_folded, 32, 16);
557         changes[index] = .{
558             .kind = .changed,
559             .recorded_index = index,
560             .current_index = index,
561         };
562     }
563 
564     var manifest = try builder.finish();
565     var manifest_moved = false;
566     errdefer if (!manifest_moved) manifest.deinit(allocator);
567     manifest_moved = true;
568     var state = try incremental.PreparedState.fromOwnedManifest(allocator, manifest);
569     errdefer state.deinit(allocator);
570 
571     const input_changes = incremental.InputChanges{
572         .changes = changes,
573         .summary = .{ .changed = input_count },
574     };
575     const accepted_relink_plan = state.planChangedInputRelinkFromInputChanges(.{}, input_changes, &.{});
576     if (accepted_relink_plan.decision != .in_place) return error.UnexpectedRelinkPlan;
577 
578     return .{
579         .allocator = allocator,
580         .state = state,
581         .replacements = &.{},
582         .image = try outputImage(allocator, input_count),
583         .expected_decision = .in_place,
584         .input_changes = input_changes,
585         .candidate_manifest = try candidate_builder.finish(),
586         .candidate_image = &.{},
587         .inputs = inputs,
588         .accepted_relink_plan = accepted_relink_plan,
589     };
590 }
591 
592 fn contributionKind(pattern: Pattern) incremental.ContributionKind {
593     return switch (pattern) {
594         .symbol_cascade => .common_symbol,
595         else => .section,
596     };
597 }
598 
599 fn inputName(allocator: Allocator, pattern: Pattern, index: usize) ![]u8 {
600     return switch (pattern) {
601         .archive_churn => try std.fmt.allocPrint(allocator, "libhot.a(member_{d}.o)", .{index}),
602         .relocation_churn => try std.fmt.allocPrint(allocator, "reloc_{d}.o", .{index}),
603         .symbol_cascade => try std.fmt.allocPrint(allocator, "cascade_{d}.o", .{index}),
604         else => try std.fmt.allocPrint(allocator, "obj_{d}.o", .{index}),
605     };
606 }
607 
608 fn contributionName(allocator: Allocator, pattern: Pattern, index: usize) ![]u8 {
609     return switch (pattern) {
610         .archive_churn => try std.fmt.allocPrint(allocator, ".text.member.{d}", .{index}),
611         .relocation_churn => try std.fmt.allocPrint(allocator, ".rela.text.{d}", .{index}),
612         .symbol_cascade => try std.fmt.allocPrint(allocator, "cascade_symbol_{d}", .{index}),
613         else => try std.fmt.allocPrint(allocator, ".text.{d}", .{index}),
614     };
615 }
616 
617 fn outputSectionName(pattern: Pattern) []const u8 {
618     return switch (pattern) {
619         .relocation_churn => ".rela.text",
620         .symbol_cascade => ".bss",
621         else => ".text",
622     };
623 }
624 
625 fn recordedSize(pattern: Pattern) u64 {
626     return switch (pattern) {
627         .relocation_churn => 24,
628         .symbol_cascade => 8,
629         else => 32,
630     };
631 }
632 
633 fn replacementSize(pattern: Pattern) u64 {
634     return switch (pattern) {
635         .section_growth => 56,
636         .relocation_churn => 24,
637         .symbol_cascade => 16,
638         else => 48,
639     };
640 }
641 
642 fn reservedSize(pattern: Pattern) u64 {
643     return switch (pattern) {
644         .relocation_churn => 32,
645         .symbol_cascade => 16,
646         else => 64,
647     };
648 }
649 
650 fn recordedAlignment(pattern: Pattern) u64 {
651     return switch (pattern) {
652         .symbol_cascade => 8,
653         else => 16,
654     };
655 }
656 
657 fn replacementAlignment(pattern: Pattern) u64 {
658     return switch (pattern) {
659         .symbol_cascade => 8,
660         else => 16,
661     };
662 }
663 
664 fn replacementPayload(allocator: Allocator, pattern: Pattern, index: usize, size: u64) ![]u8 {
665     const bytes = try allocator.alloc(u8, @intCast(size));
666     for (bytes, 0..) |*byte, offset| {
667         byte.* = @truncate((index * 31) + (offset * 17) + @backingInt(pattern));
668     }
669     return bytes;
670 }
671 
672 fn outputImage(allocator: Allocator, contribution_count: usize) ![]u8 {
673     const bytes = try allocator.alloc(u8, 0x1000 + contribution_count * 64 + 64);
674     @memset(bytes, 0xa5);
675     return bytes;
676 }
677 
678 fn replacementSourceIndex(pattern: Pattern, index: usize, contribution_count: usize) usize {
679     return switch (pattern) {
680         .disordered => contribution_count - 1 - index,
681         .archive_churn, .symbol_cascade => if (index + 1 == contribution_count) index else contribution_count - 2 - index,
682         else => index,
683     };
684 }
685 
686 fn expectedDecision(pattern: Pattern) incremental.PatchDecision {
687     return switch (pattern) {
688         .archive_churn, .symbol_cascade => .full_link,
689         else => .in_place,
690     };
691 }
692 
693 test "bench: incremental planning workloads cover scale and churn decisions" {
694     var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
695     defer arena_state.deinit();
696     const allocator = arena_state.allocator();
697 
698     const workloads = try buildSet(allocator);
699     inline for (
700         @typeInfo(Row).@"enum".field_names,
701         @typeInfo(Row).@"enum".field_values,
702     ) |field_name, field_name_value| {
703         const field = .{ .name = field_name, .value = field_name_value };
704         const row: Row = @fromBackingInt(@intCast(field.value));
705         const workload = workloads.rows[@backingInt(row)];
706         const linear = workload.state.manifest.planContributionReplacement(workload.replacements);
707         const indexed = workload.state.planContributionReplacement(workload.replacements);
708         try std.testing.expectEqual(workload.expected_decision, linear.decision);
709         try std.testing.expectEqual(linear.decision, indexed.decision);
710         try std.testing.expectEqual(linear.blocker, indexed.blocker);
711         try std.testing.expectEqual(linear.blocking_index, indexed.blocking_index);
712     }
713 }
714 
715 test "bench: incremental patch workloads apply selected mutation rows" {
716     var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
717     defer arena_state.deinit();
718     const allocator = arena_state.allocator();
719 
720     const workloads = try buildSet(allocator);
721     const rows = [_]Row{
722         .archive_churn_256,
723         .section_growth_256,
724         .symbol_cascade_256,
725     };
726     for (rows) |row| {
727         const workload = workloads.rows[@backingInt(row)];
728         const linear_image = try allocator.dupe(u8, workload.image);
729         const indexed_image = try allocator.dupe(u8, workload.image);
730         const linear = try workload.state.manifest.applyContributionReplacement(linear_image, workload.replacements);
731         const indexed = try workload.state.applyContributionReplacement(indexed_image, workload.replacements);
732         try std.testing.expectEqual(workload.expected_decision, linear.plan.decision);
733         try std.testing.expectEqual(linear.plan.decision, indexed.plan.decision);
734         try std.testing.expectEqual(linear.plan.blocker, indexed.plan.blocker);
735         try std.testing.expectEqual(linear.contributions_written, indexed.contributions_written);
736         try std.testing.expectEqual(linear.bytes_written, indexed.bytes_written);
737         if (workload.expected_decision == .in_place) {
738             try std.testing.expect(linear.bytes_written != 0);
739             try std.testing.expectEqualSlices(u8, linear_image, indexed_image);
740         } else {
741             try std.testing.expectEqual(@as(usize, 0), linear.bytes_written);
742         }
743     }
744 }
745 
746 test "bench: incremental candidate workloads cover extraction and update" {
747     var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
748     defer arena_state.deinit();
749     const allocator = arena_state.allocator();
750 
751     var workloads = try buildSet(allocator);
752     const sparse = workloads.rows[@backingInt(Row.candidate_extract_sparse_256)];
753     const sparse_replacements = try incremental.replacementContributionsFromImage(
754         allocator,
755         sparse.candidate_image,
756         sparse.candidate_manifest,
757         sparse.input_changes,
758     );
759     try std.testing.expectEqual(sparse.expected_replacements, sparse_replacements.len);
760 
761     const dense = workloads.rows[@backingInt(Row.candidate_extract_dense_256)];
762     const dense_replacements = try incremental.replacementContributionsFromImage(
763         allocator,
764         dense.candidate_image,
765         dense.candidate_manifest,
766         dense.input_changes,
767     );
768     try std.testing.expectEqual(dense.expected_replacements, dense_replacements.len);
769 
770     var update = &workloads.rows[@backingInt(Row.candidate_update_discarded_256)];
771     try update.state.updateManifestForAcceptedCandidateRelinkFromInputChanges(
772         allocator,
773         update.inputs,
774         update.input_changes,
775         update.replacements,
776         update.candidate_manifest,
777         update.accepted_relink_plan,
778     );
779     try std.testing.expectEqual(@as(usize, 256), update.state.manifest.discarded_contributions.len);
780     try std.testing.expectEqual(incremental.DiscardReason.identical_code_folded, update.state.manifest.discarded_contributions[0].reason);
781     try std.testing.expectEqual(@as(u64, 32), update.state.input_contributions[0].discarded_size);
782 }
783 
784 test "bench: incremental manifest decode workloads parse private format" {
785     var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
786     defer arena_state.deinit();
787     const allocator = arena_state.allocator();
788 
789     const workloads = try buildSet(allocator);
790     const rows = [_]Row{
791         .manifest_decode_256,
792         .manifest_decode_1024,
793     };
794     for (rows) |row| {
795         const workload = workloads.rows[@backingInt(row)];
796         const binary_bytes = try allocator.dupe(u8, workload.manifest_binary);
797         defer allocator.free(binary_bytes);
798         var binary = try incremental.Manifest.fromBinary(allocator, binary_bytes);
799         defer binary.deinit(allocator);
800         try std.testing.expectEqual(workload.expected_manifest_contributions, binary.contributions.len);
801         try std.testing.expectEqual(@as(usize, 4), binary.sections.len);
802         try std.testing.expect(binary.input_hashes_recorded);
803     }
804 }