framework.dart 9.18 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
// Copyright 2016 The Chromium 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 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'dart:isolate';

import 'package:logging/logging.dart';
12
import 'package:stack_trace/stack_trace.dart';
13

14
import 'running_processes.dart';
15 16 17 18 19
import 'utils.dart';

/// Maximum amount of time a single task is allowed to take to run.
///
/// If exceeded the task is considered to have failed.
20
const Duration _kDefaultTaskTimeout = Duration(minutes: 15);
21 22 23

/// Represents a unit of work performed in the CI environment that can
/// succeed, fail and be retried independently of others.
24
typedef TaskFunction = Future<TaskResult> Function();
25 26 27 28 29

bool _isTaskRegistered = false;

/// Registers a [task] to run, returns the result when it is complete.
///
30
/// The task does not run immediately but waits for the request via the
31 32 33 34 35 36
/// VM service protocol to run it.
///
/// It is ok for a [task] to perform many things. However, only one task can be
/// registered per Dart VM.
Future<TaskResult> task(TaskFunction task) {
  if (_isTaskRegistered)
37
    throw StateError('A task is already registered');
38 39 40

  _isTaskRegistered = true;

41
  // TODO(ianh): allow overriding logging.
42 43 44 45 46
  Logger.root.level = Level.ALL;
  Logger.root.onRecord.listen((LogRecord rec) {
    print('${rec.level.name}: ${rec.time}: ${rec.message}');
  });

47
  final _TaskRunner runner = _TaskRunner(task);
48 49 50 51 52 53 54 55
  runner.keepVmAliveUntilTaskRunRequested();
  return runner.whenDone;
}

class _TaskRunner {
  _TaskRunner(this.task) {
    registerExtension('ext.cocoonRunTask',
        (String method, Map<String, String> parameters) async {
56
      final Duration taskTimeout = parameters.containsKey('timeoutInMinutes')
57
        ? Duration(minutes: int.parse(parameters['timeoutInMinutes']))
58 59
        : _kDefaultTaskTimeout;
      final TaskResult result = await run(taskTimeout);
60
      return ServiceExtensionResponse.result(json.encode(result.toJson()));
61 62 63
    });
    registerExtension('ext.cocoonRunnerReady',
        (String method, Map<String, String> parameters) async {
64
      return ServiceExtensionResponse.result('"ready"');
65 66 67
    });
  }

68 69 70 71 72 73 74 75 76 77 78
  final TaskFunction task;

  // TODO(ianh): workaround for https://github.com/dart-lang/sdk/issues/23797
  RawReceivePort _keepAlivePort;
  Timer _startTaskTimeout;
  bool _taskStarted = false;

  final Completer<TaskResult> _completer = Completer<TaskResult>();

  static final Logger logger = Logger('TaskRunner');

79 80 81
  /// Signals that this task runner finished running the task.
  Future<TaskResult> get whenDone => _completer.future;

82
  Future<TaskResult> run(Duration taskTimeout) async {
83 84
    try {
      _taskStarted = true;
85
      print('Running task.');
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
      final String exe = Platform.isWindows ? '.exe' : '';
      section('Checking running Dart$exe processes');
      final Set<RunningProcessInfo> beforeRunningDartInstances = await getRunningProcesses(
        processName: 'dart$exe',
      ).toSet();
      beforeRunningDartInstances.forEach(print);

      TaskResult result = await _performTask().timeout(taskTimeout);

      section('Checking running Dart$exe processes after task...');
      final List<RunningProcessInfo> afterRunningDartInstances = await getRunningProcesses(
        processName: 'dart$exe',
      ).toList();
      for (final RunningProcessInfo info in afterRunningDartInstances) {
        if (!beforeRunningDartInstances.contains(info)) {
          print('$info was leaked by this test.');
          // TODO(dnfield): remove this special casing after https://github.com/flutter/flutter/issues/29141 is resolved.
          if (result is TaskResultCheckProcesses) {
            result = TaskResult.failure('This test leaked dart processes');
          }
          final bool killed = await killProcess(info.pid);
          if (!killed) {
            print('Failed to kill process ${info.pid}.');
          } else {
            print('Killed process id ${info.pid}.');
          }
        }
      }

115 116 117
      _completer.complete(result);
      return result;
    } on TimeoutException catch (_) {
118
      print('Task timed out in framework.dart after $taskTimeout.');
119
      return TaskResult.failure('Task timed out after $taskTimeout');
120
    } finally {
121
      print('Cleaning up after task...');
122
      await forceQuitRunningProcesses();
123 124 125 126 127 128 129 130
      _closeKeepAlivePort();
    }
  }

  /// Causes the Dart VM to stay alive until a request to run the task is
  /// received via the VM service protocol.
  void keepVmAliveUntilTaskRunRequested() {
    if (_taskStarted)
131
      throw StateError('Task already started.');
132 133 134

    // Merely creating this port object will cause the VM to stay alive and keep
    // the VM service server running until the port is disposed of.
135
    _keepAlivePort = RawReceivePort();
136 137

    // Timeout if nothing bothers to connect and ask us to run the task.
138
    const Duration taskStartTimeout = Duration(seconds: 60);
139
    _startTaskTimeout = Timer(taskStartTimeout, () {
140 141 142 143 144 145 146 147 148 149 150 151 152 153
      if (!_taskStarted) {
        logger.severe('Task did not start in $taskStartTimeout.');
        _closeKeepAlivePort();
        exitCode = 1;
      }
    });
  }

  /// Disables the keep-alive port, allowing the VM to exit.
  void _closeKeepAlivePort() {
    _startTaskTimeout?.cancel();
    _keepAlivePort?.close();
  }

154
  Future<TaskResult> _performTask() {
155
    final Completer<TaskResult> completer = Completer<TaskResult>();
156 157 158
    Chain.capture(() async {
      completer.complete(await task());
    }, onError: (dynamic taskError, Chain taskErrorStack) {
159
      final String message = 'Task failed: $taskError';
160 161 162 163 164 165 166 167 168
      stderr
        ..writeln(message)
        ..writeln('\nStack trace:')
        ..writeln(taskErrorStack.terse);
      // IMPORTANT: We're completing the future _successfully_ but with a value
      // that indicates a task failure. This is intentional. At this point we
      // are catching errors coming from arbitrary (and untrustworthy) task
      // code. Our goal is to convert the failure into a readable message.
      // Propagating it further is not useful.
169
      if (!completer.isCompleted)
170
        completer.complete(TaskResult.failure(message));
171 172
    });
    return completer.future;
173 174 175 176 177 178
  }
}

/// A result of running a single task.
class TaskResult {
  /// Constructs a successful result.
179
  TaskResult.success(this.data, {this.benchmarkScoreKeys = const <String>[]})
180 181
      : succeeded = true,
        message = 'success' {
182
    const JsonEncoder prettyJson = JsonEncoder.withIndent('  ');
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
    if (benchmarkScoreKeys != null) {
      for (String key in benchmarkScoreKeys) {
        if (!data.containsKey(key)) {
          throw 'Invalid Golem score key "$key". It does not exist in task '
              'result data ${prettyJson.convert(data)}';
        } else if (data[key] is! num) {
          throw 'Invalid Golem score for key "$key". It is expected to be a num '
              'but was ${data[key].runtimeType}: ${prettyJson.convert(data[key])}';
        }
      }
    }
  }

  /// Constructs a successful result using JSON data stored in a file.
  factory TaskResult.successFromFile(File file,
      {List<String> benchmarkScoreKeys}) {
199
    return TaskResult.success(json.decode(file.readAsStringSync()),
200 201 202 203 204
        benchmarkScoreKeys: benchmarkScoreKeys);
  }

  /// Constructs an unsuccessful result.
  TaskResult.failure(this.message)
205 206 207
      : succeeded = false,
        data = null,
        benchmarkScoreKeys = const <String>[];
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248

  /// Whether the task succeeded.
  final bool succeeded;

  /// Task-specific JSON data
  final Map<String, dynamic> data;

  /// Keys in [data] that store scores that will be submitted to Golem.
  ///
  /// Each key is also part of a benchmark's name tracked by Golem.
  /// A benchmark name is computed by combining [Task.name] with a key
  /// separated by a dot. For example, if a task's name is
  /// `"complex_layout__start_up"` and score key is
  /// `"engineEnterTimestampMicros"`, the score will be submitted to Golem under
  /// `"complex_layout__start_up.engineEnterTimestampMicros"`.
  ///
  /// This convention reduces the amount of configuration that needs to be done
  /// to submit benchmark scores to Golem.
  final List<String> benchmarkScoreKeys;

  /// Whether the task failed.
  bool get failed => !succeeded;

  /// Explains the result in a human-readable format.
  final String message;

  /// Serializes this task result to JSON format.
  ///
  /// The JSON format is as follows:
  ///
  ///     {
  ///       "success": true|false,
  ///       "data": arbitrary JSON data valid only for successful results,
  ///       "benchmarkScoreKeys": [
  ///         contains keys into "data" that represent benchmarks scores, which
  ///         can be uploaded, for example. to golem, valid only for successful
  ///         results
  ///       ],
  ///       "reason": failure reason string valid only for unsuccessful results
  ///     }
  Map<String, dynamic> toJson() {
249
    final Map<String, dynamic> json = <String, dynamic>{
250 251 252 253 254 255 256 257 258 259 260 261 262
      'success': succeeded,
    };

    if (succeeded) {
      json['data'] = data;
      json['benchmarkScoreKeys'] = benchmarkScoreKeys;
    } else {
      json['reason'] = message;
    }

    return json;
  }
}
263 264 265 266

class TaskResultCheckProcesses extends TaskResult {
  TaskResultCheckProcesses() : super.success(null);
}