From 9ab9bc8e197e726e2a72efec01b44934085638a1 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 17 Nov 2025 13:17:31 -0700 Subject: [PATCH] perf: small sgr parser improvements --- src/terminal/Screen.zig | 4 - src/terminal/sgr.zig | 228 +++++++++++++++++++++++++--------------- 2 files changed, 143 insertions(+), 89 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 24f4497fe..789ba90b0 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1772,10 +1772,6 @@ pub fn setAttribute(self: *Screen, attr: sgr.Attribute) !void { self.cursor.style.flags.underline = v; }, - .reset_underline => { - self.cursor.style.flags.underline = .none; - }, - .underline_color => |rgb| { self.cursor.style.underline_color = .{ .rgb = .{ .r = rgb.r, diff --git a/src/terminal/sgr.zig b/src/terminal/sgr.zig index 7712563cf..dc9505d14 100644 --- a/src/terminal/sgr.zig +++ b/src/terminal/sgr.zig @@ -32,7 +32,6 @@ pub const Attribute = union(Tag) { /// Underline the text underline: Underline, - reset_underline, underline_color: color.RGB, @"256_underline_color": u8, reset_underline_color, @@ -92,7 +91,6 @@ pub const Attribute = union(Tag) { "reset_italic", "faint", "underline", - "reset_underline", "underline_color", "256_underline_color", "reset_underline_color", @@ -186,15 +184,16 @@ pub const Parser = struct { /// Next returns the next attribute or null if there are no more attributes. pub fn next(self: *Parser) ?Attribute { if (self.idx >= self.params.len) { - // If we're at index zero it means we must have an empty - // list and an empty list implicitly means unset. - if (self.idx == 0) { - // Add one to ensure we don't loop on unset - self.idx += 1; - return .unset; - } + // We're more likely to not be done than to be done. + @branchHint(.unlikely); - return null; + // Add one to ensure we don't loop on unset + defer self.idx += 1; + + // If we're at index zero it means we must have an empty list + // and an empty list implicitly means unset, otherwise we're + // done and return null. + return if (self.idx == 0) .unset else null; } const slice = self.params[self.idx..self.params.len]; @@ -206,20 +205,30 @@ pub const Parser = struct { // If we have a colon separator then we need to ensure we're // parsing a value that allows it. - if (colon) switch (slice[0]) { - 4, 38, 48, 58 => {}, + if (colon) { + // Colons are fairly rare in the wild. + @branchHint(.unlikely); - else => { - // Consume all the colon separated values. - const start = self.idx; - while (self.params_sep.isSet(self.idx)) self.idx += 1; - self.idx += 1; - return .{ .unknown = .{ - .full = self.params, - .partial = slice[0..@min(self.idx - start + 1, slice.len)], - } }; - }, - }; + switch (slice[0]) { + 4, 38, 48, 58 => {}, + + else => { + // In real world use it's very rare + // that we receive an invalid sequence. + @branchHint(.cold); + + // Consume all the colon separated + // values and return them as unknown. + const start = self.idx; + while (self.params_sep.isSet(self.idx)) self.idx += 1; + self.idx += 1; + return .{ .unknown = .{ + .full = self.params, + .partial = slice[0..@min(self.idx - start + 1, slice.len)], + } }; + }, + } + } switch (slice[0]) { 0 => return .unset, @@ -232,25 +241,37 @@ pub const Parser = struct { 4 => underline: { if (colon) { + // Colons are fairly rare in the wild. + @branchHint(.unlikely); + assert(slice.len >= 2); if (self.isColon()) { + // Invalid/unknown SGRs are just not very likely. + @branchHint(.cold); + self.consumeUnknownColon(); break :underline; } self.idx += 1; - switch (slice[1]) { - 0 => return .reset_underline, - 1 => return .{ .underline = .single }, - 2 => return .{ .underline = .double }, - 3 => return .{ .underline = .curly }, - 4 => return .{ .underline = .dotted }, - 5 => return .{ .underline = .dashed }, + return .{ + .underline = switch (slice[1]) { + 0 => .none, + 1 => .single, + 2 => .double, + 3 => .curly, + 4 => .dotted, + 5 => .dashed, - // For unknown underline styles, just render - // a single underline. - else => return .{ .underline = .single }, - } + // For unknown underline styles, + // just render a single underline. + else => single: { + // This is quite a rare condition. + @branchHint(.cold); + break :single .single; + }, + }, + }; } return .{ .underline = .single }; @@ -272,7 +293,7 @@ pub const Parser = struct { 23 => return .reset_italic, - 24 => return .reset_underline, + 24 => return .{ .underline = .none }, 25 => return .reset_blink, @@ -286,23 +307,32 @@ pub const Parser = struct { .@"8_fg" = @enumFromInt(slice[0] - 30), }, - 38 => if (slice.len >= 2) switch (slice[1]) { - // `2` indicates direct-color (r, g, b). - // We need at least 3 more params for this to make sense. - 2 => if (self.parseDirectColor( - .direct_color_fg, - slice, - colon, - )) |v| return v, + 38 => if (slice.len >= 2) { + // We are very likely to have enough parameters. + @branchHint(.likely); - // `5` indicates indexed color. - 5 => if (slice.len >= 3) { - self.idx += 2; - return .{ - .@"256_fg" = @truncate(slice[2]), - }; - }, - else => {}, + switch (slice[1]) { + // `2` indicates direct-color (r, g, b). + // We need at least 3 more params for this to make sense. + 2 => if (self.parseDirectColor( + .direct_color_fg, + slice, + colon, + )) |v| return v, + + // `5` indicates indexed color. + 5 => if (slice.len >= 3) { + // We are very likely to have enough parameters. + @branchHint(.likely); + + self.idx += 2; + return .{ + .@"256_fg" = @truncate(slice[2]), + }; + }, + + else => {}, + } }, 39 => return .reset_fg, @@ -311,23 +341,32 @@ pub const Parser = struct { .@"8_bg" = @enumFromInt(slice[0] - 40), }, - 48 => if (slice.len >= 2) switch (slice[1]) { - // `2` indicates direct-color (r, g, b). - // We need at least 3 more params for this to make sense. - 2 => if (self.parseDirectColor( - .direct_color_bg, - slice, - colon, - )) |v| return v, + 48 => if (slice.len >= 2) { + // We are very likely to have enough parameters. + @branchHint(.likely); - // `5` indicates indexed color. - 5 => if (slice.len >= 3) { - self.idx += 2; - return .{ - .@"256_bg" = @truncate(slice[2]), - }; - }, - else => {}, + switch (slice[1]) { + // `2` indicates direct-color (r, g, b). + // We need at least 3 more params for this to make sense. + 2 => if (self.parseDirectColor( + .direct_color_bg, + slice, + colon, + )) |v| return v, + + // `5` indicates indexed color. + 5 => if (slice.len >= 3) { + // We are very likely to have enough parameters. + @branchHint(.likely); + + self.idx += 2; + return .{ + .@"256_bg" = @truncate(slice[2]), + }; + }, + + else => {}, + } }, 49 => return .reset_bg, @@ -335,23 +374,31 @@ pub const Parser = struct { 53 => return .overline, 55 => return .reset_overline, - 58 => if (slice.len >= 2) switch (slice[1]) { - // `2` indicates direct-color (r, g, b). - // We need at least 3 more params for this to make sense. - 2 => if (self.parseDirectColor( - .underline_color, - slice, - colon, - )) |v| return v, + 58 => if (slice.len >= 2) { + // We are very likely to have enough parameters. + @branchHint(.likely); - // `5` indicates indexed color. - 5 => if (slice.len >= 3) { - self.idx += 2; - return .{ - .@"256_underline_color" = @truncate(slice[2]), - }; - }, - else => {}, + switch (slice[1]) { + // `2` indicates direct-color (r, g, b). + // We need at least 3 more params for this to make sense. + 2 => if (self.parseDirectColor( + .underline_color, + slice, + colon, + )) |v| return v, + + // `5` indicates indexed color. + 5 => if (slice.len >= 3) { + // We are very likely to have enough parameters. + @branchHint(.likely); + + self.idx += 2; + return .{ + .@"256_underline_color" = @truncate(slice[2]), + }; + }, + else => {}, + } }, 59 => return .reset_underline_color, @@ -389,6 +436,9 @@ pub const Parser = struct { // If we don't have a colon, then we expect exactly 3 semicolon // separated values. if (!colon) { + // Semicolons are much more common than colons. + @branchHint(.likely); + self.idx += 4; return @unionInit(Attribute, @tagName(tag), .{ .r = @truncate(slice[2]), @@ -402,6 +452,9 @@ pub const Parser = struct { const count = self.countColon(); switch (count) { 3 => { + // This is the much more common case in the wild. + @branchHint(.likely); + self.idx += 4; return @unionInit(Attribute, @tagName(tag), .{ .r = @truncate(slice[2]), @@ -420,6 +473,9 @@ pub const Parser = struct { }, else => { + // Invalid/unknown SGRs just don't happen very often at all. + @branchHint(.cold); + self.consumeUnknownColon(); return null; }, @@ -560,7 +616,8 @@ test "sgr: underline" { { const v = testParse(&[_]u16{24}); - try testing.expect(v == .reset_underline); + try testing.expect(v == .underline); + try testing.expect(v.underline == .none); } } @@ -573,7 +630,8 @@ test "sgr: underline styles" { { const v = testParseColon(&[_]u16{ 4, 0 }); - try testing.expect(v == .reset_underline); + try testing.expect(v == .underline); + try testing.expect(v.underline == .none); } {