Skip to content

Commit 35d0a9c

Browse files
athomasCommit Queue
authored andcommitted
[test_runner] Refactor BatchRunnerProcess
* Simplify runCommand using async/await. * Remove outdated memory leak hack for DDC. * Gather batch runner functionality in BatchRunnerProcess. * Fix process leak that likely caused a hang on test.py shutdown. * Add a test. Change-Id: I87584e4c951ad0fe1650c67db090910acf0f37e4 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/408700 Reviewed-by: Bob Nystrom <[email protected]> Commit-Queue: Alexander Thomas <[email protected]>
1 parent b0a4e7f commit 35d0a9c

File tree

2 files changed

+184
-123
lines changed

2 files changed

+184
-123
lines changed

pkg/test_runner/lib/src/process_queue.dart

Lines changed: 84 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import 'dart:convert';
1010
import 'dart:io' as io;
1111
import 'dart:math' as math;
1212

13+
import 'package:collection/collection.dart';
14+
1315
import 'android.dart';
1416
import 'browser_controller.dart';
1517
import 'command.dart';
@@ -498,10 +500,6 @@ class CommandExecutorImpl implements CommandExecutor {
498500
final int maxBrowserProcesses;
499501
AdbDevicePool? adbDevicePool;
500502

501-
/// For dart2js and analyzer batch processing,
502-
/// we keep a list of batch processes.
503-
final _batchProcesses = <String, List<BatchRunnerProcess>>{};
504-
505503
/// We keep a BrowserTestRunner for every configuration.
506504
final _browserTestRunners = <TestConfiguration, Future<BrowserTestRunner>>{};
507505

@@ -516,22 +514,14 @@ class CommandExecutorImpl implements CommandExecutor {
516514
assert(!_finishing);
517515
_finishing = true;
518516

519-
Future terminateBatchRunners() {
520-
var futures = <Future>[];
521-
for (var runners in _batchProcesses.values) {
522-
futures.addAll(runners.map((runner) => runner.terminate()));
523-
}
524-
return Future.wait(futures);
525-
}
526-
527517
Future terminateBrowserRunners() async {
528518
var futures = _browserTestRunners.values
529519
.map((runner) async => (await runner).terminate());
530520
return Future.wait(futures);
531521
}
532522

533523
return Future.wait([
534-
terminateBatchRunners(),
524+
BatchRunnerProcess.terminateAll(),
535525
terminateBrowserRunners(),
536526
]);
537527
}
@@ -562,18 +552,15 @@ class CommandExecutorImpl implements CommandExecutor {
562552
// For now, we always run vm_compile_to_kernel in batch mode.
563553
var name = command.displayName;
564554
assert(name == 'vm_compile_to_kernel');
565-
return _getBatchRunner(name)
566-
.runCommand(name, command, timeout, command.arguments);
555+
return _getBatchRunner(name).runCommand(command, timeout);
567556
} else if (command is AnalysisCommand && globalConfiguration.batch) {
568-
return _getBatchRunner(command.displayName)
569-
.runCommand(command.displayName, command, timeout, command.arguments);
557+
return _getBatchRunner(command.displayName).runCommand(command, timeout);
570558
} else if (command is CompilationCommand &&
571559
(command.displayName == 'dart2js' ||
572560
command.displayName == 'ddc' ||
573561
command.displayName == 'fasta') &&
574562
globalConfiguration.batch) {
575-
return _getBatchRunner(command.displayName)
576-
.runCommand(command.displayName, command, timeout, command.arguments);
563+
return _getBatchRunner(command.displayName).runCommand(command, timeout);
577564
} else if (command is ScriptCommand) {
578565
return command.run();
579566
} else if (command is AdbPrecompilationCommand ||
@@ -776,18 +763,8 @@ class CommandExecutorImpl implements CommandExecutor {
776763
utf8.encode('$writer'), [], stopwatch.elapsed, false);
777764
}
778765

779-
BatchRunnerProcess _getBatchRunner(String identifier) {
780-
// Start batch processes if needed.
781-
var runners = _batchProcesses.putIfAbsent(
782-
identifier,
783-
() => List<BatchRunnerProcess>.generate(maxProcesses,
784-
(_) => BatchRunnerProcess(useJson: identifier == "fasta")));
785-
786-
for (var runner in runners) {
787-
if (!runner._currentlyRunning) return runner;
788-
}
789-
throw Exception('Unable to find inactive batch runner.');
790-
}
766+
BatchRunnerProcess _getBatchRunner(String identifier) =>
767+
BatchRunnerProcess.byIdentifier(identifier, maxProcesses);
791768

792769
Future<CommandOutput> _startBrowserControllerTest(
793770
BrowserTestCommand browserCommand, int timeout) async {
@@ -966,14 +943,16 @@ class TestCaseCompleter {
966943
}
967944

968945
class BatchRunnerProcess {
946+
/// For dart2js and analyzer batch processing,
947+
/// we keep a list of batch processes.
948+
static final _batchProcesses = <String, List<BatchRunnerProcess>>{};
949+
969950
/// When true, the command line is passed to the test runner as a
970951
/// JSON-encoded list of strings.
971952
final bool _useJson;
972953

973-
Completer<CommandOutput>? _completer;
974-
late ProcessCommand _command;
954+
ProcessCommand? _command;
975955
late List<String> _arguments;
976-
String? _runnerType;
977956

978957
bool _processJustStarted = false;
979958
io.Process? _process;
@@ -982,69 +961,68 @@ class BatchRunnerProcess {
982961
late Completer<void> _stderrCompleter;
983962
late StreamSubscription<String> _stdoutSubscription;
984963
late StreamSubscription<String> _stderrSubscription;
985-
late Function _processExitHandler;
964+
late Function(int) _processExitHandler;
986965

987-
bool _currentlyRunning = false;
988966
late OutputLog _testStdout;
989967
late OutputLog _testStderr;
990968
String? _status;
991969
late DateTime _startTime;
992970
Timer? _timer;
993-
int _testCount = 0;
994971

995-
static const int extraStartupTimeout = 60;
972+
static const int _extraStartupTimeout = 60;
973+
974+
static Future terminateAll() => Future.wait([
975+
for (var runners in _batchProcesses.values)
976+
for (var runner in runners) runner.terminate()
977+
]);
978+
979+
BatchRunnerProcess._(this._useJson);
980+
981+
factory BatchRunnerProcess.byIdentifier(String identifier, int maxProcesses) {
982+
// Start batch processes if needed.
983+
var runners = _batchProcesses.putIfAbsent(
984+
identifier,
985+
() => List<BatchRunnerProcess>.generate(
986+
maxProcesses, (_) => BatchRunnerProcess._(identifier == "fasta")));
987+
988+
for (var runner in runners) {
989+
if (!runner._currentlyRunning) return runner;
990+
}
991+
throw Exception('Unable to find inactive batch runner.');
992+
}
993+
994+
bool get _currentlyRunning => _command != null;
995+
996+
bool isCompatibleRunner(ProcessCommand command) => const MapEquality()
997+
.equals(_processEnvironmentOverrides, command.environmentOverrides);
996998

997-
BatchRunnerProcess({bool useJson = true}) : _useJson = useJson;
999+
bool get hasRunningProcess => _process != null;
9981000

999-
Future<CommandOutput> runCommand(String runnerType, ProcessCommand command,
1000-
int timeout, List<String> arguments) {
1001-
assert(_completer == null);
1001+
Future<CommandOutput> runCommand(ProcessCommand command, int timeout) async {
10021002
assert(!_currentlyRunning);
10031003

1004-
_completer = Completer();
1005-
var sameRunnerType = _runnerType == runnerType &&
1006-
_dictEquals(_processEnvironmentOverrides, command.environmentOverrides);
1007-
_runnerType = runnerType;
1008-
_currentlyRunning = true;
1004+
if (!isCompatibleRunner(command)) {
1005+
await terminate();
1006+
}
10091007
_command = command;
1010-
_arguments = arguments;
1008+
_arguments = command.arguments;
10111009
_processEnvironmentOverrides = command.environmentOverrides;
1012-
1013-
// TODO(jmesserly): this restarts `dartdevc --batch` to work around a
1014-
// memory leak, see https://github.com/dart-lang/sdk/issues/30314.
1015-
var clearMemoryLeak = command is CompilationCommand &&
1016-
command.displayName == 'dartdevc' &&
1017-
++_testCount % 100 == 0;
10181010
if (_process == null) {
1019-
// Start process if not yet started.
1020-
_startProcess(() {
1021-
doStartTest(command, timeout);
1022-
});
1023-
} else if (!sameRunnerType || clearMemoryLeak) {
1024-
// Restart this runner with the right executable for this test if needed.
1025-
_processExitHandler = (_) {
1026-
_startProcess(() {
1027-
doStartTest(command, timeout);
1028-
});
1029-
};
1030-
_process!.kill();
1031-
_stdoutSubscription.cancel();
1032-
_stderrSubscription.cancel();
1033-
} else {
1034-
doStartTest(command, timeout);
1011+
await _startProcess();
10351012
}
1036-
return _completer!.future;
1013+
return await _doStartTest(timeout);
10371014
}
10381015

1039-
Future<bool> terminate() {
1040-
if (_process == null) return Future.value(true);
1041-
var terminateCompleter = Completer<bool>();
1016+
Future<void> terminate() async {
1017+
if (_process == null) return;
1018+
var terminateCompleter = Completer<void>();
10421019
final sigkillTimer = Timer(const Duration(seconds: 5), () {
10431020
_process?.kill(io.ProcessSignal.sigkill);
10441021
});
10451022
_processExitHandler = (_) {
10461023
sigkillTimer.cancel();
1047-
terminateCompleter.complete(true);
1024+
_process = null;
1025+
terminateCompleter.complete();
10481026
};
10491027
_process!.kill();
10501028
_stdoutSubscription.cancel();
@@ -1053,27 +1031,30 @@ class BatchRunnerProcess {
10531031
return terminateCompleter.future;
10541032
}
10551033

1056-
void doStartTest(Command command, int timeout) {
1034+
Future<CommandOutput> _doStartTest(int timeout) async {
10571035
if (_processJustStarted) {
10581036
// We just started the process, add some extra timeout to account for
10591037
// the startup cost of the batch compiler.
10601038
_processJustStarted = false;
1061-
timeout += extraStartupTimeout;
1039+
timeout += _extraStartupTimeout;
10621040
}
10631041
_startTime = DateTime.now();
10641042
_testStdout = OutputLog();
10651043
_testStderr = OutputLog();
10661044
_status = null;
10671045
_stdoutCompleter = Completer();
10681046
_stderrCompleter = Completer();
1047+
_stdoutSubscription.resume();
1048+
_stderrSubscription.resume();
10691049
_timer = Timer(Duration(seconds: timeout), _timeoutHandler);
10701050

10711051
var line = _createArgumentsLine(_arguments, timeout);
10721052
_process!.stdin.write(line);
1073-
_stdoutSubscription.resume();
1074-
_stderrSubscription.resume();
1075-
Future.wait([_stdoutCompleter.future, _stderrCompleter.future])
1076-
.then((_) => _reportResult());
1053+
await (_stdoutCompleter.future, _stderrCompleter.future).wait;
1054+
if (_status == null) {
1055+
await _process!.exitCode;
1056+
}
1057+
return _reportResult();
10771058
}
10781059

10791060
String _createArgumentsLine(List<String> arguments, int timeout) {
@@ -1084,9 +1065,7 @@ class BatchRunnerProcess {
10841065
}
10851066
}
10861067

1087-
void _reportResult() {
1088-
if (!_currentlyRunning) return;
1089-
1068+
CommandOutput _reportResult() {
10901069
var outcome = _status!.split(" ")[2];
10911070
var exitCode = 0;
10921071
if (outcome == "CRASH") exitCode = unhandledCompilerExceptionExitCode;
@@ -1100,47 +1079,42 @@ class BatchRunnerProcess {
11001079
exitCode = truncatedFakeExitCode;
11011080
}
11021081

1103-
var output = _command.createOutput(
1082+
var output = _command!.createOutput(
11041083
exitCode,
11051084
outcome == "TIMEOUT",
11061085
_testStdout.bytes,
11071086
_testStderr.bytes,
11081087
DateTime.now().difference(_startTime),
11091088
false);
1110-
assert(_completer != null);
1111-
_completer!.complete(output);
1112-
_completer = null;
1113-
_currentlyRunning = false;
1089+
_command = null;
1090+
return output;
11141091
}
11151092

1116-
void Function(int) makeExitHandler(String status) {
1093+
void Function(int) _makeExitHandler(String status) {
11171094
return (int exitCode) {
11181095
if (_currentlyRunning) {
1119-
if (_timer != null) _timer!.cancel();
1096+
_timer?.cancel();
11201097
_status = status;
11211098
_stdoutSubscription.cancel();
11221099
_stderrSubscription.cancel();
1123-
_startProcess(_reportResult);
1124-
} else {
1125-
// No active test case running.
1126-
_process = null;
11271100
}
1101+
// No active test case running.
1102+
_process = null;
11281103
};
11291104
}
11301105

11311106
void _timeoutHandler() {
1132-
_processExitHandler = makeExitHandler(">>> TEST TIMEOUT");
1107+
_processExitHandler = _makeExitHandler(">>> TEST TIMEOUT");
11331108
_process!.kill();
11341109
}
11351110

1136-
void _startProcess(void Function() callback) async {
1137-
var executable = _command.executable;
1138-
var arguments = _command.batchArguments.toList();
1139-
arguments.add('--batch');
1140-
var environment = Map<String, String>.from(io.Platform.environment);
1141-
if (_processEnvironmentOverrides != null) {
1142-
environment.addAll(_processEnvironmentOverrides!);
1143-
}
1111+
Future<void> _startProcess() async {
1112+
var executable = _command!.executable;
1113+
var arguments = [..._command!.batchArguments, '--batch'];
1114+
var environment = {
1115+
...io.Platform.environment,
1116+
...?_processEnvironmentOverrides,
1117+
};
11441118
try {
11451119
_process = await io.Process.start(executable, arguments,
11461120
environment: environment);
@@ -1173,9 +1147,9 @@ class BatchRunnerProcess {
11731147
if (_status != null) {
11741148
_stdoutSubscription.pause();
11751149
_timer!.cancel();
1176-
_stdoutCompleter.complete(null);
1150+
_stdoutCompleter.complete();
11771151
}
1178-
});
1152+
}, onDone: () => _stdoutCompleter.complete());
11791153
_stdoutSubscription.pause();
11801154

11811155
var stderrStream = _process!.stderr
@@ -1184,35 +1158,22 @@ class BatchRunnerProcess {
11841158
_stderrSubscription = stderrStream.listen((String line) {
11851159
if (line.startsWith('>>> EOF STDERR')) {
11861160
_stderrSubscription.pause();
1187-
_stderrCompleter.complete(null);
1161+
_stderrCompleter.complete();
11881162
} else {
11891163
_testStderr.add(utf8.encode(line));
11901164
_testStderr.add("\n".codeUnits);
11911165
}
1192-
});
1166+
}, onDone: () => _stderrCompleter.complete());
11931167
_stderrSubscription.pause();
11941168

1195-
_processExitHandler = makeExitHandler(">>> TEST CRASH");
1196-
_process!.exitCode.then((exitCode) {
1197-
_processExitHandler(exitCode);
1198-
});
1199-
12001169
_process!.stdin.done.catchError((Object err) {
12011170
print('Error on batch runner input stream stdin');
12021171
print(' Previous test\'s status: $_status');
12031172
print(' Error: $err');
12041173
throw err;
12051174
});
1206-
callback();
1207-
}
12081175

1209-
bool _dictEquals(Map? a, Map? b) {
1210-
if (a == null) return b == null;
1211-
if (b == null) return false;
1212-
if (a.length != b.length) return false;
1213-
for (var key in a.keys) {
1214-
if (a[key] != b[key]) return false;
1215-
}
1216-
return true;
1176+
_processExitHandler = _makeExitHandler(">>> TEST CRASH");
1177+
_process!.exitCode.then((exitCode) => _processExitHandler(exitCode));
12171178
}
12181179
}

0 commit comments

Comments
 (0)