| // 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:convert'; |
| import 'dart:io'; |
| |
| import 'package:collection/collection.dart'; |
| import 'package:metrics_center/metrics_center.dart'; |
| |
| /// Authenticate and connect to gcloud storage. |
| /// |
| /// It supports both token and credential authentications. |
| Future<FlutterDestination> connectFlutterDestination() async { |
| const String kTokenPath = 'TOKEN_PATH'; |
| const String kGcpProject = 'GCP_PROJECT'; |
| final Map<String, String> env = Platform.environment; |
| final bool isTesting = env['IS_TESTING'] == 'true'; |
| if (env.containsKey(kTokenPath) && env.containsKey(kGcpProject)) { |
| return FlutterDestination.makeFromAccessToken( |
| File(env[kTokenPath]!).readAsStringSync(), |
| env[kGcpProject]!, |
| isTesting: isTesting, |
| ); |
| } |
| return FlutterDestination.makeFromCredentialsJson( |
| jsonDecode(env['BENCHMARK_GCP_CREDENTIALS']!) as Map<String, dynamic>, |
| isTesting: isTesting, |
| ); |
| } |
| |
| /// Parse results and append additional benchmark tags into Metric Points. |
| /// |
| /// An example of `resultsJson`: |
| /// { |
| /// "CommitBranch": "master", |
| /// "CommitSha": "abc", |
| /// "BuilderName": "test", |
| /// "ResultData": { |
| /// "average_frame_build_time_millis": 0.4550425531914895, |
| /// "90th_percentile_frame_build_time_millis": 0.473 |
| /// }, |
| /// "BenchmarkScoreKeys": [ |
| /// "average_frame_build_time_millis", |
| /// "90th_percentile_frame_build_time_millis" |
| /// ] |
| /// } |
| /// |
| /// An example of `benchmarkTags`: |
| /// { |
| /// "arch": "intel", |
| /// "device_type": "Moto G Play", |
| /// "device_version": "android-25", |
| /// "host_type": "linux", |
| /// "host_version": "debian-10.11" |
| /// } |
| List<MetricPoint> parse(Map<String, dynamic> resultsJson, Map<String, dynamic> benchmarkTags, String taskName) { |
| print('Results to upload to skia perf: $resultsJson'); |
| print('Benchmark tags to upload to skia perf: $benchmarkTags'); |
| final List<String> scoreKeys = |
| (resultsJson['BenchmarkScoreKeys'] as List<dynamic>?)?.cast<String>() ?? const <String>[]; |
| final Map<String, dynamic> resultData = |
| resultsJson['ResultData'] as Map<String, dynamic>? ?? const <String, dynamic>{}; |
| final String gitBranch = (resultsJson['CommitBranch'] as String).trim(); |
| final String gitSha = (resultsJson['CommitSha'] as String).trim(); |
| final List<MetricPoint> metricPoints = <MetricPoint>[]; |
| for (final String scoreKey in scoreKeys) { |
| Map<String, String> tags = <String, String>{ |
| kGithubRepoKey: kFlutterFrameworkRepo, |
| kGitRevisionKey: gitSha, |
| 'branch': gitBranch, |
| kNameKey: taskName, |
| kSubResultKey: scoreKey, |
| }; |
| // Append additional benchmark tags, which will surface in Skia Perf dashboards. |
| tags = mergeMaps<String, String>( |
| tags, benchmarkTags.map((String key, dynamic value) => MapEntry<String, String>(key, value.toString()))); |
| metricPoints.add( |
| MetricPoint( |
| (resultData[scoreKey] as num).toDouble(), |
| tags, |
| ), |
| ); |
| } |
| return metricPoints; |
| } |
| |
| /// Upload metrics to GCS bucket used by Skia Perf. |
| /// |
| /// Skia Perf picks up all available files under the folder, and |
| /// is robust to duplicate entries. |
| /// |
| /// Files will be named based on `taskName`, such as |
| /// `complex_layout_scroll_perf__timeline_summary_values.json`. |
| /// If no `taskName` is specified, data will be saved to |
| /// `default_values.json`. |
| Future<void> upload( |
| FlutterDestination metricsDestination, |
| List<MetricPoint> metricPoints, |
| int commitTimeSinceEpoch, |
| String taskName, |
| ) async { |
| await metricsDestination.update( |
| metricPoints, |
| DateTime.fromMillisecondsSinceEpoch( |
| commitTimeSinceEpoch, |
| isUtc: true, |
| ), |
| taskName, |
| ); |
| } |
| |
| /// Upload JSON results to skia perf. |
| /// |
| /// Flutter infrastructure's workflow is: |
| /// 1. Run DeviceLab test, writing results to a known path |
| /// 2. Request service account token from luci auth (valid for at least 3 minutes) |
| /// 3. Upload results from (1) to skia perf. |
| Future<void> uploadToSkiaPerf(String? resultsPath, String? commitTime, String? taskName, String? benchmarkTags) async { |
| int commitTimeSinceEpoch; |
| if (resultsPath == null) { |
| return; |
| } |
| if (commitTime != null) { |
| commitTimeSinceEpoch = 1000 * int.parse(commitTime); |
| } else { |
| commitTimeSinceEpoch = DateTime.now().millisecondsSinceEpoch; |
| } |
| taskName = taskName ?? 'default'; |
| final Map<String, dynamic> benchmarkTagsMap = jsonDecode(benchmarkTags ?? '{}') as Map<String, dynamic>; |
| final File resultFile = File(resultsPath); |
| Map<String, dynamic> resultsJson = <String, dynamic>{}; |
| resultsJson = json.decode(await resultFile.readAsString()) as Map<String, dynamic>; |
| final List<MetricPoint> metricPoints = parse(resultsJson, benchmarkTagsMap, taskName); |
| final FlutterDestination metricsDestination = await connectFlutterDestination(); |
| await upload( |
| metricsDestination, |
| metricPoints, |
| commitTimeSinceEpoch, |
| metricFileName(taskName, benchmarkTagsMap), |
| ); |
| } |
| |
| /// Create metric file name based on `taskName`, `arch`, `host type`, and `device type`. |
| /// |
| /// Same `taskName` may run on different platforms. Considering host/device tags to |
| /// use different metric file names. |
| /// |
| /// This affects only the metric file name which contains metric data, and does not affect |
| /// real host/device tags. |
| /// |
| /// For example: |
| /// Old file name: `backdrop_filter_perf__timeline_summary` |
| /// New file name: `backdrop_filter_perf__timeline_summary_intel_linux_motoG4` |
| String metricFileName( |
| String taskName, |
| Map<String, dynamic> benchmarkTagsMap, |
| ) { |
| final StringBuffer fileName = StringBuffer(taskName); |
| if (benchmarkTagsMap.containsKey('arch')) { |
| fileName |
| ..write('_') |
| ..write(_fileNameFormat(benchmarkTagsMap['arch'] as String)); |
| } |
| if (benchmarkTagsMap.containsKey('host_type')) { |
| fileName |
| ..write('_') |
| ..write(_fileNameFormat(benchmarkTagsMap['host_type'] as String)); |
| } |
| if (benchmarkTagsMap.containsKey('device_type')) { |
| fileName |
| ..write('_') |
| ..write(_fileNameFormat(benchmarkTagsMap['device_type'] as String)); |
| } |
| return fileName.toString(); |
| } |
| |
| /// Format `fileName` removing non letter and number characters. |
| String _fileNameFormat(String fileName) { |
| return fileName.replaceAll(RegExp('[^a-zA-Z0-9]'), ''); |
| } |