Skip to content

perf: avoid repeated raw string copies in nested lists - #4082

Open
unshorn wants to merge 1 commit into
markedjs:masterfrom
unshorn:perf/list-raw-slice
Open

perf: avoid repeated raw string copies in nested lists#4082
unshorn wants to merge 1 commit into
markedjs:masterfrom
unshorn:perf/list-raw-slice

Conversation

@unshorn

@unshorn unshorn commented Sep 3, 2026

Copy link
Copy Markdown

Marked version:

18.0.11 / 63841d3b

Markdown flavor: CommonMark

Description

Tokenizer.list() builds each item's raw a line at a time (raw += rawLine + '\n') and the
list's raw by concatenating its items. src is only ever consumed from the front, so both are
contiguous spans of the input; this computes their bounds and slices them out instead. V8 keeps a
slice as a view onto one backing string, so the per-item and per-list copies go away. Token values
are unchanged.

Local measurements from my machine:

  • cmark's deeply nested lists pathological case (500 levels): 222-226 MB RSS and 132-140 ms
    before, 157 MB and 124-127 ms after, across four rounds. The rest of cmark's pathological corpus
    is unchanged.
  • npm run bench: unchanged, 1278/1319/1320 ms before against 1276/1312/1302 ms after. The saving
    is proportional to nesting depth, so ordinary documents get no marked benefit from the improvement.
  • Maximum nesting depth before RangeError: Maximum call stack size exceeded is 2237 both before
    and after, so there is no stack cost.

blockquote() does not accumulate line by line, and its continuation paths splice a re-lexed
newToken.raw into the middle of raw, so it is not expressible as one slice. Left alone.

Contributor

  • Test(s) exist to ensure functionality and minimize regression (if no tests added, list tests
    covering this PR); or,
  • no tests required for this PR.
  • If submitting new feature, it has been documented in the appropriate places. n/a, this is not
    a new feature.

Covered by the existing list coverage in test/specs/commonmark, test/specs/new and
test/unit/Lexer.test.js; full npm test passes locally. I also diffed this branch against
master on the full token tree and rendered HTML for every input in test/specs across five option
sets, and on 40,000 seeded-random list documents across three and observed no differences in either run.

Committer

In most cases, this should be a different person than the contributor.

Tokenizer.list() built each item's raw by appending line by line, and the
list's raw by concatenating its items. src is only ever consumed from the
front, so both are contiguous spans of the input and can be sliced out of
it instead. V8 keeps a slice as a view on one backing string rather than a
fresh copy per list and per item.

Token values are unchanged. On cmark's pathological "deeply nested lists"
case (500 levels): 222 MB RSS and 135 ms before, 157 MB and 126 ms after.

No change on the CommonMark spec corpus -- the saving is proportional to
nesting depth, so ordinary documents are unaffected.
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

@unshorn is attempting to deploy a commit to the MarkedJS Team on Vercel.

A member of the Team first needs to authorize it.

@unshorn unshorn changed the title fix: avoid repeated raw string copies in nested lists perf: avoid repeated raw string copies in nested lists Sep 3, 2026
@UziTech

UziTech commented Sep 3, 2026

Copy link
Copy Markdown
Member

What is the benefit of this change? Seems like it complicates the logic to the point that any change in the future could break things with no up side unless someone is trying to nest 500 lists, which seems very impractical.

@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
marked-website Ready Ready Preview Sep 3, 2026 3:50am UTC

Request Review

@unshorn

unshorn commented Sep 3, 2026

Copy link
Copy Markdown
Author

What is the benefit of this change? Seems like it complicates the logic to the point that any change in the future could break things with no up side unless someone is trying to nest 500 lists, which seems very impractical.

Thank you for reviewing the request.

the performance benefits are only seen in the pathological cases (deeply nested lists).. unfortunately, it doesn't entirely eliminate the bug class as pushing the depth can still result in heap exhaustion. this will require more than a patch. As for the cost/benefit of the refactoring w.r.t. maintenance, I'll leave that to your judgement. I think there's less of a burden on maintaining raw in multiple places but with a shift in readability.

Kind regards.

@nrps9909 nrps9909 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed d7e1b92ed053451a3585d943323f7f858b780fd5 against base 63841d3b0368e4b8df025ac3aca699141fa7bc56.

The consumed-source bounds preserve the raw-token contract in an independent comparison: 22,160 unique inputs across five GFM/pedantic/breaks configurations, with zero differences in complete lexer token trees (including raw values and link maps) or rendered HTML across 110,800 comparisons. Inputs include 850 unique existing fixtures, exhaustive three-line combinations of list markers/blank lines/tabs/fences/definitions, missing final newlines, and nested lists. This directly checks the synthetic-final-newline assumption in the new comment.

I also reproduced the memory benefit locally on Node 24.15.0, using a fresh process for each run and alternating base/head order. For a 500-level list over five runs each, median peak RSS was 198.5 MiB on base versus 124.3 MiB on head; median parse time was 312.1 ms versus 280.8 ms. All ten outputs have the same SHA-256. These are measurements for that pathological input on this machine, not a general throughput guarantee or a new stack-depth claim.

Full npm test passes, including specs, unit tests, docs/build, UMD/CJS, type/package checks and lint. All exposed upstream checks are successful apart from the expected skipped Release job. No author branch was edited.

Review and local validation performed with Codex.

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.

3 participants