flutter_flutter/dev/devicelab/lib/tasks/web_benchmarks.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

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 (line.startsWith('This app is linked to the debug service')) {
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;
}
};
}