Summary
The device video view calls URL.createObjectURL() once per frame and never calls URL.revokeObjectURL(). Every frame is permanently registered in WebKit's blob registry, so the app grows at roughly the video bitrate for as long as the view stays open — about 25–60 MB/min, without bound.
Left open overnight on a 32 GB desktop this reached 18.1 GB and locked the machine (details below). Measured directly: 2,737 blob URLs created, 0 revoked in 8.4 minutes.
Reproduced on 2.3.4 (dashboard 2.3.18) and 2.3.5 (dashboard 2.3.25). Slicing alone never leaks — with no device connection the app sits flat at ~450 MB. Growth requires the video stream, and stops the moment it is closed.
Workaround for users: don't leave the device video view open unattended, especially overnight.
Root cause
URL.createObjectURL() registers a blob in WebKit's blob registry, which lives in the network process, keyed by the URL string. That is a native strong reference — garbage collection cannot reclaim it, and only URL.revokeObjectURL() releases it. Creating one per frame without revoking leaks at frame_rate × frame_size indefinitely.
Measurement
I hooked both blob APIs in the dashboard pages through the WebKit remote inspector (Runtime.evaluate into the flutter_web targets) and sampled the app cgroup alongside the counters:
(function(){
var c=0, r=0, live=0, bytes=0;
var oc=URL.createObjectURL, orv=URL.revokeObjectURL;
URL.createObjectURL=function(o){ c++; live++;
try{ if(o&&typeof o.size==='number') bytes+=o.size; }catch(e){}
return oc.apply(URL,arguments); };
URL.revokeObjectURL=function(u){ r++; live--; return orv.apply(URL,arguments); };
window.__blobProbe=function(){ return JSON.stringify({created:c,revoked:r,live:live,bytes:bytes}); };
})()
2.3.5 flatpak, device connected, video view open, no print running, app otherwise idle:
| Elapsed |
App memory |
Blobs created |
Blobs revoked |
Live blobs |
Blob payload |
| +63 s |
+53 MB |
608 |
0 |
608 |
45.3 MB |
| +189 s |
+110 MB |
1,038 |
0 |
1,038 |
77.3 MB |
| +252 s |
+146 MB |
1,456 |
0 |
1,456 |
108.4 MB |
| +315 s |
+178 MB |
1,883 |
0 |
1,883 |
140.2 MB |
| +441 s |
+254 MB |
2,309 |
0 |
2,309 |
171.9 MB |
| +504 s |
+277 MB |
2,737 |
0 |
2,737 |
203.7 MB |
~5–10 blobs/s (the frame rate) at ~74 KB each. Retained blob payload accounts for ~74% of total app growth; the remainder is registry and decoded-frame overhead. (Two intermediate rows showed an unchanged counter where my sampler dropped a reply; the trend and the zero revocation count are unaffected.)
Supporting evidence that rules out other causes
- Not response-body caching. Over a 10-minute window the dashboard received 1.7 MB over HTTP while the app grew 602 MB — a ~350× mismatch. A cache-model change would not fix this.
- Not collectable JS garbage. Forcing
Heap.gc on all four dashboard targets freed 24 MB of 4,816 MB (0.5%), and memory stayed flat for 60 s afterwards. The retention is native.
- Strictly tied to the stream. With the video view closed, blob creation is 0/min and memory is flat (verified over ~10 minutes idle). Reopening it resumed both immediately.
- Explains the cross-process symptom. Growth shows up in the network process even though rendering happens in the web process, because the blob registry lives in the network process.
Suggested fix
Revoke each object URL once the frame has been handed to the image element:
img.onload = () => URL.revokeObjectURL(img.src);
Equivalent approaches also work: reuse a single object URL, or avoid them entirely via createImageBitmap() / drawing to a canvas.
This lives in the precompiled Flutter bundle (resources/web/flutter_web/main.dart.js), so I could not patch and verify it locally. The probe above will confirm a fix in seconds though: after the change, revoked should track created and live should stay bounded. Happy to re-run it against any test build.
Real-world impact
Snapmaker Orca was left connected overnight with the video view open on 2.3.4. By morning the machine was unusable and required a hard power-off. From the systemd-oomd candidate dump:
- The app's cgroup had reached 18.1 GB after ~20 hours.
- oomd killed Chrome (3.4 GB, 62 processes) instead — it selects on memory pressure and reclaim activity, not absolute size, so the actual offender survived.
- The system then swap-thrashed for ~3.5 hours (input lag, D-Bus timeouts) until power-off.
On 2.3.5 the same pattern is slower but unbounded: a later session reached a 19.4 GB footprint (7.7 GB resident + 11.7 GB swapped) over ~7 hours, consuming half the machine's 24 GB swap file.
Environment
- Snapmaker Orca 2.3.4 and 2.3.5 Linux flatpak, x86_64, from the release bundles (
Snapmaker_Orca-Linux-flatpak_V2.3.4_x86_64.flatpak, ..._V2.3.5_x86_64.flatpak)
- Bundled dashboard versions from
files/share/Snapmaker_Orca/web/flutter_web/version.json: 2.3.18 (in 2.3.4), 2.3.25 (in 2.3.5)
- Ubuntu 24.04.4, kernel 6.17.0-40-generic, 32 GB RAM, 24 GB swap
- Printer: Snapmaker U1 over LAN (clean network; no OS-level disconnects logged)
Method: app cgroup sampled every 60 s (memory.current + memory.swap.current + per-process RSS from cgroup.procs). Note that RSS alone understates the leak once swapping begins.
Notes
Summary
The device video view calls
URL.createObjectURL()once per frame and never callsURL.revokeObjectURL(). Every frame is permanently registered in WebKit's blob registry, so the app grows at roughly the video bitrate for as long as the view stays open — about 25–60 MB/min, without bound.Left open overnight on a 32 GB desktop this reached 18.1 GB and locked the machine (details below). Measured directly: 2,737 blob URLs created, 0 revoked in 8.4 minutes.
Reproduced on 2.3.4 (dashboard 2.3.18) and 2.3.5 (dashboard 2.3.25). Slicing alone never leaks — with no device connection the app sits flat at ~450 MB. Growth requires the video stream, and stops the moment it is closed.
Workaround for users: don't leave the device video view open unattended, especially overnight.
Root cause
URL.createObjectURL()registers a blob in WebKit's blob registry, which lives in the network process, keyed by the URL string. That is a native strong reference — garbage collection cannot reclaim it, and onlyURL.revokeObjectURL()releases it. Creating one per frame without revoking leaks atframe_rate × frame_sizeindefinitely.Measurement
I hooked both blob APIs in the dashboard pages through the WebKit remote inspector (
Runtime.evaluateinto theflutter_webtargets) and sampled the app cgroup alongside the counters:2.3.5 flatpak, device connected, video view open, no print running, app otherwise idle:
~5–10 blobs/s (the frame rate) at ~74 KB each. Retained blob payload accounts for ~74% of total app growth; the remainder is registry and decoded-frame overhead. (Two intermediate rows showed an unchanged counter where my sampler dropped a reply; the trend and the zero revocation count are unaffected.)
Supporting evidence that rules out other causes
Heap.gcon all four dashboard targets freed 24 MB of 4,816 MB (0.5%), and memory stayed flat for 60 s afterwards. The retention is native.Suggested fix
Revoke each object URL once the frame has been handed to the image element:
Equivalent approaches also work: reuse a single object URL, or avoid them entirely via
createImageBitmap()/ drawing to a canvas.This lives in the precompiled Flutter bundle (
resources/web/flutter_web/main.dart.js), so I could not patch and verify it locally. The probe above will confirm a fix in seconds though: after the change,revokedshould trackcreatedandliveshould stay bounded. Happy to re-run it against any test build.Real-world impact
Snapmaker Orca was left connected overnight with the video view open on 2.3.4. By morning the machine was unusable and required a hard power-off. From the
systemd-oomdcandidate dump:On 2.3.5 the same pattern is slower but unbounded: a later session reached a 19.4 GB footprint (7.7 GB resident + 11.7 GB swapped) over ~7 hours, consuming half the machine's 24 GB swap file.
Environment
Snapmaker_Orca-Linux-flatpak_V2.3.4_x86_64.flatpak,..._V2.3.5_x86_64.flatpak)files/share/Snapmaker_Orca/web/flutter_web/version.json: 2.3.18 (in 2.3.4), 2.3.25 (in 2.3.5)Method: app cgroup sampled every 60 s (
memory.current+memory.swap.current+ per-process RSS fromcgroup.procs). Note that RSS alone understates the leak once swapping begins.Notes
mqttDisconnect error: Exception: -1: id is illegalon each cycle. 2.3.5 fixes the flapping — zero disconnects across 100+ minutes — but not the leak.