| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:html' as html; |
| import 'dart:js_util' as js_util; |
| |
| import 'package:ui/ui.dart' as ui; |
| |
| import 'platform_dispatcher.dart'; |
| |
| /// A function that receives a benchmark [value] labeleb by [name]. |
| typedef OnBenchmark = void Function(String name, double value); |
| |
| /// A function that computes a value of type [R]. |
| /// |
| /// Functions of this signature can be passed to [timeAction] for performance |
| /// profiling. |
| typedef Action<R> = R Function(); |
| |
| /// Uses the [Profiler] to time a synchronous [action] function and reports the |
| /// result under the give metric [name]. |
| /// |
| /// If profiling is disabled, simply calls [action] and returns the result. |
| /// |
| /// Use this for situations when the cost of an extra closure is negligible. |
| /// This function reduces the boilerplate associated with checking if profiling |
| /// is enabled and exercising the stopwatch. |
| /// |
| /// Example: |
| /// |
| /// ``` |
| /// final result = timeAction('expensive_operation', () { |
| /// ... expensive work ... |
| /// return someValue; |
| /// }); |
| /// ``` |
| R timeAction<R>(String name, Action<R> action) { |
| if (!Profiler.isBenchmarkMode) { |
| return action(); |
| } else { |
| final Stopwatch stopwatch = Stopwatch()..start(); |
| final R result = action(); |
| stopwatch.stop(); |
| Profiler.instance.benchmark(name, stopwatch.elapsedMicroseconds.toDouble()); |
| return result; |
| } |
| } |
| |
| /// The purpose of this class is to facilitate communication of |
| /// profiling/benchmark data to the outside world (e.g. a macrobenchmark that's |
| /// running a flutter app). |
| /// |
| /// To use the [Profiler]: |
| /// |
| /// 1. Set the environment variable `FLUTTER_WEB_ENABLE_PROFILING` to true. |
| /// |
| /// 2. Using JS interop, assign a listener function to |
| /// `window._flutter_internal_on_benchmark` in the browser. |
| /// |
| /// The listener function will be called every time a new benchmark number is |
| /// calculated. The signature is `Function(String name, num value)`. |
| class Profiler { |
| Profiler._() { |
| _checkBenchmarkMode(); |
| } |
| |
| static bool isBenchmarkMode = const bool.fromEnvironment( |
| 'FLUTTER_WEB_ENABLE_PROFILING', |
| defaultValue: false, |
| ); |
| |
| static Profiler ensureInitialized() { |
| _checkBenchmarkMode(); |
| return Profiler._instance ??= Profiler._(); |
| } |
| |
| static Profiler get instance { |
| _checkBenchmarkMode(); |
| final Profiler? profiler = _instance; |
| if (profiler == null) { |
| throw Exception( |
| 'Profiler has not been properly initialized. ' |
| 'Make sure Profiler.ensureInitialized() is being called before you ' |
| 'access Profiler.instance', |
| ); |
| } |
| return profiler; |
| } |
| |
| static Profiler? _instance; |
| |
| static void _checkBenchmarkMode() { |
| if (!isBenchmarkMode) { |
| throw Exception( |
| 'Cannot use Profiler unless benchmark mode is enabled. ' |
| 'You can enable it by setting the `FLUTTER_WEB_ENABLE_PROFILING` ' |
| 'environment variable to true.', |
| ); |
| } |
| } |
| |
| /// Used to send benchmark data to whoever is listening to them. |
| void benchmark(String name, double value) { |
| _checkBenchmarkMode(); |
| |
| final OnBenchmark? onBenchmark = |
| // ignore: implicit_dynamic_function |
| js_util.getProperty(html.window, '_flutter_internal_on_benchmark') as OnBenchmark?; |
| if (onBenchmark != null) { |
| onBenchmark(name, value); |
| } |
| } |
| } |
| |
| /// Whether we are collecting [ui.FrameTiming]s. |
| bool get _frameTimingsEnabled { |
| return EnginePlatformDispatcher.instance.onReportTimings != null; |
| } |
| |
| /// Collects frame timings from frames. |
| /// |
| /// This list is periodically reported to the framework (see |
| /// [_kFrameTimingsSubmitInterval]). |
| List<ui.FrameTiming> _frameTimings = <ui.FrameTiming>[]; |
| |
| /// The amount of time in microseconds we wait between submitting |
| /// frame timings. |
| const int _kFrameTimingsSubmitInterval = 100000; // 100 milliseconds |
| |
| /// The last time (in microseconds) we submitted frame timings. |
| int _frameTimingsLastSubmitTime = _nowMicros(); |
| |
| // These variables store individual [ui.FrameTiming] properties. |
| int _vsyncStartMicros = -1; |
| int _buildStartMicros = -1; |
| int _buildFinishMicros = -1; |
| int _rasterStartMicros = -1; |
| int _rasterFinishMicros = -1; |
| |
| /// Records the vsync timestamp for this frame. |
| void frameTimingsOnVsync() { |
| if (!_frameTimingsEnabled) { |
| return; |
| } |
| _vsyncStartMicros = _nowMicros(); |
| } |
| |
| /// Records the time when the framework started building the frame. |
| void frameTimingsOnBuildStart() { |
| if (!_frameTimingsEnabled) { |
| return; |
| } |
| _buildStartMicros = _nowMicros(); |
| } |
| |
| /// Records the time when the framework finished building the frame. |
| void frameTimingsOnBuildFinish() { |
| if (!_frameTimingsEnabled) { |
| return; |
| } |
| _buildFinishMicros = _nowMicros(); |
| } |
| |
| /// Records the time when the framework started rasterizing the frame. |
| /// |
| /// On the web, this value is almost always the same as [_buildFinishMicros] |
| /// because it's single-threaded so there's no delay between building |
| /// and rasterization. |
| /// |
| /// This also means different things between HTML and CanvasKit renderers. |
| /// |
| /// In HTML "rasterization" only captures DOM updates, but not the work that |
| /// the browser performs after the DOM updates are committed. The browser |
| /// does not report that information. |
| /// |
| /// CanvasKit captures everything because we control the rasterization |
| /// process, so we know exactly when rasterization starts and ends. |
| void frameTimingsOnRasterStart() { |
| if (!_frameTimingsEnabled) { |
| return; |
| } |
| _rasterStartMicros = _nowMicros(); |
| } |
| |
| /// Records the time when the framework started rasterizing the frame. |
| /// |
| /// See [_frameTimingsOnRasterStart] for more details on what rasterization |
| /// timings mean on the web. |
| void frameTimingsOnRasterFinish() { |
| if (!_frameTimingsEnabled) { |
| return; |
| } |
| final int now = _nowMicros(); |
| _rasterFinishMicros = now; |
| _frameTimings.add(ui.FrameTiming( |
| vsyncStart: _vsyncStartMicros, |
| buildStart: _buildStartMicros, |
| buildFinish: _buildFinishMicros, |
| rasterStart: _rasterStartMicros, |
| rasterFinish: _rasterFinishMicros, |
| rasterFinishWallTime: _rasterFinishMicros, |
| )); |
| _vsyncStartMicros = -1; |
| _buildStartMicros = -1; |
| _buildFinishMicros = -1; |
| _rasterStartMicros = -1; |
| _rasterFinishMicros = -1; |
| if (now - _frameTimingsLastSubmitTime > _kFrameTimingsSubmitInterval) { |
| _frameTimingsLastSubmitTime = now; |
| EnginePlatformDispatcher.instance.invokeOnReportTimings(_frameTimings); |
| _frameTimings = <ui.FrameTiming>[]; |
| } |
| } |
| |
| /// Current timestamp in microseconds taken from the high-precision |
| /// monotonically increasing timer. |
| /// |
| /// See also: |
| /// |
| /// * https://developer.mozilla.org/en-US/docs/Web/API/Performance/now, |
| /// particularly notes about Firefox rounding to 1ms for security reasons, |
| /// which can be bypassed in tests by setting certain browser options. |
| 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 bool get enabled => _enabled; |
| static set enabled(bool value) { |
| if (_enabled == value) { |
| return; |
| } |
| |
| if (!value) { |
| _instance._counters.clear(); |
| _instance._printTimer = null; |
| } |
| |
| _enabled = value; |
| } |
| static 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 StateError( |
| '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.', |
| ); |
| } |
| } |
| |
| Map<String, int> get debugCounters => _counters; |
| final Map<String, int> _counters = <String, int>{}; |
| |
| Timer? get debugPrintTimer => _printTimer; |
| 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), |
| () { |
| if (_printTimer == null || !_enabled) { |
| return; |
| } |
| final StringBuffer message = StringBuffer('Engine counters:\n'); |
| // Entries are sorted for readability and testability. |
| final List<MapEntry<String, int>> entries = _counters.entries.toList() |
| ..sort((MapEntry<String, int> a, MapEntry<String, int> b) { |
| return a.key.compareTo(b.key); |
| }); |
| for (final MapEntry<String, int> entry in entries) { |
| message.writeln(' ${entry.key}: ${entry.value}'); |
| } |
| print(message); |
| _printTimer = null; |
| }, |
| ); |
| } |
| } |