aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/libghostty-vt/src/benchmark/ApcParser.zig
blob: 9efdc4fbf7cc6289d58753a5e7bbe04f29766ee7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//! This benchmark tests the throughput of APC sequence parsing
//! through the terminal stream: VT state machine dispatch, APC
//! protocol identification, and the protocol command parsers
//! (e.g. Kitty graphics). Completed commands are parsed and then
//! discarded; command execution (image decoding, storage) is not
//! included so this isolates the parsing path.
const ApcParser = @This();

const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const terminalpkg = @import("../terminal/main.zig");
const Benchmark = @import("Benchmark.zig");
const options = @import("options.zig");
const global = @import("../global.zig");

const log = std.log.scoped(.@"apc-parser-bench");

opts: Options,
stream: Stream,

/// The file, opened in the setup function.
data_f: ?std.Io.File = null,

pub const Options = struct {
    /// The data to read as a filepath. If this is "-" then
    /// we will read stdin. If this is unset, then we will
    /// do nothing (benchmark is a noop). It'd be more unixy to
    /// use stdin by default but I find that a hanging CLI command
    /// with no interaction is a bit annoying.
    data: ?[]const u8 = null,
};

const Stream = terminalpkg.Stream(Handler);

/// A stream handler that only processes APC actions, parsing and
/// immediately discarding completed commands.
const Handler = struct {
    alloc: Allocator,
    apc: terminalpkg.apc.Handler = .{},

    pub fn deinit(self: *Handler) void {
        self.apc.deinit();
    }

    pub fn vt(
        self: *Handler,
        comptime action: Stream.Action.Tag,
        value: Stream.Action.Value(action),
    ) void {
        switch (action) {
            .apc_start => self.apc.start(),
            .apc_put => self.apc.feed(self.alloc, value),
            .apc_put_slice => self.apc.feedSlice(self.alloc, value.bytes),
            .apc_end => if (self.apc.end()) |result| {
                var r = result;
                std.mem.doNotOptimizeAway(&r);
                r.deinit(self.alloc);
            },
            else => {},
        }
    }
};

/// Create a new APC parser benchmark for the given arguments.
pub fn create(
    alloc: Allocator,
    opts: Options,
) !*ApcParser {
    const ptr = try alloc.create(ApcParser);
    errdefer alloc.destroy(ptr);
    ptr.* = .{
        .opts = opts,
        .stream = .init(.{
            .allocator = alloc,
            .handler = .{ .alloc = alloc },
        }),
    };
    return ptr;
}

pub fn destroy(self: *ApcParser, alloc: Allocator) void {
    self.stream.deinit();
    alloc.destroy(self);
}

pub fn benchmark(self: *ApcParser) Benchmark {
    return .init(self, .{
        .stepFn = step,
        .setupFn = setup,
        .teardownFn = teardown,
    });
}

fn setup(ptr: *anyopaque) Benchmark.Error!void {
    const self: *ApcParser = @ptrCast(@alignCast(ptr));

    // Open our data file to prepare for reading. We can do more
    // validation here eventually.
    assert(self.data_f == null);
    self.data_f = options.dataFile(self.opts.data) catch |err| {
        log.warn("error opening data file err={}", .{err});
        return error.BenchmarkFailed;
    };
}

fn teardown(ptr: *anyopaque) void {
    const self: *ApcParser = @ptrCast(@alignCast(ptr));
    if (self.data_f) |f| {
        f.close(global.io());
        self.data_f = null;
    }
}

fn step(ptr: *anyopaque) Benchmark.Error!void {
    const self: *ApcParser = @ptrCast(@alignCast(ptr));

    const f = self.data_f orelse return;

    var read_buf: [64 * 1024]u8 align(std.atomic.cache_line) = undefined;
    var f_reader = f.reader(global.io(), &read_buf);
    const r = &f_reader.interface;

    // This buffer size matches the read buffer size used by the
    // real IO thread (see termio Exec.zig buffer_capacity) so that
    // the benchmark exercises the stream with realistic chunk sizes.
    var buf: [64 * 1024]u8 = undefined;
    while (true) {
        const n = r.readSliceShort(&buf) catch {
            log.warn("error reading data file err={?}", .{f_reader.err});
            return error.BenchmarkFailed;
        };
        if (n == 0) break; // EOF reached
        self.stream.nextSlice(buf[0..n]);
    }
}

test ApcParser {
    const testing = std.testing;
    const alloc = testing.allocator;

    const impl: *ApcParser = try .create(alloc, .{});
    defer impl.destroy(alloc);

    const bench = impl.benchmark();
    _ = try bench.run(.once);
}