tiny.profiling.host.sampling
Defined in host.
API (18)
Actions
Public operations.
Options.minimumTimeNsOptions.validatemaximumTwoSigmaShareHalfWidthPercentagePointsplansummarizewrapArgv
Types and contracts
Public types and contracts.
Values and defaults
Public values and defaults.
children_summary_namedefault_call_graphdefault_frequencydefault_min_samplesdefault_percent_limitfolded_namekindsrcline_summary_namesymbols_summary_nametool
Source
Source: src/profiling/host/root.zig:8
zig
pub const sampling = runtime.sampling;Source: src/profiling/host/sampling.zig
zig
const std = @import("std");const capture = @import("capture");const sys = @import("sys");const root = @import("root.zig");pub const tool = "perf";pub const kind = "perf_record";pub const default_frequency: u32 = 99;pub const default_min_samples: u64 = 400;pub const default_call_graph = "fp";pub const default_percent_limit: f64 = 0.5;pub const folded_name = "perf.folded.txt";pub const symbols_summary_name = "perf.symbols.summary.json";pub const children_summary_name = "perf.children.summary.json";pub const srcline_summary_name = "perf.srcline.summary.json";pub const Options = struct { frequency: u32 = default_frequency, min_samples: u64 = default_min_samples, call_graph: []const u8 = default_call_graph, report_percent_limit: f64 = default_percent_limit, pub fn validate(self: Options) !void { if (self.call_graph.len == 0) return error.InvalidSamplingCallGraph; if (!std.math.isFinite(self.report_percent_limit) or self.report_percent_limit < 0 or self.report_percent_limit > 100) { return error.InvalidSamplingPercentLimit; } _ = try self.minimumTimeNs(); } pub fn minimumTimeNs(self: Options) !u64 { if (self.frequency == 0) return error.InvalidSamplingFrequency; if (self.min_samples == 0) return error.InvalidMinimumSampleCount; const numerator = std.math.mul( u64, self.min_samples, std.time.ns_per_s, ) catch return error.SamplingDurationOverflow; return std.math.divCeil(u64, numerator, self.frequency) catch error.SamplingDurationOverflow; }};pub const Lane = struct { frequency: u32, min_samples: u64, call_graph: []const u8, report_percent_limit: f64, data_path: []const u8, symbols_tsv_path: []const u8, symbols_summary_path: []const u8, children_tsv_path: []const u8, children_summary_path: []const u8, srcline_tsv_path: []const u8, srcline_summary_path: []const u8, folded_path: []const u8,};pub fn plan( allocator: std.mem.Allocator, options: Options, workload_root: []const u8,) !Lane { std.debug.assert(workload_root.len > 0); try options.validate(); return .{ .frequency = options.frequency, .min_samples = options.min_samples, .call_graph = options.call_graph, .report_percent_limit = options.report_percent_limit, .data_path = try std.fs.path.join(allocator, &.{ workload_root, "perf.data" }), .symbols_tsv_path = try std.fs.path.join( allocator, &.{ workload_root, "perf.symbols.tsv" }, ), .symbols_summary_path = try std.fs.path.join( allocator, &.{ workload_root, symbols_summary_name }, ), .children_tsv_path = try std.fs.path.join( allocator, &.{ workload_root, "perf.children.tsv" }, ), .children_summary_path = try std.fs.path.join( allocator, &.{ workload_root, children_summary_name }, ), .srcline_tsv_path = try std.fs.path.join( allocator, &.{ workload_root, "perf.srcline.tsv" }, ), .srcline_summary_path = try std.fs.path.join( allocator, &.{ workload_root, srcline_summary_name }, ), .folded_path = try std.fs.path.join(allocator, &.{ workload_root, folded_name }), };}pub fn wrapArgv( allocator: std.mem.Allocator, options: Options, argv: []const []const u8, lane: Lane,) ![]const []const u8 { std.debug.assert(argv.len > 0); std.debug.assert(lane.data_path.len > 0); return try capture.perfdata.recordCommand( allocator, tool, argv, lane.data_path, options.call_graph, options.frequency, );}pub fn summarize( allocator: std.mem.Allocator, process_io: std.Io, lane: Lane, exit_code: i64, stderr_path: []const u8, tool_available: bool,) !root.Capture { std.debug.assert(lane.data_path.len > 0); std.debug.assert(stderr_path.len > 0); if (!tool_available) return missingTool(lane); if (!sys.fs.exists(lane.data_path)) { return try failed(allocator, lane, exit_code, stderr_path); } try writeReports(allocator, process_io, lane); var symbols = try capture.perfreport.summarizeSymbolReportFile( allocator, lane.symbols_tsv_path, ); if (symbols.rows.len == 0) { return try failed(allocator, lane, exit_code, stderr_path); } symbols.sampling_frequency_hz = lane.frequency; symbols.minimum_sample_count = lane.min_samples; symbols.call_graph = lane.call_graph; symbols.report_percent_limit = lane.report_percent_limit; symbols.maximum_two_sigma_share_half_width_percentage_points = maximumTwoSigmaShareHalfWidthPercentagePoints(symbols.sample_count_approx); symbols.callchain_quality = try writeFolded(allocator, process_io, lane); try capture.perfreport.writeSymbolSummaryFile(lane.symbols_summary_path, symbols); var children = try capture.perfreport.summarizeChildrenReportFile( allocator, lane.children_tsv_path, ); children.sampling_frequency_hz = lane.frequency; children.minimum_sample_count = lane.min_samples; children.call_graph = lane.call_graph; children.report_percent_limit = lane.report_percent_limit; try capture.perfreport.writeChildrenSummaryFile(lane.children_summary_path, children); var srcline = try capture.perfreport.summarizeSrclineReportFile( allocator, lane.srcline_tsv_path, ); srcline.sampling_frequency_hz = lane.frequency; srcline.minimum_sample_count = lane.min_samples; srcline.call_graph = lane.call_graph; srcline.report_percent_limit = lane.report_percent_limit; try capture.perfreport.writeSrclineSummaryFile(lane.srcline_summary_path, srcline); return try classifySupport( allocator, lane, symbols.sample_count_approx, symbols.sample_count_text, symbols.total_lost_samples, symbols.callchain_quality, );}fn writeReports( allocator: std.mem.Allocator, process_io: std.Io, lane: Lane,) !void { const percent_limit_text = try std.fmt.allocPrint( allocator, "{d}", .{lane.report_percent_limit}, ); const symbols = try capture.perfdata.symbolsTsvCommand( allocator, tool, lane.data_path, percent_limit_text, ); try reportToFile(allocator, process_io, symbols, lane.symbols_tsv_path); const children = try capture.perfdata.childrenTsvCommand( allocator, tool, lane.data_path, percent_limit_text, ); try reportToFile(allocator, process_io, children, lane.children_tsv_path); const srcline = try capture.perfdata.srclineTsvCommand( allocator, tool, lane.data_path, percent_limit_text, ); try reportToFile(allocator, process_io, srcline, lane.srcline_tsv_path);}fn classifySupport( allocator: std.mem.Allocator, lane: Lane, sample_count: ?u64, sample_count_text: ?[]const u8, total_lost_samples: ?u64, callchain_quality: ?capture.stackcollapse.Summary,) !root.Capture { std.debug.assert(lane.min_samples > 0); if (sample_count != null and sample_count.? >= lane.min_samples) { if (total_lost_samples == null) return .{ .kind = kind, .tool = tool, .capture_path = lane.data_path, .summary_path = lane.symbols_summary_path, .state = "summary_written", .caveat_kind = "sample_loss_unknown", .caveat_message = "perf did not report a lost-sample count; sampling completeness is unknown", }; if (total_lost_samples.? != 0) { const observed = sample_count.?; const lost = total_lost_samples.?; const lost_percent = @as(f64, @floatFromInt(lost)) / (@as(f64, @floatFromInt(observed)) + @as(f64, @floatFromInt(lost))) * 100.0; return .{ .kind = kind, .tool = tool, .capture_path = lane.data_path, .summary_path = lane.symbols_summary_path, .state = "summary_written", .caveat_kind = "samples_lost", .caveat_message = try std.fmt.allocPrint( allocator, "perf lost {d} sample(s) ({d:.2}% of observed plus lost samples); attribution is caveated because loss need not be random", .{ lost, lost_percent }, ), }; } const quality = callchain_quality orelse return .{ .kind = kind, .tool = tool, .capture_path = lane.data_path, .summary_path = lane.symbols_summary_path, .state = "summary_written", .caveat_kind = "callchain_unavailable", .caveat_message = "perf reported samples without extractable callchains; flame attribution is unavailable", }; if (parseExactSampleCount(sample_count_text)) |report_count| { if (report_count != quality.sample_count) return .{ .kind = kind, .tool = tool, .capture_path = lane.data_path, .summary_path = lane.symbols_summary_path, .state = "summary_written", .caveat_kind = "callchain_sample_mismatch", .caveat_message = try std.fmt.allocPrint( allocator, "perf report counted {d} samples but perf script yielded {d} callchain samples; flame attribution is incomplete", .{ report_count, quality.sample_count }, ), }; } return .{ .kind = kind, .tool = tool, .capture_path = lane.data_path, .summary_path = lane.symbols_summary_path, .state = "summary_written", }; } const support_error = maximumTwoSigmaShareHalfWidthPercentagePoints( lane.min_samples, ).?; const message = if (sample_count) |observed| try std.fmt.allocPrint( allocator, "perf captured {d} samples; {d} are required for an approximate " ++ "worst-case two-standard-error share half-width no wider than " ++ "{d:.2} percentage points", .{ observed, lane.min_samples, support_error }, ) else try std.fmt.allocPrint( allocator, "perf did not report a sample count; {d} are required for an " ++ "approximate worst-case two-standard-error share half-width no " ++ "wider than {d:.2} percentage points", .{ lane.min_samples, support_error }, ); return .{ .kind = kind, .tool = tool, .capture_path = lane.data_path, .summary_path = lane.symbols_summary_path, .state = "insufficient_samples", .caveat_kind = "insufficient_sample_support", .caveat_message = message, };}pub fn maximumTwoSigmaShareHalfWidthPercentagePoints(sample_count: ?u64) ?f64 { const count = sample_count orelse return null; if (count == 0) return null; return 100.0 / @sqrt(@as(f64, @floatFromInt(count)));}fn parseExactSampleCount(sample_count_text: ?[]const u8) ?u64 { const trimmed = std.mem.trim(u8, sample_count_text orelse return null, " \t\r"); if (trimmed.len == 0) return null; for (trimmed) |byte| if (!std.ascii.isDigit(byte)) return null; return std.fmt.parseInt(u64, trimmed, 10) catch null;}fn writeFolded( allocator: std.mem.Allocator, process_io: std.Io, lane: Lane,) !?capture.stackcollapse.Summary { const argv = try capture.perfdata.scriptCommand(allocator, tool, lane.data_path); const result = sys.process.run(allocator, process_io, .{ .argv = argv, .stdout_limit = .limited(512 * 1024 * 1024), .stderr_limit = .limited(64 * 1024), }) catch return null; defer allocator.free(result.stdout); defer allocator.free(result.stderr); if (sys.process.exitCode(result.term) != 0 or result.stdout.len == 0) return null; const artifact = try capture.stackcollapse.collapsePerfScriptArtifact( allocator, result.stdout, ); if (artifact.folded.len == 0) return null; try sys.fs.writeFile(lane.folded_path, artifact.folded); return artifact.summary;}fn reportToFile( allocator: std.mem.Allocator, process_io: std.Io, argv: []const []const u8, output_path: []const u8,) !void { std.debug.assert(argv.len > 0); std.debug.assert(output_path.len > 0); const file = try sys.fs.createFile(output_path, .{ .truncate = true }); defer sys.fs.closeHandle(file); var child = try sys.process.spawn(process_io, .{ .argv = argv, .stdin = .ignore, .stdout = .{ .file = file }, .stderr = .ignore, }); errdefer sys.process.killAndReap(&child, process_io); _ = try sys.process.wait(&child, process_io); _ = allocator;}fn missingTool(lane: Lane) root.Capture { return .{ .kind = kind, .tool = tool, .capture_path = lane.data_path, .summary_path = lane.symbols_summary_path, .state = "tool_missing", .caveat_kind = "tool_unavailable", .caveat_message = "perf is not runnable on this host; samples were not captured", };}fn failed( allocator: std.mem.Allocator, lane: Lane, exit_code: i64, stderr_path: []const u8,) !root.Capture { const row = try capture.caveat.classifyCommandCapture(allocator, .{ .phase = kind, .exit_code = if (exit_code == 0) 1 else exit_code, .timed_out = false, .capture_path = stderr_path, }) orelse capture.caveat.classifyCommandText(.{ .phase = kind, .exit_code = exit_code, .timed_out = false, .capture_path = stderr_path, }, ""); return .{ .kind = kind, .tool = tool, .capture_path = lane.data_path, .summary_path = lane.symbols_summary_path, .state = "tool_failed", .caveat_kind = row.kind, .caveat_message = row.message, };}test "profiling sampling wraps workload argv in perf record" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); const lane = try plan(allocator, .{}, "zig-out/profiling/run/workloads/w"); const argv = [_][]const u8{ "zig", "build", "bench" }; const wrapped = try wrapArgv(allocator, .{}, &argv, lane); try std.testing.expectEqualStrings("perf", wrapped[0]); try std.testing.expectEqualStrings("record", wrapped[1]); try std.testing.expectEqualStrings( "zig-out/profiling/run/workloads/w/perf.data", lane.data_path, ); try std.testing.expectEqualStrings( "zig-out/profiling/run/workloads/w/perf.folded.txt", lane.folded_path, );}test "profiling sampling derives runtime from its support floor" { const options = Options{}; try options.validate(); try std.testing.expectEqual(@as(u64, 4_040_404_041), try options.minimumTimeNs()); try std.testing.expectError( error.InvalidSamplingFrequency, (Options{ .frequency = 0 }).validate(), ); try std.testing.expectError( error.InvalidMinimumSampleCount, (Options{ .min_samples = 0 }).validate(), ); try std.testing.expectError( error.InvalidSamplingCallGraph, (Options{ .call_graph = "" }).validate(), ); try std.testing.expectError( error.InvalidSamplingPercentLimit, (Options{ .report_percent_limit = 101 }).validate(), ); try std.testing.expectError( error.SamplingDurationOverflow, (Options{ .min_samples = std.math.maxInt(u64) }).validate(), );}test "profiling sampling classifies support at the configured floor" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); const lane = try plan(allocator, .{}, "zig-out/profiling/run/workloads/w"); const quality: capture.stackcollapse.Summary = .{ .sample_count = 400, .unique_callchain_count = 10, .weighted_frame_count = 2000, .resolved_frame_count = 1900, .unresolved_frame_count = 100, .samples_with_unresolved_frames = 50, .unresolved_leaf_samples = 10, .leaf_only_samples = 2, .maximum_depth_frames = 10, }; const short = try classifySupport(allocator, lane, 399, "399", 0, null); try std.testing.expectEqualStrings("insufficient_samples", short.state); try std.testing.expectEqualStrings( "insufficient_sample_support", short.caveat_kind.?, ); try std.testing.expect(std.mem.indexOf( u8, short.caveat_message.?, "5.00 percentage points", ) != null); const unknown = try classifySupport(allocator, lane, null, null, 0, null); try std.testing.expectEqualStrings("insufficient_samples", unknown.state); const unknown_loss = try classifySupport(allocator, lane, 400, "400", null, null); try std.testing.expectEqualStrings("summary_written", unknown_loss.state); try std.testing.expectEqualStrings("sample_loss_unknown", unknown_loss.caveat_kind.?); const lost = try classifySupport(allocator, lane, 400, "400", 1, null); try std.testing.expectEqualStrings("summary_written", lost.state); try std.testing.expectEqualStrings("samples_lost", lost.caveat_kind.?); try std.testing.expect(std.mem.indexOf(u8, lost.caveat_message.?, "0.25%") != null); const missing_callchains = try classifySupport(allocator, lane, 400, "400", 0, null); try std.testing.expectEqualStrings("callchain_unavailable", missing_callchains.caveat_kind.?); var short_quality = quality; short_quality.sample_count = 399; const mismatch = try classifySupport(allocator, lane, 400, "400", 0, short_quality); try std.testing.expectEqualStrings("callchain_sample_mismatch", mismatch.caveat_kind.?); try std.testing.expect(std.mem.indexOf(u8, mismatch.caveat_message.?, "399") != null); const abbreviated = try classifySupport(allocator, lane, 400, "0.4K", 0, short_quality); try std.testing.expect(abbreviated.caveat_kind == null); const supported = try classifySupport(allocator, lane, 400, "400", 0, quality); try std.testing.expectEqualStrings("summary_written", supported.state); try std.testing.expect(supported.caveat_kind == null); try std.testing.expectApproxEqAbs( @as(f64, 5), maximumTwoSigmaShareHalfWidthPercentagePoints(400).?, 0.0000001, ); try std.testing.expectEqual(@as(u64, 400), parseExactSampleCount(" 400 ").?); try std.testing.expect(parseExactSampleCount("400K") == null);}test "profiling sampling reports missing tools as caveats" { var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const allocator = arena_state.allocator(); const lane = try plan(allocator, .{}, "zig-out/profiling/run/workloads/w"); const row = missingTool(lane); try std.testing.expectEqualStrings("tool_missing", row.state); try std.testing.expectEqualStrings("perf_record", row.kind);}Audit
| Definitions | 19 |
|---|---|
| Public names | 19 |
| Members | 16 |
| Version | 26.7.0 |
| Revision | daab053ee433 |