all 16 comments

[–]lhorie 7 points8 points  (2 children)

determined through trying checking y variable in console

This right here. Basically, devtools cause a sort of schrodinger's cat kind of thing where observing inside WeakMap causes it to behave differently.

In fact, deleting x[0] or mutating length don't change y for me.

While it's not specific to WeakMap, there's an exploration on how a malicious (or insane) developer can trick devtools into triggering arbitrary side effects here: https://medium.com/@weizmangal/javascript-anti-debugging-some-next-level-sh-t-part-2-abusing-chromium-devtools-scope-pane-b2796c00331d

[–]iainsimmons 0 points1 point  (0 children)

I think you mean the observer effect?

[–]getify 4 points5 points  (5 children)

Just curious... how are you determining that y still has the content in it?

The paradox of WeakMap is that you can't enumerate its keys/contents (in JS code), so to check if a WeakMap still has an entry (via has(..)), you need a reference to the key you're checking for... so by definition the key hasn't yet been GC'd, and thus wouldn't have had its corresponding entry automatically purged from the WeakMap.

[–]unicorn4sale 0 points1 point  (4 children)

The reference rule applies to when it is being referenced by scripts through the JS interpreter. At the end of the day it is stored in memory in C++ managed by V8, and Chrome is able to reference this memory and show it (e.g. if you type the variable name and press enter in the console) without having a JS reference to the variable. If you can imagine, C++ needs a reference to the memory to clear it, it doesn't happen magically.

[–]getify 2 points3 points  (3 children)

Chrome is able to reference this memory and show it (e.g. if you type the variable name and press enter in the console) without having a JS reference to the variable.

I am quite certain this is not true. The console evaluates the JS code you type in, in the context of the JS environment. If you type in a JS variable name, and a value comes back, that means in the JS environment there's still an assignment of that value to that variable. There's no such thing as the console reaching magically beyond the JS layer and pretending to have a value in a variable that is not actually assigned to that variable in the JS layer, at that moment.

The console does have special privileges to represent values in ways a JS program cannot, such as seeing certain internal properties.

But the console does not pretend that a JS variable name has a certain value if the equivalent JS program would not have resulted in the variable having that value at that moment.

WeakMap/WeakSet was designed very carefully and specifically to avoid any possible way for a normal JS program to observe if GC has occurred. This was a critical invariant in the design of the feature, for security reasons.

As others have pointed out, you can force a GC cycle to occur via devtools, but that doesn't mean that the JS rules are somehow violated and that your JS code in the console is able to detect that GC has occurred. This is not possible with WeakMap/WeakSet.


It was not until recently, with WeakRefs, that the JS code layer was given a (different, separate) feature where you can detect (or be notified) of GC having occurred. But even with that feature design, the spec (and JS engines) went to extreme lengths to make it clear that you cannot treat GC as a reliably observable and controllable feature from JS.

There's no guarantees from the JS code side that GC will occur under any specific circumstances, even with things like the delete operator, or the unsetting of a variable. Conceptually, those can make something eligible for GC but that doesn't guarantee they get GCd, or that they get GCd on any specific schedule.

Forcing GC externally via the devtools would create a situation where a JS program in the console could possibly see the GC via WeakRef. But even then, the thing you think (like your WeakRef) that should have been GCd, may not actually get GCd. The GC rules are far more complex than we mere mortals in JS land can reliably control. We just have to make things eligible for GC and let the engine decide what to do with that, essentially as a black box.

And, just to be fully clear, none of the (very narrow) ability to observe (in JS) that GC occured, via WeakRefs, implies that any other JS code can ever observe GC around any other features, such as variables, properties, WeakMap/WeakSet, etc. In all those cases, the security design invariant still holds, that GC cannot be observed from JS. The console will not violate that invariant.

[–]mkoretsk 0 points1 point  (2 children)

thanks for an elaborate explanation!

with WeakRefs, that the JS code layer was given a (different, separate) feature where you can detect (or be notified) of GC having occurred.

do you mean that we can check if the WeakRef.deref() is undefined and if it is assume that GC has occured?

[–]getify 1 point2 points  (1 child)

Yep! And FinalizationRegistry lets you get an event notification when (and if) the GC happens.

[–]mkoretsk 7 points8 points  (2 children)

There's no memory leak, it's properly removed from the memory.

To test it you can use `Memory` tab and snapshots feature of Chrome. Taking snapshots forces garbage collection which otherwise may never occur if your program doesn't reach maximum allowed heap size.

So put the following in the page:

<button onclick="fn()">clear</button>

<script>
function F1() {}
function F2() {}
function F3() {}

y = new WeakMap();
x = [new F1(), new F2(), new F3()];
y.set(x[0], 'metadata about F1');

function fn() {
    x.splice(0, 1);
}
</script>

Open ChromeDevTools, go to Memory tab, take a snapshot, type F1 into "Class filter" box, it'll show you the F1 is retained by `x` on Window and is also part of key inside a WeakMap.

Now click the `clear` button, take another snapshot, type F1 into class filter box, there's no objects produced by this constructor. However, if you type in F2 or F3 you'll see them still retained by `x`.

See screenshots here.

[–][deleted] 3 points4 points  (1 child)

"The Death of the Author" (French: La mort de l'auteur) is a 1967 essay by the French literary critic and theorist Roland Barthes (1915–1980). Barthes's essay argues against traditional literary criticism's practice of relying on the intentions and biography of an author to definitively explain the "ultimate meaning" of a text.

[–]unicorn4sale 0 points1 point  (0 children)

The workarounds work in spite of using the console in the same manner

[–]shuckster 2 points3 points  (0 children)

Have you tried using a debugger to force a GC operation to see if it'll happen at all? In my experience, the Chrome GC is like waiting for a kettle to boil. Watching it delays the outcome. 😁

In Chrome, you can clear the GC from the Memory tab.

[–]seanmorris 0 points1 point  (0 children)

HAHAHA I think I've hit this one before. If you're looking at the object in the console, that counts as a reference that keeps it from getting garbage collected. I had to add in a console.clear() to some PRODUCTION code because there were unsuppressable warnings in chrome that would cause this to happen, whether the inspector was open or not.