-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
fix: cleanup old batches, run their still-current commits #16621
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
base: main
Are you sure you want to change the base?
Changes from 4 commits
e14561c
0f4d0b1
0ee1d56
5d6b755
bde51cd
626b9fc
3ee6ea4
28be8c9
dce2563
18a790c
075a8b9
b88b4e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'svelte': patch | ||
--- | ||
|
||
fix: associate batch with boundary |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'svelte': patch | ||
--- | ||
|
||
fix: ensure obsolete batches are removed and its necessary dom changes committed |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -95,10 +95,12 @@ export class Batch { | |
|
||
/** | ||
* When the batch is committed (and the DOM is updated), we need to remove old branches | ||
* and append new ones by calling the functions added inside (if/each/key/etc) blocks | ||
* @type {Set<() => void>} | ||
* and append new ones by calling the functions added inside (if/each/key/etc) blocks. | ||
* Key is a function that returns the block effect because #callbacks will be called before | ||
* the block effect reference exists, so we need to capture it in a closure. | ||
* @type {Map<() => Effect, () => void>} | ||
*/ | ||
#callbacks = new Set(); | ||
#callbacks = new Map(); | ||
|
||
/** | ||
* The number of async effects that are currently in flight | ||
|
@@ -112,12 +114,6 @@ export class Batch { | |
*/ | ||
#deferred = null; | ||
|
||
/** | ||
* True if an async effect inside this batch resolved and | ||
* its parent branch was already deleted | ||
*/ | ||
#neutered = false; | ||
|
||
/** | ||
* Async effects (created inside `async_derived`) encountered during processing. | ||
* These run after the rest of the batch has updated, since they should | ||
|
@@ -184,6 +180,14 @@ export class Batch { | |
/** @type {Map<Source, { v: unknown, wv: number }> | null} */ | ||
var current_values = null; | ||
|
||
/** | ||
* A batch is superseeded if all of its sources are also in the current batch. | ||
* If the current batch commits, we don't need the old batch anymore. | ||
* This also prevents memory leaks since the old batch will never be committed. | ||
* @type {Batch[]} | ||
*/ | ||
var superseeded_batches = []; | ||
|
||
// if there are multiple batches, we are 'time travelling' — | ||
// we need to undo the changes belonging to any batch | ||
// other than the current one | ||
|
@@ -196,15 +200,25 @@ export class Batch { | |
source.v = current; | ||
} | ||
|
||
let is_prior_batch = true; | ||
|
||
for (const batch of batches) { | ||
if (batch === this) continue; | ||
if (batch === this) { | ||
is_prior_batch = false; | ||
continue; | ||
} | ||
|
||
let superseeded = is_prior_batch; | ||
|
||
for (const [source, previous] of batch.#previous) { | ||
if (!current_values.has(source)) { | ||
superseeded = false; | ||
current_values.set(source, { v: source.v, wv: source.wv }); | ||
source.v = previous; | ||
} | ||
} | ||
|
||
if (superseeded) superseeded_batches.push(batch); | ||
} | ||
} | ||
|
||
|
@@ -215,6 +229,24 @@ export class Batch { | |
// if we didn't start any new async work, and no async work | ||
// is outstanding from a previous flush, commit | ||
if (this.#async_effects.length === 0 && this.#pending === 0) { | ||
if (superseeded_batches.length > 0) { | ||
const own = [...this.#callbacks.keys()].map((c) => c()); | ||
// A superseeded batch could have callbacks for e.g. destroying if blocks | ||
// that are not part of the current batch because it already happened in the prior one, | ||
// and the corresponding block effect therefore returning early because nothing was changed from its | ||
// point of view, therefore not adding a callback to the current batch, so we gotta call them here. | ||
// We do it from newest to oldest to ensure the correct callback is applied. | ||
for (const batch of superseeded_batches.reverse()) { | ||
for (const [effect, cb] of batch.#callbacks) { | ||
if (!own.includes(effect())) { | ||
cb(); | ||
own.push(effect()); | ||
} | ||
} | ||
batch.remove(); | ||
} | ||
} | ||
|
||
this.#commit(); | ||
|
||
var render_effects = this.#render_effects; | ||
|
@@ -372,8 +404,14 @@ export class Batch { | |
} | ||
} | ||
|
||
neuter() { | ||
this.#neutered = true; | ||
remove() { | ||
this.#callbacks.clear(); | ||
this.#maybe_dirty_effects = | ||
this.#dirty_effects = | ||
this.#boundary_async_effects = | ||
this.#async_effects = | ||
[]; | ||
Comment on lines
+408
to
+413
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't this stuff get GC'd along with the rest of the batch? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as said in the meeting, my hunch is that it's possible for obsolete batches to still be called at some point and then we don't want to rerun anything for them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added a test that would fail without this logic - outdated batches could resolve when they're already destroyed, and could then wrongfully run stuff. Also helps free up memory for never-resolving promises. |
||
batches.delete(this); | ||
} | ||
|
||
flush() { | ||
|
@@ -400,10 +438,8 @@ export class Batch { | |
* Append and remove branches to/from the DOM | ||
*/ | ||
#commit() { | ||
if (!this.#neutered) { | ||
for (const fn of this.#callbacks) { | ||
fn(); | ||
} | ||
for (const fn of this.#callbacks.values()) { | ||
fn(); | ||
} | ||
|
||
this.#callbacks.clear(); | ||
|
@@ -436,9 +472,12 @@ export class Batch { | |
} | ||
} | ||
|
||
/** @param {() => void} fn */ | ||
add_callback(fn) { | ||
this.#callbacks.add(fn); | ||
/** | ||
* @param {() => Effect} effect | ||
* @param {() => void} fn | ||
*/ | ||
add_callback(effect, fn) { | ||
this.#callbacks.set(effect, fn); | ||
} | ||
|
||
settled() { | ||
|
@@ -664,7 +703,7 @@ export function schedule_effect(signal) { | |
|
||
export function suspend() { | ||
var boundary = get_pending_boundary(); | ||
var batch = /** @type {Batch} */ (current_batch); | ||
var batch = boundary.get_batch(); | ||
var pending = boundary.pending; | ||
|
||
boundary.update_pending_count(1); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<script lang="ts"> | ||
import { resolve } from './main.svelte'; | ||
|
||
const bar = await new Promise((r) => resolve.push(() => r('bar'))); | ||
</script> | ||
|
||
<p>bar: {bar}</p> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<script lang="ts"> | ||
import { resolve } from './main.svelte'; | ||
import Bar from './Bar.svelte'; | ||
|
||
const foo = await new Promise((r) => resolve.push(() => r('foo'))); | ||
</script> | ||
|
||
<p>foo: {foo}</p> | ||
|
||
<Bar/> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { tick } from 'svelte'; | ||
import { test } from '../../test'; | ||
|
||
export default test({ | ||
async test({ assert, target }) { | ||
const [show, resolve] = target.querySelectorAll('button'); | ||
|
||
show.click(); | ||
await tick(); | ||
assert.htmlEqual( | ||
target.innerHTML, | ||
` | ||
<button>show</button> | ||
<button>resolve</button> | ||
<p>pending...</p> | ||
` | ||
); | ||
|
||
resolve.click(); | ||
await tick(); | ||
assert.htmlEqual( | ||
target.innerHTML, | ||
` | ||
<button>show</button> | ||
<button>resolve</button> | ||
<p>pending...</p> | ||
` | ||
); | ||
|
||
resolve.click(); | ||
await tick(); | ||
assert.htmlEqual( | ||
target.innerHTML, | ||
` | ||
<button>show</button> | ||
<button>resolve</button> | ||
<p>foo: foo</p> | ||
<p>bar: bar</p> | ||
` | ||
); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is the only thing in this PR that gives me a bit pause, basically that we have no definite way to know which batch is the latest one for which boundary, this "only" feels like a good enough approximation.