tiny.profiling.ir
Defined in tiny.profiling.
API (15)
Actions
Public operations.
Types and contracts
Public types and contracts.
Values and defaults
Public values and defaults.
maximum_findingsmaximum_functionsmaximum_line_bytesmaximum_linesmaximum_originsmaximum_symbol_bytesnormalization_version
Source
Source: src/profiling/ir.zig
zig
const std = @import("std");pub const normalization_version = "tiny.profiling.ir-origin/v1";pub const maximum_line_bytes: u32 = 16 * 1024;pub const maximum_lines: u32 = 1_000_000;pub const maximum_functions: u32 = 100_000;pub const maximum_findings: u16 = 64;pub const maximum_origins: u16 = 64;pub const maximum_symbol_bytes: u16 = 240;pub const Limits = struct { line_bytes: u32 = maximum_line_bytes, lines: u32 = maximum_lines, functions: u32 = maximum_functions, findings: u16 = maximum_findings, origins: u16 = maximum_origins, fn assertValid(self: Limits) void { std.debug.assert(self.line_bytes > 0); std.debug.assert(self.line_bytes <= maximum_line_bytes); std.debug.assert(self.lines > 0); std.debug.assert(self.lines <= maximum_lines); std.debug.assert(self.functions > 0); std.debug.assert(self.functions <= maximum_functions); std.debug.assert(self.findings > 0); std.debug.assert(self.findings <= maximum_findings); std.debug.assert(self.origins > 0); std.debug.assert(self.origins <= maximum_origins); }};pub const Kind = enum { memcpy, stack, specialization,};pub const Symbol = struct { bytes: [maximum_symbol_bytes]u8 = undefined, len: u16, fn init(value: []const u8) !Symbol { if (value.len == 0) return error.EmptyIrSymbol; if (value.len > maximum_symbol_bytes) return error.IrSymbolTooLong; var result = Symbol{ .len = @intCast(value.len) }; @memcpy(result.bytes[0..value.len], value); return result; } pub fn slice(self: *const Symbol) []const u8 { return self.bytes[0..self.len]; }};pub const Finding = struct { kind: Kind, function: Symbol, bytes: u64 = 0, occurrences: u32 = 1, ir_lines: u32 = 0,};pub const Audit = struct { line_count: u32 = 0, function_count: u32 = 0, findings: [maximum_findings]Finding = undefined, finding_count: u16 = 0, pub fn items(self: *const Audit) []const Finding { return self.findings[0..self.finding_count]; }};const Current = struct { name: Symbol, origin: ?Symbol, line_count: u32,};const Origin = struct { name: Symbol, occurrences: u32, line_count: u32,};const Parser = struct { limits: Limits, audit: Audit = .{}, current: ?Current = null, origins: [maximum_origins]Origin = undefined, origin_count: u16 = 0, fn ingest(self: *Parser, raw_line: []const u8) !void { if (raw_line.len > self.limits.line_bytes) return error.IrLineTooLong; if (self.audit.line_count == self.limits.lines) { return error.IrLineLimitExceeded; } self.audit.line_count += 1; const line = std.mem.trim(u8, raw_line, " \t\r"); if (std.mem.startsWith(u8, line, "define ")) { if (self.current != null) return error.NestedIrFunction; try self.beginFunction(try functionName(line)); return; } if (self.current) |*current| { current.line_count += 1; if (try constantMemcpyBytes(line)) |byte_count| { try self.addFinding(.{ .kind = .memcpy, .function = current.name, .bytes = byte_count, }); } if (try fixedAllocaBytes(line)) |byte_count| { try self.addFinding(.{ .kind = .stack, .function = current.name, .bytes = byte_count, }); } if (std.mem.eql(u8, line, "}")) try self.finishFunction(); } } fn beginFunction(self: *Parser, name: []const u8) !void { if (self.audit.function_count == self.limits.functions) { return error.IrFunctionLimitExceeded; } self.audit.function_count += 1; const normalized = normalizeOrigin(name); self.current = .{ .name = try Symbol.init(name), .origin = if (normalized.changed) try Symbol.init(normalized.name) else null, .line_count = 1, }; } fn finishFunction(self: *Parser) !void { const current = self.current orelse return error.MissingIrFunction; self.current = null; const origin = current.origin orelse return; for (self.origins[0..self.origin_count]) |*entry| { if (std.mem.eql(u8, entry.name.slice(), origin.slice())) { entry.occurrences += 1; entry.line_count += current.line_count; return; } } if (self.origin_count == self.limits.origins) { return error.IrOriginLimitExceeded; } self.origins[self.origin_count] = .{ .name = origin, .occurrences = 1, .line_count = current.line_count, }; self.origin_count += 1; } fn finish(self: *Parser) !Audit { if (self.current != null) return error.UnterminatedIrFunction; for (self.origins[0..self.origin_count]) |origin| { if (origin.occurrences < 2) continue; try self.addFinding(.{ .kind = .specialization, .function = origin.name, .occurrences = origin.occurrences, .ir_lines = origin.line_count, }); } return self.audit; } fn addFinding(self: *Parser, finding: Finding) !void { if (self.audit.finding_count == self.limits.findings) { return error.IrFindingLimitExceeded; } self.audit.findings[self.audit.finding_count] = finding; self.audit.finding_count += 1; }};pub fn parse(reader: *std.Io.Reader, limits: Limits) !Audit { limits.assertValid(); var parser = Parser{ .limits = limits }; while (true) { const line = try reader.takeDelimiter('\n'); try parser.ingest(line orelse break); } return parser.finish();}const Normalized = struct { name: []const u8, changed: bool,};fn normalizeOrigin(name: []const u8) Normalized { const marker = "__anon_"; const marker_index = std.mem.lastIndexOf(u8, name, marker) orelse return .{ .name = name, .changed = false }; const suffix = name[marker_index + marker.len ..]; if (suffix.len == 0) return .{ .name = name, .changed = false }; for (suffix) |byte| { if (!std.ascii.isDigit(byte)) { return .{ .name = name, .changed = false }; } } if (marker_index == 0) return .{ .name = name, .changed = false }; return .{ .name = name[0..marker_index], .changed = true };}fn functionName(line: []const u8) ![]const u8 { const at = std.mem.indexOfScalar(u8, line, '@') orelse return error.InvalidIrFunction; const name_start = at + 1; if (name_start == line.len) return error.InvalidIrFunction; if (line[name_start] != '"') { const end = std.mem.indexOfScalarPos(u8, line, name_start, '(') orelse return error.InvalidIrFunction; return line[name_start..end]; } var index = name_start + 1; var escaped = false; while (index < line.len) : (index += 1) { const byte = line[index]; if (escaped) { escaped = false; } else if (byte == '\\') { escaped = true; } else if (byte == '"') { return line[name_start + 1 .. index]; } } return error.InvalidIrFunction;}fn constantMemcpyBytes(line: []const u8) !?u64 { const name = "@llvm.memcpy"; const call = std.mem.indexOf(u8, line, name) orelse return null; const open = std.mem.indexOfScalarPos(u8, line, call + name.len, '(') orelse return error.InvalidIrMemcpy; const length = argumentAt(line[open + 1 ..], 2) orelse return error.InvalidIrMemcpy; return parseIntegerConstant(length) orelse error.InvalidIrMemcpy;}fn argumentAt(arguments: []const u8, requested: u8) ?[]const u8 { var current: u8 = 0; var start: usize = 0; var depth: u16 = 0; var quoted = false; var escaped = false; for (arguments, 0..) |byte, index| { if (escaped) { escaped = false; continue; } if (byte == '\\' and quoted) { escaped = true; continue; } if (byte == '"') { quoted = !quoted; continue; } if (quoted) continue; switch (byte) { '(', '[', '{', '<' => depth += 1, ']', '}', '>' => if (depth > 0) { depth -= 1; }, ')' => if (depth == 0) { return if (current == requested) arguments[start..index] else null; } else { depth -= 1; }, ',' => if (depth == 0) { if (current == requested) return arguments[start..index]; current += 1; start = index + 1; }, else => {}, } } return if (current == requested) arguments[start..] else null;}fn fixedAllocaBytes(line: []const u8) !?u64 { const marker = "alloca "; const index = std.mem.indexOf(u8, line, marker) orelse return null; const value = std.mem.trimStart(u8, line[index + marker.len ..], " \t"); if (value.len == 0) return error.InvalidIrAlloca; if (value[0] == '[') return try arrayTypeBytes(value); const scalar_bytes = integerTypeBytes(value) orelse return null; const comma = std.mem.indexOfScalar(u8, value, ',') orelse return scalar_bytes; const count = parseIntegerConstant(value[comma + 1 ..]) orelse return scalar_bytes; return std.math.mul(u64, scalar_bytes, count) catch return error.IrSizeOverflow;}fn arrayTypeBytes(value: []const u8) !?u64 { const close = std.mem.indexOfScalar(u8, value, ']') orelse return error.InvalidIrAlloca; const body = value[1..close]; const separator = std.mem.indexOf(u8, body, " x ") orelse return error.InvalidIrAlloca; const count = std.fmt.parseInt( u64, std.mem.trim(u8, body[0..separator], " \t"), 10, ) catch return error.InvalidIrAlloca; const scalar_bytes = integerTypeBytes(body[separator + 3 ..]) orelse return null; return std.math.mul(u64, count, scalar_bytes) catch return error.IrSizeOverflow;}fn integerTypeBytes(value: []const u8) ?u64 { const trimmed = std.mem.trimStart(u8, value, " \t"); if (trimmed.len < 2 or trimmed[0] != 'i') return null; var end: usize = 1; while (end < trimmed.len and std.ascii.isDigit(trimmed[end])) : (end += 1) {} if (end == 1) return null; const bits = std.fmt.parseInt(u64, trimmed[1..end], 10) catch return null; if (bits == 0 or bits % 8 != 0) return null; return bits / 8;}fn parseIntegerConstant(value: []const u8) ?u64 { const trimmed = std.mem.trimStart(u8, value, " \t"); if (trimmed.len < 3 or trimmed[0] != 'i') return null; var index: usize = 1; while (index < trimmed.len and std.ascii.isDigit(trimmed[index])) : (index += 1) {} if (index == 1 or index == trimmed.len or trimmed[index] != ' ') return null; index += 1; const start = index; while (index < trimmed.len and std.ascii.isDigit(trimmed[index])) : (index += 1) {} if (index == start) return null; return std.fmt.parseInt(u64, trimmed[start..index], 10) catch null;}const copy_fixture = "define void @copy_fixture(ptr %target, ptr %source) {\n" ++ "entry:\n" ++ " call void @llvm.memcpy.p0.p0.i64(" ++ "ptr %target, ptr %source, i64 4100, i1 false)\n" ++ " ret void\n" ++ "}\n";const stack_fixture = "define void @stack_fixture() {\n" ++ "entry:\n" ++ " %buffer = alloca [10240 x i8], align 16\n" ++ " ret void\n" ++ "}\n";const specialization_fixture = "define i32 @fixture.generic__anon_101(i32 %value) {\n" ++ " ret i32 %value\n" ++ "}\n" ++ "define i32 @fixture.generic__anon_202(i32 %value) {\n" ++ " ret i32 %value\n" ++ "}\n";test "IR audit fixed copy fixture finds a 4 KiB by-value copy" { var reader = std.Io.Reader.fixed(copy_fixture); const audit = try parse(&reader, .{}); try std.testing.expectEqual(@as(usize, 1), audit.items().len); const finding = audit.items()[0]; try std.testing.expectEqual(Kind.memcpy, finding.kind); try std.testing.expectEqual(@as(u64, 4100), finding.bytes); try std.testing.expectEqualStrings("copy_fixture", finding.function.slice());}test "IR audit fixed stack fixture finds a large stack candidate" { var reader = std.Io.Reader.fixed(stack_fixture); const audit = try parse(&reader, .{}); try std.testing.expectEqual(@as(usize, 1), audit.items().len); const finding = audit.items()[0]; try std.testing.expectEqual(Kind.stack, finding.kind); try std.testing.expectEqual(@as(u64, 10240), finding.bytes); try std.testing.expectEqualStrings("stack_fixture", finding.function.slice());}test "IR audit fixed specialization fixture groups duplicate bodies" { var reader = std.Io.Reader.fixed(specialization_fixture); const audit = try parse(&reader, .{}); try std.testing.expectEqual(@as(usize, 1), audit.items().len); const finding = audit.items()[0]; try std.testing.expectEqual(Kind.specialization, finding.kind); try std.testing.expectEqual(@as(u32, 2), finding.occurrences); try std.testing.expectEqual(@as(u32, 6), finding.ir_lines); try std.testing.expectEqualStrings("fixture.generic", finding.function.slice()); try std.testing.expectEqualStrings( "tiny.profiling.ir-origin/v1", normalization_version, );}test "IR audit rejects input beyond caller bounds" { var line_reader = std.Io.Reader.fixed("12345\n"); try std.testing.expectError( error.IrLineTooLong, parse(&line_reader, .{ .line_bytes = 4 }), ); var function_reader = std.Io.Reader.fixed( "define void @one() {\n}\ndefine void @two() {\n}\n", ); try std.testing.expectError( error.IrFunctionLimitExceeded, parse(&function_reader, .{ .functions = 1 }), ); var finding_reader = std.Io.Reader.fixed( "define void @copies(ptr %a, ptr %b) {\n" ++ "call void @llvm.memcpy.p0.p0.i64(ptr %a, ptr %b, i64 1, i1 false)\n" ++ "call void @llvm.memcpy.p0.p0.i64(ptr %a, ptr %b, i64 2, i1 false)\n" ++ "}\n", ); try std.testing.expectError( error.IrFindingLimitExceeded, parse(&finding_reader, .{ .findings = 1 }), ); var origin_reader = std.Io.Reader.fixed( "define void @one__anon_1() {\n}\n" ++ "define void @two__anon_2() {\n}\n", ); try std.testing.expectError( error.IrOriginLimitExceeded, parse(&origin_reader, .{ .origins = 1 }), );}Source: src/profiling/root.zig:28
zig
pub const ir = @import("ir.zig");Audit
| Definitions | 16 |
|---|---|
| Public names | 16 |
| Members | 19 |
| Version | 26.7.0 |
| Revision | daab053ee433 |