diff --git a/dev/benchmarks/complex_layout/android/settings.gradle b/dev/benchmarks/complex_layout/android/settings.gradle index 663db3adba..d3b6a4013d 100644 --- a/dev/benchmarks/complex_layout/android/settings.gradle +++ b/dev/benchmarks/complex_layout/android/settings.gradle @@ -3,3 +3,13 @@ // found in the LICENSE file. include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/dev/benchmarks/complex_layout/ios/.gitignore b/dev/benchmarks/complex_layout/ios/.gitignore new file mode 100644 index 0000000000..e96ef602b8 --- /dev/null +++ b/dev/benchmarks/complex_layout/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/dev/benchmarks/complex_layout/ios/Podfile b/dev/benchmarks/complex_layout/ios/Podfile new file mode 100644 index 0000000000..1e8c3c90a5 --- /dev/null +++ b/dev/benchmarks/complex_layout/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/dev/benchmarks/complex_layout/ios/Runner/AppDelegate.swift b/dev/benchmarks/complex_layout/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000000..d815fed684 --- /dev/null +++ b/dev/benchmarks/complex_layout/ios/Runner/AppDelegate.swift @@ -0,0 +1,17 @@ +// Copyright 2014 The Flutter 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 UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000..dc9ada4725 Binary files /dev/null and b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000000..28c6bf0301 Binary files /dev/null and b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000..2ccbfd967d Binary files /dev/null and b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000..f091b6b0bc Binary files /dev/null and b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000000..0bedcf2fd4 --- /dev/null +++ b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000..9da19eacad Binary files /dev/null and b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000..9da19eacad Binary files /dev/null and b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000..9da19eacad Binary files /dev/null and b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000000..89c2725b70 --- /dev/null +++ b/dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/dev/benchmarks/complex_layout/ios/Runner/Runner-Bridging-Header.h b/dev/benchmarks/complex_layout/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000000..95b7baf386 --- /dev/null +++ b/dev/benchmarks/complex_layout/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1,5 @@ +// Copyright 2014 The Flutter 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 "GeneratedPluginRegistrant.h" diff --git a/dev/benchmarks/complex_layout/lib/main.dart b/dev/benchmarks/complex_layout/lib/main.dart index 4496bdd56a..69d6b133f7 100644 --- a/dev/benchmarks/complex_layout/lib/main.dart +++ b/dev/benchmarks/complex_layout/lib/main.dart @@ -109,6 +109,7 @@ class ComplexLayoutState extends State { Expanded( child: ListView.builder( key: const Key('complex-scroll'), // this key is used by the driver test + controller: ScrollController(), // So that the scroll offset can be tracked itemBuilder: (BuildContext context, int index) { if (index % 2 == 0) return FancyImageItem(index, key: PageStorageKey(index)); diff --git a/dev/benchmarks/complex_layout/pubspec.yaml b/dev/benchmarks/complex_layout/pubspec.yaml index e0616ea68c..ac23c61e1c 100644 --- a/dev/benchmarks/complex_layout/pubspec.yaml +++ b/dev/benchmarks/complex_layout/pubspec.yaml @@ -3,7 +3,7 @@ description: A benchmark of a relatively complex layout. environment: # The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite. - sdk: ">=2.0.0-dev.68.0 <3.0.0" + sdk: ">=2.2.2 <3.0.0" dependencies: flutter: @@ -46,6 +46,7 @@ dev_dependencies: flutter_test: sdk: flutter test: 1.16.0-nullsafety.4 + e2e: 0.7.0 _fe_analyzer_shared: 7.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" analyzer: 0.39.17 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -90,4 +91,4 @@ flutter: - packages/flutter_gallery_assets/people/square/ali.png - packages/flutter_gallery_assets/places/india_chettinad_silk_maker.png -# PUBSPEC CHECKSUM: 4f3b +# PUBSPEC CHECKSUM: fe86 diff --git a/dev/benchmarks/complex_layout/test/measure_scroll_smoothness.dart b/dev/benchmarks/complex_layout/test/measure_scroll_smoothness.dart new file mode 100644 index 0000000000..05c5107bfd --- /dev/null +++ b/dev/benchmarks/complex_layout/test/measure_scroll_smoothness.dart @@ -0,0 +1,296 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This test is a use case of flutter/flutter#60796 +// the test should be run as: +// flutter drive -t test/using_array.dart --driver test_driver/scrolling_test_e2e_test.dart + +import 'dart:ui' as ui; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:e2e/e2e.dart'; + +import 'package:complex_layout/main.dart' as app; + +class PointerDataTestBinding extends E2EWidgetsFlutterBinding { + // PointerData injection would usually be considered device input and therefore + // blocked by [TestWidgetsFlutterBinding]. Override this behavior + // to help events go into widget tree. + @override + void dispatchEvent( + PointerEvent event, + HitTestResult hitTestResult, { + TestBindingEventSource source = TestBindingEventSource.device, + }) { + super.dispatchEvent(event, hitTestResult, source: TestBindingEventSource.test); + } +} + +/// A union of [ui.PointerDataPacket] and the time it should be sent. +class PointerDataRecord { + PointerDataRecord(this.timeStamp, List data) + : data = ui.PointerDataPacket(data: data); + final ui.PointerDataPacket data; + final Duration timeStamp; +} + +/// Generates the [PointerDataRecord] to simulate a drag operation from +/// `center - totalMove/2` to `center + totalMove/2`. +Iterable dragInputDatas( + final Duration epoch, + final Offset center, { + final Offset totalMove = const Offset(0, -400), + final Duration totalTime = const Duration(milliseconds: 2000), + final double frequency = 90, +}) sync* { + final Offset startLocation = (center - totalMove / 2) * ui.window.devicePixelRatio; + // The issue is about 120Hz input on 90Hz refresh rate device. + // We test 90Hz input on 60Hz device here, which shows similar pattern. + final int moveEventCount = totalTime.inMicroseconds * frequency ~/ const Duration(seconds: 1).inMicroseconds; + final Offset movePerEvent = totalMove / moveEventCount.toDouble() * ui.window.devicePixelRatio; + yield PointerDataRecord(epoch, [ + ui.PointerData( + timeStamp: epoch, + change: ui.PointerChange.add, + physicalX: startLocation.dx, + physicalY: startLocation.dy, + ), + ui.PointerData( + timeStamp: epoch, + change: ui.PointerChange.down, + physicalX: startLocation.dx, + physicalY: startLocation.dy, + pointerIdentifier: 1, + ), + ]); + for (int t = 0; t < moveEventCount + 1; t++) { + final Offset position = startLocation + movePerEvent * t.toDouble(); + yield PointerDataRecord( + epoch + totalTime * t ~/ moveEventCount, + [ui.PointerData( + timeStamp: epoch + totalTime * t ~/ moveEventCount, + change: ui.PointerChange.move, + physicalX: position.dx, + physicalY: position.dy, + // Scrolling behavior depends on this delta rather + // than the position difference. + physicalDeltaX: movePerEvent.dx, + physicalDeltaY: movePerEvent.dy, + pointerIdentifier: 1, + )], + ); + } + final Offset position = startLocation + totalMove; + yield PointerDataRecord(epoch + totalTime, [ui.PointerData( + timeStamp: epoch + totalTime, + change: ui.PointerChange.up, + physicalX: position.dx, + physicalY: position.dy, + pointerIdentifier: 1, + )]); +} + +enum TestScenario { + resampleOn90Hz, + resampleOn59Hz, + resampleOff90Hz, + resampleOff59Hz, +} + +class ResampleFlagVariant extends TestVariant { + ResampleFlagVariant(this.binding); + final E2EWidgetsFlutterBinding binding; + + @override + final Set values = Set.from(TestScenario.values); + + TestScenario currentValue; + bool get resample { + switch(currentValue) { + case TestScenario.resampleOn90Hz: + case TestScenario.resampleOn59Hz: + return true; + case TestScenario.resampleOff90Hz: + case TestScenario.resampleOff59Hz: + return false; + } + throw ArgumentError; + } + double get frequency { + switch(currentValue) { + case TestScenario.resampleOn90Hz: + case TestScenario.resampleOff90Hz: + return 90.0; + case TestScenario.resampleOn59Hz: + case TestScenario.resampleOff59Hz: + return 59.0; + } + throw ArgumentError; + } + + Map result; + + @override + String describeValue(TestScenario value) { + switch(value) { + case TestScenario.resampleOn90Hz: + return 'resample on with 90Hz input'; + case TestScenario.resampleOn59Hz: + return 'resample on with 59Hz input'; + case TestScenario.resampleOff90Hz: + return 'resample off with 90Hz input'; + case TestScenario.resampleOff59Hz: + return 'resample off with 59Hz input'; + } + throw ArgumentError; + } + + @override + Future setUp(TestScenario value) async { + currentValue = value; + final bool original = binding.resamplingEnabled; + binding.resamplingEnabled = resample; + return original; + } + + @override + Future tearDown(TestScenario value, bool memento) async { + binding.resamplingEnabled = memento; + binding.reportData[describeValue(value)] = result; + } +} + +Future main() async { + final PointerDataTestBinding binding = PointerDataTestBinding(); + assert(WidgetsBinding.instance == binding); + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive; + binding.reportData ??= {}; + final ResampleFlagVariant variant = ResampleFlagVariant(binding); + testWidgets('Smoothness test', (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + final Finder scrollerFinder = find.byKey(const ValueKey('complex-scroll')); + final ListView scroller = tester.widget(scrollerFinder); + final ScrollController controller = scroller.controller; + final List frameTimestamp = []; + final List scrollOffset = []; + final List delays = []; + binding.addPersistentFrameCallback((Duration timeStamp) { + if (controller.hasClients) { + // This if is necessary because by the end of the test the widget tree + // is destroyed. + frameTimestamp.add(timeStamp.inMicroseconds); + scrollOffset.add(controller.offset); + } + }); + + Duration now() => binding.currentSystemFrameTimeStamp; + Future scroll() async { + // Extra 50ms to avoid timeouts. + final Duration startTime = const Duration(milliseconds: 500) + now(); + for (final PointerDataRecord record in dragInputDatas( + startTime, + tester.getCenter(scrollerFinder), + frequency: variant.frequency, + )) { + await tester.binding.delayed(record.timeStamp - now()); + // This now measures how accurate the above delayed is. + final Duration delay = now() - record.timeStamp; + if (delays.length < frameTimestamp.length) { + while (delays.length < frameTimestamp.length - 1) { + delays.add(Duration.zero); + } + delays.add(delay); + } else if (delays.last < delay) { + delays.last = delay; + } + ui.window.onPointerDataPacket(record.data); + } + } + + for (int n = 0; n < 5; n++) { + await scroll(); + } + variant.result = scrollSummary(scrollOffset, delays, frameTimestamp); + await tester.pumpAndSettle(); + scrollOffset.clear(); + delays.clear(); + await tester.idle(); + }, semanticsEnabled: false, variant: variant); +} + +/// Calculates the smoothness measure from `scrollOffset` and `delays` list. +/// +/// Smoothness (`abs_jerk`) is measured by the absolute value of the discrete +/// 2nd derivative of the scroll offset. +/// +/// It was experimented that jerk (3rd derivative of the position) is a good +/// measure the smoothness. +/// Here we are using 2nd derivative instead because the input is completely +/// linear and the expected acceleration should be strictly zero. +/// Observed acceleration is jumping from positive to negative within +/// adjacent frames, meaning mathematically the discrete 3-rd derivative +/// (`f[3] - 3*f[2] + 3*f[1] - f[0]`) is not a good approximation of jerk +/// (continuous 3-rd derivative), while discrete 2nd +/// derivative (`f[2] - 2*f[1] + f[0]`) on the other hand is a better measure +/// of how the scrolling deviate away from linear, and given the acceleration +/// should average to zero within two frames, it's also a good approximation +/// for jerk in terms of physics. +/// We use abs rather than square because square (2-norm) amplifies the +/// effect of the data point that's relatively large, but in this metric +/// we prefer smaller data point to have similar effect. +/// This is also why we count the number of data that's larger than a +/// threshold (and the result is tested not sensitive to this threshold), +/// which is effectively a 0-norm. +/// +/// Frames that are too slow to build (longer than 40ms) or with input delay +/// longer than 16ms (1/60Hz) is filtered out to separate the janky due to slow +/// response. +/// +/// The returned map has keys: +/// `average_abs_jerk`: average for the overall smoothness. +/// `janky_count`: number of frames with `abs_jerk` larger than 0.5. +/// `dropped_frame_count`: number of frames that are built longer than 40ms and +/// are not used for smoothness measurement. +/// `frame_timestamp`: the list of the timestamp for each frame, in the time +/// order. +/// `scroll_offset`: the scroll offset for each frame. Its length is the same as +/// `frame_timestamp`. +/// `input_delay`: the list of maximum delay time of the input simulation during +/// a frame. Its length is the same as `frame_timestamp` +Map scrollSummary( + List scrollOffset, + List delays, + List frameTimestamp, +) { + double jankyCount = 0; + double absJerkAvg = 0; + int lostFrame = 0; + for (int i = 1; i < scrollOffset.length-1; i += 1) { + if (frameTimestamp[i+1] - frameTimestamp[i-1] > 40E3 || + (i >= delays.length || delays[i] > const Duration(milliseconds: 16))) { + // filter data points from slow frame building or input simulation artifact + lostFrame += 1; + continue; + } + // + final double absJerk = (scrollOffset[i-1] + scrollOffset[i+1] - 2*scrollOffset[i]).abs(); + absJerkAvg += absJerk; + if (absJerk > 0.5) + jankyCount += 1; + } + // expect(lostFrame < 0.1 * frameTimestamp.length, true); + absJerkAvg /= frameTimestamp.length - lostFrame; + + return { + 'janky_count': jankyCount, + 'average_abs_jerk': absJerkAvg, + 'dropped_frame_count': lostFrame, + 'frame_timestamp': List.from(frameTimestamp), + 'scroll_offset': List.from(scrollOffset), + 'input_delay': delays.map((Duration data) => data.inMicroseconds).toList(), + }; +} diff --git a/dev/benchmarks/complex_layout/test_driver/measure_scroll_smoothness_test.dart b/dev/benchmarks/complex_layout/test_driver/measure_scroll_smoothness_test.dart new file mode 100644 index 0000000000..fd29ebe6a9 --- /dev/null +++ b/dev/benchmarks/complex_layout/test_driver/measure_scroll_smoothness_test.dart @@ -0,0 +1,17 @@ +// Copyright 2014 The Flutter 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 'package:e2e/e2e_driver.dart' as driver; + +Future main() => driver.e2eDriver( + timeout: const Duration(minutes: 5), + responseDataCallback: (Map data) async { + await driver.writeResponseData( + data, + testOutputFilename: 'scroll_smoothness_test', + ); + } +); diff --git a/dev/devicelab/bin/tasks/complex_layout_android__scroll_smoothness.dart b/dev/devicelab/bin/tasks/complex_layout_android__scroll_smoothness.dart new file mode 100644 index 0000000000..960965e2a7 --- /dev/null +++ b/dev/devicelab/bin/tasks/complex_layout_android__scroll_smoothness.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter 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 'package:flutter_devicelab/tasks/perf_tests.dart'; +import 'package:flutter_devicelab/framework/adb.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + await task(createsScrollSmoothnessPerfTest()); +} diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart index 33f819e5e7..cab549dda0 100644 --- a/dev/devicelab/lib/tasks/perf_tests.dart +++ b/dev/devicelab/lib/tasks/perf_tests.dart @@ -298,6 +298,54 @@ TaskFunction createsMultiWidgetConstructPerfE2ETest() { ).run; } +TaskFunction createsScrollSmoothnessPerfTest() { + final String testDirectory = + '${flutterDirectory.path}/dev/benchmarks/complex_layout'; + const String testTarget = 'test/measure_scroll_smoothness.dart'; + return () { + return inDirectory(testDirectory, () async { + final Device device = await devices.workingDevice; + await device.unlock(); + final String deviceId = device.deviceId; + await flutter('packages', options: ['get']); + + await flutter('drive', options: [ + '-v', + '--verbose-system-logs', + '--profile', + '-t', testTarget, + '-d', + deviceId, + ]); + final Map data = json.decode( + file('$testDirectory/build/scroll_smoothness_test.json').readAsStringSync(), + ) as Map; + + final Map result = {}; + void addResult(dynamic data, String suffix) { + assert(data is Map); + const List metricKeys = [ + 'janky_count', + 'average_abs_jerk', + 'dropped_frame_count', + ]; + for (final String key in metricKeys) { + result[key+suffix] = data[key]; + } + } + addResult(data['resample on with 90Hz input'], '_with_resampler_90Hz'); + addResult(data['resample on with 59Hz input'], '_with_resampler_59Hz'); + addResult(data['resample off with 90Hz input'], '_without_resampler_90Hz'); + addResult(data['resample off with 59Hz input'], '_without_resampler_59Hz'); + + return TaskResult.success( + result, + benchmarkScoreKeys: result.keys.toList(), + ); + }); + }; +} + TaskFunction createFramePolicyIntegrationTest() { final String testDirectory = '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks'; diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml index 89075b38b8..5d3a10ef17 100644 --- a/dev/devicelab/manifest.yaml +++ b/dev/devicelab/manifest.yaml @@ -114,6 +114,14 @@ tasks: # Android on-device tests + complex_layout_android__scroll_smoothness: + description: > + Measures the smoothness of scrolling of the Complex Layout sample app on + Android. + stage: devicelab + required_agent_capabilities: ["linux/android"] + flaky: true + complex_layout_scroll_perf__timeline_summary: description: > Measures the runtime performance of the Complex Layout sample app on diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index fe65b2de26..dad55ba775 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -1504,7 +1504,6 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { renderView._pointers[event.pointer].decay = _kPointerDecay; _handleViewNeedsPaint(); } else if (event.down) { - assert(event is PointerDownEvent); renderView._pointers[event.pointer] = _LiveTestPointerRecord( event.pointer, event.position,