diff --git a/bench/bench.zig b/bench/bench.zig index 83afc8b4..a290a7b9 100644 --- a/bench/bench.zig +++ b/bench/bench.zig @@ -1,5 +1,6 @@ const std = @import("std"); const vaxis = @import("vaxis"); +const uucode = @import("uucode"); fn parseIterations(allocator: std.mem.Allocator) !usize { var args = try std.process.argsWithAllocator(allocator); @@ -20,6 +21,103 @@ fn printResults(writer: anytype, label: []const u8, iterations: usize, elapsed_n ); } +// Mirrors the pre-fast-path parseGround work for ASCII to provide a baseline. +fn parseAsciiSlow(input: []const u8) !vaxis.Parser.Result { + std.debug.assert(input.len > 0); + var iter = uucode.utf8.Iterator.init(input); + const first_cp = iter.next() orelse return error.InvalidUTF8; + + var n: usize = std.unicode.utf8CodepointSequenceLength(first_cp) catch return error.InvalidUTF8; + + var code = first_cp; + var grapheme_iter = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(input)); + var grapheme_len: usize = 0; + var cp_count: usize = 0; + + while (grapheme_iter.next()) |result| { + cp_count += 1; + if (result.is_break) { + grapheme_len = grapheme_iter.i; + break; + } + } + + if (grapheme_len > 0) { + n = grapheme_len; + if (cp_count > 1) { + code = vaxis.Key.multicodepoint; + } + } + + const key: vaxis.Key = .{ .codepoint = code, .text = input[0..n] }; + return .{ .event = .{ .key_press = key }, .n = n }; +} + +fn benchParseFast(writer: anytype, label: []const u8, parser: *vaxis.Parser, input: []const u8, iterations: usize) !void { + var timer = try std.time.Timer.start(); + var i: usize = 0; + while (i < iterations) : (i += 1) { + const result = try parser.parse(input, null); + std.mem.doNotOptimizeAway(result); + } + const elapsed_ns = timer.read(); + try printResults(writer, label, iterations, elapsed_ns, input.len * iterations); +} + +fn benchParseSlow(writer: anytype, label: []const u8, input: []const u8, iterations: usize) !void { + var timer = try std.time.Timer.start(); + var i: usize = 0; + while (i < iterations) : (i += 1) { + const result = try parseAsciiSlow(input); + std.mem.doNotOptimizeAway(result); + } + const elapsed_ns = timer.read(); + try printResults(writer, label, iterations, elapsed_ns, input.len * iterations); +} + +fn benchParseStreamSlow(writer: anytype, label: []const u8, parser: *vaxis.Parser, input: []const u8, iterations: usize) !void { + var timer = try std.time.Timer.start(); + var i: usize = 0; + while (i < iterations) : (i += 1) { + var idx: usize = 0; + while (idx < input.len) { + const next = if (idx + 1 < input.len) input[idx + 1] else null; + if (input[idx] >= 0x20 and input[idx] <= 0x7E and (next == null or next.? < 0x80)) { + const result = try parseAsciiSlow(input[idx..]); + if (result.n == 0) break; + idx += result.n; + std.mem.doNotOptimizeAway(result); + continue; + } + + const result = try parser.parse(input[idx..], null); + if (result.n == 0) break; + idx += result.n; + std.mem.doNotOptimizeAway(result); + } + std.mem.doNotOptimizeAway(idx); + } + const elapsed_ns = timer.read(); + try printResults(writer, label, iterations, elapsed_ns, input.len * iterations); +} + +fn benchParseStreamFast(writer: anytype, label: []const u8, parser: *vaxis.Parser, input: []const u8, iterations: usize) !void { + var timer = try std.time.Timer.start(); + var i: usize = 0; + while (i < iterations) : (i += 1) { + var idx: usize = 0; + while (idx < input.len) { + const result = try parser.parse(input[idx..], null); + if (result.n == 0) break; + idx += result.n; + std.mem.doNotOptimizeAway(result); + } + std.mem.doNotOptimizeAway(idx); + } + const elapsed_ns = timer.read(); + try printResults(writer, label, iterations, elapsed_ns, input.len * iterations); +} + pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); @@ -59,4 +157,13 @@ pub fn main() !void { const dirty_ns = timer.read(); const dirty_bytes: usize = dirty_writer.writer.end; try printResults(stdout, "dirty", iterations, dirty_ns, dirty_bytes); + + var parser: vaxis.Parser = .{}; + const ascii_input = "a"; + try benchParseSlow(stdout, "parse_ground_ascii_slow", ascii_input, iterations); + try benchParseFast(stdout, "parse_ground_ascii_fast", &parser, ascii_input, iterations); + + const mixed_input = "hello \x1b[AδΈ–η•Œ 1️⃣ πŸ‘©β€πŸš€!\r"; + try benchParseStreamSlow(stdout, "parse_stream_mixed_slow", &parser, mixed_input, iterations); + try benchParseStreamFast(stdout, "parse_stream_mixed_fast", &parser, mixed_input, iterations); } diff --git a/build.zig b/build.zig index 2265ac5f..ede5bc7c 100644 --- a/build.zig +++ b/build.zig @@ -76,6 +76,7 @@ pub fn build(b: *std.Build) void { .optimize = optimize, .imports = &.{ .{ .name = "vaxis", .module = vaxis_mod }, + .{ .name = "uucode", .module = uucode_dep.module("uucode") }, }, }), }); diff --git a/src/Parser.zig b/src/Parser.zig index 6358269a..b4c3075b 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -83,6 +83,18 @@ inline fn parseGround(input: []const u8) !Result { std.debug.assert(input.len > 0); const b = input[0]; + // Fast path for printable ASCII only (0x20..0x7E). Control bytes are + // mapped to special key handling and ESC can start control sequences. + // Require the next byte to be ASCII (or absent) to avoid multi-codepoint + // graphemes like combining marks/keycap sequences that start with ASCII + // but continue with non-ASCII bytes. + if (b >= 0x20 and b <= 0x7E and (input.len == 1 or input[1] < 0x80)) { + return .{ + .event = .{ .key_press = .{ .codepoint = b, .text = input[0..1] } }, + .n = 1, + }; + } + var n: usize = 1; // ground state generates keypresses when parsing input. We // generally get ascii characters, but anything less than