Skip to content

Commit 2a3e27f

Browse files
authored
Adding support for macrobenchmarking with "flutter run" (flutter#167692)
Notable changes: * Allows macrobenchmarks to run via `flutter run`. * Splits macrobenchmarking between "orchestration logic" and "app serving" (served on separate ports on the same machine to keep the scheme consistent with the `flutter build` path). * Adds an intercepted entrypoint for web benchmarks. We can't pass flags to the app since it's not supported, so I've hard-coded the orchestration server's port. * Adding logic to connect to an existing Chrome debugger instance (vs spawning one) for benchmarks.
1 parent b261c51 commit 2a3e27f

File tree

5 files changed

+308
-115
lines changed

5 files changed

+308
-115
lines changed

dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:convert' show json;
77
import 'dart:js_interop';
88
import 'dart:math' as math;
99

10+
import 'package:args/args.dart';
1011
import 'package:web/web.dart' as web;
1112

1213
import 'src/web/bench_build_image.dart';
@@ -81,9 +82,28 @@ final Map<String, RecorderFactory> benchmarks = <String, RecorderFactory>{
8182
BenchImageDecoding.benchmarkName: () => BenchImageDecoding(),
8283
};
8384

84-
final LocalBenchmarkServerClient _client = LocalBenchmarkServerClient();
85+
late final LocalBenchmarkServerClient _client;
86+
87+
Future<void> main(List<String> args) async {
88+
final ArgParser parser =
89+
ArgParser()..addOption(
90+
'port',
91+
abbr: 'p',
92+
help:
93+
'The port of the local benchmark server used that implements the '
94+
'API required for orchestrating macrobenchmarks.',
95+
);
96+
final ArgResults argResults = parser.parse(args);
97+
Uri serverOrigin;
98+
if (argResults.wasParsed('port')) {
99+
final int port = int.parse(argResults['port'] as String);
100+
serverOrigin = Uri.http('localhost:$port');
101+
} else {
102+
serverOrigin = Uri.base;
103+
}
104+
105+
_client = LocalBenchmarkServerClient(serverOrigin);
85106

86-
Future<void> main() async {
87107
// Check if the benchmark server wants us to run a specific benchmark.
88108
final String nextBenchmark = await _client.requestNextBenchmark();
89109

@@ -96,6 +116,14 @@ Future<void> main() async {
96116
web.window.location.reload();
97117
}
98118

119+
/// Shared entrypoint used for DDC, which runs the macrobenchmarks server on a
120+
/// separate port.
121+
// TODO(markzipan): Use `main` in `'web_benchmarks.dart` when Flutter Web supports the `--dart-entrypoint-args` flag.
122+
// ignore: unreachable_from_main
123+
Future<void> sharedMain(List<String> args) {
124+
return main(args);
125+
}
126+
99127
Future<void> _runBenchmark(String benchmarkName) async {
100128
final RecorderFactory? recorderFactory = benchmarks[benchmarkName];
101129

@@ -310,23 +338,36 @@ class TimeseriesVisualization {
310338
/// implement a manual fallback. This allows debugging benchmarks using plain
311339
/// `flutter run`.
312340
class LocalBenchmarkServerClient {
341+
LocalBenchmarkServerClient(this.serverOrigin);
342+
313343
/// This value is returned by [requestNextBenchmark].
314344
static const String kManualFallback = '__manual_fallback__';
315345

346+
/// The origin (e.g., http://localhost:1234) of the benchmark server that
347+
/// hosts the macrobenchmarking API.
348+
final Uri serverOrigin;
349+
316350
/// Whether we fell back to manual mode.
317351
///
318352
/// This happens when you run benchmarks using plain `flutter run` rather than
319353
/// devicelab test harness. The test harness spins up a special server that
320354
/// provides API for automatically picking the next benchmark to run.
321355
bool isInManualMode = false;
322356

357+
Map<String, String> get headers => <String, String>{
358+
'Access-Control-Allow-Headers': 'Origin, Content-Type, Accept',
359+
'Access-Control-Allow-Methods': 'Post',
360+
'Access-Control-Allow-Origin': serverOrigin.path,
361+
};
362+
323363
/// Asks the local server for the name of the next benchmark to run.
324364
///
325365
/// Returns [kManualFallback] if local server is not available (uses 404 as a
326366
/// signal).
327367
Future<String> requestNextBenchmark() async {
328368
final web.XMLHttpRequest request = await _requestXhr(
329-
'/next-benchmark',
369+
serverOrigin.resolve('next-benchmark'),
370+
requestHeaders: headers,
330371
method: 'POST',
331372
mimeType: 'application/json',
332373
sendData: json.encode(benchmarks.keys.toList()),
@@ -358,7 +399,8 @@ class LocalBenchmarkServerClient {
358399
Future<void> startPerformanceTracing(String benchmarkName) async {
359400
_checkNotManualMode();
360401
await _requestXhr(
361-
'/start-performance-tracing?label=$benchmarkName',
402+
serverOrigin.resolve('start-performance-tracing?label=$benchmarkName'),
403+
requestHeaders: headers,
362404
method: 'POST',
363405
mimeType: 'application/json',
364406
);
@@ -367,15 +409,21 @@ class LocalBenchmarkServerClient {
367409
/// Stops the performance tracing session started by [startPerformanceTracing].
368410
Future<void> stopPerformanceTracing() async {
369411
_checkNotManualMode();
370-
await _requestXhr('/stop-performance-tracing', method: 'POST', mimeType: 'application/json');
412+
await _requestXhr(
413+
serverOrigin.resolve('stop-performance-tracing'),
414+
requestHeaders: headers,
415+
method: 'POST',
416+
mimeType: 'application/json',
417+
);
371418
}
372419

373420
/// Sends the profile data collected by the benchmark to the local benchmark
374421
/// server.
375422
Future<void> sendProfileData(Profile profile) async {
376423
_checkNotManualMode();
377424
final web.XMLHttpRequest request = await _requestXhr(
378-
'/profile-data',
425+
serverOrigin.resolve('profile-data'),
426+
requestHeaders: headers,
379427
method: 'POST',
380428
mimeType: 'application/json',
381429
sendData: json.encode(profile.toJson()),
@@ -394,7 +442,8 @@ class LocalBenchmarkServerClient {
394442
Future<void> reportError(dynamic error, StackTrace stackTrace) async {
395443
_checkNotManualMode();
396444
await _requestXhr(
397-
'/on-error',
445+
serverOrigin.resolve('on-error'),
446+
requestHeaders: headers,
398447
method: 'POST',
399448
mimeType: 'application/json',
400449
sendData: json.encode(<String, dynamic>{'error': '$error', 'stackTrace': '$stackTrace'}),
@@ -405,7 +454,8 @@ class LocalBenchmarkServerClient {
405454
Future<void> printToConsole(String report) async {
406455
_checkNotManualMode();
407456
await _requestXhr(
408-
'/print-to-console',
457+
serverOrigin.resolve('print-to-console'),
458+
requestHeaders: headers,
409459
method: 'POST',
410460
mimeType: 'text/plain',
411461
sendData: report,
@@ -415,7 +465,7 @@ class LocalBenchmarkServerClient {
415465
/// This is the same as calling [html.HttpRequest.request] but it doesn't
416466
/// crash on 404, which we use to detect `flutter run`.
417467
Future<web.XMLHttpRequest> _requestXhr(
418-
String url, {
468+
Uri url, {
419469
String? method,
420470
bool? withCredentials,
421471
String? responseType,
@@ -427,7 +477,7 @@ class LocalBenchmarkServerClient {
427477
final web.XMLHttpRequest xhr = web.XMLHttpRequest();
428478

429479
method ??= 'GET';
430-
xhr.open(method, url, true);
480+
xhr.open(method, '$url', true);
431481

432482
if (withCredentials != null) {
433483
xhr.withCredentials = withCredentials;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
5+
import 'web_benchmarks.dart';
6+
7+
/// An entrypoint used by DDC for running macrobenchmarks.
8+
///
9+
/// DDC runs macrobenchmarks via 'flutter run', which hosts files from its own
10+
/// local server. As a result, the macrobenchmarking orchestration server needs
11+
/// to be hosted on a separate port. We split the entrypoint here because we
12+
/// can't pass command line args to Dart apps on Flutter Web.
13+
///
14+
// TODO(markzipan): Use `main` in `'web_benchmarks.dart` when Flutter Web supports the `--dart-entrypoint-args` flag.
15+
Future<void> main() async {
16+
// This is hard-coded and must be the same as `benchmarkServerPort` in `flutter/dev/devicelab/lib/tasks/web_benchmarks.dart`.
17+
await sharedMain(<String>['--port', '9999']);
18+
}

dev/benchmarks/macrobenchmarks/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies:
1818
# flutter update-packages --force-upgrade
1919
flutter_gallery_assets: 1.0.2
2020

21+
args: 2.7.0
2122
web: 1.1.1
2223

2324
async: 2.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
@@ -52,7 +53,6 @@ dev_dependencies:
5253

5354
_fe_analyzer_shared: 82.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
5455
analyzer: 7.4.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
55-
args: 2.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
5656
cli_config: 0.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
5757
convert: 3.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
5858
coverage: 1.12.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"

dev/devicelab/lib/framework/browser.dart

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class Chrome {
7474
});
7575
}
7676

77-
/// Launches Chrome with the give [options].
77+
/// Launches Chrome with the given [options].
7878
///
7979
/// The [onError] callback is called with an error message when the Chrome
8080
/// process encounters an error. In particular, [onError] is called when the
@@ -125,7 +125,28 @@ class Chrome {
125125

126126
WipConnection? debugConnection;
127127
if (withDebugging) {
128-
debugConnection = await _connectToChromeDebugPort(chromeProcess, options.debugPort!);
128+
debugConnection = await _connectToChromeDebugPort(options.debugPort!);
129+
}
130+
131+
return Chrome._(chromeProcess, onError, debugConnection);
132+
}
133+
134+
/// Connects to an existing Chrome process with the given [options].
135+
///
136+
/// The [onError] callback is called with an error message when the Chrome
137+
/// process encounters an error. In particular, [onError] is called when the
138+
/// Chrome process exits prematurely, i.e. before [stop] is called.
139+
static Future<Chrome> connect(
140+
io.Process chromeProcess,
141+
ChromeOptions options, {
142+
String? workingDirectory,
143+
required ChromeErrorCallback onError,
144+
}) async {
145+
final bool withDebugging = options.debugPort != null;
146+
147+
WipConnection? debugConnection;
148+
if (withDebugging) {
149+
debugConnection = await _connectToChromeDebugPort(options.debugPort!);
129150
}
130151

131152
return Chrome._(chromeProcess, onError, debugConnection);
@@ -260,7 +281,7 @@ String _findSystemChromeExecutable() {
260281
}
261282

262283
/// Waits for Chrome to print DevTools URI and connects to it.
263-
Future<WipConnection> _connectToChromeDebugPort(io.Process chromeProcess, int port) async {
284+
Future<WipConnection> _connectToChromeDebugPort(int port) async {
264285
final Uri devtoolsUri = await _getRemoteDebuggerUrl(Uri.parse('http://localhost:$port'));
265286
print('Connecting to DevTools: $devtoolsUri');
266287
final ChromeConnection chromeConnection = ChromeConnection('localhost', port);

0 commit comments

Comments
 (0)