<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE bugzilla SYSTEM "https://bugs.webkit.org/page.cgi?id=bugzilla.dtd">

<bugzilla version="5.0.4.1"
          urlbase="https://bugs.webkit.org/"
          
          maintainer="admin@webkit.org"
>

    <bug>
          <bug_id>172728</bug_id>
          
          <creation_ts>2017-05-30 13:32:25 -0700</creation_ts>
          <short_desc>Page that allocates and destroys canvas elements in a loop gets jettisoned on iOS</short_desc>
          <delta_ts>2017-05-31 13:45:22 -0700</delta_ts>
          <reporter_accessible>1</reporter_accessible>
          <cclist_accessible>1</cclist_accessible>
          <classification_id>1</classification_id>
          <classification>Unclassified</classification>
          <product>WebKit</product>
          <component>Canvas</component>
          <version>WebKit Local Build</version>
          <rep_platform>Unspecified</rep_platform>
          <op_sys>Unspecified</op_sys>
          <bug_status>NEW</bug_status>
          <resolution></resolution>
          
          
          <bug_file_loc>http://jsfiddle.net/vcubzdgo/1/</bug_file_loc>
          <status_whiteboard></status_whiteboard>
          <keywords>InRadar</keywords>
          <priority>P2</priority>
          <bug_severity>Normal</bug_severity>
          <target_milestone>---</target_milestone>
          
          
          <everconfirmed>1</everconfirmed>
          <reporter name="Simon Fraser (smfr)">simon.fraser</reporter>
          <assigned_to name="Nobody">webkit-unassigned</assigned_to>
          <cc>dino</cc>
    
    <cc>joepeck</cc>
    
    <cc>msaboff</cc>
    
    <cc>simon.fraser</cc>
    
    <cc>webkit-bug-importer</cc>
          

      

      

      

          <comment_sort_order>oldest_to_newest</comment_sort_order>  
          <long_desc isprivate="0" >
    <commentid>1313839</commentid>
    <comment_count>0</comment_count>
    <who name="Simon Fraser (smfr)">simon.fraser</who>
    <bug_when>2017-05-30 13:32:25 -0700</bug_when>
    <thetext>Load this page
https://github.com/kangax/fabric.js/issues/3693

on iPad, and the tab will get killed because it goes above the jetsam high watermark.

It&apos;s allocating and destroying canvas elements in a loop, so ideally would not get jettisoned. Why is GC not kicking in?</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>1313848</commentid>
    <comment_count>1</comment_count>
    <who name="Simon Fraser (smfr)">simon.fraser</who>
    <bug_when>2017-05-30 13:41:10 -0700</bug_when>
    <thetext>The repro url is http://jsfiddle.net/vcubzdgo/1/</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>1313849</commentid>
    <comment_count>2</comment_count>
    <who name="Radar WebKit Bug Importer">webkit-bug-importer</who>
    <bug_when>2017-05-30 13:41:30 -0700</bug_when>
    <thetext>&lt;rdar://problem/32470753&gt;</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>1313947</commentid>
    <comment_count>3</comment_count>
    <who name="Simon Fraser (smfr)">simon.fraser</who>
    <bug_when>2017-05-30 17:09:19 -0700</bug_when>
    <thetext>From the web inspector, and via code instrumentation, WebKit is retaining all the canvas elements and their image buffers while the loop is running. Chrome seems to be able to GC them as the loop runs.</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>1313963</commentid>
    <comment_count>4</comment_count>
    <who name="Simon Fraser (smfr)">simon.fraser</who>
    <bug_when>2017-05-30 17:46:37 -0700</bug_when>
    <thetext>This is something about fabric&apos;s addListener(fabric.window, &apos;resize&apos;, this._onResize); code.</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>1313964</commentid>
    <comment_count>5</comment_count>
    <who name="Simon Fraser (smfr)">simon.fraser</who>
    <bug_when>2017-05-30 17:59:50 -0700</bug_when>
    <thetext>If I comment out the line in fabric.js, the bug goes away:

  this._onResize = this._onResize.bind(this);</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>1314087</commentid>
    <comment_count>6</comment_count>
    <who name="Joseph Pecoraro">joepeck</who>
    <bug_when>2017-05-30 21:56:55 -0700</bug_when>
    <thetext>I think this is primarily a programming error in the page that will cause the &quot;fabric.Canvas&quot; instances to be kept alive by leaked global resize listeners being registered.

I boiled it down to:

    &lt;script&gt;
    MyCanvas = function() {
        this.initialize();
    };

    MyCanvas.prototype.initialize = function() {
        this.a = document.createElement(&apos;canvas&apos;);
        this.f = (function() {}).bind(this); // 2nd time this will overwrite &quot;this.f&quot;
        window.addEventListener(&apos;resize&apos;, this.f);
    };

    MyCanvas.prototype.dispose = function() {
        window.removeEventListener(&apos;resize&apos;, this.f);
    };

    window.addEventListener(&quot;load&quot;, function() {
        var canvas;
        for (var count = 0; count &lt; 100; count++){
            console.log(&quot;count&quot;, count);
            canvas = new MyCanvas(); // initialize once
            canvas.initialize(); // initialize again
            canvas.dispose();
        }
    }, false);
    &lt;/script&gt;

It looks like the page effectively calls initialize twice. This means:

  1. First call to initialize happens via new fabric.Canvas constructor:
      - creates the 1st this._onResize = this._onResize.bind()
      - adds this as a &quot;resize&quot; listener
      - note this constructor is generated within fabric.util.createClass as this.initialize.apply(this, args)

  2. Second call to initialize is explicit in the loop
      - overwrites the 1st this._onResize with a second
      - adds this as a &quot;resize&quot; listener

  3. Dispose clears the second this._onResize

Nobody ever clears the first resize listener added to the window as part of (1). And nobody can do that as nobody holds a reference to the function to the original onResize before it was rewritten. The reason why the resize handler is significant is because it is on the window, therefore global and will stay around forever unless removed. The others are localized to an element and can be collected with the element.

---

Compare to this fiddle:
http://jsfiddle.net/hjnhfk5L/

Before:

&gt; for(var count = 1; count &lt; 100; count++){
&gt;    console.log(&quot;count&quot;, count);
&gt;    canvas = new fabric.Canvas();
&gt;    canvas.initialize(canvasElement, {height:2000, width: 2000});
&gt;    canvas.add(rect);
&gt;    canvas.dispose();
&gt; }

After:

&gt; for (var count = 1; count &lt; 100; count++){
&gt;     console.log(&quot;count&quot;, count);
&gt;     canvas = new fabric.Canvas(canvasElement, {height:2000, width: 2000});
&gt;     canvas.add(rect);
&gt;     canvas.dispose();
&gt; }

This doesn&apos;t call initialize twice, and doesn&apos;t have the same leaks.

---

Now, even after diagnosing this as the issue doesn&apos;t mean we can&apos;t make a change to WebKit to improve WebKit&apos;s behavior in these situations. I don&apos;t think any of these Canvas elements are actually attached to the page, so maybe we can free some of the memory held by them.</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>1314088</commentid>
    <comment_count>7</comment_count>
    <who name="Simon Fraser (smfr)">simon.fraser</who>
    <bug_when>2017-05-30 22:08:38 -0700</bug_when>
    <thetext>Thanks for the analysis, Joe! I wonder why Chrome doesn&apos;t show the same memory growth.</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>1314301</commentid>
    <comment_count>8</comment_count>
    <who name="Simon Fraser (smfr)">simon.fraser</who>
    <bug_when>2017-05-31 13:45:22 -0700</bug_when>
    <thetext>I filed an issue on Fabric.js:
https://github.com/kangax/fabric.js/issues/3971</thetext>
  </long_desc>
      
      

    </bug>

</bugzilla>