diff --git a/engine/src/flutter/lib/web_ui/README.md b/engine/src/flutter/lib/web_ui/README.md index 13f54aca2c93d..d594c1588fcec 100644 --- a/engine/src/flutter/lib/web_ui/README.md +++ b/engine/src/flutter/lib/web_ui/README.md @@ -277,6 +277,9 @@ The Flutter Web engine's sources are in `localhost:` > `lib` > `_engine` > `engine`. You can set breakpoints in Dart source files and use the Chrome debugger to inspect variables' values. +For Safari-specific CanvasKit memory behavior, see +[Safari CanvasKit memory][10]. + ## Building CanvasKit and Skwasm To build CanvasKit and/or Skwasm locally, you must first set up your gclient config to @@ -329,3 +332,4 @@ Note: The provided Dart SDK is used to run the `felt` tool itself, and to compil [7]: https://developer.chrome.com/docs/devtools [8]: https://developer.chrome.com/docs/devtools/sources [9]: https://github.com/flutter/flutter/blob/main/docs/engine/contributing/Setting-up-the-Engine-development-environment.md#additional-steps-for-web-engine +[10]: docs/SAFARI_CANVASKIT_MEMORY.md diff --git a/engine/src/flutter/lib/web_ui/docs/SAFARI_CANVASKIT_MEMORY.md b/engine/src/flutter/lib/web_ui/docs/SAFARI_CANVASKIT_MEMORY.md new file mode 100644 index 0000000000000..6f68be0d0fb8d --- /dev/null +++ b/engine/src/flutter/lib/web_ui/docs/SAFARI_CANVASKIT_MEMORY.md @@ -0,0 +1,25 @@ +# Safari CanvasKit memory + +Flutter Web uses CanvasKit to render Flutter pictures into one or more canvas +surfaces. Safari's WebGL and canvas memory can remain resident longer than in +Chromium-based browsers, so a CanvasKit app that is stable in Chrome can have a +much higher physical footprint in Safari. + +The web engine applies Safari-specific CanvasKit defaults to reduce this +footprint: + +* `canvasKitForceCpuOnly` defaults to `true` in Safari. An app can still opt out + by setting `canvasKitForceCpuOnly` in `window.flutterConfiguration` or by + compiling with `FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY=false`. +* `canvasKitMaximumSurfaces` defaults to `2` in Safari. A single surface can + merge Flutter overlays with platform-view underlays, which can make platform + views such as Google Maps disappear behind Flutter content. Two surfaces keep + the underlay and overlay paths separate while reducing retained canvas state. +* The Safari CanvasKit rasterizer caps Skia's resource cache so GPU-backed + resources are not allowed to grow to the same size as other browsers. + +When changing these defaults, validate both memory and platform-view +composition. A useful regression test is a release-mode CanvasKit app that +places Flutter content above a platform view, waits for Safari WebContent memory +to stabilize, and checks that the platform view remains visible while memory +stays below the expected limit. diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index def2ed376b10d..11f45a71543b2 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -18,6 +18,8 @@ class CanvasKitRenderer extends Renderer { static CanvasKitRenderer get instance => _instance; static late CanvasKitRenderer _instance; + static const int _safariResourceCacheMaxBytes = 24 * 1024 * 1024; + Future? _initialized; @override @@ -34,14 +36,22 @@ class CanvasKitRenderer extends Renderer { FlutterFontCollection get fontCollection => _fontCollection; static Rasterizer _createRasterizer() { - if (configuration.canvasKitForceMultiSurfaceRasterizer || isSafari || isFirefox) { - return MultiSurfaceRasterizer( + final Rasterizer rasterizer; + final bool useMultiSurfaceRasterizer = + configuration.canvasKitForceMultiSurfaceRasterizer || isFirefox || isSafari; + if (useMultiSurfaceRasterizer) { + rasterizer = MultiSurfaceRasterizer( (OnscreenCanvasProvider canvasProvider) => CkOnscreenSurface(canvasProvider), ); + } else { + rasterizer = OffscreenCanvasRasterizer( + (OffscreenCanvasProvider canvasProvider) => CkOffscreenSurface(canvasProvider), + ); + } + if (isSafari) { + rasterizer.setResourceCacheMaxBytes(_safariResourceCacheMaxBytes); } - return OffscreenCanvasRasterizer( - (OffscreenCanvasProvider canvasProvider) => CkOffscreenSurface(canvasProvider), - ); + return rasterizer; } @override diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart index 9301eed17a419..04acaf78d559d 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -226,6 +226,8 @@ abstract class CkSurface extends Surface { _skSurface = null; } + void release() {} + @override void setSkiaResourceCacheMaxBytes(int bytes) { _grContext?.setResourceCacheLimitBytes(bytes.toDouble()); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/display_canvas_factory.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/display_canvas_factory.dart index b7615da4ef3e1..6c788b0083bbe 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/display_canvas_factory.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/display_canvas_factory.dart @@ -97,6 +97,7 @@ class DisplayCanvasFactory { 'was not created by this factory', ); canvas.hostElement.remove(); + canvas.release(); _liveCanvases.remove(canvas); _cache.add(canvas); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/rasterizer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/rasterizer.dart index 8ee65ca65b7bc..395d18d6174c5 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/rasterizer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/rasterizer.dart @@ -164,6 +164,9 @@ abstract class DisplayCanvas { /// Initialize the overlay. void initialize(); + /// Releases transient resources while keeping this canvas reusable. + void release() {} + /// Disposes this overlay. void dispose(); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/render_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/render_canvas.dart index 73668ad29862a..528e7c2365c54 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/render_canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/render_canvas.dart @@ -47,6 +47,7 @@ class RenderCanvas extends DisplayCanvas { final DomHTMLCanvasElement canvasElement = createDomCanvasElement(); int _pixelWidth = 0; int _pixelHeight = 0; + bool _hasTransferredBitmap = false; late final DomImageBitmapRenderingContext renderContext = canvasElement.contextBitmapRenderer; @@ -73,6 +74,7 @@ class RenderCanvas extends DisplayCanvas { void render(DomImageBitmap bitmap) { _ensureSize(BitmapSize(bitmap.width, bitmap.height)); renderContext.transferFromImageBitmap(bitmap); + _hasTransferredBitmap = true; } void renderWithNoBitmapSupport( @@ -126,8 +128,22 @@ class RenderCanvas extends DisplayCanvas { // No extra initialization needed. } + @override + void release() { + if (!_hasTransferredBitmap) { + return; + } + renderContext.transferFromImageBitmap(null); + _hasTransferredBitmap = false; + } + @override void dispose() { - // No extra cleanup needed. + release(); + _pixelWidth = 0; + _pixelHeight = 0; + canvasElement.width = 0; + canvasElement.height = 0; + _updateLogicalHtmlCanvasSize(); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/surface.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/surface.dart index d7a6392a5c12a..9b35c66f98bbe 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/surface.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositing/surface.dart @@ -43,6 +43,9 @@ abstract class SurfaceProvider { int? _resourceCacheMaxBytes; + /// The configured Skia resource cache limit, if one has been set. + int? get debugResourceCacheMaxBytes => _resourceCacheMaxBytes; + void setSkiaResourceCacheMaxBytes(int bytes) { _resourceCacheMaxBytes = bytes; for (final C surface in _createdSurfaces) { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/configuration.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/configuration.dart index cb350d848aadb..6c8d3a05ec24a 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/configuration.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/configuration.dart @@ -47,6 +47,7 @@ library configuration; import 'dart:js_interop'; import 'package:meta/meta.dart'; +import 'browser_detection.dart'; import 'dom.dart'; enum CanvasKitVariant { @@ -288,10 +289,25 @@ class FlutterConfiguration { /// /// This is mainly used for testing or for apps that want to ensure they /// run on devices which don't support WebGL. - bool get canvasKitForceCpuOnly => - _configuration?.canvasKitForceCpuOnly ?? _defaultCanvasKitForceCpuOnly; + bool get canvasKitForceCpuOnly { + final bool? configured = _configuration?.canvasKitForceCpuOnly; + if (configured != null) { + return configured; + } + if (_hasDefaultCanvasKitForceCpuOnly) { + return _defaultCanvasKitForceCpuOnly; + } + return isSafari; + } + + static const String _canvasKitForceCpuOnlyEnvironment = 'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY'; + + static const bool _hasDefaultCanvasKitForceCpuOnly = bool.hasEnvironment( + _canvasKitForceCpuOnlyEnvironment, + ); + static const bool _defaultCanvasKitForceCpuOnly = bool.fromEnvironment( - 'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY', + _canvasKitForceCpuOnlyEnvironment, ); bool get canvasKitForceMultiSurfaceRasterizer => @@ -301,11 +317,21 @@ class FlutterConfiguration { 'FLUTTER_WEB_CANVASKIT_FORCE_MULTI_SURFACE_RASTERIZER', ); + /// The default maximum number of canvases to use when rendering in CanvasKit. + static const int _defaultCanvasKitMaximumSurfaces = 8; + + /// Safari needs at least two surfaces to keep platform-view underlays and + /// Flutter overlays separate, but more surfaces retain additional GPU state. + static const int _safariDefaultCanvasKitMaximumSurfaces = 2; + /// The maximum number of canvases to use when rendering in CanvasKit. /// /// Limits the amount of overlays that can be created. int get canvasKitMaximumSurfaces { - final int maxSurfaces = _configuration?.canvasKitMaximumSurfaces?.toInt() ?? 8; + final int defaultMaxSurfaces = isSafari + ? _safariDefaultCanvasKitMaximumSurfaces + : _defaultCanvasKitMaximumSurfaces; + final int maxSurfaces = _configuration?.canvasKitMaximumSurfaces?.toInt() ?? defaultMaxSurfaces; if (maxSurfaces < 1) { return 1; } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart index f0c7e172f098d..724def7a42cbd 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart @@ -176,6 +176,7 @@ class PlatformViewManager { /// never been rendered before. void clearPlatformView(int viewId) { // Remove from our cache, and then from the DOM... + _viewIdToType.remove(viewId); _contents.remove(viewId)?.remove(); } diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/rasterizer_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/rasterizer_test.dart index 9722fb36d686a..641e2a21c6fdd 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/rasterizer_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/rasterizer_test.dart @@ -7,6 +7,7 @@ import 'dart:js_interop'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; +import 'package:ui/ui_web/src/ui_web.dart' as ui_web; import 'common.dart'; @@ -16,16 +17,44 @@ void main() { void testMain() { setUpCanvasKitTest(withImplicitView: true); - test( - 'defaults to OffscreenCanvasRasterizer on Chrome and MultiSurfaceRasterizer on Firefox and Safari', - () { - if (isChromium) { - expect(CanvasKitRenderer.instance.rasterizer, isA()); - } else { - expect(CanvasKitRenderer.instance.rasterizer, isA()); - } - }, - ); + + test('defaults to OffscreenCanvasRasterizer on Chrome and MultiSurfaceRasterizer on Firefox', () { + if (isChromium) { + expect(CanvasKitRenderer.instance.rasterizer, isA()); + } else { + expect(CanvasKitRenderer.instance.rasterizer, isA()); + } + }); + + test('defaults to MultiSurfaceRasterizer on desktop Safari', () { + try { + ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.webkit; + ui_web.browser.debugOperatingSystemOverride = ui_web.OperatingSystem.macOs; + + CanvasKitRenderer.instance.debugResetRasterizer(); + + expect(CanvasKitRenderer.instance.rasterizer, isA()); + } finally { + ui_web.browser.debugBrowserEngineOverride = null; + ui_web.browser.debugOperatingSystemOverride = null; + CanvasKitRenderer.instance.debugResetRasterizer(); + } + }); + + test('defaults to MultiSurfaceRasterizer on iOS Safari', () { + try { + ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.webkit; + ui_web.browser.debugOperatingSystemOverride = ui_web.OperatingSystem.iOs; + + CanvasKitRenderer.instance.debugResetRasterizer(); + + expect(CanvasKitRenderer.instance.rasterizer, isA()); + } finally { + ui_web.browser.debugBrowserEngineOverride = null; + ui_web.browser.debugOperatingSystemOverride = null; + CanvasKitRenderer.instance.debugResetRasterizer(); + } + }); test('can be configured to always use MultiSurfaceRasterizer', () { debugOverrideJsConfiguration( diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/renderer_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/renderer_test.dart index d0ddb1034393c..14d901ec6ee11 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/renderer_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/renderer_test.dart @@ -9,6 +9,7 @@ import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; +import 'package:ui/ui_web/src/ui_web.dart' as ui_web; import 'common.dart'; @@ -190,7 +191,7 @@ void testMain() { }); test( - 'defaults to OffscreenCanvasRasterizer on Chrome and MultiSurfaceRasterizer on Firefox and Safari', + 'defaults to OffscreenCanvasRasterizer on Chrome and MultiSurfaceRasterizer on Firefox', () { if (isChromium) { expect(CanvasKitRenderer.instance.rasterizer, isA()); @@ -200,6 +201,58 @@ void testMain() { }, ); + test('uses MultiSurfaceRasterizer and caps Skia resource cache on desktop Safari', () { + try { + ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.webkit; + ui_web.browser.debugOperatingSystemOverride = ui_web.OperatingSystem.macOs; + + CanvasKitRenderer.instance.debugResetRasterizer(); + + expect(CanvasKitRenderer.instance.rasterizer, isA()); + expect( + CanvasKitRenderer.instance.rasterizer.surfaceProvider.debugResourceCacheMaxBytes, + 24 * 1024 * 1024, + ); + } finally { + ui_web.browser.debugBrowserEngineOverride = null; + ui_web.browser.debugOperatingSystemOverride = null; + } + }); + + test('uses MultiSurfaceRasterizer and caps Skia resource cache on iOS Safari', () { + try { + ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.webkit; + ui_web.browser.debugOperatingSystemOverride = ui_web.OperatingSystem.iOs; + + CanvasKitRenderer.instance.debugResetRasterizer(); + + expect(CanvasKitRenderer.instance.rasterizer, isA()); + expect( + CanvasKitRenderer.instance.rasterizer.surfaceProvider.debugResourceCacheMaxBytes, + 24 * 1024 * 1024, + ); + } finally { + ui_web.browser.debugBrowserEngineOverride = null; + ui_web.browser.debugOperatingSystemOverride = null; + } + }); + + test('does not cap Skia resource cache on Chromium', () { + try { + ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.blink; + + CanvasKitRenderer.instance.debugResetRasterizer(); + + expect(CanvasKitRenderer.instance.rasterizer, isA()); + expect( + CanvasKitRenderer.instance.rasterizer.surfaceProvider.debugResourceCacheMaxBytes, + isNull, + ); + } finally { + ui_web.browser.debugBrowserEngineOverride = null; + } + }); + test('can be configured to always use MultiSurfaceRasterizer', () { debugOverrideJsConfiguration( {'canvasKitForceMultiSurfaceRasterizer': true}.jsify() diff --git a/engine/src/flutter/lib/web_ui/test/engine/compositing/display_canvas_factory_test.dart b/engine/src/flutter/lib/web_ui/test/engine/compositing/display_canvas_factory_test.dart index cd58d2358b18b..9815b1d78397e 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/compositing/display_canvas_factory_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/compositing/display_canvas_factory_test.dart @@ -13,6 +13,8 @@ void main() { } class DummyDisplayCanvas extends DisplayCanvas { + int releaseCount = 0; + @override void dispose() {} @@ -24,6 +26,11 @@ class DummyDisplayCanvas extends DisplayCanvas { @override void initialize() {} + @override + void release() { + releaseCount += 1; + } + @override bool get isConnected => throw UnimplementedError(); } @@ -64,6 +71,17 @@ void testMain() { expect(newCanvas, equals(canvas)); }); + test('releaseCanvas releases transient resources before caching the canvas', () { + final factory = DisplayCanvasFactory( + createCanvas: () => DummyDisplayCanvas(), + ); + + final DummyDisplayCanvas canvas = factory.getCanvas(); + factory.releaseCanvas(canvas); + + expect(canvas.releaseCount, 1); + }); + test('isLive', () { final factory = DisplayCanvasFactory(createCanvas: () => DummyDisplayCanvas()); diff --git a/engine/src/flutter/lib/web_ui/test/engine/compositing/render_canvas_test.dart b/engine/src/flutter/lib/web_ui/test/engine/compositing/render_canvas_test.dart index 536ab9ca18c4a..5ceb54cefdf8c 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/compositing/render_canvas_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/compositing/render_canvas_test.dart @@ -59,6 +59,26 @@ void testMain() { expect(canvas.canvasElement.style.height, '32px'); }); + test('releases transferred bitmap resources while remaining reusable', () async { + final canvas = RenderCanvas(); + canvas.render(await newBitmap(10, 16)); + + expect(canvas.canvasElement.width, 10); + expect(canvas.canvasElement.height, 16); + + canvas.release(); + expect(canvas.canvasElement.width, 10); + expect(canvas.canvasElement.height, 16); + + canvas.render(await newBitmap(20, 24)); + expect(canvas.canvasElement.width, 20); + expect(canvas.canvasElement.height, 24); + + canvas.dispose(); + expect(canvas.canvasElement.width, 0); + expect(canvas.canvasElement.height, 0); + }); + test('rounds physical size to nearest integer size', () async { final EngineFlutterWindow implicitView = EnginePlatformDispatcher.instance.implicitView!; implicitView.debugPhysicalSizeOverride = const ui.Size(199.999999, 200.000001); diff --git a/engine/src/flutter/lib/web_ui/test/engine/configuration_test.dart b/engine/src/flutter/lib/web_ui/test/engine/configuration_test.dart index 984127b36d27b..4fba03d954bca 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/configuration_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/configuration_test.dart @@ -10,6 +10,7 @@ import 'dart:js_interop'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; +import 'package:ui/ui_web/src/ui_web.dart' as ui_web; import '../common/matchers.dart'; @@ -82,6 +83,46 @@ void testMain() { expect(defaultConfig.canvasKitVariant, CanvasKitVariant.auto); }); + test('canvasKitMaximumSurfaces defaults to 8 outside Safari', () { + try { + ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.blink; + + expect(defaultConfig.canvasKitMaximumSurfaces, 8); + } finally { + ui_web.browser.debugBrowserEngineOverride = null; + } + }); + + test('canvasKitMaximumSurfaces defaults to 2 on Safari', () { + try { + ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.webkit; + + expect(defaultConfig.canvasKitMaximumSurfaces, 2); + } finally { + ui_web.browser.debugBrowserEngineOverride = null; + } + }); + + test('canvasKitForceCpuOnly defaults to false outside Safari', () { + try { + ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.blink; + + expect(defaultConfig.canvasKitForceCpuOnly, isFalse); + } finally { + ui_web.browser.debugBrowserEngineOverride = null; + } + }); + + test('canvasKitForceCpuOnly defaults to true on Safari', () { + try { + ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.webkit; + + expect(defaultConfig.canvasKitForceCpuOnly, isTrue); + } finally { + ui_web.browser.debugBrowserEngineOverride = null; + } + }); + test('multiViewEnabled', () { expect(defaultConfig.multiViewEnabled, isFalse); }); @@ -131,5 +172,35 @@ void testMain() { ); expect(config.multiViewEnabled, isTrue); }); + + test('canvasKitMaximumSurfaces override is preserved on Safari', () { + try { + ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.webkit; + + final config = FlutterConfiguration(); + config.setUserConfiguration( + {'canvasKitMaximumSurfaces': 4}.jsify()! as JsFlutterConfiguration, + ); + + expect(config.canvasKitMaximumSurfaces, 4); + } finally { + ui_web.browser.debugBrowserEngineOverride = null; + } + }); + + test('canvasKitForceCpuOnly override is preserved on Safari', () { + try { + ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.webkit; + + final config = FlutterConfiguration(); + config.setUserConfiguration( + {'canvasKitForceCpuOnly': false}.jsify()! as JsFlutterConfiguration, + ); + + expect(config.canvasKitForceCpuOnly, isFalse); + } finally { + ui_web.browser.debugBrowserEngineOverride = null; + } + }); }); } diff --git a/engine/src/flutter/lib/web_ui/test/engine/platform_views/content_manager_test.dart b/engine/src/flutter/lib/web_ui/test/engine/platform_views/content_manager_test.dart index e9e9b31f6e904..bca087da69067 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/platform_views/content_manager_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/platform_views/content_manager_test.dart @@ -60,6 +60,21 @@ void testMain() { expect(contentManager.knowsViewId(viewId), isFalse); expect(view.parentNode, isNull); }); + + test('forgets visibility metadata after clearing viewIds', () { + contentManager.renderContent( + ui_web.PlatformViewRegistry.defaultInvisibleViewType, + viewId, + {'tagName': 'script'}, + ); + + expect(contentManager.isInvisible(viewId), isTrue); + + contentManager.clearPlatformView(viewId); + + expect(contentManager.knowsViewId(viewId), isFalse); + expect(contentManager.isInvisible(viewId), isFalse); + }); }); group('registerFactory', () { diff --git a/packages/flutter/lib/src/widgets/_html_element_view_web.dart b/packages/flutter/lib/src/widgets/_html_element_view_web.dart index 1573d215f5823..09962e38b7e4b 100644 --- a/packages/flutter/lib/src/widgets/_html_element_view_web.dart +++ b/packages/flutter/lib/src/widgets/_html_element_view_web.dart @@ -58,7 +58,10 @@ extension HtmlElementViewImpl on HtmlElementView { /// Creates the controller and kicks off its initialization. _HtmlElementViewController _createController(PlatformViewCreationParams params) { final controller = _HtmlElementViewController(params.id, viewType, creationParams); - controller._initialize().then((_) { + controller._initialize().then((bool shouldNotifyCreated) { + if (!shouldNotifyCreated) { + return; + } params.onPlatformViewCreated(params.id); onPlatformViewCreated?.call(params.id); }); @@ -91,11 +94,18 @@ class _HtmlElementViewController extends PlatformViewController { final dynamic creationParams; bool _initialized = false; + bool _disposed = false; + bool _disposeSent = false; - Future _initialize() async { + Future _initialize() async { final args = {'id': viewId, 'viewType': viewType, 'params': creationParams}; await SystemChannels.platform_views.invokeMethod('create', args); _initialized = true; + if (_disposed) { + await _disposePlatformView(); + return false; + } + return true; } @override @@ -112,8 +122,17 @@ class _HtmlElementViewController extends PlatformViewController { @override Future dispose() async { + _disposed = true; if (_initialized) { - await SystemChannels.platform_views.invokeMethod('dispose', viewId); + await _disposePlatformView(); + } + } + + Future _disposePlatformView() async { + if (_disposeSent) { + return; } + _disposeSent = true; + await SystemChannels.platform_views.invokeMethod('dispose', viewId); } } diff --git a/packages/flutter/test/widgets/html_element_view_test.dart b/packages/flutter/test/widgets/html_element_view_test.dart index 36ad5b00d342b..071d23f4adfe9 100644 --- a/packages/flutter/test/widgets/html_element_view_test.dart +++ b/packages/flutter/test/widgets/html_element_view_test.dart @@ -208,6 +208,51 @@ void main() { expect(fakePlatformViewRegistry.views, isEmpty); }); + testWidgets('Dispose HTML view when create completes after widget disposal', ( + WidgetTester tester, + ) async { + final createCompleter = Completer(); + final methodCalls = []; + var didCallOnPlatformViewCreated = false; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform_views, + (MethodCall call) { + methodCalls.add(call.method); + return switch (call.method) { + 'create' => createCompleter.future, + 'dispose' => Future.value(), + _ => Future.value(), + }; + }, + ); + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: HtmlElementView( + viewType: 'webview', + onPlatformViewCreated: (_) { + didCallOnPlatformViewCreated = true; + }, + ), + ), + ), + ); + expect(methodCalls, ['create']); + + await tester.pumpWidget(const Center(child: SizedBox(width: 200.0, height: 100.0))); + expect(methodCalls, ['create']); + + createCompleter.complete(); + await tester.pump(); + + expect(methodCalls, ['create', 'dispose']); + expect(didCallOnPlatformViewCreated, isFalse); + }); + testWidgets('HTML view survives widget tree change', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory);