Skip to content

Mermaid flowchart layout can spin a core for the life of the process, and the render task cannot be cancelled #62330

Description

@fiorelorenzo

Summary

A Mermaid flowchart sent Zed into what looks like a non-terminating layout, and it stayed there for the entire life of the process. Zed burned a full core (110% to 145%) continuously for almost 11 hours, my MacBook got hot and battery life dropped to about an hour. The work happens on a background dispatch worker, so the UI stays usable and there is nothing on screen telling you what is going on.

Two things make this worse than a slow render: the task cannot be cancelled from the UI (archiving the agent thread that holds the diagram changes nothing), and it comes back within about 80 seconds of a restart because the thread is restored and re-rendered.

Environment

  • Zed 1.14.2 (20260805.160132), macOS 26.5.2, Apple M4
  • Cargo.lock at v1.14.2 pins merman / merman-core / merman-render 0.6.2 from zed-industries/merman at tag v0.6.2-with-patches, commit 9acc3960f04a7deeb08079d60fa8183f15e8bde1
  • The diagram was in an agent panel thread (external ACP agent), not a Markdown preview

The stack

Five sample runs over roughly two hours, across two different Zed processes, all show the same thing. The thread is at 100% of its samples on this path every time:

Thread_10165547  DispatchQueue_13: com.apple.root.default-qos (concurrent)
  _dispatch_worker_thread2
    ...
      merman::render::render_svg_with_pipeline_sync
        merman_render::layout_parsed_render_layout_only
          merman_render::flowchart::layout::layout_flowchart_v2_typed
            layout_flowchart_v2_with_model
              layout_graph_with_recursive_clusters::update_child_cluster_bounds
                merman_render::flowchart::label::flowchart_label_plain_text_for_layout
                  core::str::pattern::CharSearcher::next_match

Heaviest leaves, "sort by top of stack", from one of the runs:

core::str::pattern::CharSearcher::next_match                    (in zed)   939
merman_render::flowchart::label::flowchart_label_plain_text_for_layout  (in zed)   235

What I measured

  • Old process: 10h50m wall, 3h35m of CPU time, sitting at 111% to 145% when I looked at it.
  • I quit and relaunched Zed. The new process was back at 120% after 78 seconds, and averaged 128% over the next 1h51m (142m29s of CPU over 111m of wall clock). Same stack.
  • Resident memory grew from about 570 MB to 1.1 GB in a few minutes while looping.
  • Zed's own hang detector fired four times that afternoon, files in ~/Library/Application Support/Zed/hang_traces/ at 17:19, 18:09, 18:18 and 19:08. Their timings arrays are all empty, so they were not much help.
  • Archiving the agent thread that held the diagram did nothing. I re-sampled right after and got the identical stack with the same hot leaf. render_svg_with_pipeline_sync is sync and has no cancellation point, so once it is spawned it runs until the process dies.
  • What actually fixed it: I rewrote the fence tag on every Mermaid block in the ACP agent's stored transcripts so Zed would not render them, then restarted. Zed went from 128% average to 19% average (40.3s of CPU over 3m29s), and merman does not appear even once in a fresh sample. The top leaves are now all kernel waits.

What I could not isolate, and why

I could not produce a minimal .mmd. I extracted all 59 unique Mermaid blocks from the affected threads and ran each one through merman-cli render --format svg, built from the exact commit Zed pins. None of them hang: 34 render, 25 fail fast with parse errors, all well under a 15 second timeout.

So the input alone is not enough. My guess is that it needs Zed's own text measurer, since the CLI uses deterministic or vendored and the measured label geometry is exactly what feeds back into update_child_cluster_bounds. I have not been able to confirm that.

For what it is worth, flowchart_label_plain_text_for_layout itself is a plain linear scan, so it is hot because it is being called an unbounded number of times rather than because it loops. The worklist in layout_graph_with_recursive_clusters (crates/merman-render/src/flowchart/layout.rs, the while let Some(frame) = stack.pop() around line 1659) has no visited set and no iteration or depth cap, and the _depth parameter is unused. Between passes, ctx.extracted.remove(...) followed by the re-insert after layout, plus inject_parent_cluster mutating the child graph, means the set of children can change from one visit to the next. That is where I would look first, though it is a hypothesis and not something I proved.

What I would ask for

Even without finding the offending diagram, two changes would turn this from "my laptop is hot for 11 hours and I have no idea why" into a visible, bounded failure:

  1. An iteration or wall-clock cap in the recursive cluster layout, so a pathological diagram ends up as a diagram that failed to render rather than a core pegged for the lifetime of the process.
  2. A cancellation point in the render pipeline, so closing or archiving the thread (or closing the buffer) actually stops the work.

Happy to run anything you want on the machine that reproduces it, including a build with extra instrumentation. I still have the transcripts with the diagrams that triggered it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:needs triagetriage is not complete: no `area:*` label, or no `severity:*`/`reach:*` for Bugs/Crashes

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions