Revert the revert (#65602)
@ -3,3 +3,13 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
include ':app'
|
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"
|
||||||
|
32
dev/benchmarks/complex_layout/ios/.gitignore
vendored
Normal file
@ -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
|
41
dev/benchmarks/complex_layout/ios/Podfile
Normal file
@ -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
|
17
dev/benchmarks/complex_layout/ios/Runner/AppDelegate.swift
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 564 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.6 KiB |
23
dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
BIN
dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
After Width: | Height: | Size: 68 B |
BIN
dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
After Width: | Height: | Size: 68 B |
BIN
dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
After Width: | Height: | Size: 68 B |
5
dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@ -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.
|
@ -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"
|
@ -109,6 +109,7 @@ class ComplexLayoutState extends State<ComplexLayout> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
key: const Key('complex-scroll'), // this key is used by the driver test
|
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) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
if (index % 2 == 0)
|
if (index % 2 == 0)
|
||||||
return FancyImageItem(index, key: PageStorageKey<int>(index));
|
return FancyImageItem(index, key: PageStorageKey<int>(index));
|
||||||
|
@ -3,7 +3,7 @@ description: A benchmark of a relatively complex layout.
|
|||||||
|
|
||||||
environment:
|
environment:
|
||||||
# The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite.
|
# 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:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@ -46,6 +46,7 @@ dev_dependencies:
|
|||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
test: 1.16.0-nullsafety.4
|
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"
|
_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"
|
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/people/square/ali.png
|
||||||
- packages/flutter_gallery_assets/places/india_chettinad_silk_maker.png
|
- packages/flutter_gallery_assets/places/india_chettinad_silk_maker.png
|
||||||
|
|
||||||
# PUBSPEC CHECKSUM: 4f3b
|
# PUBSPEC CHECKSUM: fe86
|
||||||
|
@ -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<ui.PointerData> 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<PointerDataRecord> 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>[
|
||||||
|
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>[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>[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<TestScenario> {
|
||||||
|
ResampleFlagVariant(this.binding);
|
||||||
|
final E2EWidgetsFlutterBinding binding;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Set<TestScenario> values = Set<TestScenario>.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<String, dynamic> 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<bool> setUp(TestScenario value) async {
|
||||||
|
currentValue = value;
|
||||||
|
final bool original = binding.resamplingEnabled;
|
||||||
|
binding.resamplingEnabled = resample;
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> tearDown(TestScenario value, bool memento) async {
|
||||||
|
binding.resamplingEnabled = memento;
|
||||||
|
binding.reportData[describeValue(value)] = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
final PointerDataTestBinding binding = PointerDataTestBinding();
|
||||||
|
assert(WidgetsBinding.instance == binding);
|
||||||
|
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive;
|
||||||
|
binding.reportData ??= <String, dynamic>{};
|
||||||
|
final ResampleFlagVariant variant = ResampleFlagVariant(binding);
|
||||||
|
testWidgets('Smoothness test', (WidgetTester tester) async {
|
||||||
|
app.main();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
final Finder scrollerFinder = find.byKey(const ValueKey<String>('complex-scroll'));
|
||||||
|
final ListView scroller = tester.widget<ListView>(scrollerFinder);
|
||||||
|
final ScrollController controller = scroller.controller;
|
||||||
|
final List<int> frameTimestamp = <int>[];
|
||||||
|
final List<double> scrollOffset = <double>[];
|
||||||
|
final List<Duration> delays = <Duration>[];
|
||||||
|
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<void> 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<String, dynamic> scrollSummary(
|
||||||
|
List<double> scrollOffset,
|
||||||
|
List<Duration> delays,
|
||||||
|
List<int> 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 <String, dynamic>{
|
||||||
|
'janky_count': jankyCount,
|
||||||
|
'average_abs_jerk': absJerkAvg,
|
||||||
|
'dropped_frame_count': lostFrame,
|
||||||
|
'frame_timestamp': List<int>.from(frameTimestamp),
|
||||||
|
'scroll_offset': List<double>.from(scrollOffset),
|
||||||
|
'input_delay': delays.map<int>((Duration data) => data.inMicroseconds).toList(),
|
||||||
|
};
|
||||||
|
}
|
@ -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<void> main() => driver.e2eDriver(
|
||||||
|
timeout: const Duration(minutes: 5),
|
||||||
|
responseDataCallback: (Map<String, dynamic> data) async {
|
||||||
|
await driver.writeResponseData(
|
||||||
|
data,
|
||||||
|
testOutputFilename: 'scroll_smoothness_test',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -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<void> main() async {
|
||||||
|
deviceOperatingSystem = DeviceOperatingSystem.android;
|
||||||
|
await task(createsScrollSmoothnessPerfTest());
|
||||||
|
}
|
@ -298,6 +298,54 @@ TaskFunction createsMultiWidgetConstructPerfE2ETest() {
|
|||||||
).run;
|
).run;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TaskFunction createsScrollSmoothnessPerfTest() {
|
||||||
|
final String testDirectory =
|
||||||
|
'${flutterDirectory.path}/dev/benchmarks/complex_layout';
|
||||||
|
const String testTarget = 'test/measure_scroll_smoothness.dart';
|
||||||
|
return () {
|
||||||
|
return inDirectory<TaskResult>(testDirectory, () async {
|
||||||
|
final Device device = await devices.workingDevice;
|
||||||
|
await device.unlock();
|
||||||
|
final String deviceId = device.deviceId;
|
||||||
|
await flutter('packages', options: <String>['get']);
|
||||||
|
|
||||||
|
await flutter('drive', options: <String>[
|
||||||
|
'-v',
|
||||||
|
'--verbose-system-logs',
|
||||||
|
'--profile',
|
||||||
|
'-t', testTarget,
|
||||||
|
'-d',
|
||||||
|
deviceId,
|
||||||
|
]);
|
||||||
|
final Map<String, dynamic> data = json.decode(
|
||||||
|
file('$testDirectory/build/scroll_smoothness_test.json').readAsStringSync(),
|
||||||
|
) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
final Map<String, dynamic> result = <String, dynamic>{};
|
||||||
|
void addResult(dynamic data, String suffix) {
|
||||||
|
assert(data is Map<String, dynamic>);
|
||||||
|
const List<String> metricKeys = <String>[
|
||||||
|
'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() {
|
TaskFunction createFramePolicyIntegrationTest() {
|
||||||
final String testDirectory =
|
final String testDirectory =
|
||||||
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks';
|
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks';
|
||||||
|
@ -114,6 +114,14 @@ tasks:
|
|||||||
|
|
||||||
# Android on-device tests
|
# 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:
|
complex_layout_scroll_perf__timeline_summary:
|
||||||
description: >
|
description: >
|
||||||
Measures the runtime performance of the Complex Layout sample app on
|
Measures the runtime performance of the Complex Layout sample app on
|
||||||
|
@ -1504,7 +1504,6 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
|||||||
renderView._pointers[event.pointer].decay = _kPointerDecay;
|
renderView._pointers[event.pointer].decay = _kPointerDecay;
|
||||||
_handleViewNeedsPaint();
|
_handleViewNeedsPaint();
|
||||||
} else if (event.down) {
|
} else if (event.down) {
|
||||||
assert(event is PointerDownEvent);
|
|
||||||
renderView._pointers[event.pointer] = _LiveTestPointerRecord(
|
renderView._pointers[event.pointer] = _LiveTestPointerRecord(
|
||||||
event.pointer,
|
event.pointer,
|
||||||
event.position,
|
event.position,
|
||||||
|