blob: 873dbc64cf37eb51138a8988f0c18291a77fb3bd [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:developer' as developer;
import 'dart:html' as html;
import 'dart:typed_data';
import 'package:ui/src/engine/assets.dart';
import 'package:ui/src/engine/browser_detection.dart';
import 'package:ui/src/engine/canvaskit/initialization.dart';
import 'package:ui/src/engine/embedder.dart';
import 'package:ui/src/engine/keyboard.dart';
import 'package:ui/src/engine/mouse_cursor.dart';
import 'package:ui/src/engine/navigation.dart';
import 'package:ui/src/engine/platform_dispatcher.dart';
import 'package:ui/src/engine/platform_views/content_manager.dart';
import 'package:ui/src/engine/profiler.dart';
import 'package:ui/src/engine/safe_browser_api.dart';
import 'package:ui/src/engine/text/font_collection.dart';
import 'package:ui/src/engine/text/line_break_properties.dart';
import 'package:ui/src/engine/window.dart';
import 'package:ui/ui.dart' as ui;
/// The mode the app is running in.
/// Keep these in sync with the same constants on the framework-side under foundation/constants.dart.
const bool kReleaseMode =
bool.fromEnvironment('dart.vm.product', defaultValue: false);
/// A constant that is true if the application was compiled in profile mode.
const bool kProfileMode =
bool.fromEnvironment('dart.vm.profile', defaultValue: false);
/// A constant that is true if the application was compiled in debug mode.
const bool kDebugMode = !kReleaseMode && !kProfileMode;
/// Returns mode of the app is running in as a string.
String get buildMode => kReleaseMode
? 'release'
: kProfileMode
? 'profile'
: 'debug';
/// A benchmark metric that includes frame-related computations prior to
/// submitting layer and picture operations to the underlying renderer, such as
/// HTML and CanvasKit. During this phase we compute transforms, clips, and
/// other information needed for rendering.
const String kProfilePrerollFrame = 'preroll_frame';
/// A benchmark metric that includes submitting layer and picture information
/// to the renderer.
const String kProfileApplyFrame = 'apply_frame';
final List<ui.VoidCallback> _hotRestartListeners = <ui.VoidCallback>[];
/// Requests that [listener] is called just before hot restarting the app.
void registerHotRestartListener(ui.VoidCallback listener) {
_hotRestartListeners.add(listener);
}
/// Pretends that hot restart is about to happen.
///
/// Useful in tests to check that the engine performs appropriate clean-ups,
/// such as removing static DOM listeners, prior to allowing the Dart runtime
/// to re-initialize the program.
void debugEmulateHotRestart() {
for (final ui.VoidCallback listener in _hotRestartListeners) {
listener();
}
}
/// Fully initializes the engine, including services and UI.
Future<void> initializeEngine({
AssetManager? assetManager,
}) async {
await initializeEngineServices(assetManager: assetManager);
await initializeEngineUi();
}
/// How far along the initialization process the engine is currently is.
///
/// The initialization process starts with [none] and proceeds in increasing
/// `index` number until [initialized].
enum DebugEngineInitializationState {
/// Initialization hasn't started yet.
uninitialized,
/// The engine is initializing its non-UI services.
initializingServices,
/// The engine has initialized its non-UI services, but hasn't started
/// initializing the UI.
initializedServices,
/// The engine started attaching UI surfaces to the web page.
initializingUi,
/// The engine has fully completed initialization.
///
/// At this point the framework can start using the engine for I/O, rendering,
/// etc.
///
/// This is the final state of the engine.
initialized,
}
/// The current initialization state of the engine.
///
/// See [DebugEngineInitializationState] for possible states.
DebugEngineInitializationState get initializationState => _initializationState;
DebugEngineInitializationState _initializationState = DebugEngineInitializationState.uninitialized;
/// Resets the state back to [DebugEngineInitializationState.uninitialized].
///
/// This is for testing only.
void debugResetEngineInitializationState() {
_initializationState = DebugEngineInitializationState.uninitialized;
}
/// Initializes non-UI engine services.
///
/// Does not put any UI onto the page. It is therefore safe to call this
/// function while the page is showing non-Flutter UI, such as a loading
/// indicator, a splash screen, or in an add-to-app scenario where the host page
/// is written using a different web framework.
///
/// See also:
///
/// * [initializeEngineUi], which is typically called after this function, and
/// puts UI elements on the page.
Future<void> initializeEngineServices({
AssetManager? assetManager,
}) async {
if (_initializationState != DebugEngineInitializationState.uninitialized) {
assert(() {
throw StateError(
'Invalid engine initialization state. `initializeEngineServices` was '
'called, but the engine has already started initialization and is '
'currently in state "$_initializationState".'
);
}());
return;
}
_initializationState = DebugEngineInitializationState.initializingServices;
if (!useCanvasKit) {
scheduleMicrotask(() {
// Access [lineLookup] to force the lazy unpacking of line break data
// now. Removing this line won't break anything. It's just an optimization
// to make the unpacking happen while we are waiting for network requests.
lineLookup;
});
}
// Setup the hook that allows users to customize URL strategy before running
// the app.
_addUrlStrategyListener();
// Called by the Web runtime just before hot restarting the app.
//
// This extension cleans up resources that are registered with browser's
// global singletons that Dart compiler is unable to clean-up automatically.
//
// This extension does not need to clean-up Dart statics. Those are cleaned
// up by the compiler.
developer.registerExtension('ext.flutter.disassemble', (_, __) {
for (final ui.VoidCallback listener in _hotRestartListeners) {
listener();
}
return Future<developer.ServiceExtensionResponse>.value(
developer.ServiceExtensionResponse.result('OK'));
});
if (Profiler.isBenchmarkMode) {
Profiler.ensureInitialized();
}
bool waitingForAnimation = false;
scheduleFrameCallback = () {
// We're asked to schedule a frame and call `frameHandler` when the frame
// fires.
if (!waitingForAnimation) {
waitingForAnimation = true;
html.window.requestAnimationFrame((num highResTime) {
frameTimingsOnVsync();
// Reset immediately, because `frameHandler` can schedule more frames.
waitingForAnimation = false;
// We have to convert high-resolution time to `int` so we can construct
// a `Duration` out of it. However, high-res time is supplied in
// milliseconds as a double value, with sub-millisecond information
// hidden in the fraction. So we first multiply it by 1000 to uncover
// microsecond precision, and only then convert to `int`.
final int highResTimeMicroseconds = (1000 * highResTime).toInt();
// In Flutter terminology "building a frame" consists of "beginning
// frame" and "drawing frame".
//
// We do not call `frameTimingsOnBuildFinish` from here because
// part of the rasterization process, particularly in the HTML
// renderer, takes place in the `SceneBuilder.build()`.
frameTimingsOnBuildStart();
if (EnginePlatformDispatcher.instance.onBeginFrame != null) {
EnginePlatformDispatcher.instance.invokeOnBeginFrame(
Duration(microseconds: highResTimeMicroseconds));
}
if (EnginePlatformDispatcher.instance.onDrawFrame != null) {
// TODO(yjbanov): technically Flutter flushes microtasks between
// onBeginFrame and onDrawFrame. We don't, which hasn't
// been an issue yet, but eventually we'll have to
// implement it properly.
EnginePlatformDispatcher.instance.invokeOnDrawFrame();
}
});
}
};
// This needs to be after `initializeEngine` because that is where the
// canvaskit script is added to the page.
if (useCanvasKit) {
await initializeCanvasKit();
}
assetManager ??= const AssetManager();
await _setAssetManager(assetManager);
if (useCanvasKit) {
await skiaFontCollection.ensureFontsLoaded();
} else {
await _fontCollection!.ensureFontsLoaded();
}
_initializationState = DebugEngineInitializationState.initializedServices;
}
/// Initializes the UI surfaces for the Flutter framework to render to.
///
/// Must be called after [initializeEngineServices].
///
/// This function will start altering the HTML structure of the page. If used
/// in an add-to-app scenario, the host page is expected to prepare for Flutter
/// UI appearing on screen prior to calling this function.
Future<void> initializeEngineUi() async {
if (_initializationState != DebugEngineInitializationState.initializedServices) {
assert(() {
throw StateError(
'Invalid engine initialization state. `initializeEngineUi` was '
'called while the engine initialization state was '
'"$_initializationState". `initializeEngineUi` can only be called '
'when the engine is in state '
'"${DebugEngineInitializationState.initializedServices}".'
);
}());
return;
}
_initializationState = DebugEngineInitializationState.initializingUi;
Keyboard.initialize(onMacOs: operatingSystem == OperatingSystem.macOs);
MouseCursor.initialize();
ensureFlutterViewEmbedderInitialized();
_initializationState = DebugEngineInitializationState.initialized;
}
AssetManager get assetManager => _assetManager!;
AssetManager? _assetManager;
FontCollection get fontCollection => _fontCollection!;
FontCollection? _fontCollection;
Future<void> _setAssetManager(AssetManager assetManager) async {
// ignore: unnecessary_null_comparison
assert(assetManager != null, 'Cannot set assetManager to null');
if (assetManager == _assetManager) {
return;
}
_assetManager = assetManager;
if (useCanvasKit) {
ensureSkiaFontCollectionInitialized();
} else {
_fontCollection ??= FontCollection();
_fontCollection!.clear();
}
if (_assetManager != null) {
if (useCanvasKit) {
await skiaFontCollection.registerFonts(_assetManager!);
} else {
await _fontCollection!.registerFonts(_assetManager!);
}
}
if (ui.debugEmulateFlutterTesterEnvironment) {
if (useCanvasKit) {
skiaFontCollection.debugRegisterTestFonts();
} else {
_fontCollection!.debugRegisterTestFonts();
}
}
}
void _addUrlStrategyListener() {
jsSetUrlStrategy = allowInterop((JsUrlStrategy? jsStrategy) {
customUrlStrategy =
jsStrategy == null ? null : CustomUrlStrategy.fromJs(jsStrategy);
});
registerHotRestartListener(() {
jsSetUrlStrategy = null;
});
}
/// The shared instance of PlatformViewManager shared across the engine to handle
/// rendering of PlatformViews into the web app.
// TODO(dit): How to make this overridable from tests?
final PlatformViewManager platformViewManager = PlatformViewManager();
// TODO(yjbanov): this does not belong here.
// https://github.com/flutter/flutter/issues/100394
/// Converts a matrix represented using [Float64List] to one represented using
/// [Float32List].
///
/// 32-bit precision is sufficient because Flutter Engine itself (as well as
/// Skia) use 32-bit precision under the hood anyway.
///
/// 32-bit matrices require 2x less memory and in V8 they are allocated on the
/// JavaScript heap, thus avoiding a malloc.
///
/// See also:
/// * https://bugs.chromium.org/p/v8/issues/detail?id=9199
/// * https://bugs.chromium.org/p/v8/issues/detail?id=2022
Float32List toMatrix32(Float64List matrix64) {
final Float32List matrix32 = Float32List(16);
matrix32[15] = matrix64[15];
matrix32[14] = matrix64[14];
matrix32[13] = matrix64[13];
matrix32[12] = matrix64[12];
matrix32[11] = matrix64[11];
matrix32[10] = matrix64[10];
matrix32[9] = matrix64[9];
matrix32[8] = matrix64[8];
matrix32[7] = matrix64[7];
matrix32[6] = matrix64[6];
matrix32[5] = matrix64[5];
matrix32[4] = matrix64[4];
matrix32[3] = matrix64[3];
matrix32[2] = matrix64[2];
matrix32[1] = matrix64[1];
matrix32[0] = matrix64[0];
return matrix32;
}