Skip to content

fix: better $inspect.trace() output #16131

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/strong-clouds-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: better `$inspect.trace()` output
66 changes: 19 additions & 47 deletions packages/svelte/src/internal/client/dev/tracing.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,28 @@ import { snapshot } from '../../shared/clone.js';
import { define_property } from '../../shared/utils.js';
import { DERIVED, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { effect_tracking } from '../reactivity/effects.js';
import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js';
import { active_reaction } from '../runtime.js';

/** @type { any } */
/**
* @typedef {{
* traces: Error[];
* }} TraceEntry
*/

/** @type {{ reaction: Reaction | null, entries: Map<Value, TraceEntry> } | null} */
export let tracing_expressions = null;

/**
* @param { Value } signal
* @param { { read: Error[] } } [entry]
* @param {Value} signal
* @param {TraceEntry} [entry]
*/
function log_entry(signal, entry) {
const debug = signal.debug;
const value = signal.trace_need_increase ? signal.trace_v : signal.v;
const value = signal.v;

if (value === UNINITIALIZED) {
return;
}

if (debug) {
var previous_captured_signals = captured_signals;
var captured = new Set();
set_captured_signals(captured);
try {
untrack(() => {
debug();
});
} finally {
set_captured_signals(previous_captured_signals);
}
if (captured.size > 0) {
for (const dep of captured) {
log_entry(dep);
}
return;
}
}

const type = (signal.f & DERIVED) !== 0 ? '$derived' : '$state';
const current_reaction = /** @type {Reaction} */ (active_reaction);
const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0;
Expand Down Expand Up @@ -69,17 +55,15 @@ function log_entry(signal, entry) {
console.log(signal.created);
}

if (signal.updated) {
if (dirty && signal.updated) {
// eslint-disable-next-line no-console
console.log(signal.updated);
}

const read = entry?.read;

if (read && read.length > 0) {
for (var stack of read) {
if (entry) {
for (var trace of entry.traces) {
// eslint-disable-next-line no-console
console.log(stack);
console.log(trace);
}
}

Expand All @@ -94,6 +78,7 @@ function log_entry(signal, entry) {
*/
export function trace(label, fn) {
var previously_tracing_expressions = tracing_expressions;

try {
tracing_expressions = { entries: new Map(), reaction: active_reaction };

Expand All @@ -111,29 +96,16 @@ export function trace(label, fn) {
// eslint-disable-next-line no-console
console.group(`${label()} %c(${time}ms)`, 'color: grey');

var entries = tracing_expressions.entries;
for (const [signal, traces] of tracing_expressions.entries) {
log_entry(signal, traces);
}

tracing_expressions = null;

for (const [signal, entry] of entries) {
log_entry(signal, entry);
}
// eslint-disable-next-line no-console
console.groupEnd();
}

if (previously_tracing_expressions !== null && tracing_expressions !== null) {
for (const [signal, entry] of tracing_expressions.entries) {
var prev_entry = previously_tracing_expressions.get(signal);

if (prev_entry === undefined) {
previously_tracing_expressions.set(signal, entry);
} else {
prev_entry.read.push(...entry.read);
}
}
}

return value;
} finally {
tracing_expressions = previously_tracing_expressions;
Expand Down
10 changes: 0 additions & 10 deletions packages/svelte/src/internal/client/dom/blocks/each.js
Original file line number Diff line number Diff line change
Expand Up @@ -524,16 +524,6 @@ function create_item(
var v = reactive ? (mutable ? mutable_source(value) : source(value)) : value;
var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index);

if (DEV && reactive) {
// For tracing purposes, we need to link the source signal we create with the
// collection + index so that tracing works as intended
/** @type {Value} */ (v).debug = () => {
var collection_index = typeof i === 'number' ? index : i.v;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
get_collection()[collection_index];
};
}

/** @type {EachItem} */
var item = {
i,
Expand Down
11 changes: 7 additions & 4 deletions packages/svelte/src/internal/client/reactivity/sources.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';

export let inspect_effects = new Set();

/** @type {Map<Source, any>} */
export const old_values = new Map();

/**
Expand Down Expand Up @@ -66,7 +68,8 @@ export function source(v, stack) {

if (DEV && tracing_mode_flag) {
signal.created = stack ?? get_stack('CreatedAt');
signal.debug = null;
signal.updated = null;
signal.set_during_effect = false;
}

return signal;
Expand Down Expand Up @@ -168,9 +171,9 @@ export function internal_set(source, value) {

if (DEV && tracing_mode_flag) {
source.updated = get_stack('UpdatedAt');
if (active_effect != null) {
source.trace_need_increase = true;
source.trace_v ??= old_value;

if (active_effect !== null) {
source.set_during_effect = true;
}
}

Expand Down
16 changes: 11 additions & 5 deletions packages/svelte/src/internal/client/reactivity/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@ export interface Value<V = unknown> extends Signal {
rv: number;
/** The latest value for this signal */
v: V;
/** Dev only */

// dev-only
/** A label (e.g. the `foo` in `let foo = $state(...)`) used for `$inspect.trace()` */
label?: string;
/** An error with a stack trace showing when the source was created */
created?: Error | null;
/** An error with a stack trace showing when the source was last updated */
updated?: Error | null;
trace_need_increase?: boolean;
trace_v?: V;
label?: string;
debug?: null | (() => void);
/**
* Whether or not the source was set while running an effect — if so, we need to
* increment the write version so that it shows up as dirty when the effect re-runs
*/
set_during_effect?: boolean;
}

export interface Reaction extends Signal {
Expand Down
40 changes: 17 additions & 23 deletions packages/svelte/src/internal/client/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,6 @@ export let skip_reaction = false;
/** @type {Set<Value> | null} */
export let captured_signals = null;

/** @param {Set<Value> | null} value */
export function set_captured_signals(value) {
captured_signals = value;
}

export function increment_write_version() {
return ++write_version;
}
Expand Down Expand Up @@ -447,19 +442,13 @@ export function update_effect(effect) {
effect.teardown = typeof teardown === 'function' ? teardown : null;
effect.wv = write_version;

var deps = effect.deps;

// In DEV, we need to handle a case where $inspect.trace() might
// incorrectly state a source dependency has not changed when it has.
// That's beacuse that source was changed by the same effect, causing
// the versions to match. We can avoid this by incrementing the version
if (DEV && tracing_mode_flag && (effect.f & DIRTY) !== 0 && deps !== null) {
for (let i = 0; i < deps.length; i++) {
var dep = deps[i];
if (dep.trace_need_increase) {
// In DEV, increment versions of any sources that were written to during the effect,
// so that they are correctly marked as dirty when the effect re-runs
if (DEV && tracing_mode_flag && (effect.f & DIRTY) !== 0 && effect.deps !== null) {
for (var dep of effect.deps) {
if (dep.set_during_effect) {
dep.wv = increment_write_version();
dep.trace_need_increase = undefined;
dep.trace_v = undefined;
dep.set_during_effect = false;
}
}
}
Expand Down Expand Up @@ -779,18 +768,23 @@ export function get(signal) {
active_reaction !== null &&
tracing_expressions.reaction === active_reaction
) {
// Used when mapping state between special blocks like `each`
if (signal.debug) {
signal.debug();
} else if (signal.created) {
var trace = get_stack('TracedAt');

if (trace) {
var entry = tracing_expressions.entries.get(signal);

if (entry === undefined) {
entry = { read: [] };
entry = { traces: [] };
tracing_expressions.entries.set(signal, entry);
}

entry.read.push(get_stack('TracedAt'));
var last = entry.traces[entry.traces.length - 1];

// traces can be duplicated, e.g. by `snapshot` invoking both
// both `getOwnPropertyDescriptor` and `get` traps at once
if (trace.stack !== last?.stack) {
entry.traces.push(trace);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ export default test({
{ log: true },
{ log: '$state', highlighted: true },
{ log: 'count', highlighted: false },
{ log: 1 },
{ log: 2 },
{ log: 'effect' },
{ log: '$state', highlighted: false },
{ log: 'checked', highlighted: false },
{ log: true },
{ log: '$state', highlighted: true },
{ log: 'count', highlighted: false },
{ log: 2 },
{ log: 3 },
{ log: 'effect' },
{ log: '$state', highlighted: false },
{ log: 'checked', highlighted: false },
Expand Down