Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit b87bb0a

Browse files
authored
[web] Fix canvas leak when dpi changes. Evict from BitmapCanvas cache under… (#16721)
* Fix canvas leak when dpi changes. Evict from BitmapCanvas cache under memory pressure. * optimized _reduceCanvasMemoryUsage * Addressed review comments
1 parent 1aef7a4 commit b87bb0a

File tree

2 files changed

+54
-20
lines changed

2 files changed

+54
-20
lines changed

lib/web_ui/lib/src/engine/surface/picture.dart

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ const int _kCanvasCacheSize = 30;
1919
/// Canvases available for reuse, capped at [_kCanvasCacheSize].
2020
final List<BitmapCanvas> _recycledCanvases = <BitmapCanvas>[];
2121

22+
/// Reduces recycled canvas list by 50% to reduce bitmap canvas memory use.
23+
void _reduceCanvasMemoryUsage() {
24+
final int canvasCount = _recycledCanvases.length;
25+
for (int i = 0; i < canvasCount; i++) {
26+
_recycledCanvases[i].dispose();
27+
}
28+
_recycledCanvases.clear();
29+
}
30+
2231
/// A request to repaint a canvas.
2332
///
2433
/// Paint requests are prioritized such that the larger pictures go first. This
@@ -42,22 +51,27 @@ class _PaintRequest {
4251
List<_PaintRequest> _paintQueue = <_PaintRequest>[];
4352

4453
void _recycleCanvas(EngineCanvas canvas) {
45-
if (canvas is BitmapCanvas && canvas.isReusable()) {
46-
_recycledCanvases.add(canvas);
47-
if (_recycledCanvases.length > _kCanvasCacheSize) {
48-
final BitmapCanvas removedCanvas = _recycledCanvases.removeAt(0);
49-
removedCanvas.dispose();
54+
if (canvas is BitmapCanvas) {
55+
if (canvas.isReusable()) {
56+
_recycledCanvases.add(canvas);
57+
if (_recycledCanvases.length > _kCanvasCacheSize) {
58+
final BitmapCanvas removedCanvas = _recycledCanvases.removeAt(0);
59+
removedCanvas.dispose();
60+
if (_debugShowCanvasReuseStats) {
61+
DebugCanvasReuseOverlay.instance.disposedCount++;
62+
}
63+
}
5064
if (_debugShowCanvasReuseStats) {
51-
DebugCanvasReuseOverlay.instance.disposedCount++;
65+
DebugCanvasReuseOverlay.instance.inRecycleCount =
66+
_recycledCanvases.length;
5267
}
53-
}
54-
if (_debugShowCanvasReuseStats) {
55-
DebugCanvasReuseOverlay.instance.inRecycleCount =
56-
_recycledCanvases.length;
68+
} else {
69+
canvas.dispose();
5770
}
5871
}
5972
}
6073

74+
6175
/// Signature of a function that instantiates a [PersistedPicture].
6276
typedef PersistedPictureFactory = PersistedPicture Function(
6377
double dx,
@@ -272,7 +286,6 @@ class PersistedStandardPicture extends PersistedPicture {
272286
final ui.Size canvasSize = bounds.size;
273287
BitmapCanvas bestRecycledCanvas;
274288
double lastPixelCount = double.infinity;
275-
276289
for (int i = 0; i < _recycledCanvases.length; i++) {
277290
final BitmapCanvas candidate = _recycledCanvases[i];
278291
if (!candidate.isReusable()) {
@@ -286,13 +299,21 @@ class PersistedStandardPicture extends PersistedPicture {
286299
final bool fits = candidate.doesFitBounds(bounds);
287300
final bool isSmaller = candidatePixelCount < lastPixelCount;
288301
if (fits && isSmaller) {
289-
bestRecycledCanvas = candidate;
290-
lastPixelCount = candidatePixelCount;
291-
final bool fitsExactly = candidateSize.width == canvasSize.width &&
292-
candidateSize.height == canvasSize.height;
293-
if (fitsExactly) {
294-
// No need to keep looking any more.
295-
break;
302+
// [isTooSmall] is used to make sure that a small picture doesn't
303+
// reuse and hold onto memory of a large canvas.
304+
final double requestedPixelCount = bounds.width * bounds.height;
305+
final bool isTooSmall = isSmaller &&
306+
requestedPixelCount > 1 &&
307+
(candidatePixelCount / requestedPixelCount) > 4;
308+
if (!isTooSmall) {
309+
bestRecycledCanvas = candidate;
310+
lastPixelCount = candidatePixelCount;
311+
final bool fitsExactly = candidateSize.width == canvasSize.width &&
312+
candidateSize.height == canvasSize.height;
313+
if (fitsExactly) {
314+
// No need to keep looking any more.
315+
break;
316+
}
296317
}
297318
}
298319
}

lib/web_ui/lib/src/engine/surface/recording_canvas.dart

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,6 @@ class RecordingCanvas {
183183
}
184184

185185
void drawColor(ui.Color color, ui.BlendMode blendMode) {
186-
_hasArbitraryPaint = true;
187-
_didDraw = true;
188186
_paintBounds.grow(_paintBounds.maxPaintBounds);
189187
_commands.add(PaintDrawColor(color, blendMode));
190188
}
@@ -315,6 +313,21 @@ class RecordingCanvas {
315313
}
316314

317315
void drawPath(ui.Path path, SurfacePaint paint) {
316+
if (paint.shader == null) {
317+
// For Rect/RoundedRect paths use drawRect/drawRRect code paths for
318+
// DomCanvas optimization.
319+
SurfacePath sPath = path;
320+
final ui.Rect rect = sPath.webOnlyPathAsRect;
321+
if (rect != null) {
322+
drawRect(rect, paint);
323+
return;
324+
}
325+
final ui.RRect rrect = sPath.webOnlyPathAsRoundedRect;
326+
if (rrect != null) {
327+
drawRRect(rrect, paint);
328+
return;
329+
}
330+
}
318331
_hasArbitraryPaint = true;
319332
_didDraw = true;
320333
ui.Rect pathBounds = path.getBounds();

0 commit comments

Comments
 (0)