Skip to content

Commit 438349e

Browse files
authored
fix: better $inspect.trace() output (#16131)
* remove code thatcan't be reached and would error if it could * tidy up types, fix duplication * this seems needless * better naming * tidy up * reorder * this code doesn't appear to do anything useful, and no tests fail without it * unused * WIP * revert part of #14811. it makes no sense to show the initial value, it just makes things inconsistent with deriveds. personally i find it more confusing anyway * explanatory comment on both sides * make things a bit more self-explanatory * simplify * missing type * only log UpdatedAt for dirty signals * changeset * lint * Revert "unused" This reverts commit a95b625. * complete revert * ok it works now
1 parent 292af8d commit 438349e

File tree

11 files changed

+143
-83
lines changed

11 files changed

+143
-83
lines changed

.changeset/strong-clouds-switch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: better `$inspect.trace()` output

packages/svelte/src/internal/client/dev/tracing.js

Lines changed: 26 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,26 @@ import { DERIVED, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
66
import { effect_tracking } from '../reactivity/effects.js';
77
import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js';
88

9-
/** @type { any } */
9+
/**
10+
* @typedef {{
11+
* traces: Error[];
12+
* }} TraceEntry
13+
*/
14+
15+
/** @type {{ reaction: Reaction | null, entries: Map<Value, TraceEntry> } | null} */
1016
export let tracing_expressions = null;
1117

1218
/**
13-
* @param { Value } signal
14-
* @param { { read: Error[] } } [entry]
19+
* @param {Value} signal
20+
* @param {TraceEntry} [entry]
1521
*/
1622
function log_entry(signal, entry) {
17-
const debug = signal.debug;
18-
const value = signal.trace_need_increase ? signal.trace_v : signal.v;
23+
const value = signal.v;
1924

2025
if (value === UNINITIALIZED) {
2126
return;
2227
}
2328

24-
if (debug) {
25-
var previous_captured_signals = captured_signals;
26-
var captured = new Set();
27-
set_captured_signals(captured);
28-
try {
29-
untrack(() => {
30-
debug();
31-
});
32-
} finally {
33-
set_captured_signals(previous_captured_signals);
34-
}
35-
if (captured.size > 0) {
36-
for (const dep of captured) {
37-
log_entry(dep);
38-
}
39-
return;
40-
}
41-
}
42-
4329
const type = (signal.f & DERIVED) !== 0 ? '$derived' : '$state';
4430
const current_reaction = /** @type {Reaction} */ (active_reaction);
4531
const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0;
@@ -69,17 +55,15 @@ function log_entry(signal, entry) {
6955
console.log(signal.created);
7056
}
7157

72-
if (signal.updated) {
58+
if (dirty && signal.updated) {
7359
// eslint-disable-next-line no-console
7460
console.log(signal.updated);
7561
}
7662

77-
const read = entry?.read;
78-
79-
if (read && read.length > 0) {
80-
for (var stack of read) {
63+
if (entry) {
64+
for (var trace of entry.traces) {
8165
// eslint-disable-next-line no-console
82-
console.log(stack);
66+
console.log(trace);
8367
}
8468
}
8569

@@ -94,46 +78,40 @@ function log_entry(signal, entry) {
9478
*/
9579
export function trace(label, fn) {
9680
var previously_tracing_expressions = tracing_expressions;
81+
9782
try {
9883
tracing_expressions = { entries: new Map(), reaction: active_reaction };
9984

10085
var start = performance.now();
10186
var value = fn();
10287
var time = (performance.now() - start).toFixed(2);
10388

89+
var prefix = untrack(label);
90+
10491
if (!effect_tracking()) {
10592
// eslint-disable-next-line no-console
106-
console.log(`${label()} %cran outside of an effect (${time}ms)`, 'color: grey');
93+
console.log(`${prefix} %cran outside of an effect (${time}ms)`, 'color: grey');
10794
} else if (tracing_expressions.entries.size === 0) {
10895
// eslint-disable-next-line no-console
109-
console.log(`${label()} %cno reactive dependencies (${time}ms)`, 'color: grey');
96+
console.log(`${prefix} %cno reactive dependencies (${time}ms)`, 'color: grey');
11097
} else {
11198
// eslint-disable-next-line no-console
112-
console.group(`${label()} %c(${time}ms)`, 'color: grey');
99+
console.group(`${prefix} %c(${time}ms)`, 'color: grey');
113100

114101
var entries = tracing_expressions.entries;
115102

103+
untrack(() => {
104+
for (const [signal, traces] of entries) {
105+
log_entry(signal, traces);
106+
}
107+
});
108+
116109
tracing_expressions = null;
117110

118-
for (const [signal, entry] of entries) {
119-
log_entry(signal, entry);
120-
}
121111
// eslint-disable-next-line no-console
122112
console.groupEnd();
123113
}
124114

125-
if (previously_tracing_expressions !== null && tracing_expressions !== null) {
126-
for (const [signal, entry] of tracing_expressions.entries) {
127-
var prev_entry = previously_tracing_expressions.get(signal);
128-
129-
if (prev_entry === undefined) {
130-
previously_tracing_expressions.set(signal, entry);
131-
} else {
132-
prev_entry.read.push(...entry.read);
133-
}
134-
}
135-
}
136-
137115
return value;
138116
} finally {
139117
tracing_expressions = previously_tracing_expressions;

packages/svelte/src/internal/client/dom/blocks/each.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ function create_item(
527527
if (DEV && reactive) {
528528
// For tracing purposes, we need to link the source signal we create with the
529529
// collection + index so that tracing works as intended
530-
/** @type {Value} */ (v).debug = () => {
530+
/** @type {Value} */ (v).trace = () => {
531531
var collection_index = typeof i === 'number' ? index : i.v;
532532
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
533533
get_collection()[collection_index];

packages/svelte/src/internal/client/reactivity/sources.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import { proxy } from '../proxy.js';
3737
import { execute_derived } from './deriveds.js';
3838

3939
export let inspect_effects = new Set();
40+
41+
/** @type {Map<Source, any>} */
4042
export const old_values = new Map();
4143

4244
/**
@@ -66,7 +68,9 @@ export function source(v, stack) {
6668

6769
if (DEV && tracing_mode_flag) {
6870
signal.created = stack ?? get_stack('CreatedAt');
69-
signal.debug = null;
71+
signal.updated = null;
72+
signal.set_during_effect = false;
73+
signal.trace = null;
7074
}
7175

7276
return signal;
@@ -168,9 +172,9 @@ export function internal_set(source, value) {
168172

169173
if (DEV && tracing_mode_flag) {
170174
source.updated = get_stack('UpdatedAt');
171-
if (active_effect != null) {
172-
source.trace_need_increase = true;
173-
source.trace_v ??= old_value;
175+
176+
if (active_effect !== null) {
177+
source.set_during_effect = true;
174178
}
175179
}
176180

packages/svelte/src/internal/client/reactivity/types.d.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,21 @@ export interface Value<V = unknown> extends Signal {
1616
rv: number;
1717
/** The latest value for this signal */
1818
v: V;
19-
/** Dev only */
19+
20+
// dev-only
21+
/** A label (e.g. the `foo` in `let foo = $state(...)`) used for `$inspect.trace()` */
22+
label?: string;
23+
/** An error with a stack trace showing when the source was created */
2024
created?: Error | null;
25+
/** An error with a stack trace showing when the source was last updated */
2126
updated?: Error | null;
22-
trace_need_increase?: boolean;
23-
trace_v?: V;
24-
label?: string;
25-
debug?: null | (() => void);
27+
/**
28+
* Whether or not the source was set while running an effect — if so, we need to
29+
* increment the write version so that it shows up as dirty when the effect re-runs
30+
*/
31+
set_during_effect?: boolean;
32+
/** A function that retrieves the underlying source, used for each block item signals */
33+
trace?: null | (() => void);
2634
}
2735

2836
export interface Reaction extends Signal {

packages/svelte/src/internal/client/runtime.js

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
set_dev_current_component_function
4040
} from './context.js';
4141
import { handle_error, invoke_error_boundary } from './error-handling.js';
42+
import { snapshot } from '../shared/clone.js';
4243

4344
let is_flushing = false;
4445

@@ -447,19 +448,13 @@ export function update_effect(effect) {
447448
effect.teardown = typeof teardown === 'function' ? teardown : null;
448449
effect.wv = write_version;
449450

450-
var deps = effect.deps;
451-
452-
// In DEV, we need to handle a case where $inspect.trace() might
453-
// incorrectly state a source dependency has not changed when it has.
454-
// That's beacuse that source was changed by the same effect, causing
455-
// the versions to match. We can avoid this by incrementing the version
456-
if (DEV && tracing_mode_flag && (effect.f & DIRTY) !== 0 && deps !== null) {
457-
for (let i = 0; i < deps.length; i++) {
458-
var dep = deps[i];
459-
if (dep.trace_need_increase) {
451+
// In DEV, increment versions of any sources that were written to during the effect,
452+
// so that they are correctly marked as dirty when the effect re-runs
453+
if (DEV && tracing_mode_flag && (effect.f & DIRTY) !== 0 && effect.deps !== null) {
454+
for (var dep of effect.deps) {
455+
if (dep.set_during_effect) {
460456
dep.wv = increment_write_version();
461-
dep.trace_need_increase = undefined;
462-
dep.trace_v = undefined;
457+
dep.set_during_effect = false;
463458
}
464459
}
465460
}
@@ -775,22 +770,33 @@ export function get(signal) {
775770
if (
776771
DEV &&
777772
tracing_mode_flag &&
773+
!untracking &&
778774
tracing_expressions !== null &&
779775
active_reaction !== null &&
780776
tracing_expressions.reaction === active_reaction
781777
) {
782778
// Used when mapping state between special blocks like `each`
783-
if (signal.debug) {
784-
signal.debug();
785-
} else if (signal.created) {
786-
var entry = tracing_expressions.entries.get(signal);
787-
788-
if (entry === undefined) {
789-
entry = { read: [] };
790-
tracing_expressions.entries.set(signal, entry);
791-
}
779+
if (signal.trace) {
780+
signal.trace();
781+
} else {
782+
var trace = get_stack('TracedAt');
792783

793-
entry.read.push(get_stack('TracedAt'));
784+
if (trace) {
785+
var entry = tracing_expressions.entries.get(signal);
786+
787+
if (entry === undefined) {
788+
entry = { traces: [] };
789+
tracing_expressions.entries.set(signal, entry);
790+
}
791+
792+
var last = entry.traces[entry.traces.length - 1];
793+
794+
// traces can be duplicated, e.g. by `snapshot` invoking both
795+
// both `getOwnPropertyDescriptor` and `get` traps at once
796+
if (trace.stack !== last?.stack) {
797+
entry.traces.push(trace);
798+
}
799+
}
794800
}
795801
}
796802

packages/svelte/tests/runtime-legacy/shared.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { setImmediate } from 'node:timers/promises';
33
import { globSync } from 'tinyglobby';
44
import { createClassComponent } from 'svelte/legacy';
55
import { proxy } from 'svelte/internal/client';
6-
import { flushSync, hydrate, mount, unmount } from 'svelte';
6+
import { flushSync, hydrate, mount, unmount, untrack } from 'svelte';
77
import { render } from 'svelte/server';
88
import { afterAll, assert, beforeAll } from 'vitest';
99
import { compile_directory, fragments } from '../helpers.js';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
let { entry } = $props();
3+
4+
$effect(() => {
5+
$inspect.trace('effect');
6+
entry;
7+
});
8+
</script>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
import { normalise_trace_logs } from '../../../helpers.js';
4+
5+
export default test({
6+
compileOptions: {
7+
dev: true
8+
},
9+
10+
test({ assert, target, logs }) {
11+
assert.deepEqual(normalise_trace_logs(logs), [
12+
{ log: 'effect' },
13+
{ log: '$state', highlighted: true },
14+
{ log: 'array', highlighted: false },
15+
{ log: [{ id: 1, hi: true }] },
16+
// this _doesn't_ appear in the browser, but it does appear during tests
17+
// and i cannot for the life of me figure out why. this does at least
18+
// test that we don't log `array[0].id` etc
19+
{ log: '$state', highlighted: true },
20+
{ log: 'array[0]', highlighted: false },
21+
{ log: { id: 1, hi: true } }
22+
]);
23+
24+
logs.length = 0;
25+
26+
const button = target.querySelector('button');
27+
button?.click();
28+
flushSync();
29+
30+
assert.deepEqual(normalise_trace_logs(logs), [
31+
{ log: 'effect' },
32+
{ log: '$state', highlighted: true },
33+
{ log: 'array', highlighted: false },
34+
{ log: [{ id: 1, hi: false }] },
35+
{ log: '$state', highlighted: false },
36+
{ log: 'array[0]', highlighted: false },
37+
{ log: { id: 1, hi: false } }
38+
]);
39+
}
40+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
import Entry from './Entry.svelte';
3+
4+
let array = $state([{ id: 1, hi: true }]);
5+
</script>
6+
7+
<button onclick={() => array = [{ id: 1, hi: false}]}>update</button>
8+
9+
{#each array as entry (entry.id)}
10+
<Entry {entry} />
11+
{/each}

packages/svelte/tests/runtime-runes/samples/inspect-trace-reassignment/_config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ export default test({
3636
{ log: true },
3737
{ log: '$state', highlighted: true },
3838
{ log: 'count', highlighted: false },
39-
{ log: 1 },
39+
{ log: 2 },
4040
{ log: 'effect' },
4141
{ log: '$state', highlighted: false },
4242
{ log: 'checked', highlighted: false },
4343
{ log: true },
4444
{ log: '$state', highlighted: true },
4545
{ log: 'count', highlighted: false },
46-
{ log: 2 },
46+
{ log: 3 },
4747
{ log: 'effect' },
4848
{ log: '$state', highlighted: false },
4949
{ log: 'checked', highlighted: false },

0 commit comments

Comments
 (0)