Skip to content

Commit 16117d9

Browse files
authored
perf: skip repeatedly traversing the same derived (#17016)
* perf: skip repeatedly traversing the same derived We introduce a new flag which marks a derived during the mark_reaction phase and lift it during the execution phase. * fix This introduces a new flag which marks a derived as visited during the mark_reaction phase and lifts it during the dirty-check/execution phase (both are necessary because not all deriveds are necessarily executed, but they're always checked). We could've used a Set on mark_reactions which would've been simpler to reason about, alas it was performing consistenly worse on the kairo_mux_unowned / kairo_mux_owned benchmarks (it kinda makes sense generally, creating and filling a set is more expensive than juggling flags). Closes #16658, which showcases are particularly gnarly signal graph where many deriveds point to the same deriveds, where we would be traversing the same reactions over and over if we didn't do this. I also added a similar measure (this time a set/map is unavoidable/makes more sense) for mark_effects, hopefully this helps for #16990
1 parent a28904c commit 16117d9

File tree

6 files changed

+60
-12
lines changed

6 files changed

+60
-12
lines changed

.changeset/slimy-gifts-cough.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+
perf: skip repeatedly traversing the same derived

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1+
// General flags
12
export const DERIVED = 1 << 1;
23
export const EFFECT = 1 << 2;
34
export const RENDER_EFFECT = 1 << 3;
45
export const BLOCK_EFFECT = 1 << 4;
56
export const BRANCH_EFFECT = 1 << 5;
67
export const ROOT_EFFECT = 1 << 6;
78
export const BOUNDARY_EFFECT = 1 << 7;
8-
export const UNOWNED = 1 << 8;
9-
export const DISCONNECTED = 1 << 9;
109
export const CLEAN = 1 << 10;
1110
export const DIRTY = 1 << 11;
1211
export const MAYBE_DIRTY = 1 << 12;
1312
export const INERT = 1 << 13;
1413
export const DESTROYED = 1 << 14;
14+
15+
// Flags exclusive to effects
1516
export const EFFECT_RAN = 1 << 15;
1617
/** 'Transparent' effects do not create a transition boundary */
1718
export const EFFECT_TRANSPARENT = 1 << 16;
@@ -20,6 +21,16 @@ export const HEAD_EFFECT = 1 << 18;
2021
export const EFFECT_PRESERVED = 1 << 19;
2122
export const USER_EFFECT = 1 << 20;
2223

24+
// Flags exclusive to deriveds
25+
export const UNOWNED = 1 << 8;
26+
export const DISCONNECTED = 1 << 9;
27+
/**
28+
* Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase.
29+
* Will be lifted during execution of the derived and during checking its dirty state (both are necessary
30+
* because a derived might be checked but not executed).
31+
*/
32+
export const WAS_MARKED = 1 << 15;
33+
2334
// Flags used for async
2435
export const REACTION_IS_UPDATING = 1 << 21;
2536
export const ASYNC = 1 << 22;

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

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -377,8 +377,12 @@ export class Batch {
377377
// Re-run async/block effects that depend on distinct values changed in both batches
378378
const others = [...batch.current.keys()].filter((s) => !this.current.has(s));
379379
if (others.length > 0) {
380+
/** @type {Set<Value>} */
381+
const marked = new Set();
382+
/** @type {Map<Reaction, boolean>} */
383+
const checked = new Map();
380384
for (const source of sources) {
381-
mark_effects(source, others);
385+
mark_effects(source, others, marked, checked);
382386
}
383387

384388
if (queued_root_effects.length > 0) {
@@ -688,15 +692,24 @@ function flush_queued_effects(effects) {
688692
* these effects can re-run after another batch has been committed
689693
* @param {Value} value
690694
* @param {Source[]} sources
695+
* @param {Set<Value>} marked
696+
* @param {Map<Reaction, boolean>} checked
691697
*/
692-
function mark_effects(value, sources) {
698+
function mark_effects(value, sources, marked, checked) {
699+
if (marked.has(value)) return;
700+
marked.add(value);
701+
693702
if (value.reactions !== null) {
694703
for (const reaction of value.reactions) {
695704
const flags = reaction.f;
696705

697706
if ((flags & DERIVED) !== 0) {
698-
mark_effects(/** @type {Derived} */ (reaction), sources);
699-
} else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0 && depends_on(reaction, sources)) {
707+
mark_effects(/** @type {Derived} */ (reaction), sources, marked, checked);
708+
} else if (
709+
(flags & (ASYNC | BLOCK_EFFECT)) !== 0 &&
710+
(flags & DIRTY) === 0 && // we may have scheduled this one already
711+
depends_on(reaction, sources, checked)
712+
) {
700713
set_signal_status(reaction, DIRTY);
701714
schedule_effect(/** @type {Effect} */ (reaction));
702715
}
@@ -707,20 +720,27 @@ function mark_effects(value, sources) {
707720
/**
708721
* @param {Reaction} reaction
709722
* @param {Source[]} sources
723+
* @param {Map<Reaction, boolean>} checked
710724
*/
711-
function depends_on(reaction, sources) {
725+
function depends_on(reaction, sources, checked) {
726+
const depends = checked.get(reaction);
727+
if (depends !== undefined) return depends;
728+
712729
if (reaction.deps !== null) {
713730
for (const dep of reaction.deps) {
714731
if (sources.includes(dep)) {
715732
return true;
716733
}
717734

718-
if ((dep.f & DERIVED) !== 0 && depends_on(/** @type {Derived} */ (dep), sources)) {
735+
if ((dep.f & DERIVED) !== 0 && depends_on(/** @type {Derived} */ (dep), sources, checked)) {
736+
checked.set(/** @type {Derived} */ (dep), true);
719737
return true;
720738
}
721739
}
722740
}
723741

742+
checked.set(reaction, false);
743+
724744
return false;
725745
}
726746

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
MAYBE_DIRTY,
1111
STALE_REACTION,
1212
UNOWNED,
13-
ASYNC
13+
ASYNC,
14+
WAS_MARKED
1415
} from '#client/constants';
1516
import {
1617
active_reaction,
@@ -326,6 +327,7 @@ export function execute_derived(derived) {
326327

327328
stack.push(derived);
328329

330+
derived.f &= ~WAS_MARKED;
329331
destroy_derived_effects(derived);
330332
value = update_reaction(derived);
331333
} finally {
@@ -335,6 +337,7 @@ export function execute_derived(derived) {
335337
}
336338
} else {
337339
try {
340+
derived.f &= ~WAS_MARKED;
338341
destroy_derived_effects(derived);
339342
value = update_reaction(derived);
340343
} finally {

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import {
2727
MAYBE_DIRTY,
2828
BLOCK_EFFECT,
2929
ROOT_EFFECT,
30-
ASYNC
30+
ASYNC,
31+
WAS_MARKED
3132
} from '#client/constants';
3233
import * as e from '../errors.js';
3334
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@@ -332,7 +333,10 @@ function mark_reactions(signal, status) {
332333
}
333334

334335
if ((flags & DERIVED) !== 0) {
335-
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
336+
if ((flags & WAS_MARKED) === 0) {
337+
reaction.f |= WAS_MARKED;
338+
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
339+
}
336340
} else if (not_dirty) {
337341
if ((flags & BLOCK_EFFECT) !== 0) {
338342
if (eager_block_effects !== null) {

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
DISCONNECTED,
2121
REACTION_IS_UPDATING,
2222
STALE_REACTION,
23-
ERROR_VALUE
23+
ERROR_VALUE,
24+
WAS_MARKED
2425
} from './constants.js';
2526
import { old_values } from './reactivity/sources.js';
2627
import {
@@ -161,6 +162,10 @@ export function is_dirty(reaction) {
161162
var dependencies = reaction.deps;
162163
var is_unowned = (flags & UNOWNED) !== 0;
163164

165+
if (flags & DERIVED) {
166+
reaction.f &= ~WAS_MARKED;
167+
}
168+
164169
if (dependencies !== null) {
165170
var i;
166171
var dependency;

0 commit comments

Comments
 (0)