Skip to content

fix: quadratic inline parse time on failed link candidates (IJPL-96392) - #233

Merged
Ilia Permiashkin (p3rmiashkin) merged 1 commit into
masterfrom
fix/ijpl-96392-quadratic-link-candidate-scans
Sep 29, 2026
Merged

Ilia Permiashkin (p3rmiashkin) merged 1 commit into
masterfrom
fix/ijpl-96392-quadratic-link-candidate-scans

Conversation

@jetbrains-air

@jetbrains-air jetbrains-air Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Problem

IJPL-96392 reports ~15 seconds of full CPU load after every keystroke in a big (~1k lines) Markdown file, with laggy highlighting. The IDE re-parses the file on each keystroke, so any super-linear parse cost in this library multiplies into exactly that symptom.

Profiling the parser against a battery of inputs found that inline link parsing is quadratic on text with many bracket link candidates that never form a link. Every [ triggers forward token scans (parseLinkText, parseLinkDestination, parseLinkTitle) that are thrown away when the candidate fails, so the next candidate re-scans the same tokens:

  • Nested matched brackets [[[…]]]: ReferenceLinkParser (and the GFM MathParser link-range scan) runs parseLinkText up to the matching ] for every opening bracket — measured 1.67 s for 16 KB of input (4× time per size doubling), i.e. the reported ~15 s at realistic file sizes.
  • [...]( without a closing ): InlineLinkParser scans for a link title up to the end of the paragraph for every candidate — measured 2.67 s for 40 KB.

This is the same family as the earlier IJPL-93535 "Freeze in LinkParserUtil" fix (buildBracketStarts), which pruned unmatched-bracket candidates but not these two shapes.

Fix

Successful parses may keep their scans — they consume the tokens they scanned, so their cost amortizes to linear. Only failed candidates need to be cheap:

  • LinkParserUtil.buildBracketMatches precomputes the [ → matching ] index map in one pass. ReferenceLinkParser and MathParser use it to skip the full-reference-link attempt in O(1) unless the raw token right after the matching ] is [ — a necessary condition the old code only discovered after scanning the whole link text (it.start == linkTextEnd + LBRACKET inside parseLinkLabel).
  • LinkParserUtil.collectRParenIndices precomputes the sorted ) token indices in the parsed ranges. InlineLinkParser and MathParser reject a candidate in O(1) when no ) exists after its ]( — a necessary condition for parseInlineLink's final RPAREN check.

Both pre-checks are pure necessary conditions of the existing parsing code, so parse results are unchanged.

Verification

  • New performance regression tests (testNestedMatchedBracketsAreLinear, testLinkCandidatesWithoutClosingParenAreLinear, both CommonMark and GFM flavours) fail on master (808 ms+ per parse vs the 250 ms budget) and pass with the fix at ~6 ms / ~14 ms per parse — a ~200–250× improvement on those inputs.
  • Full jvmTest + performanceTest suites: 1582 tests, 0 failures. JS target compiles.
  • Realistic large documents (gitBook.md repeated up to 8 MB / 122k lines) already scaled linearly and are unaffected.

Remaining uncertainty / follow-up

The reporter's file is not attached to the issue, so the exact trigger cannot be confirmed; these are the only inline-parser behaviors found that reach the reported magnitude on bracket-heavy text. Two smaller, separate pathologies were also identified and are left out of scope:

  • The GFM lexer is quadratic on repeated scheme prefixes (e.g. http://http://…): after each autolink candidate the DFA stays alive through the rest of a whitespace-free run via the optional {URL_USER_INFO_CHAR}+ "@" userinfo branch (whose char class includes / and :), then falls back. / is not valid in an RFC 3986 userinfo, so removing it from URL_USER_INFO_CHAR in gfm.flex would bound the scan, but that requires regenerating _GFMLexer.kt.
  • parseLinkDestination can still scan far on whitespace-free runs of unbalanced ( — reachable only with highly artificial input.

Fixes IJPL-96392.

Produced by Air Automations. Name: Markdown library: fix the bug / Run: https://air.jetbrains.cloud/org/05cf1a7f-6ab5-713b-abd3-29d0c8a05e2d/automations/cb6a17f6-e7a9-41a8-9611-7c95c4eb0141?run=e9cbf485-8272-43f8-9187-aab50412fdd2

🤖 Generated with Claude Code

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Verdict: request changes — the two blocking inline comments need to be addressed before merge (posted as a comment review because GitHub does not allow this account to formally request changes on its own PR).

Produced by Air Automations. Name: Markdown library: review the PR / Run: https://air.jetbrains.cloud/org/05cf1a7f-6ab5-713b-abd3-29d0c8a05e2d/automations/2269eaa4-da15-4cac-957f-3341151c79ec?run=93e2f7b9-7d8c-4f66-a936-89c5392f1cca

Inline parsing was quadratic on text full of link or image candidates
that never form a link. For every `[` or `![`, the link parsers scanned
forward for the end of the link text, destination, and title, then threw
the scans away when the candidate failed, so the next candidate rescanned
the same tokens. With 8,000 repetitions, each of these took seconds per
parse, up to ~14 s:

- nested brackets such as `[[[a]]]`, `[[[a](](` or `[[[a][]][]`
  rescanned the link text up to its matching `]` for every `[`;
- `[a](b (` or `[a](<` scanned for the end of the title or of the `<...>`
  destination up to the end of the paragraph, even when a single `)`
  followed;
- `[a](((` without white space scanned the destination up to the end of
  the word for every candidate.

A successful link consumes the tokens it scanned, so only failed
candidates need to be cheap. LinkParserUtil.ScanIndex is built once per
parsed range list and records where each link part starting at a token
ends: the `]` matching a link text, the end of a destination without
braces, and the next `>`, `)`, or quote closing a `<...>` destination or
a title. The image, inline link, reference link, and GFM math parsers
pass it to parseLinkDestination and parseLinkTitle, which look the end
up instead of scanning, and collect the link text only once a link is
known to be complete. The index is built only when a range list has a
link candidate, and its destination and title parts only on first use,
so plain text does no extra work.

Parse results are unchanged: the trees of 30k random bracket-heavy
inputs are identical to master's in both flavours. The shapes above now
parse in 6-21 ms and scale linearly, and realistic documents (gitBook,
commonMarkSpec, fogChangelog) parse as fast as before. The new
performance tests cover these shapes and their image variants.

Not covered: the GFM lexer is still quadratic on long runs of autolink
starts without white space, such as `http://http://...` or
`www.a!www.a!...`, because the optional `userinfo@` prefix of
GFM_AUTOLINK keeps its scan going to the end of the run. That needs a
separate lexer change.

Fixes IJPL-96392.

Produced by Air Automations. Name: Markdown library: fix the bug / Run: https://air.jetbrains.cloud/org/05cf1a7f-6ab5-713b-abd3-29d0c8a05e2d/automations/cb6a17f6-e7a9-41a8-9611-7c95c4eb0141?run=e9cbf485-8272-43f8-9187-aab50412fdd2

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@p3rmiashkin

Copy link
Copy Markdown
Contributor

I've addressed problems above ^, so should be better now

@donnerpeter

Copy link
Copy Markdown
Member

Codex (@codex) review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-29T15:44:15.587307Z 36d0b02 Manual request
ℹ️ About Codex in GitHub

Your 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36d0b02ee2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

@p3rmiashkin
Ilia Permiashkin (p3rmiashkin) merged commit 02d0654 into master Sep 29, 2026
6 checks passed
@p3rmiashkin
Ilia Permiashkin (p3rmiashkin) deleted the fix/ijpl-96392-quadratic-link-candidate-scans branch September 29, 2026 15:59
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.

2 participants