diff --git a/dev/benchmarks/complex_layout/test_driver/semantics_perf.dart b/dev/benchmarks/complex_layout/test_driver/semantics_perf.dart new file mode 100644 index 0000000000..df4ca137b5 --- /dev/null +++ b/dev/benchmarks/complex_layout/test_driver/semantics_perf.dart @@ -0,0 +1,11 @@ +// 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_driver/driver_extension.dart'; +import 'package:complex_layout/main.dart' as app; + +void main() { + enableFlutterDriverExtension(); + app.main(); +} diff --git a/dev/benchmarks/complex_layout/test_driver/semantics_perf_test.dart b/dev/benchmarks/complex_layout/test_driver/semantics_perf_test.dart new file mode 100644 index 0000000000..28b1ed026c --- /dev/null +++ b/dev/benchmarks/complex_layout/test_driver/semantics_perf_test.dart @@ -0,0 +1,43 @@ +// 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:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + group('semantics performance test', () { + FlutterDriver driver; + + setUpAll(() async { + driver = await FlutterDriver.connect(printCommunication: true); + }); + + tearDownAll(() async { + if (driver != null) + driver.close(); + }); + + test('inital tree creation', () async { + // Let app become fully idle. + await new Future.delayed(const Duration(seconds: 1)); + + final Timeline timeline = await driver.traceAction(() async { + expect(await driver.setSemantics(true), isTrue); + }); + + final Iterable semanticsEvents = timeline.events.where((TimelineEvent event) => event.name == "Semantics"); + if (semanticsEvents.length != 1) + fail('Expected exactly one semantics event, got ${semanticsEvents.length}'); + final Duration semanticsTreeCreation = semanticsEvents.first.duration; + + final String json = JSON.encode({'initialSemanticsTreeCreation': semanticsTreeCreation.inMilliseconds}); + new File(p.join(testOutputsDirectory, 'complex_layout_semantics_perf.json')).writeAsStringSync(json); + }); + }); +} diff --git a/dev/devicelab/bin/tasks/complex_layout_semantics_perf.dart b/dev/devicelab/bin/tasks/complex_layout_semantics_perf.dart new file mode 100644 index 0000000000..028447a75d --- /dev/null +++ b/dev/devicelab/bin/tasks/complex_layout_semantics_perf.dart @@ -0,0 +1,38 @@ +// 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 'package:flutter_devicelab/framework/adb.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; +import 'package:path/path.dart' as p; + +void main() { + task(() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + + final Device device = await devices.workingDevice; + await device.unlock(); + final String deviceId = device.deviceId; + await flutter('packages', options: ['get']); + + final String complexLayoutPath = p.join(flutterDirectory.path, 'dev', 'benchmarks', 'complex_layout'); + + await inDirectory(complexLayoutPath, () async { + await flutter('drive', options: [ + '-v', + '--profile', + '--trace-startup', // Enables "endless" timeline event buffering. + '-t', + p.join(complexLayoutPath, 'test_driver', 'semantics_perf.dart'), + '-d', + deviceId, + ]); + }); + + final String dataPath = p.join(complexLayoutPath, 'build', 'complex_layout_semantics_perf.json'); + return new TaskResult.successFromFile(file(dataPath), benchmarkScoreKeys: [ + 'initialSemanticsTreeCreation', + ]); + }); +} diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml index 02ea757e7c..5a5b22096f 100644 --- a/dev/devicelab/manifest.yaml +++ b/dev/devicelab/manifest.yaml @@ -133,6 +133,13 @@ tasks: required_agent_capabilities: ["linux/android"] flaky: true + complex_layout_semantics_perf: + description: > + Measures duration of building the initial semantics tree. + stage: devicelab + required_agent_capabilities: ["linux/android"] + flaky: true + # iOS on-device tests channels_integration_test_ios: diff --git a/packages/flutter_driver/lib/src/driver.dart b/packages/flutter_driver/lib/src/driver.dart index 2b8addaeea..edd18518bf 100644 --- a/packages/flutter_driver/lib/src/driver.dart +++ b/packages/flutter_driver/lib/src/driver.dart @@ -21,6 +21,7 @@ import 'gesture.dart'; import 'health.dart'; import 'message.dart'; import 'render_tree.dart'; +import 'semantics.dart'; import 'timeline.dart'; /// Timeline stream identifier. @@ -383,6 +384,15 @@ class FlutterDriver { return GetTextResult.fromJson(await _sendCommand(new GetText(finder, timeout: timeout))).text; } + /// Turns semantics on or off in the Flutter app under test. + /// + /// Returns `true` when the call actually changed the state from on to off or + /// vice versa. + Future setSemantics(bool enabled, { Duration timeout: _kShortTimeout }) async { + final SetSemanticsResult result = SetSemanticsResult.fromJson(await _sendCommand(new SetSemantics(enabled, timeout: timeout))); + return result.changedState; + } + /// Take a screenshot. The image will be returned as a PNG. Future> screenshot({ Duration timeout }) async { timeout ??= _kLongTimeout; diff --git a/packages/flutter_driver/lib/src/extension.dart b/packages/flutter_driver/lib/src/extension.dart index 851c2cb3b4..cb5ff5fcc9 100644 --- a/packages/flutter_driver/lib/src/extension.dart +++ b/packages/flutter_driver/lib/src/extension.dart @@ -8,7 +8,7 @@ import 'package:meta/meta.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart' show RendererBinding; +import 'package:flutter/rendering.dart' show RendererBinding, SemanticsHandle; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -20,6 +20,7 @@ import 'gesture.dart'; import 'health.dart'; import 'message.dart'; import 'render_tree.dart'; +import 'semantics.dart'; const String _extensionMethodName = 'driver'; const String _extensionMethod = 'ext.flutter.$_extensionMethodName'; @@ -70,6 +71,7 @@ class FlutterDriverExtension { 'tap': _tap, 'get_text': _getText, 'set_frame_sync': _setFrameSync, + 'set_semantics': _setSemantics, 'scroll': _scroll, 'scrollIntoView': _scrollIntoView, 'waitFor': _waitFor, @@ -82,6 +84,7 @@ class FlutterDriverExtension { 'tap': (Map params) => new Tap.deserialize(params), 'get_text': (Map params) => new GetText.deserialize(params), 'set_frame_sync': (Map params) => new SetFrameSync.deserialize(params), + 'set_semantics': (Map params) => new SetSemantics.deserialize(params), 'scroll': (Map params) => new Scroll.deserialize(params), 'scrollIntoView': (Map params) => new ScrollIntoView.deserialize(params), 'waitFor': (Map params) => new WaitFor.deserialize(params), @@ -271,4 +274,27 @@ class FlutterDriverExtension { _frameSync = setFrameSyncCommand.enabled; return new SetFrameSyncResult(); } + + SemanticsHandle _semantics; + bool get _semanticsIsEnabled => RendererBinding.instance.pipelineOwner.semanticsOwner != null; + + Future _setSemantics(Command command) async { + final SetSemantics setSemanticsCommand = command; + final bool semanticsWasEnabled = _semanticsIsEnabled; + if (setSemanticsCommand.enabled && _semantics == null) { + _semantics = RendererBinding.instance.pipelineOwner.ensureSemantics(); + if (!semanticsWasEnabled) { + // wait for the first frame where semantics is enabled. + final Completer completer = new Completer(); + SchedulerBinding.instance.addPostFrameCallback((Duration d) { + completer.complete(); + }); + await completer.future; + } + } else if (!setSemanticsCommand.enabled && _semantics != null) { + _semantics.dispose(); + _semantics = null; + } + return new SetSemanticsResult(semanticsWasEnabled != _semanticsIsEnabled); + } } diff --git a/packages/flutter_driver/lib/src/message.dart b/packages/flutter_driver/lib/src/message.dart index ebc421a5cd..2444444102 100644 --- a/packages/flutter_driver/lib/src/message.dart +++ b/packages/flutter_driver/lib/src/message.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:meta/meta.dart'; + /// An object sent from the Flutter Driver to a Flutter application to instruct /// the application to perform a task. abstract class Command { @@ -20,6 +22,7 @@ abstract class Command { String get kind; /// Serializes this command to parameter name/value pairs. + @mustCallSuper Map serialize() => { 'command': kind, 'timeout': '${timeout.inMilliseconds}', diff --git a/packages/flutter_driver/lib/src/semantics.dart b/packages/flutter_driver/lib/src/semantics.dart new file mode 100644 index 0000000000..b9af7c15be --- /dev/null +++ b/packages/flutter_driver/lib/src/semantics.dart @@ -0,0 +1,43 @@ +// 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 'message.dart'; + +/// Enables or disables semantics. +class SetSemantics extends Command { + @override + final String kind = 'set_semantics'; + + /// Whether semantics should be enabled or disabled. + final bool enabled; + + SetSemantics(this.enabled, { Duration timeout }) : super(timeout: timeout); + + /// Deserializes this command from the value generated by [serialize]. + SetSemantics.deserialize(Map params) + : this.enabled = params['enabled'].toLowerCase() == 'true', + super.deserialize(params); + + @override + Map serialize() => super.serialize()..addAll({ + 'enabled': '$enabled', + }); +} + +/// The result of a [SetSemantics] command. +class SetSemanticsResult extends Result { + SetSemanticsResult(this.changedState); + + final bool changedState; + + /// Deserializes this result from JSON. + static SetSemanticsResult fromJson(Map json) { + return new SetSemanticsResult(json['changedState']); + } + + @override + Map toJson() => { + 'changedState': changedState, + }; +}