mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-10 11:19:45 +00:00
terminal: bulk-parse CSI parameter bytes at the slice level
After the CSI dispatch fast paths, profiling showed the remaining escape-sequence cost was the per-byte plumbing itself: for every parameter byte of a sequence like "ESC [ 38;2;10;20;30 m" the stream re-entered nextNonUtf8, re-checked the parser state, and re-dispatched through the fast-path switch, paying call and state check overhead per digit. consumeUntilGround now hands whole input slices to a new consumeCsiParams loop whenever the parser is in the csi_param state. It consumes runs of digits and separators with the parser accumulator state held in locals, dispatches directly when it reaches the final byte, and returns to the general path on the first byte it doesn't understand (C0 controls, intermediates, etc.), guaranteeing byte-for-byte identical semantics with the per-byte fast path it hoists. Like the dispatch fast paths, this is disabled at comptime for handlers that declare vtRaw so the inspector continues to observe every action. Throughput measured with ghostty-bench terminal-stream (full terminal handler, 100 MB deterministic corpora, 120x80, M4 Max, ReleaseFast, hyperfine means of 10 runs): | stream | before | after | change | |--------|--------|--------|--------| | csi | 525 ms | 407 ms | +29% | | sgr | 414 ms | 294 ms | +41% | Combined with the previous commit, CSI-heavy streams are 1.5-1.7x faster end to end than before this series.
This commit is contained in:
@@ -611,12 +611,88 @@ pub fn Stream(comptime H: type) type {
|
||||
var offset: usize = 0;
|
||||
while (self.parser.state != .ground) {
|
||||
if (offset >= input.len) return input.len;
|
||||
|
||||
// Bulk-consume CSI parameter bytes. This can't be used
|
||||
// for handlers with a vtRaw hook because it dispatches
|
||||
// the CSI directly (see nextNonUtf8).
|
||||
if (comptime !@hasDecl(T, "vtRaw")) {
|
||||
if (self.parser.state == .csi_param) {
|
||||
offset += self.consumeCsiParams(input[offset..]);
|
||||
if (offset >= input.len) return input.len;
|
||||
// If we're still in csi_param then the next byte
|
||||
// isn't a parameter byte; let nextNonUtf8 below
|
||||
// handle it. Otherwise re-check our state.
|
||||
if (self.parser.state != .csi_param) continue;
|
||||
}
|
||||
}
|
||||
|
||||
self.nextNonUtf8(input[offset]);
|
||||
offset += 1;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
/// Bulk-consume CSI parameter bytes (digits and separators)
|
||||
/// and, if reached, the final byte (dispatching the CSI).
|
||||
/// Returns the number of bytes consumed. Stops at the first
|
||||
/// byte that isn't handled here, leaving the parser in the
|
||||
/// csi_param state so the caller can process that byte.
|
||||
fn consumeCsiParams(self: *Self, input: []const u8) usize {
|
||||
const p = &self.parser;
|
||||
assert(p.state == .csi_param);
|
||||
|
||||
// Accumulate parser state in locals for the hot loop.
|
||||
var acc = p.param_acc;
|
||||
var acc_idx = p.param_acc_idx;
|
||||
var idx = p.params_idx;
|
||||
|
||||
var offset: usize = 0;
|
||||
while (offset < input.len) {
|
||||
const c = input[offset];
|
||||
switch (c) {
|
||||
// A parameter digit.
|
||||
'0'...'9' => {
|
||||
if (idx < Parser.MAX_PARAMS) {
|
||||
acc *|= 10;
|
||||
acc +|= c - '0';
|
||||
acc_idx |= 1;
|
||||
}
|
||||
offset += 1;
|
||||
},
|
||||
|
||||
// A parameter separator.
|
||||
':', ';' => {
|
||||
if (idx < Parser.MAX_PARAMS) {
|
||||
p.params[idx] = acc;
|
||||
if (c == ':') p.params_sep.set(idx);
|
||||
idx += 1;
|
||||
acc = 0;
|
||||
acc_idx = 0;
|
||||
}
|
||||
offset += 1;
|
||||
},
|
||||
|
||||
// A final byte: dispatch the CSI.
|
||||
0x40...0x7E => {
|
||||
p.param_acc = acc;
|
||||
p.param_acc_idx = acc_idx;
|
||||
p.params_idx = idx;
|
||||
self.csiDispatchFinal(c);
|
||||
return offset + 1;
|
||||
},
|
||||
|
||||
// Anything else (C0 controls, intermediates, etc.)
|
||||
// is handled by the caller.
|
||||
else => break,
|
||||
}
|
||||
}
|
||||
|
||||
p.param_acc = acc;
|
||||
p.param_acc_idx = acc_idx;
|
||||
p.params_idx = idx;
|
||||
return offset;
|
||||
}
|
||||
|
||||
/// Like nextSlice but takes one byte and is necessarily a scalar
|
||||
/// operation that can't use SIMD. Prefer nextSlice if you can and
|
||||
/// try to get multiple bytes at once.
|
||||
|
||||
Reference in New Issue
Block a user