An unexpected memory leak in JS

Поделиться
HTML-код
  • Опубликовано: 1 янв 2025

Комментарии • 318

  • @w01dnick
    @w01dnick 4 месяца назад +457

    TBH, first language that cames into my mind after hearing "grabage collection" is Java, then bunch of scripting languages, not Go.

    • @tranquangthang8897
      @tranquangthang8897 4 месяца назад +18

      Fair and agree

    • @twitchizle
      @twitchizle 4 месяца назад +27

      my guy thinks he is advanced in js and go

    • @adicandra9940
      @adicandra9940 4 месяца назад +8

      yes, Xmx and Xms hell.

    • @StiekemeHenk
      @StiekemeHenk 4 месяца назад +5

      Java thanks to Minecraft
      Scripting like Lua and Python because of Gmod and.. well python is absolutely everywhere

    • @thomassynths
      @thomassynths 4 месяца назад +16

      Languages I think of in order when hearing GC: Java, JavaScript, C#, Haskell, Python, Lisp, Lua, Fsharp, Ruby, Erlang, …. There is no world where Go even blips on the radar. Such a strange association in the intro.

  • @arcanernz
    @arcanernz 4 месяца назад +133

    I believe it's all about closures. innerFunc1 and innerFunc2 share the same closure that's why bigArrayBuffer is not cleaned up until all of the functions that share the same closure gets dereferenced. JS doesn't care if one function uses bigArrayBuffer and another doesn't as long as it's being used by any function then the bigArrayBuffer will be included in the closure. If however no inner function uses the bigArrayBuffer then it can be optimized out.

    • @kyledl6357
      @kyledl6357 4 месяца назад +17

      Exactly this. I'm hesitant to call it a "bug", though. It's definitely a feature of Javascript (if we can even call it a feature) that can be misused/exploited, but I personally already knew that this was how it worked and assumed nothing of it. I'm not sure why people are surprised about this "bug", tbh.

    • @Mothuzad
      @Mothuzad 4 месяца назад +8

      So glad somebody already wrote the explanation.
      As for why it can be considered a bug, it's just because it's unexpected behavior. You think anything unreachable will be a candidate for GC, but any function you create in some scope will keep alive every other piece of memory used in a closure from that same scope. So now you need to put a mental red flag on any function that creates "multiple" closures. Fortunately, you can always split closures manually, but it's just not something you'd think is necessary. Frankly, the engines should have a heuristic for splitting closures into separate objects or just make that the default.

    • @arcanernz
      @arcanernz 4 месяца назад

      @@Mothuzad I’ve tried to split closures myself but saw no difference in behavior since I believe the engine will still create one top level closure shared by all nested closures. If you could somehow split closures I’m not sure if the complexity involved is worth the gain considering there may be other unintentional side effects which may break existing behavior.

    • @dealloc
      @dealloc 4 месяца назад +1

      This is it.

    • @Mothuzad
      @Mothuzad 4 месяца назад +1

      ​@@arcanernz In my imagining, the separate closures require totally separate scopes created by passing arguments into different functions. Not just nesting more functions into the same scope.

  • @samdembiny7082
    @samdembiny7082 4 месяца назад +42

    Its not memory leak, but its by design, each inner function instace referencing on its upper scope context so each variable in upper scope is not garbage collected.
    there is nothing to do with global this, but its way how inner functions are keeping upper context in memory 👌

    • @Technoyote
      @Technoyote 4 месяца назад +3

      Yes, it's all about when function closures / execution contexts are GC'ed.

    • @robertholtz
      @robertholtz 4 месяца назад +5

      You are totally correct. Thank you for saving me the trouble to explain the same. I built a project a while back that had a bunch of concurrent timers running and resetting all over the place and learned this the hard way. As I worked my way through it, at first I thought JS was stupid, next I started to think I was stupid. Once I achieved understanding everything made complete sense and I came to appreciate it as a feature. It yields completely consistent and reliable results once you wrap your head around it.

    • @Kirides
      @Kirides 4 месяца назад

      This.
      The issue should only happen with arrow functions though, as regular functions should not "func.bind(this)" automatically.

    • @robertholtz
      @robertholtz 4 месяца назад +1

      @@Kirides I have come to avoid using "this." There is just too much unpredictability about it that I'd rather not use it on anything that is mission-critical. Arrow functions are the future, I like them, but there too I tend to lean on traditional functions on anything where browser compatibility and support issues might arise.

  • @imadetheuniverse4fun
    @imadetheuniverse4fun 4 месяца назад +59

    how is it possible to be a JS dev and not know about GC, and why did this guy try to make it sound like GC is synonymous only with GO? that's some next-level subliminal gas-lighting or something, lol

    • @Me__Myself__and__I
      @Me__Myself__and__I 4 месяца назад +17

      Sadly a lot of JS devs are what we used to call "script kiddies" back in the day.
      Q: What do you call a person who barely understands coding?
      A: A web developer.
      And before you all hate, no not everyone (but sadly far too many).

    • @flint0131
      @flint0131 4 месяца назад +1

      ​@@Me__Myself__and__I they call themselves "engineers" now

  • @tedchirvasiu
    @tedchirvasiu 4 месяца назад +40

    I guess it's as simple as this:
    A child lambda will keep a reference to it's entire parent scope. The big array buffer lives in the parent scope. It doesn't matter if you use globalThis or simply return func2 and then hold on to that reference some other way.
    I think compiled languages such as C# are able to see what is being referenced from the parent scope (if anything is referenced at all) and create a new class containing only pointers to what is being accessed by the lambda. But I don't see how JavaScript would do that, so I think this is the expected behaviour.
    *Update*: This comment is probably not accurate, see the replies

    • @Mothuzad
      @Mothuzad 4 месяца назад +4

      The examples in the article already show that if no closure references the array buffer, then the buffer gets collected. JS engines are already tracking what is referenced, and it's as simple as matching the symbols used in a function against higher scopes.
      So this behavior can suddenly appear if you simply define a short-lived function using some memory in a scope where there's a long-lived function that doesn't use the memory. The memory will be excluded from GC after it becomes unreachable. That's exactly what the article demonstrates.

    • @tedchirvasiu
      @tedchirvasiu 4 месяца назад +4

      @@Mothuzad Fair, that's weird / unintuitive behavior.

    • @vigneshwarrv
      @vigneshwarrv 4 месяца назад

      ​@@MothuzadExactly if we are adding a long lived function into a scope, we are already aware that the scope gonna exist as long as this long lived function exist.
      ( Doesn't matter whether the long lived function uses any of the values from its parent scope )
      If anyone writing likewise, it more of a skill issue. Its like writing Javascript to do some animations when it can be done by css alone.

    • @tedchirvasiu
      @tedchirvasiu 4 месяца назад +2

      ​@@vigneshwarrv I think you're missing his point. He pointed out there is an example where the leak does not happen if the long lived function does not reference the array buffer.
      So it appears it does matter whether the long lived function uses the parent scope or not. It looks like JS does not automatically hold a reference to the parent scope all the time.

    • @vigneshwarrv
      @vigneshwarrv 4 месяца назад +2

      @@tedchirvasiu yeah it's not about the long lived or short lived functions. If any of the inner functions access a particular value from its parent scope, then a closure is created with that accessed value and that closure is available to all of its inner functions.
      One of the main reason to create closure likewise is to have values that can be accessed across multiple inner functions without needing them to be passed as argument.
      The closure will be destroyed if all the inner functions are once dereferenced. If we create separate closure for each of the inner functions based on the values accessed, then the values with primitive data-types will be copied by value ( memory duplication ) and it wouldn't be shareable across those inner functions.

  • @mojajojajo
    @mojajojajo 4 месяца назад +83

    Isn’t this just how closures are supposed to work?

    • @t3dotgg
      @t3dotgg  4 месяца назад +13

      No

    • @w01dnick
      @w01dnick 4 месяца назад +21

      No, each lambda should have it's own closure. But it seems like functions defined in one function call are sharing single closure.

    • @mojajojajo
      @mojajojajo 4 месяца назад +17

      @@w01dnick yes but you’re still holding a reference to the returned function, you don’t expect this to be garbage collected. You can see that memory is freed once it’s set to null? What recommendations do you have to handle this differently?

    • @mojajojajo
      @mojajojajo 4 месяца назад +21

      @@t3dotgg you can test this using any language with garbage collection and you most likely should have the same result

    • @essamal-mansouri2689
      @essamal-mansouri2689 4 месяца назад +20

      Yeah i also don't see how this is a bug. Obviously if there's a reference to the function that uses the array buffer existing in globalThis, then we can't clear the buffer. I don't get how they expect any scripting language to infer that it is safe to garbage collect

  • @Z3rgatul
    @Z3rgatul 4 месяца назад +11

    When you capture local variable inside any function, this local variable gets lifted into closure object. You can actually see closure object from dev tools. For each function only one closure object exists. When you have code like this:
    function something() {
    let x = 1;
    setTimeout(() => console.log(x), 1000);
    }
    it is getting "transformed" into:
    function something() {
    const closure = {};
    closure.x = 1;
    setTimeout(((closure2) => console.log(closure2.x)).bind(this, closure), 1000);
    }
    I took "transformed" into brackets, because it is doing more complex things under the hood.
    And because each function can only have only 1 closure object which captures all necessary variables, we get behaviors mentioned in the video.

    • @john_paul_r
      @john_paul_r 4 месяца назад +2

      Thank you, right answer. So much confusion in this comments section

  • @samisiddiqui5286
    @samisiddiqui5286 4 месяца назад +1

    Correction 0:10 - In Rust you don't have to clean up your memory yourself.

  • @brod515
    @brod515 4 месяца назад +1

    Theo I'm glad you took the time to do the playground. (I didn't even know about the snapshot stuff and now I need to try it).
    testing things out is how you break your intuitions and really understand concepts.
    I've been learning the Vulkan api and actually testing the concepts I'm learning makes me realize how flawed my initial intuition is.

  • @berbold
    @berbold 4 месяца назад +7

    I don' t think this is a bug but I'm guessing it is just how JS closures work. When you define () => {} inside allocate, you are capturing ALL of the parent context of demo() which includes bigArrayBuffer. You don't have to reference bigArrayBuffer in innerFunc2(). Since you assign innerFunc2 to the global scope, it stays alive. So this becomes no different to returning bigArrayBuffer from a function and keeping it in a global scope variable.

    • @argh523
      @argh523 4 месяца назад +3

      Yeah, it just holds on to the entire stack frame that the clojure references. Maybe not expected, but seems straight forward and good enough for a scripting language

  • @Oler-yx7xj
    @Oler-yx7xj 4 месяца назад +81

    Meanwhile some other scripting language: "@a[1] = \@a;" necromancy and a memory leak at the same time, and you say JS is weird

    • @GaussianWonder
      @GaussianWonder 4 месяца назад +11

      What lang is this?

    • @well.8395
      @well.8395 4 месяца назад +11

      @@GaussianWonder Perl

    • @RyanLynch1
      @RyanLynch1 4 месяца назад

      @@well.8395that's why I don't love Perl for anything more than a simple regex

    • @mattmurphy7030
      @mattmurphy7030 4 месяца назад +4

      That’s so ugly

    • @Oler-yx7xj
      @Oler-yx7xj 4 месяца назад +1

      @@mattmurphy7030 Line noise, baby

  • @SomewhatLazy
    @SomewhatLazy 4 месяца назад +1

    The first video I've found about JavaScript memory leaks that actually demonstrates a memory leak.

  • @michaelutech4786
    @michaelutech4786 4 месяца назад +4

    I think you misunderstand the problem. A scope exists and retains its component for as long as it's referenced. A scope does not get partially collected, meaning that if a scope has an entry that is not longer used, that entry is still referenced by its scope and only be collected once that reference or the scope itself is collected.
    If you create a circular reference between two scopes, reference counting may or may not detect the cycle. That's why sweep and clean GC are often preferred. That's why closures and reference counting GC are so difficult to use without leakage. Just look at iOS apps written in Objective-C, blocks are full of weak references manually written despite "automatic reference counting".
    window and globalthis refer to the same scope (object).

  • @kaviisuri9997
    @kaviisuri9997 4 месяца назад +3

    The fact that theo had to assume that most js devs don’t know about garbage collection is just sad, the fact that he was right in this assumption is sadder.
    How engineers aren’t excited to understand the intricacies of how their code works is just astonishing to me

    • @3_smh_3
      @3_smh_3 4 месяца назад

      JS devs aren't even used to manipulating the dom without an abstraction layer.

    • @tantalus_complex
      @tantalus_complex 4 месяца назад

      ​@@3_smh_3 Normally, I feel like I've missed a lot by not having had much front-end framework experience, despite having focused on the Front-End since VB Script was still something you might consider writing to solve a problem for most users in the browser.
      But right now I'm feeling cute because I know vanilla JS and the DOM API better than English. My biggest annoyance is that we still don't have the Pipe Operator.

  • @cameronbehar7358
    @cameronbehar7358 4 месяца назад +5

    EDIT: I was saying “This is not a memory leak, a bug, or JS garbage collection being dumb. This is a necessary consequence of closures in any dynamic language…” but as correctly pointed out in a reply, I was mistaken. Edited so as not to mislead those who didn’t read the reply.

    • @mdintaman
      @mdintaman 4 месяца назад +2

      If I'm understanding you correctly, I think you may be mistaken.
      function foo() {
      let x = 10;
      let y = 20;
      let z = 30;
      return function() {
      eval("console.log(x + y)");
      };
      }
      const evalFromFoo = foo();
      console.log(evalFromFoo());
      In this example, if you break on the last line the closure on evalFromFoo will include x, y, and z as you say, because it isn't going to look at the eval string to try and determine what is needed.
      However if you remove the eval from the returned function and just do
      return function () {
      console.log(x + y);
      };
      When you look at the closure for evalFromFoo, the z will not be included. Although I suppose it is possible this behavior is dependent on the engine, but I don't think I've worked in an environment where the closure would be default include everything.

    • @cameronbehar7358
      @cameronbehar7358 4 месяца назад +1

      @@mdintaman Looking more closely and trying it out in the browser, you’re absolutely right! Thank you for correcting me. I was going off of my memory of seeing the full parent scope in the Chrome dev tools at least 7 years ago, but that was either outdated or (more likely) misremembered. I should’ve read the referenced article more closely, since it actually clearly outlines cases where the symbols can be used to optimize away unreferenced variables in some cases.
      
      Thanks again for taking the time to help me learn. I’ll edit my comment so as not to mislead others.

  • @SomewhatLazy
    @SomewhatLazy 4 месяца назад +1

    "JavaScript: The world's most flawed popular language."
    PHP: "Excuse me!?"

  • @von_nobody
    @von_nobody 4 месяца назад +6

    Problem is `() =>` and fact that local variables are promoted to closure aka virtual object that represents all local variables, you can rewrite this code from:
    ```
    function demo()
    {
    let x = 13;
    let y = 42;
    () => y;
    }
    ```
    to this:
    ```
    function demo()
    {
    let x = 13; //this is can be keep on CPU register or stack and can removed at function exist
    let local = { y: 42};
    () => local.y; // similar to { l: local, foo: function () { return this.l.y; } }
    }
    ```
    if you leak `x` too then function change to:
    ```
    function demo()
    {
    let local = { x: 13, y: 42};
    () => local.x;
    () => local.y;
    }
    ```
    and even if you drop one reference, whole `local` closure still need exists as whole.

  • @akuoko_konadu
    @akuoko_konadu 4 месяца назад +1

    This is why I stay Theo, learning new things each day, nice vid

  • @sam02h
    @sam02h 4 месяца назад +9

    My (probably wrong) guess this has to do with JIT's LVA.
    - "buffer is GEN at the end of the function
    - "buffer" is marked as KILL when created
    - "buffer "is alive for both the entry and exit for the blocks creating those functions, hence it deems it a reference.
    probably poorly explained, but In my head it makes sense :3

    • @cuauth
      @cuauth 4 месяца назад

      i think it has to do with the innerFunc2 capturing func (for some strange reason), and func capturing the buffer (and i think innerFunc2 capturing func without needing it, is the bug)

  • @SomewhatLazy
    @SomewhatLazy 4 месяца назад +1

    Time: 5:45
    It is because of scopes. The JavaScript garbage collector works on scopes, or closures. All the stuff inside { }.

  • @mythrail
    @mythrail 4 месяца назад +1

    at 6:11 both external references to globalThis are from the same closure record as bigArrayBuffer, once they are manually cleared the closure has no external references and can be collected

  • @glukki
    @glukki 4 месяца назад +3

    Think in scopes.
    The whole tree of full scopes is preserved, as long as you have a reference to a thing from the scope.
    And ID from the setTimeout is not a reference - it's just a number.

    • @blizzy78
      @blizzy78 4 месяца назад

      it would be too easy to simply hover over the id variable to see its type 🤷

  • @pencilcheck
    @pencilcheck 4 месяца назад +12

    Not sure why Theo keep getting confirmation from the chat :P

  • @Lemmy4555
    @Lemmy4555 4 месяца назад +2

    The GC behavior is correct, this is a limit caused by how the scope of a function works in JS and the fact that GC cannot know what variables you may be referencing inside a nested function because js allows a lot of trickeries being a dynamic language.

  • @2547techno
    @2547techno 4 месяца назад +2

    js dev discovers closures

  • @Me__Myself__and__I
    @Me__Myself__and__I 4 месяца назад +5

    The answer is very easy actuall if you adjust your pe4spective..
    The entire contents of the demo function is ONE SINGLE context / closure. Individual items are not garbage collected, the entire closure is GCed or not. So if anything within a closure of demo remains referenced, then everything in that closure is retained. When all the contents are no longer externally referenced the whole closure is freed.
    Think of the contents of demo as one single bubble. You pop the bubble and it all goes away.

    • @RandomGeometryDashStuff
      @RandomGeometryDashStuff 4 месяца назад

      nah, ["o2"] will get GCed and ["o1"] will stay even if both are referenced by o1 and o2 variables in demo closure (test in firefox add-ons manager page because Components.utils.forceGC exists):
      const w=new WeakSet();
      function demo(){
      const o1=["o1"],o2=["o2"];
      console.log(o1.length,o2.length);
      (function(){o1});//

  • @artyomnomnom
    @artyomnomnom 4 месяца назад +32

    `globalThis` is a simple alias for the `window`, c‘mon Theo

    • @t3dotgg
      @t3dotgg  4 месяца назад +7

      I guess node just doesn’t exist

    • @astral6749
      @astral6749 4 месяца назад +23

      @@t3dotgg I checked the docs. You are right that `globalThis` is not just an alias. However, on the browser, `globalThis === window` is true. So in this context, on 7:05, you're not really changing anything.

    • @blk_squirrel
      @blk_squirrel 4 месяца назад

      @@t3dotgglol got ‘em

    • @wassafshahzad8618
      @wassafshahzad8618 4 месяца назад +9

      @@blk_squirrel in this case theo is wrong tho cause he is running the code in browser

    • @Efecretion
      @Efecretion 4 месяца назад

      ​@t3dotgg to be fair, JS should only ever be used in a browser. Node.js is terrible at a conceptual level

  • @jannemyllyla1223
    @jannemyllyla1223 4 месяца назад +10

    It is easier that that: the outer function exists as long as returned or assigned innerfunctions do.

  • @dehrk9024
    @dehrk9024 4 месяца назад

    Imagine being so formal you explain what garbage collection is on an advanced video

  • @danielbaulig
    @danielbaulig 4 месяца назад +1

    The tl;dr is: if JavaScript needs to keep a closed over function scope alive, it will keep all closed over function scopes alive, even if some of them are technically no longer referenced.
    Avoid creating more than one closure per function body, to avoid this issue and perk your metaphorical ears whenever you see multiple closures being created in a single function body.

  • @DJDrewDub
    @DJDrewDub 4 месяца назад

    They're describing closures. That's exactly how closures work, and you're correct that the references, not the "call ability" is the reason.

  • @ossonku
    @ossonku 4 месяца назад +33

    You don't have to clean up your memory in Rust, nor there is a gc. Rust has its own way of memory management

    • @wisdomelue
      @wisdomelue 4 месяца назад +3

      ownership and borrowing / borrow checking

    • @thomassynths
      @thomassynths 4 месяца назад +1

      Clearing ignoring that Drop exists

    • @3_smh_3
      @3_smh_3 4 месяца назад

      too many guardrails.

    • @mattmurphy7030
      @mattmurphy7030 4 месяца назад +1

      Don’t have to clean up your own memory in C either. Just let it go man

  • @JarheadCrayonEater
    @JarheadCrayonEater 4 месяца назад +12

    JS doesn't use "reference-counters", it uses a stack and queues that are controlled by the event loop.
    What you're seeing here is predictable behavior based on how the stack works, and is NOT a "memory-leak".
    They're holding a reference to the function in a global variable. So, or course it's not going to be able to be garbage collected.

    • @dinhero21
      @dinhero21 4 месяца назад +1

      but it didn't leak in this case:
      function demo() {
      const bigArrayBuffer = new ArrayBuffer(100_000_000);
      globalThis.innerFunc2 = () => {
      console.log('hello');
      };
      }
      demo();

    • @NabekenProG87
      @NabekenProG87 4 месяца назад

      Doesn't use reference counters? Like at all? I thought V8 was using a multi generational GC?

    • @JarheadCrayonEater
      @JarheadCrayonEater 4 месяца назад +2

      @@NabekenProG87, no, since reference counters do not supply any reference details and can potentially permit circular references, which actually cause memory leaks.
      Everything seen in the video is predictable behavior, it's just not easy to troubleshoot without knowledge of it. So, the video does identify what to look out for when assigning functions.

    • @NabekenProG87
      @NabekenProG87 4 месяца назад

      @@JarheadCrayonEater Then what kind of GC is V8s Orinoco? The articles I read don't go into too much detail.

    • @mattmurphy7030
      @mattmurphy7030 4 месяца назад

      @@JarheadCrayonEatergoogle says “The V8 JavaScript engine's Orinoco garbage collector uses reference counting to reclaim memory when the last pointer to an object is lost”

  • @walidchtioui9328
    @walidchtioui9328 4 месяца назад +3

    I think doing this will make it much clearer:
    function demo() {
    const bigArrayBuffer = new ArrayBuffer(100_000_000);
    window.innerFunc1 = () {
    this; // this is refers to the parent scope
    console.log()
    }
    // same for innerFunc2 ...
    }
    demo();
    window.innerFunc1 = undefined;
    // array buffer still in memory

  • @hlrbBrambleX
    @hlrbBrambleX 4 месяца назад +6

    For some reason only in JS language I'm getting trust issues when dealing with memory management.

    • @abb0tt
      @abb0tt 4 месяца назад

      lol…trust issues warranted.

    • @bren.r
      @bren.r 4 месяца назад

      So use a lower level language like c/c++/rust to not deal with the black box nature of GC languages.

  • @HroiG
    @HroiG 4 месяца назад +5

    This doesn't seem too surprising. JS is an interpreted language so it can't determine the ways a function could use its outer scope, so it instead captures all the variables in the outer scope. This optimization might be done by compiled langues, but I'm not sure, still doesn't seem like a simple optimization to do since you have to analyze that entire function. Would be interesting to do this test in a compiled language like Go/C#/Java.
    That also explains why when you were dropping the result of the allocated function it was able to optimize there, since it just dropped the result, it was never in scope for innerFunc2.
    Also, there are some languages where you have to explicitly tell them what variables you want to capture in a lamda function. C++ uses the syntax [x](int y) -> int { return x + y; } to say that x is captured from the lambdas outer scope while y is sent in as an argument. C++ doesn't have GC though, so I'm not exactly sure why it is done like this other than just making it clear what the programmer has to clean up when. Maybe some of the smart pointers use this? I'm not sure, not really much of a C++ person.

    • @HroiG
      @HroiG 4 месяца назад +2

      That thing at the end of the video was quite surprising, so it can do some optimizations without just capturing everything in scope.
      In that case its probably just ignoring weird usages where you try to access the variable without mentioning it like eval: const hello = "a"; eval("hel"+"lo"), or maybe it is smart enough to see that and decide to just keep everything in scope, but I doubt that.
      But yeah, I guess this might then be something that can be fixed.

    • @tedchirvasiu
      @tedchirvasiu 4 месяца назад +2

      True. I am pretty sure in C# the scope captured by a local function is a class created on the fly capturing only the referenced variables, but it can do that because it can tell at compile time what is being accessed.

  • @Spudz76
    @Spudz76 4 месяца назад

    There are also some subtle differences in function scopes/closures depending on whether you use the "cool" rocketship notation or the "legacy" function keyword, which have bitten me before but I still don't fully understand why they are any different.

  • @fionnlanghans
    @fionnlanghans 4 месяца назад +1

    Btw. V8 doesn't use a reference counter for GC.

  • @SharunKumar
    @SharunKumar 4 месяца назад

    You can press the Escape key when you're on any of the other tabs, to bring up the console tab without needed to switch to it

  • @ProjSHiNKiROU
    @ProjSHiNKiROU 4 месяца назад

    Using weak references to the timeout handler or the array might help: Either the cancellation or the running of the timeout will clear the weak reference.
    Meanwhile the Rust version of this API will be designed in very tricky ways to remind you "cancel" holds the timeout handler or not.

  • @g.c955
    @g.c955 4 месяца назад

    JavaScript closures capture the entire lexical environment in which the function was created, not just the variables that are actually used by the function. This behavior can lead to unintended memory retention because as long as globalThis.innerFunc exists and references the closure, the bigArrayBuffer will not be garbage collected, even though it is not explicitly used inside innerFunc.

  • @echobucket
    @echobucket 4 месяца назад

    This is because of closures. Closures are kind of like objects and reference stuff, and as long as there's just 1 thing in the closure that has an outside reference, nothing in the closure will go away. The closure has a reference to all of the other objects.

  • @figloalds
    @figloalds 4 месяца назад

    It makes sense to me the way it works
    It could be smart like C# which transforms "closures" into function calls with precision-generated class scopes that only pass what's really being used and doesn't reuse the same scope on multiple different closures, but, it does make sense that it works like that
    I don't even consider this a bug, it's just good-to-know runtime intrinsics

  • @CottidaeSEA
    @CottidaeSEA 4 месяца назад

    For the first one I think it makes sense, just a matter of basic referencing, but the memory not being cleaned up in your example is just crazy.

  • @harald4game
    @harald4game 4 месяца назад

    I suspect that there is only one capture containing both variables and the capture is preventing the item from being freed. This makes sense since if you consider a third function modifications to both variables the functions are visible by the other functions.
    So in the original example id and bigarraybuffer aren't captured individually but as a set like an capture= {id, bigarraybuffer} and that object is referenced not the individual fields. And only if you release both reference to the capture finally frees the object.

  • @SomewhatLazy
    @SomewhatLazy 4 месяца назад +1

    Time: 13:11
    Again, it is because of the scope being retained in memory.

  • @appelnonsurtaxe
    @appelnonsurtaxe 4 месяца назад

    It's not surprising that the whole parent stack frame is reachable from the closure. The very existence of the "eval" function means you can't cherry-pick which locals the closure will capture, because any of them may be accessed

  • @nomadtrails
    @nomadtrails 4 месяца назад +1

    Umm Theo, I am 10s in and I have news for you: Absolutely no one thought JS wasn't garbage collected. People either don't know what it is, or know that JS is not one of the languages without it. 😂

  • @aaaaaaaaaaaaaaa420
    @aaaaaaaaaaaaaaa420 4 месяца назад

    A unexpected memory leak in js????? My entire world is plummeting into insanity 😳😳😳

  • @liangkui
    @liangkui 4 месяца назад

    Not group together, my guess is because closures will capture its context, so the buffer was captured in both of the fns

  • @pierwszywolnynick
    @pierwszywolnynick 4 месяца назад

    When you changed globalThis to window and expected this to change something I started questioning your sanity.
    Also fun fact - I cannot reproduce this in edge, looks like that's one of the memory optimizations that edge has implemented

  • @LewisCowles
    @LewisCowles 4 месяца назад

    Theo, if you close over local variables, the whole local scope is kept until all references are set to null / undefined. Otherwise closures wouldn't work. JS isn't dumb; any language with the ability to pull everything without explicit capture groups, will have to keep around all locals.

  • @bagofmanytricks
    @bagofmanytricks 4 месяца назад

    Someone probably told already, but `globalThis` is a catch all name for the global object. This is `window` in browsers.

  • @overhead4577
    @overhead4577 4 месяца назад

    Dude, functions are created in the same scope, it means they can call each other, so you cannot just delete one function while the other still exists. This is how functions are working together and "anonymous" doesn't change this behavior generally. What a smart GC🎉

  • @Jankoekepannekoek
    @Jankoekepannekoek 4 месяца назад

    Scala mentioned, let's go!

  • @mr.rabbit5642
    @mr.rabbit5642 Месяц назад

    To my knowledge, defining a func = () => {*reference to The Array*} creates a 'pending reference' to the array, preventing it from being cleaned up, untill func is either overwritten or cleaned by having the scope it resides in cleared.
    Tbh I don't see anything wrong with the original question, as it returned a function to use (to clear the timeout) referencing the array in question. I wonder if calling the "cancell" would clear it up

  • @Meligy
    @Meligy 4 месяца назад

    How likely is it to fall into this issue in react use effect code?

  • @algj
    @algj 4 месяца назад

    Surprisingly, I experienced a similar problem with a memory leak by reading a fetch response... The HTML string returns around 2MB, I extract the JSON from the page and it HTML does not get cleaned up.. Had to clone the object return to get rid of the previous memory!

  • @danielchodnicki3632
    @danielchodnicki3632 4 месяца назад

    What if bigArrayBuffer is referenced indirectly, for example by using eval() inside innerFunc?

  • @suvajitchakrabarty
    @suvajitchakrabarty 4 месяца назад +1

    Did he just say you have to clean up the memory yourself in Rust?

  • @echobucket
    @echobucket 4 месяца назад

    I believe if you put this into say the VSCode debugger for Node you will see the referenced closures hanging around.

  • @SomewhatLazy
    @SomewhatLazy 4 месяца назад +1

    Time: 11:30
    No, you already proved it had nothing to do with globalThis when you used window.

  • @cuauth
    @cuauth 4 месяца назад

    i think it happens because, the function in globalThis, have a reference to all other functions and variables, in the current scope, so the globalthis.innerFunc2 at the end, is +1 the reference counting of func, and thats reference counting +1 the arrayBuffer, so it's not able to be cleaned up. the allocator/RC could do a better job, only "moving" or referencing the values being used in the function, and the references in that function.

  • @mattmurphy7030
    @mattmurphy7030 4 месяца назад

    The hair and mustache are such a tragedy

  • @Matt23488
    @Matt23488 4 месяца назад +2

    I think you still misunderstand what's happening... I don't think globalThis has anything to do with it. It's just used to keep the functions callable. The scope is the reason it happens. Since one of the functions defined in the scope are still callable, the scope can't be cleaned up, and due to that, the huge buffer can't be cleaned up since it belongs to the scope. At least that's how I understand it.

    • @blizzy78
      @blizzy78 4 месяца назад

      you're correct, good sir

  • @zackvoase1348
    @zackvoase1348 4 месяца назад

    It has nothing to do with globalThis. To prove it, have your demo function return an array with the closures, rather than doing any assignment. One single scope is created for all closures made in the function, and that scope will contain references to all the variables captured by *any* of the closures. Thus, as long as any of the closures are still alive, any of the variables captured by their sibling closures will be kept alive, even if those sibling closures have since been GC’d.
    A ‘fix’ would involve creating a separate scope for each closure created from that original lexical scope (ie the body of demo()), so that the capture set of each closure could be GC’d independently. I’m not sure if this would break other expected behaviors though, or be hard or memory-intensive to implement.

  • @GaryGreene1977
    @GaryGreene1977 4 месяца назад

    The fun with function scoping and the problem of languages with real global scopes. Most scripting languages have this problem unless they have ways of guarding scoping, like Perl's 'my` and 'our' usage with lexical subroutines. I've run into similar issues with Ruby and JS code in the past and had to do tricks like what you did with in-lining and setting members to undefined as well to get around this mess.

  • @jfftck
    @jfftck 4 месяца назад

    Why is JavaScript designed this way? Due to the fact that the issue is cross browser, it confirms that it is part of the specification. Living through the Netscape and IE war, and having to code large amounts of code for supporting each implementation, that was easier to identify which browser was being used and would also lead to harder to manage code.

  • @ErikTheHalibut
    @ErikTheHalibut 4 месяца назад

    Theo has the Beavis hairdo

  • @paxdriver
    @paxdriver 4 месяца назад

    Imho there's an easier way to think about this so it's jot a headache: when globalThis has a property initialized that means any other functions defined next to it may use that scoped function, so it to needs wait to for the globalThis to garbage collect to be sure that the function gets collected and the array buffer with it.
    I will try this theory out on my own so this is sort of a bookmark for myself, but I would bet functions wait to garbage collect until after everything in that scope is collected first since functions are hoisted when defined they're probable last to get destroyed.
    That would mean defining functions in global scope or well defined scopes (unlike the example which initialized part of globalThis from a nested scope, effectively holding back garbage collection of the entire globalThis object). The practical way to avoid this is good practise already - don't initialize a global key from a nested function. A class instantiation of all possible keys would prevent this, I bet compiler frameworks prevent it, and even wasm or other transpilations would too, because they're not jit interpreters. It's probably the interpreter part that needs to hoist scopes like this because otherwise you'd just be setting an arbitrary depth for cutoff and that would be way worse performance than recursively hoisting an object being structurally modified in a deeper scope.
    Either by defining all functions in their useable widest scope like functional programming doctrine dictates, or by defining all objects with all possible placeholder keys like modular OOP doctrine suggests, I bet these paradigms eliminate this memory leak by design, or at worst limit it to an interpreted, unoptimized runtime anyway where that kind of leakage should be expected.
    Love these videos btw, thanks a mil. Can't wait to try this out with iffe and async and every variety I can imagine lol. I bet the browser throwing timeout into its own thread loop has something to do with it too, or specifically array buffers working at lower level than standard objects due to their memory allocation benefits maybe plays a role? There are so many different reason I can imagine this made sense to ECMA lol

  • @IvanKravarscan
    @IvanKravarscan 4 месяца назад

    Is this rediscovery of LOH (large object heap)?
    PS: nope, sorry for intrusion but know that this beast lurks too in GC land.

  • @daleryanaldover6545
    @daleryanaldover6545 4 месяца назад

    This is not a bug but if you understand JavaScript GC, you would know it is stupid to put a reference of a huge array buffer into the cancel function. You basically make to hold it hostage by doing that and GC cannot touch it unless you manually call the cancel function or destroy the reference by setting the variable to null.

  • @cid-chan-2
    @cid-chan-2 4 месяца назад

    😅As youre trying to understand the snippet, before you eveb got properly started.
    Here is my assumption:
    setTimeout and the cancel-callback both contain a reference to their closure, which contains the ArrayBuffer. setTimeout releases the reference when the delayed function is called. The cancel-function is only collected when you run `delete globalThis.cancelDemo` (which in turn drops the last reference to the closure and its variables get then collected in turn).
    ---
    Minute 5: interesting, so js knows not to pull in variables into the closure if its nlt needed.
    ---
    Minute 9: so if you create a function instance, js determines if it needs to pull in the closure at all, good to know.

  • @hehimselfishim
    @hehimselfishim 4 месяца назад

    either i don’t know how to code or im really just dumb, wtf is all of this? i’ve only used JS my entire career, about 2 years and this sounds like PAIN. interested in how this works but this really just sounds like pain honestly.

  • @anonimoanonimo-wb5gk
    @anonimoanonimo-wb5gk 4 месяца назад

    Hi, I'm a begginer fullstack developer, and I'm leraning react. I'm kinda feeling lost with the Overhelming quantit of information there is about It. I learned a lot of concepts, how to use hooks, etc. And now there is this react compiler I literally didn't knew about the existance... Can comeone give me some tips of what I should study to get into te market? I'm already building wesites, but I'm scared I'm missing out on something I should know and I don't wight now...

  • @lazyteddy123456
    @lazyteddy123456 4 месяца назад

    at 6:18 I started guessing it was a scope/context thing. By 8:50 I was screaming that it's a scope/context thing.

    • @Mateking92
      @Mateking92 4 месяца назад

      Okay I'm trying to wrap my head around this, but the main point is that everything in the functions scope gets saved when closure happens, not just the variables that the function references, right? So at 6:18 even if you set innerFunc1 = undefined, inner2func still has the buffer referenced/saved, thats why it is not cleared? So it is not a bug, just closure is not efficient/optimized and saves everything in the scope even when not referenced by the function? And in the first example, the () => clearTimeout(id) function is the culprit because it has the buffer saved with its scope?

  • @PeterDragonPPG
    @PeterDragonPPG 4 месяца назад

    I ran into this issue a couple years ago, had leaking memory and no idea where it was going even dropping everything, brute forcing garbage collection did nothing. just need an Arc::Mutex for everything ;) but honestly this is one of those things that rust catches/prevents before compile (ownership across async, pita, but better than a memory leak).

  • @willcoder
    @willcoder 4 месяца назад

    I'd have thought a really _good_ garbage collector in JS wouldn't leave any JS behind. 😜

  • @jaredsmith5826
    @jaredsmith5826 4 месяца назад +1

    I know you're trying to keep it simple for folks that aren't nerds for this stuff, but JS engine GCs don't actually use reference counting. It's still probably a good enough mental model for this sort of analysis, but just to clarify. As for hating yourself for deep knowledge of JS I like to call it being a "reluctant expert" lol.

  • @linuxguy1199
    @linuxguy1199 4 месяца назад

    I'd rather use FORTH, BF or straight assembly, JS is probably the most cursed programming language out there. I'm happy I do C for a living, it's pure and simple.

  • @inayelle
    @inayelle 4 месяца назад

    I guess that's why other languages have either an ability to declare lambdas as static or require capture groups😅

  • @Me__Myself__and__I
    @Me__Myself__and__I 4 месяца назад +16

    This confirms my suspicion that most front end developers exist in easy mode and don't understand more technical / lower level concepts. 😂

    • @kleinesmaddy
      @kleinesmaddy 4 месяца назад

      I've worked a lot with Phaser 3 in Electron just for fun, and yeah, you really need to understand JavaScript on a completely different level. And yes, you have to worry much more about lower-level aspects of programming.
      And yeah, JS GC is an a......

    • @ipodtouch470
      @ipodtouch470 4 месяца назад +2

      Bro when he said. “I know some of y’all JS developers may not know what a garbage collection” I was actually a little shocked. I know we are building user apps but I assumed people still knew how some of it works underneath the hood.

    • @PraiseYeezus
      @PraiseYeezus 4 месяца назад +2

      yeah most front end rarely even think about that at all unless they're senior. But ask your average backend dev to debug and/or write some CSS and watch their flight or fight kick on.

    • @tantalus_complex
      @tantalus_complex 4 месяца назад +1

      ​@@PraiseYeezus Which is really unfortunate. And it even happens with some Front-Enders.
      Almost all of us were taught CSS incompletely (in terms of the system - how it _thinks_), so we pass on our knowledge incompletely.
      Rachel Andrews has done phenomenal work reintroducing the core concept of the Normal Flow of the Document, which I've found unlocks a lot of aha moments for devs, demystifying the CSS debugging and dev experience.

    • @Me__Myself__and__I
      @Me__Myself__and__I 4 месяца назад

      @@tantalus_complex Even being very technical and detail oriented CSS is just weird sometimes. You can completely undrrstand how it is SUPPOSED to work and still end up struggling to get the effect you want. Some things are easy yet other things (that should be easy like horiz center) are not intuitive.
      Though I vaguely recall that a more intuitive way to horiz center is being added. I think.

  • @avinashthakur80
    @avinashthakur80 4 месяца назад

    That's crazy. `id` is the return value of setTimeout. It is computed and has no relation to what is inside the callback.
    Why should there be any reference relation between id and the buffer ?

    • @thomassynths
      @thomassynths 4 месяца назад

      Because the runtime maps the id to the timeout somewhere. That mapping is the link

  • @nikjee
    @nikjee 4 месяца назад

    Maybe it can be fixed by using WeakRef?

  • @gavinharris8619
    @gavinharris8619 4 месяца назад

    Isn’t it due to use of a Lambda? Lambda functions bind to ‘this’, I wonder if this would have the same issue if he used a boomer function?

  • @naughtiousmaximus7853
    @naughtiousmaximus7853 4 месяца назад

    Is anything unexpected in JS world? What is next, A NEW FRAMEWORK PERHAPS?

  • @ChungusTheLarge
    @ChungusTheLarge 4 месяца назад

    "Unexpected"

  • @MrWertixvost
    @MrWertixvost 4 месяца назад

    One more f-ing thing to fear about😂😂

  • @aprilmintacpineda2713
    @aprilmintacpineda2713 4 месяца назад

    In the browser, globalThis === window

  • @Aleks-fp1kq
    @Aleks-fp1kq 4 месяца назад

    Not sure if that's a leak. The returned function could be called any number of times, what if it was a proxy there?

  • @jonathan-._.-
    @jonathan-._.- 4 месяца назад

    i dont think its about the references , but about the scope , since its theoretically available in the lambda function 🤔
    also dont think its because of a globalThis assignment , as you have demonstrated it also stays around if you return the callback,
    instead i think its like this:
    are there sub-scopes that use the variable ?
    yes ? keep reference until all sub scopes are cleared
    (tho in theory it should only be kept until the referencing sub scopes are cleared )

  • @abb0tt
    @abb0tt 4 месяца назад

    This video is more controversial than another browser vid. 🤣

  • @soyitiel
    @soyitiel 4 месяца назад

    Wait, garbage collection has Go?

  • @dgdev69
    @dgdev69 4 месяца назад +2

    Damn I never thought Theo would make this video. I read this article a month ago. When you are new to memory leak this article is excellent to learn.

    • @cameronbehar7358
      @cameronbehar7358 4 месяца назад

      It’s not-this isn’t a memory leak. It’s a misunderstanding of how closures are supposed to work in JS (and really any scripting language that uses closures). Theo is misunderstanding reference counting.

  • @sub-harmonik
    @sub-harmonik 4 месяца назад

    if you're any type of dev, 'js' or not, you should know what garbage collection is
    Ideally everyone should know some C to get some insight into memory and allocation imo, as well as looking into which kind of gc your programming language/environment uses.
    Lydia Hallie has a good video on this, I also recommend her other 'javascript visualized' videos

  • @BogdanPolonsky
    @BogdanPolonsky 4 месяца назад

    I encountered this problem while fixing memory leaks in my project. It's funny that this memory leak with closures can be fixed through `bind`:
    ```js
    function demo() {
    const bigArrayBuffer = new ArrayBuffer(100_000_000);
    globalThis.innerFunc1 = (function(bigArrayBuffer) {
    console.log(bigArrayBuffer.byteLength);
    }).bind(undefined, bigArrayBuffer);
    globalThis.innerFunc2 = () => {
    console.log('hello');
    };
    }
    demo();
    globalThis.innerFunc1();
    globalThis.innerFunc1 = undefined;
    ```
    Yes, this is very ugly code, but it works...

  • @lena-in-the-it-company
    @lena-in-the-it-company 4 месяца назад

    What really slows javascript apps, are not cleaned properly event listeners.

    • @pokefreak2112
      @pokefreak2112 4 месяца назад

      how so? they get garbage collected when the node they're bound to is removed. I almost never manually remove eventlisteners

    • @theReal_WKD
      @theReal_WKD 4 месяца назад

      @@pokefreak2112 It is common using React that a component adds an event listener to the window/document, or any node that is outside that component. If it ever unmounts, the event listener will still be bound to the outside node. And a new event listener will be added if the component remounts, increasing the memory leak every time. Just clean your event listeners as a good practice.

    • @lena-in-the-it-company
      @lena-in-the-it-company 4 месяца назад

      @@pokefreak2112 if you're using components like react or angular, in case if component rerendered without cleaning of event listeners then it would stack up until the app is slow. It really depends on the case, but I have seen this a looot.

    • @pokefreak2112
      @pokefreak2112 4 месяца назад

      @@lena-in-the-it-company Sure that's just the downside of vdom; you can't safely use the native API's anymore.
      should still by fine if you use the synthetic events provided by the framework though

  • @kajoma1782
    @kajoma1782 4 месяца назад

    um... i need to look back at my code

  • @krzysztofwisniewski8926
    @krzysztofwisniewski8926 4 месяца назад +3

    Okay, imo obvious since higher order function keep reference to outer scope for possibility to reference variables from outer scope. JS engine is not performing ahead of time analysis to determine what variables are used in higher-order functions, only at runtime it performs search through scopes (local, non-local, global) to find a variable.
    This is also the case in eg. Python

    • @dinhero21
      @dinhero21 4 месяца назад

      how is it obvious? JS is definetely doing some kind of AOT analysis since it optimizes out bigArrayBuffer when it isn't being referenced anywhere but doesn't when it is being referenced in any function, even if those are local, and if there is at least one function, even if it doesn't reference it, that is exposed.

    • @krzysztofwisniewski8926
      @krzysztofwisniewski8926 4 месяца назад

      @@dinhero21 I guess I might have exaggerated when I said it was obvious. However, fact that there are optimizations in place doesn't mean that they will hide underlying implementation everywhere. On the other hand I don't know that much about JS standard and what it implies about how scopes are handled in closures. Anyway, you are always on mercy of implementation, let's be happy that we are not talking about C++

  • @GuilhermeHCardozo
    @GuilhermeHCardozo 3 месяца назад

    You can't make Garbage collection so good in JS bc if you do it'd collect itself 🥴