From 1fc5bb34c454f875292c244a00ea0ba288b3b5c2 Mon Sep 17 00:00:00 2001 From: knsv Date: Mon, 22 Jun 2026 13:11:40 +0200 Subject: [PATCH 1/4] =?UTF-8?q?perf(flowchart):=20avoid=20O(n=C2=B2)=20par?= =?UTF-8?q?se=20cost=20on=20deeply-indented=20diagrams?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two whitespace-related regex pathologies made deeply-indented flowcharts parse in quadratic time: - anyCommentRegex (used by diagram-type detection and preprocessing) re-scanned each indent run from every position because of its leading greedy \s*. Guarding where the run may start makes it linear, with byte-identical output (proven by regexes.spec.ts against the legacy pattern as an oracle, plus an O(n) assertion). - The jison flowchart lexer matched a single whitespace char per SPACE token; it now matches a whitespace run. The grammar only uses SPACE as a separator, so parsing is unchanged. A deeply-indented fixture that took ~1.4 s to parse now takes ~30 ms. Split out of #7872 per review feedback so it can land and bisect independently. --- .../flowchart-deep-indent-parse-perf.md | 17 ++++++ .../mermaid/src/diagram-api/regexes.spec.ts | 55 +++++++++++++++++++ packages/mermaid/src/diagram-api/regexes.ts | 10 +++- .../src/diagrams/flowchart/parser/flow.jison | 2 +- 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 .changeset/flowchart-deep-indent-parse-perf.md create mode 100644 packages/mermaid/src/diagram-api/regexes.spec.ts diff --git a/.changeset/flowchart-deep-indent-parse-perf.md b/.changeset/flowchart-deep-indent-parse-perf.md new file mode 100644 index 00000000000..f7526b4d4f8 --- /dev/null +++ b/.changeset/flowchart-deep-indent-parse-perf.md @@ -0,0 +1,17 @@ +--- +'mermaid': patch +--- + +perf(flowchart): avoid O(n²) parse cost on deeply-indented diagrams + +Two whitespace-related regex pathologies made deeply-indented flowcharts parse in +quadratic time: + +- `anyCommentRegex` (used by diagram type detection and preprocessing) re-scanned + each indent run from every position because of its leading greedy `\s*`. Guarding + where the run may start makes it linear, with byte-identical output. +- The jison flowchart lexer matched a single whitespace char per `SPACE` token, + emitting one token per space; it now matches a whitespace run. The grammar only + uses `SPACE` as a separator, so parsing is unchanged. + +A deeply-indented fixture that took ~1.4 s to parse now takes ~30 ms. diff --git a/packages/mermaid/src/diagram-api/regexes.spec.ts b/packages/mermaid/src/diagram-api/regexes.spec.ts new file mode 100644 index 00000000000..43aaf3622ad --- /dev/null +++ b/packages/mermaid/src/diagram-api/regexes.spec.ts @@ -0,0 +1,55 @@ +import { describe, it, expect } from 'vitest'; +import { anyCommentRegex } from './regexes.js'; + +// The pre-optimization pattern, kept here as the equivalence oracle. The optimized +// `anyCommentRegex` must strip comments byte-for-byte identically to this — the only change is +// avoiding O(whitespace²) backtracking on deeply-indented input, not the matched text. +const LEGACY_ANY_COMMENT = /\s*%%.*\n/gm; + +// Fresh regex per call so the global `lastIndex` never leaks between assertions. +const strip = (re: RegExp, text: string) => text.replace(new RegExp(re.source, re.flags), '\n'); + +const CORPUS: string[] = [ + '%% a line comment\n', + 'A\n%% comment between\nB\n', + ' %% indented comment\n', + 'A --> B %% inline comment\n', + 'A\n\n%% comment after a blank line\n', + '%% first\n%% second\n', + 'graph TD\n A %% trailing\n B\n', + ' \n %% comment after whitespace-only line\n', + 'no comments here\nA-->B\nC-->D\n', + 'A\n%% comment with no trailing newline', + ' \n%% comment at col0 after a ws line\n', + 'X\n \n %% deep\n \nY\n', + '%%nospaceaftermarker\n', + 'a%%b\n', // `%%` mid-token, no leading whitespace + 'graph TD\n%% top\nA-->B %% mid\n%% bottom\n', + '\t\t%% tab indented\n', + 'flowchart LR\n subgraph S\n %% inside subgraph\n a-->b\n end\n', + '', + '\n\n\n', + '%%\n', // empty comment +]; + +describe('anyCommentRegex', () => { + it('strips comments byte-identically to the legacy /\\s*%%.*\\n/gm pattern', () => { + for (const input of CORPUS) { + expect(strip(anyCommentRegex, input)).toBe(strip(LEGACY_ANY_COMMENT, input)); + } + }); + + it('is O(n) on deeply-indented input (no catastrophic backtracking)', () => { + // Deep indentation with no comments is the worst case for the old leading-`\s*` pattern: the + // global match re-scanned each indent run from every position — O(whitespace²), ~250ms+ on the + // perf fixture huge3 (1.6k-space lines). The guarded pattern is O(n). + const pathological = + Array.from({ length: 300 }, () => ' '.repeat(1672) + 'classDef x fill:#fff').join('\n') + + '\n'; + const t0 = performance.now(); + const out = strip(anyCommentRegex, pathological); + const ms = performance.now() - t0; + expect(out).toBe(pathological); // no `%%` → nothing stripped + expect(ms).toBeLessThan(200); // legacy pattern takes seconds on this input + }); +}); diff --git a/packages/mermaid/src/diagram-api/regexes.ts b/packages/mermaid/src/diagram-api/regexes.ts index d133cd78f1b..71cd1f1e189 100644 --- a/packages/mermaid/src/diagram-api/regexes.ts +++ b/packages/mermaid/src/diagram-api/regexes.ts @@ -9,4 +9,12 @@ export const frontMatterRegex = /^([^\S\n\r]*)-{3}\s*[\n\r](.*?)[\n\r]\1-{3}\s*[ export const directiveRegex = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi; -export const anyCommentRegex = /\s*%%.*\n/gm; +// `(?:^|(?<=\S))` only lets the greedy leading `\s*` start where a whitespace run actually begins +// (string/line start, or right after a non-whitespace char). Without it, the global match retries +// `\s*` from every position inside an indent, re-scanning the run each time — O(whitespace²), which +// makes a deeply-indented diagram (1.6k-space lines) cost ~250ms here. The guard makes it O(n) while +// matching at the exact same positions, so the stripped output is byte-identical (see regexes.spec.ts). +// Note: the `(?<=\S)` lookbehind requires a modern engine (Chrome 62+, Firefox 78+, Safari 16.4+, +// Node 9+); this preprocessing runs for every diagram, so on an engine without lookbehind support it +// would throw a module-load SyntaxError. That's well within the project's supported runtimes. +export const anyCommentRegex = /(?:^|(?<=\S))\s*%%.*\n/gm; diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow.jison b/packages/mermaid/src/diagrams/flowchart/parser/flow.jison index 6be7526f955..e832fac96c8 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow.jison +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow.jison @@ -286,7 +286,7 @@ that id. "\"" return 'QUOTE'; (\r?\n)+ return 'NEWLINE'; -\s return 'SPACE'; +[^\S\n\r]+ return 'SPACE'; <> return 'EOF'; /lex From ba0deed758a7a8bb38155cee1b4f2374a96d14d7 Mon Sep 17 00:00:00 2001 From: Alois Klink Date: Tue, 25 Aug 2026 20:21:58 +0530 Subject: [PATCH 2/4] fix: bundle `fastdom` to fix issues with global `define` Fastdom is currently using UMD, and so uses AMD vs CommonJS based on whether there is a global `define` function available. Unfortunately, this breaks some setups, so we need an ESM version of this module. The easiest way of doing is to patch `fastdom`, but this means we need to bundle it, moving it to a `devDependency`. --- .changeset/silent-laws-find.md | 5 + cypress/platform/iife.html | 9 ++ package.json | 1 + packages/mermaid/package.json | 2 +- .../mermaid/src/rendering-util/fastdom.ts | 29 ++-- patches/fastdom.patch | 131 ++++++++++++++++++ pnpm-lock.yaml | 11 +- 7 files changed, 168 insertions(+), 20 deletions(-) create mode 100644 .changeset/silent-laws-find.md create mode 100644 patches/fastdom.patch diff --git a/.changeset/silent-laws-find.md b/.changeset/silent-laws-find.md new file mode 100644 index 00000000000..0648c5f0f0e --- /dev/null +++ b/.changeset/silent-laws-find.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: bundle `fastdom` to fix issues with global `define` diff --git a/cypress/platform/iife.html b/cypress/platform/iife.html index 7122785fc2d..b1a8dddf2b6 100644 --- a/cypress/platform/iife.html +++ b/cypress/platform/iife.html @@ -10,6 +10,15 @@
+ +