blob: 59a9466e1f0c891cff58f02ced19254114391b75 [file] [log] [blame]
// 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;
},
);
}
}