diff --git a/experimental/material_3_demo/lib/main.dart b/experimental/material_3_demo/lib/main.dart index 5a0ec563b..30426c56e 100644 --- a/experimental/material_3_demo/lib/main.dart +++ b/experimental/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/experimental/material_3_demo/pubspec.yaml b/experimental/material_3_demo/pubspec.yaml index 98ee2daa5..ee9449aa1 100644 --- a/experimental/material_3_demo/pubspec.yaml +++ b/experimental/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/experimental/material_3_demo/web/index.html b/experimental/material_3_demo/web/index.html index 2168a65da..8f59269bf 100644 --- a/experimental/material_3_demo/web/index.html +++ b/experimental/material_3_demo/web/index.html @@ -38,22 +38,30 @@ + - + }); + diff --git a/tool/flutter_ci_script_beta.sh b/tool/flutter_ci_script_beta.sh index 82f71bdea..f7e1d228c 100755 --- a/tool/flutter_ci_script_beta.sh +++ b/tool/flutter_ci_script_beta.sh @@ -32,7 +32,8 @@ declare -ar PROJECT_NAMES=( "experimental/federated_plugin/federated_plugin_web" "experimental/federated_plugin/federated_plugin_windows" "experimental/linting_tool" - "experimental/material_3_demo" + # TODO(DomesticMouse): re-enable once deps allow + # "experimental/material_3_demo" "experimental/pedometer" "experimental/pedometer/example" # TODO(DomesticMouse): Dart formatting required diff --git a/tool/flutter_ci_script_stable.sh b/tool/flutter_ci_script_stable.sh index 0fb47212a..eebae0fd3 100755 --- a/tool/flutter_ci_script_stable.sh +++ b/tool/flutter_ci_script_stable.sh @@ -32,7 +32,8 @@ declare -ar PROJECT_NAMES=( "experimental/federated_plugin/federated_plugin_web" "experimental/federated_plugin/federated_plugin_windows" "experimental/linting_tool" - "experimental/material_3_demo" + # TODO(DomesticMouse): re-enable once deps allow + # "experimental/material_3_demo" "experimental/pedometer" "experimental/pedometer/example" "experimental/varfont_shader_puzzle" diff --git a/web/_packages/web_startup_analyzer/.gitignore b/web/_packages/web_startup_analyzer/.gitignore new file mode 100644 index 000000000..ac5aa9893 --- /dev/null +++ b/web/_packages/web_startup_analyzer/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/web/_packages/web_startup_analyzer/.metadata b/web/_packages/web_startup_analyzer/.metadata new file mode 100644 index 000000000..d8c07e3ff --- /dev/null +++ b/web/_packages/web_startup_analyzer/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "f5fb61b953a631f47191124a31169701911ee1f4" + channel: "main" + +project_type: package diff --git a/web/_packages/web_startup_analyzer/analysis_options.yaml b/web/_packages/web_startup_analyzer/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/web/_packages/web_startup_analyzer/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/web/_packages/web_startup_analyzer/example/.gitignore b/web/_packages/web_startup_analyzer/example/.gitignore new file mode 100644 index 000000000..29a3a5017 --- /dev/null +++ b/web/_packages/web_startup_analyzer/example/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/web/_packages/web_startup_analyzer/example/.metadata b/web/_packages/web_startup_analyzer/example/.metadata new file mode 100644 index 000000000..0ff836c25 --- /dev/null +++ b/web/_packages/web_startup_analyzer/example/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "f5fb61b953a631f47191124a31169701911ee1f4" + channel: "main" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f5fb61b953a631f47191124a31169701911ee1f4 + base_revision: f5fb61b953a631f47191124a31169701911ee1f4 + - platform: android + create_revision: f5fb61b953a631f47191124a31169701911ee1f4 + base_revision: f5fb61b953a631f47191124a31169701911ee1f4 + - platform: ios + create_revision: f5fb61b953a631f47191124a31169701911ee1f4 + base_revision: f5fb61b953a631f47191124a31169701911ee1f4 + - platform: linux + create_revision: f5fb61b953a631f47191124a31169701911ee1f4 + base_revision: f5fb61b953a631f47191124a31169701911ee1f4 + - platform: macos + create_revision: f5fb61b953a631f47191124a31169701911ee1f4 + base_revision: f5fb61b953a631f47191124a31169701911ee1f4 + - platform: web + create_revision: f5fb61b953a631f47191124a31169701911ee1f4 + base_revision: f5fb61b953a631f47191124a31169701911ee1f4 + - platform: windows + create_revision: f5fb61b953a631f47191124a31169701911ee1f4 + base_revision: f5fb61b953a631f47191124a31169701911ee1f4 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/web/_packages/web_startup_analyzer/example/analysis_options.yaml b/web/_packages/web_startup_analyzer/example/analysis_options.yaml new file mode 100644 index 000000000..57346b583 --- /dev/null +++ b/web/_packages/web_startup_analyzer/example/analysis_options.yaml @@ -0,0 +1,3 @@ +include: package:flutter_lints/flutter.yaml +linter: + rules: diff --git a/web/_packages/web_startup_analyzer/example/lib/main.dart b/web/_packages/web_startup_analyzer/example/lib/main.dart new file mode 100644 index 000000000..7cdc19335 --- /dev/null +++ b/web/_packages/web_startup_analyzer/example/lib/main.dart @@ -0,0 +1,152 @@ +// 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:convert'; + +import 'package:flutter/material.dart'; +import 'package:web_startup_analyzer/web_startup_analyzer.dart'; + +main() async { + var analyzer = WebStartupAnalyzer(additionalFrameCount: 10); + print(json.encode(analyzer.startupTiming)); + analyzer.onFirstFrame.addListener(() { + print(json.encode({'firstFrame': analyzer.onFirstFrame.value})); + }); + analyzer.onFirstPaint.addListener(() { + print(json.encode({ + 'firstPaint': analyzer.onFirstPaint.value?.$1, + 'firstContentfulPaint': analyzer.onFirstPaint.value?.$2, + })); + }); + analyzer.onAdditionalFrames.addListener(() { + print(json.encode({ + 'additionalFrames': analyzer.onAdditionalFrames.value, + })); + }); + runApp( + WebStartupAnalyzerSample( + analyzer: analyzer, + ), + ); +} + +class WebStartupAnalyzerSample extends StatelessWidget { + final WebStartupAnalyzer analyzer; + const WebStartupAnalyzerSample({super.key, required this.analyzer}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter web app timing', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.green.shade100), + useMaterial3: true, + ), + home: WebStartupAnalyzerScreen(analyzer: analyzer), + ); + } +} + +class WebStartupAnalyzerScreen extends StatefulWidget { + final WebStartupAnalyzer analyzer; + + const WebStartupAnalyzerScreen({super.key, required this.analyzer}); + + @override + State createState() => + _WebStartupAnalyzerScreenState(); +} + +class _WebStartupAnalyzerScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.amber.shade50, + body: Align( + alignment: Alignment.topCenter, + child: Container( + margin: const EdgeInsets.all(8.0), + constraints: const BoxConstraints(maxWidth: 400), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + ), + child: ListenableBuilder( + listenable: widget.analyzer.onChange, + builder: (BuildContext context, child) { + return ListView( + shrinkWrap: true, + children: [ + TimingWidget( + name: 'DCL', + timingMs: widget.analyzer.domContentLoaded, + ), + TimingWidget( + name: 'Load entrypoint', + timingMs: widget.analyzer.loadEntrypoint, + ), + TimingWidget( + name: 'Initialize engine', + timingMs: widget.analyzer.initializeEngine, + ), + TimingWidget( + name: 'Run app', + timingMs: widget.analyzer.appRunnerRunApp, + ), + if (widget.analyzer.firstFrame != null) + TimingWidget( + name: 'First frame', + timingMs: widget.analyzer.firstFrame!, + ), + if (widget.analyzer.firstPaint != null) + TimingWidget( + name: 'First paint', + timingMs: widget.analyzer.firstPaint!), + if (widget.analyzer.firstContentfulPaint != null) + TimingWidget( + name: 'First contentful paint', + timingMs: widget.analyzer.firstContentfulPaint!), + if (widget.analyzer.additionalFrames != null) ...[ + for (var i in widget.analyzer.additionalFrames!) + TimingWidget(name: 'Frame', timingMs: i.toDouble()), + ] else + TextButton( + child: const Text('Trigger frames'), + onPressed: () {}, + ), + ], + ); + }, + ), + ), + ), + ); + } +} + +class TimingWidget extends StatelessWidget { + final String name; + final double timingMs; + + const TimingWidget({ + super.key, + required this.name, + required this.timingMs, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text( + name, + style: const TextStyle(fontSize: 18), + overflow: TextOverflow.ellipsis, + ), + trailing: Text( + '${timingMs.truncate()}ms', + style: const TextStyle(fontSize: 18), + ), + ); + } +} diff --git a/web/_packages/web_startup_analyzer/example/pubspec.yaml b/web/_packages/web_startup_analyzer/example/pubspec.yaml new file mode 100644 index 000000000..ca456165c --- /dev/null +++ b/web/_packages/web_startup_analyzer/example/pubspec.yaml @@ -0,0 +1,18 @@ +name: example +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' +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.6 + web_startup_analyzer: + path: ../ +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 +flutter: + uses-material-design: true diff --git a/web/_packages/web_startup_analyzer/example/web/favicon.png b/web/_packages/web_startup_analyzer/example/web/favicon.png new file mode 100644 index 000000000..8aaa46ac1 Binary files /dev/null and b/web/_packages/web_startup_analyzer/example/web/favicon.png differ diff --git a/web/_packages/web_startup_analyzer/example/web/icons/Icon-192.png b/web/_packages/web_startup_analyzer/example/web/icons/Icon-192.png new file mode 100644 index 000000000..b749bfef0 Binary files /dev/null and b/web/_packages/web_startup_analyzer/example/web/icons/Icon-192.png differ diff --git a/web/_packages/web_startup_analyzer/example/web/icons/Icon-512.png b/web/_packages/web_startup_analyzer/example/web/icons/Icon-512.png new file mode 100644 index 000000000..88cfd48df Binary files /dev/null and b/web/_packages/web_startup_analyzer/example/web/icons/Icon-512.png differ diff --git a/web/_packages/web_startup_analyzer/example/web/icons/Icon-maskable-192.png b/web/_packages/web_startup_analyzer/example/web/icons/Icon-maskable-192.png new file mode 100644 index 000000000..eb9b4d76e Binary files /dev/null and b/web/_packages/web_startup_analyzer/example/web/icons/Icon-maskable-192.png differ diff --git a/web/_packages/web_startup_analyzer/example/web/icons/Icon-maskable-512.png b/web/_packages/web_startup_analyzer/example/web/icons/Icon-maskable-512.png new file mode 100644 index 000000000..d69c56691 Binary files /dev/null and b/web/_packages/web_startup_analyzer/example/web/icons/Icon-maskable-512.png differ diff --git a/web/_packages/web_startup_analyzer/example/web/index.html b/web/_packages/web_startup_analyzer/example/web/index.html new file mode 100644 index 000000000..0160c6e1e --- /dev/null +++ b/web/_packages/web_startup_analyzer/example/web/index.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + web_perf_metrics example + + + + + + + + + + diff --git a/web/_packages/web_startup_analyzer/example/web/manifest.json b/web/_packages/web_startup_analyzer/example/web/manifest.json new file mode 100644 index 000000000..096edf8fe --- /dev/null +++ b/web/_packages/web_startup_analyzer/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/web/_packages/web_startup_analyzer/lib/src/frame_analyzer.dart b/web/_packages/web_startup_analyzer/lib/src/frame_analyzer.dart new file mode 100644 index 000000000..62e3dff4d --- /dev/null +++ b/web/_packages/web_startup_analyzer/lib/src/frame_analyzer.dart @@ -0,0 +1,43 @@ +// 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 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; + +class FrameAnalyzer { + final WidgetsBinding _binding; + final Completer _onDone = Completer(); + int _remainingFrames; + + final int additionalFrames; + List additionalFrameTimes = []; + + FrameAnalyzer(this._binding, {this.additionalFrames = 10}) + : _remainingFrames = additionalFrames; + + Future captureAdditionalFrames() { + _binding.addTimingsCallback(_timingsCallback); + return _onDone.future; + } + + _reportFrame(FrameTiming frameTiming) { + additionalFrameTimes.add(frameTiming.totalSpan.inMilliseconds); + } + + _timingsCallback(timings) { + int i = 0; + while (_remainingFrames > 0 && i < timings.length) { + _reportFrame(timings[i]); + i++; + _remainingFrames--; + } + if (_remainingFrames <= 0) { + _binding.removeTimingsCallback(_timingsCallback); + + _onDone.complete(); + } + } +} diff --git a/web/_packages/web_startup_analyzer/lib/src/startup_analyzer.dart b/web/_packages/web_startup_analyzer/lib/src/startup_analyzer.dart new file mode 100644 index 000000000..3f4913b19 --- /dev/null +++ b/web/_packages/web_startup_analyzer/lib/src/startup_analyzer.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 'dart:js_interop'; + +@JS() +@staticInterop +external FlutterWebStartupAnalyzer get flutterWebStartupAnalyzer; + +@JS() +@staticInterop +class FlutterWebStartupAnalyzer { + external factory FlutterWebStartupAnalyzer(); +} + +extension FlutterWebStartupAnalyzerExtensions on FlutterWebStartupAnalyzer { + external JSObject get timings; + external void markStart(String name); + external void markFinished(String name); + external void capture(String name); + external void captureAll(); + external void capturePaint(); + external void report(); +} diff --git a/web/_packages/web_startup_analyzer/lib/web_startup_analyzer.dart b/web/_packages/web_startup_analyzer/lib/web_startup_analyzer.dart new file mode 100644 index 000000000..e90ffb2c5 --- /dev/null +++ b/web/_packages/web_startup_analyzer/lib/web_startup_analyzer.dart @@ -0,0 +1,97 @@ +// 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 '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; + } +} diff --git a/web/_packages/web_startup_analyzer/lib/web_startup_analyzer.js b/web/_packages/web_startup_analyzer/lib/web_startup_analyzer.js new file mode 100644 index 000000000..a88ad0f20 --- /dev/null +++ b/web/_packages/web_startup_analyzer/lib/web_startup_analyzer.js @@ -0,0 +1,47 @@ +// 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. + +// Helper class to capture Flutter web app startup timing information +class FlutterWebStartupAnalyzer { + timings; + + constructor() { + this.timings = {}; + } + + markStart(name) { + this.timings[name] = null; + performance.mark('flt-' + name + '-started'); + } + markFinished(name) { + performance.mark('flt-' + name + '-finished'); + } + capture(name) { + var timingName = 'flt-' + name; + var started = 'flt-' + name + 'started'; + try { + var measurement = performance.measure('flt-' + name, 'flt-' + name + '-started', 'flt-' + name + '-finished'); + } catch(e) { + // ignore errors if the mark doesn't exist + return; + } + this.timings[name] = measurement.duration; + } + captureAll() { + for (var [key, value] of Object.entries(this.timings)) { + this.capture(key); + } + // Capture + this.timings['load'] = performance.timing.loadEventEnd - performance.timing.domContentLoadedEventEnd; + this.timings['domContentLoaded'] = performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart; + } + + capturePaint() { + const entries = performance.getEntriesByType("paint"); + // Collect first-paint and first-contentful-paint entries + entries.forEach((entry) => { + this.timings[entry.name] = entry.startTime; + }); + } +} diff --git a/web/_packages/web_startup_analyzer/pubspec.yaml b/web/_packages/web_startup_analyzer/pubspec.yaml new file mode 100644 index 000000000..5810bbb51 --- /dev/null +++ b/web/_packages/web_startup_analyzer/pubspec.yaml @@ -0,0 +1,19 @@ +name: web_startup_analyzer +description: "Captures web startup timing data in a Flutter web app" +version: 0.1.0-wip + +environment: + sdk: ^3.2.0 + flutter: ^3.16.0 + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 +flutter: + assets: + - lib/web_startup_analyzer.js diff --git a/web/_tool/verify_packages.dart b/web/_tool/verify_packages.dart index d5e1457f6..41c37ea49 100644 --- a/web/_tool/verify_packages.dart +++ b/web/_tool/verify_packages.dart @@ -11,6 +11,7 @@ import 'common.dart'; void main() async { final packageDirs = listPackageDirs(Directory.current) .map((path) => p.relative(path, from: Directory.current.path)) + .where((path) => !p.dirname(path).startsWith('_')) .toList(); print('Package dirs:\n${packageDirs.map((path) => ' $path').join('\n')}');