summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--build.zig25
-rw-r--r--src/pardes.zig9
-rw-r--r--src/syntax.zig3
-rw-r--r--src/tracy.zig50
-rw-r--r--src/tty.zig126
6 files changed, 163 insertions, 52 deletions
diff --git a/.gitignore b/.gitignore
index c006b9dd..e78d336f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
zig-pkg
-zig-out
+zig-out*
.zig-cache/
.zig-cache/*
assets/MapleMono-NF-Regular.ttf
diff --git a/build.zig b/build.zig
index 22dd7158..41f4c0ec 100644
--- a/build.zig
+++ b/build.zig
@@ -16,6 +16,7 @@ pub fn build(b: *std.Build) void {
const optimize = if (is_emscripten) .ReleaseSmall else requested_optimize;
const default_grammars: TreeSitterGrammars = if (is_emscripten) .minimal else .full;
const tree_sitter_grammars = b.option(TreeSitterGrammars, "tree-sitter", "tree-sitter grammar set: disabled, minimal, full") orelse default_grammars;
+ const tracy = b.option([]const u8, "tracy", "enable Tracy profiling; supply the path to a Tracy source checkout");
// the browser shell: zig object + emcc link + html, installed to zig-out/web
const web_step = b.step("web", "build the browser shell into zig-out/web (-Dplatform=web -Dtarget=wasm32-emscripten -Ddump=<dump.zon>)");
@@ -64,8 +65,32 @@ pub fn build(b: *std.Build) void {
opts.addOption(bool, "syntax_highlighting", tree_sitter_grammars != .disabled);
opts.addOption(bool, "syntax_minimal_grammars", tree_sitter_grammars != .disabled);
opts.addOption(bool, "syntax_full_grammars", tree_sitter_grammars == .full);
+ opts.addOption(bool, "enable_tracy", tracy != null);
root_mod.addOptions("pardes_config", opts);
+ // Tracy zones (src/tracy.zig): compile the client into the binary only when
+ // -Dtracy=<path> names a Tracy checkout; otherwise every zone is a no-op.
+ // Sampling/callstacks/system tracing stay off: tracy's symbol worker
+ // SIGSEGVs on this binary's debug info, and its crash handler then parks
+ // every thread — the app wedges before the first frame. Zones don't need
+ // any of it.
+ if (tracy) |tracy_path| {
+ root_mod.addIncludePath(.{ .cwd_relative = tracy_path });
+ root_mod.addCSourceFile(.{
+ .file = .{ .cwd_relative = b.pathJoin(&.{ tracy_path, "public", "TracyClient.cpp" }) },
+ .flags = &.{
+ "-DTRACY_ENABLE=1",
+ "-DTRACY_NO_SAMPLING",
+ "-DTRACY_NO_CALLSTACK",
+ "-DTRACY_NO_SYSTEM_TRACING",
+ "-DTRACY_NO_CRASH_HANDLER",
+ "-DTRACY_NO_CODE_TRANSFER",
+ "-fno-sanitize=undefined",
+ },
+ });
+ root_mod.link_libcpp = true;
+ }
+
// tree-sitter: the zig bindings + C runtime, one static lib per grammar,
// and each grammar's highlights.scm slurped at build time into the
// ts_queries options module (codegen consumed by comptime in syntax.zig).
diff --git a/src/pardes.zig b/src/pardes.zig
index c4893b25..63943c04 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -14,6 +14,7 @@ const uucode = @import("uucode");
const modal = @import("modal.zig");
const look = @import("look.zig");
const syntax = @import("syntax.zig");
+const tracy = @import("tracy.zig");
pub const image = @import("image.zig");
pub const dump = @import("dump.zig");
@@ -3077,6 +3078,8 @@ pub const Pardes = struct {
/// re-highlight the visible window of any file whose syntax went stale
/// (edit, scroll, load) — visible-range-first so big files stay snappy
fn refreshDirtyHighlights(p: *Pardes) void {
+ const tz = tracy.zone(@src(), "refreshDirtyHighlights");
+ defer tz.end();
for (p.panes) |slot| {
const pane = slot orelse continue;
if (pane.file == null) continue;
@@ -3226,6 +3229,8 @@ pub const Pardes = struct {
}
fn renderPane(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, active: bool) !void {
+ const tz = tracy.zone(@src(), "renderPane");
+ defer tz.end();
if (r.w <= GUTTER or r.h == 0) return;
const s = &p.surface;
const th = p.theme();
@@ -3278,7 +3283,9 @@ pub const Pardes = struct {
.fg = if (th.fg) |c| .{ .rgb = c } else .default,
.bg = if (th.bg) |c| .{ .rgb = c } else .default,
};
+ const tz_body = tracy.zone(@src(), "bodyText");
const body = try p.bodyText(arena, pane);
+ tz_body.end();
var it = std.mem.splitScalar(u8, body, '\n');
var i: u16 = 0;
while (it.next()) |line| : (i += 1) {
@@ -3303,6 +3310,8 @@ pub const Pardes = struct {
// tree-sitter style byte; content starts after the lineno gutter
if (p.colors_on) if (pane.file) |*f| {
if (f.highlights.len > 0) {
+ const tz_recolor = tracy.zone(@src(), "synRecolor");
+ defer tz_recolor.end();
var flines = std.mem.splitScalar(u8, f.content, '\n');
var skip: usize = 0;
while (skip < f.scroll) : (skip += 1) {
diff --git a/src/syntax.zig b/src/syntax.zig
index a9914fd7..0d2fa291 100644
--- a/src/syntax.zig
+++ b/src/syntax.zig
@@ -4,6 +4,7 @@
//! compiles queries at startup; `full` adds ~23 languages lazily on first use.
const std = @import("std");
const config = @import("pardes_config");
+const tracy = @import("tracy.zig");
pub const enabled = config.syntax_highlighting;
const minimal_grammars = config.syntax_minimal_grammars;
@@ -140,6 +141,8 @@ pub fn highlightFile(gpa: std.mem.Allocator, path: []const u8, content: []const
/// One Syn byte per content byte in [start, end). Caller frees.
pub fn highlightFileRange(gpa: std.mem.Allocator, path: []const u8, content: []const u8, start_byte_raw: usize, end_byte_raw: usize) ![]u8 {
+ const tz = tracy.zone(@src(), "highlightFileRange");
+ defer tz.end();
if (!enabled) return &.{};
const ext = std.fs.path.extension(path);
const selected = (forExt(ext) catch return &.{}) orelse return &.{};
diff --git a/src/tracy.zig b/src/tracy.zig
new file mode 100644
index 00000000..3b0b8621
--- /dev/null
+++ b/src/tracy.zig
@@ -0,0 +1,50 @@
+//! Minimal Tracy client (the Zig compiler's pattern): `zone(@src(), "name")`
+//! ... `.end()`, plus `frameMark()`. Everything compiles to a no-op unless the
+//! build sets -Dtracy=<path-to-tracy-checkout>, which also compiles and links
+//! TracyClient.cpp. Capture with tracy-capture, inspect with tracy-csvexport.
+const std = @import("std");
+const builtin = @import("builtin");
+
+pub const enable = if (builtin.is_test) false else @import("pardes_config").enable_tracy;
+
+const SourceLocation = extern struct {
+ name: ?[*:0]const u8,
+ function: [*:0]const u8,
+ file: [*:0]const u8,
+ line: u32,
+ color: u32,
+};
+
+pub const Ctx = if (enable) extern struct {
+ id: u32,
+ active: i32,
+
+ pub inline fn end(c: @This()) void {
+ ___tracy_emit_zone_end(c);
+ }
+} else struct {
+ pub inline fn end(_: @This()) void {}
+};
+
+pub inline fn zone(comptime src: std.builtin.SourceLocation, comptime name: [:0]const u8) Ctx {
+ if (!enable) return .{};
+ const static = struct {
+ const loc: SourceLocation = .{
+ .name = name.ptr,
+ .function = src.fn_name.ptr,
+ .file = src.file.ptr,
+ .line = src.line,
+ .color = 0,
+ };
+ };
+ return ___tracy_emit_zone_begin(&static.loc, 1);
+}
+
+pub inline fn frameMark() void {
+ if (!enable) return;
+ ___tracy_emit_frame_mark(null);
+}
+
+extern fn ___tracy_emit_zone_begin(srcloc: *const SourceLocation, active: c_int) Ctx;
+extern fn ___tracy_emit_zone_end(ctx: Ctx) void;
+extern fn ___tracy_emit_frame_mark(name: ?[*:0]const u8) void;
diff --git a/src/tty.zig b/src/tty.zig
index d9821343..3290cde9 100644
--- a/src/tty.zig
+++ b/src/tty.zig
@@ -7,6 +7,7 @@ const posix = std.posix;
const linux = std.os.linux;
const vaxis = @import("vaxis");
const pardes = @import("pardes.zig");
+const tracy = @import("tracy.zig");
extern "c" fn forkpty(amaster: *c_int, name: ?[*:0]u8, termp: ?*const anyopaque, winp: ?*const posix.winsize) c_int;
extern "c" fn execv(path: [*:0]const u8, argv: [*:null]const ?[*:0]const u8) c_int;
@@ -87,61 +88,77 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
var pending: ?@TypeOf(Command.value) = .tick;
while (!core.quit) {
- const event = if (pending) |ev| blk: {
+ var event = if (pending) |ev| blk: {
pending = null;
break :blk ev;
} else try loop.nextEvent();
- switch (event) {
- .nop, .tick => {},
- .quit => break,
- .winsize => |ws| {
- try vx.resize(gpa, tty.writer(), ws);
- core.update(.{ .resize = .{ .cols = @intCast(ws.cols), .rows = @intCast(ws.rows) } });
- },
- .pty_read => |pr| {
- core.update(.{ .output = .{ .pane = @intCast(pr.id), .bytes = pr.bytes } });
- gpa.free(pr.bytes);
- },
- .pty_eof => |id| {
- if (ptys[id]) |*pt| {
- _ = linux.close(pt.file.handle);
- ptys[id] = null;
- }
- core.update(.{ .eof = .{ .pane = @intCast(id) } });
- },
- .key_press => |key| core.update(.{ .key = .{
- .cp = mapKey(effCp(key)),
- .text = key.text orelse "",
- .ctrl = key.mods.ctrl,
- .alt = key.mods.alt,
- } }),
- .mouse => |m| {
- const button: ?pardes.Mouse.Button = switch (m.button) {
- .left => .left,
- .middle => .middle,
- .right => .right,
- .wheel_up => .wheel_up,
- .wheel_down => .wheel_down,
- .none => .none, // button-less motion: hover tracking
- else => null,
- };
- if (button) |b| core.update(.{ .mouse = .{
- .button = b,
- .kind = switch (m.type) {
- .press => .press,
- .release => .release,
- .motion => .motion,
- .drag => .drag,
- },
- .col = @intCast(m.col),
- .row = @intCast(m.row),
- } });
- },
- .paste => |bytes| {
- core.update(.{ .paste = bytes });
- gpa.free(@constCast(bytes));
- },
+ const tz_event = tracy.zone(@src(), "event");
+ // Apply every queued INPUT event, then render ONCE — the gui shell
+ // drains SDL's queue the same way. Without this a wheel flick is fifty
+ // full render+repaint (and re-highlight) cycles instead of one.
+ // pty output keeps a frame per chunk: progress paints as it arrives.
+ var stop = false;
+ var batch: usize = 0;
+ while (true) {
+ var output = false;
+ switch (event) {
+ .nop, .tick => {},
+ .quit => stop = true,
+ .winsize => |ws| {
+ try vx.resize(gpa, tty.writer(), ws);
+ core.update(.{ .resize = .{ .cols = @intCast(ws.cols), .rows = @intCast(ws.rows) } });
+ },
+ .pty_read => |pr| {
+ core.update(.{ .output = .{ .pane = @intCast(pr.id), .bytes = pr.bytes } });
+ gpa.free(pr.bytes);
+ output = true;
+ },
+ .pty_eof => |id| {
+ if (ptys[id]) |*pt| {
+ _ = linux.close(pt.file.handle);
+ ptys[id] = null;
+ }
+ core.update(.{ .eof = .{ .pane = @intCast(id) } });
+ },
+ .key_press => |key| core.update(.{ .key = .{
+ .cp = mapKey(effCp(key)),
+ .text = key.text orelse "",
+ .ctrl = key.mods.ctrl,
+ .alt = key.mods.alt,
+ } }),
+ .mouse => |m| {
+ const button: ?pardes.Mouse.Button = switch (m.button) {
+ .left => .left,
+ .middle => .middle,
+ .right => .right,
+ .wheel_up => .wheel_up,
+ .wheel_down => .wheel_down,
+ .none => .none, // button-less motion: hover tracking
+ else => null,
+ };
+ if (button) |b| core.update(.{ .mouse = .{
+ .button = b,
+ .kind = switch (m.type) {
+ .press => .press,
+ .release => .release,
+ .motion => .motion,
+ .drag => .drag,
+ },
+ .col = @intCast(m.col),
+ .row = @intCast(m.row),
+ } });
+ },
+ .paste => |bytes| {
+ core.update(.{ .paste = bytes });
+ gpa.free(@constCast(bytes));
+ },
+ }
+ batch += 1;
+ if (stop or output or batch >= 64) break;
+ event = (try loop.tryEvent()) orelse break;
}
+ tz_event.end();
+ if (stop) break;
drainEffects(core, &ptys, io, gpa, &loop, &vx, &tty, true);
if (core.quit) break;
@@ -158,7 +175,10 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
// ---- render: surface -> vaxis, cell for cell ----
_ = frame_arena.reset(.retain_capacity);
+ const tz_core = tracy.zone(@src(), "core.render");
const surface = try core.render(frame_arena.allocator());
+ tz_core.end();
+ const tz_cells = tracy.zone(@src(), "surface->vaxis");
const win = vx.window();
win.clear();
var y: u16 = 0;
@@ -173,6 +193,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
});
}
}
+ tz_cells.end();
// pixel attachments (kitty graphics): transmit once per pane, then
// re-place every frame (placements aren't persistent)
for (surface.images[0..surface.nimages]) |maybe| {
@@ -191,7 +212,10 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
}
}
if (surface.cursor) |cur| win.showCursor(cur.x, cur.y);
+ const tz_render = tracy.zone(@src(), "vx.render");
try vx.render(tty.writer());
+ tz_render.end();
+ tracy.frameMark();
}
}