perf(formatter_core): search line terminators with memchr (#28699)

For languages like CSS, YAML, Markdown, etc, `normalize_newlines` is called for CRLF normalization.

About 3~6% improvement for CPU instruction.
This commit is contained in:
leaysgur committed 2026-11-02 04:52:00 +00:00
1 parent ceb26be83c
commit e9392c88c6
2 files changed
+67 -24

No files matched your search

@@ -205,42 +205,52 @@ pub const LINE_TERMINATORS: [char; 3] = ['\r', LINE_SEPARATOR, PARAGRAPH_SEPARAT
/// Replace the line terminators matching the provided list with "\n"
/// since its the only line break type supported by the printer
pub fn normalize_newlines<const N: usize>(text: &str, terminators: [char; N]) -> Cow<'_, str> {
// Most texts have none: rule them out by the terminators' first UTF-8 bytes, all at once
// Searched by the terminators' first UTF-8 bytes, all at once:
// a `[char; N]` pattern would decode every char in between
const { assert!(N >= 1 && N <= 3) };
let bytes = text.as_bytes();
let first_bytes = terminators.map(|c| c.encode_utf8(&mut [0; 4]).as_bytes()[0]);
let first = if N == 1 {
memchr::memchr(first_bytes[0], text.as_bytes())
} else {
let byte = |i: usize| first_bytes[i.min(N - 1)];
memchr::memchr3(byte(0), byte(1), byte(2), text.as_bytes())
let find = |from: usize| {
let found = if N == 1 {
memchr::memchr(first_bytes[0], &bytes[from..])
} else {
let byte = |i: usize| first_bytes[i.min(N - 1)];
memchr::memchr3(byte(0), byte(1), byte(2), &bytes[from..])
};
found.map(|i| from + i)
};
// A first UTF-8 byte starts a char, so the search can resume there
let Some(first) = first else { return Cow::Borrowed(text) };
let mut result = String::new();
let mut last_end = 0;
for (start, part) in text[first..].match_indices(terminators) {
let start = first + start;
let mut from = 0;
while let Some(start) = find(from) {
// A first UTF-8 byte starts a char, so `start` is a char boundary
let Some(terminator) = terminators.into_iter().find(|&c| text[start..].starts_with(c))
else {
// Another char with the same first byte
from = start + 1;
continue;
};
if last_end == 0 {
// Never longer than `text`
result.reserve(text.len());
}
result.push_str(&text[last_end..start]);
result.push('\n');
last_end = start + part.len();
// If the current character is \r and the
// next is \n, skip over the entire sequence
if part == "\r" && text[last_end..].starts_with('\n') {
last_end = start + terminator.len_utf8();
// `\r\n` is one line break
if terminator == '\r' && bytes.get(last_end) == Some(&b'\n') {
last_end += 1;
}
from = last_end;
}
// If the result is empty no line terminators were matched,
// return the entire input text without allocating a new String
if result.is_empty() {
Cow::Borrowed(text)
} else {
result.push_str(&text[last_end..text.len()]);
Cow::Owned(result)
// Nothing matched
if last_end == 0 {
return Cow::Borrowed(text);
}
result.push_str(&text[last_end..]);
Cow::Owned(result)
}
impl<'a> FormatElement<'a> {
@@ -581,12 +591,45 @@ pub fn text_width(text: &str, indent_width: IndentWidth) -> u32 {
#[cfg(test)]
mod tests {
use cow_utils::CowUtils;
use super::*;
fn indent_width(value: u8) -> IndentWidth {
IndentWidth::try_from(value).expect("valid indent width")
}
#[test]
fn normalize_newlines_edge_cases() {
let texts = [
"",
"a",
"a\nb",
"\r",
"\r\n",
"\n\r",
"\r\r",
"\r\r\n",
"\r\n\r\n",
"a\rb\r\nc\nd",
"a\r",
"\ra",
"あ\r\nい\rう",
"\u{2028}\r\u{2029}",
"\u{2028}\n",
"…€\u{2028}…",
];
for text in texts {
let crlf = text.cow_replace("\r\n", "\n");
let cr = crlf.cow_replace('\r', "\n");
assert_eq!(normalize_newlines(text, ['\r']), cr, "{text:?}");
let all = cr.cow_replace(&LINE_TERMINATORS[..], "\n");
assert_eq!(normalize_newlines(text, LINE_TERMINATORS), all, "{text:?}");
}
// Chars sharing a terminator's first byte (`0xE2`) are not terminators
assert!(matches!(normalize_newlines("a…€b", LINE_TERMINATORS), Cow::Borrowed(_)));
}
#[test]
fn single_width_round_trips_zero() {
let width = TextWidth::single(0);
@@ -1,7 +1,7 @@
checker.ts:
file size: 2922154 # 2.92 MB
sys allocs: 18979
sys reallocs: 494
sys reallocs: 491
sys deallocs: 18978
sys alloc bytes: 6244889 # 6.24 MB
sys peak growth: 2973842 # 2.97 MB