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

Commit 4be3a03

Browse files
authored
[web] Reuse ImageElement(s) across frames (#18437)
* reuse images across frames * Change implementation to CrossFrameCache at picture level * Update licenses_flutter
1 parent e4cee54 commit 4be3a03

File tree

7 files changed

+217
-29
lines changed

7 files changed

+217
-29
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/conic.dart
438438
FILE: ../../../flutter/lib/web_ui/lib/src/engine/dom_canvas.dart
439439
FILE: ../../../flutter/lib/web_ui/lib/src/engine/dom_renderer.dart
440440
FILE: ../../../flutter/lib/web_ui/lib/src/engine/engine_canvas.dart
441+
FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart
441442
FILE: ../../../flutter/lib/web_ui/lib/src/engine/history.dart
442443
FILE: ../../../flutter/lib/web_ui/lib/src/engine/houdini_canvas.dart
443444
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html_image_codec.dart

lib/web_ui/lib/src/engine.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ part 'engine/conic.dart';
5656
part 'engine/dom_canvas.dart';
5757
part 'engine/dom_renderer.dart';
5858
part 'engine/engine_canvas.dart';
59+
part 'engine/frame_reference.dart';
5960
part 'engine/history.dart';
6061
part 'engine/houdini_canvas.dart';
6162
part 'engine/html_image_codec.dart';

lib/web_ui/lib/src/engine/bitmap_canvas.dart

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class BitmapCanvas extends EngineCanvas {
2626
}
2727

2828
ui.Rect _bounds;
29+
CrossFrameCache<html.HtmlElement> _elementCache;
2930

3031
/// The amount of padding to add around the edges of this canvas to
3132
/// ensure that anti-aliased arcs are not clipped.
@@ -116,6 +117,11 @@ class BitmapCanvas extends EngineCanvas {
116117
_setupInitialTransform();
117118
}
118119

120+
/// Setup cache for reusing DOM elements across frames.
121+
set elementCache(CrossFrameCache<html.HtmlElement> cache) {
122+
_elementCache = cache;
123+
}
124+
119125
void _updateRootElementTransform() {
120126
// Flutter emits paint operations positioned relative to the parent layer's
121127
// coordinate system. However, canvas' coordinate system's origin is always
@@ -354,6 +360,26 @@ class BitmapCanvas extends EngineCanvas {
354360
_cachedLastStyle = null;
355361
}
356362

363+
html.ImageElement _reuseOrCreateImage(HtmlImage htmlImage) {
364+
final String cacheKey = htmlImage.imgElement.src;
365+
if (_elementCache != null) {
366+
html.ImageElement imageElement = _elementCache.reuse(cacheKey);
367+
if (imageElement != null) {
368+
return imageElement;
369+
}
370+
}
371+
// Can't reuse, create new instance.
372+
html.ImageElement newImageElement = htmlImage.cloneImageElement();
373+
if (_elementCache != null) {
374+
_elementCache.cache(cacheKey, newImageElement, _onEvictElement);
375+
}
376+
return newImageElement;
377+
}
378+
379+
static void _onEvictElement(html.HtmlElement element) {
380+
element.remove();
381+
}
382+
357383
html.HtmlElement _drawImage(
358384
ui.Image image, ui.Offset p, SurfacePaintData paint) {
359385
final HtmlImage htmlImage = image;
@@ -363,7 +389,7 @@ class BitmapCanvas extends EngineCanvas {
363389
html.HtmlElement imgElement;
364390
if (colorFilterBlendMode == null) {
365391
// No Blending, create an image by cloning original loaded image.
366-
imgElement = htmlImage.cloneImageElement();
392+
imgElement = _reuseOrCreateImage(htmlImage);
367393
} else {
368394
switch (colorFilterBlendMode) {
369395
case ui.BlendMode.colorBurn:
@@ -596,7 +622,7 @@ class BitmapCanvas extends EngineCanvas {
596622
html.Element.html(svgFilter, treeSanitizer: _NullTreeSanitizer());
597623
rootElement.append(filterElement);
598624
_children.add(filterElement);
599-
final html.HtmlElement imgElement = image.cloneImageElement();
625+
final html.HtmlElement imgElement = _reuseOrCreateImage(image);
600626
imgElement.style.filter = 'url(#_fcf${_filterIdCounter})';
601627
if (colorFilterBlendMode == ui.BlendMode.saturation) {
602628
imgElement.style.backgroundColor = colorToCssString(filterColor);
@@ -787,6 +813,7 @@ class BitmapCanvas extends EngineCanvas {
787813
void endOfPaint() {
788814
assert(_saveCount == 0);
789815
_canvasPool.endOfPaint();
816+
_elementCache?.commitFrame();
790817
}
791818
}
792819

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// @dart = 2.6
6+
part of engine;
7+
8+
/// A monotonically increasing frame number being rendered.
9+
///
10+
/// Used for debugging only.
11+
int _debugFrameNumber = 1;
12+
13+
List<FrameReference<dynamic>> _frameReferences = <FrameReference<dynamic>>[];
14+
15+
/// A temporary reference to a value of type [V].
16+
///
17+
/// The value automatically gets set to null after the current frame is
18+
/// rendered.
19+
///
20+
/// It is useful to think of this as a weak reference that's scoped to a
21+
/// single frame.
22+
class FrameReference<V> {
23+
/// Creates a frame reference to a value.
24+
FrameReference([this.value]) {
25+
_frameReferences.add(this);
26+
}
27+
28+
/// The current value of this reference.
29+
V value;
30+
}
31+
32+
/// Cache where items cached before frame(N) is committed, can be reused in
33+
/// frame(N+1) and are evicted if not.
34+
///
35+
/// A typical use case is image elements. As images are created and added to
36+
/// DOM when painting a scene they are cached and if possible reused on next
37+
/// update. If the next update does not reuse the element, it is evicted.
38+
///
39+
/// Maps are lazily allocated since many pictures don't contain cachable images
40+
/// at all.
41+
class CrossFrameCache<T> {
42+
// Cached items in a scene.
43+
Map<String, List<_CrossFrameCacheItem<T>>> _cache;
44+
45+
// Cached items that have been committed, ready for reuse on next frame.
46+
Map<String, List<_CrossFrameCacheItem<T>>> _reusablePool;
47+
48+
// Called when a scene or picture update is committed.
49+
void commitFrame() {
50+
// Evict unused items from prior frame.
51+
if (_reusablePool != null) {
52+
for (List<_CrossFrameCacheItem<T>> items in _reusablePool.values) {
53+
for (_CrossFrameCacheItem<T> item in items) {
54+
item.evict();
55+
}
56+
}
57+
}
58+
// Move cached items to reusable pool.
59+
_reusablePool = _cache;
60+
_cache = null;
61+
}
62+
63+
/// Caches an item for reuse on next update frame.
64+
///
65+
/// Duplicate keys are allowed. For example the same image url may be used
66+
/// to create multiple instances of [ImageElement] to be reused in the future.
67+
void cache(String key, T value, [CrossFrameCacheEvictCallback<T> callback]) {
68+
_addToCache(key, _CrossFrameCacheItem<T>(value, callback));
69+
}
70+
71+
void _addToCache(String key, _CrossFrameCacheItem<T> item) {
72+
_cache ??= {};
73+
(_cache[key] ??= [])..add(item);
74+
}
75+
76+
/// Given a key, consumes an item that has been cached in a prior frame.
77+
T reuse(String key) {
78+
if (_reusablePool == null) {
79+
return null;
80+
}
81+
List<_CrossFrameCacheItem<T>> items = _reusablePool[key];
82+
if (items == null || items.isEmpty) {
83+
return null;
84+
}
85+
_CrossFrameCacheItem<T> item = items.removeAt(0);
86+
_addToCache(key, item);
87+
return item.value;
88+
}
89+
}
90+
91+
class _CrossFrameCacheItem<T> {
92+
final T value;
93+
final CrossFrameCacheEvictCallback<T> evictCallback;
94+
_CrossFrameCacheItem(this.value, this.evictCallback);
95+
void evict() {
96+
if (evictCallback != null) {
97+
evictCallback(value);
98+
}
99+
}
100+
}
101+
102+
typedef CrossFrameCacheEvictCallback<T> = void Function(T value);

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,16 +218,13 @@ class PersistedStandardPicture extends PersistedPicture {
218218
return bitmapCanvas.bitmapPixelCount;
219219
}
220220

221-
FrameReference<bool> _didApplyPaint = FrameReference<bool>(false);
222-
223221
@override
224222
void applyPaint(EngineCanvas oldCanvas) {
225223
if (picture.recordingCanvas.hasArbitraryPaint) {
226224
_applyBitmapPaint(oldCanvas);
227225
} else {
228226
_applyDomPaint(oldCanvas);
229227
}
230-
_didApplyPaint.value = true;
231228
}
232229

233230
void _applyDomPaint(EngineCanvas oldCanvas) {
@@ -335,13 +332,15 @@ class PersistedStandardPicture extends PersistedPicture {
335332
DebugCanvasReuseOverlay.instance.reusedCount++;
336333
}
337334
bestRecycledCanvas.bounds = bounds;
335+
bestRecycledCanvas.elementCache = _elementCache;
338336
return bestRecycledCanvas;
339337
}
340338

341339
if (_debugShowCanvasReuseStats) {
342340
DebugCanvasReuseOverlay.instance.createdCount++;
343341
}
344342
final BitmapCanvas canvas = BitmapCanvas(bounds);
343+
canvas.elementCache = _elementCache;
345344
if (_debugExplainSurfaceStats) {
346345
_surfaceStatsFor(this)
347346
..allocateBitmapCanvasCount += 1
@@ -371,6 +370,10 @@ abstract class PersistedPicture extends PersistedLeafSurface {
371370
final ui.Rect localPaintBounds;
372371
final int hints;
373372

373+
/// Cache for reusing elements such as images across picture updates.
374+
CrossFrameCache<html.HtmlElement> _elementCache =
375+
CrossFrameCache<html.HtmlElement>();
376+
374377
@override
375378
html.Element createElement() {
376379
return defaultCreateElement('flt-picture');
@@ -591,6 +594,8 @@ abstract class PersistedPicture extends PersistedLeafSurface {
591594
@override
592595
void update(PersistedPicture oldSurface) {
593596
super.update(oldSurface);
597+
// Transfer element cache over.
598+
_elementCache = oldSurface._elementCache;
594599

595600
if (dx != oldSurface.dx || dy != oldSurface.dy) {
596601
_applyTranslate();

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

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,30 +30,6 @@ bool debugShowClipLayers = false;
3030
/// reasonable.
3131
const double _kScreenPixelRatioWarningThreshold = 6.0;
3232

33-
/// A monotonically increasing frame number being rendered.
34-
///
35-
/// Used for debugging only.
36-
int _debugFrameNumber = 1;
37-
38-
List<FrameReference<dynamic>> _frameReferences = <FrameReference<dynamic>>[];
39-
40-
/// A temporary reference to a value of type [V].
41-
///
42-
/// The value automatically gets set to null after the current frame is
43-
/// rendered.
44-
///
45-
/// It is useful to think of this as a weak reference that's scoped to a
46-
/// single frame.
47-
class FrameReference<V> {
48-
/// Creates a frame reference to a value.
49-
FrameReference([this.value]) {
50-
_frameReferences.add(this);
51-
}
52-
53-
/// The current value of this reference.
54-
V value;
55-
}
56-
5733
/// Performs any outstanding painting work enqueued by [PersistedPicture]s.
5834
void commitScene(PersistedScene scene) {
5935
if (_paintQueue.isNotEmpty) {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// @dart = 2.6
6+
import 'package:ui/src/engine.dart';
7+
import 'package:test/test.dart';
8+
9+
void main() {
10+
group('CrossFrameCache', () {
11+
test('Reuse returns no object when cache empty', () {
12+
final CrossFrameCache<TestItem> cache = CrossFrameCache();
13+
cache.commitFrame();
14+
TestItem requestedItem = cache.reuse('item1');
15+
expect(requestedItem, null);
16+
});
17+
18+
test('Reuses object across frames', () {
19+
final CrossFrameCache<TestItem> cache = CrossFrameCache();
20+
final TestItem testItem1 = TestItem('item1');
21+
cache.cache(testItem1.label, testItem1);
22+
cache.commitFrame();
23+
TestItem requestedItem = cache.reuse('item1');
24+
expect(requestedItem, testItem1);
25+
requestedItem = cache.reuse('item1');
26+
expect(requestedItem, null);
27+
});
28+
29+
test('Reuses objects that have same key across frames', () {
30+
final CrossFrameCache<TestItem> cache = CrossFrameCache();
31+
final TestItem testItem1 = TestItem('sameLabel');
32+
final TestItem testItem2 = TestItem('sameLabel');
33+
final TestItem testItemX = TestItem('X');
34+
cache.cache(testItem1.label, testItem1);
35+
cache.cache(testItemX.label, testItemX);
36+
cache.cache(testItem2.label, testItem2);
37+
cache.commitFrame();
38+
TestItem requestedItem = cache.reuse('sameLabel');
39+
expect(requestedItem, testItem1);
40+
requestedItem = cache.reuse('sameLabel');
41+
expect(requestedItem, testItem2);
42+
requestedItem = cache.reuse('sameLabel');
43+
expect(requestedItem, null);
44+
});
45+
46+
test('Values don\'t survive beyond next frame', () {
47+
final CrossFrameCache<TestItem> cache = CrossFrameCache();
48+
final TestItem testItem1 = TestItem('item1');
49+
cache.cache(testItem1.label, testItem1);
50+
cache.commitFrame();
51+
cache.commitFrame();
52+
TestItem requestedItem = cache.reuse('item1');
53+
expect(requestedItem, null);
54+
});
55+
56+
test('Values are evicted when not reused', () {
57+
final Set<TestItem> _evictedItems = {};
58+
final CrossFrameCache<TestItem> cache = CrossFrameCache();
59+
final TestItem testItem1 = TestItem('item1');
60+
final TestItem testItem2 = TestItem('item2');
61+
cache.cache(testItem1.label, testItem1, (TestItem item) {_evictedItems.add(item);});
62+
cache.cache(testItem2.label, testItem2, (TestItem item) {_evictedItems.add(item);});
63+
cache.commitFrame();
64+
expect(_evictedItems.length, 0);
65+
cache.reuse('item2');
66+
cache.commitFrame();
67+
expect(_evictedItems.contains(testItem1), true);
68+
expect(_evictedItems.contains(testItem2), false);
69+
});
70+
});
71+
}
72+
73+
class TestItem {
74+
final String label;
75+
TestItem(this.label);
76+
}

0 commit comments

Comments
 (0)