Skip to content

fix: free the runtime flow-node catalog on App.deinit (shutdown leak) (#209)#211

Merged
apotema merged 2 commits into
mainfrom
fix/shutdown-leak-209
Jun 9, 2026
Merged

fix: free the runtime flow-node catalog on App.deinit (shutdown leak) (#209)#211
apotema merged 2 commits into
mainfrom
fix/shutdown-leak-209

Conversation

@apotema

@apotema apotema commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Closes #209.

Root cause

The process-global current_runtime: ?*RuntimeCatalog (src/flow_node_catalog.zig) holds an arena with the entries/pin_styles slices built via ArrayList.append + toOwnedSlice (the ensureTotalCapacityPrecise grow the #209 trace showed). App.reloadFlowNodeCatalog installs a catalog via setRuntime(cat) when a project has a .labelle/flow_catalog.json sidecar (bouncing-ball does), but setRuntime only frees the PREVIOUS catalog on the next project transition. Shutdown isn't a transition, so the last-loaded catalog's arena leaked on window close.

The sendFile WriteFailed in the trace is a red herring — it's the generic error.WriteFailed from std.Io.Dir.writeFile (any failed save), on a separate path from the catalog (parsed on project LOAD). The leak is unconditional after loading a sidecar project, not gated on a failed write.

Fix

App.deinit now calls flow_node_catalog.setRuntime(null), reverting to the static fallback and freeing the active runtime catalog + arena.

Verification — reproduced under testing.allocator

Regression test in flow_node_catalog.zig: loads a real sidecar, installs it (project-load path), reverts once (shutdown path) under std.testing.allocator. Confirmed it catches the leak — skipping the free makes it report "Memory Leak Detected"; with the fix it passes. (Existing gui-tests missed it: they open .flow.jsonc without a sidecar-bearing project, and gui_tests.zig discards the DebugAllocator result.)

zig build test 464/464; zig build gui-test 26/26; zig build clean.

The editor leaked an allocation on exit whenever a project with a
.labelle/flow_catalog.json sidecar was loaded. App.reloadFlowNodeCatalog
installs a RuntimeCatalog into the process-global current_runtime via
setRuntime(cat); setRuntime only frees the *previous* catalog on the
next project transition. Shutdown is not a transition, so the
last-loaded catalog's arena -- holding the entries/pin_styles slices
grown from ArrayLists in loadFromPath -- outlived App.deinit and was
reported as a leaked ArrayList grow by the DebugAllocator on window
close.

Fix: App.deinit now calls flow_node_catalog.setRuntime(null), reverting
to the static fallback and freeing the active runtime catalog.

Adds a leak-checked regression test that loads a real sidecar, installs
it as the active catalog (the project-load path), and reverts exactly
once (the shutdown path). Verified the test reproduces the leak: with
the revert skipped it reports 'Memory Leak Detected' under
testing.allocator.

zig build clean; zig build test 464/464; zig build gui-test 26/26.
@cursor

cursor Bot commented Jun 9, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Small, targeted cleanup in deinit using the existing setRuntime API; no behavior change during normal editing, only on exit.

Overview
Fixes a shutdown memory leak when a project with a .labelle/flow_catalog.json sidecar was loaded: the process-global runtime catalog was only torn down on the next project transition via setRuntime, not when the app exits.

App.deinit now calls flow_node_catalog.setRuntime(null) after flow-runtime teardown, reverting to the static catalog and freeing the sidecar RuntimeCatalog (arena-backed entries / pin_styles).

A regression test in flow_node_catalog.zig loads a synthetic sidecar, installs it, then performs the same single revert as shutdown under std.testing.allocator, with a defer setRuntime(null) guard so failed assertions do not leak into later tests.

Reviewed by Cursor Bugbot for commit b2ee3cd. Bugbot is set up for automated code reviews on this repo. Configure here.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request fixes a memory leak (issue #209) by ensuring that the process-global flow-node catalog is reverted to its static fallback via setRuntime(null) during application shutdown in App.deinit. It also adds a regression test to verify this behavior. The review feedback suggests using defer setRuntime(null); in the test to guarantee cleanup and prevent global state pollution if any assertions fail before the manual cleanup is reached.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/flow_node_catalog.zig
Comment on lines +1199 to +1208
setRuntime(cat);
try std.testing.expect(current_runtime != null);
try std.testing.expectEqualStrings("synthetic.do_thing", entries[0].name);

// Shutdown path: the single revert `App.deinit` now performs. This
// is the *only* free of `cat`'s arena — drop it and the test leaks.
setRuntime(null);
try std.testing.expect(current_runtime == null);
try std.testing.expectEqual(static_pin_styles.len, pin_styles.len);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If any of the assertions fail before setRuntime(null) is reached, the test will exit early, leaking the allocated RuntimeCatalog and leaving the global current_runtime state dirty for subsequent tests.

Since setRuntime(null) is idempotent and safe to call multiple times, we can use defer setRuntime(null); immediately after setRuntime(cat); to guarantee cleanup on failure.

    setRuntime(cat);
    defer setRuntime(null);
    try std.testing.expect(current_runtime != null);
    try std.testing.expectEqualStrings("synthetic.do_thing", entries[0].name);

    // Shutdown path: the single revert App.deinit now performs. This
    // is the *only* free of cat's arena — drop it and the test leaks.
    setRuntime(null);
    try std.testing.expect(current_runtime == null);
    try std.testing.expectEqual(static_pin_styles.len, pin_styles.len);
}

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.

Done — defer setRuntime(null); right after install, so a failed assertion can't leak cat or leave current_runtime dirty. The explicit revert (the thing under test) stays; the defer is an idempotent no-op after it.

Copilot AI 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.

Pull request overview

Fixes a shutdown-time memory leak by ensuring the process-global runtime flow-node catalog is freed when the app exits (not only on project transitions).

Changes:

  • Call flow_node_catalog.setRuntime(null) from App.deinit to free the active runtime catalog arena on shutdown.
  • Add a regression test that installs a sidecar-loaded runtime catalog and verifies a single shutdown-style revert restores the static catalog without leaking.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/flow_node_catalog.zig Adds a regression test covering shutdown-style cleanup of a sidecar-loaded runtime catalog.
src/app.zig Ensures shutdown resets the global flow-node catalog to the static fallback, freeing the last runtime catalog.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/flow_node_catalog.zig
Comment on lines +1198 to +1202
const cat = try loadFromPath(aa, path);
setRuntime(cat);
try std.testing.expect(current_runtime != null);
try std.testing.expectEqualStrings("synthetic.do_thing", entries[0].name);

@apotema apotema merged commit 3b0c3d8 into main Jun 9, 2026
4 checks passed
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.

Editor: DebugAllocator leak (+ sendFile WriteFailed) on shutdown after opening a project

2 participants