Skip to documentation
SLOP

tiny.profiling.ir

Reference 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.

No direct callersNo direct callstiny.profilingir
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallstest; no linksrc.profiling.irtest: IR audit fixed copy fixture fin...test; no linksrc.profiling.irtest: IR audit fixed specialization f...test; no linksrc.profiling.irtest: IR audit fixed stack fixture fi...test; no linksrc.profiling.irtest: IR audit rejects input beyond c...private; no linksrc.profiling.ingest.parseringestirparse
Static calls · unresolved targets: 0 · external targets: 3.

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

Definitions16
Public names16
Members19
Version26.7.0
Revisiondaab053ee433