Reviewer leaks ~70 MB/min of graphics memory while an animated GIF is on screen (macOS) — repaint-rate threshold identified

Summary

On macOS, the reviewer webview leaks graphics memory continuously — roughly 70 MB/min on this machine — for as long as a card containing an animated GIF is displayed. No interaction is needed; the process footprint grows until the system starts compressing/swapping (observed past 5.5 GB on an 8 GB machine), and quitting Anki then hangs for over a minute tearing down the graphics context.

The trigger is not GIFs specifically but repaint rate: the same content repainting at ~30 fps leaks at the same rate, while repainting at 20 fps or less measures flat. Details and data below.

Environment

  • Anki 26.5 (official build, Qt6/QtWebEngine)
  • macOS 26.5.2 (build 25F84), MacBook Air (Apple Silicon), 8 GB RAM
  • Reproduced with the add-on that first exposed it disabled; a plain Basic card is sufficient

Minimal reproduction

  1. Create a Basic card whose Front contains one animated GIF (ours: 480x480, 2.4 MB, 599 frames, 30 ms/frame — but a small ~100 KB GIF leaks at the same rate).
  2. Review the card and leave it on screen. The Anki window must be visible on the active desktop — an occluded window (other Space, minimized) throttles rendering and the leak pauses, which can hide the problem during testing.
  3. Sample footprint -p <anki pid> every 15 s for 4 minutes and fit a slope. (Use footprint, not ps RSS — RSS reads ~130 MB while the true footprint is gigabytes once macOS starts compressing the process.)

Measured: +69.8 MB/min, linear, zero interaction. With a static image instead: ~0 MB/min. The rate looks independent of GIF file size and pixel area, consistent with a fixed per-repaint loss.

The repaint-rate threshold

To separate GIF decoding from repainting, we replaced the GIF with JavaScript that decodes the frames once (WebCodecs ImageDecoder) and draws them onto a <canvas> in a setTimeout loop, then varied only the draw interval. Same content, window visible throughout:

Repaint interval Rate Footprint slope
native GIF playback (30-40 ms frames) ~30 fps +68 to +70 MB/min
canvas, 30-40 ms ~30 fps +70.4 MB/min
canvas, >= 50 ms <= 20 fps +6.2 MB/min (flat)
canvas, 100 ms 10 fps -15.5 MB/min (flat)
static content 0 ~0 MB/min

So the leak sits in the compositing path, engaged above some repaint rate between 20 and 30 fps on this machine — not in the GIF decoder.

Where the memory goes

vmmap at a 5156 MB footprint: MALLOC_SMALL 3719 MB across 1003 regions, IOSurface 555 MB, IOAccelerator (GPU) 369 MB, graphics-unmapped 105 MB.

Related symptom: quit hang

After the footprint has grown, quitting Anki hangs. macOS wrote a hang report (81.94 s, footprint 5484 MB) with all 128 samples of the main thread inside -[NSOpenGLContext dealloc]CGLReleaseContextgliDestroyContext — i.e. teardown of the accumulated graphics state. The longer a session with an animated GIF, the longer the quit hang.

Notes

  • Raw sample TSVs (timestamp, footprint MB) for every run are available, as is the full hang report.
  • Workaround we ship in the triggering add-on: animate via canvas capped at >= 50 ms/frame — flat memory even during active reviewing (+9.7 MB/min while answering cards, within measurement noise).
  • Happy to run diagnostics or test builds on this machine.

Interesting. I cannot reproduce this on linux, maybe this info helps debugging?

I was not able to reproduce the reported behavior.

  • M1 Mac mini 2020, macOS Tahoe 26.6.1, Anki 26.08.1, Video driver: Metal (default).
  • Using the animated GIF on this page. 320 x 240 px, 41 frames, 0.08 delay time, 0.08 unclamped delay time.
  • I reviewed the card, leaving it in the window in the unanswered state (with the “Show Answer” button at the bottom).

First test:
(I didn’t keep strict track of time during the early part of testing.)
Before testing, the footprint was 214 MB.
After starting testing, the footprint jumped to 243 MB.
Within a minute, it dropped to 238 MB.
A few minutes later, it dropped to 209 MB.
A few minutes later: 208 MB.
At that point, I started keeping track of timestamps.
2 minutes later: 209 MB.
1 minute later: 212 MB.
45 seconds later: 210 MB.
For the next 5 minutes, it fluctuated between 209-210 MB.
6 minutes later, it jumped to 237 MB.
For the next 10 minutes, it fluctuated between 236-237 MB.
3 minutes later: 237 MB.
3 minutes later: 237 MB.
13 minutes later, 237 MB.

Second test:
Before pressing “Study Now” to view the card: 160 MB
Immediately after pressing “Study Now” and viewing the card: 193 MB
2 minutes later: 189 MB
5 minutes later: 192 MB
5 minutes later: 192 MB
9 minutes later: 191 MB

Memory leaks are unfortunately a common report on macOS:

I’m not sure if there’s anything Anki can do if it’s a Qt issue.