Repository navigation
Conversation
After fb55#2481 the open-element stack is push/pop, but three places still scan the whole stack, each once per tag, so each is quadratic in the nesting depth on its own: - `onclosetag` runs `lastIndexOf` for an end tag. When no element of that name is open, it reads every open element and finds nothing. - `emitOpenTag` runs `stack.includes("form")` on every `<form>`. - `readTagName` runs `stack.includes(adjusted)` for an SVG-cased name (`clippath`, `foreignobject`, ...) once a foreign context is open. The parser now keeps a `Map` from name to how many elements of that name are open, updated in push, pop and reset. A name that is not open is one lookup. An open one is still found with `lastIndexOf` from the top, so that scan only passes the elements the end tag then closes, and their open tags already paid for them. Reads of the stack, counted the way the existing linear-work tests count them, at depth 500 / 1,000 / 2,000: shape before after <div> x n, then </span> x n 250,499 / 1,000,999 / 4,001,999 499 / 999 / 1,999 <div> x n, then <form> x n 250,999 / 1,001,999 / 4,003,999 500 / 1,000 / 2,000 <svg><foreignObject>, <div> x n, then <clippath> x n 376,250 / 1,502,500 / 6,005,000 500 / 1,000 / 2,000 The event stream is unchanged: 20,000 seeded random malformed documents, each under 5 option sets and split into two writes at a random point, give the same open, close, text, comment, CDATA, processing-instruction and attribute events, with the same start and end indices, before and after.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
[Medium impact] The PR appears safe to merge; no actionable issues were found.
|
#2481 made the open-element stack push/pop, which fixed deep nesting. Three lookups still read every open element, once per tag, so each one is quadratic in the nesting depth on its own:
onclosetagrunsthis.stack.lastIndexOf(name). For an end tag whose name is not open, it reads the whole stack and finds nothing.emitOpenTagrunsthis.stack.includes("form")on every<form>.readTagNamerunsthis.stack.includes(adjusted)for an SVG-cased name (clippath,foreignobject, ...) once a foreign context is open.A comment on #2481 points out the same three (#2481 (comment)) and offers a follow-up. I haven't seen one land, so here is one. Happy to close this in favour of theirs.
The change
The parser keeps a
Mapfrom tag name to how many elements of that name are open. It's updated in the same three places the stack changes: push inemitOpenTag, pop inpopElement, clear inreset. Then:lastIndexOffrom the top. That scan only passes the elements the end tag then closes, and their open tags already paid for them.<form>and SVG-casing checks becomeopenCounts.has(...).Numbers
Reads of
stack, counted with thecountArrayReadshelper the linear-work tests already use:<div>x n, then</span>x n, before<div>x n, then<form>x n, before<svg><foreignObject>,<div>x n, then<clippath>x n, before<div>x n, then</div>x n (unchanged)Wall time for one
parser.end(input)with anonclosetaghandler, master and this branch run back to back in the same session. The machine was under heavy load the whole time, so trust the ratios, not the absolute times:<b>xx n, then</i>x n<b>xx n, then<form>x n<svg><foreignObject>,<b>xx n, then<clippath>x nOn the same machine, 12.0.0 took 46 s for 40,000 stray end tags (313 KB) and master 107 s.
Behaviour
Unchanged, as far as I can tell:
npm run test:vi: 204 tests pass, including three new ones, one per shape, in the style of the existing linear-work tests. Without theParser.tschange the three new tests fail with 250,499, 250,999 and 376,250 reads against a limit of 5,000.xmlMode,recognizeSelfClosing,lowerCaseTags: false,decodeEntities: false) and written in two chunks split at a random point: 100,000 comparisons of the full event stream (open, close, text, comment, CDATA, processing instruction, attribute, each withstartIndexandendIndex) between master and this branch, 0 differences. The same harness finds 433 differences in 15,000 comparisons between master and 12.0.0, so it does see changes.eslint .,tsc --noEmit -p tsconfig.eslint.jsonandbiome checkon the two changed files pass.One thing to know:
openCountsis a new private member, so a subclass that declares its own privateopenCountswould hit TS2415, as with any new private member. Happy to rename it.Fixes #2548