mirror of
https://github.com/flutter/packages.git
synced 2025-06-29 22:33:11 +08:00
Import metrics_center (#268)
This commit is contained in:
@ -38,7 +38,7 @@ linter:
|
|||||||
- always_specify_types
|
- always_specify_types
|
||||||
- annotate_overrides
|
- annotate_overrides
|
||||||
# - avoid_annotating_with_dynamic # conflicts with always_specify_types
|
# - avoid_annotating_with_dynamic # conflicts with always_specify_types
|
||||||
- avoid_as
|
# - avoid_as # conflicts with NNBD
|
||||||
- avoid_bool_literals_in_conditional_expressions
|
- avoid_bool_literals_in_conditional_expressions
|
||||||
# - avoid_catches_without_on_clauses # we do this commonly
|
# - avoid_catches_without_on_clauses # we do this commonly
|
||||||
# - avoid_catching_errors # we do this commonly
|
# - avoid_catching_errors # we do this commonly
|
||||||
|
1
packages/metrics_center/.gitignore
vendored
Normal file
1
packages/metrics_center/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
secret
|
3
packages/metrics_center/CHANGELOG.md
Normal file
3
packages/metrics_center/CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# 0.0.4+1
|
||||||
|
|
||||||
|
- Moved to the `flutter/packages` repository
|
25
packages/metrics_center/LICENSE
Normal file
25
packages/metrics_center/LICENSE
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following
|
||||||
|
disclaimer in the documentation and/or other materials provided
|
||||||
|
with the distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived
|
||||||
|
from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
9
packages/metrics_center/README.md
Normal file
9
packages/metrics_center/README.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Metrics Center
|
||||||
|
|
||||||
|
Metrics center is a minimal set of code and services to support multiple perf
|
||||||
|
metrics generators (e.g., Cocoon device lab, Cirrus bots, LUCI bots, Firebase
|
||||||
|
Test Lab) and destinations (e.g., old Cocoon perf dashboard, Skia perf
|
||||||
|
dashboard). The work and maintenance it requires is very close to that of just
|
||||||
|
supporting a single generator and destination (e.g., engine bots to Skia perf),
|
||||||
|
and the small amount of extra work is designed to make it easy to support more
|
||||||
|
generators and destinations in the future.
|
8
packages/metrics_center/lib/metrics_center.dart
Normal file
8
packages/metrics_center/lib/metrics_center.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
export 'src/common.dart';
|
||||||
|
export 'src/flutter.dart';
|
||||||
|
export 'src/google_benchmark.dart';
|
||||||
|
export 'src/skiaperf.dart';
|
65
packages/metrics_center/lib/src/common.dart
Normal file
65
packages/metrics_center/lib/src/common.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// 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:collection';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
import 'package:googleapis_auth/auth.dart';
|
||||||
|
import 'package:googleapis_auth/auth_io.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
|
||||||
|
/// Common format of a metric data point.
|
||||||
|
class MetricPoint extends Equatable {
|
||||||
|
/// Creates a new data point.
|
||||||
|
MetricPoint(
|
||||||
|
this.value,
|
||||||
|
Map<String, String> tags,
|
||||||
|
) : _tags = SplayTreeMap<String, String>.from(tags);
|
||||||
|
|
||||||
|
/// Can store integer values.
|
||||||
|
final double value;
|
||||||
|
|
||||||
|
/// Test name, unit, timestamp, configs, git revision, ..., in sorted order.
|
||||||
|
UnmodifiableMapView<String, String> get tags =>
|
||||||
|
UnmodifiableMapView<String, String>(_tags);
|
||||||
|
|
||||||
|
/// Unique identifier for updating existing data point.
|
||||||
|
///
|
||||||
|
/// We shouldn't have to worry about hash collisions until we have about
|
||||||
|
/// 2^128 points.
|
||||||
|
///
|
||||||
|
/// This id should stay constant even if the [tags.keys] are reordered.
|
||||||
|
/// (Because we are using an ordered SplayTreeMap to generate the id.)
|
||||||
|
String get id => sha256.convert(utf8.encode('$_tags')).toString();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'MetricPoint(value=$value, tags=$_tags)';
|
||||||
|
}
|
||||||
|
|
||||||
|
final SplayTreeMap<String, String> _tags;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => <Object>[value, tags];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interface to write [MetricPoint].
|
||||||
|
abstract class MetricDestination {
|
||||||
|
/// Insert new data points or modify old ones with matching id.
|
||||||
|
Future<void> update(List<MetricPoint> points);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create `AuthClient` in case we only have an access token without the full
|
||||||
|
/// credentials json. It's currently the case for Chrmoium LUCI bots.
|
||||||
|
AuthClient authClientFromAccessToken(String token, List<String> scopes) {
|
||||||
|
final DateTime anHourLater = DateTime.now().add(const Duration(hours: 1));
|
||||||
|
final AccessToken accessToken =
|
||||||
|
AccessToken('Bearer', token, anHourLater.toUtc());
|
||||||
|
final AccessCredentials accessCredentials =
|
||||||
|
AccessCredentials(accessToken, null, scopes);
|
||||||
|
return authenticatedClient(Client(), accessCredentials);
|
||||||
|
}
|
55
packages/metrics_center/lib/src/constants.dart
Normal file
55
packages/metrics_center/lib/src/constants.dart
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
/// Strings used for MetricPoint tag keys
|
||||||
|
const String kGithubRepoKey = 'gitRepo';
|
||||||
|
|
||||||
|
/// Strings used for MetricPoint tag keys
|
||||||
|
const String kGitRevisionKey = 'gitRevision';
|
||||||
|
|
||||||
|
/// Strings used for MetricPoint tag keys
|
||||||
|
const String kUnitKey = 'unit';
|
||||||
|
|
||||||
|
/// Strings used for MetricPoint tag keys
|
||||||
|
const String kNameKey = 'name';
|
||||||
|
|
||||||
|
/// Strings used for MetricPoint tag keys
|
||||||
|
const String kSubResultKey = 'subResult';
|
||||||
|
|
||||||
|
/// Flutter repo name.
|
||||||
|
const String kFlutterFrameworkRepo = 'flutter/flutter';
|
||||||
|
|
||||||
|
/// Engine repo name.
|
||||||
|
const String kFlutterEngineRepo = 'flutter/engine';
|
||||||
|
|
||||||
|
/// The key for the GCP project id in the credentials json.
|
||||||
|
const String kProjectId = 'project_id';
|
||||||
|
|
||||||
|
/// Timeline key in JSON.
|
||||||
|
const String kSourceTimeMicrosName = 'sourceTimeMicros';
|
||||||
|
|
||||||
|
/// The size of 500 is currently limited by Google datastore. It cannot write
|
||||||
|
/// more than 500 entities in a single call.
|
||||||
|
const int kMaxBatchSize = 500;
|
||||||
|
|
||||||
|
/// The prod bucket name for Flutter's instance of Skia Perf.
|
||||||
|
const String kBucketName = 'flutter-skia-perf-prod';
|
||||||
|
|
||||||
|
/// The test bucket name for Flutter's instance of Skia Perf.
|
||||||
|
const String kTestBucketName = 'flutter-skia-perf-test';
|
||||||
|
|
||||||
|
/// JSON key for Skia Perf's git hash entry.
|
||||||
|
const String kSkiaPerfGitHashKey = 'gitHash';
|
||||||
|
|
||||||
|
/// JSON key for Skia Perf's results entry.
|
||||||
|
const String kSkiaPerfResultsKey = 'results';
|
||||||
|
|
||||||
|
/// JSON key for Skia Perf's value entry.
|
||||||
|
const String kSkiaPerfValueKey = 'value';
|
||||||
|
|
||||||
|
/// JSON key for Skia Perf's options entry.
|
||||||
|
const String kSkiaPerfOptionsKey = 'options';
|
||||||
|
|
||||||
|
/// JSON key for Skia Perf's default config entry.
|
||||||
|
const String kSkiaPerfDefaultConfig = 'default';
|
60
packages/metrics_center/lib/src/flutter.dart
Normal file
60
packages/metrics_center/lib/src/flutter.dart
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// 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 'common.dart';
|
||||||
|
import 'constants.dart';
|
||||||
|
import 'legacy_datastore.dart';
|
||||||
|
import 'legacy_flutter.dart';
|
||||||
|
|
||||||
|
/// Convenient class to capture the benchmarks in the Flutter engine repo.
|
||||||
|
class FlutterEngineMetricPoint extends MetricPoint {
|
||||||
|
/// Creates a metric point for the Flutter engine repository.
|
||||||
|
///
|
||||||
|
/// The `name`, `value`, and `gitRevision` parameters must not be null.
|
||||||
|
FlutterEngineMetricPoint(
|
||||||
|
String name,
|
||||||
|
double value,
|
||||||
|
String gitRevision, {
|
||||||
|
Map<String, String> moreTags = const <String, String>{},
|
||||||
|
}) : super(
|
||||||
|
value,
|
||||||
|
<String, String>{
|
||||||
|
kNameKey: name,
|
||||||
|
kGithubRepoKey: kFlutterEngineRepo,
|
||||||
|
kGitRevisionKey: gitRevision,
|
||||||
|
}..addAll(moreTags),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All Flutter performance metrics (framework, engine, ...) should be written
|
||||||
|
/// to this destination.
|
||||||
|
class FlutterDestination extends MetricDestination {
|
||||||
|
// TODO(liyuqian): change the implementation of this class (without changing
|
||||||
|
// its public APIs) to remove `LegacyFlutterDestination` and directly use
|
||||||
|
// `SkiaPerfDestination` once the migration is fully done.
|
||||||
|
FlutterDestination._(this._legacyDestination);
|
||||||
|
|
||||||
|
/// Creates a [FlutterDestination] from service account JSON.
|
||||||
|
static Future<FlutterDestination> makeFromCredentialsJson(
|
||||||
|
Map<String, dynamic> json) async {
|
||||||
|
final LegacyFlutterDestination legacyDestination =
|
||||||
|
LegacyFlutterDestination(await datastoreFromCredentialsJson(json));
|
||||||
|
return FlutterDestination._(legacyDestination);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [FlutterDestination] from an OAuth access token.
|
||||||
|
static FlutterDestination makeFromAccessToken(
|
||||||
|
String accessToken, String projectId) {
|
||||||
|
final LegacyFlutterDestination legacyDestination = LegacyFlutterDestination(
|
||||||
|
datastoreFromAccessToken(accessToken, projectId));
|
||||||
|
return FlutterDestination._(legacyDestination);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> update(List<MetricPoint> points) async {
|
||||||
|
await _legacyDestination.update(points);
|
||||||
|
}
|
||||||
|
|
||||||
|
final LegacyFlutterDestination _legacyDestination;
|
||||||
|
}
|
90
packages/metrics_center/lib/src/gcs_lock.dart
Normal file
90
packages/metrics_center/lib/src/gcs_lock.dart
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// 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 'package:googleapis/storage/v1.dart';
|
||||||
|
import 'package:googleapis_auth/auth.dart';
|
||||||
|
|
||||||
|
/// Global (in terms of earth) mutex using Google Cloud Storage.
|
||||||
|
class GcsLock {
|
||||||
|
/// Create a lock with an authenticated client and a GCS bucket name.
|
||||||
|
///
|
||||||
|
/// The client is used to communicate with Google Cloud Storage APIs.
|
||||||
|
GcsLock(this._client, this._bucketName)
|
||||||
|
: assert(_client != null),
|
||||||
|
assert(_bucketName != null) {
|
||||||
|
_api = StorageApi(_client);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a temporary lock file in GCS, and use it as a mutex mechanism to
|
||||||
|
/// run a piece of code exclusively.
|
||||||
|
///
|
||||||
|
/// There must be no existing lock file with the same name in order to
|
||||||
|
/// proceed. If multiple [GcsLock]s with the same `bucketName` and
|
||||||
|
/// `lockFileName` try [protectedRun] simultaneously, only one will proceed
|
||||||
|
/// and create the lock file. All others will be blocked.
|
||||||
|
///
|
||||||
|
/// When [protectedRun] finishes, the lock file is deleted, and other blocked
|
||||||
|
/// [protectedRun] may proceed.
|
||||||
|
///
|
||||||
|
/// If the lock file is stuck (e.g., `_unlock` is interrupted unexpectedly),
|
||||||
|
/// one may need to manually delete the lock file from GCS to unblock any
|
||||||
|
/// [protectedRun] that may depend on it.
|
||||||
|
Future<void> protectedRun(String lockFileName, Future<void> f()) async {
|
||||||
|
await _lock(lockFileName);
|
||||||
|
try {
|
||||||
|
await f();
|
||||||
|
} catch (e, stacktrace) {
|
||||||
|
print(stacktrace);
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
await _unlock(lockFileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _lock(String lockFileName) async {
|
||||||
|
final Object object = Object();
|
||||||
|
object.bucket = _bucketName;
|
||||||
|
object.name = lockFileName;
|
||||||
|
final Media content = Media(const Stream<List<int>>.empty(), 0);
|
||||||
|
|
||||||
|
Duration waitPeriod = const Duration(milliseconds: 10);
|
||||||
|
bool locked = false;
|
||||||
|
while (!locked) {
|
||||||
|
try {
|
||||||
|
await _api.objects.insert(object, _bucketName,
|
||||||
|
ifGenerationMatch: '0', uploadMedia: content);
|
||||||
|
locked = true;
|
||||||
|
} on DetailedApiRequestError catch (e) {
|
||||||
|
if (e.status == 412) {
|
||||||
|
// Status 412 means that the lock file already exists. Wait until
|
||||||
|
// that lock file is deleted.
|
||||||
|
await Future<void>.delayed(waitPeriod);
|
||||||
|
waitPeriod *= 2;
|
||||||
|
if (waitPeriod >= _kWarningThreshold) {
|
||||||
|
print(
|
||||||
|
'The lock is waiting for a long time: $waitPeriod. '
|
||||||
|
'If the lock file $lockFileName in bucket $_bucketName '
|
||||||
|
'seems to be stuck (i.e., it was created a long time ago and '
|
||||||
|
'no one seems to be owning it currently), delete it manually '
|
||||||
|
'to unblock this.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _unlock(String lockFileName) async {
|
||||||
|
await _api.objects.delete(_bucketName, lockFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
StorageApi _api;
|
||||||
|
|
||||||
|
final String _bucketName;
|
||||||
|
final AuthClient _client;
|
||||||
|
|
||||||
|
static const Duration _kWarningThreshold = Duration(seconds: 10);
|
||||||
|
}
|
35
packages/metrics_center/lib/src/github_helper.dart
Normal file
35
packages/metrics_center/lib/src/github_helper.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// 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 'package:github/github.dart';
|
||||||
|
|
||||||
|
/// Singleton class to query some Github info with an in-memory cache.
|
||||||
|
class GithubHelper {
|
||||||
|
/// Return the singleton helper.
|
||||||
|
factory GithubHelper() {
|
||||||
|
return _singleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
GithubHelper._internal();
|
||||||
|
|
||||||
|
/// The result is cached in memory so querying the same thing again in the
|
||||||
|
/// same process is fast.
|
||||||
|
///
|
||||||
|
/// Our unit test requires that calling this method 1000 times for the same
|
||||||
|
/// `githubRepo` and `sha` should be done in 1 second.
|
||||||
|
Future<DateTime> getCommitDateTime(String githubRepo, String sha) async {
|
||||||
|
final String key = '$githubRepo/commit/$sha';
|
||||||
|
if (_commitDateTimeCache[key] == null) {
|
||||||
|
final RepositoryCommit commit = await _github.repositories
|
||||||
|
.getCommit(RepositorySlug.full(githubRepo), sha);
|
||||||
|
_commitDateTimeCache[key] = commit.commit.committer.date;
|
||||||
|
}
|
||||||
|
return _commitDateTimeCache[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
static final GithubHelper _singleton = GithubHelper._internal();
|
||||||
|
|
||||||
|
final GitHub _github = GitHub(auth: findAuthenticationFromEnvironment());
|
||||||
|
final Map<String, DateTime> _commitDateTimeCache = <String, DateTime>{};
|
||||||
|
}
|
74
packages/metrics_center/lib/src/google_benchmark.dart
Normal file
74
packages/metrics_center/lib/src/google_benchmark.dart
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// 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 'common.dart';
|
||||||
|
import 'constants.dart';
|
||||||
|
|
||||||
|
const String _kTimeUnitKey = 'time_unit';
|
||||||
|
|
||||||
|
const List<String> _kNonNumericalValueSubResults = <String>[
|
||||||
|
kNameKey,
|
||||||
|
_kTimeUnitKey,
|
||||||
|
'iterations',
|
||||||
|
'big_o',
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Parse the json result of https://github.com/google/benchmark.
|
||||||
|
class GoogleBenchmarkParser {
|
||||||
|
/// Given a Google benchmark json output, parse its content into a list of [MetricPoint].
|
||||||
|
static Future<List<MetricPoint>> parse(String jsonFileName) async {
|
||||||
|
final Map<String, dynamic> jsonResult =
|
||||||
|
jsonDecode(File(jsonFileName).readAsStringSync())
|
||||||
|
as Map<String, dynamic>;
|
||||||
|
|
||||||
|
final Map<String, dynamic> rawContext =
|
||||||
|
jsonResult['context'] as Map<String, dynamic>;
|
||||||
|
final Map<String, String> context = rawContext.map<String, String>(
|
||||||
|
(String k, dynamic v) => MapEntry<String, String>(k, v.toString()),
|
||||||
|
);
|
||||||
|
final List<MetricPoint> points = <MetricPoint>[];
|
||||||
|
for (final dynamic item in jsonResult['benchmarks']) {
|
||||||
|
_parseAnItem(item as Map<String, dynamic>, points, context);
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseAnItem(
|
||||||
|
Map<String, dynamic> item,
|
||||||
|
List<MetricPoint> points,
|
||||||
|
Map<String, String> context,
|
||||||
|
) {
|
||||||
|
final String name = item[kNameKey] as String;
|
||||||
|
final Map<String, String> timeUnitMap = <String, String>{
|
||||||
|
kUnitKey: item[_kTimeUnitKey] as String
|
||||||
|
};
|
||||||
|
for (final String subResult in item.keys) {
|
||||||
|
if (!_kNonNumericalValueSubResults.contains(subResult)) {
|
||||||
|
num rawValue;
|
||||||
|
try {
|
||||||
|
rawValue = item[subResult] as num;
|
||||||
|
} catch (e) {
|
||||||
|
print(
|
||||||
|
'$subResult: ${item[subResult]} (${item[subResult].runtimeType}) is not a number');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double value =
|
||||||
|
rawValue is int ? rawValue.toDouble() : rawValue as double;
|
||||||
|
points.add(
|
||||||
|
MetricPoint(
|
||||||
|
value,
|
||||||
|
<String, String>{kNameKey: name, kSubResultKey: subResult}
|
||||||
|
..addAll(context)
|
||||||
|
..addAll(
|
||||||
|
subResult.endsWith('time') ? timeUnitMap : <String, String>{}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
packages/metrics_center/lib/src/legacy_datastore.dart
Normal file
33
packages/metrics_center/lib/src/legacy_datastore.dart
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// TODO(liyuqian): Remove this file once the migration is fully done and we no
|
||||||
|
// longer need to fall back to the datastore.
|
||||||
|
|
||||||
|
import 'package:gcloud/db.dart';
|
||||||
|
import 'package:googleapis_auth/auth.dart';
|
||||||
|
import 'package:googleapis_auth/auth_io.dart';
|
||||||
|
|
||||||
|
// The official pub.dev/packages/gcloud documentation uses datastore_impl
|
||||||
|
// so we have to ignore implementation_imports here.
|
||||||
|
// ignore: implementation_imports
|
||||||
|
import 'package:gcloud/src/datastore_impl.dart';
|
||||||
|
|
||||||
|
import 'common.dart';
|
||||||
|
import 'constants.dart';
|
||||||
|
|
||||||
|
/// Creates a [DatastoreDB] connection from JSON service account credentials.
|
||||||
|
Future<DatastoreDB> datastoreFromCredentialsJson(
|
||||||
|
Map<String, dynamic> json) async {
|
||||||
|
final AutoRefreshingAuthClient client = await clientViaServiceAccount(
|
||||||
|
ServiceAccountCredentials.fromJson(json), DatastoreImpl.SCOPES);
|
||||||
|
return DatastoreDB(DatastoreImpl(client, json[kProjectId] as String));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [DatastoreDB] from an auth token.
|
||||||
|
DatastoreDB datastoreFromAccessToken(String token, String projectId) {
|
||||||
|
final AuthClient client =
|
||||||
|
authClientFromAccessToken(token, DatastoreImpl.SCOPES);
|
||||||
|
return DatastoreDB(DatastoreImpl(client, projectId));
|
||||||
|
}
|
91
packages/metrics_center/lib/src/legacy_flutter.dart
Normal file
91
packages/metrics_center/lib/src/legacy_flutter.dart
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// TODO(liyuqian): Remove this legacy file once the migration is fully done.
|
||||||
|
// See go/flutter-metrics-center-migration for detailed plans.
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:gcloud/db.dart';
|
||||||
|
|
||||||
|
import 'common.dart';
|
||||||
|
import 'constants.dart';
|
||||||
|
import 'legacy_datastore.dart';
|
||||||
|
|
||||||
|
/// This model corresponds to the existing data model 'MetricPoint' used in the
|
||||||
|
/// flutter-cirrus GCP project.
|
||||||
|
///
|
||||||
|
/// The originId and sourceTimeMicros fields are no longer used but we are still
|
||||||
|
/// providing valid values to them so it's compatible with old code and services
|
||||||
|
/// during the migration.
|
||||||
|
@Kind(name: 'MetricPoint', idType: IdType.String)
|
||||||
|
class LegacyMetricPointModel extends Model<String> {
|
||||||
|
/// Initializes a metrics point data model for the flutter-cirrus GCP project.
|
||||||
|
LegacyMetricPointModel({MetricPoint fromMetricPoint}) {
|
||||||
|
if (fromMetricPoint != null) {
|
||||||
|
id = fromMetricPoint.id;
|
||||||
|
value = fromMetricPoint.value;
|
||||||
|
originId = 'legacy-flutter';
|
||||||
|
sourceTimeMicros = null;
|
||||||
|
tags = fromMetricPoint.tags.keys
|
||||||
|
.map((String key) =>
|
||||||
|
jsonEncode(<String, dynamic>{key: fromMetricPoint.tags[key]}))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value of this metric.
|
||||||
|
@DoubleProperty(required: true, indexed: false)
|
||||||
|
double value;
|
||||||
|
|
||||||
|
/// Any tags associated with this metric.
|
||||||
|
@StringListProperty()
|
||||||
|
List<String> tags;
|
||||||
|
|
||||||
|
/// The origin of this metric, which is no longer used.
|
||||||
|
@StringProperty(required: true)
|
||||||
|
String originId;
|
||||||
|
|
||||||
|
/// The sourceTimeMicros field, which is no longer used.
|
||||||
|
@IntProperty(propertyName: kSourceTimeMicrosName)
|
||||||
|
int sourceTimeMicros;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [FlutterDestination] that is backwards compatible with the flutter-cirrus
|
||||||
|
/// GCP project.
|
||||||
|
class LegacyFlutterDestination extends MetricDestination {
|
||||||
|
/// Creates a legacy destination compatible with the flutter-cirrus GCP
|
||||||
|
/// project.
|
||||||
|
LegacyFlutterDestination(this._db);
|
||||||
|
|
||||||
|
/// Creates this destination from a service account credentials JSON file.
|
||||||
|
static Future<LegacyFlutterDestination> makeFromCredentialsJson(
|
||||||
|
Map<String, dynamic> json) async {
|
||||||
|
return LegacyFlutterDestination(await datastoreFromCredentialsJson(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates this destination to authorize with an OAuth access token.
|
||||||
|
static LegacyFlutterDestination makeFromAccessToken(
|
||||||
|
String accessToken, String projectId) {
|
||||||
|
return LegacyFlutterDestination(
|
||||||
|
datastoreFromAccessToken(accessToken, projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> update(List<MetricPoint> points) async {
|
||||||
|
final List<LegacyMetricPointModel> flutterCenterPoints = points
|
||||||
|
.map((MetricPoint p) => LegacyMetricPointModel(fromMetricPoint: p))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
for (int start = 0; start < points.length; start += kMaxBatchSize) {
|
||||||
|
final int end = min(start + kMaxBatchSize, points.length);
|
||||||
|
await _db.withTransaction((Transaction tx) async {
|
||||||
|
tx.queueMutations(inserts: flutterCenterPoints.sublist(start, end));
|
||||||
|
await tx.commit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final DatastoreDB _db;
|
||||||
|
}
|
438
packages/metrics_center/lib/src/skiaperf.dart
Normal file
438
packages/metrics_center/lib/src/skiaperf.dart
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
// 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 'package:gcloud/storage.dart';
|
||||||
|
import 'package:googleapis/storage/v1.dart' show DetailedApiRequestError;
|
||||||
|
import 'package:googleapis_auth/auth.dart';
|
||||||
|
import 'package:googleapis_auth/auth_io.dart';
|
||||||
|
|
||||||
|
import 'common.dart';
|
||||||
|
import 'constants.dart';
|
||||||
|
import 'gcs_lock.dart';
|
||||||
|
import 'github_helper.dart';
|
||||||
|
|
||||||
|
/// A [MetricPoint] modeled after the format that Skia Perf expects.
|
||||||
|
///
|
||||||
|
/// Skia Perf Format is a JSON file that looks like:
|
||||||
|
/// ```json
|
||||||
|
/// {
|
||||||
|
/// "gitHash": "fe4a4029a080bc955e9588d05a6cd9eb490845d4",
|
||||||
|
/// "key": {
|
||||||
|
/// "arch": "x86",
|
||||||
|
/// "gpu": "GTX660",
|
||||||
|
/// "model": "ShuttleA",
|
||||||
|
/// "os": "Ubuntu12"
|
||||||
|
/// },
|
||||||
|
/// "results": {
|
||||||
|
/// "ChunkAlloc_PushPop_640_480": {
|
||||||
|
/// "nonrendering": {
|
||||||
|
/// "min_ms": 0.01485466666666667,
|
||||||
|
/// "options": {
|
||||||
|
/// "source_type": "bench"
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// },
|
||||||
|
/// "DeferredSurfaceCopy_discardable_640_480": {
|
||||||
|
/// "565": {
|
||||||
|
/// "min_ms": 2.215988,
|
||||||
|
/// "options": {
|
||||||
|
/// "source_type": "bench"
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
class SkiaPerfPoint extends MetricPoint {
|
||||||
|
SkiaPerfPoint._(this.githubRepo, this.gitHash, this.testName, this.subResult,
|
||||||
|
double value, this._options, this.jsonUrl)
|
||||||
|
: assert(_options[kGithubRepoKey] == null),
|
||||||
|
assert(_options[kGitRevisionKey] == null),
|
||||||
|
assert(_options[kNameKey] == null),
|
||||||
|
super(
|
||||||
|
value,
|
||||||
|
<String, String>{}
|
||||||
|
..addAll(_options)
|
||||||
|
..addAll(<String, String>{
|
||||||
|
kGithubRepoKey: githubRepo,
|
||||||
|
kGitRevisionKey: gitHash,
|
||||||
|
kNameKey: testName,
|
||||||
|
kSubResultKey: subResult,
|
||||||
|
}),
|
||||||
|
) {
|
||||||
|
assert(tags[kGithubRepoKey] != null);
|
||||||
|
assert(tags[kGitRevisionKey] != null);
|
||||||
|
assert(tags[kNameKey] != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct [SkiaPerfPoint] from a well-formed [MetricPoint].
|
||||||
|
///
|
||||||
|
/// The [MetricPoint] must have [kGithubRepoKey], [kGitRevisionKey],
|
||||||
|
/// [kNameKey] in its tags for this to be successful.
|
||||||
|
///
|
||||||
|
/// If the [MetricPoint] has a tag 'date', that tag will be removed so Skia
|
||||||
|
/// perf can plot multiple metrics with different date as a single trace.
|
||||||
|
/// Skia perf will use the git revision's date instead of this date tag in
|
||||||
|
/// the time axis.
|
||||||
|
factory SkiaPerfPoint.fromPoint(MetricPoint p) {
|
||||||
|
final String githubRepo = p.tags[kGithubRepoKey];
|
||||||
|
final String gitHash = p.tags[kGitRevisionKey];
|
||||||
|
final String name = p.tags[kNameKey];
|
||||||
|
|
||||||
|
if (githubRepo == null || gitHash == null || name == null) {
|
||||||
|
throw '$kGithubRepoKey, $kGitRevisionKey, $kNameKey must be set in'
|
||||||
|
' the tags of $p.';
|
||||||
|
}
|
||||||
|
|
||||||
|
final String subResult = p.tags[kSubResultKey] ?? kSkiaPerfValueKey;
|
||||||
|
|
||||||
|
final Map<String, String> options = <String, String>{}..addEntries(
|
||||||
|
p.tags.entries.where(
|
||||||
|
(MapEntry<String, dynamic> entry) =>
|
||||||
|
entry.key != kGithubRepoKey &&
|
||||||
|
entry.key != kGitRevisionKey &&
|
||||||
|
entry.key != kNameKey &&
|
||||||
|
entry.key != kSubResultKey &&
|
||||||
|
// https://github.com/google/benchmark automatically generates a
|
||||||
|
// 'date' field. If it's included in options, the Skia perf won't
|
||||||
|
// be able to connect different points in a single trace because
|
||||||
|
// the date is always different.
|
||||||
|
entry.key != 'date',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return SkiaPerfPoint._(
|
||||||
|
githubRepo, gitHash, name, subResult, p.value, options, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// In the format of '<owner>/<name>' such as 'flutter/flutter' or
|
||||||
|
/// 'flutter/engine'.
|
||||||
|
final String githubRepo;
|
||||||
|
|
||||||
|
/// SHA such as 'ad20d368ffa09559754e4b2b5c12951341ca3b2d'
|
||||||
|
final String gitHash;
|
||||||
|
|
||||||
|
/// For Flutter devicelab, this is the task name (e.g.,
|
||||||
|
/// 'flutter_gallery__transition_perf'); for Google benchmark, this is the
|
||||||
|
/// benchmark name (e.g., 'BM_ShellShutdown').
|
||||||
|
///
|
||||||
|
/// In Skia perf web dashboard, this value can be queried and filtered by
|
||||||
|
/// "test".
|
||||||
|
final String testName;
|
||||||
|
|
||||||
|
/// The name of "subResult" comes from the special treatment of "sub_result"
|
||||||
|
/// in SkiaPerf. If not provided, its value will be set to kSkiaPerfValueKey.
|
||||||
|
///
|
||||||
|
/// When Google benchmarks are converted to SkiaPerfPoint, this subResult
|
||||||
|
/// could be "cpu_time" or "real_time".
|
||||||
|
///
|
||||||
|
/// When devicelab benchmarks are converted to SkiaPerfPoint, this subResult
|
||||||
|
/// is often the metric name such as "average_frame_build_time_millis" whereas
|
||||||
|
/// the [testName] is the benchmark or task name such as
|
||||||
|
/// "flutter_gallery__transition_perf".
|
||||||
|
final String subResult;
|
||||||
|
|
||||||
|
/// The url to the Skia perf json file in the Google Cloud Storage bucket.
|
||||||
|
///
|
||||||
|
/// This can be null if the point has been stored in the bucket yet.
|
||||||
|
final String jsonUrl;
|
||||||
|
|
||||||
|
Map<String, dynamic> _toSubResultJson() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
subResult: value,
|
||||||
|
kSkiaPerfOptionsKey: _options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a list of SkiaPoints with the same git repo and git revision into
|
||||||
|
/// a single json file in the Skia perf format.
|
||||||
|
///
|
||||||
|
/// The list must be non-empty.
|
||||||
|
static Map<String, dynamic> toSkiaPerfJson(List<SkiaPerfPoint> points) {
|
||||||
|
assert(points.isNotEmpty);
|
||||||
|
assert(() {
|
||||||
|
for (final SkiaPerfPoint p in points) {
|
||||||
|
if (p.githubRepo != points[0].githubRepo ||
|
||||||
|
p.gitHash != points[0].gitHash) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}(), 'All points must have same githubRepo and gitHash');
|
||||||
|
|
||||||
|
final Map<String, dynamic> results = <String, dynamic>{};
|
||||||
|
for (final SkiaPerfPoint p in points) {
|
||||||
|
final Map<String, dynamic> subResultJson = p._toSubResultJson();
|
||||||
|
if (results[p.testName] == null) {
|
||||||
|
results[p.testName] = <String, dynamic>{
|
||||||
|
kSkiaPerfDefaultConfig: subResultJson,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Flutter currently doesn't support having the same name but different
|
||||||
|
// options/configurations. If this actually happens in the future, we
|
||||||
|
// probably can use different values of config (currently there's only
|
||||||
|
// one kSkiaPerfDefaultConfig) to resolve the conflict.
|
||||||
|
assert(results[p.testName][kSkiaPerfDefaultConfig][kSkiaPerfOptionsKey]
|
||||||
|
.toString() ==
|
||||||
|
subResultJson[kSkiaPerfOptionsKey].toString());
|
||||||
|
assert(
|
||||||
|
results[p.testName][kSkiaPerfDefaultConfig][p.subResult] == null);
|
||||||
|
results[p.testName][kSkiaPerfDefaultConfig][p.subResult] = p.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <String, dynamic>{
|
||||||
|
kSkiaPerfGitHashKey: points[0].gitHash,
|
||||||
|
kSkiaPerfResultsKey: results,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equivalent to tags without git repo, git hash, and name because those two
|
||||||
|
// are already stored somewhere else.
|
||||||
|
final Map<String, String> _options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle writing and updates of Skia perf GCS buckets.
|
||||||
|
class SkiaPerfGcsAdaptor {
|
||||||
|
/// Construct the adaptor given the associated GCS bucket where the data is
|
||||||
|
/// read from and written to.
|
||||||
|
SkiaPerfGcsAdaptor(this._gcsBucket) : assert(_gcsBucket != null);
|
||||||
|
|
||||||
|
/// Used by Skia to differentiate json file format versions.
|
||||||
|
static const int version = 1;
|
||||||
|
|
||||||
|
/// Write a list of SkiaPerfPoint into a GCS file with name `objectName` in
|
||||||
|
/// the proper json format that's understandable by Skia perf services.
|
||||||
|
///
|
||||||
|
/// The `objectName` must be a properly formatted string returned by
|
||||||
|
/// [computeObjectName].
|
||||||
|
///
|
||||||
|
/// The read may retry multiple times if transient network errors with code
|
||||||
|
/// 504 happens.
|
||||||
|
Future<void> writePoints(
|
||||||
|
String objectName, List<SkiaPerfPoint> points) async {
|
||||||
|
final String jsonString = jsonEncode(SkiaPerfPoint.toSkiaPerfJson(points));
|
||||||
|
final List<int> content = utf8.encode(jsonString);
|
||||||
|
|
||||||
|
// Retry multiple times as GCS may return 504 timeout.
|
||||||
|
for (int retry = 0; retry < 5; retry += 1) {
|
||||||
|
try {
|
||||||
|
await _gcsBucket.writeBytes(objectName, content);
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
if (e is DetailedApiRequestError && e.status == 504) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Retry one last time and let the exception go through.
|
||||||
|
await _gcsBucket.writeBytes(objectName, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a list of `SkiaPerfPoint` that have been previously written to the
|
||||||
|
/// GCS file with name `objectName`.
|
||||||
|
///
|
||||||
|
/// The Github repo and revision of those points will be inferred from the
|
||||||
|
/// `objectName`.
|
||||||
|
///
|
||||||
|
/// Return an empty list if the object does not exist in the GCS bucket.
|
||||||
|
///
|
||||||
|
/// The read may retry multiple times if transient network errors with code
|
||||||
|
/// 504 happens.
|
||||||
|
Future<List<SkiaPerfPoint>> readPoints(String objectName) async {
|
||||||
|
// Retry multiple times as GCS may return 504 timeout.
|
||||||
|
for (int retry = 0; retry < 5; retry += 1) {
|
||||||
|
try {
|
||||||
|
return await _readPointsWithoutRetry(objectName);
|
||||||
|
} catch (e) {
|
||||||
|
if (e is DetailedApiRequestError && e.status == 504) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Retry one last time and let the exception go through.
|
||||||
|
return await _readPointsWithoutRetry(objectName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SkiaPerfPoint>> _readPointsWithoutRetry(String objectName) async {
|
||||||
|
ObjectInfo info;
|
||||||
|
|
||||||
|
try {
|
||||||
|
info = await _gcsBucket.info(objectName);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('No such object')) {
|
||||||
|
return <SkiaPerfPoint>[];
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Stream<List<int>> stream = _gcsBucket.read(objectName);
|
||||||
|
final Stream<int> byteStream = stream.expand((List<int> x) => x);
|
||||||
|
final Map<String, dynamic> decodedJson =
|
||||||
|
jsonDecode(utf8.decode(await byteStream.toList()))
|
||||||
|
as Map<String, dynamic>;
|
||||||
|
|
||||||
|
final List<SkiaPerfPoint> points = <SkiaPerfPoint>[];
|
||||||
|
|
||||||
|
final String firstGcsNameComponent = objectName.split('/')[0];
|
||||||
|
_populateGcsNameToGithubRepoMapIfNeeded();
|
||||||
|
final String githubRepo = _gcsNameToGithubRepo[firstGcsNameComponent];
|
||||||
|
assert(githubRepo != null);
|
||||||
|
|
||||||
|
final String gitHash = decodedJson[kSkiaPerfGitHashKey] as String;
|
||||||
|
final Map<String, dynamic> results =
|
||||||
|
decodedJson[kSkiaPerfResultsKey] as Map<String, dynamic>;
|
||||||
|
for (final String name in results.keys) {
|
||||||
|
final Map<String, dynamic> subResultMap =
|
||||||
|
results[name][kSkiaPerfDefaultConfig] as Map<String, dynamic>;
|
||||||
|
for (final String subResult
|
||||||
|
in subResultMap.keys.where((String s) => s != kSkiaPerfOptionsKey)) {
|
||||||
|
points.add(SkiaPerfPoint._(
|
||||||
|
githubRepo,
|
||||||
|
gitHash,
|
||||||
|
name,
|
||||||
|
subResult,
|
||||||
|
subResultMap[subResult] as double,
|
||||||
|
(subResultMap[kSkiaPerfOptionsKey] as Map<String, dynamic>)
|
||||||
|
.cast<String, String>(),
|
||||||
|
info.downloadLink.toString(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the GCS file name that's used to store metrics for a given commit
|
||||||
|
/// (git revision).
|
||||||
|
///
|
||||||
|
/// Skia perf needs all directory names to be well formatted. The final name
|
||||||
|
/// of the json file (currently `values.json`) can be arbitrary, and multiple
|
||||||
|
/// json files can be put in that leaf directory. We intend to use multiple
|
||||||
|
/// json files in the future to scale up the system if too many writes are
|
||||||
|
/// competing for the same json file.
|
||||||
|
static Future<String> computeObjectName(String githubRepo, String revision,
|
||||||
|
{GithubHelper githubHelper}) async {
|
||||||
|
assert(_githubRepoToGcsName[githubRepo] != null);
|
||||||
|
final String topComponent = _githubRepoToGcsName[githubRepo];
|
||||||
|
final DateTime t = await (githubHelper ?? GithubHelper())
|
||||||
|
.getCommitDateTime(githubRepo, revision);
|
||||||
|
final String month = t.month.toString().padLeft(2, '0');
|
||||||
|
final String day = t.day.toString().padLeft(2, '0');
|
||||||
|
final String hour = t.hour.toString().padLeft(2, '0');
|
||||||
|
final String dateComponents = '${t.year}/$month/$day/$hour';
|
||||||
|
return '$topComponent/$dateComponents/$revision/values.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
static final Map<String, String> _githubRepoToGcsName = <String, String>{
|
||||||
|
kFlutterFrameworkRepo: 'flutter-flutter',
|
||||||
|
kFlutterEngineRepo: 'flutter-engine',
|
||||||
|
};
|
||||||
|
static final Map<String, String> _gcsNameToGithubRepo = <String, String>{};
|
||||||
|
|
||||||
|
static void _populateGcsNameToGithubRepoMapIfNeeded() {
|
||||||
|
if (_gcsNameToGithubRepo.isEmpty) {
|
||||||
|
for (final String repo in _githubRepoToGcsName.keys) {
|
||||||
|
final String gcsName = _githubRepoToGcsName[repo];
|
||||||
|
assert(_gcsNameToGithubRepo[gcsName] == null);
|
||||||
|
_gcsNameToGithubRepo[gcsName] = repo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Bucket _gcsBucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [MetricDestination] that conforms to Skia Perf's protocols.
|
||||||
|
class SkiaPerfDestination extends MetricDestination {
|
||||||
|
/// Creates a new [SkiaPerfDestination].
|
||||||
|
SkiaPerfDestination(this._gcs, this._lock);
|
||||||
|
|
||||||
|
/// Create from a full credentials json (of a service account).
|
||||||
|
static Future<SkiaPerfDestination> makeFromGcpCredentials(
|
||||||
|
Map<String, dynamic> credentialsJson,
|
||||||
|
{bool isTesting = false}) async {
|
||||||
|
final AutoRefreshingAuthClient client = await clientViaServiceAccount(
|
||||||
|
ServiceAccountCredentials.fromJson(credentialsJson), Storage.SCOPES);
|
||||||
|
return make(
|
||||||
|
client,
|
||||||
|
credentialsJson[kProjectId] as String,
|
||||||
|
isTesting: isTesting,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create from an access token and its project id.
|
||||||
|
static Future<SkiaPerfDestination> makeFromAccessToken(
|
||||||
|
String token, String projectId,
|
||||||
|
{bool isTesting = false}) async {
|
||||||
|
final AuthClient client = authClientFromAccessToken(token, Storage.SCOPES);
|
||||||
|
return make(client, projectId, isTesting: isTesting);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create from an [AuthClient] and a GCP project id.
|
||||||
|
///
|
||||||
|
/// [AuthClient] can be obtained from functions like `clientViaUserConsent`.
|
||||||
|
static Future<SkiaPerfDestination> make(AuthClient client, String projectId,
|
||||||
|
{bool isTesting = false}) async {
|
||||||
|
final Storage storage = Storage(client, projectId);
|
||||||
|
final String bucketName = isTesting ? kTestBucketName : kBucketName;
|
||||||
|
if (!await storage.bucketExists(bucketName)) {
|
||||||
|
throw 'Bucket $bucketName does not exist.';
|
||||||
|
}
|
||||||
|
final SkiaPerfGcsAdaptor adaptor =
|
||||||
|
SkiaPerfGcsAdaptor(storage.bucket(bucketName));
|
||||||
|
final GcsLock lock = GcsLock(client, bucketName);
|
||||||
|
return SkiaPerfDestination(adaptor, lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> update(List<MetricPoint> points) async {
|
||||||
|
// 1st, create a map based on git repo, git revision, and point id. Git repo
|
||||||
|
// and git revision are the top level components of the Skia perf GCS object
|
||||||
|
// name.
|
||||||
|
final Map<String, Map<String, Map<String, SkiaPerfPoint>>> pointMap =
|
||||||
|
<String, Map<String, Map<String, SkiaPerfPoint>>>{};
|
||||||
|
for (final SkiaPerfPoint p
|
||||||
|
in points.map((MetricPoint x) => SkiaPerfPoint.fromPoint(x))) {
|
||||||
|
if (p != null) {
|
||||||
|
pointMap[p.githubRepo] ??= <String, Map<String, SkiaPerfPoint>>{};
|
||||||
|
pointMap[p.githubRepo][p.gitHash] ??= <String, SkiaPerfPoint>{};
|
||||||
|
pointMap[p.githubRepo][p.gitHash][p.id] = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2nd, read existing points from the gcs object and update with new ones.
|
||||||
|
for (final String repo in pointMap.keys) {
|
||||||
|
for (final String revision in pointMap[repo].keys) {
|
||||||
|
final String objectName =
|
||||||
|
await SkiaPerfGcsAdaptor.computeObjectName(repo, revision);
|
||||||
|
final Map<String, SkiaPerfPoint> newPoints = pointMap[repo][revision];
|
||||||
|
// If too many bots are writing the metrics of a git revision into this
|
||||||
|
// single json file (with name `objectName`), the contention on the lock
|
||||||
|
// might be too high. In that case, break the json file into multiple
|
||||||
|
// json files according to bot names or task names. Skia perf read all
|
||||||
|
// json files in the directory so one can use arbitrary names for those
|
||||||
|
// sharded json file names.
|
||||||
|
_lock.protectedRun('$objectName.lock', () async {
|
||||||
|
final List<SkiaPerfPoint> oldPoints =
|
||||||
|
await _gcs.readPoints(objectName);
|
||||||
|
for (final SkiaPerfPoint p in oldPoints) {
|
||||||
|
if (newPoints[p.id] == null) {
|
||||||
|
newPoints[p.id] = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await _gcs.writePoints(objectName, newPoints.values.toList());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final SkiaPerfGcsAdaptor _gcs;
|
||||||
|
final GcsLock _lock;
|
||||||
|
}
|
25
packages/metrics_center/pubspec.yaml
Normal file
25
packages/metrics_center/pubspec.yaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
name: metrics_center
|
||||||
|
version: 0.0.4+1
|
||||||
|
description:
|
||||||
|
Support multiple performance metrics sources/formats and destinations.
|
||||||
|
homepage:
|
||||||
|
https://github.com/flutter/packages/tree/master/packages/metrics_center
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=2.10.0 <3.0.0'
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
args: ^1.6.0
|
||||||
|
crypto: ^2.1.5
|
||||||
|
equatable: ^1.2.5
|
||||||
|
gcloud: ^0.7.3
|
||||||
|
github: ^7.0.4
|
||||||
|
googleapis: ^0.56.1
|
||||||
|
googleapis_auth: ^0.2.12
|
||||||
|
http: ^0.12.2
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
fake_async: ^1.2.0-nullsafety.3
|
||||||
|
mockito: ^4.1.1
|
||||||
|
pedantic: ^1.10.0-nullsafety.3
|
||||||
|
test: ^1.16.0-nullsafety.9
|
27
packages/metrics_center/test/common.dart
Normal file
27
packages/metrics_center/test/common.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// 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:io';
|
||||||
|
|
||||||
|
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
|
||||||
|
import 'package:test/test.dart' as test_package show TypeMatcher;
|
||||||
|
|
||||||
|
export 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
|
||||||
|
|
||||||
|
// Defines a 'package:test' shim.
|
||||||
|
// TODO(ianh): Remove this file once https://github.com/dart-lang/matcher/issues/98 is fixed
|
||||||
|
|
||||||
|
/// A matcher that compares the type of the actual value to the type argument T.
|
||||||
|
test_package.TypeMatcher<T> isInstanceOf<T>() => isA<T>();
|
||||||
|
|
||||||
|
void tryToDelete(Directory directory) {
|
||||||
|
// This should not be necessary, but it turns out that
|
||||||
|
// on Windows it's common for deletions to fail due to
|
||||||
|
// bogus (we think) "access denied" errors.
|
||||||
|
try {
|
||||||
|
directory.deleteSync(recursive: true);
|
||||||
|
} on FileSystemException catch (error) {
|
||||||
|
print('Failed to delete ${directory.path}: $error');
|
||||||
|
}
|
||||||
|
}
|
32
packages/metrics_center/test/example_google_benchmark.json
Normal file
32
packages/metrics_center/test/example_google_benchmark.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"context": {
|
||||||
|
"date": "2019-12-17 15:14:14",
|
||||||
|
"num_cpus": 56,
|
||||||
|
"mhz_per_cpu": 2594,
|
||||||
|
"cpu_scaling_enabled": true,
|
||||||
|
"library_build_type": "release"
|
||||||
|
},
|
||||||
|
"benchmarks": [
|
||||||
|
{
|
||||||
|
"name": "BM_PaintRecordInit",
|
||||||
|
"iterations": 6749079,
|
||||||
|
"real_time": 101,
|
||||||
|
"cpu_time": 101,
|
||||||
|
"time_unit": "ns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BM_ParagraphShortLayout",
|
||||||
|
"iterations": 151761,
|
||||||
|
"real_time": 4460,
|
||||||
|
"cpu_time": 4460,
|
||||||
|
"time_unit": "ns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BM_ParagraphStylesBigO_BigO",
|
||||||
|
"cpu_coefficient": 6548,
|
||||||
|
"real_coefficient": 6548,
|
||||||
|
"big_o": "N",
|
||||||
|
"time_unit": "ns"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
38
packages/metrics_center/test/flutter_test.dart
Normal file
38
packages/metrics_center/test/flutter_test.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// 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 'package:metrics_center/src/constants.dart';
|
||||||
|
import 'package:metrics_center/src/flutter.dart';
|
||||||
|
|
||||||
|
import 'common.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('FlutterEngineMetricPoint works.', () {
|
||||||
|
const String gitRevision = 'ca799fa8b2254d09664b78ee80c43b434788d112';
|
||||||
|
final FlutterEngineMetricPoint simplePoint = FlutterEngineMetricPoint(
|
||||||
|
'BM_ParagraphLongLayout',
|
||||||
|
287235,
|
||||||
|
gitRevision,
|
||||||
|
);
|
||||||
|
expect(simplePoint.value, equals(287235));
|
||||||
|
expect(simplePoint.tags[kGithubRepoKey], kFlutterEngineRepo);
|
||||||
|
expect(simplePoint.tags[kGitRevisionKey], gitRevision);
|
||||||
|
expect(simplePoint.tags[kNameKey], 'BM_ParagraphLongLayout');
|
||||||
|
|
||||||
|
final FlutterEngineMetricPoint detailedPoint = FlutterEngineMetricPoint(
|
||||||
|
'BM_ParagraphLongLayout',
|
||||||
|
287224,
|
||||||
|
'ca799fa8b2254d09664b78ee80c43b434788d112',
|
||||||
|
moreTags: const <String, String>{
|
||||||
|
'executable': 'txt_benchmarks',
|
||||||
|
'sub_result': 'CPU',
|
||||||
|
kUnitKey: 'ns',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(detailedPoint.value, equals(287224));
|
||||||
|
expect(detailedPoint.tags['executable'], equals('txt_benchmarks'));
|
||||||
|
expect(detailedPoint.tags['sub_result'], equals('CPU'));
|
||||||
|
expect(detailedPoint.tags[kUnitKey], equals('ns'));
|
||||||
|
});
|
||||||
|
}
|
105
packages/metrics_center/test/gcs_lock_test.dart
Normal file
105
packages/metrics_center/test/gcs_lock_test.dart
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// 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 'package:googleapis/storage/v1.dart';
|
||||||
|
import 'package:fake_async/fake_async.dart';
|
||||||
|
import 'package:gcloud/storage.dart';
|
||||||
|
import 'package:googleapis_auth/auth_io.dart';
|
||||||
|
import 'package:metrics_center/src/constants.dart';
|
||||||
|
import 'package:metrics_center/src/gcs_lock.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
|
||||||
|
import 'common.dart';
|
||||||
|
import 'utility.dart';
|
||||||
|
|
||||||
|
enum TestPhase {
|
||||||
|
run1,
|
||||||
|
run2,
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockClient extends Mock implements AuthClient {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
const Duration kDelayStep = Duration(milliseconds: 10);
|
||||||
|
final Map<String, dynamic> credentialsJson = getTestGcpCredentialsJson();
|
||||||
|
|
||||||
|
test('GcsLock prints warnings for long waits', () {
|
||||||
|
// Capture print to verify error messages.
|
||||||
|
final List<String> prints = <String>[];
|
||||||
|
final ZoneSpecification spec =
|
||||||
|
ZoneSpecification(print: (_, __, ___, String msg) => prints.add(msg));
|
||||||
|
|
||||||
|
Zone.current.fork(specification: spec).run<void>(() {
|
||||||
|
fakeAsync((FakeAsync fakeAsync) {
|
||||||
|
final MockClient mockClient = MockClient();
|
||||||
|
final GcsLock lock = GcsLock(mockClient, 'mockBucket');
|
||||||
|
when(mockClient.send(any)).thenThrow(DetailedApiRequestError(412, ''));
|
||||||
|
final Future<void> runFinished =
|
||||||
|
lock.protectedRun('mock.lock', () async {});
|
||||||
|
fakeAsync.elapse(const Duration(seconds: 10));
|
||||||
|
when(mockClient.send(any)).thenThrow(AssertionError('Stop!'));
|
||||||
|
runFinished.catchError((dynamic e) {
|
||||||
|
final AssertionError error = e as AssertionError;
|
||||||
|
expect(error.message, 'Stop!');
|
||||||
|
print('${error.message}');
|
||||||
|
});
|
||||||
|
fakeAsync.elapse(const Duration(seconds: 20));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const String kExpectedErrorMessage = 'The lock is waiting for a long time: '
|
||||||
|
'0:00:10.240000. If the lock file mock.lock in bucket mockBucket '
|
||||||
|
'seems to be stuck (i.e., it was created a long time ago and no one '
|
||||||
|
'seems to be owning it currently), delete it manually to unblock this.';
|
||||||
|
expect(prints, equals(<String>[kExpectedErrorMessage, 'Stop!']));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GcsLock integration test: single protectedRun is successful', () async {
|
||||||
|
final AutoRefreshingAuthClient client = await clientViaServiceAccount(
|
||||||
|
ServiceAccountCredentials.fromJson(credentialsJson), Storage.SCOPES);
|
||||||
|
final GcsLock lock = GcsLock(client, kTestBucketName);
|
||||||
|
int testValue = 0;
|
||||||
|
await lock.protectedRun('test.lock', () async {
|
||||||
|
testValue = 1;
|
||||||
|
});
|
||||||
|
expect(testValue, 1);
|
||||||
|
}, skip: credentialsJson == null);
|
||||||
|
|
||||||
|
test('GcsLock integration test: protectedRun is exclusive', () async {
|
||||||
|
final AutoRefreshingAuthClient client = await clientViaServiceAccount(
|
||||||
|
ServiceAccountCredentials.fromJson(credentialsJson), Storage.SCOPES);
|
||||||
|
final GcsLock lock1 = GcsLock(client, kTestBucketName);
|
||||||
|
final GcsLock lock2 = GcsLock(client, kTestBucketName);
|
||||||
|
|
||||||
|
TestPhase phase = TestPhase.run1;
|
||||||
|
final Completer<void> started1 = Completer<void>();
|
||||||
|
final Future<void> finished1 = lock1.protectedRun('test.lock', () async {
|
||||||
|
started1.complete();
|
||||||
|
while (phase == TestPhase.run1) {
|
||||||
|
await Future<void>.delayed(kDelayStep);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await started1.future;
|
||||||
|
|
||||||
|
final Completer<void> started2 = Completer<void>();
|
||||||
|
final Future<void> finished2 = lock2.protectedRun('test.lock', () async {
|
||||||
|
started2.complete();
|
||||||
|
});
|
||||||
|
|
||||||
|
// started2 should not be set even after a long wait because lock1 is
|
||||||
|
// holding the GCS lock file.
|
||||||
|
await Future<void>.delayed(kDelayStep * 10);
|
||||||
|
expect(started2.isCompleted, false);
|
||||||
|
|
||||||
|
// When phase is switched to run2, lock1 should be released soon and
|
||||||
|
// lock2 should soon be able to proceed its protectedRun.
|
||||||
|
phase = TestPhase.run2;
|
||||||
|
await started2.future;
|
||||||
|
await finished1;
|
||||||
|
await finished2;
|
||||||
|
}, skip: credentialsJson == null);
|
||||||
|
}
|
41
packages/metrics_center/test/github_helper_test.dart
Normal file
41
packages/metrics_center/test/github_helper_test.dart
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
@Timeout(Duration(seconds: 3600))
|
||||||
|
|
||||||
|
import 'package:metrics_center/src/github_helper.dart';
|
||||||
|
|
||||||
|
import 'common.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('GithubHelper gets correct commit date time', () async {
|
||||||
|
final GithubHelper helper = GithubHelper();
|
||||||
|
expect(
|
||||||
|
await helper.getCommitDateTime(
|
||||||
|
'flutter/flutter',
|
||||||
|
'ad20d368ffa09559754e4b2b5c12951341ca3b2d',
|
||||||
|
),
|
||||||
|
equals(DateTime.parse('2019-12-06 03:33:01.000Z')),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GithubHelper is a singleton', () {
|
||||||
|
final GithubHelper helper1 = GithubHelper();
|
||||||
|
final GithubHelper helper2 = GithubHelper();
|
||||||
|
expect(helper1, equals(helper2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GithubHelper can query the same commit 1000 times within 1 second',
|
||||||
|
() async {
|
||||||
|
final DateTime start = DateTime.now();
|
||||||
|
for (int i = 0; i < 1000; i += 1) {
|
||||||
|
await GithubHelper().getCommitDateTime(
|
||||||
|
'flutter/flutter',
|
||||||
|
'ad20d368ffa09559754e4b2b5c12951341ca3b2d',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final Duration duration = DateTime.now().difference(start);
|
||||||
|
expect(duration, lessThan(const Duration(seconds: 1)));
|
||||||
|
});
|
||||||
|
}
|
39
packages/metrics_center/test/google_benchmark_test.dart
Normal file
39
packages/metrics_center/test/google_benchmark_test.dart
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// 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 'package:metrics_center/src/constants.dart';
|
||||||
|
import 'package:metrics_center/src/common.dart';
|
||||||
|
import 'package:metrics_center/src/google_benchmark.dart';
|
||||||
|
|
||||||
|
import 'common.dart';
|
||||||
|
import 'utility.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('GoogleBenchmarkParser parses example json.', () async {
|
||||||
|
final List<MetricPoint> points =
|
||||||
|
await GoogleBenchmarkParser.parse('test/example_google_benchmark.json');
|
||||||
|
expect(points.length, 6);
|
||||||
|
expectSetMatch(
|
||||||
|
points.map((MetricPoint p) => p.value),
|
||||||
|
<int>[101, 101, 4460, 4460, 6548, 6548],
|
||||||
|
);
|
||||||
|
expectSetMatch(
|
||||||
|
points.map((MetricPoint p) => p.tags[kSubResultKey]),
|
||||||
|
<String>[
|
||||||
|
'cpu_time',
|
||||||
|
'real_time',
|
||||||
|
'cpu_coefficient',
|
||||||
|
'real_coefficient',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expectSetMatch(
|
||||||
|
points.map((MetricPoint p) => p.tags[kNameKey]),
|
||||||
|
<String>[
|
||||||
|
'BM_PaintRecordInit',
|
||||||
|
'BM_ParagraphShortLayout',
|
||||||
|
'BM_ParagraphStylesBigO_BigO',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
41
packages/metrics_center/test/legacy_flutter_test.dart
Normal file
41
packages/metrics_center/test/legacy_flutter_test.dart
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// 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 'package:gcloud/src/datastore_impl.dart';
|
||||||
|
import 'package:googleapis_auth/auth_io.dart';
|
||||||
|
import 'package:metrics_center/src/constants.dart';
|
||||||
|
import 'package:metrics_center/src/common.dart';
|
||||||
|
import 'package:metrics_center/src/legacy_flutter.dart';
|
||||||
|
|
||||||
|
import 'common.dart';
|
||||||
|
import 'utility.dart';
|
||||||
|
|
||||||
|
const String kTestSourceId = 'test';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final Map<String, dynamic> credentialsJson = getTestGcpCredentialsJson();
|
||||||
|
test(
|
||||||
|
'LegacyFlutterDestination integration test: '
|
||||||
|
'update does not crash.', () async {
|
||||||
|
final LegacyFlutterDestination dst =
|
||||||
|
await LegacyFlutterDestination.makeFromCredentialsJson(credentialsJson);
|
||||||
|
await dst.update(<MetricPoint>[MetricPoint(1.0, const <String, String>{})]);
|
||||||
|
}, skip: credentialsJson == null);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'LegacyFlutterDestination integration test: '
|
||||||
|
'can update with an access token.', () async {
|
||||||
|
final AutoRefreshingAuthClient client = await clientViaServiceAccount(
|
||||||
|
ServiceAccountCredentials.fromJson(credentialsJson),
|
||||||
|
DatastoreImpl.SCOPES,
|
||||||
|
);
|
||||||
|
final String token = client.credentials.accessToken.data;
|
||||||
|
final LegacyFlutterDestination dst =
|
||||||
|
LegacyFlutterDestination.makeFromAccessToken(
|
||||||
|
token,
|
||||||
|
credentialsJson[kProjectId] as String,
|
||||||
|
);
|
||||||
|
await dst.update(<MetricPoint>[MetricPoint(1.0, const <String, String>{})]);
|
||||||
|
}, skip: credentialsJson == null);
|
||||||
|
}
|
573
packages/metrics_center/test/skiaperf_test.dart
Normal file
573
packages/metrics_center/test/skiaperf_test.dart
Normal file
@ -0,0 +1,573 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
@Timeout(Duration(seconds: 3600))
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:gcloud/storage.dart';
|
||||||
|
import 'package:googleapis/storage/v1.dart' show DetailedApiRequestError;
|
||||||
|
import 'package:googleapis_auth/auth_io.dart';
|
||||||
|
import 'package:metrics_center/metrics_center.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
|
||||||
|
import 'package:metrics_center/src/constants.dart';
|
||||||
|
import 'package:metrics_center/src/github_helper.dart';
|
||||||
|
import 'package:metrics_center/src/gcs_lock.dart';
|
||||||
|
|
||||||
|
import 'common.dart';
|
||||||
|
import 'utility.dart';
|
||||||
|
|
||||||
|
class MockBucket extends Mock implements Bucket {}
|
||||||
|
|
||||||
|
class MockObjectInfo extends Mock implements ObjectInfo {}
|
||||||
|
|
||||||
|
class MockGithubHelper extends Mock implements GithubHelper {}
|
||||||
|
|
||||||
|
class MockGcsLock implements GcsLock {
|
||||||
|
@override
|
||||||
|
Future<void> protectedRun(
|
||||||
|
String exclusiveObjectName, Future<void> Function() f) async {
|
||||||
|
await f();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockSkiaPerfGcsAdaptor implements SkiaPerfGcsAdaptor {
|
||||||
|
@override
|
||||||
|
Future<List<SkiaPerfPoint>> readPoints(String objectName) async {
|
||||||
|
return _storage[objectName] ?? <SkiaPerfPoint>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> writePoints(
|
||||||
|
String objectName, List<SkiaPerfPoint> points) async {
|
||||||
|
_storage[objectName] = points.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map from the object name to the list of SkiaPoint that mocks the GCS.
|
||||||
|
final Map<String, List<SkiaPerfPoint>> _storage =
|
||||||
|
<String, List<SkiaPerfPoint>>{};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
const double kValue1 = 1.0;
|
||||||
|
const double kValue2 = 2.0;
|
||||||
|
const double kValue3 = 3.0;
|
||||||
|
|
||||||
|
const String kFrameworkRevision1 = '9011cece2595447eea5dd91adaa241c1c9ef9a33';
|
||||||
|
const String kFrameworkRevision2 = '372fe290e4d4f3f97cbf02a57d235771a9412f10';
|
||||||
|
const String kEngineRevision1 = '617938024315e205f26ed72ff0f0647775fa6a71';
|
||||||
|
const String kEngineRevision2 = '5858519139c22484aaff1cf5b26bdf7951259344';
|
||||||
|
const String kTaskName = 'analyzer_benchmark';
|
||||||
|
const String kMetric1 = 'flutter_repo_batch_maximum';
|
||||||
|
const String kMetric2 = 'flutter_repo_watch_maximum';
|
||||||
|
|
||||||
|
final MetricPoint cocoonPointRev1Metric1 = MetricPoint(
|
||||||
|
kValue1,
|
||||||
|
const <String, String>{
|
||||||
|
kGithubRepoKey: kFlutterFrameworkRepo,
|
||||||
|
kGitRevisionKey: kFrameworkRevision1,
|
||||||
|
kNameKey: kTaskName,
|
||||||
|
kSubResultKey: kMetric1,
|
||||||
|
kUnitKey: 's',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final MetricPoint cocoonPointRev1Metric2 = MetricPoint(
|
||||||
|
kValue2,
|
||||||
|
const <String, String>{
|
||||||
|
kGithubRepoKey: kFlutterFrameworkRepo,
|
||||||
|
kGitRevisionKey: kFrameworkRevision1,
|
||||||
|
kNameKey: kTaskName,
|
||||||
|
kSubResultKey: kMetric2,
|
||||||
|
kUnitKey: 's',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final MetricPoint cocoonPointRev2Metric1 = MetricPoint(
|
||||||
|
kValue3,
|
||||||
|
const <String, String>{
|
||||||
|
kGithubRepoKey: kFlutterFrameworkRepo,
|
||||||
|
kGitRevisionKey: kFrameworkRevision2,
|
||||||
|
kNameKey: kTaskName,
|
||||||
|
kSubResultKey: kMetric1,
|
||||||
|
kUnitKey: 's',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final MetricPoint cocoonPointBetaRev1Metric1 = MetricPoint(
|
||||||
|
kValue1,
|
||||||
|
const <String, String>{
|
||||||
|
kGithubRepoKey: kFlutterFrameworkRepo,
|
||||||
|
kGitRevisionKey: kFrameworkRevision1,
|
||||||
|
kNameKey: 'beta/$kTaskName',
|
||||||
|
kSubResultKey: kMetric1,
|
||||||
|
kUnitKey: 's',
|
||||||
|
'branch': 'beta',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final MetricPoint cocoonPointBetaRev1Metric1BadBranch = MetricPoint(
|
||||||
|
kValue1,
|
||||||
|
const <String, String>{
|
||||||
|
kGithubRepoKey: kFlutterFrameworkRepo,
|
||||||
|
kGitRevisionKey: kFrameworkRevision1,
|
||||||
|
kNameKey: kTaskName,
|
||||||
|
kSubResultKey: kMetric1,
|
||||||
|
kUnitKey: 's',
|
||||||
|
|
||||||
|
// If we only add this 'branch' tag without changing the test or sub-result name, an exception
|
||||||
|
// would be thrown as Skia Perf currently only supports the same set of tags for a pair of
|
||||||
|
// kNameKey and kSubResultKey values. So to support branches, one also has to add the branch
|
||||||
|
// name to the test name.
|
||||||
|
'branch': 'beta',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const String engineMetricName = 'BM_PaintRecordInit';
|
||||||
|
const String engineRevision = 'ca799fa8b2254d09664b78ee80c43b434788d112';
|
||||||
|
const double engineValue1 = 101;
|
||||||
|
const double engineValue2 = 102;
|
||||||
|
|
||||||
|
final FlutterEngineMetricPoint enginePoint1 = FlutterEngineMetricPoint(
|
||||||
|
engineMetricName,
|
||||||
|
engineValue1,
|
||||||
|
engineRevision,
|
||||||
|
moreTags: const <String, String>{
|
||||||
|
kSubResultKey: 'cpu_time',
|
||||||
|
kUnitKey: 'ns',
|
||||||
|
'date': '2019-12-17 15:14:14',
|
||||||
|
'num_cpus': '56',
|
||||||
|
'mhz_per_cpu': '2594',
|
||||||
|
'cpu_scaling_enabled': 'true',
|
||||||
|
'library_build_type': 'release',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final FlutterEngineMetricPoint enginePoint2 = FlutterEngineMetricPoint(
|
||||||
|
engineMetricName,
|
||||||
|
engineValue2,
|
||||||
|
engineRevision,
|
||||||
|
moreTags: const <String, String>{
|
||||||
|
kSubResultKey: 'real_time',
|
||||||
|
kUnitKey: 'ns',
|
||||||
|
'date': '2019-12-17 15:14:14',
|
||||||
|
'num_cpus': '56',
|
||||||
|
'mhz_per_cpu': '2594',
|
||||||
|
'cpu_scaling_enabled': 'true',
|
||||||
|
'library_build_type': 'release',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('Throw if invalid points are converted to SkiaPoint', () {
|
||||||
|
final MetricPoint noGithubRepoPoint = MetricPoint(
|
||||||
|
kValue1,
|
||||||
|
const <String, String>{
|
||||||
|
kGitRevisionKey: kFrameworkRevision1,
|
||||||
|
kNameKey: kTaskName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final MetricPoint noGitRevisionPoint = MetricPoint(
|
||||||
|
kValue1,
|
||||||
|
const <String, String>{
|
||||||
|
kGithubRepoKey: kFlutterFrameworkRepo,
|
||||||
|
kNameKey: kTaskName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final MetricPoint noTestNamePoint = MetricPoint(
|
||||||
|
kValue1,
|
||||||
|
const <String, String>{
|
||||||
|
kGithubRepoKey: kFlutterFrameworkRepo,
|
||||||
|
kGitRevisionKey: kFrameworkRevision1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() => SkiaPerfPoint.fromPoint(noGithubRepoPoint), throwsA(anything));
|
||||||
|
expect(
|
||||||
|
() => SkiaPerfPoint.fromPoint(noGitRevisionPoint), throwsA(anything));
|
||||||
|
expect(() => SkiaPerfPoint.fromPoint(noTestNamePoint), throwsA(anything));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Correctly convert a metric point from cocoon to SkiaPoint', () {
|
||||||
|
final SkiaPerfPoint skiaPoint1 =
|
||||||
|
SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1);
|
||||||
|
expect(skiaPoint1, isNotNull);
|
||||||
|
expect(skiaPoint1.testName, equals(kTaskName));
|
||||||
|
expect(skiaPoint1.subResult, equals(kMetric1));
|
||||||
|
expect(skiaPoint1.value, equals(cocoonPointRev1Metric1.value));
|
||||||
|
expect(skiaPoint1.jsonUrl, isNull); // Not inserted yet
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Cocoon points correctly encode into Skia perf json format', () {
|
||||||
|
final SkiaPerfPoint p1 = SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1);
|
||||||
|
final SkiaPerfPoint p2 = SkiaPerfPoint.fromPoint(cocoonPointRev1Metric2);
|
||||||
|
final SkiaPerfPoint p3 =
|
||||||
|
SkiaPerfPoint.fromPoint(cocoonPointBetaRev1Metric1);
|
||||||
|
|
||||||
|
const JsonEncoder encoder = JsonEncoder.withIndent(' ');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
encoder
|
||||||
|
.convert(SkiaPerfPoint.toSkiaPerfJson(<SkiaPerfPoint>[p1, p2, p3])),
|
||||||
|
equals('''
|
||||||
|
{
|
||||||
|
"gitHash": "9011cece2595447eea5dd91adaa241c1c9ef9a33",
|
||||||
|
"results": {
|
||||||
|
"analyzer_benchmark": {
|
||||||
|
"default": {
|
||||||
|
"flutter_repo_batch_maximum": 1.0,
|
||||||
|
"options": {
|
||||||
|
"unit": "s"
|
||||||
|
},
|
||||||
|
"flutter_repo_watch_maximum": 2.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"beta/analyzer_benchmark": {
|
||||||
|
"default": {
|
||||||
|
"flutter_repo_batch_maximum": 1.0,
|
||||||
|
"options": {
|
||||||
|
"branch": "beta",
|
||||||
|
"unit": "s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'''));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Engine metric points correctly encode into Skia perf json format', () {
|
||||||
|
const JsonEncoder encoder = JsonEncoder.withIndent(' ');
|
||||||
|
expect(
|
||||||
|
encoder.convert(SkiaPerfPoint.toSkiaPerfJson(<SkiaPerfPoint>[
|
||||||
|
SkiaPerfPoint.fromPoint(enginePoint1),
|
||||||
|
SkiaPerfPoint.fromPoint(enginePoint2),
|
||||||
|
])),
|
||||||
|
equals(
|
||||||
|
'''
|
||||||
|
{
|
||||||
|
"gitHash": "ca799fa8b2254d09664b78ee80c43b434788d112",
|
||||||
|
"results": {
|
||||||
|
"BM_PaintRecordInit": {
|
||||||
|
"default": {
|
||||||
|
"cpu_time": 101.0,
|
||||||
|
"options": {
|
||||||
|
"cpu_scaling_enabled": "true",
|
||||||
|
"library_build_type": "release",
|
||||||
|
"mhz_per_cpu": "2594",
|
||||||
|
"num_cpus": "56",
|
||||||
|
"unit": "ns"
|
||||||
|
},
|
||||||
|
"real_time": 102.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}''',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Throw if engine points with the same test name but different options are converted to '
|
||||||
|
'Skia perf points', () {
|
||||||
|
final FlutterEngineMetricPoint enginePoint1 = FlutterEngineMetricPoint(
|
||||||
|
'BM_PaintRecordInit',
|
||||||
|
101,
|
||||||
|
'ca799fa8b2254d09664b78ee80c43b434788d112',
|
||||||
|
moreTags: const <String, String>{
|
||||||
|
kSubResultKey: 'cpu_time',
|
||||||
|
kUnitKey: 'ns',
|
||||||
|
'cpu_scaling_enabled': 'true',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final FlutterEngineMetricPoint enginePoint2 = FlutterEngineMetricPoint(
|
||||||
|
'BM_PaintRecordInit',
|
||||||
|
102,
|
||||||
|
'ca799fa8b2254d09664b78ee80c43b434788d112',
|
||||||
|
moreTags: const <String, String>{
|
||||||
|
kSubResultKey: 'real_time',
|
||||||
|
kUnitKey: 'ns',
|
||||||
|
'cpu_scaling_enabled': 'false',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const JsonEncoder encoder = JsonEncoder.withIndent(' ');
|
||||||
|
expect(
|
||||||
|
() => encoder.convert(SkiaPerfPoint.toSkiaPerfJson(<SkiaPerfPoint>[
|
||||||
|
SkiaPerfPoint.fromPoint(enginePoint1),
|
||||||
|
SkiaPerfPoint.fromPoint(enginePoint2),
|
||||||
|
])),
|
||||||
|
throwsA(anything),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Throw if two Cocoon metric points with the same name and subResult keys '
|
||||||
|
'but different options are converted to Skia perf points', () {
|
||||||
|
final SkiaPerfPoint p1 = SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1);
|
||||||
|
final SkiaPerfPoint p2 =
|
||||||
|
SkiaPerfPoint.fromPoint(cocoonPointBetaRev1Metric1BadBranch);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => SkiaPerfPoint.toSkiaPerfJson(<SkiaPerfPoint>[p1, p2]),
|
||||||
|
throwsA(anything),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SkiaPerfGcsAdaptor computes name correctly', () async {
|
||||||
|
final MockGithubHelper mockHelper = MockGithubHelper();
|
||||||
|
when(mockHelper.getCommitDateTime(
|
||||||
|
kFlutterFrameworkRepo, kFrameworkRevision1))
|
||||||
|
.thenAnswer((_) => Future<DateTime>.value(DateTime(2019, 12, 4, 23)));
|
||||||
|
expect(
|
||||||
|
await SkiaPerfGcsAdaptor.computeObjectName(
|
||||||
|
kFlutterFrameworkRepo,
|
||||||
|
kFrameworkRevision1,
|
||||||
|
githubHelper: mockHelper,
|
||||||
|
),
|
||||||
|
equals('flutter-flutter/2019/12/04/23/$kFrameworkRevision1/values.json'),
|
||||||
|
);
|
||||||
|
when(mockHelper.getCommitDateTime(kFlutterEngineRepo, kEngineRevision1))
|
||||||
|
.thenAnswer((_) => Future<DateTime>.value(DateTime(2019, 12, 3, 20)));
|
||||||
|
expect(
|
||||||
|
await SkiaPerfGcsAdaptor.computeObjectName(
|
||||||
|
kFlutterEngineRepo,
|
||||||
|
kEngineRevision1,
|
||||||
|
githubHelper: mockHelper,
|
||||||
|
),
|
||||||
|
equals('flutter-engine/2019/12/03/20/$kEngineRevision1/values.json'),
|
||||||
|
);
|
||||||
|
when(mockHelper.getCommitDateTime(kFlutterEngineRepo, kEngineRevision2))
|
||||||
|
.thenAnswer((_) => Future<DateTime>.value(DateTime(2020, 1, 3, 15)));
|
||||||
|
expect(
|
||||||
|
await SkiaPerfGcsAdaptor.computeObjectName(
|
||||||
|
kFlutterEngineRepo,
|
||||||
|
kEngineRevision2,
|
||||||
|
githubHelper: mockHelper,
|
||||||
|
),
|
||||||
|
equals('flutter-engine/2020/01/03/15/$kEngineRevision2/values.json'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Successfully read mock GCS that fails 1st time with 504', () async {
|
||||||
|
final MockBucket testBucket = MockBucket();
|
||||||
|
final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket);
|
||||||
|
|
||||||
|
final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
|
||||||
|
kFlutterFrameworkRepo, kFrameworkRevision1);
|
||||||
|
|
||||||
|
final List<SkiaPerfPoint> writePoints = <SkiaPerfPoint>[
|
||||||
|
SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1),
|
||||||
|
];
|
||||||
|
final String skiaPerfJson =
|
||||||
|
jsonEncode(SkiaPerfPoint.toSkiaPerfJson(writePoints));
|
||||||
|
await skiaPerfGcs.writePoints(testObjectName, writePoints);
|
||||||
|
verify(testBucket.writeBytes(testObjectName, utf8.encode(skiaPerfJson)));
|
||||||
|
|
||||||
|
// Emulate the first network request to fail with 504.
|
||||||
|
when(testBucket.info(testObjectName))
|
||||||
|
.thenThrow(DetailedApiRequestError(504, 'Test Failure'));
|
||||||
|
|
||||||
|
final MockObjectInfo mockObjectInfo = MockObjectInfo();
|
||||||
|
when(mockObjectInfo.downloadLink)
|
||||||
|
.thenReturn(Uri.https('test.com', 'mock.json'));
|
||||||
|
when(testBucket.info(testObjectName))
|
||||||
|
.thenAnswer((_) => Future<ObjectInfo>.value(mockObjectInfo));
|
||||||
|
when(testBucket.read(testObjectName))
|
||||||
|
.thenAnswer((_) => Stream<List<int>>.value(utf8.encode(skiaPerfJson)));
|
||||||
|
|
||||||
|
final List<SkiaPerfPoint> readPoints =
|
||||||
|
await skiaPerfGcs.readPoints(testObjectName);
|
||||||
|
expect(readPoints.length, equals(1));
|
||||||
|
expect(readPoints[0].testName, kTaskName);
|
||||||
|
expect(readPoints[0].subResult, kMetric1);
|
||||||
|
expect(readPoints[0].value, kValue1);
|
||||||
|
expect(readPoints[0].githubRepo, kFlutterFrameworkRepo);
|
||||||
|
expect(readPoints[0].gitHash, kFrameworkRevision1);
|
||||||
|
expect(readPoints[0].jsonUrl, 'https://test.com/mock.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Return empty list if the GCS file does not exist', () async {
|
||||||
|
final MockBucket testBucket = MockBucket();
|
||||||
|
final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket);
|
||||||
|
final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
|
||||||
|
kFlutterFrameworkRepo, kFrameworkRevision1);
|
||||||
|
when(testBucket.info(testObjectName))
|
||||||
|
.thenThrow(Exception('No such object'));
|
||||||
|
expect((await skiaPerfGcs.readPoints(testObjectName)).length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The following is for integration tests.
|
||||||
|
Bucket testBucket;
|
||||||
|
GcsLock testLock;
|
||||||
|
final Map<String, dynamic> credentialsJson = getTestGcpCredentialsJson();
|
||||||
|
if (credentialsJson != null) {
|
||||||
|
final ServiceAccountCredentials credentials =
|
||||||
|
ServiceAccountCredentials.fromJson(credentialsJson);
|
||||||
|
|
||||||
|
final AutoRefreshingAuthClient client =
|
||||||
|
await clientViaServiceAccount(credentials, Storage.SCOPES);
|
||||||
|
final Storage storage =
|
||||||
|
Storage(client, credentialsJson['project_id'] as String);
|
||||||
|
|
||||||
|
const String kTestBucketName = 'flutter-skia-perf-test';
|
||||||
|
|
||||||
|
assert(await storage.bucketExists(kTestBucketName));
|
||||||
|
testBucket = storage.bucket(kTestBucketName);
|
||||||
|
testLock = GcsLock(client, kTestBucketName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> skiaPerfGcsAdapterIntegrationTest() async {
|
||||||
|
final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket);
|
||||||
|
|
||||||
|
final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
|
||||||
|
kFlutterFrameworkRepo, kFrameworkRevision1);
|
||||||
|
|
||||||
|
await skiaPerfGcs.writePoints(testObjectName, <SkiaPerfPoint>[
|
||||||
|
SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1),
|
||||||
|
SkiaPerfPoint.fromPoint(cocoonPointRev1Metric2),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final List<SkiaPerfPoint> points =
|
||||||
|
await skiaPerfGcs.readPoints(testObjectName);
|
||||||
|
expect(points.length, equals(2));
|
||||||
|
expectSetMatch(
|
||||||
|
points.map((SkiaPerfPoint p) => p.testName), <String>[kTaskName]);
|
||||||
|
expectSetMatch(points.map((SkiaPerfPoint p) => p.subResult),
|
||||||
|
<String>[kMetric1, kMetric2]);
|
||||||
|
expectSetMatch(
|
||||||
|
points.map((SkiaPerfPoint p) => p.value), <double>[kValue1, kValue2]);
|
||||||
|
expectSetMatch(points.map((SkiaPerfPoint p) => p.githubRepo),
|
||||||
|
<String>[kFlutterFrameworkRepo]);
|
||||||
|
expectSetMatch(points.map((SkiaPerfPoint p) => p.gitHash),
|
||||||
|
<String>[kFrameworkRevision1]);
|
||||||
|
for (int i = 0; i < 2; i += 1) {
|
||||||
|
expect(points[0].jsonUrl, startsWith('https://'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> skiaPerfGcsIntegrationTestWithEnginePoints() async {
|
||||||
|
final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket);
|
||||||
|
|
||||||
|
final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
|
||||||
|
kFlutterEngineRepo, engineRevision);
|
||||||
|
|
||||||
|
await skiaPerfGcs.writePoints(testObjectName, <SkiaPerfPoint>[
|
||||||
|
SkiaPerfPoint.fromPoint(enginePoint1),
|
||||||
|
SkiaPerfPoint.fromPoint(enginePoint2),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final List<SkiaPerfPoint> points =
|
||||||
|
await skiaPerfGcs.readPoints(testObjectName);
|
||||||
|
expect(points.length, equals(2));
|
||||||
|
expectSetMatch(
|
||||||
|
points.map((SkiaPerfPoint p) => p.testName),
|
||||||
|
<String>[engineMetricName, engineMetricName],
|
||||||
|
);
|
||||||
|
expectSetMatch(
|
||||||
|
points.map((SkiaPerfPoint p) => p.value),
|
||||||
|
<double>[engineValue1, engineValue2],
|
||||||
|
);
|
||||||
|
expectSetMatch(
|
||||||
|
points.map((SkiaPerfPoint p) => p.githubRepo),
|
||||||
|
<String>[kFlutterEngineRepo],
|
||||||
|
);
|
||||||
|
expectSetMatch(
|
||||||
|
points.map((SkiaPerfPoint p) => p.gitHash), <String>[engineRevision]);
|
||||||
|
for (int i = 0; i < 2; i += 1) {
|
||||||
|
expect(points[0].jsonUrl, startsWith('https://'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To run the following integration tests, there must be a valid Google Cloud
|
||||||
|
// Project service account credentials in secret/test_gcp_credentials.json so
|
||||||
|
// `testBucket` won't be null. Currently, these integration tests are skipped
|
||||||
|
// in the CI, and only verified locally.
|
||||||
|
test(
|
||||||
|
'SkiaPerfGcsAdaptor passes integration test with Google Cloud Storage',
|
||||||
|
skiaPerfGcsAdapterIntegrationTest,
|
||||||
|
skip: testBucket == null,
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'SkiaPerfGcsAdaptor integration test with engine points',
|
||||||
|
skiaPerfGcsIntegrationTestWithEnginePoints,
|
||||||
|
skip: testBucket == null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// `SkiaPerfGcsAdaptor.computeObjectName` uses `GithubHelper` which requires
|
||||||
|
// network connections. Hence we put them as integration tests instead of unit
|
||||||
|
// tests.
|
||||||
|
test(
|
||||||
|
'SkiaPerfGcsAdaptor integration test for name computations',
|
||||||
|
() async {
|
||||||
|
expect(
|
||||||
|
await SkiaPerfGcsAdaptor.computeObjectName(
|
||||||
|
kFlutterFrameworkRepo, kFrameworkRevision1),
|
||||||
|
equals(
|
||||||
|
'flutter-flutter/2019/12/04/23/$kFrameworkRevision1/values.json'),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await SkiaPerfGcsAdaptor.computeObjectName(
|
||||||
|
kFlutterEngineRepo, kEngineRevision1),
|
||||||
|
equals('flutter-engine/2019/12/03/20/$kEngineRevision1/values.json'),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await SkiaPerfGcsAdaptor.computeObjectName(
|
||||||
|
kFlutterEngineRepo, kEngineRevision2),
|
||||||
|
equals('flutter-engine/2020/01/03/15/$kEngineRevision2/values.json'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
skip: testBucket == null,
|
||||||
|
);
|
||||||
|
|
||||||
|
test('SkiaPerfDestination correctly updates points', () async {
|
||||||
|
final SkiaPerfGcsAdaptor mockGcs = MockSkiaPerfGcsAdaptor();
|
||||||
|
final GcsLock mockLock = MockGcsLock();
|
||||||
|
final SkiaPerfDestination dst = SkiaPerfDestination(mockGcs, mockLock);
|
||||||
|
await dst.update(<MetricPoint>[cocoonPointRev1Metric1]);
|
||||||
|
await dst.update(<MetricPoint>[cocoonPointRev1Metric2]);
|
||||||
|
List<SkiaPerfPoint> points = await mockGcs.readPoints(
|
||||||
|
await SkiaPerfGcsAdaptor.computeObjectName(
|
||||||
|
kFlutterFrameworkRepo, kFrameworkRevision1));
|
||||||
|
expect(points.length, equals(2));
|
||||||
|
expectSetMatch(
|
||||||
|
points.map((SkiaPerfPoint p) => p.testName), <String>[kTaskName]);
|
||||||
|
expectSetMatch(points.map((SkiaPerfPoint p) => p.subResult),
|
||||||
|
<String>[kMetric1, kMetric2]);
|
||||||
|
expectSetMatch(
|
||||||
|
points.map((SkiaPerfPoint p) => p.value), <double>[kValue1, kValue2]);
|
||||||
|
|
||||||
|
final MetricPoint updated =
|
||||||
|
MetricPoint(kValue3, cocoonPointRev1Metric1.tags);
|
||||||
|
|
||||||
|
await dst.update(<MetricPoint>[updated, cocoonPointRev2Metric1]);
|
||||||
|
|
||||||
|
points = await mockGcs.readPoints(
|
||||||
|
await SkiaPerfGcsAdaptor.computeObjectName(
|
||||||
|
kFlutterFrameworkRepo, kFrameworkRevision2));
|
||||||
|
expect(points.length, equals(1));
|
||||||
|
expect(points[0].gitHash, equals(kFrameworkRevision2));
|
||||||
|
expect(points[0].value, equals(kValue3));
|
||||||
|
|
||||||
|
points = await mockGcs.readPoints(
|
||||||
|
await SkiaPerfGcsAdaptor.computeObjectName(
|
||||||
|
kFlutterFrameworkRepo, kFrameworkRevision1));
|
||||||
|
expectSetMatch(
|
||||||
|
points.map((SkiaPerfPoint p) => p.value), <double>[kValue2, kValue3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> skiaPerfDestinationIntegrationTest() async {
|
||||||
|
final SkiaPerfDestination destination =
|
||||||
|
SkiaPerfDestination(SkiaPerfGcsAdaptor(testBucket), testLock);
|
||||||
|
await destination.update(<MetricPoint>[cocoonPointRev1Metric1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test(
|
||||||
|
'SkiaPerfDestination integration test',
|
||||||
|
skiaPerfDestinationIntegrationTest,
|
||||||
|
skip: testBucket == null,
|
||||||
|
);
|
||||||
|
}
|
23
packages/metrics_center/test/utility.dart
Normal file
23
packages/metrics_center/test/utility.dart
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// 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 'common.dart';
|
||||||
|
|
||||||
|
// This will be used in many of our unit tests.
|
||||||
|
void expectSetMatch<T>(Iterable<T> actual, Iterable<T> expected) {
|
||||||
|
expect(Set<T>.from(actual), equals(Set<T>.from(expected)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// May return null if the credentials file doesn't exist.
|
||||||
|
Map<String, dynamic> getTestGcpCredentialsJson() {
|
||||||
|
final File f = File('secret/test_gcp_credentials.json');
|
||||||
|
if (!f.existsSync()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return jsonDecode(File('secret/test_gcp_credentials.json').readAsStringSync())
|
||||||
|
as Map<String, dynamic>;
|
||||||
|
}
|
Reference in New Issue
Block a user