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

[web:multiview] Make CanvasKitRenderer listen for view creation/disposal events #48812

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
Original file line number Diff line number Diff line change
Expand Up @@ -657,8 +657,8 @@ class HtmlViewEmbedder {
element.remove();
}

/// Clears the state of this view embedder. Used in tests.
void debugClear() {
/// Disposes the state of this view embedder.
void dispose() {
final Set<int> allViews = PlatformViewManager.instance.debugClear();
disposeViews(allViews);
_context = EmbedderFrameContext();
Expand Down
10 changes: 8 additions & 2 deletions lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ class Rasterizer {
final CkPictureRecorder pictureRecorder = CkPictureRecorder();
pictureRecorder.beginRecording(ui.Offset.zero & _currentFrameSize);
pictureRecorder.recordingCanvas!.clear(const ui.Color(0x00000000));
final Frame compositorFrame = context.acquireFrame(
pictureRecorder.recordingCanvas!, viewEmbedder);
final Frame compositorFrame =
context.acquireFrame(pictureRecorder.recordingCanvas!, viewEmbedder);

compositorFrame.raster(layerTree, ignoreRasterCache: true);

Expand All @@ -54,4 +54,10 @@ class Rasterizer {

viewEmbedder.submitFrame();
}

/// Disposes of this rasterizer.
void dispose() {
viewEmbedder.dispose();
renderCanvasFactory.dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import '../../engine.dart';
class RenderCanvasFactory {
RenderCanvasFactory() {
assert(() {
registerHotRestartListener(debugClear);
registerHotRestartListener(dispose);
return true;
}());
}
Expand Down Expand Up @@ -102,8 +102,8 @@ class RenderCanvasFactory {
return false;
}

/// Dispose all canvases created by this factory. Used in tests.
void debugClear() {
/// Dispose all canvases created by this factory.
void dispose() {
for (final RenderCanvas canvas in _cache) {
canvas.dispose();
}
Expand Down
64 changes: 45 additions & 19 deletions lib/web_ui/lib/src/engine/canvaskit/renderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ class CanvasKitRenderer implements Renderer {
/// is supported.
final Surface pictureToImageSurface = Surface();

// Listens for view creation events from the view manager.
StreamSubscription<int>? _onViewCreatedListener;
// Listens for view disposal events from the view manager.
StreamSubscription<int>? _onViewDisposedListener;

@override
Future<void> initialize() async {
_initialized ??= () async {
Expand All @@ -64,6 +69,20 @@ class CanvasKitRenderer implements Renderer {
canvasKit = await downloadCanvasKit();
windowFlutterCanvasKit = canvasKit;
}
// Views may have been registered before this renderer was initialized.
// Create rasterizers for them and then start listening for new view
// creation/disposal events.
final FlutterViewManager viewManager =
EnginePlatformDispatcher.instance.viewManager;
if (_onViewCreatedListener == null) {
for (final EngineFlutterView view in viewManager.views) {
_onViewCreated(view.viewId);
}
}
_onViewCreatedListener ??=
viewManager.onViewCreated.listen(_onViewCreated);
_onViewDisposedListener ??=
viewManager.onViewDisposed.listen(_onViewDisposed);
_instance = this;
}();
return _initialized;
Expand Down Expand Up @@ -394,37 +413,44 @@ class CanvasKitRenderer implements Renderer {
frameTimingsOnBuildFinish();
frameTimingsOnRasterStart();

// TODO(harryterkelsen): Use `FlutterViewManager.onViewsChanged` to manage
// the lifecycle of Rasterizers,
// https://github.com/flutter/flutter/issues/137073.
final Rasterizer rasterizer =
_getRasterizerForView(view as EngineFlutterView);
assert(_rasterizers.containsKey(view.viewId),
"Unable to render to a view which hasn't been registered");
final Rasterizer rasterizer = _rasterizers[view.viewId]!;

rasterizer.draw((scene as LayerScene).layerTree);
frameTimingsOnRasterFinish();
}

final Map<EngineFlutterView, Rasterizer> _rasterizers =
<EngineFlutterView, Rasterizer>{};
// Map from view id to the associated Rasterizer for that view.
final Map<int, Rasterizer> _rasterizers = <int, Rasterizer>{};

Rasterizer _getRasterizerForView(EngineFlutterView view) {
return _rasterizers.putIfAbsent(view, () {
return Rasterizer(view);
});
void _onViewCreated(int viewId) {
final EngineFlutterView view =
EnginePlatformDispatcher.instance.viewManager[viewId]!;
_rasterizers[view.viewId] = Rasterizer(view);
}

void _onViewDisposed(int viewId) {
// The view has already been disposed.
if (!_rasterizers.containsKey(viewId)) {
return;
}
final Rasterizer rasterizer = _rasterizers.remove(viewId)!;
rasterizer.dispose();
}

/// Returns the [Rasterizer] that has been created for the given [view].
/// Used in tests.
Rasterizer debugGetRasterizerForView(EngineFlutterView view) {
return _getRasterizerForView(view);
Rasterizer? debugGetRasterizerForView(EngineFlutterView view) {
return _rasterizers[view.viewId];
}

/// Resets the state of the renderer. Used in tests.
void debugClear() {
/// Disposes this renderer.
void dispose() {
_onViewCreatedListener?.cancel();
_onViewDisposedListener?.cancel();
for (final Rasterizer rasterizer in _rasterizers.values) {
rasterizer.renderCanvasFactory.debugClear();
rasterizer.viewEmbedder.debugClear();
rasterizer.dispose();
}
_rasterizers.clear();
}

@override
Expand Down
28 changes: 18 additions & 10 deletions lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@ class FlutterViewManager {
// A map of (optional) JsFlutterViewOptions, indexed by their viewId.
final Map<int, JsFlutterViewOptions> _jsViewOptions =
<int, JsFlutterViewOptions>{};
// The controller of the [onViewsChanged] stream.
final StreamController<void> _onViewsChangedController =
StreamController<void>.broadcast();
// The controller of the [onViewCreated] stream.
final StreamController<int> _onViewCreatedController =
StreamController<int>.broadcast(sync: true);
// The controller of the [onViewDisposed] stream.
final StreamController<int> _onViewDisposedController =
StreamController<int>.broadcast(sync: true);

/// A stream of `void` events that will fire when a view is registered/unregistered.
Stream<void> get onViewsChanged => _onViewsChangedController.stream;
/// A stream of viewIds that will fire when a view is created.
Stream<int> get onViewCreated => _onViewCreatedController.stream;

/// A stream of viewIds that will fire when a view is disposed.
Stream<int> get onViewDisposed => _onViewDisposedController.stream;

/// Exposes all the [EngineFlutterView]s registered so far.
Iterable<EngineFlutterView> get views => _viewData.values;
Expand All @@ -33,7 +39,8 @@ class FlutterViewManager {
EngineFlutterView createAndRegisterView(
JsFlutterViewOptions jsViewOptions,
) {
final EngineFlutterView view = EngineFlutterView(_dispatcher, jsViewOptions.hostElement);
final EngineFlutterView view =
EngineFlutterView(_dispatcher, jsViewOptions.hostElement);
registerView(view, jsViewOptions: jsViewOptions);
return view;
}
Expand All @@ -53,7 +60,7 @@ class FlutterViewManager {
if (jsViewOptions != null) {
_jsViewOptions[viewId] = jsViewOptions;
}
_onViewsChangedController.add(null);
_onViewCreatedController.add(viewId);

return view;
}
Expand All @@ -74,7 +81,7 @@ class FlutterViewManager {
JsFlutterViewOptions? unregisterView(int viewId) {
_viewData.remove(viewId); // .dispose();
final JsFlutterViewOptions? jsViewOptions = _jsViewOptions.remove(viewId);
_onViewsChangedController.add(null);
_onViewDisposedController.add(viewId);
return jsViewOptions;
}

Expand All @@ -91,7 +98,8 @@ class FlutterViewManager {
// inside the loop.
_viewData.keys.toList().forEach(disposeAndUnregisterView);
// Let listeners receive the unregistration events from the loop above, then
// close the stream.
_onViewsChangedController.close();
// close the streams.
_onViewCreatedController.close();
_onViewDisposedController.close();
}
}
4 changes: 0 additions & 4 deletions lib/web_ui/test/canvaskit/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ void setUpCanvasKitTest() {
setUpTestViewDimensions: false,
);

tearDown(() {
CanvasKitRenderer.instance.debugClear();
});

setUp(() => renderer.fontCollection.fontFallbackManager!.downloadQueue
.fallbackFontUrlPrefixOverride = 'assets/fallback_fonts/');
tearDown(() => renderer.fontCollection.fontFallbackManager!.downloadQueue
Expand Down
43 changes: 37 additions & 6 deletions lib/web_ui/test/canvaskit/embedded_views_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ void testMain() {
reason: 'The slot reenables pointer events.');
expect(contentsHost.getAttribute('slot'), slot.getAttribute('name'),
reason: 'The contents and slot are correctly related.');

await disposePlatformView(0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it makes more sense to have this here, since creating and adding of platformView 0 happened in the test body too!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change was required since I no longer dispose of the Renderer between each test

});

test('clips platform views with RRects', () async {
Expand Down Expand Up @@ -101,6 +103,8 @@ void testMain() {
sceneHost.querySelectorAll('flt-clip').single.style.height,
'100%',
);

await disposePlatformView(0);
});

test('correctly transforms platform views', () async {
Expand Down Expand Up @@ -131,6 +135,8 @@ void testMain() {
// 503 (5 * 100 + 3).
'matrix3d(5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 5, 0, 515, 515, 0, 1)',
);

await disposePlatformView(0);
});

test('correctly offsets platform views', () async {
Expand All @@ -157,6 +163,8 @@ void testMain() {
expect(slotRect.top, 4);
expect(slotRect.right, 8);
expect(slotRect.bottom, 10);

await disposePlatformView(0);
});

// Returns the list of CSS transforms applied to the ancestor chain of
Expand Down Expand Up @@ -189,8 +197,7 @@ void testMain() {
CanvasKitRenderer.instance.renderScene(sb.build(), implicitView);

// Transformations happen on the slot element.
DomElement slotHost =
sceneHost.querySelector('flt-platform-view-slot')!;
DomElement slotHost = sceneHost.querySelector('flt-platform-view-slot')!;

expect(
getTransformChain(slotHost),
Expand Down Expand Up @@ -220,6 +227,8 @@ void testMain() {
'matrix(1, 0, 0, 1, 3, 3)',
],
);

await disposePlatformView(0);
});

test('converts device pixels to logical pixels (no clips)', () async {
Expand All @@ -245,6 +254,8 @@ void testMain() {
getTransformChain(slotHost),
<String>['matrix(0.25, 0, 0, 0.25, 1.5, 1.5)'],
);

await disposePlatformView(0);
});

test('converts device pixels to logical pixels (with clips)', () async {
Expand Down Expand Up @@ -276,6 +287,8 @@ void testMain() {
'matrix(0.25, 0, 0, 0.25, 0.75, 0.75)',
],
);

await disposePlatformView(0);
});

test('renders overlays on top of platform views', () async {
Expand Down Expand Up @@ -428,8 +441,11 @@ void testMain() {
await createPlatformView(0, 'test-platform-view');
renderTestScene(viewCount: 0);
_expectSceneMatches(<_EmbeddedViewMarker>[_overlay]);
// TODO(yjbanov): skipped due to https://github.com/flutter/flutter/issues/73867
}, skip: isSafari);

for (int i = 0; i < 16; i++) {
await disposePlatformView(i);
}
});

test('correctly reuses overlays', () async {
final CkPicture testPicture =
Expand Down Expand Up @@ -561,8 +577,11 @@ void testMain() {
_overlay,
]);

// TODO(yjbanov): skipped due to https://github.com/flutter/flutter/issues/73867
}, skip: isSafari);
for (int i = 0; i < 20; i++) {
await disposePlatformView(i);
}

});

test('embeds and disposes of a platform view', () async {
ui_web.platformViewRegistry.registerViewFactory(
Expand Down Expand Up @@ -641,6 +660,8 @@ void testMain() {

implicitView.debugPhysicalSizeOverride = null;
implicitView.debugForceResize();

await disposePlatformView(0);
// ImageDecoder is not supported in Safari or Firefox.
}, skip: isSafari || isFirefox);

Expand Down Expand Up @@ -695,6 +716,9 @@ void testMain() {
platformViewsHost.querySelectorAll('flt-platform-view'),
hasLength(2),
);

await disposePlatformView(0);
await disposePlatformView(1);
});

test(
Expand Down Expand Up @@ -729,6 +753,8 @@ void testMain() {
await Future<void>.delayed(Duration.zero);
renderTestScene();
expect(skPathDefs.childNodes, hasLength(1));

await disposePlatformView(0);
});

test('does not crash when a prerolled platform view is not composited',
Expand All @@ -749,6 +775,8 @@ void testMain() {
_expectSceneMatches(<_EmbeddedViewMarker>[
_overlay,
]);

await disposePlatformView(0);
});

test('does not create overlays for invisible platform views', () async {
Expand Down Expand Up @@ -951,6 +979,9 @@ void testMain() {
_platformView,
_overlay,
]);
for (int i = 0; i < 7; i++) {
await disposePlatformView(i);
}
});
});
}
Expand Down
Loading