Skip to content

Commit f95a84d

Browse files
liamappelbeCommit Queue
authored andcommitted
[test_runner] Improve timeout deflaking
The usual deflaking logic is to simply rerun the test 5 times, but if the failure was a timeout this can be very expensive. Instead, only rerun timeout failures twice, and use the last successful run time to set a tighter timeout. Repeat counts and timeouts are now per-test. The name, repeat count, and timeout make a `DeflakeInfo`. This object is constructed in compare_results.dart, and passed to test.py's `--tests` flag. The recipe was already passing the output of compare_results directly to that flag, so no change is needed to the recipies. Backwards compatibility: If you want the old behavior of compare_results.dart, use the `--name-only` flag. test.py only uses this new logic if it detects that the `--tests` flag is JSON (if it starts with a `{`). Change-Id: I4113b68c54bfb7fd9a5e8fc9dab7a265807f3e77 Bug: #55044 Fixes: #55044 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/435760 Commit-Queue: Liam Appelbe <[email protected]> Reviewed-by: Alexander Thomas <[email protected]>
1 parent e6e16b4 commit f95a84d

File tree

9 files changed

+99
-15
lines changed

9 files changed

+99
-15
lines changed

pkg/test_runner/bin/compare_results.dart

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
// readable mode that explains the results and how they changed.
99

1010
import 'dart:collection';
11+
import 'dart:convert';
1112
import 'dart:io';
1213

1314
import 'package:args/args.dart';
1415
import 'package:test_runner/bot_results.dart';
16+
import 'package:test_runner/src/deflake_info.dart';
1517

1618
class Event {
1719
final Result? before;
@@ -50,6 +52,18 @@ class Event {
5052
throw Exception("Unreachable");
5153
}
5254
}
55+
56+
String deflakeInfo(String name) {
57+
final isTimeout = after.outcome == 'Timeout';
58+
final lastTimeMs = before?.timeMs;
59+
return jsonEncode(DeflakeInfo(
60+
name: name,
61+
repeat: isTimeout ? 2 : 5,
62+
timeout: isTimeout && lastTimeMs != null
63+
? ((2 * lastTimeMs) / 1000).ceil()
64+
: -1,
65+
).toJson());
66+
}
5367
}
5468

5569
class Options {
@@ -70,6 +84,7 @@ class Options {
7084
Iterable<String> get statusFilter => ["passing", "flaky", "failing"]
7185
.where((option) => _options[option] as bool);
7286
bool get unchanged => _options["unchanged"] as bool;
87+
bool get nameOnly => _options["name-only"] as bool;
7388
bool get verbose => _options["verbose"] as bool;
7489
List<String> get rest => _options.rest;
7590
}
@@ -148,8 +163,10 @@ bool search(
148163
"${before?.matches} ${after.matches} "
149164
"${before?.flaked} ${after.flaked}";
150165
}
151-
} else {
166+
} else if (options.nameOnly) {
152167
output = name;
168+
} else {
169+
output = event.deflakeInfo(name);
153170
}
154171
final log = logs[event.after.key];
155172
final bar = '=' * (output.length + 2);
@@ -191,6 +208,8 @@ void main(List<String> args) async {
191208
abbr: 'u',
192209
negatable: false,
193210
help: "Show only tests with unchanged results.");
211+
parser.addFlag("name-only",
212+
help: "Only show the test names.", negatable: false);
194213
parser.addFlag("verbose",
195214
abbr: "v",
196215
help: "Show the old and new result for each test",

pkg/test_runner/lib/bot_results.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class Result {
3030
final bool flaked; // From optional flakiness_data argument to constructor.
3131
final bool? isFlaky; // From results.json after it is extended.
3232
final String? previousOutcome;
33+
final int? timeMs;
3334

3435
Result(
3536
this.configuration,
@@ -40,7 +41,8 @@ class Result {
4041
this.changed,
4142
this.commitHash,
4243
this.isFlaky,
43-
this.previousOutcome, {
44+
this.previousOutcome,
45+
this.timeMs, {
4446
this.flaked = false,
4547
});
4648

@@ -56,6 +58,7 @@ class Result {
5658
commitHash = map["commit_hash"] as String?,
5759
isFlaky = map["flaky"] as bool?,
5860
previousOutcome = map["previous_result"] as String?,
61+
timeMs = map["timeMs"] as int?,
5962
flaked = flakinessData != null &&
6063
(flakinessData["active"] ?? true) == true &&
6164
(flakinessData["outcomes"] as List).contains(map["result"]);

pkg/test_runner/lib/src/configuration.dart

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:smith/configuration.dart';
1111
import 'package:smith/smith.dart';
1212

1313
import 'compiler_configuration.dart';
14+
import 'deflake_info.dart';
1415
import 'feature.dart';
1516
import 'path.dart';
1617
import 'repository.dart';
@@ -31,7 +32,8 @@ class TestConfiguration {
3132
this.selectors = const {},
3233
this.build = false,
3334
this.testList = const [],
34-
this.repeat = 1,
35+
this.deflakeInfoMap = const {},
36+
int repeat = 1,
3537
this.batch = false,
3638
this.copyCoreDumps = false,
3739
this.rr = false,
@@ -74,7 +76,8 @@ class TestConfiguration {
7476
: packages = packages ??
7577
Repository.uri
7678
.resolve('.dart_tool/package_config.json')
77-
.toFilePath();
79+
.toFilePath(),
80+
_repeat = repeat;
7881

7982
final Map<String, RegExp?> selectors;
8083
final Progress progress;
@@ -136,11 +139,12 @@ class TestConfiguration {
136139
final String? dartPrecompiledPath;
137140
final String? genSnapshotPath;
138141
final List<String>? testList;
142+
final Map<String, DeflakeInfo> deflakeInfoMap;
139143

140144
final int taskCount;
141145
final int shardCount;
142146
final int shard;
143-
final int repeat;
147+
final int _repeat;
144148

145149
final int testServerPort;
146150
final int testServerCrossOriginPort;
@@ -221,15 +225,15 @@ class TestConfiguration {
221225
/// build/ReleaseX64
222226
String get buildDirectory => system.outputDirectory + configurationDirectory;
223227

224-
int? _timeout;
228+
int? _defaultTimeout;
225229

226230
// TODO(whesse): Put non-default timeouts explicitly in configs, not this.
227231
/// Calculates a default timeout based on the compiler and runtime used,
228232
/// and the mode, architecture, etc.
229-
int get timeout {
230-
if (_timeout == null) {
233+
int get defaultTimeout {
234+
if (_defaultTimeout == null) {
231235
if (configuration.timeout > 0) {
232-
_timeout = configuration.timeout;
236+
_defaultTimeout = configuration.timeout;
233237
} else {
234238
var isReload = hotReload || hotReloadRollback;
235239

@@ -241,13 +245,22 @@ class TestConfiguration {
241245
arch: architecture,
242246
system: system);
243247

244-
_timeout = 30 * compilerMultiplier * runtimeMultiplier;
248+
_defaultTimeout = 30 * compilerMultiplier * runtimeMultiplier;
245249
}
246250
}
247251

248-
return _timeout!;
252+
return _defaultTimeout!;
249253
}
250254

255+
/// Returns the timeout for the given test name.
256+
int timeout(String name) {
257+
final t = deflakeInfoMap[name]?.timeout ?? -1;
258+
return t >= 0 ? t : defaultTimeout;
259+
}
260+
261+
/// Returns the repeat count for the given test name.
262+
int repeat(String name) => deflakeInfoMap[name]?.repeat ?? _repeat;
263+
251264
List<String> get standardOptions {
252265
if (compiler != Compiler.dart2js) {
253266
return const ["--ignore-unrecognized-flags"];
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
/// During deflaking, each test can be run with a custom timeout and repetition
6+
/// count.
7+
class DeflakeInfo {
8+
String name;
9+
int repeat;
10+
int timeout;
11+
12+
DeflakeInfo(
13+
{required this.name, required this.repeat, required this.timeout});
14+
15+
Map<dynamic, dynamic> toJson() => {
16+
'name': name,
17+
'repeat': repeat,
18+
'timeout': timeout,
19+
};
20+
21+
static DeflakeInfo fromJson(Map<dynamic, dynamic> json) => DeflakeInfo(
22+
name: json['name'] as String,
23+
repeat: json['repeat'] as int? ?? 5,
24+
timeout: json['timeout'] as int? ?? -1);
25+
}

pkg/test_runner/lib/src/options.dart

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:args/args.dart';
1111
import 'package:path/path.dart' as path;
1212

1313
import 'configuration.dart';
14+
import 'deflake_info.dart';
1415
import 'path.dart';
1516
import 'repository.dart';
1617
import 'test_configurations.dart';
@@ -626,12 +627,17 @@ has been specified on the command line.''')
626627

627628
void addConfiguration(Configuration innerConfiguration,
628629
[String? namedConfiguration]) {
630+
631+
final (testList, deflakeInfoMap) = parseTestList(
632+
data["test-list-contents"] as List<String>?);
633+
629634
var configuration = TestConfiguration(
630635
configuration: innerConfiguration,
631636
progress: progress,
632637
selectors: _expandSelectors(data),
633638
build: data["build"] as bool,
634-
testList: data["test-list-contents"] as List<String>?,
639+
testList: testList,
640+
deflakeInfoMap: deflakeInfoMap ?? const {},
635641
repeat: int.parse(data["repeat"] as String),
636642
batch: !(data["no-batch"] as bool),
637643
copyCoreDumps: data["copy-coredumps"] as bool,
@@ -1009,3 +1015,16 @@ final Map<String, String> sanitizerEnvironmentVariables = (() {
10091015

10101016
return environment;
10111017
})();
1018+
1019+
(List<String>?, Map<String, DeflakeInfo>?) parseTestList(List<String>? raw) {
1020+
final isJson = raw != null && raw.isNotEmpty && raw[0].startsWith('{');
1021+
if (!isJson) return (raw, null);
1022+
final deflakes = <DeflakeInfo>[
1023+
for (var line in raw)
1024+
DeflakeInfo.fromJson(jsonDecode(line) as Map<dynamic, dynamic>),
1025+
];
1026+
return (
1027+
[ for (var i in deflakes) i.name ],
1028+
{ for (var i in deflakes) i.name: i },
1029+
);
1030+
}

pkg/test_runner/lib/src/process_queue.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ class TestCaseEnqueuer {
253253
/// test completing successfully, just on it completing.
254254
void _add(TestCase testCase) {
255255
Node<Command>? lastNode;
256-
for (var i = 0; i < testCase.configuration.repeat; ++i) {
256+
for (var i = 0; i < testCase.repeat; ++i) {
257257
if (i > 0) {
258258
testCase = testCase.indexedCopy(i);
259259
}

pkg/test_runner/lib/src/test_case.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ class TestCase {
133133
}
134134

135135
int get timeout {
136-
var result = configuration.timeout;
136+
var result = configuration.timeout(displayName);
137137
if (expectedOutcomes.contains(Expectation.slow)) {
138138
result *= _slowTimeoutMultiplier;
139139
} else if (expectedOutcomes.contains(Expectation.extraSlow)) {
@@ -142,6 +142,8 @@ class TestCase {
142142
return result;
143143
}
144144

145+
int get repeat => configuration.repeat(displayName);
146+
145147
String get configurationString {
146148
var compiler = configuration.compiler.name;
147149
var runtime = configuration.runtime.name;

pkg/test_runner/lib/test_runner.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,7 @@ Future<void> deflake(Directory outDirectory, List<String> configurations,
577577
// Find the list of tests to deflake.
578578
var deflakeListOutput = await runProcess(Platform.resolvedExecutable, [
579579
"tools/bots/compare_results.dart",
580+
"--name-only",
580581
"--changed",
581582
"--failing",
582583
"--passing",

pkg/test_runner/test/compare_results/compare_results_test.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,8 @@ Result _result(
197197
String commitHash = 'abcdabcd',
198198
bool flaked = false,
199199
bool isFlaky = false,
200-
String previousOutcome = 'Pass'}) {
200+
String previousOutcome = 'Pass',
201+
int timeMs = -1}) {
201202
return Result(
202203
configuration,
203204
name,
@@ -208,6 +209,7 @@ Result _result(
208209
commitHash,
209210
isFlaky,
210211
previousOutcome,
212+
timeMs,
211213
flaked: flaked,
212214
);
213215
}

0 commit comments

Comments
 (0)