diff --git a/material_3_demo/lib/main.dart b/material_3_demo/lib/main.dart index 5a0ec563b..30426c56e 100644 --- a/material_3_demo/lib/main.dart +++ b/material_3_demo/lib/main.dart @@ -2,12 +2,31 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; + import 'package:flutter/material.dart'; +import 'package:web_startup_analyzer/web_startup_analyzer.dart'; import 'constants.dart'; import 'home.dart'; -void main() { +void main() async { + var analyzer = WebStartupAnalyzer(additionalFrameCount: 10); + debugPrint(json.encode(analyzer.startupTiming)); + analyzer.onFirstFrame.addListener(() { + debugPrint(json.encode({'firstFrame': analyzer.onFirstFrame.value})); + }); + analyzer.onFirstPaint.addListener(() { + debugPrint(json.encode({ + 'firstPaint': analyzer.onFirstPaint.value?.$1, + 'firstContentfulPaint': analyzer.onFirstPaint.value?.$2, + })); + }); + analyzer.onAdditionalFrames.addListener(() { + debugPrint(json.encode({ + 'additionalFrames': analyzer.onAdditionalFrames.value, + })); + }); runApp( const App(), ); diff --git a/material_3_demo/pubspec.yaml b/material_3_demo/pubspec.yaml index 25be7578b..0f3b334f7 100644 --- a/material_3_demo/pubspec.yaml +++ b/material_3_demo/pubspec.yaml @@ -9,6 +9,7 @@ version: 1.0.0+1 environment: sdk: ^3.2.0 + flutter: ^3.16.0 dependencies: flutter: @@ -16,6 +17,8 @@ dependencies: cupertino_icons: ^1.0.2 url_launcher: ^6.1.8 + web_startup_analyzer: + path: ../web/_packages/web_startup_analyzer dev_dependencies: analysis_defaults: diff --git a/material_3_demo/web/index.html b/material_3_demo/web/index.html index 2168a65da..8f59269bf 100644 --- a/material_3_demo/web/index.html +++ b/material_3_demo/web/index.html @@ -38,22 +38,30 @@ + - + }); + diff --git a/web/_packages/web_startup_analyzer/example/pubspec.yaml b/web/_packages/web_startup_analyzer/example/pubspec.yaml index ca456165c..92007fc6a 100644 --- a/web/_packages/web_startup_analyzer/example/pubspec.yaml +++ b/web/_packages/web_startup_analyzer/example/pubspec.yaml @@ -3,7 +3,8 @@ description: "flutter_web_startup_analyzer example" publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=3.4.0-16.0.dev <4.0.0' + sdk: ^3.2.0 + flutter: ^3.16.0 dependencies: flutter: sdk: flutter diff --git a/web/_packages/web_startup_analyzer/lib/src/web_startup_analyzer.dart b/web/_packages/web_startup_analyzer/lib/src/web_startup_analyzer.dart new file mode 100644 index 000000000..3a2d765f6 --- /dev/null +++ b/web/_packages/web_startup_analyzer/lib/src/web_startup_analyzer.dart @@ -0,0 +1,103 @@ +// Copyright 2021 The Flutter team. 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:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:flutter/widgets.dart'; +import 'package:web_startup_analyzer/src/web_startup_analyzer_base.dart'; + +import 'frame_analyzer.dart'; +import 'startup_analyzer.dart'; + +class WebStartupAnalyzer extends WebStartupAnalyzerBase { + final WidgetsBinding _widgetsBinding; + late final FrameAnalyzer _frameAnalyzer; + List? _additionalFrames; + + @override + Map startupTiming = {}; + + @override + double get domContentLoaded => + (flutterWebStartupAnalyzer.timings['domContentLoaded'] as JSNumber) + .toDartDouble; + @override + double get loadEntrypoint => + (flutterWebStartupAnalyzer.timings['loadEntrypoint'] as JSNumber) + .toDartDouble; + @override + double get initializeEngine => + (flutterWebStartupAnalyzer.timings['initializeEngine'] as JSNumber) + .toDartDouble; + @override + double get appRunnerRunApp => + (flutterWebStartupAnalyzer.timings['appRunnerRunApp'] as JSNumber) + .toDartDouble; + @override + double? get firstFrame => + (flutterWebStartupAnalyzer.timings['firstFrame'] as JSNumber?) + ?.toDartDouble; + @override + double? get firstPaint => + (flutterWebStartupAnalyzer.timings['first-paint'] as JSNumber?) + ?.toDartDouble; + @override + double? get firstContentfulPaint => + (flutterWebStartupAnalyzer.timings['first-contentful-paint'] as JSNumber?) + ?.toDartDouble; + @override + List? get additionalFrames => _additionalFrames; + + WebStartupAnalyzer({int additionalFrameCount = 5}) + : _widgetsBinding = WidgetsFlutterBinding.ensureInitialized() { + _frameAnalyzer = + FrameAnalyzer(_widgetsBinding, additionalFrames: additionalFrameCount); + _captureStartupMetrics(); + startupTiming = { + 'domContentLoaded': domContentLoaded, + 'loadEntrypoint': loadEntrypoint, + 'initializeEngine': initializeEngine, + 'appRunnerRunApp': appRunnerRunApp, + }; + _captureFirstFrame().then((value) { + flutterWebStartupAnalyzer.captureAll(); + onFirstFrame.value = firstFrame; + + // Capture first-paint and first-contentful-paint + Future.delayed(const Duration(milliseconds: 200)).then((_) { + flutterWebStartupAnalyzer.capturePaint(); + onFirstPaint.value = (firstPaint!, firstContentfulPaint!); + }); + }); + captureFlutterFrameData().then((value) { + _additionalFrames = value; + onAdditionalFrames.value = value; + }); + onChange = + Listenable.merge([onFirstFrame, onFirstPaint, onAdditionalFrames]); + } + + _captureStartupMetrics() { + flutterWebStartupAnalyzer.markFinished('appRunnerRunApp'); + flutterWebStartupAnalyzer.captureAll(); + } + + Future _captureFirstFrame() { + final completer = Completer(); + flutterWebStartupAnalyzer.markStart('firstFrame'); + _widgetsBinding.addPostFrameCallback((timeStamp) { + flutterWebStartupAnalyzer.markFinished('firstFrame'); + flutterWebStartupAnalyzer.capture('firstFrame'); + completer.complete(); + }); + return completer.future; + } + + Future> captureFlutterFrameData() async { + await _frameAnalyzer.captureAdditionalFrames(); + return _frameAnalyzer.additionalFrameTimes; + } +} diff --git a/web/_packages/web_startup_analyzer/lib/src/web_startup_analyzer_base.dart b/web/_packages/web_startup_analyzer/lib/src/web_startup_analyzer_base.dart new file mode 100644 index 000000000..ea3a08a91 --- /dev/null +++ b/web/_packages/web_startup_analyzer/lib/src/web_startup_analyzer_base.dart @@ -0,0 +1,25 @@ +// Copyright 2021 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +// Base class for the web (real) implementation and dart:io (stub) +// implementation. +abstract class WebStartupAnalyzerBase { + late final Listenable onChange; + ValueNotifier onFirstFrame = ValueNotifier(null); + ValueNotifier<(double, double)?> onFirstPaint = ValueNotifier(null); + ValueNotifier?> onAdditionalFrames = ValueNotifier(null); + + double get domContentLoaded; + double get loadEntrypoint; + double get initializeEngine; + double get appRunnerRunApp; + double? get firstFrame; + double? get firstPaint; + double? get firstContentfulPaint; + List? get additionalFrames; + + Map get startupTiming; +} diff --git a/web/_packages/web_startup_analyzer/lib/src/web_startup_analyzer_io.dart b/web/_packages/web_startup_analyzer/lib/src/web_startup_analyzer_io.dart new file mode 100644 index 000000000..287ecfda2 --- /dev/null +++ b/web/_packages/web_startup_analyzer/lib/src/web_startup_analyzer_io.dart @@ -0,0 +1,37 @@ +// Copyright 2021 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'web_startup_analyzer_base.dart'; + +// This class is a stub so that unit tests can run without importing +// dart:js_interop and related packages. +class WebStartupAnalyzer extends WebStartupAnalyzerBase { + WebStartupAnalyzer({int additionalFrameCount = 0}); + + List? get additionalFrames => []; + + @override + double get appRunnerRunApp => 0.0; + + @override + double get domContentLoaded => 0.0; + + @override + double? get firstContentfulPaint => 0.0; + + @override + double? get firstFrame => 0.0; + + @override + double? get firstPaint => 0.0; + + @override + double get initializeEngine => 0.0; + + @override + double get loadEntrypoint => 0.0; + + @override + Map get startupTiming => {}; +} diff --git a/web/_packages/web_startup_analyzer/lib/web_startup_analyzer.dart b/web/_packages/web_startup_analyzer/lib/web_startup_analyzer.dart index e90ffb2c5..8d7e8a549 100644 --- a/web/_packages/web_startup_analyzer/lib/web_startup_analyzer.dart +++ b/web/_packages/web_startup_analyzer/lib/web_startup_analyzer.dart @@ -2,96 +2,5 @@ // 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:js_interop'; -import 'dart:js_interop_unsafe'; - -import 'package:flutter/widgets.dart'; - -import 'src/frame_analyzer.dart'; -import 'src/startup_analyzer.dart'; - -class WebStartupAnalyzer { - final WidgetsBinding _widgetsBinding; - late final FrameAnalyzer _frameAnalyzer; - List? _additionalFrames; - - late final Listenable onChange; - Map startupTiming = {}; - ValueNotifier onFirstFrame = ValueNotifier(null); - ValueNotifier<(double, double)?> onFirstPaint = ValueNotifier(null); - ValueNotifier?> onAdditionalFrames = ValueNotifier(null); - - double get domContentLoaded => - (flutterWebStartupAnalyzer.timings['domContentLoaded'] as JSNumber) - .toDartDouble; - double get loadEntrypoint => - (flutterWebStartupAnalyzer.timings['loadEntrypoint'] as JSNumber) - .toDartDouble; - double get initializeEngine => - (flutterWebStartupAnalyzer.timings['initializeEngine'] as JSNumber) - .toDartDouble; - double get appRunnerRunApp => - (flutterWebStartupAnalyzer.timings['appRunnerRunApp'] as JSNumber) - .toDartDouble; - double? get firstFrame => - (flutterWebStartupAnalyzer.timings['firstFrame'] as JSNumber?) - ?.toDartDouble; - double? get firstPaint => - (flutterWebStartupAnalyzer.timings['first-paint'] as JSNumber?) - ?.toDartDouble; - double? get firstContentfulPaint => - (flutterWebStartupAnalyzer.timings['first-contentful-paint'] as JSNumber?) - ?.toDartDouble; - List? get additionalFrames => _additionalFrames; - - WebStartupAnalyzer({int additionalFrameCount = 5}) - : _widgetsBinding = WidgetsFlutterBinding.ensureInitialized() { - _frameAnalyzer = - FrameAnalyzer(_widgetsBinding, additionalFrames: additionalFrameCount); - _captureStartupMetrics(); - startupTiming = { - 'domContentLoaded': domContentLoaded, - 'loadEntrypoint': loadEntrypoint, - 'initializeEngine': initializeEngine, - 'appRunnerRunApp': appRunnerRunApp, - }; - _captureFirstFrame().then((value) { - flutterWebStartupAnalyzer.captureAll(); - onFirstFrame.value = firstFrame; - - // Capture first-paint and first-contentful-paint - Future.delayed(const Duration(milliseconds: 200)).then((_) { - flutterWebStartupAnalyzer.capturePaint(); - onFirstPaint.value = (firstPaint!, firstContentfulPaint!); - }); - }); - captureFlutterFrameData().then((value) { - _additionalFrames = value; - onAdditionalFrames.value = value; - }); - onChange = - Listenable.merge([onFirstFrame, onFirstPaint, onAdditionalFrames]); - } - - _captureStartupMetrics() { - flutterWebStartupAnalyzer.markFinished('appRunnerRunApp'); - flutterWebStartupAnalyzer.captureAll(); - } - - Future _captureFirstFrame() { - final completer = Completer(); - flutterWebStartupAnalyzer.markStart('firstFrame'); - _widgetsBinding.addPostFrameCallback((timeStamp) { - flutterWebStartupAnalyzer.markFinished('firstFrame'); - flutterWebStartupAnalyzer.capture('firstFrame'); - completer.complete(); - }); - return completer.future; - } - - Future> captureFlutterFrameData() async { - await _frameAnalyzer.captureAdditionalFrames(); - return _frameAnalyzer.additionalFrameTimes; - } -} +export 'src/web_startup_analyzer_io.dart' + if (dart.library.js_interop) 'src/web_startup_analyzer.dart';