1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:convert' show json;
7import 'dart:io' as io;
8
9import 'package:logging/logging.dart';
10import 'package:path/path.dart' as path;
11import 'package:shelf/shelf.dart';
12import 'package:shelf/shelf_io.dart' as shelf_io;
13import 'package:shelf_static/shelf_static.dart';
14
15import '../framework/browser.dart';
16import '../framework/task_result.dart';
17import '../framework/utils.dart';
18
19/// The port number used by the local benchmark server.
20const int benchmarkServerPort = 9999;
21const int chromeDebugPort = 10000;
22
23typedef WebBenchmarkOptions = ({bool useWasm, bool forceSingleThreadedSkwasm});
24
25Future<TaskResult> runWebBenchmark(WebBenchmarkOptions benchmarkOptions) async {
26 // Reduce logging level. Otherwise, package:webkit_inspection_protocol is way too spammy.
27 Logger.root.level = Level.INFO;
28 final String macrobenchmarksDirectory = path.join(
29 flutterDirectory.path,
30 'dev',
31 'benchmarks',
32 'macrobenchmarks',
33 );
34 return inDirectory(macrobenchmarksDirectory, () async {
35 await flutter('clean');
36 await evalFlutter(
37 'build',
38 options: <String>[
39 'web',
40 '--no-tree-shake-icons', // local engine builds are frequently out of sync with the Dart Kernel version
41 if (benchmarkOptions.useWasm) ...<String>['--wasm', '--no-strip-wasm'],
42 '--dart-define=FLUTTER_WEB_ENABLE_PROFILING=true',
43 '--profile',
44 '--no-web-resources-cdn',
45 '-t',
46 'lib/web_benchmarks.dart',
47 ],
48 );
49 final Completer<List<Map<String, dynamic>>> profileData =
50 Completer<List<Map<String, dynamic>>>();
51 final List<Map<String, dynamic>> collectedProfiles = <Map<String, dynamic>>[];
52 List<String>? benchmarks;
53 late Iterator<String> benchmarkIterator;
54
55 // This future fixes a race condition between the web-page loading and
56 // asking to run a benchmark, and us connecting to Chrome's DevTools port.
57 // Sometime one wins. Other times, the other wins.
58 Future<Chrome>? whenChromeIsReady;
59 Chrome? chrome;
60 late io.HttpServer server;
61 Cascade cascade = Cascade();
62 List<Map<String, dynamic>>? latestPerformanceTrace;
63 cascade = cascade
64 .add((Request request) async {
65 try {
66 chrome ??= await whenChromeIsReady;
67 if (request.requestedUri.path.endsWith('/profile-data')) {
68 final Map<String, dynamic> profile =
69 json.decode(await request.readAsString()) as Map<String, dynamic>;
70 final String benchmarkName = profile['name'] as String;
71 if (benchmarkName != benchmarkIterator.current) {
72 profileData.completeError(
73 Exception(
74 'Browser returned benchmark results from a wrong benchmark.\n'
75 'Requested to run benchmark ${benchmarkIterator.current}, but '
76 'got results for $benchmarkName.',
77 ),
78 );
79 unawaited(server.close());
80 }
81
82 // Trace data is null when the benchmark is not frame-based, such as RawRecorder.
83 if (latestPerformanceTrace != null) {
84 final BlinkTraceSummary traceSummary =
85 BlinkTraceSummary.fromJson(latestPerformanceTrace!)!;
86 profile['totalUiFrame.average'] =
87 traceSummary.averageTotalUIFrameTime.inMicroseconds;
88 profile['scoreKeys'] ??= <dynamic>[]; // using dynamic for consistency with JSON
89 (profile['scoreKeys'] as List<dynamic>).add('totalUiFrame.average');
90 latestPerformanceTrace = null;
91 }
92 collectedProfiles.add(profile);
93 return Response.ok('Profile received');
94 } else if (request.requestedUri.path.endsWith('/start-performance-tracing')) {
95 latestPerformanceTrace = null;
96 await chrome!.beginRecordingPerformance(
97 request.requestedUri.queryParameters['label']!,
98 );
99 return Response.ok('Started performance tracing');
100 } else if (request.requestedUri.path.endsWith('/stop-performance-tracing')) {
101 latestPerformanceTrace = await chrome!.endRecordingPerformance();
102 return Response.ok('Stopped performance tracing');
103 } else if (request.requestedUri.path.endsWith('/on-error')) {
104 final Map<String, dynamic> errorDetails =
105 json.decode(await request.readAsString()) as Map<String, dynamic>;
106 unawaited(server.close());
107 // Keep the stack trace as a string. It's thrown in the browser, not this Dart VM.
108 profileData.completeError('${errorDetails['error']}\n${errorDetails['stackTrace']}');
109 return Response.ok('');
110 } else if (request.requestedUri.path.endsWith('/next-benchmark')) {
111 if (benchmarks == null) {
112 benchmarks =
113 (json.decode(await request.readAsString()) as List<dynamic>).cast<String>();
114 benchmarkIterator = benchmarks!.iterator;
115 }
116 if (benchmarkIterator.moveNext()) {
117 final String nextBenchmark = benchmarkIterator.current;
118 print('Launching benchmark "$nextBenchmark"');
119 return Response.ok(nextBenchmark);
120 } else {
121 profileData.complete(collectedProfiles);
122 return Response.notFound('Finished running benchmarks.');
123 }
124 } else if (request.requestedUri.path.endsWith('/print-to-console')) {
125 // A passthrough used by
126 // `dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart`
127 // to print information.
128 final String message = await request.readAsString();
129 print('[APP] $message');
130 return Response.ok('Reported.');
131 } else {
132 return Response.notFound('This request is not handled by the profile-data handler.');
133 }
134 } catch (error, stackTrace) {
135 profileData.completeError(error, stackTrace);
136 return Response.internalServerError(body: '$error');
137 }
138 })
139 .add(createBuildDirectoryHandler(path.join(macrobenchmarksDirectory, 'build', 'web')));
140
141 server = await io.HttpServer.bind('localhost', benchmarkServerPort);
142 try {
143 shelf_io.serveRequests(server, cascade.handler);
144
145 final String dartToolDirectory = path.join('$macrobenchmarksDirectory/.dart_tool');
146 final String userDataDir =
147 io.Directory(dartToolDirectory).createTempSync('flutter_chrome_user_data.').path;
148
149 // TODO(yjbanov): temporarily disables headful Chrome until we get
150 // devicelab hardware that is able to run it. Our current
151 // GCE VMs can only run in headless mode.
152 // See: https://github.com/flutter/flutter/issues/50164
153 final bool isUncalibratedSmokeTest = io.Platform.environment['CALIBRATED'] != 'true';
154 // final bool isUncalibratedSmokeTest =
155 // io.Platform.environment['UNCALIBRATED_SMOKE_TEST'] == 'true';
156 final String urlParams = benchmarkOptions.forceSingleThreadedSkwasm ? '?force_st=true' : '';
157 final ChromeOptions options = ChromeOptions(
158 url: 'http://localhost:$benchmarkServerPort/index.html$urlParams',
159 userDataDirectory: userDataDir,
160 headless: isUncalibratedSmokeTest,
161 debugPort: chromeDebugPort,
162 enableWasmGC: benchmarkOptions.useWasm,
163 );
164
165 print('Launching Chrome.');
166 whenChromeIsReady = Chrome.launch(
167 options,
168 onError: (String error) {
169 profileData.completeError(Exception(error));
170 },
171 workingDirectory: cwd,
172 );
173
174 print('Waiting for the benchmark to report benchmark profile.');
175 final Map<String, dynamic> taskResult = <String, dynamic>{};
176 final List<String> benchmarkScoreKeys = <String>[];
177 final List<Map<String, dynamic>> profiles = await profileData.future;
178
179 print('Received profile data');
180 for (final Map<String, dynamic> profile in profiles) {
181 final String benchmarkName = profile['name'] as String;
182 if (benchmarkName.isEmpty) {
183 throw 'Benchmark name is empty';
184 }
185
186 final String webRendererName;
187 if (benchmarkOptions.useWasm) {
188 webRendererName = benchmarkOptions.forceSingleThreadedSkwasm ? 'skwasm_st' : 'skwasm';
189 } else {
190 webRendererName = 'canvaskit';
191 }
192 final String namespace = '$benchmarkName.$webRendererName';
193 final List<String> scoreKeys = List<String>.from(profile['scoreKeys'] as List<dynamic>);
194 if (scoreKeys.isEmpty) {
195 throw 'No score keys in benchmark "$benchmarkName"';
196 }
197 for (final String scoreKey in scoreKeys) {
198 if (scoreKey.isEmpty) {
199 throw 'Score key is empty in benchmark "$benchmarkName". '
200 'Received [${scoreKeys.join(', ')}]';
201 }
202 benchmarkScoreKeys.add('$namespace.$scoreKey');
203 }
204
205 for (final String key in profile.keys) {
206 if (key == 'name' || key == 'scoreKeys') {
207 continue;
208 }
209 taskResult['$namespace.$key'] = profile[key];
210 }
211 }
212 return TaskResult.success(taskResult, benchmarkScoreKeys: benchmarkScoreKeys);
213 } finally {
214 unawaited(server.close());
215 chrome?.stop();
216 }
217 });
218}
219
220Handler createBuildDirectoryHandler(String buildDirectoryPath) {
221 final Handler childHandler = createStaticHandler(buildDirectoryPath);
222 return (Request request) async {
223 final Response response = await childHandler(request);
224 final String? mimeType = response.mimeType;
225
226 // Provide COOP/COEP headers so that the browser loads the page as
227 // crossOriginIsolated. This will make sure that we get high-resolution
228 // timers for our benchmark measurements.
229 if (mimeType == 'text/html' || mimeType == 'text/javascript') {
230 return response.change(
231 headers: <String, String>{
232 'Cross-Origin-Opener-Policy': 'same-origin',
233 'Cross-Origin-Embedder-Policy': 'require-corp',
234 },
235 );
236 } else {
237 return response;
238 }
239 };
240}
241

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com