Status Update
Comments
ec...@chromium.org <ec...@chromium.org> #2
[Monorail components: GC]
ml...@chromium.org <ml...@chromium.org> #3
ve...@google.com <ve...@google.com> #4
The issue lies in that all values passing through WeakRefs are being kept alive until the end of a turn:
Good code (non-node-tool at least) should typically return to the event loop fairly quickly, so this shouldn't really be a big problem. For node tools I could see this become a problem. The above probably shouldn't be a set, but essentially a WeakMap(weakref|finalizationregistry->value)
Tentatively assigning to victorgomes@ as a node.js problem.
[Monorail components: Language]
ml...@chromium.org <ml...@chromium.org> #5
One can argue that in this particular case the WeakRef dies and there's no FinalizationRegistry, so there's no GC to observe as there's no referent anymore.
If AddToKeptObjects() [2] is not otherwise observable, than this could be optimized. E.g. keeping a strong link from the WeakRef to the target in such cases instead of just blindly adding target to a list that is only cleared on turn. Wheather some memory overhead for all other use cases is acceptable or this can be further optimized is still open.
[1]
[2]
[Monorail components: Runtime]
ml...@chromium.org <ml...@chromium.org> #6
sy...@chromium.org <sy...@chromium.org> #7
verwaest@'s point also stands that this could be optimized better for the node use case: AddToKeptObjects is not _intended_ to be observable outside of WeakRefs, WeakMaps, and WeakSets.
My reading of why even FinalizationRegistry doesn't matter in HTML: FinalizationRegistry's semantics depends on Liveness [0], which unconditionally considers any object in [[KeptAlive]] to be live. The only way finalizations are observed is via a callback in a macrotask that the FR enqueues (we never shipped the sync `cleanupSome` method). Note that ClearKeptObjects() is called at the microtask checkpoint in [3]. Since a macrotask would always execute after the first microtask checkpoint after the current JS execution finishes, the earliest observable point of a WeakRef referent that's collected during the same turn because all its WeakRefs also died is after a ClearKeptObjects() call. And [1] says an implementation can enqueue a FR task "at any time" during execution for objects that aren't live, so you can always explain the same-turn finalization as having been done right after the ClearKeptObjects() call in the microtask checkpoint.
For node, I think they can do whatever they want so long as they keep with FR tasks being macrotasks.
[0]
[1]
[2]
sy...@chromium.org <sy...@chromium.org> #8
sy...@chromium.org <sy...@chromium.org> #9
gu...@gmail.com <gu...@gmail.com> #10
dt...@google.com <dt...@google.com> #11
dt...@google.com <dt...@google.com> #12
is...@google.com <is...@google.com> #13
[Auto-CCs applied]
[Multiple monorail components: GarbageCollection, Language, Runtime]
[Monorail components added to Component Tags custom field.]
br...@gmail.com <br...@gmail.com> #14
This issue seems to be related to
I ran the above code with BunJS. BunJS uses JavascriptCore.
while (true) {
new WeakRef({});
}
BunJS did not crash & had stable memory usage due to Garbage Collection. NodeJS did crash after accumulating memory.
#
# Fatal error in , line 0
# Check failed: (location_) != nullptr.
#
#
#
#FailureMessage Object: 0x7fff8969b030
----- Native stack trace -----
1: 0xd203d1 [node]
2: 0x2118e21 V8_Fatal(char const*, ...) [node]
3: 0x10de5fd v8::internal::Heap::KeepDuringJob(v8::internal::Handle<v8::internal::HeapObject>) [node]
4: 0x1534381 v8::internal::Runtime_JSWeakRefAddToKeptObjects(int, unsigned long*, v8::internal::Isolate*) [node]
5: 0x193cef6 [node]
[1] 1956898 trace trap (core dumped) node ./tmp/oom.js
Awaiting a queueMicrotask has the same issue.
while (true) {
new WeakRef({});
await new Promise(resolve=>queueMicrotask(()=>resolve()));
}
Awaiting a setTimeout does not have this issue.
while (true) {
new WeakRef({});
await new Promise(resolve=>setTimeout(()=>resolve(), 0));
}
sy...@chromium.org <sy...@chromium.org> #15
This is in fact the specified behavior, so V8 is working as intended to run out of memory here. It is JSC that's behaving in a non-standards compliant way.
WeakRef
s are specified to be cleared only when execution returns to the event loop. The rationale in TC39 was that during synchronous execution, it is too surprising and makes reasoning about programs too difficult if WeakRef
s could be cleared out from under the execution. So, only when execution returns to the event loop (that is, the JS stack becomes empty) is there an opportunity to null out WeakRef
s.
The following snippet never returns to the event loop, so none of the WeakRef
s are ever cleared. It infinitely loops until it runs out of memory.
while (true) {
new WeakRef({});
}
Similarly, the following snippet also never returns to the event loop. It keeps allocating WeakRef
s and Promise
s, but never actually returns execution to the event loop to service the setTimeout
handlers.
while (true) {
new WeakRef({});
new Promise(resolve=>setTimeout(()=>resolve(), 0));
}
The details from the spec are:
- When a
WeakRef
is created or dereferenced, it calls the AO. This adds the target to a list called [[KeptAlive]]. As long as a value is in [[KeptAlive]], it is consideredAddToKeptObjects .live - The JS embedder (e.g. HTML) is supposed to call the
AO to clear that list when execution returns to the event loop.ClearKeptObjects - The HTML spec calls ClearKeptObjects when
. See step 6.performing a microtask checkpoint
br...@gmail.com <br...@gmail.com> #16
Sorry. I edited my comment. I was editing the benchmark & pasted in the incorrect code. I meant to paste in the following which does not seem to exit from the event loop:
while (true) {
new WeakRef({});
await new Promise(resolve=>queueMicrotask(()=>resolve(), 0));
}
I'm going to further research this. I think that GC behavior should be consistent whether it's on regular allocated objects or WeakRef. Meaning the least surprising behavior is if the GC acts the same for Objects or WeakRef. So if the following does not result in an OOM, neither should WeakRef:
while (true) {
const o = {}
}
To me that would be the least surprising behavior. The spec seems like a footgun that is baffling. I cannot wrap my head around why it's done this way. It makes no sense to me.
sy...@google.com <sy...@google.com> #17
To me that would be the least surprising behavior. The spec seems like a footgun that is baffling. I cannot wrap my head around why it's done this way. It makes no sense to me.
It's done this way because certain delegates in TC39 felt very strongly that WeakRefs be only clearable when returning to the event loop, as they thought otherwise programs would be too hard to reason about.
I can see a reasonable relaxation of the spec where if all WeakRef
s containing a target
themselves die within a turn, the target
is removed from [[KeptAlive]]. This isn't what the spec currently says though.
Is there an issue with periodically returning to the event loop?
br...@gmail.com <br...@gmail.com> #18
It's done this way because certain delegates in TC39 felt very strongly that WeakRefs be only clearable when returning to the event loop, as they thought otherwise programs would be too hard to reason about.
That's ironic b/c I find this behavior "too hard to reason about" because it is inconsistent from the rest of Javascipt.
Is there an issue with periodically returning to the event loop?
I updated the benchmarks to be asynchronous. The async benchmarks are generally slower than the synchronous benchmarks. Except when benchmarking WeakRef in a synchronous benchmark.
I'm using this behavior for a reactive memo library called
I see 2 problems. WeakRef is not Garbage Collectable in the nursery. And allocating WeakRefs seems to degrade the performance of the V8 runtime. Presumably due to excessive memory allocation. Which is not present in JSC.
I
The synchronous & asynchronous benchmarks seem to run faster with JSC, for all of the benchmarks.
sy...@google.com <sy...@google.com> #19
WeakRef is not Garbage Collectable in the nursery.
Yep, that is a real shortcoming that we should fix at some point (prioritization and all that).
I'm using this behavior for a reactive memo library called rmemo. rmemo is the reactive base of an isomorphic DOM rendering library relementjs. DOM rendering libraries are expected to render synchronously. If there are > 20000 rmemos instantiated from the rendering, then there will be memory or performance issues.
In this use case, do the WeakRef
s themselves also die synchronously, or just their targets?
br...@gmail.com <br...@gmail.com> #20
Yep, that is a real shortcoming that we should fix at some point (prioritization and all that).
I appreciate that. I noticed that Firefox (Spider Monkey) has the same behavior. I can file an issue with them.
In this use case, do the WeakRefs themselves also die synchronously, or just their targets?
I think the WeakRefs + targets would normally be held in memory. Unless there is a synchronous operation that replaces many DOM elements. In this case:
- If there are no parent rmemos being referenced, then the WeakRef + targets will be out of scope
- If there is a parent rmemo in memory scope, holding child WeakRefs of the removed DOM elements. Then the parent rmemo holds onto the WeakRefs until it's value changes. Then the WeakRefs where
deref()
returnsundefined
are removed from the collection.
I updated
time node memory-rmemo.js
...
total: 1000000 {
rss: 1152708608,
heapTotal: 1103556608,
heapUsed: 1060476424,
external: 1619130,
arrayBuffers: 10467
}
node ./memory-rmemo.js 2.59s user 0.53s system 224% cpu 1.387 total
time node memory-signal.js
...
total: 1000000 {
rss: 1333592064,
heapTotal: 1281544192,
heapUsed: 1239816688,
external: 1619130,
arrayBuffers: 10467
}
node ./memory-signal.js 3.21s user 0.59s system 238% cpu 1.598 total
br...@gmail.com <br...@gmail.com> #21
I created
Yep, that is a real shortcoming that we should fix at some point (prioritization and all that).
Is this the current issue for this work? Or will it be
I'm hoping to demonstrate to the Firefox team that there is an active issue. This issue was switched to "Won't fix (Intended behavior)".
sy...@google.com <sy...@google.com> #22
Is this the current issue for this work? Or will it be
? https://issues.chromium.org/issues/333584632
Please file a new issue specifically for supporting WeakRefs in the young generation.
ml...@chromium.org <ml...@chromium.org> #23
Young gen support is tracked in
Description
### NodeJS Version
v16.8.0
### Platform
Linux lion-gram 5.4.0-64-generic #72~18.04.1-Ubuntu SMP Fri Jan 15 14:06:34 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
### What steps will reproduce the bug?
Run this script
```js
while (true) {
new WeakRef({});
}
```
### How often does it reproduce? Is there a required condition?
Always
### What is the expected behavior?
After each loop the newly created object `{}` should get garbage collected. Eventually the weak ref to that object should get garbage collected too. So the application should run forever with memory increasing and decreasing as gc kicks in.
### What do you see instead?
The process memory increases linearly until ~2.5GB then crashes with
```
$ node oom.js
#
# Fatal error in , line 0
# Check failed: (location_) != nullptr.
#
#
#
#FailureMessage Object: 0x7ffcce0ed040
1: 0xb6ef91 [node]
2: 0x1bd8004 V8_Fatal(char const*, ...) [node]
3: 0xeb772f v8::internal::Heap::KeepDuringJob(v8::internal::Handle<v8::internal::JSReceiver>) [node]
4: 0x12239ec v8::internal::Runtime_JSWeakRefAddToKeptObjects(int, unsigned long*, v8::internal::Isolate*) [node]
5: 0x15cdf39 [node]
Trace/breakpoint trap (core dumped)
```
### Additional information
Analyzing the issue further seems that any object passed to the constructor of WeakRef is hold forever. I have other examples that cause an OOM with WeakRef usage:
Example with logs to visualize linear memory growth until OOM
```js
let i = 0;
const heapUsed = process.memoryUsage().heapUsed;
while (true) {
const arr = [];
for (let i = 0; i < 1e7; i++) arr.push(i);
new WeakRef(arr);
global.gc();
console.log(i++, (process.memoryUsage().heapUsed - heapUsed) / 1e6, "MB");
}
```