Skip to content

Commit 6a714db

Browse files
committed
feat: add per-pass performance profiling support
1 parent 715238d commit 6a714db

8 files changed

Lines changed: 803 additions & 270 deletions

File tree

src/main.zig

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,17 @@ fn registerAllPasses(pipeline: *Pipeline) !void {
7171
// try pipeline.registerPass(OmniScope.cross_lang.ThreadCrossingPass);
7272
}
7373

74-
fn runModulePipeline(allocator: std.mem.Allocator, loader: *IRLoader) !AnalyzeResult {
74+
fn runModulePipeline(allocator: std.mem.Allocator, loader: *IRLoader, config: Config) !AnalyzeResult {
7575
var pipeline = try Pipeline.init(allocator);
7676
if (loader.getModule()) |module_ref| {
7777
pipeline.setModule(module_ref);
7878
}
7979

80+
// Enable per-pass profiling if --perf-stats flag is set
81+
if (config.perf_stats) {
82+
pipeline.setPerfStats(true);
83+
}
84+
8085
try registerAllPasses(&pipeline);
8186

8287
const analysis_start = std.time.milliTimestamp();
@@ -630,7 +635,7 @@ fn runSingleFileAnalysis(allocator: std.mem.Allocator, path: []const u8, config:
630635

631636
log.debug("Loaded: {d} functions\n\n", .{loader.getFunctionCount()});
632637

633-
var result = try runModulePipeline(allocator, &loader);
638+
var result = try runModulePipeline(allocator, &loader, config);
634639
defer deinitAnalyzeResult(&result);
635640

636641
try emitOutput(allocator, result.issues, result.func_count, result.time_ms, config);
@@ -711,7 +716,7 @@ fn runMultiFileAnalysis(allocator: std.mem.Allocator, files: []const []const u8,
711716
log.info("[*] Running per-file pipelines...\n", .{});
712717
for (loaders.items, 0..) |*loader, i| {
713718
log.info(" [{d}/{d}] Analyzing: {s} ({d} functions)\n", .{ i + 1, loaders.items.len, files[i], loader.getFunctionCount() });
714-
const result = runModulePipeline(allocator, loader) catch |err| {
719+
const result = runModulePipeline(allocator, loader, config) catch |err| {
715720
log.err(" [{d}/{d}] Analysis FAILED: {}\n", .{ i + 1, loaders.items.len, err });
716721
continue;
717722
};

src/pass/analysis/rust_ffi/rust_ffi_auditor.zig

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1904,3 +1904,70 @@ test "isPureConsumptionFunction - safe functions" {
19041904
try std.testing.expect(!isPureConsumptionFunction("c_ffi_store_pointer"));
19051905
try std.testing.expect(!isPureConsumptionFunction("pthread_create"));
19061906
}
1907+
1908+
test "StringHashMap pairing stability - same content, different backing" {
1909+
// Verify StringHashMap key stability: same function name from different
1910+
// memory locations must map to the same entry (POT-BUG-8 regression test).
1911+
// This ensures into_raw/from_raw pairing doesn't miss matches due to
1912+
// slice header pointer instability.
1913+
const testing = std.testing;
1914+
1915+
var into_raw_set = std.StringHashMap(void).init(testing.allocator);
1916+
defer into_raw_set.deinit();
1917+
var from_raw_set = std.StringHashMap(void).init(testing.allocator);
1918+
defer from_raw_set.deinit();
1919+
1920+
// Simulate LLVM returning same name from different allocations (common in
1921+
// real modules where LLVMGetValueName may return different pointers for
1922+
// the same logical string across calls).
1923+
const name1 = "my_func_into_raw";
1924+
const name2 = "my_func_into_raw";
1925+
// name1 and name2 have identical content but are separate comptime
1926+
// constants — in runtime they'd be separate allocations.
1927+
1928+
try into_raw_set.put(name1, {});
1929+
try testing.expect(into_raw_set.contains(name2));
1930+
try testing.expectEqual(@as(usize, 1), into_raw_set.count());
1931+
1932+
// Verify from_raw set behaves identically
1933+
const name3 = "my_func_from_raw";
1934+
const name4 = "my_func_from_raw";
1935+
try from_raw_set.put(name3, {});
1936+
try testing.expect(from_raw_set.contains(name4));
1937+
try testing.expectEqual(@as(usize, 1), from_raw_set.count());
1938+
1939+
// Cross-check: different names must not collide
1940+
try testing.expect(!into_raw_set.contains("other_func"));
1941+
try testing.expect(!from_raw_set.contains("my_func_into_raw"));
1942+
}
1943+
1944+
test "StringHashMap pairing stability - mangled Rust names" {
1945+
// Verify mangled Rust names pair correctly through StringHashMap.
1946+
// Real-world Rust FFI uses mangled names like _ZN5alloc... which must
1947+
// match regardless of backing allocation.
1948+
const testing = std.testing;
1949+
1950+
var set = std.StringHashMap(void).init(testing.allocator);
1951+
defer set.deinit();
1952+
1953+
const mangled_names = [_][]const u8{
1954+
"_ZN5alloc3boxed3Box*.*8into_raw17habc123",
1955+
"_ZN5alloc3boxed3Box*.*8from_raw17hdef456",
1956+
"_RNvCsfLfy6EI15iL_7___rustc12___rust_alloc",
1957+
};
1958+
1959+
// Insert each name
1960+
for (mangled_names) |name| {
1961+
try set.put(name, {});
1962+
}
1963+
1964+
try testing.expectEqual(@as(usize, 3), set.count());
1965+
1966+
// Verify each name can be looked up with an identical copy
1967+
for (mangled_names) |name| {
1968+
// Create a duplicate to simulate different backing memory
1969+
const dup = try testing.dupe(u8, name);
1970+
defer testing.allocator.free(dup);
1971+
try testing.expect(set.contains(dup));
1972+
}
1973+
}

src/pass/manager.zig

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//!
33
//! This module manages pass registration, dependency resolution,
44
//! and execution in the correct order using topological sorting.
5+
//! Supports optional per-pass performance profiling via --perf-stats flag.
56

67
const std = @import("std");
78
const Allocator = std.mem.Allocator;
@@ -12,6 +13,7 @@ const DiagnosticWriter = @import("pass.zig").DiagnosticWriter;
1213
const PassKind = @import("pass.zig").PassKind;
1314
const FactStore = @import("../fact/store.zig").FactStore;
1415
const QueryEngine = @import("../fact/query.zig").QueryEngine;
16+
const profiler = @import("../perf/profiler.zig");
1517

1618
/// Dependency resolution error
1719
pub const DependencyError = error{
@@ -26,6 +28,8 @@ pub const PassManager = struct {
2628
pass_map: std.StringHashMap(usize), // name -> index
2729
resolved_order: ?[]usize, // indices in execution order
2830
execution_names: ?[]const []const u8, // cached pass names in order
31+
perf_stats: bool = false, // Enable per-pass performance profiling
32+
pass_stats_collector: ?profiler.PassStatsCollector = null, // Collected statistics
2933

3034
const PassEntry = struct {
3135
name: []const u8,
@@ -53,6 +57,10 @@ pub const PassManager = struct {
5357
if (self.resolved_order) |order| {
5458
self.allocator.free(order);
5559
}
60+
if (self.pass_stats_collector) |*collector| {
61+
collector.deinit();
62+
self.pass_stats_collector = null;
63+
}
5664
self.pass_map.deinit();
5765
self.passes.deinit(self.allocator);
5866
}
@@ -195,16 +203,40 @@ pub const PassManager = struct {
195203
_ = try self.resolveDependencies();
196204
}
197205

206+
// Initialize stats collector if profiling is enabled
207+
if (self.perf_stats) {
208+
self.pass_stats_collector = profiler.PassStatsCollector.init(self.allocator);
209+
}
210+
198211
// Execute in resolved order with graceful degradation (v0.1.6)
199212
var pass_failures: usize = 0;
200213
for (self.resolved_order.?) |idx| {
201214
const pass_name = self.passes.items[idx].name;
215+
216+
// Per-pass timing and memory sampling (only when enabled)
217+
var pass_timer: ?profiler.PassTimer = null;
218+
if (self.perf_stats) {
219+
pass_timer = profiler.PassTimer.startPass() catch null;
220+
}
221+
202222
const t0 = std.time.nanoTimestamp();
203223
self.passes.items[idx].run_fn(ctx, diag) catch |err| {
204224
diag.warn("PassManager: pass '{s}' failed with error: {any}, degrading gracefully", .{ pass_name, err });
205225
pass_failures += 1;
206226
// Continue running remaining passes
207227
};
228+
229+
// Record per-pass statistics
230+
if (self.perf_stats and pass_timer != null) {
231+
if (pass_timer) |*timer| {
232+
if (timer.stopPass(pass_name)) |stats| {
233+
if (self.pass_stats_collector) |*collector| {
234+
collector.record(stats) catch {};
235+
}
236+
} else |_| {}
237+
}
238+
}
239+
208240
// Early exit: no FFI boundaries found, skip remaining heavy passes
209241
if (ctx.early_exit) {
210242
diag.info("PassManager: early exit after '{s}' — no FFI boundaries, remaining passes skipped", .{pass_name});
@@ -217,11 +249,33 @@ pub const PassManager = struct {
217249
}
218250
}
219251

252+
// Print performance report if profiling was enabled
253+
if (self.perf_stats) {
254+
if (self.pass_stats_collector) |*collector| {
255+
collector.printReport(true);
256+
}
257+
}
258+
220259
if (pass_failures > 0) {
221260
diag.info("PassManager: completed with {} degraded passes out of {}", .{ pass_failures, self.resolved_order.?.len });
222261
}
223262
}
224263

264+
/// Enable or disable per-pass performance profiling
265+
/// Must be called before run()
266+
pub fn setPerfStats(self: *PassManager, enabled: bool) void {
267+
self.perf_stats = enabled;
268+
}
269+
270+
/// Get the collected pass statistics (for programmatic access)
271+
/// Returns null if profiling was not enabled or no data collected
272+
pub fn getPassStats(self: *const PassManager) ?[]const profiler.PassStats {
273+
if (self.pass_stats_collector) |*collector| {
274+
return collector.stats.items;
275+
}
276+
return null;
277+
}
278+
225279
/// Get the number of registered passes
226280
pub fn count(self: *const PassManager) usize {
227281
return self.passes.items.len;

0 commit comments

Comments
 (0)