Skip to content

Commit e673a5b

Browse files
686 bug cant open breadcrumbs setting menu (#688)
* TS leak fix - src/views/tree.ts, src/views/matrix.ts: onOpen now await unmount(old) before mounting new component. - src/views/page.ts: track mounts in WeakMap<MarkdownView, ...>, unmount prior mount per leaf each redraw_page_views, plus cleanup_page_views(plugin) export. - src/main.ts: call cleanup_page_views(this) in onunload. WASM soft-skip (rebuilt via bun run wasm:dev) - wasm/src/data/edge_struct.rs: new is_current_revision() helper. - wasm/src/data/traversal.rs rendering_obj_at_index: returns JsValue::UNDEFINED for stale edges instead of throwing. NestedEdgeList's {#if render_data} already swallows undefined → no row, no console error. - wasm/src/edge_sorting.rs sort_traversal_data / sort_flat_traversal_data: skip sort if any edge stale. * fix(686): settings tab freeze on Windows — eager init reactive_settings The reactive_settings.current getter assigned to its own $state variable when read before init, which Svelte 5 detected as a derivation self-mutating its own dependency and looped to effect_update_depth_exceeded. Initialise _settings eagerly with DEFAULT_SETTINGS so the getter is a pure read; init() simply swaps in the real settings. release: 4.9.3-beta.2 * chore(diagnostics): perf marks + reactive-loop counters around settings tab Add src/utils/perf.ts with perf_start/perf_end/perf_sync helpers and an effect_counter that warns once when an effect re-runs >50x in <250ms. All gated by debug.level <= DEBUG, so default INFO users see nothing. Wrap SettingsTab.display, SettingsTab._build, each mount() call, and each _add_settings_* section with perf_sync. Wrap reactive_settings.init with perf_start/perf_end to confirm init timing. Drop effect_counter ticks into the $effect bodies most likely to storm: - TrailView writeback - Matrix writeback - TreeView writeback - TransitiveImpliedRelations opens_sync - NestedEdgeList opens_sync Goal: diagnose remaining few-second freeze + effect_update_depth_exceeded on Windows in 4.9.3-beta.2. release: 4.9.3-beta.3 * chore(diagnostics): cover settings sub-components with effect counters Beta.3 showed SettingsTab.display completes in 27ms cleanly, yet the effect_update_depth_exceeded fires on the next microtask. The loop must live in an $effect inside a sub-component that beta.3 did not instrument. Add effect_counter ticks to: - MatrixFieldOrderSettingItem - FieldGroupLabelsSettingItem - ShowAttributesSettingItem - EdgeSortIdSettingItem - ShowAttributesSelectorMenu (strip-excluded $effect.pre) These are all mounted from _add_settings_matrix / _add_settings_tree_view / _add_settings_list_index / _add_settings_freeze_implied_edges with $bindable + select_cb pattern that writes back into plugin.settings — a plausible cycle if Svelte 5 deep proxy notifies the prop subscriber. release: 4.9.3-beta.4 * chore(diagnostics): cumulative effect counter + cover remaining effects Beta.4 used a 250ms sliding window: an effect had to fire 50 times within that window to trip. The reported 38-second hang on Windows fires ~2 iterations/second (each iteration awaits saveData disk I/O), so the window never accumulated 50 runs and no counter logged. Rework effect_counter to track cumulative runs and log at thresholds 1, 10, 50, 200, 1000. First-tick logs at DEBUG; subsequent thresholds log at ERROR as effect-storm. Also instrument: - TrailView.log - TrailView.depth - TreeView.depth - TreeView.root_open - LockViewButton (writes lock_path bound to side-view settings) - RenderMarkdown release: 4.9.3-beta.5 * fix(686): break ShowAttributes/EdgeSortId/FieldGroupLabels reactive loop ShowAttributesSettingItem, EdgeSortIdSettingItem, FieldGroupLabelsSettingItem each ran a $effect that read a $bindable prop and called select_cb(value) on every change. select_cb writes plugin.settings.X = value. Svelte 5 deep proxies notify subscribers on every property assignment — even with the identical reference — so the prop reads back, the $effect re-fires, calls select_cb again, ad infinitum. Each iteration awaits async saveData, so the loop ran ~2/sec for ~38s on Windows before hitting effect_update_depth_exceeded. Cache previously-emitted value, skip select_cb when current === prev. First mount still fires the cb (prev starts undefined), genuine user changes still fire it (different reference), but the self-triggered re-notification stops at the equality check. release: 4.9.3-beta.6 * release: 4.9.3 Fix Windows Settings tab freeze (#686 follow-up). Two stacked bugs from 4.9.2: 1. reactive_settings.current getter mutated its own $state variable when read before init — Svelte 5 raised effect_update_depth_exceeded as a derivation self-mutating its own dependency. Initialise eagerly with DEFAULT_SETTINGS so the getter is a pure read. 2. ShowAttributesSettingItem, EdgeSortIdSettingItem, and FieldGroupLabelsSettingItem each had a $effect that watched a $bindable prop and called select_cb(value) on every read. The callback writes plugin.settings.X = value; Svelte 5 deep proxies notify subscribers on every property assignment (even with the same reference), the prop re-notifies, the effect re-fires. Each iteration awaited saveData, so the loop ran ~2/sec for ~38s before hitting the update-depth limit. Cache previously-emitted value to skip self-triggered re-notifications. Also ships the gated perf-mark + effect-counter instrumentation used to diagnose this; active only at debug.level = DEBUG.
1 parent 12bd613 commit e673a5b

27 files changed

Lines changed: 364 additions & 107 deletions

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,43 @@ All notable changes to this project will be documented in this file. See [standa
44

55
## 4.X
66

7+
### [4.9.3](https://github.com/SkepticMystic/breadcrumbs/compare/4.9.2...4.9.3) (2026-05-12)
8+
9+
### Bug Fixes
10+
11+
* Fix Settings tab freezing Obsidian on Windows in 4.9.2 — the freeze had two stacked causes. (1) `reactive_settings.current` assigned to its own `$state` variable when read before init, which Svelte 5 detected as a derivation self-mutating its own dependency. (2) Three settings sub-components (`ShowAttributesSettingItem`, `EdgeSortIdSettingItem`, `FieldGroupLabelsSettingItem`) had a `$effect` that watched a `$bindable` prop and called `select_cb(value)` on every read; the callback writes back into `plugin.settings.X`, which re-notifies the same `$bindable` (Svelte 5 deep proxies fire on any property assignment, even with an identical reference), re-runs the effect, and so on until `effect_update_depth_exceeded` triggered after ~38s of awaited `saveData` calls. The store now initialises eagerly with `DEFAULT_SETTINGS` and the three sub-components cache the previously-emitted value to skip self-triggered re-notifications.
12+
* Add gated perf marks (`debug.level` set to `DEBUG`) around `SettingsTab.display`, each section mount, and reactive-loop counters around the suspect `$effect` bodies. Active only at `DEBUG` log level so default users see nothing.
13+
14+
### [4.9.3-beta.6](https://github.com/SkepticMystic/breadcrumbs/compare/4.9.3-beta.5...4.9.3-beta.6) (2026-05-12)
15+
16+
### Bug Fixes
17+
18+
* Fix the Windows Settings-tab freeze (`effect_update_depth_exceeded` + ~38s click hang). Three settings sub-components — `ShowAttributesSettingItem`, `EdgeSortIdSettingItem`, `FieldGroupLabelsSettingItem` — had a `$effect` that watched a `$bindable` prop and invoked `select_cb(value)` on every read. The callback writes back into `plugin.settings.X`, which re-notifies the same `$bindable` prop (Svelte 5 deep proxies fire on any property assignment, even with an identical reference), re-runs the effect, and so on until Svelte's update-depth limit triggers. Each iteration awaits an async `saveData` disk write, which is why the hang stretched to ~38s. Each effect now caches the previously-emitted value and skips the callback when the prop reads back as the same reference, breaking the cycle.
19+
20+
### [4.9.3-beta.5](https://github.com/SkepticMystic/breadcrumbs/compare/4.9.3-beta.4...4.9.3-beta.5) (2026-05-12)
21+
22+
### Diagnostics
23+
24+
* Rework `effect_counter` to track cumulative runs (no 250ms reset window) and log at thresholds 1/10/50/200/1000. Beta.4's window-based counter missed slow loops — the 38-second hang on Windows ran ~2 iterations/second, never hitting 50 in any 250ms window. Also extend coverage to remaining $effects in `TrailView.log`, `TrailView.depth`, `TreeView.depth`, `TreeView.root_open`, `LockViewButton`, `RenderMarkdown`.
25+
26+
### [4.9.3-beta.4](https://github.com/SkepticMystic/breadcrumbs/compare/4.9.3-beta.3...4.9.3-beta.4) (2026-05-12)
27+
28+
### Diagnostics
29+
30+
* Extend reactive-loop counters to the four settings sub-components most likely to fire on tab open (`MatrixFieldOrderSettingItem`, `FieldGroupLabelsSettingItem`, `ShowAttributesSettingItem`, `EdgeSortIdSettingItem`) plus `ShowAttributesSelectorMenu.strip`. Beta.3 timings showed `SettingsTab.display` completed in 27ms but the `effect_update_depth_exceeded` error still fired on the next microtask, so the looping `$effect` lives in a sub-component that beta.3 did not instrument.
31+
32+
### [4.9.3-beta.3](https://github.com/SkepticMystic/breadcrumbs/compare/4.9.3-beta.2...4.9.3-beta.3) (2026-05-12)
33+
34+
### Diagnostics
35+
36+
* Add gated perf marks and reactive-loop counters around the Settings tab (per-section timings, mount durations, `effect-storm` warnings on TrailView/Matrix/TreeView/TransitiveImpliedRelations/NestedEdgeList) to diagnose the remaining few-second Settings-open freeze on Windows. Active only when `debug.level` is set to `DEBUG`; no effect at default log level.
37+
38+
### [4.9.3-beta.2](https://github.com/SkepticMystic/breadcrumbs/compare/4.9.2...4.9.3-beta.2) (2026-05-12)
39+
40+
### Bug Fixes
41+
42+
* Fix settings tab freezing Obsidian on Windows in 4.9.2 — the `reactive_settings.current` getter assigned to its own `$state` variable when read before init, which Svelte 5 detected as a derivation self-mutating its own dependency and looped to `effect_update_depth_exceeded`. The store now initialises `_settings` eagerly with `DEFAULT_SETTINGS` so the getter is a pure read and `init()` simply swaps in the real settings.
43+
744
### [4.9.2](https://github.com/SkepticMystic/breadcrumbs/compare/4.9.1...4.9.2) (2026-05-12)
845

946
### Bug Fixes

manifest-beta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "breadcrumbs",
33
"name": "Breadcrumbs",
4-
"version": "4.9.2",
4+
"version": "4.9.3",
55
"minAppVersion": "1.12.0",
66
"description": "Add structured hierarchies to your notes",
77
"author": "SkepticMystic",

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "breadcrumbs",
33
"name": "Breadcrumbs",
4-
"version": "4.9.2",
4+
"version": "4.9.3",
55
"minAppVersion": "1.12.0",
66
"description": "Add structured hierarchies to your notes",
77
"author": "SkepticMystic",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "breadcrumbs",
3-
"version": "4.9.2",
3+
"version": "4.9.3",
44
"description": "Add typed-links to your Obsidian notes",
55
"main": "main.js",
66
"scripts": {

src/components/NestedEdgeList.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
} from "wasm/pkg/breadcrumbs_graph_wasm";
1010
import type { EdgeAttribute } from "src/graph/utils";
1111
import ObsidianLink from "./ObsidianLink.svelte";
12+
import { effect_counter } from "src/utils/perf";
1213
1314
interface Props {
1415
plugin: BreadcrumbsPlugin;
@@ -30,7 +31,9 @@
3031
3132
let opens = $state<boolean[]>([]);
3233
34+
const tick_nested_opens = effect_counter("NestedEdgeList.opens_sync");
3335
$effect(() => {
36+
tick_nested_opens();
3437
const n = items.length;
3538
if (opens.length !== n) {
3639
opens = Array(n).fill(true);

src/components/button/LockViewButton.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { LockKeyholeIcon, LockKeyholeOpenIcon } from "lucide-svelte";
33
import { ICON_SIZE } from "src/const";
44
import { log } from "src/logger";
5+
import { effect_counter } from "src/utils/perf";
56
67
interface Props {
78
cls?: string;
@@ -12,7 +13,9 @@
1213
1314
let { cls = "", lock_view = $bindable(), lock_path = $bindable(), active_path }: Props = $props();
1415
16+
const tick_lock = effect_counter("LockViewButton");
1517
$effect(() => {
18+
tick_lock();
1619
if (!lock_view && active_path) {
1720
lock_path = active_path;
1821
}

src/components/obsidian/RenderMarkdown.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import type BreadcrumbsPlugin from "src/main";
77
import { active_file_store } from "src/stores/active_file";
88
import { onDestroy } from "svelte";
9+
import { effect_counter } from "src/utils/perf";
910
1011
interface Props {
1112
cls?: string;
@@ -51,7 +52,9 @@
5152
);
5253
}
5354
55+
const tick_render_md = effect_counter("RenderMarkdown");
5456
$effect(() => {
57+
tick_render_md();
5558
void render(markdown);
5659
});
5760

src/components/page_views/TrailView.svelte

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import { untrack } from "svelte";
1313
import { log } from "src/logger";
1414
import { json_clone } from "src/utils/json_clone";
15+
import { effect_counter } from "src/utils/perf";
1516
1617
interface Props {
1718
plugin: BreadcrumbsPlugin;
@@ -37,13 +38,17 @@
3738
}
3839
});
3940
41+
const tick_trail_log = effect_counter("TrailView.log");
4042
$effect(() => {
43+
tick_trail_log();
4144
log.debug("Rendering Trail page view for file:", file_path);
4245
});
4346
4447
let is_initial_mount = true;
48+
const tick_trail_writeback = effect_counter("TrailView.writeback");
4549
4650
$effect(() => {
51+
tick_trail_writeback();
4752
const trail_snapshot = $state.snapshot(settings);
4853
untrack(() => {
4954
plugin.settings.views.page.trail = trail_snapshot;
@@ -87,7 +92,9 @@
8792
Math.max(0, data.selected_paths?.max_depth() ?? 0),
8893
);
8994
let depth = $state(0);
95+
const tick_trail_depth = effect_counter("TrailView.depth");
9096
$effect(() => {
97+
tick_trail_depth();
9198
depth = Math.min(MAX_DEPTH, settings.default_depth);
9299
});
93100

src/components/selector/ShowAttributesSelectorMenu.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { ICON_SIZE } from "src/const";
44
import type { EdgeAttribute } from "src/graph/utils";
55
import { ShowAttributesSelectorMenu } from "src/menus/ShowAttributesMenu";
6+
import { effect_counter } from "src/utils/perf";
67
78
interface Props {
89
show_attributes: EdgeAttribute[];
@@ -20,7 +21,9 @@
2021
2122
// Remove any excluded items in the initial value
2223
// This makes it cleaner to pass in EDGE_ATTRIBUTES as the starter, then immediately exclude some
24+
const tick_strip = effect_counter("ShowAttributesSelectorMenu.strip");
2325
$effect.pre(() => {
26+
tick_strip();
2427
if (did_strip_excluded) return;
2528
const ex = exclude_attributes;
2629
if (!ex?.length) return;

src/components/settings/EdgeSortIdSettingItem.svelte

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script lang="ts">
22
import { type EdgeSortId } from "src/const/graph";
3+
import { effect_counter } from "src/utils/perf";
34
import EdgeSortIdSelector from "../selector/EdgeSortIdSelector.svelte";
45
import SettingItem from "./SettingItem.svelte";
56
@@ -10,7 +11,12 @@
1011
1112
let { edge_sort_id = $bindable(), select_cb = () => {} }: Props = $props();
1213
14+
const tick_esi = effect_counter("EdgeSortIdSettingItem");
15+
let prev_esi: EdgeSortId | undefined;
1316
$effect(() => {
17+
tick_esi();
18+
if (edge_sort_id === prev_esi) return;
19+
prev_esi = edge_sort_id;
1420
if (edge_sort_id) {
1521
select_cb(edge_sort_id);
1622
}

0 commit comments

Comments
 (0)