mirror of
https://github.com/flutter/flutter.git
synced 2026-01-21 05:04:42 +08:00
Refactoring of code in `package:dwds` `26.2.1` changed execution ordering enough for a race condition in the web benchmark harness to be hit. This change updates the web benchmark harness to not rely on waiting for console output from the spawned browser which can be missed. Fixes https://github.com/flutter/flutter/issues/178326 --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
360 lines
15 KiB
Dart
360 lines
15 KiB
Dart
// Copyright 2014 The Flutter Authors. 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:convert' show LineSplitter, json, utf8;
|
|
import 'dart:io' as io;
|
|
|
|
import 'package:logging/logging.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:shelf/shelf.dart';
|
|
import 'package:shelf/shelf_io.dart' as shelf_io;
|
|
import 'package:shelf_static/shelf_static.dart';
|
|
|
|
import '../framework/browser.dart';
|
|
import '../framework/task_result.dart';
|
|
import '../framework/utils.dart';
|
|
|
|
/// The port at which the local benchmark server is served.
|
|
/// This is hard-coded and must be the same as the port used for DDC's benchmark at `flutter/dev/benchmarks/macrobenchmarks/lib/web_benchmarks_ddc.dart`.
|
|
const int benchmarkServerPort = 9999;
|
|
|
|
/// The port at which Chrome listens for a debug connection.
|
|
const int chromeDebugPort = 10000;
|
|
|
|
/// The port at which the benchmark's app is being served.
|
|
const int benchmarksAppPort = 10001;
|
|
|
|
typedef WebBenchmarkOptions = ({
|
|
bool useWasm,
|
|
bool forceSingleThreadedSkwasm,
|
|
bool useDdc,
|
|
bool withHotReload,
|
|
String buildMode,
|
|
});
|
|
|
|
Future<TaskResult> runWebBenchmark(WebBenchmarkOptions benchmarkOptions) async {
|
|
// Reduce logging level. Otherwise, package:webkit_inspection_protocol is way too spammy.
|
|
Logger.root.level = Level.INFO;
|
|
final String macrobenchmarksDirectory = path.join(
|
|
flutterDirectory.path,
|
|
'dev',
|
|
'benchmarks',
|
|
'macrobenchmarks',
|
|
);
|
|
return inDirectory(macrobenchmarksDirectory, () async {
|
|
await flutter('clean');
|
|
// DDC runs the benchmarks suite with 'flutter run', attaching to its
|
|
// Chrome instance instead of starting a new one.
|
|
io.Process? flutterRunProcess;
|
|
if (benchmarkOptions.useDdc) {
|
|
final ddcAppReady = Completer<void>();
|
|
flutterRunProcess = await startFlutter(
|
|
'run',
|
|
options: <String>[
|
|
'-d',
|
|
'chrome',
|
|
'--web-port',
|
|
'$benchmarksAppPort',
|
|
'--web-browser-debug-port',
|
|
'$chromeDebugPort',
|
|
'--web-launch-url',
|
|
'http://localhost:$benchmarksAppPort/index.html',
|
|
'--debug',
|
|
'--web-run-headless',
|
|
'--no-web-enable-expression-evaluation',
|
|
'--web-browser-flag=--disable-popup-blocking',
|
|
'--web-browser-flag=--bwsi',
|
|
'--web-browser-flag=--no-first-run',
|
|
'--web-browser-flag=--no-default-browser-check',
|
|
'--web-browser-flag=--disable-default-apps',
|
|
'--web-browser-flag=--disable-translate',
|
|
'--web-browser-flag=--disable-background-timer-throttling',
|
|
'--web-browser-flag=--disable-backgrounding-occluded-windows',
|
|
'--web-browser-flag=--disable-renderer-backgrounding',
|
|
'--web-browser-flag=--headless=new',
|
|
'--web-browser-flag=--no-sandbox',
|
|
'--dart-define=FLUTTER_WEB_ENABLE_PROFILING=true',
|
|
if (benchmarkOptions.withHotReload)
|
|
'--web-experimental-hot-reload'
|
|
else
|
|
'--no-web-experimental-hot-reload',
|
|
'--no-web-resources-cdn',
|
|
'lib/web_benchmarks_ddc.dart',
|
|
],
|
|
);
|
|
flutterRunProcess.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((
|
|
String line,
|
|
) {
|
|
if (!ddcAppReady.isCompleted && line.startsWith('Debug service listening on')) {
|
|
ddcAppReady.complete();
|
|
}
|
|
print('[CHROME STDOUT]: $line');
|
|
});
|
|
flutterRunProcess.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((
|
|
String line,
|
|
) {
|
|
print('[CHROME STDERR]: $line');
|
|
});
|
|
// Wait for the app to load in DDC's Chrome instance before trying to
|
|
// connect the debugger.
|
|
await ddcAppReady.future;
|
|
} else {
|
|
await evalFlutter(
|
|
'build',
|
|
options: <String>[
|
|
'web',
|
|
'--no-tree-shake-icons', // local engine builds are frequently out of sync with the Dart Kernel version
|
|
if (benchmarkOptions.useWasm) ...<String>['--wasm', '--no-strip-wasm'],
|
|
'--dart-define=FLUTTER_WEB_ENABLE_PROFILING=true',
|
|
'--${benchmarkOptions.buildMode}',
|
|
'--no-web-resources-cdn',
|
|
'-t',
|
|
'lib/web_benchmarks.dart',
|
|
],
|
|
);
|
|
}
|
|
final profileData = Completer<List<Map<String, dynamic>>>();
|
|
final collectedProfiles = <Map<String, dynamic>>[];
|
|
List<String>? benchmarks;
|
|
late Iterator<String> benchmarkIterator;
|
|
|
|
// This future fixes a race condition between the web-page loading and
|
|
// asking to run a benchmark, and us connecting to Chrome's DevTools port.
|
|
// Sometime one wins. Other times, the other wins.
|
|
Future<Chrome>? whenChromeIsReady;
|
|
Chrome? chrome;
|
|
late io.HttpServer server;
|
|
var cascade = Cascade();
|
|
List<Map<String, dynamic>>? latestPerformanceTrace;
|
|
final requestHeaders = <String, List<String>>{
|
|
'Access-Control-Allow-Headers': <String>[
|
|
'Accept',
|
|
'Access-Control-Allow-Headers',
|
|
'Access-Control-Allow-Methods',
|
|
'Access-Control-Allow-Origin',
|
|
'Content-Type',
|
|
'Origin',
|
|
],
|
|
'Access-Control-Allow-Methods': <String>['Post'],
|
|
'Access-Control-Allow-Origin': <String>['http://localhost:$benchmarksAppPort'],
|
|
};
|
|
|
|
cascade = cascade.add((Request request) async {
|
|
final String requestContents = await request.readAsString();
|
|
try {
|
|
chrome ??= await whenChromeIsReady;
|
|
if (request.method == 'OPTIONS') {
|
|
return Response.ok('', headers: requestHeaders);
|
|
}
|
|
if (request.requestedUri.path.endsWith('/profile-data')) {
|
|
final profile = json.decode(requestContents) as Map<String, dynamic>;
|
|
final benchmarkName = profile['name'] as String;
|
|
if (benchmarkName != benchmarkIterator.current) {
|
|
profileData.completeError(
|
|
Exception(
|
|
'Browser returned benchmark results from a wrong benchmark.\n'
|
|
'Requested to run benchmark ${benchmarkIterator.current}, but '
|
|
'got results for $benchmarkName.',
|
|
),
|
|
);
|
|
unawaited(server.close());
|
|
}
|
|
|
|
// Trace data is null when the benchmark is not frame-based, such as RawRecorder.
|
|
if (latestPerformanceTrace != null) {
|
|
final BlinkTraceSummary traceSummary = BlinkTraceSummary.fromJson(
|
|
latestPerformanceTrace!,
|
|
)!;
|
|
profile['totalUiFrame.average'] = traceSummary.averageTotalUIFrameTime.inMicroseconds;
|
|
profile['scoreKeys'] ??= <dynamic>[]; // using dynamic for consistency with JSON
|
|
(profile['scoreKeys'] as List<dynamic>).add('totalUiFrame.average');
|
|
latestPerformanceTrace = null;
|
|
}
|
|
collectedProfiles.add(profile);
|
|
return Response.ok('Profile received', headers: requestHeaders);
|
|
} else if (request.requestedUri.path.endsWith('/start-performance-tracing')) {
|
|
latestPerformanceTrace = null;
|
|
await chrome!.beginRecordingPerformance(request.requestedUri.queryParameters['label']!);
|
|
return Response.ok('Started performance tracing', headers: requestHeaders);
|
|
} else if (request.requestedUri.path.endsWith('/stop-performance-tracing')) {
|
|
latestPerformanceTrace = await chrome!.endRecordingPerformance();
|
|
return Response.ok('Stopped performance tracing', headers: requestHeaders);
|
|
} else if (request.requestedUri.path.endsWith('/on-error')) {
|
|
final errorDetails = json.decode(requestContents) as Map<String, dynamic>;
|
|
unawaited(server.close());
|
|
// Keep the stack trace as a string. It's thrown in the browser, not this Dart VM.
|
|
profileData.completeError('${errorDetails['error']}\n${errorDetails['stackTrace']}');
|
|
return Response.ok('', headers: requestHeaders);
|
|
} else if (request.requestedUri.path.endsWith('/next-benchmark')) {
|
|
if (benchmarks == null) {
|
|
benchmarks = (json.decode(requestContents) as List<dynamic>).cast<String>();
|
|
benchmarkIterator = benchmarks!.iterator;
|
|
}
|
|
if (benchmarkIterator.moveNext()) {
|
|
final String nextBenchmark = benchmarkIterator.current;
|
|
print('Launching benchmark "$nextBenchmark"');
|
|
return Response.ok(nextBenchmark, headers: requestHeaders);
|
|
} else {
|
|
profileData.complete(collectedProfiles);
|
|
return Response.notFound('Finished running benchmarks.', headers: requestHeaders);
|
|
}
|
|
} else if (request.requestedUri.path.endsWith('/print-to-console')) {
|
|
// A passthrough used by
|
|
// `dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart`
|
|
// to print information.
|
|
final message = requestContents;
|
|
print('[APP] $message');
|
|
return Response.ok('Reported.', headers: requestHeaders);
|
|
} else {
|
|
return Response.notFound(
|
|
'This request is not handled by the profile-data handler.',
|
|
headers: requestHeaders,
|
|
);
|
|
}
|
|
} catch (error, stackTrace) {
|
|
profileData.completeError(error, stackTrace);
|
|
return Response.internalServerError(body: '$error', headers: requestHeaders);
|
|
}
|
|
});
|
|
// Macrobenchmarks using 'flutter build' serve files from their local build directory alongside the orchestration logic.
|
|
if (!benchmarkOptions.useDdc) {
|
|
cascade = cascade.add(
|
|
createBuildDirectoryHandler(path.join(macrobenchmarksDirectory, 'build', 'web')),
|
|
);
|
|
}
|
|
|
|
server = await io.HttpServer.bind('localhost', benchmarkServerPort);
|
|
try {
|
|
shelf_io.serveRequests(server, cascade.handler);
|
|
|
|
final String dartToolDirectory = path.join('$macrobenchmarksDirectory/.dart_tool');
|
|
final String userDataDir = io.Directory(
|
|
dartToolDirectory,
|
|
).createTempSync('flutter_chrome_user_data.').path;
|
|
|
|
// TODO(yjbanov): temporarily disables headful Chrome until we get
|
|
// devicelab hardware that is able to run it. Our current
|
|
// GCE VMs can only run in headless mode.
|
|
// See: https://github.com/flutter/flutter/issues/50164
|
|
final isUncalibratedSmokeTest = io.Platform.environment['CALIBRATED'] != 'true';
|
|
// final bool isUncalibratedSmokeTest =
|
|
// io.Platform.environment['UNCALIBRATED_SMOKE_TEST'] == 'true';
|
|
final urlParams = benchmarkOptions.forceSingleThreadedSkwasm ? '?force_st=true' : '';
|
|
// DDC apps are served from a different port from the orchestration server.
|
|
final int appServingPort = benchmarkOptions.useDdc ? benchmarksAppPort : benchmarkServerPort;
|
|
final options = ChromeOptions(
|
|
url: 'http://localhost:$appServingPort/index.html$urlParams',
|
|
userDataDirectory: userDataDir,
|
|
headless: isUncalibratedSmokeTest,
|
|
debugPort: chromeDebugPort,
|
|
enableWasmGC: benchmarkOptions.useWasm,
|
|
);
|
|
|
|
print('Launching Chrome.');
|
|
|
|
if (benchmarkOptions.useDdc) {
|
|
// DDC reuses the existing Chrome connection spawned via 'flutter run'.
|
|
whenChromeIsReady = Chrome.connect(
|
|
flutterRunProcess!,
|
|
options,
|
|
onError: (String error) {
|
|
profileData.completeError(Exception(error));
|
|
},
|
|
workingDirectory: cwd,
|
|
);
|
|
} else {
|
|
whenChromeIsReady = Chrome.launch(
|
|
options,
|
|
onError: (String error) {
|
|
profileData.completeError(Exception(error));
|
|
},
|
|
workingDirectory: cwd,
|
|
);
|
|
}
|
|
|
|
print('Waiting for the benchmark to report benchmark profile.');
|
|
final taskResult = <String, dynamic>{};
|
|
final benchmarkScoreKeys = <String>[];
|
|
final List<Map<String, dynamic>> profiles = await profileData.future;
|
|
|
|
print('Received profile data');
|
|
for (final profile in profiles) {
|
|
final benchmarkName = profile['name'] as String;
|
|
if (benchmarkName.isEmpty) {
|
|
throw 'Benchmark name is empty';
|
|
}
|
|
|
|
final String webRendererName;
|
|
if (benchmarkOptions.useWasm) {
|
|
webRendererName = benchmarkOptions.forceSingleThreadedSkwasm ? 'skwasm_st' : 'skwasm';
|
|
} else {
|
|
webRendererName = 'canvaskit';
|
|
}
|
|
final namespace = '$benchmarkName.$webRendererName';
|
|
final scoreKeys = List<String>.from(profile['scoreKeys'] as List<dynamic>);
|
|
if (scoreKeys.isEmpty) {
|
|
throw 'No score keys in benchmark "$benchmarkName"';
|
|
}
|
|
for (final scoreKey in scoreKeys) {
|
|
if (scoreKey.isEmpty) {
|
|
throw 'Score key is empty in benchmark "$benchmarkName". '
|
|
'Received [${scoreKeys.join(', ')}]';
|
|
}
|
|
benchmarkScoreKeys.add('$namespace.$scoreKey');
|
|
}
|
|
|
|
for (final String key in profile.keys) {
|
|
if (key == 'name' || key == 'scoreKeys') {
|
|
continue;
|
|
}
|
|
taskResult['$namespace.$key'] = profile[key];
|
|
}
|
|
}
|
|
return TaskResult.success(taskResult, benchmarkScoreKeys: benchmarkScoreKeys);
|
|
} finally {
|
|
unawaited(server.close());
|
|
chrome?.stop();
|
|
if (flutterRunProcess != null) {
|
|
// Sending a SIGINT/SIGTERM to the process here isn't reliable because [process] is
|
|
// the shell (flutter is a shell script) and doesn't pass the signal on.
|
|
// Sending a `q` is an instruction to quit using the console runner.
|
|
flutterRunProcess.stdin.write('q');
|
|
await flutterRunProcess.stdin.flush();
|
|
// Give the process a couple of seconds to exit and run shutdown hooks
|
|
// before sending kill signal.
|
|
await flutterRunProcess.exitCode.timeout(
|
|
const Duration(seconds: 2),
|
|
onTimeout: () {
|
|
flutterRunProcess!.kill(io.ProcessSignal.sigint);
|
|
return 0;
|
|
},
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
Handler createBuildDirectoryHandler(String buildDirectoryPath) {
|
|
final Handler childHandler = createStaticHandler(buildDirectoryPath);
|
|
return (Request request) async {
|
|
final Response response = await childHandler(request);
|
|
final String? mimeType = response.mimeType;
|
|
|
|
// Provide COOP/COEP headers so that the browser loads the page as
|
|
// crossOriginIsolated. This will make sure that we get high-resolution
|
|
// timers for our benchmark measurements.
|
|
if (mimeType == 'text/html' || mimeType == 'text/javascript') {
|
|
return response.change(
|
|
headers: <String, String>{
|
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
'Cross-Origin-Embedder-Policy': 'require-corp',
|
|
},
|
|
);
|
|
} else {
|
|
return response;
|
|
}
|
|
};
|
|
}
|