| // 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'; |
| import 'dart:io'; |
| |
| import 'package:path/path.dart' as path; |
| |
| import 'package:flutter_devicelab/framework/adb.dart'; |
| import 'package:flutter_devicelab/framework/framework.dart'; |
| import 'package:flutter_devicelab/framework/task_result.dart'; |
| import 'package:flutter_devicelab/framework/utils.dart'; |
| |
| /// Creates a device lab task that runs benchmarks in |
| /// `dev/benchmarks/microbenchmarks` reports results to the dashboard. |
| TaskFunction createMicrobenchmarkTask() { |
| return () async { |
| final Device device = await devices.workingDevice; |
| await device.unlock(); |
| |
| Future<Map<String, double>> _runMicrobench(String benchmarkPath) async { |
| Future<Map<String, double>> _run() async { |
| print('Running $benchmarkPath'); |
| final Directory appDir = dir( |
| path.join(flutterDirectory.path, 'dev/benchmarks/microbenchmarks')); |
| final Process flutterProcess = await inDirectory(appDir, () async { |
| final List<String> options = <String>[ |
| '-v', |
| // --release doesn't work on iOS due to code signing issues |
| '--profile', |
| '--no-publish-port', |
| '-d', |
| device.deviceId, |
| ]; |
| options.add(benchmarkPath); |
| return await _startFlutter( |
| options: options, |
| canFail: false, |
| ); |
| }); |
| |
| return await _readJsonResults(flutterProcess); |
| } |
| return _run(); |
| } |
| |
| final Map<String, double> allResults = <String, double>{ |
| ...await _runMicrobench('lib/stocks/layout_bench.dart'), |
| ...await _runMicrobench('lib/stocks/build_bench.dart'), |
| ...await _runMicrobench('lib/geometry/matrix_utils_transform_bench.dart'), |
| ...await _runMicrobench('lib/geometry/rrect_contains_bench.dart'), |
| ...await _runMicrobench('lib/gestures/velocity_tracker_bench.dart'), |
| ...await _runMicrobench('lib/gestures/gesture_detector_bench.dart'), |
| ...await _runMicrobench('lib/stocks/animation_bench.dart'), |
| ...await _runMicrobench('lib/language/sync_star_bench.dart'), |
| ...await _runMicrobench('lib/language/sync_star_semantics_bench.dart'), |
| ...await _runMicrobench('lib/foundation/all_elements_bench.dart'), |
| ...await _runMicrobench('lib/foundation/change_notifier_bench.dart'), |
| }; |
| |
| return TaskResult.success(allResults, benchmarkScoreKeys: allResults.keys.toList()); |
| }; |
| } |
| |
| Future<Process> _startFlutter({ |
| List<String> options = const <String>[], |
| bool canFail = false, |
| Map<String, String> environment, |
| }) { |
| final List<String> args = flutterCommandArgs('run', options); |
| return startProcess(path.join(flutterDirectory.path, 'bin', 'flutter'), args, environment: environment); |
| } |
| |
| Future<Map<String, double>> _readJsonResults(Process process) { |
| // IMPORTANT: keep these values in sync with dev/benchmarks/microbenchmarks/lib/common.dart |
| const String jsonStart = '================ RESULTS ================'; |
| const String jsonEnd = '================ FORMATTED =============='; |
| const String jsonPrefix = ':::JSON:::'; |
| bool jsonStarted = false; |
| final StringBuffer jsonBuf = StringBuffer(); |
| final Completer<Map<String, double>> completer = Completer<Map<String, double>>(); |
| |
| final StreamSubscription<String> stderrSub = process.stderr |
| .transform<String>(const Utf8Decoder()) |
| .transform<String>(const LineSplitter()) |
| .listen((String line) { |
| stderr.writeln('[STDERR] $line'); |
| }); |
| |
| bool processWasKilledIntentionally = false; |
| bool resultsHaveBeenParsed = false; |
| final StreamSubscription<String> stdoutSub = process.stdout |
| .transform<String>(const Utf8Decoder()) |
| .transform<String>(const LineSplitter()) |
| .listen((String line) async { |
| print(line); |
| |
| if (line.contains(jsonStart)) { |
| jsonStarted = true; |
| return; |
| } |
| |
| if (line.contains(jsonEnd)) { |
| final String jsonOutput = jsonBuf.toString(); |
| |
| // If we end up here and have already parsed the results, it suggests that |
| // we have received output from another test because our `flutter run` |
| // process did not terminate correctly. |
| // https://github.com/flutter/flutter/issues/19096#issuecomment-402756549 |
| if (resultsHaveBeenParsed) { |
| throw 'Additional JSON was received after results has already been ' |
| 'processed. This suggests the `flutter run` process may have lived ' |
| 'past the end of our test and collected additional output from the ' |
| 'next test.\n\n' |
| 'The JSON below contains all collected output, including both from ' |
| 'the original test and what followed.\n\n' |
| '$jsonOutput'; |
| } |
| |
| jsonStarted = false; |
| processWasKilledIntentionally = true; |
| resultsHaveBeenParsed = true; |
| // 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. |
| // See https://github.com/flutter/flutter/issues/19208 |
| process.stdin.write('q'); |
| await process.stdin.flush(); |
| // Also send a kill signal in case the `q` above didn't work. |
| process.kill(ProcessSignal.sigint); |
| try { |
| completer.complete(Map<String, double>.from(json.decode(jsonOutput) as Map<String, dynamic>)); |
| } catch (ex) { |
| completer.completeError('Decoding JSON failed ($ex). JSON string was: $jsonOutput'); |
| } |
| return; |
| } |
| |
| if (jsonStarted && line.contains(jsonPrefix)) |
| jsonBuf.writeln(line.substring(line.indexOf(jsonPrefix) + jsonPrefix.length)); |
| }); |
| |
| process.exitCode.then<void>((int code) async { |
| await Future.wait<void>(<Future<void>>[ |
| stdoutSub.cancel(), |
| stderrSub.cancel(), |
| ]); |
| if (!processWasKilledIntentionally && code != 0) { |
| completer.completeError('flutter run failed: exit code=$code'); |
| } |
| }); |
| |
| return completer.future; |
| } |