mirror of https://github.com/flutter/samples.git
Add web startup analyzer to material 3 demo (#2144)
This adds a tool to measure web app startup for the Material 3 demo. Demo: - [Example app](https://flutter-web-perf-experiments.web.app/) - [Material 3](https://flutter-web-perf-experiments--material3-vswzldcy.web.app/) (open console) --------- Co-authored-by: Brett Morgan <brett.morgan@gmail.com> Co-authored-by: Kevin Moore <kevmoo@google.com>pull/2150/head
parent
dc37f37b5c
commit
d96bb336b6
@ -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/
|
@ -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
|
@ -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
|
@ -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
|
@ -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'
|
@ -0,0 +1,3 @@
|
|||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
linter:
|
||||||
|
rules:
|
@ -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<WebStartupAnalyzerScreen> createState() =>
|
||||||
|
_WebStartupAnalyzerScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WebStartupAnalyzerScreenState extends State<WebStartupAnalyzerScreen> {
|
||||||
|
@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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
After Width: | Height: | Size: 917 B |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 8.1 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 20 KiB |
@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<base href="$FLUTTER_BASE_HREF">
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
|
<meta name="description" content="web_startup_analyzer example">
|
||||||
|
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="example">
|
||||||
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
|
<title>web_perf_metrics example</title>
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const serviceWorkerVersion = null;
|
||||||
|
</script>
|
||||||
|
<script src="flutter.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="text/javascript" src="assets/packages/web_startup_analyzer/lib/web_startup_analyzer.js"></script>
|
||||||
|
<script>
|
||||||
|
var flutterWebStartupAnalyzer = new FlutterWebStartupAnalyzer();
|
||||||
|
var analyzer = flutterWebStartupAnalyzer;
|
||||||
|
|
||||||
|
window.addEventListener('load', function(ev) {
|
||||||
|
analyzer.markStart("loadEntrypoint");
|
||||||
|
_flutter.loader.loadEntrypoint({
|
||||||
|
serviceWorker: {
|
||||||
|
serviceWorkerVersion: serviceWorkerVersion,
|
||||||
|
},
|
||||||
|
onEntrypointLoaded: function(engineInitializer) {
|
||||||
|
analyzer.markFinished("loadEntrypoint");
|
||||||
|
analyzer.markStart("initializeEngine");
|
||||||
|
engineInitializer.initializeEngine().then(function(appRunner) {
|
||||||
|
analyzer.markFinished("initializeEngine");
|
||||||
|
analyzer.markStart("appRunnerRunApp");
|
||||||
|
appRunner.runApp();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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<int> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -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<int>? _additionalFrames;
|
||||||
|
|
||||||
|
late final Listenable onChange;
|
||||||
|
Map<String, dynamic> startupTiming = {};
|
||||||
|
ValueNotifier<double?> onFirstFrame = ValueNotifier(null);
|
||||||
|
ValueNotifier<(double, double)?> onFirstPaint = ValueNotifier(null);
|
||||||
|
ValueNotifier<List<int>?> 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<int>? 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<void> _captureFirstFrame() {
|
||||||
|
final completer = Completer();
|
||||||
|
flutterWebStartupAnalyzer.markStart('firstFrame');
|
||||||
|
_widgetsBinding.addPostFrameCallback((timeStamp) {
|
||||||
|
flutterWebStartupAnalyzer.markFinished('firstFrame');
|
||||||
|
flutterWebStartupAnalyzer.capture('firstFrame');
|
||||||
|
completer.complete();
|
||||||
|
});
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<int>> captureFlutterFrameData() async {
|
||||||
|
await _frameAnalyzer.captureAdditionalFrames();
|
||||||
|
return _frameAnalyzer.additionalFrameTimes;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
Loading…
Reference in new issue