Future run()

in packages/web_benchmarks/lib/src/runner.dart [87:317]


  Future<BenchmarkResults> run() async {
    // Reduce logging level. Otherwise, package:webkit_inspection_protocol is way too spammy.
    Logger.root.level = Level.INFO;

    if (!_processManager.canRun('flutter')) {
      throw Exception(
          'flutter executable is not runnable. Make sure it\'s in the PATH.');
    }

    final io.ProcessResult buildResult = await _processManager.run(
      <String>[
        'flutter',
        'build',
        'web',
        '--dart-define=FLUTTER_WEB_ENABLE_PROFILING=true',
        if (useCanvasKit) '--dart-define=FLUTTER_WEB_USE_SKIA=true',
        '--profile',
        '-t',
        entryPoint,
      ],
      workingDirectory: benchmarkAppDirectory.path,
    );

    if (buildResult.exitCode != 0) {
      io.stderr.writeln(buildResult.stdout);
      io.stderr.writeln(buildResult.stderr);
      throw Exception('Failed to build the benchmark.');
    }

    final Completer<List<Map<String, dynamic>>> profileData =
        Completer<List<Map<String, dynamic>>>();
    final List<Map<String, dynamic>> collectedProfiles =
        <Map<String, dynamic>>[];
    List<String> benchmarks;
    Iterator<String> benchmarkIterator;

    // This future fixes a race condition between the web-page loading and
    // asking to run a benchmark, and us connecting to Chrome's DevTools port.
    // Sometime one wins. Other times, the other wins.
    Future<Chrome> whenChromeIsReady;
    Chrome chrome;
    io.HttpServer server;
    List<Map<String, dynamic>> latestPerformanceTrace;
    Cascade cascade = Cascade();

    // Serves the static files built for the app (html, js, images, fonts, etc)
    cascade = cascade.add(createStaticHandler(
      path.join(benchmarkAppDirectory.path, 'build', 'web'),
      defaultDocument: 'index.html',
    ));

    // Serves the benchmark server API used by the benchmark app to coordinate
    // the running of benchmarks.
    cascade = cascade.add((Request request) async {
      try {
        chrome ??= await whenChromeIsReady;
        if (request.requestedUri.path.endsWith('/profile-data')) {
          final Map<String, dynamic> profile =
              json.decode(await request.readAsString());
          final String benchmarkName = profile['name'];
          if (benchmarkName != benchmarkIterator.current) {
            profileData.completeError(Exception(
              'Browser returned benchmark results from a wrong benchmark.\n'
              'Requested to run bechmark ${benchmarkIterator.current}, but '
              'got results for $benchmarkName.',
            ));
            server.close();
          }

          // Trace data is null when the benchmark is not frame-based, such as RawRecorder.
          if (latestPerformanceTrace != null) {
            final BlinkTraceSummary traceSummary =
                BlinkTraceSummary.fromJson(latestPerformanceTrace);
            profile['totalUiFrame.average'] =
                traceSummary.averageTotalUIFrameTime.inMicroseconds;
            profile['scoreKeys'] ??=
                <dynamic>[]; // using dynamic for consistency with JSON
            profile['scoreKeys'].add('totalUiFrame.average');
            latestPerformanceTrace = null;
          }
          collectedProfiles.add(profile);
          return Response.ok('Profile received');
        } else if (request.requestedUri.path
            .endsWith('/start-performance-tracing')) {
          latestPerformanceTrace = null;
          await chrome.beginRecordingPerformance(
              request.requestedUri.queryParameters['label']);
          return Response.ok('Started performance tracing');
        } else if (request.requestedUri.path
            .endsWith('/stop-performance-tracing')) {
          latestPerformanceTrace = await chrome.endRecordingPerformance();
          return Response.ok('Stopped performance tracing');
        } else if (request.requestedUri.path.endsWith('/on-error')) {
          final Map<String, dynamic> errorDetails =
              json.decode(await request.readAsString());
          server.close();
          // Keep the stack trace as a string. It's thrown in the browser, not this Dart VM.
          final String errorMessage =
              'Caught browser-side error: ${errorDetails['error']}\n${errorDetails['stackTrace']}';
          if (!profileData.isCompleted) {
            profileData.completeError(errorMessage);
          } else {
            io.stderr.writeln(errorMessage);
          }
          return Response.ok('');
        } else if (request.requestedUri.path.endsWith('/next-benchmark')) {
          if (benchmarks == null) {
            benchmarks =
                (json.decode(await request.readAsString())).cast<String>();
            benchmarkIterator = benchmarks.iterator;
          }
          if (benchmarkIterator.moveNext()) {
            final String nextBenchmark = benchmarkIterator.current;
            print('Launching benchmark "$nextBenchmark"');
            return Response.ok(nextBenchmark);
          } else {
            profileData.complete(collectedProfiles);
            return Response.ok(kEndOfBenchmarks);
          }
        } else if (request.requestedUri.path.endsWith('/print-to-console')) {
          // A passthrough used by
          // `dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart`
          // to print information.
          final String message = await request.readAsString();
          print('[APP] $message');
          return Response.ok('Reported.');
        } else {
          return Response.notFound(
              'This request is not handled by the profile-data handler.');
        }
      } catch (error, stackTrace) {
        if (!profileData.isCompleted) {
          profileData.completeError(error, stackTrace);
        } else {
          io.stderr.writeln('Caught error: $error');
          io.stderr.writeln('$stackTrace');
        }
        return Response.internalServerError(body: '$error');
      }
    });

    // If all previous handlers returned HTTP 404, this is the last handler
    // that simply warns about the unrecognized path.
    cascade = cascade.add((Request request) {
      io.stderr.writeln('Unrecognized URL path: ${request.requestedUri.path}');
      return Response.notFound('Not found: ${request.requestedUri.path}');
    });

    server = await io.HttpServer.bind('localhost', benchmarkServerPort);
    try {
      shelf_io.serveRequests(server, cascade.handler);

      final String dartToolDirectory =
          path.join(benchmarkAppDirectory.path, '.dart_tool');
      final String userDataDir = io.Directory(dartToolDirectory)
          .createTempSync('chrome_user_data_')
          .path;

      final ChromeOptions options = ChromeOptions(
        url: 'http://localhost:$benchmarkServerPort/index.html',
        userDataDirectory: userDataDir,
        windowHeight: 1024,
        windowWidth: 1024,
        headless: headless,
        debugPort: chromeDebugPort,
      );

      print('Launching Chrome.');
      whenChromeIsReady = Chrome.launch(
        options,
        onError: (String error) {
          if (!profileData.isCompleted) {
            profileData.completeError(Exception(error));
          } else {
            io.stderr.writeln('Chrome error: $error');
          }
        },
        workingDirectory: benchmarkAppDirectory.path,
      );

      print('Waiting for the benchmark to report benchmark profile.');
      final List<Map<String, dynamic>> profiles = await profileData.future;

      print('Received profile data');
      final Map<String, List<BenchmarkScore>> results =
          <String, List<BenchmarkScore>>{};
      for (final Map<String, dynamic> profile in profiles) {
        final String benchmarkName = profile['name'];
        if (benchmarkName.isEmpty) {
          throw 'Benchmark name is empty';
        }

        final List<String> scoreKeys = List<String>.from(profile['scoreKeys']);
        if (scoreKeys == null || scoreKeys.isEmpty) {
          throw 'No score keys in benchmark "$benchmarkName"';
        }
        for (final String scoreKey in scoreKeys) {
          if (scoreKey == null || scoreKey.isEmpty) {
            throw 'Score key is empty in benchmark "$benchmarkName". '
                'Received [${scoreKeys.join(', ')}]';
          }
        }

        final List<BenchmarkScore> scores = <BenchmarkScore>[];
        for (final String key in profile.keys) {
          if (key == 'name' || key == 'scoreKeys') {
            continue;
          }
          scores.add(BenchmarkScore(
            metric: key,
            value: profile[key],
          ));
        }
        results[benchmarkName] = scores;
      }
      return BenchmarkResults(results);
    } finally {
      if (headless) {
        chrome?.stop();
      } else {
        // In non-headless mode wait for the developer to close Chrome
        // manually. Otherwise, they won't get a chance to debug anything.
        print(
          'Benchmark finished. Chrome running in windowed mode. Close '
          'Chrome manually to continue.',
        );
        await chrome?.whenExits;
      }
      server?.close();
    }
  }