Skip to content

Commit 32d49ec

Browse files
Kim DuganKim Dugan
authored andcommitted
fix(rrweb): aggressive blob cleanup / caching limit for safari GC
1 parent febc744 commit 32d49ec

File tree

1 file changed

+46
-2
lines changed

1 file changed

+46
-2
lines changed

packages/rrweb/src/record/workers/image-bitmap-data-url-worker.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import type {
88
const lastBlobMap: Map<number, string> = new Map();
99
const transparentBlobMap: Map<string, string> = new Map();
1010

11+
// Safari memory management: limit cached blob count to prevent unbounded growth
12+
const MAX_CACHED_BLOBS = 100;
13+
const MAX_TRANSPARENT_CACHE = 20;
14+
15+
// Periodic cleanup to help Safari's garbage collector
16+
let cleanupCounter = 0;
17+
const CLEANUP_INTERVAL = 50; // Run cleanup every 50 frames
18+
1119
export interface ImageBitmapDataURLRequestWorker {
1220
postMessage: (
1321
message: ImageBitmapDataURLWorkerParams,
@@ -31,11 +39,22 @@ async function getTransparentBlobFor(
3139
const id = `${width}-${height}`;
3240
if ('OffscreenCanvas' in globalThis) {
3341
if (transparentBlobMap.has(id)) return transparentBlobMap.get(id)!;
42+
43+
// Limit cache size to prevent memory growth in Safari
44+
if (transparentBlobMap.size >= MAX_TRANSPARENT_CACHE) {
45+
const firstKey = transparentBlobMap.keys().next().value;
46+
transparentBlobMap.delete(firstKey);
47+
}
48+
3449
const offscreen = new OffscreenCanvas(width, height);
3550
offscreen.getContext('2d'); // creates rendering context for `converToBlob`
36-
const blob = await offscreen.convertToBlob(dataURLOptions); // takes a while
51+
let blob: Blob | null = await offscreen.convertToBlob(dataURLOptions); // takes a while
3752
const arrayBuffer = await blob.arrayBuffer();
3853
const base64 = encode(arrayBuffer); // cpu intensive
54+
55+
// Explicitly null out blob reference to help Safari's GC
56+
blob = null;
57+
3958
transparentBlobMap.set(id, base64);
4059
return base64;
4160
} else {
@@ -51,6 +70,24 @@ worker.onmessage = async function (e) {
5170
if ('OffscreenCanvas' in globalThis) {
5271
const { id, bitmap, width, height, dataURLOptions } = e.data;
5372

73+
// Periodic cleanup to help Safari manage memory
74+
cleanupCounter++;
75+
if (cleanupCounter >= CLEANUP_INTERVAL) {
76+
cleanupCounter = 0;
77+
78+
// Limit lastBlobMap size to prevent unbounded growth
79+
if (lastBlobMap.size > MAX_CACHED_BLOBS) {
80+
const entriesToRemove = lastBlobMap.size - MAX_CACHED_BLOBS;
81+
const iterator = lastBlobMap.keys();
82+
for (let i = 0; i < entriesToRemove; i++) {
83+
const key = iterator.next().value;
84+
if (key !== undefined) {
85+
lastBlobMap.delete(key);
86+
}
87+
}
88+
}
89+
}
90+
5491
const transparentBase64 = getTransparentBlobFor(
5592
width,
5693
height,
@@ -62,11 +99,18 @@ worker.onmessage = async function (e) {
6299

63100
ctx.drawImage(bitmap, 0, 0);
64101
bitmap.close();
65-
const blob = await offscreen.convertToBlob(dataURLOptions); // takes a while
102+
let blob: Blob | null = await offscreen.convertToBlob(dataURLOptions); // takes a while
66103
const type = blob.type;
67104
const arrayBuffer = await blob.arrayBuffer();
68105
const base64 = encode(arrayBuffer); // cpu intensive
69106

107+
// Explicitly null out blob reference to help Safari's GC
108+
// This is critical for Safari to release the blob from memory
109+
blob = null;
110+
111+
// Clear the canvas to release any internal buffers
112+
ctx.clearRect(0, 0, width, height);
113+
70114
// on first try we should check if canvas is transparent,
71115
// no need to save it's contents in that case
72116
if (!lastBlobMap.has(id) && (await transparentBase64) === base64) {

0 commit comments

Comments
 (0)