Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/metrics_center/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 1.0.1

- `update` now requires taskName to scale metric writes

# 1.0.0

- Null safety support
Expand Down
3 changes: 2 additions & 1 deletion packages/metrics_center/lib/src/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ class MetricPoint extends Equatable {
/// Interface to write [MetricPoint].
abstract class MetricDestination {
Comment thread
keyonghan marked this conversation as resolved.
/// Insert new data points or modify old ones with matching id.
Future<void> update(List<MetricPoint> points, DateTime commitTime);
Future<void> update(
List<MetricPoint> points, DateTime commitTime, String taskName);
}

/// Create `AuthClient` in case we only have an access token without the full
Expand Down
5 changes: 3 additions & 2 deletions packages/metrics_center/lib/src/flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ class FlutterDestination extends MetricDestination {
}

@override
Future<void> update(List<MetricPoint> points, DateTime commitTime) async {
await _skiaPerfDestination.update(points, commitTime);
Future<void> update(
List<MetricPoint> points, DateTime commitTime, String taskName) async {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: metric name is more apt in this context

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task name is more preferred, as a task contains multiple different metrics and we want to separate values based on different tasks.

await _skiaPerfDestination.update(points, commitTime, taskName);
}

final SkiaPerfDestination _skiaPerfDestination;
Expand Down
30 changes: 15 additions & 15 deletions packages/metrics_center/lib/src/skiaperf.dart
Original file line number Diff line number Diff line change
Expand Up @@ -312,12 +312,12 @@ class SkiaPerfGcsAdaptor {
/// (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, DateTime commitTime) async {
/// of the json file can be arbitrary, and multiple json files can be put
/// in that leaf directory. We are using multiple json files divided by test
/// names to scale up the system to avoid too many writes competing for
/// the same json file.
static Future<String> computeObjectName(String githubRepo, String? revision,
DateTime commitTime, String taskName) async {
assert(_githubRepoToGcsName[githubRepo] != null);
final String? topComponent = _githubRepoToGcsName[githubRepo];
// [commitTime] is not guranteed to be UTC. Ensure it is so all results
Expand All @@ -327,7 +327,7 @@ class SkiaPerfGcsAdaptor {
final String day = commitUtcTime.day.toString().padLeft(2, '0');
final String hour = commitUtcTime.hour.toString().padLeft(2, '0');
final String dateComponents = '${commitUtcTime.year}/$month/$day/$hour';
return '$topComponent/$dateComponents/$revision/values.json';
return '$topComponent/$dateComponents/$revision/${taskName}_values.json';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does task name matter? Could we just generate a random id

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it would be more meaningful to include the task name so we can track different values for a specific task.

}

static final Map<String, String> _githubRepoToGcsName = <String, String>{
Expand Down Expand Up @@ -392,7 +392,8 @@ class SkiaPerfDestination extends MetricDestination {
}

@override
Future<void> update(List<MetricPoint> points, DateTime commitTime) async {
Future<void> update(
List<MetricPoint> points, DateTime commitTime, String taskName) 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.
Expand All @@ -411,14 +412,13 @@ class SkiaPerfDestination extends MetricDestination {
for (final String repo in pointMap.keys) {
for (final String? revision in pointMap[repo]!.keys) {
final String objectName = await SkiaPerfGcsAdaptor.computeObjectName(
repo, revision, commitTime);
repo, revision, commitTime, taskName);
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.
// Too many bots writing the metrics of a git revision into a single json

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm for this change, but it doesn't make sense why infra runs into this. Is the results directory being cached between runs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the results are cached anywhere. Different tasks/bots randomly access the file, read and write back. It is strange why the issue pops up after switching logic from cocoon to test runner, but it does sound like lock contention.

Hopefully this solves the problem.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the issue. The recipe implemented retry logic on the entire test runner. Thus,
-> recipe creates tmp results.json
-> test run 1: infra failure, writes results.json
-> test run 2: recipe retries, infra failure, results.json contention
-> test run 3: recipe retries last time, infra failure, results.json contention

If we're going to implement retries in the recipe, it needs to handle this cleanup. Or we can delete the file if it exists instead. However, it's intentional we fail as it indicates a dirty tree.

Preferably, we'd implement retries in the test runner and not the recipe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a gcs lock on values.json, rather than a local lock on the results json from the test runner.
The retries are already implemented in test runner: https://cs.opensource.google/flutter/flutter/+/master:dev/devicelab/lib/framework/runner.dart;l=47, so it should not cause local json file read/write contention since we delete it first if existing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah perfect! So the issue is we're uploading values.json directly to GCS? How does that get to skia perf?

@keyonghan keyonghan Aug 26, 2021 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are reading values.json and write it back on the fly, and that's why we have the gcs lock.

With this PR, we are dividing the single json file in gcs to be dependent on task name, so that each task has its own json file and there should not be any contention.

Skia perf reads data directly from GCS bucket.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to verify my understanding, the test runner pushes to GCS, and skia perf pulls from GCS? Then the contention is because we're uploading ~100 results to the same location, correct?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct.

// file will cause high contention on the lock. We use multiple
// json files according to 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);
Expand Down
2 changes: 1 addition & 1 deletion packages/metrics_center/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: metrics_center
version: 1.0.0
version: 1.0.1
description:
Support multiple performance metrics sources/formats and destinations.
repository: https://github.com/flutter/packages/tree/master/packages/metrics_center
Expand Down
2 changes: 1 addition & 1 deletion packages/metrics_center/test/flutter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ void main() {
await FlutterDestination.makeFromCredentialsJson(credentialsJson!,
isTesting: true);
dst.update(<FlutterEngineMetricPoint>[simplePoint],
DateTime.fromMillisecondsSinceEpoch(123));
DateTime.fromMillisecondsSinceEpoch(123), 'test');
}, skip: credentialsJson == null);
}
111 changes: 76 additions & 35 deletions packages/metrics_center/test/skiaperf_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -317,24 +317,28 @@ Future<void> main() async {
kFlutterFrameworkRepo,
kFrameworkRevision1,
DateTime.utc(2019, 12, 04, 23),
'test',
),
equals('flutter-flutter/2019/12/04/23/$kFrameworkRevision1/values.json'),
equals(
'flutter-flutter/2019/12/04/23/$kFrameworkRevision1/test_values.json'),
);
expect(
await SkiaPerfGcsAdaptor.computeObjectName(
kFlutterEngineRepo,
kEngineRevision1,
DateTime.utc(2019, 12, 03, 20),
'test',
),
equals('flutter-engine/2019/12/03/20/$kEngineRevision1/values.json'),
equals('flutter-engine/2019/12/03/20/$kEngineRevision1/test_values.json'),
);
expect(
await SkiaPerfGcsAdaptor.computeObjectName(
kFlutterEngineRepo,
kEngineRevision2,
DateTime.utc(2020, 01, 03, 15),
'test',
),
equals('flutter-engine/2020/01/03/15/$kEngineRevision2/values.json'),
equals('flutter-engine/2020/01/03/15/$kEngineRevision2/test_values.json'),
);
});

Expand All @@ -343,9 +347,11 @@ Future<void> main() async {
final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket);

final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
kFlutterFrameworkRepo,
kFrameworkRevision1,
DateTime.fromMillisecondsSinceEpoch(123));
kFlutterFrameworkRepo,
kFrameworkRevision1,
DateTime.fromMillisecondsSinceEpoch(123),
'test',
);

final List<SkiaPerfPoint> writePoints = <SkiaPerfPoint>[
SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1),
Expand Down Expand Up @@ -384,9 +390,11 @@ Future<void> main() async {
final MockBucket testBucket = MockBucket();
final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket);
final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
kFlutterFrameworkRepo,
kFrameworkRevision1,
DateTime.fromMillisecondsSinceEpoch(123));
kFlutterFrameworkRepo,
kFrameworkRevision1,
DateTime.fromMillisecondsSinceEpoch(123),
'test',
);
when(testBucket.info(testObjectName))
.thenThrow(Exception('No such object'));
expect((await skiaPerfGcs.readPoints(testObjectName)).length, 0);
Expand Down Expand Up @@ -416,9 +424,11 @@ Future<void> main() async {
final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket!);

final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
kFlutterFrameworkRepo,
kFrameworkRevision1,
DateTime.fromMillisecondsSinceEpoch(123));
kFlutterFrameworkRepo,
kFrameworkRevision1,
DateTime.fromMillisecondsSinceEpoch(123),
'test',
);

await skiaPerfGcs.writePoints(testObjectName, <SkiaPerfPoint>[
SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1),
Expand Down Expand Up @@ -447,9 +457,11 @@ Future<void> main() async {
final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket!);

final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
kFlutterEngineRepo,
engineRevision,
DateTime.fromMillisecondsSinceEpoch(123));
kFlutterEngineRepo,
engineRevision,
DateTime.fromMillisecondsSinceEpoch(123),
'test',
);

await skiaPerfGcs.writePoints(testObjectName, <SkiaPerfPoint>[
SkiaPerfPoint.fromPoint(enginePoint1),
Expand Down Expand Up @@ -505,25 +517,30 @@ Future<void> main() async {
kFlutterFrameworkRepo,
kFrameworkRevision1,
DateTime.utc(2019, 12, 04, 23),
'test',
),
equals(
'flutter-flutter/2019/12/04/23/$kFrameworkRevision1/values.json'),
'flutter-flutter/2019/12/04/23/$kFrameworkRevision1/test_values.json'),
);
expect(
await SkiaPerfGcsAdaptor.computeObjectName(
kFlutterEngineRepo,
kEngineRevision1,
DateTime.utc(2019, 12, 03, 20),
'test',
),
equals('flutter-engine/2019/12/03/20/$kEngineRevision1/values.json'),
equals(
'flutter-engine/2019/12/03/20/$kEngineRevision1/test_values.json'),
);
expect(
await SkiaPerfGcsAdaptor.computeObjectName(
kFlutterEngineRepo,
kEngineRevision2,
DateTime.utc(2020, 01, 03, 15),
'test',
),
equals('flutter-engine/2020/01/03/15/$kEngineRevision2/values.json'),
equals(
'flutter-engine/2020/01/03/15/$kEngineRevision2/test_values.json'),
);
},
skip: testBucket == null,
Expand All @@ -533,13 +550,23 @@ Future<void> main() async {
final SkiaPerfGcsAdaptor mockGcs = MockSkiaPerfGcsAdaptor();
final GcsLock mockLock = MockGcsLock();
final SkiaPerfDestination dst = SkiaPerfDestination(mockGcs, mockLock);
await dst.update(<MetricPoint>[cocoonPointRev1Metric1],
DateTime.fromMillisecondsSinceEpoch(123));
await dst.update(<MetricPoint>[cocoonPointRev1Metric2],
DateTime.fromMillisecondsSinceEpoch(123));
List<SkiaPerfPoint> points = await mockGcs.readPoints(
await SkiaPerfGcsAdaptor.computeObjectName(kFlutterFrameworkRepo,
kFrameworkRevision1, DateTime.fromMillisecondsSinceEpoch(123)));
await dst.update(
<MetricPoint>[cocoonPointRev1Metric1],
DateTime.fromMillisecondsSinceEpoch(123),
'test',
);
await dst.update(
<MetricPoint>[cocoonPointRev1Metric2],
DateTime.fromMillisecondsSinceEpoch(123),
'test',
);
List<SkiaPerfPoint> points =
await mockGcs.readPoints(await SkiaPerfGcsAdaptor.computeObjectName(
kFlutterFrameworkRepo,
kFrameworkRevision1,
DateTime.fromMillisecondsSinceEpoch(123),
'test',
));
expect(points.length, equals(2));
expectSetMatch(
points.map((SkiaPerfPoint p) => p.testName), <String>[kTaskName]);
Expand All @@ -551,28 +578,42 @@ Future<void> main() async {
final MetricPoint updated =
MetricPoint(kValue3, cocoonPointRev1Metric1.tags);

await dst.update(<MetricPoint>[updated, cocoonPointRev2Metric1],
DateTime.fromMillisecondsSinceEpoch(123));
await dst.update(
<MetricPoint>[updated, cocoonPointRev2Metric1],
DateTime.fromMillisecondsSinceEpoch(123),
'test',
);

points = await mockGcs.readPoints(
await SkiaPerfGcsAdaptor.computeObjectName(kFlutterFrameworkRepo,
kFrameworkRevision2, DateTime.fromMillisecondsSinceEpoch(123)));
points =
await mockGcs.readPoints(await SkiaPerfGcsAdaptor.computeObjectName(
kFlutterFrameworkRepo,
kFrameworkRevision2,
DateTime.fromMillisecondsSinceEpoch(123),
'test',
));
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, DateTime.fromMillisecondsSinceEpoch(123)));
points =
await mockGcs.readPoints(await SkiaPerfGcsAdaptor.computeObjectName(
kFlutterFrameworkRepo,
kFrameworkRevision1,
DateTime.fromMillisecondsSinceEpoch(123),
'test',
));
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],
DateTime.fromMillisecondsSinceEpoch(123));
await destination.update(
<MetricPoint>[cocoonPointRev1Metric1],
DateTime.fromMillisecondsSinceEpoch(123),
'test',
);
}

test(
Expand Down