1

I was recently working with <canvas> in JavaScript, and discovered the possibility to create a really bad "memory leak" (more like a memory explosion). When working with the canvas context, you have the ability to do context.save() to add the drawing styles to the "state stack" and context.restore() to remove it. (See the documentation for the rendering context on MDN.)

The problem occurs when you happen to continually save to the state stack without restoring. In Chrome v50 and Firefox v45 this seems to just take up more and more private memory, eventually crashing the browser tab. (Incidentally the JavaScript memory is unaffected in Chrome, so it's very hard to debug this with the profiler/timeline tools.)

My question: How can you clear out or delete the state stack for a canvas context? With a normal array, you would be able to check on the length, trim it with splice or simply reset is back to empty [], but I haven't seen a way to do any of this with the state stack.

Luke
  • 18,811
  • 16
  • 99
  • 115
  • I don't think you can really have the "length" of the stack. To avoid it, simply remember that each time you don't need the saved state anymore, you **have to** `restore` it. (also note that it will save the whole properties of your context, when most of the time you just need to save one or two, I personally avoid as much as possible the call to this heavy method). So the answer to your question is already in your question. – Kaiido May 01 '16 at 08:02
  • 1
    I'm looking for a solution that might help with debugging if "simply remember" has failed. Imagine you are just given a context... how would you clean it up or be able to detect if there is an ongoing problem? – Luke May 05 '16 at 22:00
  • Bonus points if anyone can explain why the Private memory increases rather than the JavaScript memory in Chrome. That makes it very difficult to diagnose. – Luke May 05 '16 at 22:03

1 Answers1

1

[I].. discovered the possibility to create a really bad "memory leak"

This is technically not a memory leak. A leak would be to allocate memory and loose the pointer to it so it could not be freed. In this case the pointer is tracked but the memory not released.

The problem occurs when you happen to continually save to the state stack without restoring.

That is to be expected. Allocating memory without freeing it will accumulate allocated memory blocks.

How can you clear out or delete the state stack for a canvas context?

The only way is to either restore all saved states, or to reset the context by setting some size to the canvas element (ie. canvas.width = canvas.width).

It's also safe to call restore() more times than save() (in which case it just returns without doing anything) so you could in theory run it through a loop of n number of iterations. This latter would be more in the bad practice category though.

But with that being said: if there is a mismatch in numbers of save and restore when it's suppose to be equal, usually indicates a problem somewhere else in the code. Working around the problem with a reset or running multiple restores in post probably will only contribute to cover up the actual problem.

Here's an example on how to track the count of save/restore calls -

// NOTE: this code needs to run before a canvas context is created
CanvasRenderingContext2D.prototype.__save = CanvasRenderingContext2D.prototype.save;
CanvasRenderingContext2D.prototype.__restore = CanvasRenderingContext2D.prototype.restore;

// Our patch vectors
CanvasRenderingContext2D.prototype.__tracker = 0;
CanvasRenderingContext2D.prototype.save = function() {
  this.__tracker++;
  console.log("Track save:", this.__tracker);
  this.__save() 
}

CanvasRenderingContext2D.prototype.restore = function() {
  this.__tracker--;
  console.log("Track restore:", this.__tracker);
  this.__restore() 
}

// custom method to dump status
CanvasRenderingContext2D.prototype.trackstat = function() {
  if (this.__tracker)
    console.warn("Track stat:", this.__tracker);
  else
    console.log("Track stat: OK");
}

var ctx = document.createElement("canvas").getContext("2d");
ctx.save();                     // do a couple of save()s
ctx.save();
ctx.restore();                  // single restore()
ctx.trackstat();                // should report mismatch of 1
ctx.restore();                  // last restore()
ctx.trackstat();                // should report OK
  • "we need to make sure we keep track of number of pushes (or saves) as well as pops (restores) ourselves" <-- That is certainly best practice, no arguments there! But we could make a mistake or be using code written by someone who wasn't as careful. Ideally I was hoping for a way to detect this problem, and reverse it if detected. If this is true - "We don't have access to the stack pointer" - then our options are limited. – Luke May 05 '16 at 21:55
  • @Luke access is indeed limited. You could "patch" the calls to keep temporary track for debugging purposes. I'll add an example on how to do this, but this is of course like taking a flu shot for pain in the toe. The problem indicates design problems. –  May 06 '16 at 00:03
  • No, this really is a memory leak. Even technically! – Ry- Jan 17 '18 at 22:39
  • @Ryan no, a leak is "lost" memory. *Holding onto* memory that you can free at any time is just bad practice, not a leak (and it's very difficult to produce actual memory leaks in a language such as JS unless there is a deliberate attempt or a browser bug involved). –  Jan 17 '18 at 22:55
  • @K3N: A leak is memory that isn’t freed when it’s no longer being used. This fits the bill exactly. The view that it’s hard to make memory leaks in JS is just… wrong. – Ry- Jan 17 '18 at 23:10
  • I wasn't gonna nitty-gritty on this, but you're using the definition coming from the computer science camp, which is initially very broad. The classic definition of a leak has always been memory that becomes *unusable*/*unreachable* (i.e. "lost", usually because of lost references/pointers). Even the cs definition goes into that area if you go past the initial broad definition. Here that is not the problem - The problem is that the code doesn't properly pop the stack still referencing some memory, but memory that is still *reachable* and regainable within the app (i.e. not lost/leaked). –  Jan 18 '18 at 01:25
  • @K3N: I was going to do it right away (because the first 40% of your answer is wrong and unhelpful), but figured I’d explain and give you a chance to fix it! Sorry. – Ry- Jan 18 '18 at 16:42