Skip to content

Commit ea3f38d

Browse files
Piinkscaseycrogers
authored andcommitted
Update VelocityTracker (4) (flutter#139166)
This updates the implementation to use the stopwatch from the Clock object and pipes it through to the TestWidgetsFlutterBinding so it will be kept in sync with FakeAsync. Relands flutter#138843 attempted to reland flutter#137381 which attempted to reland flutter#132291 Fixes flutter#97761 1. The original change was reverted due to flakiness it introduced in tests that use fling gestures. * Using a mocked clock through the test binding fixes this now 2. It was reverted a second time because a change at tip of tree broke it, exposing memory leaks, but it was not rebased before landing. * These leaks are now fixed 3. It was reverted a third time, because we were so excellently quick to revert those other times, that we did not notice the broken benchmark that only runs in postsubmit. * The benchmark is now fixed
1 parent 324421a commit ea3f38d

File tree

9 files changed

+178
-65
lines changed

9 files changed

+178
-65
lines changed

dev/benchmarks/microbenchmarks/lib/gestures/velocity_tracker_bench.dart

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'package:flutter/gestures.dart';
6+
import 'package:flutter_test/flutter_test.dart';
67

78
import '../common.dart';
89
import 'data/velocity_tracker_data.dart';
@@ -16,38 +17,43 @@ class TrackerBenchmark {
1617
final String name;
1718
}
1819

19-
void main() {
20+
Future<void> main() async {
2021
assert(false, "Don't run benchmarks in debug mode! Use 'flutter run --release'.");
2122
final BenchmarkResultPrinter printer = BenchmarkResultPrinter();
2223
final List<TrackerBenchmark> benchmarks = <TrackerBenchmark>[
23-
TrackerBenchmark(name: 'velocity_tracker_iteration', tracker: VelocityTracker.withKind(PointerDeviceKind.touch)),
24-
TrackerBenchmark(name: 'velocity_tracker_iteration_ios_fling', tracker: IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch)),
24+
TrackerBenchmark(name: 'velocity_tracker_iteration',
25+
tracker: VelocityTracker.withKind(PointerDeviceKind.touch)),
26+
TrackerBenchmark(name: 'velocity_tracker_iteration_ios_fling',
27+
tracker: IOSScrollViewFlingVelocityTracker(PointerDeviceKind.touch)),
2528
];
2629
final Stopwatch watch = Stopwatch();
2730

28-
for (final TrackerBenchmark benchmark in benchmarks) {
29-
print('${benchmark.name} benchmark...');
30-
final VelocityTracker tracker = benchmark.tracker;
31-
watch.reset();
32-
watch.start();
33-
for (int i = 0; i < _kNumIters; i += 1) {
34-
for (final PointerEvent event in velocityEventData) {
35-
if (event is PointerDownEvent || event is PointerMoveEvent) {
36-
tracker.addPosition(event.timeStamp, event.position);
37-
}
38-
if (event is PointerUpEvent) {
39-
tracker.getVelocity();
31+
await benchmarkWidgets((WidgetTester tester) async {
32+
for (final TrackerBenchmark benchmark in benchmarks) {
33+
print('${benchmark.name} benchmark...');
34+
final VelocityTracker tracker = benchmark.tracker;
35+
watch.reset();
36+
watch.start();
37+
for (int i = 0; i < _kNumIters; i += 1) {
38+
for (final PointerEvent event in velocityEventData) {
39+
if (event is PointerDownEvent || event is PointerMoveEvent) {
40+
tracker.addPosition(event.timeStamp, event.position);
41+
}
42+
if (event is PointerUpEvent) {
43+
tracker.getVelocity();
44+
}
4045
}
4146
}
47+
watch.stop();
48+
49+
printer.addResult(
50+
description: 'Velocity tracker: ${tracker.runtimeType}',
51+
value: watch.elapsedMicroseconds / _kNumIters,
52+
unit: 'µs per iteration',
53+
name: benchmark.name,
54+
);
4255
}
43-
watch.stop();
44-
printer.addResult(
45-
description: 'Velocity tracker: ${tracker.runtimeType}',
46-
value: watch.elapsedMicroseconds / _kNumIters,
47-
unit: 'µs per iteration',
48-
name: benchmark.name,
49-
);
50-
}
56+
});
5157

5258
printer.printToStdout();
5359
}

packages/flutter/lib/src/gestures/binding.dart

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
373373

374374
if (resamplingEnabled) {
375375
_resampler.addOrDispatch(event);
376-
_resampler.sample(samplingOffset, _samplingClock);
376+
_resampler.sample(samplingOffset, samplingClock);
377377
return;
378378
}
379379

@@ -512,24 +512,29 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
512512
_hitTests.clear();
513513
}
514514

515-
/// Overrides the sampling clock for debugging and testing.
516-
///
517-
/// This value is ignored in non-debug builds.
518-
@protected
519-
SamplingClock? get debugSamplingClock => null;
520-
521515
void _handleSampleTimeChanged() {
522516
if (!locked) {
523517
if (resamplingEnabled) {
524-
_resampler.sample(samplingOffset, _samplingClock);
518+
_resampler.sample(samplingOffset, samplingClock);
525519
}
526520
else {
527521
_resampler.stop();
528522
}
529523
}
530524
}
531525

532-
SamplingClock get _samplingClock {
526+
/// Overrides the sampling clock for debugging and testing.
527+
///
528+
/// This value is ignored in non-debug builds.
529+
@protected
530+
SamplingClock? get debugSamplingClock => null;
531+
532+
/// Provides access to the current [DateTime] and `StopWatch` objects for
533+
/// sampling.
534+
///
535+
/// Overridden by [debugSamplingClock] for debug builds and testing. Using
536+
/// this object under test will maintain synchronization with [FakeAsync].
537+
SamplingClock get samplingClock {
533538
SamplingClock value = SamplingClock();
534539
assert(() {
535540
final SamplingClock? debugValue = debugSamplingClock;

packages/flutter/lib/src/gestures/velocity_tracker.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import 'package:flutter/foundation.dart';
77

8+
import 'binding.dart';
89
import 'events.dart';
910
import 'lsq_solver.dart';
1011

@@ -149,12 +150,21 @@ class VelocityTracker {
149150
/// The kind of pointer this tracker is for.
150151
final PointerDeviceKind kind;
151152

153+
// Time difference since the last sample was added
154+
Stopwatch get _sinceLastSample {
155+
_stopwatch ??= GestureBinding.instance.samplingClock.stopwatch();
156+
return _stopwatch!;
157+
}
158+
Stopwatch? _stopwatch;
159+
152160
// Circular buffer; current sample at _index.
153161
final List<_PointAtTime?> _samples = List<_PointAtTime?>.filled(_historySize, null);
154162
int _index = 0;
155163

156164
/// Adds a position as the given time to the tracker.
157165
void addPosition(Duration time, Offset position) {
166+
_sinceLastSample.start();
167+
_sinceLastSample.reset();
158168
_index += 1;
159169
if (_index == _historySize) {
160170
_index = 0;
@@ -169,6 +179,16 @@ class VelocityTracker {
169179
///
170180
/// Returns null if there is no data on which to base an estimate.
171181
VelocityEstimate? getVelocityEstimate() {
182+
// Has user recently moved since last sample?
183+
if (_sinceLastSample.elapsedMilliseconds > _assumePointerMoveStoppedMilliseconds) {
184+
return const VelocityEstimate(
185+
pixelsPerSecond: Offset.zero,
186+
confidence: 1.0,
187+
duration: Duration.zero,
188+
offset: Offset.zero,
189+
);
190+
}
191+
172192
final List<double> x = <double>[];
173193
final List<double> y = <double>[];
174194
final List<double> w = <double>[];
@@ -288,6 +308,8 @@ class IOSScrollViewFlingVelocityTracker extends VelocityTracker {
288308

289309
@override
290310
void addPosition(Duration time, Offset position) {
311+
_sinceLastSample.start();
312+
_sinceLastSample.reset();
291313
assert(() {
292314
final _PointAtTime? previousPoint = _touchSamples[_index];
293315
if (previousPoint == null || previousPoint.time <= time) {
@@ -326,6 +348,16 @@ class IOSScrollViewFlingVelocityTracker extends VelocityTracker {
326348

327349
@override
328350
VelocityEstimate getVelocityEstimate() {
351+
// Has user recently moved since last sample?
352+
if (_sinceLastSample.elapsedMilliseconds > VelocityTracker._assumePointerMoveStoppedMilliseconds) {
353+
return const VelocityEstimate(
354+
pixelsPerSecond: Offset.zero,
355+
confidence: 1.0,
356+
duration: Duration.zero,
357+
offset: Offset.zero,
358+
);
359+
}
360+
329361
// The velocity estimated using this expression is an approximation of the
330362
// scroll velocity of an iOS scroll view at the moment the user touch was
331363
// released, not the final velocity of the iOS pan gesture recognizer
@@ -387,6 +419,16 @@ class MacOSScrollViewFlingVelocityTracker extends IOSScrollViewFlingVelocityTrac
387419

388420
@override
389421
VelocityEstimate getVelocityEstimate() {
422+
// Has user recently moved since last sample?
423+
if (_sinceLastSample.elapsedMilliseconds > VelocityTracker._assumePointerMoveStoppedMilliseconds) {
424+
return const VelocityEstimate(
425+
pixelsPerSecond: Offset.zero,
426+
confidence: 1.0,
427+
duration: Duration.zero,
428+
offset: Offset.zero,
429+
);
430+
}
431+
390432
// The velocity estimated using this expression is an approximation of the
391433
// scroll velocity of a macOS scroll view at the moment the user touch was
392434
// released.

packages/flutter/test/gestures/double_tap_test.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ void main() {
3333

3434
setUp(() {
3535
tap = DoubleTapGestureRecognizer();
36+
addTearDown(tap.dispose);
3637

3738
doubleTapRecognized = false;
3839
tap.onDoubleTap = () {
@@ -156,6 +157,7 @@ void main() {
156157
final DoubleTapGestureRecognizer tapSecondary = DoubleTapGestureRecognizer(
157158
allowedButtonsFilter: (int buttons) => buttons == kSecondaryButton,
158159
);
160+
addTearDown(tapSecondary.dispose);
159161
tapSecondary.onDoubleTap = () {
160162
doubleTapRecognized = true;
161163
};
@@ -545,6 +547,7 @@ void main() {
545547
final DoubleTapGestureRecognizer tapPrimary = DoubleTapGestureRecognizer(
546548
allowedButtonsFilter: (int buttons) => buttons == kPrimaryButton,
547549
);
550+
addTearDown(tapPrimary.dispose);
548551
tapPrimary.onDoubleTap = () {
549552
doubleTapRecognized = true;
550553
};
@@ -647,14 +650,17 @@ void main() {
647650
..onTapDown = (TapDownDetails details) {
648651
recognized.add('tapPrimary');
649652
};
653+
addTearDown(tapPrimary.dispose);
650654
tapSecondary = TapGestureRecognizer()
651655
..onSecondaryTapDown = (TapDownDetails details) {
652656
recognized.add('tapSecondary');
653657
};
658+
addTearDown(tapSecondary.dispose);
654659
doubleTap = DoubleTapGestureRecognizer()
655660
..onDoubleTap = () {
656661
recognized.add('doubleTap');
657662
};
663+
addTearDown(doubleTap.dispose);
658664
});
659665

660666
tearDown(() {
@@ -692,6 +698,7 @@ void main() {
692698
..onDoubleTap = () {
693699
recognized.add('primary');
694700
};
701+
addTearDown(doubleTap.dispose);
695702

696703
// Down/up pair 7: normal tap sequence close to pair 6
697704
const PointerDownEvent down7 = PointerDownEvent(
@@ -730,6 +737,7 @@ void main() {
730737
..onDoubleTap = () {
731738
recognized.add('primary');
732739
};
740+
addTearDown(doubleTap.dispose);
733741

734742
// Down/up pair 7: normal tap sequence close to pair 6
735743
const PointerDownEvent down7 = PointerDownEvent(
@@ -765,8 +773,10 @@ void main() {
765773
int tapCount = 0;
766774
final DoubleTapGestureRecognizer doubleTap = DoubleTapGestureRecognizer()
767775
..onDoubleTap = () {};
776+
addTearDown(doubleTap.dispose);
768777
final TapGestureRecognizer tap = TapGestureRecognizer()
769778
..onTap = () => tapCount++;
779+
addTearDown(tap.dispose);
770780

771781
// Open a arena with 2 members and holding.
772782
doubleTap.addPointer(down1);

packages/flutter/test/gestures/gesture_binding_resample_event_on_widget_test.dart

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,17 @@
44

55
import 'dart:ui' as ui;
66

7-
import 'package:clock/clock.dart';
87
import 'package:flutter/gestures.dart';
98
import 'package:flutter/scheduler.dart';
109
import 'package:flutter/widgets.dart';
1110
import 'package:flutter_test/flutter_test.dart';
1211
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
1312

14-
class TestResampleEventFlutterBinding extends AutomatedTestWidgetsFlutterBinding {
15-
@override
16-
SamplingClock? get debugSamplingClock => TestSamplingClock(this.clock);
17-
}
18-
19-
class TestSamplingClock implements SamplingClock {
20-
TestSamplingClock(this._clock);
21-
22-
@override
23-
DateTime now() => _clock.now();
24-
25-
@override
26-
Stopwatch stopwatch() => _clock.stopwatch();
27-
28-
final Clock _clock;
29-
}
30-
3113
void main() {
32-
final TestWidgetsFlutterBinding binding = TestResampleEventFlutterBinding();
3314
testWidgetsWithLeakTracking('PointerEvent resampling on a widget', (WidgetTester tester) async {
34-
assert(WidgetsBinding.instance == binding);
35-
Duration currentTestFrameTime() => Duration(milliseconds: binding.clock.now().millisecondsSinceEpoch);
15+
Duration currentTestFrameTime() => Duration(
16+
milliseconds: TestWidgetsFlutterBinding.instance.clock.now().millisecondsSinceEpoch,
17+
);
3618
void requestFrame() => SchedulerBinding.instance.scheduleFrameCallback((_) {});
3719
final Duration epoch = currentTestFrameTime();
3820
final ui.PointerDataPacket packet = ui.PointerDataPacket(

packages/flutter/test/gestures/gesture_tester.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import 'package:fake_async/fake_async.dart';
66
import 'package:flutter/gestures.dart';
7-
import 'package:flutter_test/flutter_test.dart';
7+
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
88
import 'package:meta/meta.dart';
99

1010
class GestureTester {
@@ -26,7 +26,7 @@ typedef GestureTest = void Function(GestureTester tester);
2626

2727
@isTest
2828
void testGesture(String description, GestureTest callback) {
29-
test(description, () {
29+
testWidgetsWithLeakTracking(description, (_) async {
3030
FakeAsync().run((FakeAsync async) {
3131
callback(GestureTester._(async));
3232
});

0 commit comments

Comments
 (0)