Skip to content

Commit f3a3b37

Browse files
committed
fix memory leak + optimization signals
1 parent a3c460b commit f3a3b37

File tree

11 files changed

+422
-11
lines changed

11 files changed

+422
-11
lines changed

src/runtime/blockdom/block_compiler.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -568,9 +568,7 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
568568
}
569569
}
570570

571-
nodeInsertBefore.call(parent, el, afterNode);
572-
573-
// preparing all children
571+
// preparing all children (off-DOM, before inserting el into the live document)
574572
if (childN) {
575573
const children = this.children;
576574
for (let i = 0; i < childN; i++) {
@@ -584,6 +582,8 @@ function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
584582
}
585583
}
586584
}
585+
586+
nodeInsertBefore.call(parent, el, afterNode);
587587
this.el = el as HTMLElement;
588588
this.parentEl = parent;
589589

src/runtime/blockdom/list.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ class VList {
9090
beforeRemove.call(ch1[i]);
9191
}
9292
}
93-
9493
nodeSetTextContent.call(parent, "");
9594
nodeAppendChild.call(parent, _anchor);
9695
return;

src/runtime/component_node.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
ComputationAtom,
88
ComputationState,
99
createComputation,
10+
disposeComputation,
1011
getCurrentComputation,
1112
setComputation,
1213
} from "./reactivity/computations";
@@ -43,6 +44,7 @@ export class ComponentNode implements VNode<ComponentNode> {
4344
patched: LifecycleHook[] = [];
4445
willDestroy: LifecycleHook[] = [];
4546
signalComputation: ComputationAtom;
47+
computations: ComputationAtom[] = [];
4648

4749
pluginManager: PluginManager;
4850

@@ -204,6 +206,10 @@ export class ComponentNode implements VNode<ComponentNode> {
204206
this.app.handleError({ error: e, node: this });
205207
}
206208
}
209+
for (const computation of this.computations) {
210+
disposeComputation(computation);
211+
}
212+
disposeComputation(this.signalComputation);
207213
this.status = STATUS.DESTROYED;
208214
}
209215

src/runtime/plugin_manager.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { OwlError } from "../common/owl_error";
22
import { App } from "./app";
33
import { contextStack } from "./context";
4-
import { untrack } from "./reactivity/computations";
4+
import { ComputationAtom, disposeComputation, untrack } from "./reactivity/computations";
55
import { effect } from "./reactivity/effect";
66
import { Resource } from "./resource";
77
import { STATUS } from "./status";
@@ -38,6 +38,7 @@ export class PluginManager {
3838
app: App;
3939
config: Record<string, any>;
4040
onDestroyCb: Function[] = [];
41+
computations: ComputationAtom[] = [];
4142
plugins: Record<string, Plugin>;
4243
status: STATUS = STATUS.NEW;
4344

@@ -59,7 +60,9 @@ export class PluginManager {
5960
while (cbs.length) {
6061
cbs.pop()!();
6162
}
62-
63+
for (const computation of this.computations) {
64+
disposeComputation(computation);
65+
}
6366
this.status = STATUS.DESTROYED;
6467
}
6568

src/runtime/reactivity/computations.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,23 @@ export function removeSources(computation: ComputationAtom) {
127127
sources.clear();
128128
}
129129

130+
export function disposeComputation(computation: ComputationAtom) {
131+
for (const source of computation.sources) {
132+
source.observers.delete(computation);
133+
// Recursively dispose derived computations that lost all observers
134+
if (
135+
"compute" in source &&
136+
(source as ComputationAtom).isDerived &&
137+
source.observers.size === 0
138+
) {
139+
disposeComputation(source as ComputationAtom);
140+
}
141+
}
142+
computation.sources.clear();
143+
// Mark as stale so it recomputes correctly if ever re-used (shared computed case)
144+
computation.state = ComputationState.STALE;
145+
}
146+
130147
function markDownstream(computation: ComputationAtom) {
131148
const stack: ComputationAtom[] = [computation];
132149
let current: ComputationAtom | undefined;

src/runtime/reactivity/computed.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
updateComputation,
88
createComputation,
99
} from "./computations";
10+
import { contextStack } from "../context";
1011

1112
interface ComputedOptions<TWrite> {
1213
set?(value: TWrite): void;
@@ -17,8 +18,11 @@ export function computed<TRead, TWrite = TRead>(
1718
options: ComputedOptions<TWrite> = {}
1819
): ReactiveValue<TRead, TWrite> {
1920
const computation = createComputation(() => {
20-
onWriteAtom(computation);
21-
return getter();
21+
const newValue = getter();
22+
if (!Object.is(computation.value, newValue)) {
23+
onWriteAtom(computation);
24+
}
25+
return newValue;
2226
}, true);
2327

2428
function readComputed() {
@@ -31,5 +35,14 @@ export function computed<TRead, TWrite = TRead>(
3135
readComputed[atomSymbol] = computation;
3236
readComputed.set = options.set ?? (() => {});
3337

38+
const context = contextStack.at(-1);
39+
if (context) {
40+
if (context.type === "component") {
41+
context.node.computations.push(computation);
42+
} else if (context.type === "plugin") {
43+
context.manager.computations.push(computation);
44+
}
45+
}
46+
3447
return readComputed;
3548
}

src/runtime/rendering/fibers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { OwlError } from "../../common/owl_error";
22
import { BDom, mount } from "../blockdom";
33
import type { ComponentNode } from "../component_node";
4-
import { getCurrentComputation, setComputation } from "../reactivity/computations";
4+
import { getCurrentComputation, removeSources, setComputation } from "../reactivity/computations";
55
import { STATUS } from "../status";
66
import { fibersInError } from "./error_handling";
77

@@ -136,6 +136,7 @@ export class Fiber {
136136
if (root) {
137137
// todo: should use updateComputation somewhere else.
138138
const c = getCurrentComputation();
139+
removeSources(node.signalComputation);
139140
setComputation(node.signalComputation);
140141
try {
141142
(this.bdom as any) = true;

tests/components/__snapshots__/reactivity.test.ts.snap

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,120 @@ exports[`components and computed simple component and computed value 1`] = `
4040
}"
4141
`;
4242
43+
exports[`reactive cleanup on component destruction benchmark-like scenario: signal.Array rows with computed isSelected 1`] = `
44+
"function anonymous(app, bdom, helpers
45+
) {
46+
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
47+
let { prepareList, createComponent, withKey } = helpers;
48+
const comp1 = createComponent(app, \`TableRow\`, true, false, false, ["row","selected"]);
49+
50+
let block1 = createBlock(\`<table><tbody><block-child-0/></tbody></table>\`);
51+
52+
return function template(ctx, node, key = "") {
53+
const ctx1 = ctx;
54+
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['this'].rows());;
55+
for (let i1 = 0; i1 < l_block2; i1++) {
56+
let ctx = Object.create(ctx1);
57+
ctx[\`row\`] = k_block2[i1];
58+
const key1 = ctx['row'].id;
59+
c_block2[i1] = withKey(comp1({row: ctx['row'],selected: ctx['this'].selectedId}, key + \`__1__\${key1}\`, node, this, null), key1);
60+
}
61+
const b2 = list(c_block2);
62+
return block1([], [b2]);
63+
}
64+
}"
65+
`;
66+
67+
exports[`reactive cleanup on component destruction benchmark-like scenario: signal.Array rows with computed isSelected 2`] = `
68+
"function anonymous(app, bdom, helpers
69+
) {
70+
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
71+
let { safeOutput } = helpers;
72+
73+
let block1 = createBlock(\`<tr block-attribute-0="class"><td><block-child-0/></td><td><a><block-child-1/></a></td></tr>\`);
74+
75+
return function template(ctx, node, key = "") {
76+
let attr1 = ctx['this'].isSelected()?'danger':'';
77+
const b2 = safeOutput(ctx['this'].p.row.id);
78+
const b3 = safeOutput(ctx['this'].p.row.label());
79+
return block1([attr1], [b2, b3]);
80+
}
81+
}"
82+
`;
83+
84+
exports[`reactive cleanup on component destruction destroying a component with computed cleans up signal observers 1`] = `
85+
"function anonymous(app, bdom, helpers
86+
) {
87+
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
88+
let { createComponent } = helpers;
89+
const comp1 = createComponent(app, \`Child\`, true, false, false, ["selected"]);
90+
91+
let block1 = createBlock(\`<div><block-child-0/></div>\`);
92+
93+
return function template(ctx, node, key = "") {
94+
let b2;
95+
if (ctx['this'].show()) {
96+
b2 = comp1({selected: ctx['this'].selectedId}, key + \`__1\`, node, this, null);
97+
}
98+
return block1([], [b2]);
99+
}
100+
}"
101+
`;
102+
103+
exports[`reactive cleanup on component destruction destroying a component with computed cleans up signal observers 2`] = `
104+
"function anonymous(app, bdom, helpers
105+
) {
106+
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
107+
let { safeOutput } = helpers;
108+
109+
let block1 = createBlock(\`<span><block-child-0/></span>\`);
110+
111+
return function template(ctx, node, key = "") {
112+
const b2 = safeOutput(ctx['this'].isSelected());
113+
return block1([], [b2]);
114+
}
115+
}"
116+
`;
117+
118+
exports[`reactive cleanup on component destruction destroying multiple children with computeds cleans up all signal observers 1`] = `
119+
"function anonymous(app, bdom, helpers
120+
) {
121+
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
122+
let { prepareList, createComponent, withKey } = helpers;
123+
const comp1 = createComponent(app, \`Row\`, true, false, false, ["id","selected"]);
124+
125+
let block1 = createBlock(\`<div><block-child-0/></div>\`);
126+
127+
return function template(ctx, node, key = "") {
128+
const ctx1 = ctx;
129+
const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['this'].rows());;
130+
for (let i1 = 0; i1 < l_block2; i1++) {
131+
let ctx = Object.create(ctx1);
132+
ctx[\`row\`] = k_block2[i1];
133+
const key1 = ctx['row'];
134+
c_block2[i1] = withKey(comp1({id: ctx['row'],selected: ctx['this'].selectedId}, key + \`__1__\${key1}\`, node, this, null), key1);
135+
}
136+
const b2 = list(c_block2);
137+
return block1([], [b2]);
138+
}
139+
}"
140+
`;
141+
142+
exports[`reactive cleanup on component destruction destroying multiple children with computeds cleans up all signal observers 2`] = `
143+
"function anonymous(app, bdom, helpers
144+
) {
145+
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
146+
let { safeOutput } = helpers;
147+
148+
let block1 = createBlock(\`<span><block-child-0/></span>\`);
149+
150+
return function template(ctx, node, key = "") {
151+
const b2 = safeOutput(ctx['this'].isSelected());
152+
return block1([], [b2]);
153+
}
154+
}"
155+
`;
156+
43157
exports[`reactivity in lifecycle Child component doesn't render when state they depend on changes but their parent is about to unmount them 1`] = `
44158
"function anonymous(app, bdom, helpers
45159
) {

0 commit comments

Comments
 (0)