diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index 8c0ccee7da07e..b6d3132711055 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -1801,6 +1801,11 @@ class ProductionCollector implements Collector { @override void register(Object wrapper, SkDeletable deletable) { + if (Instrumentation.enabled) { + Instrumentation.instance.incrementCounter( + '${deletable.constructor.name} registered', + ); + } _skObjectFinalizationRegistry.register(wrapper, deletable); } @@ -1859,6 +1864,11 @@ class ProductionCollector implements Collector { // again if the objects is worth collecting. continue; } + if (Instrumentation.enabled) { + Instrumentation.instance.incrementCounter( + '${deletable.constructor.name} deleted', + ); + } try { deletable.delete(); } catch (error, stackTrace) { @@ -1907,6 +1917,21 @@ class SkDeletable { /// Returns whether the correcponding C++ object has been deleted. external bool isDeleted(); + + /// Returns the JavaScript constructor for this object. + /// + /// This is useful for debugging. + external JsConstructor get constructor; +} + +@JS() +@anonymous +class JsConstructor { + /// The name of the "constructor", typically the function name called with + /// the `new` keyword, or the ES6 class name. + /// + /// This is useful for debugging. + external String get name; } /// Attaches a weakly referenced object to another object and calls a finalizer diff --git a/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart b/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart index 96b6ebf6605ba..8ea1ec27c1a01 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart @@ -147,6 +147,11 @@ abstract class ManagedSkiaObject extends SkiaObject { } else { // If FinalizationRegistry is _not_ supported we may need to delete // and resurrect the object multiple times before deleting it forever. + if (Instrumentation.enabled) { + Instrumentation.instance.incrementCounter( + '${(defaultObject as SkDeletable).constructor.name} created', + ); + } if (isResurrectionExpensive) { SkiaObjects.manageExpensive(this); } else { @@ -161,6 +166,11 @@ abstract class ManagedSkiaObject extends SkiaObject { T _doResurrect() { assert(!browserSupportsFinalizationRegistry); final T skiaObject = resurrect(); + if (Instrumentation.enabled) { + Instrumentation.instance.incrementCounter( + '${(skiaObject as SkDeletable).constructor.name} resurrected', + ); + } rawSkiaObject = skiaObject; if (isResurrectionExpensive) { SkiaObjects.manageExpensive(this); @@ -173,6 +183,11 @@ abstract class ManagedSkiaObject extends SkiaObject { @override void didDelete() { assert(!browserSupportsFinalizationRegistry); + if (Instrumentation.enabled) { + Instrumentation.instance.incrementCounter( + '${(rawSkiaObject as SkDeletable).constructor.name} deleted', + ); + } rawSkiaObject = null; } @@ -303,6 +318,11 @@ class SkiaObjectBox extends Skia void _initialize(R debugReferrer, T initialValue) { _update(initialValue); + if (Instrumentation.enabled) { + Instrumentation.instance.incrementCounter( + '${_skDeletable?.constructor.name} created', + ); + } if (assertionsEnabled) { debugReferrers.add(debugReferrer); } @@ -355,6 +375,11 @@ class SkiaObjectBox extends Skia assert(_resurrector != null); assert(!_isDeletedPermanently, 'Cannot use deleted object.'); _update(_resurrector!()); + if (Instrumentation.enabled) { + Instrumentation.instance.incrementCounter( + '${_skDeletable?.constructor.name} resurrected', + ); + } SkiaObjects.manageExpensive(this); return skiaObject; } @@ -366,6 +391,11 @@ class SkiaObjectBox extends Skia @override void didDelete() { + if (Instrumentation.enabled) { + Instrumentation.instance.incrementCounter( + '${_skDeletable?.constructor.name} deleted', + ); + } assert(!browserSupportsFinalizationRegistry); _update(null); } @@ -419,7 +449,8 @@ class SkiaObjectBox extends Skia if (browserSupportsFinalizationRegistry) { Collector.instance.collect(_skDeletable!); } else { - _skDeletable!.delete(); + delete(); + didDelete(); } } rawSkiaObject = null; diff --git a/lib/web_ui/lib/src/engine/profiler.dart b/lib/web_ui/lib/src/engine/profiler.dart index 825bdbeb35e50..57f1527cb53f3 100644 --- a/lib/web_ui/lib/src/engine/profiler.dart +++ b/lib/web_ui/lib/src/engine/profiler.dart @@ -218,3 +218,66 @@ void _frameTimingsOnRasterFinish() { int _nowMicros() { return (html.window.performance.now() * 1000).toInt(); } + +/// Counts various events that take place while the app is running. +/// +/// This class will slow down the app, and therefore should be disabled while +/// benchmarking. For example, avoid using it in conjunction with [Profiler]. +class Instrumentation { + Instrumentation._() { + _checkInstrumentationEnabled(); + } + + /// Whether instrumentation is enabled. + /// + /// Check this value before calling any other methods in this class. + static const bool enabled = const bool.fromEnvironment( + 'FLUTTER_WEB_ENABLE_INSTRUMENTATION', + defaultValue: false, + ); + + /// Returns the singleton that provides instrumentation API. + static Instrumentation get instance { + _checkInstrumentationEnabled(); + return _instance; + } + + static late final Instrumentation _instance = Instrumentation._(); + + static void _checkInstrumentationEnabled() { + if (!enabled) { + throw Exception( + 'Cannot use Instrumentation unless it is enabled. ' + 'You can enable it by setting the `FLUTTER_WEB_ENABLE_INSTRUMENTATION` ' + 'environment variable to true, or by passing ' + '--dart-define=FLUTTER_WEB_ENABLE_INSTRUMENTATION=true to the flutter ' + 'tool.', + ); + } + } + + final Map _counters = {}; + Timer? _printTimer; + + /// Increments the count of a particular event by one. + void incrementCounter(String event) { + _checkInstrumentationEnabled(); + final int currentCount = _counters[event] ?? 0; + _counters[event] = currentCount + 1; + _printTimer ??= Timer( + const Duration(seconds: 2), + () { + final StringBuffer message = StringBuffer('Engine counters:\n'); + final List> entries = _counters.entries.toList() + ..sort((MapEntry a, MapEntry b) { + return a.key.compareTo(b.key); + }); + for (MapEntry entry in entries) { + message.writeln(' ${entry.key}: ${entry.value}'); + } + print(message); + _printTimer = null; + }, + ); + } +} diff --git a/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart b/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart index 2ec933b23e8d5..cfdb9e75a35d6 100644 --- a/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart +++ b/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart @@ -292,6 +292,9 @@ class TestSkDeletable implements SkDeletable { _isDeleted = true; deleteCount++; } + + @override + JsConstructor get constructor => TestJsConstructor('TestSkDeletable'); } class TestOneShotSkiaObject extends OneShotSkiaObject implements SkDeletable { @@ -310,6 +313,16 @@ class TestOneShotSkiaObject extends OneShotSkiaObject implements SkDele rawSkiaObject?.delete(); deleteCount++; } + + @override + JsConstructor get constructor => TestJsConstructor('TestOneShotSkiaObject'); +} + +class TestJsConstructor implements JsConstructor{ + TestJsConstructor(this.name); + + @override + final String name; } class TestSkiaObject extends ManagedSkiaObject {