Skip to content

Commit 0ccef6c

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

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 worker messages
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,27 @@ 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 (
76+
cleanupCounter >= CLEANUP_INTERVAL ||
77+
lastBlobMap.size > MAX_CACHED_BLOBS
78+
) {
79+
cleanupCounter = 0;
80+
81+
// Limit lastBlobMap size to prevent unbounded growth
82+
if (lastBlobMap.size > MAX_CACHED_BLOBS) {
83+
const entriesToRemove = lastBlobMap.size - MAX_CACHED_BLOBS;
84+
const iterator = lastBlobMap.keys();
85+
for (let i = 0; i < entriesToRemove; i++) {
86+
const key = iterator.next().value;
87+
if (key !== undefined) {
88+
lastBlobMap.delete(key);
89+
}
90+
}
91+
}
92+
}
93+
5494
const transparentBase64 = getTransparentBlobFor(
5595
width,
5696
height,
@@ -62,11 +102,15 @@ worker.onmessage = async function (e) {
62102

63103
ctx.drawImage(bitmap, 0, 0);
64104
bitmap.close();
65-
const blob = await offscreen.convertToBlob(dataURLOptions); // takes a while
105+
let blob: Blob | null = await offscreen.convertToBlob(dataURLOptions); // takes a while
66106
const type = blob.type;
67107
const arrayBuffer = await blob.arrayBuffer();
68108
const base64 = encode(arrayBuffer); // cpu intensive
69109

110+
// Explicitly null out blob reference to help Safari's GC
111+
// This is critical for Safari to release the blob from memory
112+
blob = null;
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)