lib/choir/src/passes/instrumentation.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const pretty = @import("pretty");
   3 const sys = @import("sys");
   4 const ir = @import("../core/root.zig");
   5 const pass_mod = @import("pass/root.zig");
   6 const verify_mod = @import("../core/root.zig").verify;
   7 const hashing = @import("../root.zig").product.hashing;
   8 
   9 fn nowNanos() i128 {
  10     return sys.time.nanoTimestamp();
  11 }
  12 
  13 pub const PassInfo = struct {
  14     name: []const u8,
  15     description: []const u8,
  16     target_op: ?*ir.Operation,
  17     mutation_scope: pass_mod.PassMutationScope = .whole_module,
  18 };
  19 
  20 pub const PipelineInfo = struct {
  21     target_op_name: ?[]const u8,
  22     depth: usize,
  23 };
  24 
  25 pub const AnalysisInfo = struct {
  26     id: pass_mod.AnalysisId,
  27     name: []const u8,
  28     target_op: *ir.Operation,
  29 };
  30 
  31 pub const PassStatisticInfo = struct {
  32     pass: PassInfo,
  33     name: []const u8,
  34     description: []const u8,
  35     value: u64,
  36 };
  37 
  38 const PassStatisticsEntry = struct {
  39     pass_name: []u8,
  40     name: []u8,
  41     description: []u8,
  42     value: u64,
  43 };
  44 
  45 pub const PassStatisticSummary = struct {
  46     pass_name: []const u8,
  47     name: []const u8,
  48     description: []const u8,
  49     value: u64,
  50 };
  51 
  52 pub const PassInstrumentation = struct {
  53     ctx: ?*anyopaque = null,
  54 
  55     runBeforePipeline: ?*const fn (ctx: ?*anyopaque, info: PipelineInfo, op: *ir.Operation) void = null,
  56 
  57     runAfterPipeline: ?*const fn (ctx: ?*anyopaque, info: PipelineInfo, op: *ir.Operation, failed: bool) void = null,
  58 
  59     runBeforePass: ?*const fn (ctx: ?*anyopaque, info: PassInfo) void = null,
  60 
  61     runAfterPass: ?*const fn (ctx: ?*anyopaque, info: PassInfo, modified: bool) void = null,
  62 
  63     runAfterPassFailed: ?*const fn (ctx: ?*anyopaque, info: PassInfo) void = null,
  64 
  65     runBeforeAnalysis: ?*const fn (ctx: ?*anyopaque, info: AnalysisInfo) void = null,
  66 
  67     runAfterAnalysis: ?*const fn (ctx: ?*anyopaque, info: AnalysisInfo) void = null,
  68 
  69     runPassStatistic: ?*const fn (ctx: ?*anyopaque, info: PassStatisticInfo) void = null,
  70 
  71     pub fn beforePipeline(self: *const PassInstrumentation, info: PipelineInfo, op: *ir.Operation) void {
  72         if (self.runBeforePipeline) |hook| {
  73             hook(self.ctx, info, op);
  74         }
  75     }
  76 
  77     pub fn afterPipeline(self: *const PassInstrumentation, info: PipelineInfo, op: *ir.Operation, failed: bool) void {
  78         if (self.runAfterPipeline) |hook| {
  79             hook(self.ctx, info, op, failed);
  80         }
  81     }
  82 
  83     pub fn beforePass(self: *const PassInstrumentation, info: PassInfo) void {
  84         if (self.runBeforePass) |hook| {
  85             hook(self.ctx, info);
  86         }
  87     }
  88 
  89     pub fn afterPass(self: *const PassInstrumentation, info: PassInfo, modified: bool) void {
  90         if (self.runAfterPass) |hook| {
  91             hook(self.ctx, info, modified);
  92         }
  93     }
  94 
  95     pub fn afterPassFailed(self: *const PassInstrumentation, info: PassInfo) void {
  96         if (self.runAfterPassFailed) |hook| {
  97             hook(self.ctx, info);
  98         }
  99     }
 100 
 101     pub fn beforeAnalysis(self: *const PassInstrumentation, info: AnalysisInfo) void {
 102         if (self.runBeforeAnalysis) |hook| {
 103             hook(self.ctx, info);
 104         }
 105     }
 106 
 107     pub fn afterAnalysis(self: *const PassInstrumentation, info: AnalysisInfo) void {
 108         if (self.runAfterAnalysis) |hook| {
 109             hook(self.ctx, info);
 110         }
 111     }
 112 
 113     pub fn passStatistic(self: *const PassInstrumentation, info: PassStatisticInfo) void {
 114         if (self.runPassStatistic) |hook| {
 115             hook(self.ctx, info);
 116         }
 117     }
 118 };
 119 
 120 pub const PassInstrumentor = struct {
 121     allocator: std.mem.Allocator,
 122     instrumentations: std.ArrayListUnmanaged(PassInstrumentation),
 123 
 124     pub fn init(allocator: std.mem.Allocator) PassInstrumentor {
 125         return .{
 126             .allocator = allocator,
 127             .instrumentations = .empty,
 128         };
 129     }
 130 
 131     pub fn deinit(self: *PassInstrumentor) void {
 132         self.instrumentations.deinit(self.allocator);
 133     }
 134 
 135     pub fn addInstrumentation(self: *PassInstrumentor, inst: PassInstrumentation) !void {
 136         try self.instrumentations.append(self.allocator, inst);
 137     }
 138 
 139     pub fn clear(self: *PassInstrumentor) void {
 140         self.instrumentations.clearRetainingCapacity();
 141     }
 142 
 143     pub fn runBeforePipeline(self: *const PassInstrumentor, info: PipelineInfo, op: *ir.Operation) void {
 144         for (self.instrumentations.items) |*inst| {
 145             inst.beforePipeline(info, op);
 146         }
 147     }
 148 
 149     pub fn runAfterPipeline(self: *const PassInstrumentor, info: PipelineInfo, op: *ir.Operation, failed: bool) void {
 150         var i = self.instrumentations.items.len;
 151         while (i > 0) {
 152             i -= 1;
 153             self.instrumentations.items[i].afterPipeline(info, op, failed);
 154         }
 155     }
 156 
 157     pub fn runBeforePass(self: *const PassInstrumentor, info: PassInfo) void {
 158         for (self.instrumentations.items) |*inst| {
 159             inst.beforePass(info);
 160         }
 161     }
 162 
 163     pub fn runAfterPass(self: *const PassInstrumentor, info: PassInfo, modified: bool) void {
 164         var i = self.instrumentations.items.len;
 165         while (i > 0) {
 166             i -= 1;
 167             self.instrumentations.items[i].afterPass(info, modified);
 168         }
 169     }
 170 
 171     pub fn runAfterPassFailed(self: *const PassInstrumentor, info: PassInfo) void {
 172         var i = self.instrumentations.items.len;
 173         while (i > 0) {
 174             i -= 1;
 175             self.instrumentations.items[i].afterPassFailed(info);
 176         }
 177     }
 178 
 179     pub fn runBeforeAnalysis(self: *const PassInstrumentor, info: AnalysisInfo) void {
 180         for (self.instrumentations.items) |*inst| {
 181             inst.beforeAnalysis(info);
 182         }
 183     }
 184 
 185     pub fn runAfterAnalysis(self: *const PassInstrumentor, info: AnalysisInfo) void {
 186         var i = self.instrumentations.items.len;
 187         while (i > 0) {
 188             i -= 1;
 189             self.instrumentations.items[i].afterAnalysis(info);
 190         }
 191     }
 192 
 193     pub fn runPassStatistic(self: *const PassInstrumentor, info: PassStatisticInfo) void {
 194         for (self.instrumentations.items) |*inst| {
 195             inst.passStatistic(info);
 196         }
 197     }
 198 };
 199 
 200 pub const PassStatisticsInstrumentation = struct {
 201     allocator: std.mem.Allocator,
 202     entries: std.ArrayListUnmanaged(PassStatisticsEntry),
 203 
 204     pub fn init(allocator: std.mem.Allocator) PassStatisticsInstrumentation {
 205         return .{
 206             .allocator = allocator,
 207             .entries = .empty,
 208         };
 209     }
 210 
 211     pub fn deinit(self: *PassStatisticsInstrumentation) void {
 212         self.reset();
 213         self.entries.deinit(self.allocator);
 214     }
 215 
 216     pub fn instrumentation(self: *PassStatisticsInstrumentation) PassInstrumentation {
 217         return .{
 218             .ctx = self,
 219             .runPassStatistic = recordImpl,
 220         };
 221     }
 222 
 223     pub fn reset(self: *PassStatisticsInstrumentation) void {
 224         for (self.entries.items) |entry| {
 225             self.allocator.free(entry.pass_name);
 226             self.allocator.free(entry.name);
 227             self.allocator.free(entry.description);
 228         }
 229         self.entries.clearRetainingCapacity();
 230     }
 231 
 232     pub fn record(self: *PassStatisticsInstrumentation, info: PassStatisticInfo) !void {
 233         if (self.findEntry(info.pass.name, info.name)) |entry| {
 234             entry.value +|= info.value;
 235             return;
 236         }
 237 
 238         const pass_name = try self.allocator.dupe(u8, info.pass.name);
 239         errdefer self.allocator.free(pass_name);
 240         const name = try self.allocator.dupe(u8, info.name);
 241         errdefer self.allocator.free(name);
 242         const description = try self.allocator.dupe(u8, info.description);
 243         errdefer self.allocator.free(description);
 244 
 245         try self.entries.append(self.allocator, .{
 246             .pass_name = pass_name,
 247             .name = name,
 248             .description = description,
 249             .value = info.value,
 250         });
 251     }
 252 
 253     pub fn get(self: *const PassStatisticsInstrumentation, pass_name: []const u8, name: []const u8) ?u64 {
 254         if (self.findEntry(pass_name, name)) |entry| return entry.value;
 255         return null;
 256     }
 257 
 258     pub fn summariesAlloc(
 259         self: *const PassStatisticsInstrumentation,
 260         allocator: std.mem.Allocator,
 261     ) ![]PassStatisticSummary {
 262         var summaries = try allocator.alloc(PassStatisticSummary, self.entries.items.len);
 263         errdefer allocator.free(summaries);
 264 
 265         for (self.entries.items, 0..) |entry, index| {
 266             summaries[index] = .{
 267                 .pass_name = entry.pass_name,
 268                 .name = entry.name,
 269                 .description = entry.description,
 270                 .value = entry.value,
 271             };
 272         }
 273         std.mem.sort(PassStatisticSummary, summaries, {}, summaryLessThan);
 274         return summaries;
 275     }
 276 
 277     fn findEntry(self: anytype, pass_name: []const u8, name: []const u8) ?@TypeOf(&self.entries.items[0]) {
 278         for (self.entries.items) |*entry| {
 279             if (std.mem.eql(u8, entry.pass_name, pass_name) and std.mem.eql(u8, entry.name, name)) {
 280                 return entry;
 281             }
 282         }
 283         return null;
 284     }
 285 
 286     fn recordImpl(ctx: ?*anyopaque, info: PassStatisticInfo) void {
 287         const self: *PassStatisticsInstrumentation = @ptrCast(@alignCast(ctx.?));
 288         self.record(info) catch {};
 289     }
 290 
 291     fn summaryLessThan(_: void, lhs: PassStatisticSummary, rhs: PassStatisticSummary) bool {
 292         const pass_order = std.mem.order(u8, lhs.pass_name, rhs.pass_name);
 293         if (pass_order != .eq) return pass_order == .lt;
 294         return std.mem.lessThan(u8, lhs.name, rhs.name);
 295     }
 296 };
 297 
 298 pub const PassTimingOptions = struct {
 299     collect_pass_ir_sizes: bool = false,
 300     allocation_snapshot_provider: ?PassAllocationSnapshotProvider = null,
 301 };
 302 
 303 pub const PassAllocationSnapshot = struct {
 304     alloc_count: u64 = 0,
 305     free_count: u64 = 0,
 306     alloc_bytes: u64 = 0,
 307 
 308     fn delta(after: PassAllocationSnapshot, before: PassAllocationSnapshot) PassAllocationSnapshot {
 309         return .{
 310             .alloc_count = after.alloc_count -| before.alloc_count,
 311             .free_count = after.free_count -| before.free_count,
 312             .alloc_bytes = after.alloc_bytes -| before.alloc_bytes,
 313         };
 314     }
 315 };
 316 
 317 pub const PassAllocationSnapshotProvider = struct {
 318     context: ?*const anyopaque = null,
 319     snapshot: *const fn (?*const anyopaque) PassAllocationSnapshot,
 320 
 321     fn read(self: PassAllocationSnapshotProvider) PassAllocationSnapshot {
 322         return self.snapshot(self.context);
 323     }
 324 };
 325 
 326 const PassTimingEntry = struct {
 327     total_ns: i128 = 0,
 328     count: u64 = 0,
 329     modified_count: u64 = 0,
 330     max_ns: i128 = 0,
 331     min_ns: i128 = std.math.maxInt(i128),
 332 };
 333 
 334 const PassIrSizeEntry = struct {
 335     count: u64 = 0,
 336     before_total: u64 = 0,
 337     after_total: u64 = 0,
 338     delta_total: i128 = 0,
 339 };
 340 
 341 const PassMemoryEntry = struct {
 342     count: u64 = 0,
 343     alloc_count: u64 = 0,
 344     free_count: u64 = 0,
 345     alloc_bytes: u64 = 0,
 346 };
 347 
 348 pub const PipelineTimingSummary = struct {
 349     target: ?[]const u8,
 350     depth: usize,
 351     total_ns: i128,
 352 };
 353 
 354 pub const PassRunTimingSummary = struct {
 355     ordinal: u64,
 356     name: []const u8,
 357     elapsed_ns: i128,
 358     modified: bool,
 359     op_count_before: ?u64 = null,
 360     op_count_after: ?u64 = null,
 361     op_count_delta: ?i128 = null,
 362     alloc_count: ?u64 = null,
 363     free_count: ?u64 = null,
 364     alloc_bytes: ?u64 = null,
 365 };
 366 
 367 pub const PassTimingSummary = struct {
 368     name: []const u8,
 369     total_ns: i128,
 370     count: u64,
 371     modified_count: u64 = 0,
 372     max_ns: i128,
 373     min_ns: i128,
 374     op_count_before: ?u64 = null,
 375     op_count_after: ?u64 = null,
 376     op_count_delta: ?i128 = null,
 377     alloc_count: ?u64 = null,
 378     free_count: ?u64 = null,
 379     alloc_bytes: ?u64 = null,
 380 };
 381 
 382 pub const TimingInstrumentation = struct {
 383     allocator: std.mem.Allocator,
 384     options: PassTimingOptions,
 385     pass_times: std.StringHashMapUnmanaged(PassTimingEntry),
 386     analysis_times: std.StringHashMapUnmanaged(PassTimingEntry),
 387     pass_ir_sizes: std.StringHashMapUnmanaged(PassIrSizeEntry),
 388     pass_memory: std.StringHashMapUnmanaged(PassMemoryEntry),
 389     pass_runs: std.ArrayListUnmanaged(PassRunTimingSummary),
 390     pipeline_times: std.ArrayListUnmanaged(PipelineTimingSummary),
 391     timing_stack: std.ArrayListUnmanaged(i128),
 392     pass_ir_size_stack: std.ArrayListUnmanaged(u64),
 393     pass_memory_stack: std.ArrayListUnmanaged(PassAllocationSnapshot),
 394 
 395     const FAILED_PUSH_SENTINEL: i128 = std.math.minInt(i128);
 396     const FAILED_IR_SIZE_SENTINEL: u64 = std.math.maxInt(u64);
 397     const FAILED_MEMORY_SNAPSHOT = PassAllocationSnapshot{
 398         .alloc_count = std.math.maxInt(u64),
 399         .free_count = std.math.maxInt(u64),
 400         .alloc_bytes = std.math.maxInt(u64),
 401     };
 402 
 403     pub fn init(allocator: std.mem.Allocator) TimingInstrumentation {
 404         return initWithOptions(allocator, .{});
 405     }
 406 
 407     pub fn initWithOptions(
 408         allocator: std.mem.Allocator,
 409         options: PassTimingOptions,
 410     ) TimingInstrumentation {
 411         return .{
 412             .allocator = allocator,
 413             .options = options,
 414             .pass_times = .{},
 415             .analysis_times = .{},
 416             .pass_ir_sizes = .{},
 417             .pass_memory = .{},
 418             .pass_runs = .empty,
 419             .pipeline_times = .empty,
 420             .timing_stack = .empty,
 421             .pass_ir_size_stack = .empty,
 422             .pass_memory_stack = .empty,
 423         };
 424     }
 425 
 426     pub fn deinit(self: *TimingInstrumentation) void {
 427         self.pass_times.deinit(self.allocator);
 428         self.analysis_times.deinit(self.allocator);
 429         self.pass_ir_sizes.deinit(self.allocator);
 430         self.pass_memory.deinit(self.allocator);
 431         self.pass_runs.deinit(self.allocator);
 432         self.pipeline_times.deinit(self.allocator);
 433         self.timing_stack.deinit(self.allocator);
 434         self.pass_ir_size_stack.deinit(self.allocator);
 435         self.pass_memory_stack.deinit(self.allocator);
 436     }
 437 
 438     pub fn setAllocationSnapshotProvider(
 439         self: *TimingInstrumentation,
 440         provider: ?PassAllocationSnapshotProvider,
 441     ) void {
 442         self.options.allocation_snapshot_provider = provider;
 443     }
 444 
 445     pub fn instrumentation(self: *TimingInstrumentation) PassInstrumentation {
 446         return .{
 447             .ctx = self,
 448             .runBeforePipeline = beforePipelineImpl,
 449             .runAfterPipeline = afterPipelineImpl,
 450             .runBeforePass = beforePassImpl,
 451             .runAfterPass = afterPassImpl,
 452             .runAfterPassFailed = afterPassFailedImpl,
 453             .runBeforeAnalysis = beforeAnalysisImpl,
 454             .runAfterAnalysis = afterAnalysisImpl,
 455         };
 456     }
 457 
 458     fn pushTimestamp(self: *TimingInstrumentation) void {
 459         const timestamp = nowNanos();
 460         self.timing_stack.append(self.allocator, timestamp) catch {
 461             self.timing_stack.append(self.allocator, FAILED_PUSH_SENTINEL) catch {};
 462         };
 463     }
 464 
 465     fn popTimestamp(self: *TimingInstrumentation) ?i128 {
 466         if (self.timing_stack.pop()) |value| {
 467             if (value == FAILED_PUSH_SENTINEL) {
 468                 return null;
 469             }
 470             return value;
 471         }
 472         return null;
 473     }
 474 
 475     fn beforePipelineImpl(ctx: ?*anyopaque, _: PipelineInfo, _: *ir.Operation) void {
 476         const self: *TimingInstrumentation = @ptrCast(@alignCast(ctx.?));
 477         self.pushTimestamp();
 478     }
 479 
 480     fn afterPipelineImpl(ctx: ?*anyopaque, info: PipelineInfo, _: *ir.Operation, _: bool) void {
 481         const self: *TimingInstrumentation = @ptrCast(@alignCast(ctx.?));
 482         if (self.popTimestamp()) |start| {
 483             const elapsed = nowNanos() - start;
 484             self.recordPipelineTiming(info.target_op_name, info.depth, elapsed);
 485         }
 486     }
 487 
 488     fn beforePassImpl(ctx: ?*anyopaque, info: PassInfo) void {
 489         const self: *TimingInstrumentation = @ptrCast(@alignCast(ctx.?));
 490         self.pushTimestamp();
 491         if (self.options.collect_pass_ir_sizes) {
 492             self.pushPassIrSize(info);
 493         }
 494         if (self.options.allocation_snapshot_provider != null) {
 495             self.pushPassMemory();
 496         }
 497     }
 498 
 499     fn afterPassImpl(ctx: ?*anyopaque, info: PassInfo, modified: bool) void {
 500         recordPassTime(ctx, info, modified);
 501     }
 502 
 503     fn afterPassFailedImpl(ctx: ?*anyopaque, info: PassInfo) void {
 504         recordPassTime(ctx, info, false);
 505     }
 506 
 507     fn recordPassTime(ctx: ?*anyopaque, info: PassInfo, modified: bool) void {
 508         const self: *TimingInstrumentation = @ptrCast(@alignCast(ctx.?));
 509         const elapsed = if (self.popTimestamp()) |start| nowNanos() - start else null;
 510         const before = if (self.options.collect_pass_ir_sizes) self.popPassIrSize() else null;
 511         const before_memory = if (self.options.allocation_snapshot_provider != null) self.popPassMemory() else null;
 512         var after: ?u64 = null;
 513         var delta: ?i128 = null;
 514         var memory_delta: ?PassAllocationSnapshot = null;
 515         if (before_memory) |snapshot_before| {
 516             if (self.options.allocation_snapshot_provider) |provider| {
 517                 memory_delta = PassAllocationSnapshot.delta(provider.read(), snapshot_before);
 518                 self.recordPassMemoryValues(info.name, memory_delta.?);
 519             }
 520         }
 521         if (before) |before_count| {
 522             if (info.target_op) |op| {
 523                 const after_count = countOperationTree(op);
 524                 after = after_count;
 525                 delta = @as(i128, @intCast(after_count)) - @as(i128, @intCast(before_count));
 526                 self.recordPassIrSizeValues(info.name, before_count, after_count, delta.?);
 527             }
 528         }
 529         if (elapsed) |actual| {
 530             self.record(&self.pass_times, info.name, actual, modified);
 531             self.recordPassRun(info.name, actual, modified, before, after, delta, memory_delta);
 532         }
 533     }
 534 
 535     fn recordPassRun(
 536         self: *TimingInstrumentation,
 537         name: []const u8,
 538         elapsed_ns: i128,
 539         modified: bool,
 540         op_count_before: ?u64,
 541         op_count_after: ?u64,
 542         op_count_delta: ?i128,
 543         memory_delta: ?PassAllocationSnapshot,
 544     ) void {
 545         self.pass_runs.append(self.allocator, .{
 546             .ordinal = @intCast(self.pass_runs.items.len + 1),
 547             .name = name,
 548             .elapsed_ns = elapsed_ns,
 549             .modified = modified,
 550             .op_count_before = op_count_before,
 551             .op_count_after = op_count_after,
 552             .op_count_delta = op_count_delta,
 553             .alloc_count = if (memory_delta) |delta| delta.alloc_count else null,
 554             .free_count = if (memory_delta) |delta| delta.free_count else null,
 555             .alloc_bytes = if (memory_delta) |delta| delta.alloc_bytes else null,
 556         }) catch {};
 557     }
 558 
 559     fn recordPassIrSizeValues(
 560         self: *TimingInstrumentation,
 561         name: []const u8,
 562         before: u64,
 563         after: u64,
 564         delta: i128,
 565     ) void {
 566         const gop = self.pass_ir_sizes.getOrPut(self.allocator, name) catch return;
 567         if (!gop.found_existing) {
 568             gop.value_ptr.* = .{};
 569         }
 570         gop.value_ptr.count += 1;
 571         gop.value_ptr.before_total +|= before;
 572         gop.value_ptr.after_total +|= after;
 573         gop.value_ptr.delta_total += delta;
 574     }
 575 
 576     fn recordPassMemoryValues(
 577         self: *TimingInstrumentation,
 578         name: []const u8,
 579         memory_delta: PassAllocationSnapshot,
 580     ) void {
 581         const gop = self.pass_memory.getOrPut(self.allocator, name) catch return;
 582         if (!gop.found_existing) {
 583             gop.value_ptr.* = .{};
 584         }
 585         gop.value_ptr.count += 1;
 586         gop.value_ptr.alloc_count +|= memory_delta.alloc_count;
 587         gop.value_ptr.free_count +|= memory_delta.free_count;
 588         gop.value_ptr.alloc_bytes +|= memory_delta.alloc_bytes;
 589     }
 590 
 591     fn beforeAnalysisImpl(ctx: ?*anyopaque, _: AnalysisInfo) void {
 592         const self: *TimingInstrumentation = @ptrCast(@alignCast(ctx.?));
 593         self.pushTimestamp();
 594     }
 595 
 596     fn afterAnalysisImpl(ctx: ?*anyopaque, info: AnalysisInfo) void {
 597         const self: *TimingInstrumentation = @ptrCast(@alignCast(ctx.?));
 598         if (self.popTimestamp()) |start| {
 599             const elapsed = nowNanos() - start;
 600             self.record(&self.analysis_times, info.name, elapsed, false);
 601         }
 602     }
 603 
 604     fn record(
 605         self: *TimingInstrumentation,
 606         table: *std.StringHashMapUnmanaged(PassTimingEntry),
 607         name: []const u8,
 608         elapsed: i128,
 609         modified: bool,
 610     ) void {
 611         const gop = table.getOrPut(self.allocator, name) catch return;
 612         if (!gop.found_existing) {
 613             gop.value_ptr.* = .{};
 614         }
 615         gop.value_ptr.total_ns += elapsed;
 616         gop.value_ptr.count += 1;
 617         if (modified) gop.value_ptr.modified_count += 1;
 618         if (elapsed > gop.value_ptr.max_ns) gop.value_ptr.max_ns = elapsed;
 619         if (elapsed < gop.value_ptr.min_ns) gop.value_ptr.min_ns = elapsed;
 620     }
 621 
 622     pub fn recordPipelineTiming(
 623         self: *TimingInstrumentation,
 624         target: ?[]const u8,
 625         depth: usize,
 626         elapsed_ns: i128,
 627     ) void {
 628         self.pipeline_times.append(self.allocator, .{
 629             .target = target,
 630             .depth = depth,
 631             .total_ns = elapsed_ns,
 632         }) catch {};
 633     }
 634 
 635     fn pushPassIrSize(self: *TimingInstrumentation, info: PassInfo) void {
 636         const size = if (info.target_op) |op| countOperationTree(op) else FAILED_IR_SIZE_SENTINEL;
 637         self.pass_ir_size_stack.append(self.allocator, size) catch {
 638             self.pass_ir_size_stack.append(self.allocator, FAILED_IR_SIZE_SENTINEL) catch {};
 639         };
 640     }
 641 
 642     fn popPassIrSize(self: *TimingInstrumentation) ?u64 {
 643         const size = self.pass_ir_size_stack.pop() orelse return null;
 644         if (size == FAILED_IR_SIZE_SENTINEL) return null;
 645         return size;
 646     }
 647 
 648     fn pushPassMemory(self: *TimingInstrumentation) void {
 649         const provider = self.options.allocation_snapshot_provider orelse return;
 650         self.pass_memory_stack.append(self.allocator, provider.read()) catch {
 651             self.pass_memory_stack.append(self.allocator, FAILED_MEMORY_SNAPSHOT) catch {};
 652         };
 653     }
 654 
 655     fn popPassMemory(self: *TimingInstrumentation) ?PassAllocationSnapshot {
 656         const snapshot = self.pass_memory_stack.pop() orelse return null;
 657         if (snapshot.alloc_count == FAILED_MEMORY_SNAPSHOT.alloc_count and
 658             snapshot.free_count == FAILED_MEMORY_SNAPSHOT.free_count and
 659             snapshot.alloc_bytes == FAILED_MEMORY_SNAPSHOT.alloc_bytes)
 660         {
 661             return null;
 662         }
 663         return snapshot;
 664     }
 665 
 666     fn countOperationTree(op: *ir.Operation) u64 {
 667         var count: u64 = 1;
 668         for (op.regions.items) |*region| {
 669             var block_iter = region.getBlocks();
 670             while (block_iter.next()) |block| {
 671                 var op_node = block.operations.head;
 672                 while (op_node) |node| {
 673                     const child: *ir.Operation = @ptrCast(@alignCast(node));
 674                     count +|= countOperationTree(child);
 675                     op_node = child.next_op;
 676                 }
 677             }
 678         }
 679         return count;
 680     }
 681 
 682     pub fn getPassTime(self: *const TimingInstrumentation, name: []const u8) ?i128 {
 683         if (self.pass_times.get(name)) |entry| {
 684             return entry.total_ns;
 685         }
 686         return null;
 687     }
 688 
 689     pub fn getPassCount(self: *const TimingInstrumentation, name: []const u8) ?u64 {
 690         if (self.pass_times.get(name)) |entry| {
 691             return entry.count;
 692         }
 693         return null;
 694     }
 695 
 696     pub fn getPassModifiedCount(self: *const TimingInstrumentation, name: []const u8) ?u64 {
 697         if (self.pass_times.get(name)) |entry| {
 698             return entry.modified_count;
 699         }
 700         return null;
 701     }
 702 
 703     pub fn getPassOpCountBefore(self: *const TimingInstrumentation, name: []const u8) ?u64 {
 704         if (self.pass_ir_sizes.get(name)) |entry| {
 705             return entry.before_total;
 706         }
 707         return null;
 708     }
 709 
 710     pub fn getPassOpCountAfter(self: *const TimingInstrumentation, name: []const u8) ?u64 {
 711         if (self.pass_ir_sizes.get(name)) |entry| {
 712             return entry.after_total;
 713         }
 714         return null;
 715     }
 716 
 717     pub fn getPassOpCountDelta(self: *const TimingInstrumentation, name: []const u8) ?i128 {
 718         if (self.pass_ir_sizes.get(name)) |entry| {
 719             return entry.delta_total;
 720         }
 721         return null;
 722     }
 723 
 724     pub fn getPassAllocCount(self: *const TimingInstrumentation, name: []const u8) ?u64 {
 725         if (self.pass_memory.get(name)) |entry| {
 726             return entry.alloc_count;
 727         }
 728         return null;
 729     }
 730 
 731     pub fn getPassFreeCount(self: *const TimingInstrumentation, name: []const u8) ?u64 {
 732         if (self.pass_memory.get(name)) |entry| {
 733             return entry.free_count;
 734         }
 735         return null;
 736     }
 737 
 738     pub fn getPassAllocBytes(self: *const TimingInstrumentation, name: []const u8) ?u64 {
 739         if (self.pass_memory.get(name)) |entry| {
 740             return entry.alloc_bytes;
 741         }
 742         return null;
 743     }
 744 
 745     pub fn getAnalysisTime(self: *const TimingInstrumentation, name: []const u8) ?i128 {
 746         if (self.analysis_times.get(name)) |entry| {
 747             return entry.total_ns;
 748         }
 749         return null;
 750     }
 751 
 752     pub fn getAnalysisCount(self: *const TimingInstrumentation, name: []const u8) ?u64 {
 753         if (self.analysis_times.get(name)) |entry| {
 754             return entry.count;
 755         }
 756         return null;
 757     }
 758 
 759     pub fn hasPassTimings(self: *const TimingInstrumentation) bool {
 760         return self.pass_times.count() != 0;
 761     }
 762 
 763     pub fn hasAnalysisTimings(self: *const TimingInstrumentation) bool {
 764         return self.analysis_times.count() != 0;
 765     }
 766 
 767     pub fn hasPipelineTimings(self: *const TimingInstrumentation) bool {
 768         return self.pipeline_times.items.len != 0;
 769     }
 770 
 771     pub fn collectsPassIrSizes(self: *const TimingInstrumentation) bool {
 772         return self.options.collect_pass_ir_sizes;
 773     }
 774 
 775     pub fn collectsPassMemory(self: *const TimingInstrumentation) bool {
 776         return self.options.allocation_snapshot_provider != null;
 777     }
 778 
 779     pub fn pipelineSummariesAlloc(
 780         self: *const TimingInstrumentation,
 781         allocator: std.mem.Allocator,
 782     ) ![]PipelineTimingSummary {
 783         const summaries = try allocator.alloc(PipelineTimingSummary, self.pipeline_times.items.len);
 784         @memcpy(summaries, self.pipeline_times.items);
 785         return summaries;
 786     }
 787 
 788     pub fn passRunSummariesAlloc(
 789         self: *const TimingInstrumentation,
 790         allocator: std.mem.Allocator,
 791     ) ![]PassRunTimingSummary {
 792         const summaries = try allocator.alloc(PassRunTimingSummary, self.pass_runs.items.len);
 793         @memcpy(summaries, self.pass_runs.items);
 794         return summaries;
 795     }
 796 
 797     pub fn passSummariesAlloc(
 798         self: *const TimingInstrumentation,
 799         allocator: std.mem.Allocator,
 800     ) ![]PassTimingSummary {
 801         var summaries = try allocator.alloc(PassTimingSummary, self.pass_times.count());
 802         errdefer allocator.free(summaries);
 803 
 804         var index: usize = 0;
 805         var iter = self.pass_times.iterator();
 806         while (iter.next()) |entry| : (index += 1) {
 807             summaries[index] = timingSummary(entry.key_ptr.*, entry.value_ptr.*);
 808             if (self.pass_ir_sizes.get(entry.key_ptr.*)) |ir_size| {
 809                 summaries[index].op_count_before = ir_size.before_total;
 810                 summaries[index].op_count_after = ir_size.after_total;
 811                 summaries[index].op_count_delta = ir_size.delta_total;
 812             }
 813             if (self.pass_memory.get(entry.key_ptr.*)) |memory| {
 814                 summaries[index].alloc_count = memory.alloc_count;
 815                 summaries[index].free_count = memory.free_count;
 816                 summaries[index].alloc_bytes = memory.alloc_bytes;
 817             }
 818         }
 819         std.mem.sort(PassTimingSummary, summaries, {}, timingSummaryLessThan);
 820         return summaries;
 821     }
 822 
 823     pub fn analysisSummariesAlloc(
 824         self: *const TimingInstrumentation,
 825         allocator: std.mem.Allocator,
 826     ) ![]PassTimingSummary {
 827         var summaries = try allocator.alloc(PassTimingSummary, self.analysis_times.count());
 828         errdefer allocator.free(summaries);
 829 
 830         var index: usize = 0;
 831         var iter = self.analysis_times.iterator();
 832         while (iter.next()) |entry| : (index += 1) {
 833             summaries[index] = timingSummary(entry.key_ptr.*, entry.value_ptr.*);
 834         }
 835         std.mem.sort(PassTimingSummary, summaries, {}, timingSummaryLessThan);
 836         return summaries;
 837     }
 838 
 839     fn timingSummary(name: []const u8, entry: PassTimingEntry) PassTimingSummary {
 840         return .{
 841             .name = name,
 842             .total_ns = entry.total_ns,
 843             .count = entry.count,
 844             .modified_count = entry.modified_count,
 845             .max_ns = entry.max_ns,
 846             .min_ns = entry.min_ns,
 847         };
 848     }
 849 
 850     fn timingSummaryLessThan(_: void, lhs: PassTimingSummary, rhs: PassTimingSummary) bool {
 851         return std.mem.lessThan(u8, lhs.name, rhs.name);
 852     }
 853 };
 854 
 855 pub const IRPrintingOptions = struct {
 856     print_before: bool = false,
 857     print_after: bool = false,
 858     print_after_change: bool = false,
 859     print_before_pipeline: bool = false,
 860     print_after_pipeline: bool = false,
 861     writer: ?*std.Io.Writer = null,
 862 };
 863 
 864 pub const IRPrintingInstrumentation = struct {
 865     allocator: std.mem.Allocator,
 866     options: IRPrintingOptions,
 867     last_hash: ?u64 = null,
 868     output: std.ArrayListUnmanaged(u8),
 869 
 870     pub fn init(
 871         allocator: std.mem.Allocator,
 872         options: IRPrintingOptions,
 873     ) IRPrintingInstrumentation {
 874         return .{
 875             .allocator = allocator,
 876             .options = options,
 877             .output = .empty,
 878         };
 879     }
 880 
 881     pub fn deinit(self: *IRPrintingInstrumentation) void {
 882         self.output.deinit(self.allocator);
 883     }
 884 
 885     pub fn instrumentation(self: *IRPrintingInstrumentation) PassInstrumentation {
 886         return .{
 887             .ctx = self,
 888             .runBeforePipeline = if (self.options.print_before_pipeline) beforePipelineImpl else null,
 889             .runAfterPipeline = if (self.options.print_after_pipeline) afterPipelineImpl else null,
 890             .runBeforePass = if (self.options.print_before) beforePassImpl else null,
 891             .runAfterPass = if (self.options.print_after or self.options.print_after_change) afterPassImpl else null,
 892         };
 893     }
 894 
 895     fn beforePipelineImpl(ctx: ?*anyopaque, info: PipelineInfo, op: *ir.Operation) void {
 896         const self: *IRPrintingInstrumentation = @ptrCast(@alignCast(ctx.?));
 897         self.printHeader("Before pipeline", info.target_op_name, info.depth);
 898         self.printOp(op);
 899     }
 900 
 901     fn afterPipelineImpl(ctx: ?*anyopaque, info: PipelineInfo, op: *ir.Operation, failed: bool) void {
 902         const self: *IRPrintingInstrumentation = @ptrCast(@alignCast(ctx.?));
 903         if (failed) {
 904             self.printHeader("After pipeline (FAILED)", info.target_op_name, info.depth);
 905         } else {
 906             self.printHeader("After pipeline", info.target_op_name, info.depth);
 907         }
 908         self.printOp(op);
 909     }
 910 
 911     fn beforePassImpl(ctx: ?*anyopaque, info: PassInfo) void {
 912         const self: *IRPrintingInstrumentation = @ptrCast(@alignCast(ctx.?));
 913         self.printHeader("Before pass", info.name, 0);
 914         if (info.target_op) |op| {
 915             self.last_hash = self.computeOpHash(op);
 916             self.printOp(op);
 917         }
 918     }
 919 
 920     fn afterPassImpl(ctx: ?*anyopaque, info: PassInfo, modified: bool) void {
 921         const self: *IRPrintingInstrumentation = @ptrCast(@alignCast(ctx.?));
 922 
 923         if (self.options.print_after_change) {
 924             if (info.target_op) |op| {
 925                 const new_hash = self.computeOpHash(op);
 926                 if (self.last_hash != null and new_hash != null) {
 927                     if (self.last_hash.? == new_hash.?) {
 928                         return;
 929                     }
 930                 }
 931             }
 932         }
 933 
 934         const label = if (modified) "After pass (modified)" else "After pass";
 935         self.printHeader(label, info.name, 0);
 936         if (info.target_op) |op| {
 937             self.printOp(op);
 938         }
 939     }
 940 
 941     fn printHeader(self: *IRPrintingInstrumentation, label: []const u8, name: ?[]const u8, depth: usize) void {
 942         self.print("\n", .{});
 943         for (0..depth) |_| {
 944             self.print("  ", .{});
 945         }
 946         self.print("// === {s}", .{label});
 947         if (name) |n| {
 948             self.print(": {s}", .{n});
 949         }
 950         self.print(" ===\n", .{});
 951     }
 952 
 953     fn printOp(self: *IRPrintingInstrumentation, op: *ir.Operation) void {
 954         self.output.clearRetainingCapacity();
 955 
 956         self.print("{s}", .{op.name.name});
 957         self.printAttrs(op);
 958         self.print("\n", .{});
 959     }
 960 
 961     fn printAttrs(self: *IRPrintingInstrumentation, op: *const ir.Operation) void {
 962         var attrs = op.getAttrs();
 963         const first = attrs.next() orelse return;
 964         self.print(" {{{s}", .{first.name});
 965         while (attrs.next()) |attr| self.print(", {s}", .{attr.name});
 966         self.print("}}", .{});
 967     }
 968 
 969     fn print(self: *IRPrintingInstrumentation, comptime fmt: []const u8, args: anytype) void {
 970         if (self.options.writer) |writer| {
 971             writer.print(fmt, args) catch {};
 972         } else {
 973             pretty.diagnostic.writeStderrText(fmt, args);
 974         }
 975     }
 976 
 977     fn computeOpHash(self: *IRPrintingInstrumentation, op: *ir.Operation) ?u64 {
 978         return hashing.operationFingerprint(self.allocator, op) catch null;
 979     }
 980 };
 981 
 982 pub const VerifierInstrumentationFailure = struct {
 983     pass_name: []const u8,
 984     err: anyerror,
 985 };
 986 
 987 pub const VerifierInstrumentationOptions = struct {
 988     verify_options: verify_mod.VerifyOptions = verify_mod.default_options,
 989     stop_on_failure: bool = true,
 990 };
 991 
 992 pub const VerifierInstrumentation = struct {
 993     allocator: std.mem.Allocator,
 994     options: verify_mod.VerifyOptions,
 995     failures: std.ArrayListUnmanaged(VerifierInstrumentationFailure),
 996     stop_on_failure: bool,
 997     has_stopped: bool,
 998 
 999     pub fn init(allocator: std.mem.Allocator, options: verify_mod.VerifyOptions) VerifierInstrumentation {
1000         return initWithOptions(allocator, .{ .verify_options = options });
1001     }
1002 
1003     pub fn initWithOptions(
1004         allocator: std.mem.Allocator,
1005         options: VerifierInstrumentationOptions,
1006     ) VerifierInstrumentation {
1007         return .{
1008             .allocator = allocator,
1009             .options = options.verify_options,
1010             .failures = .empty,
1011             .stop_on_failure = options.stop_on_failure,
1012             .has_stopped = false,
1013         };
1014     }
1015 
1016     pub fn deinit(self: *VerifierInstrumentation) void {
1017         self.failures.deinit(self.allocator);
1018     }
1019 
1020     pub fn instrumentation(self: *VerifierInstrumentation) PassInstrumentation {
1021         return .{
1022             .ctx = self,
1023             .runAfterPass = afterPassImpl,
1024         };
1025     }
1026 
1027     fn afterPassImpl(ctx: ?*anyopaque, info: PassInfo, _: bool) void {
1028         const self: *VerifierInstrumentation = @ptrCast(@alignCast(ctx.?));
1029 
1030         if (self.stop_on_failure and self.has_stopped) {
1031             return;
1032         }
1033 
1034         if (info.target_op) |op| {
1035             verify_mod.verifyOperation(op, self.options) catch |err| {
1036                 self.failures.append(self.allocator, .{
1037                     .pass_name = info.name,
1038                     .err = err,
1039                 }) catch {};
1040 
1041                 if (self.stop_on_failure) {
1042                     self.has_stopped = true;
1043                 }
1044             };
1045         }
1046     }
1047 
1048     pub fn hasFailures(self: *const VerifierInstrumentation) bool {
1049         return self.failures.items.len > 0;
1050     }
1051 
1052     pub fn getFailureCount(self: *const VerifierInstrumentation) usize {
1053         return self.failures.items.len;
1054     }
1055 
1056     pub fn wasStopped(self: *const VerifierInstrumentation) bool {
1057         return self.has_stopped;
1058     }
1059 
1060     pub fn reset(self: *VerifierInstrumentation) void {
1061         self.failures.clearRetainingCapacity();
1062         self.has_stopped = false;
1063     }
1064 
1065     pub fn printFailures(self: *const VerifierInstrumentation, writer: anytype) !void {
1066         for (self.failures.items) |failure| {
1067             try writer.print("Verification failed after pass '{s}': {}\n", .{ failure.pass_name, failure.err });
1068         }
1069         if (self.has_stopped) {
1070             try writer.print("(Verification stopped after first failure)\n", .{});
1071         }
1072     }
1073 };
1074 
1075 pub const CountingInstrumentation = struct {
1076     pipeline_count: usize = 0,
1077     pass_count: usize = 0,
1078     pass_failures: usize = 0,
1079     analysis_count: usize = 0,
1080 
1081     pub fn instrumentation(self: *CountingInstrumentation) PassInstrumentation {
1082         return .{
1083             .ctx = self,
1084             .runBeforePipeline = beforePipelineImpl,
1085             .runBeforePass = beforePassImpl,
1086             .runAfterPassFailed = afterPassFailedImpl,
1087             .runBeforeAnalysis = beforeAnalysisImpl,
1088         };
1089     }
1090 
1091     fn beforePipelineImpl(ctx: ?*anyopaque, _: PipelineInfo, _: *ir.Operation) void {
1092         const self: *CountingInstrumentation = @ptrCast(@alignCast(ctx.?));
1093         self.pipeline_count += 1;
1094     }
1095 
1096     fn beforePassImpl(ctx: ?*anyopaque, _: PassInfo) void {
1097         const self: *CountingInstrumentation = @ptrCast(@alignCast(ctx.?));
1098         self.pass_count += 1;
1099     }
1100 
1101     fn afterPassFailedImpl(ctx: ?*anyopaque, _: PassInfo) void {
1102         const self: *CountingInstrumentation = @ptrCast(@alignCast(ctx.?));
1103         self.pass_failures += 1;
1104     }
1105 
1106     fn beforeAnalysisImpl(ctx: ?*anyopaque, _: AnalysisInfo) void {
1107         const self: *CountingInstrumentation = @ptrCast(@alignCast(ctx.?));
1108         self.analysis_count += 1;
1109     }
1110 
1111     pub fn reset(self: *CountingInstrumentation) void {
1112         self.* = .{};
1113     }
1114 };
1115 
1116 const BasicHookTracker = struct {
1117     var called_before: bool = false;
1118     var called_after: bool = false;
1119 
1120     fn beforeImpl(_: ?*anyopaque, _: PassInfo) void {
1121         called_before = true;
1122     }
1123 
1124     fn afterImpl(_: ?*anyopaque, _: PassInfo, _: bool) void {
1125         called_after = true;
1126     }
1127 };
1128 
1129 fn ChainedInstrumentationType(comptime before_value: u8, comptime after_value: u8) type {
1130     return struct {
1131         var order_ptr: *[4]u8 = undefined;
1132         var idx_ptr: *usize = undefined;
1133 
1134         fn before(_: ?*anyopaque, _: PassInfo) void {
1135             order_ptr[idx_ptr.*] = before_value;
1136             idx_ptr.* += 1;
1137         }
1138 
1139         fn after(_: ?*anyopaque, _: PassInfo, _: bool) void {
1140             order_ptr[idx_ptr.*] = after_value;
1141             idx_ptr.* += 1;
1142         }
1143     };
1144 }
1145 
1146 const FirstChainedInstrumentation = ChainedInstrumentationType('1', 'A');
1147 const SecondChainedInstrumentation = ChainedInstrumentationType('2', 'B');
1148 
1149 test "PassInstrumentation basic hooks" {
1150     const testing = std.testing;
1151 
1152     BasicHookTracker.called_before = false;
1153     BasicHookTracker.called_after = false;
1154 
1155     const inst = PassInstrumentation{
1156         .runBeforePass = BasicHookTracker.beforeImpl,
1157         .runAfterPass = BasicHookTracker.afterImpl,
1158     };
1159 
1160     const info = PassInfo{
1161         .name = "test-pass",
1162         .description = "Test pass",
1163         .target_op = null,
1164     };
1165 
1166     inst.beforePass(info);
1167     try testing.expect(BasicHookTracker.called_before);
1168     try testing.expect(!BasicHookTracker.called_after);
1169 
1170     inst.afterPass(info, false);
1171     try testing.expect(BasicHookTracker.called_after);
1172 }
1173 
1174 test "PassInstrumentor chains multiple instrumentations" {
1175     const testing = std.testing;
1176     const allocator = testing.allocator;
1177 
1178     var instrumentor = PassInstrumentor.init(allocator);
1179     defer instrumentor.deinit();
1180 
1181     var order: [4]u8 = undefined;
1182     var idx: usize = 0;
1183 
1184     FirstChainedInstrumentation.order_ptr = &order;
1185     FirstChainedInstrumentation.idx_ptr = &idx;
1186     SecondChainedInstrumentation.order_ptr = &order;
1187     SecondChainedInstrumentation.idx_ptr = &idx;
1188 
1189     try instrumentor.addInstrumentation(.{
1190         .runBeforePass = FirstChainedInstrumentation.before,
1191         .runAfterPass = FirstChainedInstrumentation.after,
1192     });
1193     try instrumentor.addInstrumentation(.{
1194         .runBeforePass = SecondChainedInstrumentation.before,
1195         .runAfterPass = SecondChainedInstrumentation.after,
1196     });
1197 
1198     const info = PassInfo{ .name = "test", .description = "", .target_op = null };
1199 
1200     instrumentor.runBeforePass(info);
1201     instrumentor.runAfterPass(info, false);
1202 
1203     try testing.expectEqualStrings("12BA", &order);
1204 }
1205 
1206 test "TimingInstrumentation records pass times" {
1207     const testing = std.testing;
1208     const allocator = testing.allocator;
1209 
1210     var timing = TimingInstrumentation.init(allocator);
1211     defer timing.deinit();
1212 
1213     const inst = timing.instrumentation();
1214 
1215     const info = PassInfo{
1216         .name = "test-pass",
1217         .description = "Test pass",
1218         .target_op = null,
1219     };
1220 
1221     inst.beforePass(info);
1222     inst.afterPass(info, false);
1223 
1224     try testing.expect(timing.getPassCount("test-pass") != null);
1225     try testing.expectEqual(@as(u64, 1), timing.getPassCount("test-pass").?);
1226     try testing.expectEqual(@as(u64, 0), timing.getPassModifiedCount("test-pass").?);
1227     try testing.expect(timing.getPassTime("test-pass") != null);
1228     try testing.expect(!timing.collectsPassIrSizes());
1229     try testing.expect(timing.getPassOpCountBefore("test-pass") == null);
1230 }
1231 
1232 test "TimingInstrumentation records pass IR operation deltas when enabled" {
1233     const testing = std.testing;
1234     const allocator = testing.allocator;
1235     const test_dialect = @import("../dialects/fixture/root.zig");
1236 
1237     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1238     defer ctx.deinit(allocator);
1239 
1240     const loc = ir.Location.getUnknown();
1241     const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1242 
1243     var timing = TimingInstrumentation.initWithOptions(allocator, .{ .collect_pass_ir_sizes = true });
1244     defer timing.deinit();
1245     const inst = timing.instrumentation();
1246 
1247     const info = PassInfo{
1248         .name = "grow-ir",
1249         .description = "Test pass that adds an operation",
1250         .target_op = module_op.op,
1251     };
1252 
1253     inst.beforePass(info);
1254     const func_op = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "f", &.{});
1255     try module_op.getBodyBlock().addOperation(func_op.op);
1256     inst.afterPass(info, true);
1257 
1258     try testing.expect(timing.collectsPassIrSizes());
1259     try testing.expectEqual(@as(u64, 1), timing.getPassModifiedCount("grow-ir").?);
1260     try testing.expectEqual(@as(u64, 1), timing.getPassOpCountBefore("grow-ir").?);
1261     try testing.expectEqual(@as(u64, 2), timing.getPassOpCountAfter("grow-ir").?);
1262     try testing.expectEqual(@as(i128, 1), timing.getPassOpCountDelta("grow-ir").?);
1263 
1264     const summaries = try timing.passSummariesAlloc(allocator);
1265     defer allocator.free(summaries);
1266     try testing.expectEqual(@as(usize, 1), summaries.len);
1267     try testing.expectEqualStrings("grow-ir", summaries[0].name);
1268     try testing.expectEqual(@as(u64, 1), summaries[0].modified_count);
1269     try testing.expectEqual(@as(u64, 1), summaries[0].op_count_before.?);
1270     try testing.expectEqual(@as(u64, 2), summaries[0].op_count_after.?);
1271     try testing.expectEqual(@as(i128, 1), summaries[0].op_count_delta.?);
1272 
1273     const runs = try timing.passRunSummariesAlloc(allocator);
1274     defer allocator.free(runs);
1275     try testing.expectEqual(@as(usize, 1), runs.len);
1276     try testing.expectEqual(@as(u64, 1), runs[0].ordinal);
1277     try testing.expectEqualStrings("grow-ir", runs[0].name);
1278     try testing.expect(runs[0].modified);
1279     try testing.expectEqual(@as(u64, 1), runs[0].op_count_before.?);
1280     try testing.expectEqual(@as(u64, 2), runs[0].op_count_after.?);
1281     try testing.expectEqual(@as(i128, 1), runs[0].op_count_delta.?);
1282 }
1283 
1284 test "TimingInstrumentation preserves duplicate pass run order" {
1285     const testing = std.testing;
1286     const allocator = testing.allocator;
1287     const test_dialect = @import("../dialects/fixture/root.zig");
1288 
1289     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1290     defer ctx.deinit(allocator);
1291 
1292     const loc = ir.Location.getUnknown();
1293     const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, loc);
1294 
1295     var timing = TimingInstrumentation.initWithOptions(allocator, .{ .collect_pass_ir_sizes = true });
1296     defer timing.deinit();
1297     const inst = timing.instrumentation();
1298 
1299     const info = PassInfo{
1300         .name = "duplicate-pass",
1301         .description = "Duplicate pass",
1302         .target_op = module_op.op,
1303     };
1304 
1305     inst.beforePass(info);
1306     inst.afterPass(info, false);
1307 
1308     inst.beforePass(info);
1309     const func_op = try test_dialect.TestDialect.FuncOp.create(&ctx, loc, "f", &.{});
1310     try module_op.getBodyBlock().addOperation(func_op.op);
1311     inst.afterPass(info, true);
1312 
1313     const runs = try timing.passRunSummariesAlloc(allocator);
1314     defer allocator.free(runs);
1315     try testing.expectEqual(@as(usize, 2), runs.len);
1316     try testing.expectEqual(@as(u64, 1), runs[0].ordinal);
1317     try testing.expectEqual(@as(u64, 2), runs[1].ordinal);
1318     try testing.expectEqualStrings("duplicate-pass", runs[0].name);
1319     try testing.expectEqualStrings("duplicate-pass", runs[1].name);
1320     try testing.expect(!runs[0].modified);
1321     try testing.expect(runs[1].modified);
1322     try testing.expectEqual(@as(i128, 0), runs[0].op_count_delta.?);
1323     try testing.expectEqual(@as(i128, 1), runs[1].op_count_delta.?);
1324 }
1325 
1326 test "CountingInstrumentation counts events" {
1327     const testing = std.testing;
1328     const allocator = testing.allocator;
1329 
1330     var counter = CountingInstrumentation{};
1331     const inst = counter.instrumentation();
1332 
1333     var instrumentor = PassInstrumentor.init(allocator);
1334     defer instrumentor.deinit();
1335     try instrumentor.addInstrumentation(inst);
1336 
1337     const test_dialect = @import("../dialects/fixture/root.zig");
1338 
1339     var ctx = try ir.Context.init(allocator, ir.Context.Limits.testing);
1340     defer ctx.deinit(allocator);
1341 
1342     const module_op = try test_dialect.TestDialect.ModuleOp.create(&ctx, ir.Location.getUnknown());
1343 
1344     instrumentor.runBeforePipeline(.{ .target_op_name = null, .depth = 0 }, module_op.op);
1345     instrumentor.runBeforePass(.{ .name = "pass1", .description = "", .target_op = null });
1346     instrumentor.runAfterPass(.{ .name = "pass1", .description = "", .target_op = null }, false);
1347     instrumentor.runBeforePass(.{ .name = "pass2", .description = "", .target_op = null });
1348     instrumentor.runAfterPassFailed(.{ .name = "pass2", .description = "", .target_op = null });
1349     instrumentor.runAfterPipeline(.{ .target_op_name = null, .depth = 0 }, module_op.op, true);
1350 
1351     try testing.expectEqual(@as(usize, 1), counter.pipeline_count);
1352     try testing.expectEqual(@as(usize, 2), counter.pass_count);
1353     try testing.expectEqual(@as(usize, 1), counter.pass_failures);
1354 }
1355 
1356 test "VerifierInstrumentation verifies operations" {
1357     const testing = std.testing;
1358     const allocator = testing.allocator;
1359 
1360     const options = verify_mod.VerifyOptions{
1361         .check_terminators = false,
1362         .require_terminators = false,
1363         .recursive = false,
1364         .check_use_def = false,
1365         .check_cfg = false,
1366     };
1367 
1368     var verifier = VerifierInstrumentation.init(allocator, options);
1369     defer verifier.deinit();
1370 
1371     try testing.expect(!verifier.hasFailures());
1372     try testing.expectEqual(@as(usize, 0), verifier.getFailureCount());
1373 }
1374 
1375 test "IRPrintingInstrumentation print_after_change" {
1376     const testing = std.testing;
1377     const allocator = testing.allocator;
1378 
1379     var printer = IRPrintingInstrumentation.init(allocator, .{
1380         .print_after_change = true,
1381     });
1382     defer printer.deinit();
1383 
1384     try testing.expect(printer.options.print_after_change);
1385     try testing.expect(!printer.options.print_before);
1386     try testing.expect(!printer.options.print_after);
1387 }
1388 
1389 test "PassInstrumentor clear removes all instrumentations" {
1390     const testing = std.testing;
1391     const allocator = testing.allocator;
1392 
1393     var instrumentor = PassInstrumentor.init(allocator);
1394     defer instrumentor.deinit();
1395 
1396     try instrumentor.addInstrumentation(.{});
1397     try instrumentor.addInstrumentation(.{});
1398     try testing.expectEqual(@as(usize, 2), instrumentor.instrumentations.items.len);
1399 
1400     instrumentor.clear();
1401     try testing.expectEqual(@as(usize, 0), instrumentor.instrumentations.items.len);
1402 }