Skip to content

Trim text around Liquid tags in linear time in liquid-html-parser - #1330

Open
PhilippeCollin wants to merge 1 commit into
mainfrom
liquid-html-parser-linear-text-trim
Open

PhilippeCollin wants to merge 1 commit into
mainfrom
liquid-html-parser-linear-text-trim

Conversation

@PhilippeCollin

Copy link
Copy Markdown

Why

Profiling toLiquidHtmlAST on Dawn, Horizon and the base theme showed one regular expression taking about 15% of all parse time. It is value.replace(/\s+$/, ''), which trims trailing whitespace from text next to Liquid tags.

That pattern isn't anchored at the start, so the regex engine tries a match at every position inside each whitespace run, and each attempt reads to the end of the run before failing at $. A run of k spaces costs about k² steps. Raw-tag bodies such as indented {% schema %} JSON or {% style %} CSS contain many such runs.

What

The trimming now uses trimEnd(), and trimStart() for the leading side. These strip exactly the characters \s matches, because the spec defines both with the same WhiteSpace and LineTerminator sets, and they run in linear time. A new test pins that character set, including a zero-width space that must be kept. A second test parses text containing a 100,000-space run within 1 s; on main it takes about 5 s.

Benchmarks

Apple M3 Pro, Node 24.15. The input is the parser's 429 fixture files (Dawn, Horizon, base theme; 3.4 MB). Builds ran in alternating processes, 5 rounds × 30 passes. Each value is the median.

Full parse of all 429 files before after Time saved
On main: toLiquidHtmlAST 96.6 ms 92.4 ms 4%
On main: toLiquidAST 100.8 ms 90.0 ms 11%
On top of #1329: toLiquidHtmlAST 43.3 ms 37.2 ms 14%
On top of #1329: toLiquidAST 46.4 ms 34.7 ms 25%

The relative gain is larger with #1329, because that PR removes most of the tokenizer time that otherwise dominates. A text node with a long whitespace run also stops scaling quadratically: an 80,000-space run takes 3.5 s on main and 3 ms with this change.

ASTs are unchanged. liquid-html-parser (including the local fixture oracle suites), prettier-plugin-liquid and theme-check-common tests pass. A differential run against main over about 25,700 inputs (every .liquid file in the repo, plus random and mutated templates) found 0 differences in tokens or ASTs, positions included.

Related: #1329 (tokenizer fast path). Found while profiling after it.

`value.replace(/\s+$/, '')` retries the match at every position of each
whitespace run, so its cost grows with the square of the run length.
It was the most expensive single step when parsing Dawn and Horizon.

`trimStart()`/`trimEnd()` strip exactly the characters `\s` matches
(both use the spec's WhiteSpace and LineTerminator sets), in linear
time. A test pins that set and the long-run case.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant