Repository navigation
feat(profiling): structured render profiler + benchmark harness + perf baseline - #7870
Conversation
… Explorer compare Add an opt-in hierarchical render profiler that times the pipeline in four phases — parse, prep (prepare+measure), layout, paint — plus a serialize tail, with nestable sub-spans. - Build-time gated by `injected.profiling` so it tree-shakes out of production entirely (zero bytes); runtime `enabled` toggle keeps it a no-op until turned on even in dev/profiling builds. - Emits User Timing measures (DevTools "Timings" track) and a structured console summary; exposes a single shared `globalThis.__mermaidProfiler` instance so spans from separately-bundled layouts (e.g. layout-elk) aggregate into one tree. - Instrumented at the unified seams: parse/draw/serialize in mermaidAPI and prepare/measure/layout/paint in the common layout renderer (dagre/elk/swimlane). - Dev Explorer gains a "Profile" tab: run the current diagram through selected layouts (median of N) and compare per-phase timings side by side. - Dev server builds a profiling-enabled mermaid core up front so external layout bundles that inline mermaid pick up live spans. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A neo-look dashed/dotted edge shorter than its combined marker offsets (negative middle length) or with a degenerate path (NaN getTotalLength) produced a negative/NaN count, so `Array(numberOfPairs)` threw "RangeError: Invalid array length" — surfacing on large diagrams where some edge hits that case. Clamp the dash-pair count to a non-negative finite integer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…phase dagre supplies a no-op measureLayout hook and sizes nodes inside its layout core, so its profiler "measure" phase read 0 while that cost hid inside "layout" — making it non-comparable to elk. Bracket the DOM node-sizing block (group setup, insertNode/getBBox, edge labels) with a "measure" span that ends before dagreLayout(), so the explorer can report measure on its own and layout as pure algorithm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ming, JSON export - Profile scope toggle: the current diagram or every .mmd in the folder. - Per-layout "score" = total render time across the set (lower is better) for tracking optimizations, plus per-phase totals and a per-diagram breakdown. - Warmup pass (absorbs the one-time dynamic layout-loader import) and drop the fastest + slowest run per series before averaging. - Normalize the measure/layout split so dagre and elk are comparable cell-by-cell. - "Copy JSON" button exports a structured snapshot of the last run. - Raise maxTextSize/maxEdges via initialize() (both are secure keys a diagram's frontmatter can't change) so large diagrams render in the explorer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15 flowchart fixtures (medium/large/huge) under dev-diagrams/performance plus a baseline.json captured with the Dev Explorer benchmark (folder scope, 5 iters, trimmed mean): dagre 11723ms vs elk 16766ms across the set. Tracks render performance so optimizations can be measured against a fixed baseline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap the actual external layout call — dagreLayout() and elkjs's elk.layout() — in a "layoutCore" span, so the layout phase breaks into: - lib: the external algorithm (untouchable) - ours: our wrapper around it (graph build/apply, serialization, DOM) Shown as sub-rows in the Dev Explorer table and carried in the JSON export as layoutLib/layoutOurs. elk reads the shared profiler off globalThis (it can't import it from the external package); dagre is gated by injected.profiling. Caveat: dagre lays out subgraphs recursively inside the measure span, so its "lib" reflects only the top-level call on diagrams with subgraphs; flat diagrams are exact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Four fixtures hardcoded `layout: dagre` in frontmatter, which overrode the benchmark's chosen layout (layout is not a secured config key) — so the elk column was silently rendering dagre. Drop the directive so each engine is actually profiled, and refresh baseline.json with valid numbers (dagre 11855ms vs elk 16406ms across the 15-fixture set). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add labeled performance.mark at each phase start and a "Mermaid render" custom DevTools track (detail.devtools), and instrument the per-node measure reads (getBBox / getBoundingClientRect in labelHelper, insertLabel and updateNodeBounds) into summed profiler buckets so the measure phase's cost can be attributed. All sites are guarded by `injected.profiling`, so production builds tree-shake them away. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Show the summed getBBox/getBoundingClientRect buckets in the Profile tab and take the external-library layout time from the direct child of the layout span (dagre emits nested subgraph layoutCore spans that a depth-first search would wrongly attribute). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for mermaid-js ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
🦋 Changeset detectedLatest commit: 71b8843 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
eslint require-await failed in CI (3 errors) — sync test callbacks were marked async. profiler.span accepts sync functions, so the async is unnecessary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The machine-generated baseline.json wasn't run through prettier (the pre-commit lint-staged glob excludes .json), so CI's prettier --check flagged it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js
@mermaid-js/examples
mermaid
@mermaid-js/layout-elk
@mermaid-js/layout-tidy-tree
@mermaid-js/mermaid-zenuml
@mermaid-js/parser
@mermaid-js/tiny
commit: |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #7870 +/- ##
==========================================
- Coverage 2.94% 2.91% -0.03%
==========================================
Files 655 656 +1
Lines 69752 70406 +654
Branches 978 979 +1
==========================================
+ Hits 2053 2055 +2
- Misses 67699 68351 +652
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
… don't throw esbuild's `define` replaces `injected.profiling` etc. only in bundled builds. The docs generator runs source through tsx without that define, so profiler.ts's module-level `injected.profiling` threw `ReferenceError: injected is not defined`, failing build-docs. Seed a production-equivalent default on globalThis before the read; bundled builds replace the reads with literals and never touch it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Hi @knsv-bot [sisyphos-bot] What's Working Well 🎉 The dual-gate pattern is the right architecture. Build-time constant (injected.profiling) eliminates the code from production bundles entirely. The runtime toggle 🎉 profiler.span() correctly prevents span leaks on exceptions — the try/finally ensures end() is called regardless of whether the wrapped callback throws. The same guarantee 🎉 profiler.stop() defensively drains the entire stack (while (this.stack.length > 0) { this.end(); }), so a thrown exception that skips stop() won't corrupt the profiler state 🎉 The ELK cross-bundle solution is well-conceived. The @mermaid-js/layout-elk package can't import profiler.ts from the mermaid package, so it reaches the shared instance via 🎉 The generateDashArray bug fix is clean and well-commented. Array(n) with negative n throws a RangeError; clamping to Math.max(0, rawPairs) with a NaN guard is the correct 🎉 injected global fallback seed in profiler.ts correctly handles tsx/ts-node environments where esbuild's define replacements haven't run. Well-documented. Issues 🟡 [important] — Missing changeset for the generateDashArray bug fix packages/mermaid/src/rendering-util/rendering-elements/edges.js has a production fix: Array(numberOfPairs) was throwing RangeError: Invalid array length on short or degenerate 🟡 [important] — tickSync, tick, and buckets have no test coverage packages/mermaid/src/profiler.spec.ts tickSync and tick (added in a later commit) are the methods actually called in the shared rendering code (createText.ts instrumented three getBBox/getBoundingClientRect call Suggested additions to profiler.spec.ts: it('buckets reset between renders', () => { 🟢 [nit] — profiler.begin('serialize') is manually paired in mermaidAPI.ts packages/mermaid/src/mermaidAPI.ts The serialize phase uses an explicit begin/end pair rather than span(). If anything between them throws (unlikely, but attachFunctions() mutates DOM state), the span leaks open. 🟢 [nit] — && profiler.tickSync optional check in call sites could use a comment packages/mermaid/src/rendering-util/createText.ts and shapes/util.ts The pattern: Security The profiler is entirely dev-only. In production (injected.profiling = false), every if (injected.profiling) block folds away and profiler.ts tree-shakes out entirely. No |
- Add a patch changeset for the generateDashArray RangeError fix (a user-visible edge-rendering crash on short/degenerate paths). - Cover tickSync/tick, bucket accumulation, and the per-render bucket reset in profiler.spec.ts (previously untested). - Run the serialize phase inside a guarded profiler.span() instead of a manual begin/end pair, so a throw can't leak the span or record duration:-1; the `injected.profiling` ternary keeps it tree-shaken and zero-cost in production (consistent with parse/draw/measure). - Document the `&& profiler.tickSync` cross-version guard at the createText.ts / shapes/util.ts call sites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough review @pbrolin47 — all four addressed in 71b8843: 🟡 Missing changeset for the 🟡 🟢 🟢 |
|
@knsv-bot , thanks for addressing the issue [sisyphos-bot] PR #7870 — Follow-up Review (commit 14/14: "address PR review feedback") All items from the previous review are resolved: ✅ Missing changeset — resolved .changeset/fix-edge-dasharray-rangeerror.md added with patch bump for mermaid. The description correctly explains both failure scenarios (short edge → negative count, degenerate ✅ tickSync/tick/buckets untested — resolved profiler.spec.ts adds 6 new tests that cover every case that was missing:
The test for multiple tickSync calls summing into a single bucket (sort().toEqual([...])) is particularly thorough. ✅ profiler.begin('serialize') manual pair — resolved, and improved The serialize phase is now extracted into a serializeSvg() closure and run via profiler.span('serialize', serializeSvg). This is cleaner than a simple begin/end swap — DOMPurify ✅ && profiler.tickSync guard comment — resolved Both createText.ts and shapes/util.ts now have clear comments explaining the cross-version guard. The shapes/util.ts comment also explicitly notes "Same pattern applies to the Verdict: APPROVE — 🔴 0 / 🟡 0 / 🟢 0 / 🎉 4 |
Upstream release: https://github.com/mermaid-js/mermaid/releases/tag/mermaid%4011.17.0 Release notes: ### Minor Changes - [#7842](mermaid-js/mermaid#7842) [`3670b4e`](mermaid-js/mermaid@3670b4e) Thanks [@filipsajdak](https://github.com/filipsajdak)! - feat(c4): render C4 elements through the unified shape system, using the new person shape - [#7812](mermaid-js/mermaid#7812) [`cdfc0ea`](mermaid-js/mermaid@cdfc0ea) Thanks [@knsv-bot](https://github.com/knsv-bot)! - feat(class): route `classDiagram` to the unified (v2) renderer by default Set `class: { defaultRenderer: 'dagre-d3' }` in the config to restore the legacy renderer. - [#7785](mermaid-js/mermaid#7785) [`c45cde9`](mermaid-js/mermaid@c45cde9) Thanks [@knsv-bot](https://github.com/knsv-bot)! - feat(flowchart): add collapsible flowchart subgraphs via `subgraphId@{ view: collapsed }` - [#7828](mermaid-js/mermaid#7828) [`8eb3afc`](mermaid-js/mermaid@8eb3afc) Thanks [@knsv-bot](https://github.com/knsv-bot)! - feat(elk): add `elk.keepEntryNodeOnTop` config option to keep a recursive flow's entry node on top - [#7803](mermaid-js/mermaid#7803) [`74e44eb`](mermaid-js/mermaid@74e44eb) Thanks [@knsv-bot](https://github.com/knsv-bot)! - feat(elk): add `elk.nodePlacementAlignment` config option - [#7792](mermaid-js/mermaid#7792) [`ea55b31`](mermaid-js/mermaid@ea55b31) Thanks [@RodrigojndSantos](https://github.com/RodrigojndSantos)! - feat(er): add subgraph support to ER diagrams. - [#7970](mermaid-js/mermaid#7970) [`a2c0fb6`](mermaid-js/mermaid@a2c0fb6) Thanks [@filipsajdak](https://github.com/filipsajdak)! - feat(flowchart): add `folder`, `bucket`, `console` (terminal window) and `browser` shapes - [#7842](mermaid-js/mermaid#7842) [`ae3e115`](mermaid-js/mermaid@ae3e115) Thanks [@filipsajdak](https://github.com/filipsajdak)! - feat(flowchart): add `person` shape (circular head above a rounded body), usable in flowcharts via `A@{ shape: person }` - [#7724](mermaid-js/mermaid#7724) [`0fd7a9f`](mermaid-js/mermaid@0fd7a9f) Thanks [@xdumaine](https://github.com/xdumaine)! - feat(xyChart): add legends for named line and bar series ### Patch Changes - [#7847](mermaid-js/mermaid#7847) [`215fe89`](mermaid-js/mermaid@215fe89) Thanks [@filipsajdak](https://github.com/filipsajdak)! - fix(c4): named attributes such as `$tags`, `$link` and `$sprite` are no longer clobbered to undefined when they arrive in an earlier positional slot of Person/System/Container/Component/Boundary/Rel statements. - [#7871](mermaid-js/mermaid#7871) [`8d874c4`](mermaid-js/mermaid@8d874c4) Thanks [@knsv-bot](https://github.com/knsv-bot)! - fix(flowchart): stop dagre layout from spamming `warn`-level logs on every node/edge/cluster - [#8071](mermaid-js/mermaid#8071) [`b3d1f63`](mermaid-js/mermaid@b3d1f63) Thanks [@pbrolin47](https://github.com/pbrolin47)! - fix(block): sibling blocks overlapping in block diagrams when one has a label wider than 200px - [#7870](mermaid-js/mermaid#7870) [`71b8843`](mermaid-js/mermaid@71b8843) Thanks [@knsv-bot](https://github.com/knsv-bot)! - fix: a `RangeError: Invalid array length` crash when rendering certain edges. - [#7924](mermaid-js/mermaid#7924) [`9cbef5d`](mermaid-js/mermaid@9cbef5d) Thanks [@nightt5879](https://github.com/nightt5879)! - fix(treeView): icons disappearing after strict security sanitization. - [#7850](mermaid-js/mermaid#7850) [`a34cbf0`](mermaid-js/mermaid@a34cbf0) Thanks [@aloisklink](https://github.com/aloisklink)! - fix(block): allow classdefs to update text color - [#7937](mermaid-js/mermaid#7937) [`f9cbe1e`](mermaid-js/mermaid@f9cbe1e) Thanks [@filipsajdak](https://github.com/filipsajdak)! - fix(dagre): let a diagram's own nodeSpacing/rankSpacing take effect in the unified dagre layout - [#8005](mermaid-js/mermaid#8005) [`90eeece`](mermaid-js/mermaid@90eeece) Thanks [@pbrolin47](https://github.com/pbrolin47)! - fix(flowchart): reverts the behavior change from #7672 (fix/4648-directions), since arrows between subgraphs are broken - [#7951](mermaid-js/mermaid#7951) [`afa2f80`](mermaid-js/mermaid@afa2f80) Thanks [@aloisklink](https://github.com/aloisklink)! - perf: use `fastdom` to batch DOM measurements (up to 25% speedup) - Updated dependencies \[[`e848423`](mermaid-js/mermaid@e848423)]: - @mermaid-js/parser@1.2.1 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: give me a 98K <240642031+inhuman-0@users.noreply.github.com>
Summary
Adds a zero-production-cost structured render profiler and a Dev Explorer
benchmark harness so we can see exactly where time goes when rendering large
diagrams — plus performance fixtures and a baseline score to track improvements.
Measurement only: no rendering behavior changes.
This is the foundation for a series of large-diagram perf optimizations; the
follow-up PRs build on it.
Motivation
Optimizing large-diagram rendering responsibly needs reliable, low-overhead
measurement that:
dagreLayout/ elkelk.layout,not ours to optimize) from our wrapper code; and
getBBox/getBoundingClientRect)inside the measure phase.
Zero production cost
Every call site is guarded by
injected.profiling, a build-time constant esbuildreplaces with
falsein normal builds. The guards fold away, allprofilerreferences drop, and the module tree-shakes out — zero bytes, zero runtime cost.
Two gates:
injected.profiling) — present only in dev/profiling builds.profiler.enabled) — off by default even when compiled in; opt inwith
__mermaidProfiler.enable().What's included
Core profiler (
packages/mermaid/src/profiler.ts+ spec)performance.measureper phase (DevTools Timings), labeledperformance.markat each phase start, and a "Mermaid render" custom DevTools track (Chrome 130+).
core) via
globalThis.Phase instrumentation
mermaidAPI.ts: parse + serialize spans.dagre/index.js: dagre node-measurement attributed to measure;dagreLayout()bracketed as layoutCore.
mermaid-layout-elk/src/render.ts):elk.layout()bracketed as layoutCore.getBBox/getBoundingClientRect).Dev Explorer Profile tab
Copy-as-JSON for cross-session tracking.
emits nested subgraph
layoutCorespans a depth-first search would misattribute).Build plumbing —
injected.profilingdefine wired through the esbuild/vite configs.Perf fixtures + baseline —
cypress/platform/dev-diagrams/performance/flowcharts/(medium/large/huge ×5) +
baseline.json.Drive-by fix — guard
generateDashArrayagainst short/degenerate edges (aRangeErrorsurfaced while profiling huge flowcharts).How to use
pnpm dev./dev→ pick a diagram → Profile tab → Run profile (or benchmark the folder).__mermaidProfiler.enable(), enable "Show custom tracks" in thePerformance panel, record one render → phases appear as labeled bars in the
"Mermaid render" track with
▶start markers.Risk / testing
profiler.spec.tscovers the profiler; existing suites unaffected.🤖 Generated with Claude Code