Skip to content

Commit 18d7fe8

Browse files
yjbanovgspencergoog
authored andcommitted
[canvaskit] cache and reuse platform view overlays (flutter#23061)
1 parent a97d167 commit 18d7fe8

File tree

5 files changed

+305
-56
lines changed

5 files changed

+305
-56
lines changed

lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart

Lines changed: 98 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,16 @@ class HtmlViewEmbedder {
3030
/// The root view in the stack of mutator elements for the view id.
3131
final Map<int?, html.Element?> _rootViews = <int?, html.Element?>{};
3232

33-
/// The overlay for the view id.
34-
final Map<int, Overlay> _overlays = <int, Overlay>{};
33+
/// Surfaces used to draw on top of platform views, keyed by platform view ID.
34+
///
35+
/// These surfaces are cached in the [OverlayCache] and reused.
36+
final Map<int, Surface> _overlays = <int, Surface>{};
3537

3638
/// The views that need to be recomposited into the scene on the next frame.
3739
final Set<int> _viewsToRecomposite = <int>{};
3840

3941
/// The views that need to be disposed of on the next frame.
40-
final Set<int?> _viewsToDispose = <int?>{};
42+
final Set<int> _viewsToDispose = <int>{};
4143

4244
/// The list of view ids that should be composited, in order.
4345
List<int> _compositionOrder = <int>[];
@@ -115,14 +117,15 @@ class HtmlViewEmbedder {
115117

116118
void _dispose(
117119
MethodCall methodCall, ui.PlatformMessageResponseCallback callback) {
118-
int? viewId = methodCall.arguments;
120+
final int? viewId = methodCall.arguments;
119121
const MethodCodec codec = StandardMethodCodec();
120-
if (!_views.containsKey(viewId)) {
122+
if (viewId == null || !_views.containsKey(viewId)) {
121123
callback(codec.encodeErrorEnvelope(
122124
code: 'unknown_view',
123125
message: 'trying to dispose an unknown view',
124126
details: 'view id: $viewId',
125127
));
128+
return;
126129
}
127130
_viewsToDispose.add(viewId);
128131
callback(codec.encodeSuccessEnvelope(null));
@@ -339,9 +342,9 @@ class HtmlViewEmbedder {
339342

340343
for (int i = 0; i < _compositionOrder.length; i++) {
341344
int viewId = _compositionOrder[i];
342-
ensureOverlayInitialized(viewId);
345+
_ensureOverlayInitialized(viewId);
343346
final SurfaceFrame frame =
344-
_overlays[viewId]!.surface.acquireFrame(_frameSize);
347+
_overlays[viewId]!.acquireFrame(_frameSize);
345348
final CkCanvas canvas = frame.skiaCanvas;
346349
canvas.drawPicture(
347350
_pictureRecorders[viewId]!.endRecording(),
@@ -353,53 +356,127 @@ class HtmlViewEmbedder {
353356
_compositionOrder.clear();
354357
return;
355358
}
359+
360+
final Set<int> unusedViews = Set<int>.from(_activeCompositionOrder);
356361
_activeCompositionOrder.clear();
357362

358363
for (int i = 0; i < _compositionOrder.length; i++) {
359364
int viewId = _compositionOrder[i];
365+
366+
assert(
367+
_views.containsKey(viewId),
368+
'Cannot render platform view $viewId. '
369+
'It has not been created, or it has been deleted.',
370+
);
371+
372+
unusedViews.remove(viewId);
360373
html.Element platformViewRoot = _rootViews[viewId]!;
361-
html.Element overlay = _overlays[viewId]!.surface.htmlElement!;
374+
html.Element overlay = _overlays[viewId]!.htmlElement;
362375
platformViewRoot.remove();
363376
skiaSceneHost!.append(platformViewRoot);
364377
overlay.remove();
365378
skiaSceneHost!.append(overlay);
366379
_activeCompositionOrder.add(viewId);
367380
}
368381
_compositionOrder.clear();
382+
383+
for (final int unusedViewId in unusedViews) {
384+
_releaseOverlay(unusedViewId);
385+
}
369386
}
370387

371388
void disposeViews() {
372389
if (_viewsToDispose.isEmpty) {
373390
return;
374391
}
375392

376-
for (int? viewId in _viewsToDispose) {
393+
for (final int viewId in _viewsToDispose) {
377394
final html.Element rootView = _rootViews[viewId]!;
378395
rootView.remove();
379396
_views.remove(viewId);
380397
_rootViews.remove(viewId);
381-
if (_overlays[viewId] != null) {
382-
final Overlay overlay = _overlays[viewId]!;
383-
overlay.surface.htmlElement?.remove();
384-
overlay.surface.htmlElement = null;
385-
overlay.skSurface?.dispose();
386-
}
387-
_overlays.remove(viewId);
398+
_releaseOverlay(viewId);
388399
_currentCompositionParams.remove(viewId);
389400
_clipCount.remove(viewId);
390401
_viewsToRecomposite.remove(viewId);
391402
}
392403
_viewsToDispose.clear();
393404
}
394405

395-
void ensureOverlayInitialized(int viewId) {
396-
Overlay? overlay = _overlays[viewId];
406+
void _releaseOverlay(int viewId) {
407+
if (_overlays[viewId] != null) {
408+
OverlayCache.instance.releaseOverlay(_overlays[viewId]!);
409+
_overlays.remove(viewId);
410+
}
411+
}
412+
413+
void _ensureOverlayInitialized(int viewId) {
414+
// If there's an active overlay for the view ID, continue using it.
415+
Surface? overlay = _overlays[viewId];
397416
if (overlay != null) {
398417
return;
399418
}
400-
Surface surface = Surface(this);
401-
CkSurface? skSurface = surface.acquireRenderSurface(_frameSize);
402-
_overlays[viewId] = Overlay(surface, skSurface);
419+
420+
// Try reusing a cached overlay created for another platform view.
421+
overlay = OverlayCache.instance.reserveOverlay();
422+
423+
// If nothing to reuse, create a new overlay.
424+
if (overlay == null) {
425+
overlay = Surface(this);
426+
}
427+
428+
_overlays[viewId] = overlay;
429+
}
430+
}
431+
432+
/// Caches surfaces used to overlay platform views.
433+
class OverlayCache {
434+
static const int kDefaultCacheSize = 5;
435+
436+
/// The cache singleton.
437+
static final OverlayCache instance = OverlayCache(kDefaultCacheSize);
438+
439+
OverlayCache(this.maximumSize);
440+
441+
/// The cache will not grow beyond this size.
442+
final int maximumSize;
443+
444+
/// Cached surfaces, available for reuse.
445+
final List<Surface> _cache = <Surface>[];
446+
447+
/// Returns the list of cached surfaces.
448+
///
449+
/// Useful in tests.
450+
List<Surface> get debugCachedSurfaces => _cache;
451+
452+
/// Reserves an overlay from the cache, if available.
453+
///
454+
/// Returns null if the cache is empty.
455+
Surface? reserveOverlay() {
456+
if (_cache.isEmpty) {
457+
return null;
458+
}
459+
return _cache.removeLast();
460+
}
461+
462+
/// Returns an overlay back to the cache.
463+
///
464+
/// If the cache is full, the overlay is deleted.
465+
void releaseOverlay(Surface overlay) {
466+
overlay.htmlElement.remove();
467+
if (_cache.length < maximumSize) {
468+
_cache.add(overlay);
469+
} else {
470+
overlay.dispose();
471+
}
472+
}
473+
474+
int get debugLength => _cache.length;
475+
476+
void debugClear() {
477+
for (final Surface overlay in _cache) {
478+
overlay.dispose();
479+
}
403480
}
404481
}
405482

@@ -547,11 +624,3 @@ class MutatorsStack extends Iterable<Mutator> {
547624
@override
548625
Iterator<Mutator> get iterator => _mutators.reversed.iterator;
549626
}
550-
551-
/// Represents a surface overlaying a platform view.
552-
class Overlay {
553-
final Surface surface;
554-
final CkSurface? skSurface;
555-
556-
Overlay(this.surface, this.skSurface);
557-
}

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

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,27 @@ class Surface {
3838
Surface(this.viewEmbedder);
3939

4040
CkSurface? _surface;
41-
html.Element? htmlElement;
41+
42+
/// If true, forces a new WebGL context to be created, even if the window
43+
/// size is the same. This is used to restore the UI after the browser tab
44+
/// goes dormant and loses the GL context.
45+
bool _forceNewContext = true;
46+
bool get debugForceNewContext => _forceNewContext;
47+
4248
SkGrContext? _grContext;
4349
int? _skiaCacheBytes;
4450

51+
/// The root HTML element for this surface.
52+
///
53+
/// This element contains the canvas used to draw the UI. Unlike the canvas,
54+
/// this element is permanent. It is never replaced or deleted, until this
55+
/// surface is disposed of via [dispose].
56+
///
57+
/// Conversely, the canvas that lives inside this element can be swapped, for
58+
/// example, when the screen size changes, or when the WebGL context is lost
59+
/// due to the browser tab becoming dormant.
60+
final html.Element htmlElement = html.Element.tag('flt-canvas-container');
61+
4562
/// Specify the GPU resource cache limits.
4663
void setSkiaResourceCacheMaxBytes(int bytes) {
4764
_skiaCacheBytes = bytes;
@@ -64,7 +81,7 @@ class Surface {
6481
///
6582
/// The given [size] is in physical pixels.
6683
SurfaceFrame acquireFrame(ui.Size size) {
67-
final CkSurface surface = acquireRenderSurface(size);
84+
final CkSurface surface = _createOrUpdateSurfaces(size);
6885

6986
if (surface.context != null) {
7087
canvasKit.setCurrentContext(surface.context!);
@@ -77,33 +94,29 @@ class Surface {
7794
return SurfaceFrame(surface, submitCallback);
7895
}
7996

80-
CkSurface acquireRenderSurface(ui.Size size) {
81-
_createOrUpdateSurfaces(size);
82-
return _surface!;
83-
}
84-
8597
void addToScene() {
8698
if (!_addedToScene) {
87-
skiaSceneHost!.children.insert(0, htmlElement!);
99+
skiaSceneHost!.children.insert(0, htmlElement);
88100
}
89101
_addedToScene = true;
90102
}
91103

92104
ui.Size? _currentSize;
93105

94-
void _createOrUpdateSurfaces(ui.Size size) {
106+
CkSurface _createOrUpdateSurfaces(ui.Size size) {
95107
if (size.isEmpty) {
96108
throw CanvasKitError('Cannot create surfaces of empty size.');
97109
}
98110

99111
// Check if the window is shrinking in size, and if so, don't allocate a
100112
// new canvas as the previous canvas is big enough to fit everything.
101113
final ui.Size? previousSize = _currentSize;
102-
if (previousSize != null &&
114+
if (!_forceNewContext &&
115+
previousSize != null &&
103116
size.width <= previousSize.width &&
104117
size.height <= previousSize.height) {
105118
// The existing surface is still reusable.
106-
return;
119+
return _surface!;
107120
}
108121

109122
_currentSize = _currentSize == null
@@ -116,14 +129,17 @@ class Surface {
116129

117130
_surface?.dispose();
118131
_surface = null;
119-
htmlElement?.remove();
120-
htmlElement = null;
121132
_addedToScene = false;
122133

123-
_surface = _wrapHtmlCanvas(_currentSize!);
134+
return _surface = _wrapHtmlCanvas(_currentSize!);
124135
}
125136

126137
CkSurface _wrapHtmlCanvas(ui.Size physicalSize) {
138+
// Clear the container, if it's not empty.
139+
while (htmlElement.firstChild != null) {
140+
htmlElement.firstChild!.remove();
141+
}
142+
127143
// If `physicalSize` is not precise, use a slightly bigger canvas. This way
128144
// we ensure that the rendred picture covers the entire browser window.
129145
final int pixelWidth = physicalSize.width.ceil();
@@ -146,9 +162,28 @@ class Surface {
146162
..width = '${logicalWidth}px'
147163
..height = '${logicalHeight}px';
148164

149-
htmlElement = htmlCanvas;
150-
if (webGLVersion == -1 || canvasKitForceCpuOnly) {
151-
return _makeSoftwareCanvasSurface(htmlCanvas);
165+
// When the browser tab using WebGL goes dormant the browser and/or OS may
166+
// decide to clear GPU resources to let other tabs/programs use the GPU.
167+
// When this happens, the browser sends the "webglcontextlost" event as a
168+
// notification. When we receive this notification we force a new context.
169+
//
170+
// See also: https://www.khronos.org/webgl/wiki/HandlingContextLost
171+
htmlCanvas.addEventListener('webglcontextlost', (event) {
172+
print('Flutter: restoring WebGL context.');
173+
_forceNewContext = true;
174+
// Force the framework to rerender the frame.
175+
EnginePlatformDispatcher.instance.invokeOnMetricsChanged();
176+
event.stopPropagation();
177+
event.preventDefault();
178+
}, false);
179+
_forceNewContext = false;
180+
181+
htmlElement.append(htmlCanvas);
182+
183+
if (webGLVersion == -1) {
184+
return _makeSoftwareCanvasSurface(htmlCanvas, 'WebGL support not detected');
185+
} else if (canvasKitForceCpuOnly) {
186+
return _makeSoftwareCanvasSurface(htmlCanvas, 'CPU rendering forced by application');
152187
} else {
153188
// Try WebGL first.
154189
final int glContext = canvasKit.GetWebGLContext(
@@ -162,7 +197,7 @@ class Surface {
162197
);
163198

164199
if (glContext == 0) {
165-
return _makeSoftwareCanvasSurface(htmlCanvas);
200+
return _makeSoftwareCanvasSurface(htmlCanvas, 'Failed to initialize WebGL context');
166201
}
167202

168203
_grContext = canvasKit.MakeGrContext(glContext);
@@ -183,7 +218,7 @@ class Surface {
183218
);
184219

185220
if (skSurface == null) {
186-
return _makeSoftwareCanvasSurface(htmlCanvas);
221+
return _makeSoftwareCanvasSurface(htmlCanvas, 'Failed to initialize WebGL surface');
187222
}
188223

189224
return CkSurface(skSurface, _grContext, glContext);
@@ -192,9 +227,11 @@ class Surface {
192227

193228
static bool _didWarnAboutWebGlInitializationFailure = false;
194229

195-
CkSurface _makeSoftwareCanvasSurface(html.CanvasElement htmlCanvas) {
230+
CkSurface _makeSoftwareCanvasSurface(html.CanvasElement htmlCanvas, String reason) {
196231
if (!_didWarnAboutWebGlInitializationFailure) {
197-
html.window.console.warn('WARNING: failed to initialize WebGL. Falling back to CPU-only rendering.');
232+
html.window.console.warn(
233+
'WARNING: Falling back to CPU-only rendering. $reason.'
234+
);
198235
_didWarnAboutWebGlInitializationFailure = true;
199236
}
200237
return CkSurface(
@@ -211,6 +248,11 @@ class Surface {
211248
_surface!.flush();
212249
return true;
213250
}
251+
252+
void dispose() {
253+
htmlElement.remove();
254+
_surface?.dispose();
255+
}
214256
}
215257

216258
/// A Dart wrapper around Skia's CkSurface.

lib/web_ui/test/canvaskit/common.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ void setUpCanvasKitTest() {
3838
tearDown(() {
3939
testCollector.cleanUpAfterTest();
4040
debugResetBrowserSupportsFinalizationRegistry();
41+
OverlayCache.instance.debugClear();
4142
});
4243

4344
tearDownAll(() {

0 commit comments

Comments
 (0)