tweakpane / plugin-essentials

Essential components for Tweakpane
https://cocopon.github.io/tweakpane/plugins/#essentials
MIT License
67 stars 5 forks source link

Performance issues when quickly updating cubic bezier values #18

Open kitschpatrol opened 7 months ago

kitschpatrol commented 7 months ago

Thank you for this superb library!

I noticed creeping memory usage when repeatedly setting the value of a cubicbezier blade.

A minimal (but admittedly extreme) reproduction is something like this. After about five minutes, I see several hundred megabytes of usage in the tab.

Maybe CubicBezier value objects are being retained somewhere internally? Manually invoking GC helps a bit but still doesn't completely stop the accumulation.

<script type="module">
  import * as Tweakpane from 'https://unpkg.com/tweakpane@4.0.1/dist/tweakpane.js';
  import * as EssentialsPlugin from 'https://unpkg.com/@tweakpane/plugin-essentials@0.2.0/dist/tweakpane-plugin-essentials.js';

  const pane = new Tweakpane.Pane({ title: 'Cubic Bezier Stress Test' });
  pane.registerPlugin(EssentialsPlugin);

  const bezierApi = pane.addBlade({
    view: 'cubicbezier',
    value: [0, 0, 0, 0],
  });

  setInterval(() => {
      bezierApi.value = new EssentialsPlugin.CubicBezier(
        Math.random(),
        Math.random(),
        Math.random(),
        Math.random()
      );
  }, 4);
</script>
cocopon commented 6 months ago

Thank you for reporting the issue. I've tried to reproduce the issue with your code but I can't.

What is your environment? Here is mine:

kitschpatrol commented 6 months ago

Thank you very much for looking into this — I've updated my browser since I opened the issue, and when re-testing today I'm not able to reproduce any memory accumulation either, so that doesn't seem to be the issue.

I realize I should have explained why I was looking into a possible leak in the first place: I had observed frame rate slow-downs in tabs that have been open for a long time with regularly-updated CubicBezier controls.

My memory leak hypothesis was wrong, apologies — but I investigated the issue a bit further today, and think I found the cause.

If the time between setting the .value on the CubicBezier API is less than the duration of the preview animation (~1400ms?), then the un-cancelled calls to requestAnimationFrame in cubic-bezier-preview.ts keep accumulating and firing, eventually leading to the frame rate slowdown.

Tracking the animation frame ID and then cancelling the last requestAnimationFrame in stop() seems to fix this. I'm happy to share a PR with the change.

Here's a video (of a very exaggerated scenario) that demonstrates the performance difference. The plugin-essentials version 0.2.1 is shown at left, and a fork with requestAnimationFrame cancellation is shown at right. (Tested on the latest Chrome running on macOS 14.2.)

https://github.com/tweakpane/plugin-essentials/assets/194164/c0f61efd-0e96-4f2d-b3ae-8d713f522efd

Here's the test code. I'm using a bunch of blades to demonstrate the effect quickly, but even with a single blade the slowdown will eventually occur as long as the value update interval is < 1400ms.

<script type="module">
  import * as Tweakpane from "https://unpkg.com/tweakpane@4.0.2/dist/tweakpane.js";
  import * as EssentialsPlugin from "https://unpkg.com/@tweakpane/plugin-essentials@0.2.1/dist/tweakpane-plugin-essentials.js";

  const pane = new Tweakpane.Pane({ title: "Cubic Bezier Stress Test 2" });
  pane.registerPlugin(EssentialsPlugin);
  const fpsGraph = pane.addBlade({ view: "fpsgraph" });

  const cbCount = 12;
  const cbs = [];
  for (let i = 0; i < cbCount; i++) {
    cbs.push(
      pane.addBlade({
        view: "cubicbezier",
        value: [0, 0, 0, 0],
        picker: "inline",
        expanded: true,
      })
    );
  }

  function render() {
    fpsGraph.begin();
    for (const cb of cbs) {
      cb.value = new EssentialsPlugin.CubicBezier(
        Math.random(),
        Math.random(),
        Math.random(),
        Math.random()
      );
    }
    fpsGraph.end();
    requestAnimationFrame(render);
  }

  render();
</script>

I'll re-title the issue to better reflect the observed behavior rather than a possible cause.