From 4b4cabb761ec1cb7fb5f2e8ec6e850de29a03056 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Thu, 27 Jul 2017 15:34:53 -0700 Subject: [PATCH] fire service protocol extension events for frames (#10966) * fire service protocol extension events for frames * start time in micros * introduce a profile() function; only send frame events when in profile (or debug) modes * moved the profile() function to foundation/profile.dart * refactor to make the change more testable; test the change * fire service protocol events by listening to onFrameInfo * remove the frame event stream; add a devicelab test * remove a todo * final --- .../bin/tasks/service_extensions_test.dart | 88 +++++++++++++++++++ dev/devicelab/manifest.yaml | 7 ++ packages/flutter/lib/foundation.dart | 1 + .../flutter/lib/src/foundation/profile.dart | 18 ++++ .../flutter/lib/src/scheduler/binding.dart | 32 +++++-- .../flutter/test/foundation/profile_test.dart | 18 ++++ 6 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 dev/devicelab/bin/tasks/service_extensions_test.dart create mode 100644 packages/flutter/lib/src/foundation/profile.dart create mode 100644 packages/flutter/test/foundation/profile_test.dart diff --git a/dev/devicelab/bin/tasks/service_extensions_test.dart b/dev/devicelab/bin/tasks/service_extensions_test.dart new file mode 100644 index 0000000000..0763ea83d8 --- /dev/null +++ b/dev/devicelab/bin/tasks/service_extensions_test.dart @@ -0,0 +1,88 @@ +// Copyright (c) 2017 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:io'; + +import 'package:path/path.dart' as path; +import 'package:vm_service_client/vm_service_client.dart'; + +import 'package:flutter_devicelab/framework/adb.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; + +const int kObservatoryPort = 8888; + +void main() { + task(() async { + final Device device = await devices.workingDevice; + await device.unlock(); + final Directory appDir = dir(path.join(flutterDirectory.path, 'dev/integration_tests/ui')); + await inDirectory(appDir, () async { + final Completer ready = new Completer(); + bool ok; + print('run: starting...'); + final Process run = await startProcess( + path.join(flutterDirectory.path, 'bin', 'flutter'), + ['run', '--verbose', '--observatory-port=$kObservatoryPort', '-d', device.deviceId, 'lib/main.dart'], + ); + run.stdout + .transform(UTF8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + print('run:stdout: $line'); + if (line.contains(new RegExp(r'^\[\s+\] For a more detailed help message, press "h"\. To quit, press "q"\.'))) { + print('run: ready!'); + ready.complete(); + ok ??= true; + } + }); + run.stderr + .transform(UTF8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + stderr.writeln('run:stderr: $line'); + }); + run.exitCode.then((int exitCode) { ok = false; }); + await Future.any(>[ ready.future, run.exitCode ]); + if (!ok) + throw 'Failed to run test app.'; + + final VMServiceClient client = new VMServiceClient.connect('ws://localhost:$kObservatoryPort/ws'); + final VM vm = await client.getVM(); + final VMIsolateRef isolate = vm.isolates.first; + final Stream frameEvents = isolate.onExtensionEvent.where( + (VMExtensionEvent e) => e.kind == 'Flutter.Frame'); + + print('reassembling app...'); + final Future frameFuture = frameEvents.first; + await isolate.invokeExtension('ext.flutter.reassemble'); + + // ensure we get an event + final VMExtensionEvent event = await frameFuture; + print('${event.kind}: ${event.data}'); + + // validate the fields + // {number: 8, startTime: 0, elapsed: 1437} + expect(event.data['number'] is int); + expect(event.data['number'] >= 0); + expect(event.data['startTime'] is int); + expect(event.data['startTime'] >= 0); + expect(event.data['elapsed'] is int); + expect(event.data['elapsed'] >= 0); + + run.stdin.write('q'); + final int result = await run.exitCode; + if (result != 0) + throw 'Received unexpected exit code $result from run process.'; + }); + return new TaskResult.success(null); + }); +} + +void expect(bool value) { + if (!value) + throw 'failed assertion in service extensions test'; +} diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml index 2b46fe19ba..be41e45e09 100644 --- a/dev/devicelab/manifest.yaml +++ b/dev/devicelab/manifest.yaml @@ -121,6 +121,13 @@ tasks: required_agent_capabilities: ["has-android-device"] flaky: true + service_extensions_test: + description: > + Validates our service protocol extensions. + stage: devicelab + required_agent_capabilities: ["has-android-device"] + flaky: true + android_sample_catalog_generator: description: > Builds sample catalog markdown pages and Android screenshots diff --git a/packages/flutter/lib/foundation.dart b/packages/flutter/lib/foundation.dart index 738b375b6d..039afae035 100644 --- a/packages/flutter/lib/foundation.dart +++ b/packages/flutter/lib/foundation.dart @@ -40,6 +40,7 @@ export 'src/foundation/licenses.dart'; export 'src/foundation/observer_list.dart'; export 'src/foundation/platform.dart'; export 'src/foundation/print.dart'; +export 'src/foundation/profile.dart'; export 'src/foundation/serialization.dart'; export 'src/foundation/synchronous_future.dart'; export 'src/foundation/tree_diagnostics_mixin.dart'; diff --git a/packages/flutter/lib/src/foundation/profile.dart b/packages/flutter/lib/src/foundation/profile.dart new file mode 100644 index 0000000000..82718a9765 --- /dev/null +++ b/packages/flutter/lib/src/foundation/profile.dart @@ -0,0 +1,18 @@ +// Copyright 2017 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:ui'; + +/// Whether we've been built in release mode. +const bool _kReleaseMode = const bool.fromEnvironment("dart.vm.product"); + +/// When running in profile mode (or debug mode), invoke the given function. +/// +/// In release mode, the function is not invoked. +// TODO(devoncarew): Going forward, we'll want the call to profile() to be tree-shaken out. +void profile(VoidCallback function) { + if (_kReleaseMode) + return; + function(); +} diff --git a/packages/flutter/lib/src/scheduler/binding.dart b/packages/flutter/lib/src/scheduler/binding.dart index c8121995b2..77b611e110 100644 --- a/packages/flutter/lib/src/scheduler/binding.dart +++ b/packages/flutter/lib/src/scheduler/binding.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:developer'; +import 'dart:developer' as developer; import 'dart:ui' as ui show window; import 'dart:ui' show VoidCallback; @@ -550,7 +550,8 @@ abstract class SchedulerBinding extends BindingBase { } Duration _currentFrameTimeStamp; - int _debugFrameNumber = 0; + int _profileFrameNumber = 0; + final Stopwatch _profileFrameStopwatch = new Stopwatch(); String _debugBanner; /// Called by the engine to prepare the framework to produce a new frame. @@ -577,14 +578,19 @@ abstract class SchedulerBinding extends BindingBase { /// statements printed during a frame from those printed between frames (e.g. /// in response to events or timers). void handleBeginFrame(Duration rawTimeStamp) { - Timeline.startSync('Frame'); + developer.Timeline.startSync('Frame'); _firstRawTimeStampInEpoch ??= rawTimeStamp; _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp); if (rawTimeStamp != null) _lastRawTimeStamp = rawTimeStamp; + profile(() { + _profileFrameNumber += 1; + _profileFrameStopwatch.reset(); + _profileFrameStopwatch.start(); + }); + assert(() { - _debugFrameNumber += 1; if (debugPrintBeginFrameBanner || debugPrintEndFrameBanner) { final StringBuffer frameTimeStampDescription = new StringBuffer(); if (rawTimeStamp != null) { @@ -592,7 +598,7 @@ abstract class SchedulerBinding extends BindingBase { } else { frameTimeStampDescription.write('(warm-up frame)'); } - _debugBanner = '▄▄▄▄▄▄▄▄ Frame ${_debugFrameNumber.toString().padRight(7)} ${frameTimeStampDescription.toString().padLeft(18)} ▄▄▄▄▄▄▄▄'; + _debugBanner = '▄▄▄▄▄▄▄▄ Frame ${_profileFrameNumber.toString().padRight(7)} ${frameTimeStampDescription.toString().padLeft(18)} ▄▄▄▄▄▄▄▄'; if (debugPrintBeginFrameBanner) debugPrint(_debugBanner); } @@ -603,7 +609,7 @@ abstract class SchedulerBinding extends BindingBase { _hasScheduledFrame = false; try { // TRANSIENT FRAME CALLBACKS - Timeline.startSync('Animate'); + developer.Timeline.startSync('Animate'); _schedulerPhase = SchedulerPhase.transientCallbacks; final Map callbacks = _transientCallbacks; _transientCallbacks = {}; @@ -628,7 +634,7 @@ abstract class SchedulerBinding extends BindingBase { /// useful when working with frame callbacks. void handleDrawFrame() { assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks); - Timeline.finishSync(); // end the "Animate" phase + developer.Timeline.finishSync(); // end the "Animate" phase try { // PERSISTENT FRAME CALLBACKS _schedulerPhase = SchedulerPhase.persistentCallbacks; @@ -644,14 +650,22 @@ abstract class SchedulerBinding extends BindingBase { _invokeFrameCallback(callback, _currentFrameTimeStamp); } finally { _schedulerPhase = SchedulerPhase.idle; - _currentFrameTimeStamp = null; - Timeline.finishSync(); + developer.Timeline.finishSync(); // end the Frame + profile(() { + _profileFrameStopwatch.stop(); + developer.postEvent('Flutter.Frame', { + 'number': _profileFrameNumber, + 'startTime': _currentFrameTimeStamp.inMicroseconds, + 'elapsed': _profileFrameStopwatch.elapsedMicroseconds + }); + }); assert(() { if (debugPrintEndFrameBanner) debugPrint('▀' * _debugBanner.length); _debugBanner = null; return true; }); + _currentFrameTimeStamp = null; } // All frame-related callbacks have been executed. Run lower-priority tasks. diff --git a/packages/flutter/test/foundation/profile_test.dart b/packages/flutter/test/foundation/profile_test.dart new file mode 100644 index 0000000000..e8c6606142 --- /dev/null +++ b/packages/flutter/test/foundation/profile_test.dart @@ -0,0 +1,18 @@ +// Copyright 2017 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 'package:flutter/foundation.dart'; +import 'package:test/test.dart'; + +const bool isReleaseMode = const bool.fromEnvironment("dart.vm.product"); + +void main() { + test("profile invokes its closure in debug or profile mode", () { + int count = 0; + profile(() { + count++; + }); + expect(count, isReleaseMode ? 0 : 1); + }); +}