1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | /// @docImport 'package:flutter/material.dart'; |
6 | /// @docImport 'package:flutter/rendering.dart'; |
7 | /// @docImport 'package:flutter/services.dart'; |
8 | /// @docImport 'package:flutter/widgets.dart'; |
9 | /// |
10 | /// @docImport 'ticker.dart'; |
11 | library; |
12 | |
13 | import 'dart:async'; |
14 | import 'dart:collection'; |
15 | import 'dart:developer' show Flow, Timeline, TimelineTask; |
16 | import 'dart:ui' show AppLifecycleState, DartPerformanceMode, FramePhase, FrameTiming, PlatformDispatcher, TimingsCallback; |
17 | |
18 | import 'package:collection/collection.dart' show HeapPriorityQueue, PriorityQueue; |
19 | import 'package:flutter/foundation.dart'; |
20 | |
21 | import 'debug.dart'; |
22 | import 'priority.dart'; |
23 | import 'service_extensions.dart'; |
24 | |
25 | export 'dart:ui' show AppLifecycleState, FrameTiming, TimingsCallback; |
26 | |
27 | export 'priority.dart' show Priority; |
28 | |
29 | /// Slows down animations by this factor to help in development. |
30 | double get timeDilation => _timeDilation; |
31 | double _timeDilation = 1.0; |
32 | /// If the [SchedulerBinding] has been initialized, setting the time dilation |
33 | /// automatically calls [SchedulerBinding.resetEpoch] to ensure that time stamps |
34 | /// seen by consumers of the scheduler binding are always increasing. |
35 | /// |
36 | /// It is safe to set this before initializing the binding. |
37 | set timeDilation(double value) { |
38 | assert(value > 0.0); |
39 | if (_timeDilation == value) { |
40 | return; |
41 | } |
42 | // If the binding has been created, we need to resetEpoch first so that we |
43 | // capture start of the epoch with the current time dilation. |
44 | SchedulerBinding._instance?.resetEpoch(); |
45 | _timeDilation = value; |
46 | } |
47 | |
48 | /// Signature for frame-related callbacks from the scheduler. |
49 | /// |
50 | /// The `timeStamp` is the number of milliseconds since the beginning of the |
51 | /// scheduler's epoch. Use timeStamp to determine how far to advance animation |
52 | /// timelines so that all the animations in the system are synchronized to a |
53 | /// common time base. |
54 | typedef FrameCallback = void Function(Duration timeStamp); |
55 | |
56 | /// Signature for [SchedulerBinding.scheduleTask] callbacks. |
57 | /// |
58 | /// The type argument `T` is the task's return value. Consider `void` if the |
59 | /// task does not return a value. |
60 | typedef TaskCallback<T> = FutureOr<T> Function(); |
61 | |
62 | /// Signature for the [SchedulerBinding.schedulingStrategy] callback. Called |
63 | /// whenever the system needs to decide whether a task at a given |
64 | /// priority needs to be run. |
65 | /// |
66 | /// Return true if a task with the given priority should be executed at this |
67 | /// time, false otherwise. |
68 | /// |
69 | /// See also: |
70 | /// |
71 | /// * [defaultSchedulingStrategy], the default [SchedulingStrategy] for [SchedulerBinding.schedulingStrategy]. |
72 | typedef SchedulingStrategy = bool Function({ required int priority, required SchedulerBinding scheduler }); |
73 | |
74 | class _TaskEntry<T> { |
75 | _TaskEntry(this.task, this.priority, this.debugLabel, this.flow) { |
76 | assert(() { |
77 | debugStack = StackTrace.current; |
78 | return true; |
79 | }()); |
80 | } |
81 | final TaskCallback<T> task; |
82 | final int priority; |
83 | final String? debugLabel; |
84 | final Flow? flow; |
85 | |
86 | late StackTrace debugStack; |
87 | final Completer<T> completer = Completer<T>(); |
88 | |
89 | void run() { |
90 | if (!kReleaseMode) { |
91 | Timeline.timeSync( |
92 | debugLabel ?? 'Scheduled Task' , |
93 | () { |
94 | completer.complete(task()); |
95 | }, |
96 | flow: flow != null ? Flow.step(flow!.id) : null, |
97 | ); |
98 | } else { |
99 | completer.complete(task()); |
100 | } |
101 | } |
102 | } |
103 | |
104 | class _FrameCallbackEntry { |
105 | _FrameCallbackEntry(this.callback, { bool rescheduling = false }) { |
106 | assert(() { |
107 | if (rescheduling) { |
108 | assert(() { |
109 | if (debugCurrentCallbackStack == null) { |
110 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
111 | ErrorSummary('scheduleFrameCallback called with rescheduling true, but no callback is in scope.' ), |
112 | ErrorDescription( |
113 | 'The "rescheduling" argument should only be set to true if the ' |
114 | 'callback is being reregistered from within the callback itself, ' |
115 | 'and only then if the callback itself is entirely synchronous.' , |
116 | ), |
117 | ErrorHint( |
118 | 'If this is the initial registration of the callback, or if the ' |
119 | 'callback is asynchronous, then do not use the "rescheduling" ' |
120 | 'argument.' , |
121 | ), |
122 | ]); |
123 | } |
124 | return true; |
125 | }()); |
126 | debugStack = debugCurrentCallbackStack; |
127 | } else { |
128 | // TODO(ianh): trim the frames from this library, so that the call to scheduleFrameCallback is the top one |
129 | debugStack = StackTrace.current; |
130 | } |
131 | return true; |
132 | }()); |
133 | } |
134 | |
135 | final FrameCallback callback; |
136 | |
137 | static StackTrace? debugCurrentCallbackStack; |
138 | StackTrace? debugStack; |
139 | } |
140 | |
141 | /// The various phases that a [SchedulerBinding] goes through during |
142 | /// [SchedulerBinding.handleBeginFrame]. |
143 | /// |
144 | /// This is exposed by [SchedulerBinding.schedulerPhase]. |
145 | /// |
146 | /// The values of this enum are ordered in the same order as the phases occur, |
147 | /// so their relative index values can be compared to each other. |
148 | /// |
149 | /// See also: |
150 | /// |
151 | /// * [WidgetsBinding.drawFrame], which pumps the build and rendering pipeline |
152 | /// to generate a frame. |
153 | enum SchedulerPhase { |
154 | /// No frame is being processed. Tasks (scheduled by |
155 | /// [SchedulerBinding.scheduleTask]), microtasks (scheduled by |
156 | /// [scheduleMicrotask]), [Timer] callbacks, event handlers (e.g. from user |
157 | /// input), and other callbacks (e.g. from [Future]s, [Stream]s, and the like) |
158 | /// may be executing. |
159 | idle, |
160 | |
161 | /// The transient callbacks (scheduled by |
162 | /// [SchedulerBinding.scheduleFrameCallback]) are currently executing. |
163 | /// |
164 | /// Typically, these callbacks handle updating objects to new animation |
165 | /// states. |
166 | /// |
167 | /// See [SchedulerBinding.handleBeginFrame]. |
168 | transientCallbacks, |
169 | |
170 | /// Microtasks scheduled during the processing of transient callbacks are |
171 | /// current executing. |
172 | /// |
173 | /// This may include, for instance, callbacks from futures resolved during the |
174 | /// [transientCallbacks] phase. |
175 | midFrameMicrotasks, |
176 | |
177 | /// The persistent callbacks (scheduled by |
178 | /// [SchedulerBinding.addPersistentFrameCallback]) are currently executing. |
179 | /// |
180 | /// Typically, this is the build/layout/paint pipeline. See |
181 | /// [WidgetsBinding.drawFrame] and [SchedulerBinding.handleDrawFrame]. |
182 | persistentCallbacks, |
183 | |
184 | /// The post-frame callbacks (scheduled by |
185 | /// [SchedulerBinding.addPostFrameCallback]) are currently executing. |
186 | /// |
187 | /// Typically, these callbacks handle cleanup and scheduling of work for the |
188 | /// next frame. |
189 | /// |
190 | /// See [SchedulerBinding.handleDrawFrame]. |
191 | postFrameCallbacks, |
192 | } |
193 | |
194 | /// This callback is invoked when a request for [DartPerformanceMode] is disposed. |
195 | /// |
196 | /// See also: |
197 | /// |
198 | /// * [PerformanceModeRequestHandle] for more information on the lifecycle of the handle. |
199 | typedef _PerformanceModeCleanupCallback = VoidCallback; |
200 | |
201 | /// An opaque handle that keeps a request for [DartPerformanceMode] active until |
202 | /// disposed. |
203 | /// |
204 | /// To create a [PerformanceModeRequestHandle], use [SchedulerBinding.requestPerformanceMode]. |
205 | /// The component that makes the request is responsible for disposing the handle. |
206 | class PerformanceModeRequestHandle { |
207 | PerformanceModeRequestHandle._(_PerformanceModeCleanupCallback this._cleanup) { |
208 | // TODO(polina-c): stop duplicating code across disposables |
209 | // https://github.com/flutter/flutter/issues/137435 |
210 | if (kFlutterMemoryAllocationsEnabled) { |
211 | FlutterMemoryAllocations.instance.dispatchObjectCreated( |
212 | library: 'package:flutter/scheduler.dart' , |
213 | className: ' $PerformanceModeRequestHandle' , |
214 | object: this, |
215 | ); |
216 | } |
217 | } |
218 | |
219 | _PerformanceModeCleanupCallback? _cleanup; |
220 | |
221 | /// Call this method to signal to [SchedulerBinding] that a request for a [DartPerformanceMode] |
222 | /// is no longer needed. |
223 | /// |
224 | /// This method must only be called once per object. |
225 | void dispose() { |
226 | assert(_cleanup != null); |
227 | // TODO(polina-c): stop duplicating code across disposables |
228 | // https://github.com/flutter/flutter/issues/137435 |
229 | if (kFlutterMemoryAllocationsEnabled) { |
230 | FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this); |
231 | } |
232 | _cleanup!(); |
233 | _cleanup = null; |
234 | } |
235 | } |
236 | |
237 | /// Scheduler for running the following: |
238 | /// |
239 | /// * _Transient callbacks_, triggered by the system's |
240 | /// [dart:ui.PlatformDispatcher.onBeginFrame] callback, for synchronizing the |
241 | /// application's behavior to the system's display. For example, [Ticker]s and |
242 | /// [AnimationController]s trigger from these. |
243 | /// |
244 | /// * _Persistent callbacks_, triggered by the system's |
245 | /// [dart:ui.PlatformDispatcher.onDrawFrame] callback, for updating the |
246 | /// system's display after transient callbacks have executed. For example, the |
247 | /// rendering layer uses this to drive its rendering pipeline. |
248 | /// |
249 | /// * _Post-frame callbacks_, which are run after persistent callbacks, just |
250 | /// before returning from the [dart:ui.PlatformDispatcher.onDrawFrame] callback. |
251 | /// |
252 | /// * Non-rendering tasks, to be run between frames. These are given a |
253 | /// priority and are executed in priority order according to a |
254 | /// [schedulingStrategy]. |
255 | mixin SchedulerBinding on BindingBase { |
256 | @override |
257 | void initInstances() { |
258 | super.initInstances(); |
259 | _instance = this; |
260 | |
261 | if (!kReleaseMode) { |
262 | addTimingsCallback((List<FrameTiming> timings) { |
263 | timings.forEach(_profileFramePostEvent); |
264 | }); |
265 | } |
266 | } |
267 | |
268 | /// The current [SchedulerBinding], if one has been created. |
269 | /// |
270 | /// Provides access to the features exposed by this mixin. The binding must |
271 | /// be initialized before using this getter; this is typically done by calling |
272 | /// [runApp] or [WidgetsFlutterBinding.ensureInitialized]. |
273 | static SchedulerBinding get instance => BindingBase.checkInstance(_instance); |
274 | static SchedulerBinding? _instance; |
275 | |
276 | final List<TimingsCallback> _timingsCallbacks = <TimingsCallback>[]; |
277 | |
278 | /// Add a [TimingsCallback] that receives [FrameTiming] sent from |
279 | /// the engine. |
280 | /// |
281 | /// This API enables applications to monitor their graphics |
282 | /// performance. Data from the engine is batched into lists of |
283 | /// [FrameTiming] objects which are reported approximately once a |
284 | /// second in release mode and approximately once every 100ms in |
285 | /// debug and profile builds. The list is sorted in ascending |
286 | /// chronological order (earliest frame first). The timing of the |
287 | /// first frame is sent immediately without batching. |
288 | /// |
289 | /// The data returned can be used to catch missed frames (by seeing |
290 | /// if [FrameTiming.buildDuration] or [FrameTiming.rasterDuration] |
291 | /// exceed the frame budget, e.g. 16ms at 60Hz), and to catch high |
292 | /// latency (by seeing if [FrameTiming.totalSpan] exceeds the frame |
293 | /// budget). It is possible for no frames to be missed but for the |
294 | /// latency to be more than one frame in the case where the Flutter |
295 | /// engine is pipelining the graphics updates, e.g. because the sum |
296 | /// of the [FrameTiming.buildDuration] and the |
297 | /// [FrameTiming.rasterDuration] together exceed the frame budget. |
298 | /// In those cases, animations will be smooth but touch input will |
299 | /// feel more sluggish. |
300 | /// |
301 | /// Using [addTimingsCallback] is preferred over using |
302 | /// [dart:ui.PlatformDispatcher.onReportTimings] directly because the |
303 | /// [dart:ui.PlatformDispatcher.onReportTimings] API only allows one callback, |
304 | /// which prevents multiple libraries from registering listeners |
305 | /// simultaneously, while this API allows multiple callbacks to be registered |
306 | /// independently. |
307 | /// |
308 | /// This API is implemented in terms of |
309 | /// [dart:ui.PlatformDispatcher.onReportTimings]. In release builds, when no |
310 | /// libraries have registered with this API, the |
311 | /// [dart:ui.PlatformDispatcher.onReportTimings] callback is not set, which |
312 | /// disables the performance tracking and reduces the runtime overhead to |
313 | /// approximately zero. The performance overhead of the performance tracking |
314 | /// when one or more callbacks are registered (i.e. when it is enabled) is |
315 | /// very approximately 0.01% CPU usage per second (measured on an iPhone 6s). |
316 | /// |
317 | /// In debug and profile builds, the [SchedulerBinding] itself |
318 | /// registers a timings callback to update the [Timeline]. |
319 | /// |
320 | /// If the same callback is added twice, it will be executed twice. |
321 | /// |
322 | /// See also: |
323 | /// |
324 | /// * [removeTimingsCallback], which can be used to remove a callback |
325 | /// added using this method. |
326 | void addTimingsCallback(TimingsCallback callback) { |
327 | _timingsCallbacks.add(callback); |
328 | if (_timingsCallbacks.length == 1) { |
329 | assert(platformDispatcher.onReportTimings == null); |
330 | platformDispatcher.onReportTimings = _executeTimingsCallbacks; |
331 | } |
332 | assert(platformDispatcher.onReportTimings == _executeTimingsCallbacks); |
333 | } |
334 | |
335 | /// Removes a callback that was earlier added by [addTimingsCallback]. |
336 | void removeTimingsCallback(TimingsCallback callback) { |
337 | assert(_timingsCallbacks.contains(callback)); |
338 | _timingsCallbacks.remove(callback); |
339 | if (_timingsCallbacks.isEmpty) { |
340 | platformDispatcher.onReportTimings = null; |
341 | } |
342 | } |
343 | |
344 | @pragma('vm:notify-debugger-on-exception' ) |
345 | void _executeTimingsCallbacks(List<FrameTiming> timings) { |
346 | final List<TimingsCallback> clonedCallbacks = |
347 | List<TimingsCallback>.of(_timingsCallbacks); |
348 | for (final TimingsCallback callback in clonedCallbacks) { |
349 | try { |
350 | if (_timingsCallbacks.contains(callback)) { |
351 | callback(timings); |
352 | } |
353 | } catch (exception, stack) { |
354 | InformationCollector? collector; |
355 | assert(() { |
356 | collector = () => <DiagnosticsNode>[ |
357 | DiagnosticsProperty<TimingsCallback>( |
358 | 'The TimingsCallback that gets executed was' , |
359 | callback, |
360 | style: DiagnosticsTreeStyle.errorProperty, |
361 | ), |
362 | ]; |
363 | return true; |
364 | }()); |
365 | FlutterError.reportError(FlutterErrorDetails( |
366 | exception: exception, |
367 | stack: stack, |
368 | context: ErrorDescription('while executing callbacks for FrameTiming' ), |
369 | informationCollector: collector, |
370 | )); |
371 | } |
372 | } |
373 | } |
374 | |
375 | @override |
376 | void initServiceExtensions() { |
377 | super.initServiceExtensions(); |
378 | |
379 | if (!kReleaseMode) { |
380 | registerNumericServiceExtension( |
381 | name: SchedulerServiceExtensions.timeDilation.name, |
382 | getter: () async => timeDilation, |
383 | setter: (double value) async { |
384 | timeDilation = value; |
385 | }, |
386 | ); |
387 | } |
388 | } |
389 | |
390 | /// Whether the application is visible, and if so, whether it is currently |
391 | /// interactive. |
392 | /// |
393 | /// This is set by [handleAppLifecycleStateChanged] when the |
394 | /// [SystemChannels.lifecycle] notification is dispatched. |
395 | /// |
396 | /// The preferred ways to watch for changes to this value are using |
397 | /// [WidgetsBindingObserver.didChangeAppLifecycleState], or through an |
398 | /// [AppLifecycleListener] object. |
399 | AppLifecycleState? get lifecycleState => _lifecycleState; |
400 | AppLifecycleState? _lifecycleState; |
401 | |
402 | /// Allows the test framework to reset the lifecycle state and framesEnabled |
403 | /// back to their initial values. |
404 | @visibleForTesting |
405 | void resetInternalState() { |
406 | _lifecycleState = null; |
407 | _framesEnabled = true; |
408 | } |
409 | |
410 | /// Called when the application lifecycle state changes. |
411 | /// |
412 | /// Notifies all the observers using |
413 | /// [WidgetsBindingObserver.didChangeAppLifecycleState]. |
414 | /// |
415 | /// This method exposes notifications from [SystemChannels.lifecycle]. |
416 | @protected |
417 | @mustCallSuper |
418 | void handleAppLifecycleStateChanged(AppLifecycleState state) { |
419 | if (lifecycleState == state) { |
420 | return; |
421 | } |
422 | _lifecycleState = state; |
423 | switch (state) { |
424 | case AppLifecycleState.resumed: |
425 | case AppLifecycleState.inactive: |
426 | _setFramesEnabledState(true); |
427 | case AppLifecycleState.hidden: |
428 | case AppLifecycleState.paused: |
429 | case AppLifecycleState.detached: |
430 | _setFramesEnabledState(false); |
431 | } |
432 | } |
433 | |
434 | /// The strategy to use when deciding whether to run a task or not. |
435 | /// |
436 | /// Defaults to [defaultSchedulingStrategy]. |
437 | SchedulingStrategy schedulingStrategy = defaultSchedulingStrategy; |
438 | |
439 | static int _taskSorter (_TaskEntry<dynamic> e1, _TaskEntry<dynamic> e2) { |
440 | return -e1.priority.compareTo(e2.priority); |
441 | } |
442 | final PriorityQueue<_TaskEntry<dynamic>> _taskQueue = HeapPriorityQueue<_TaskEntry<dynamic>>(_taskSorter); |
443 | |
444 | /// Schedules the given `task` with the given `priority`. |
445 | /// |
446 | /// If `task` returns a future, the future returned by [scheduleTask] will |
447 | /// complete after the former future has been scheduled to completion. |
448 | /// Otherwise, the returned future for [scheduleTask] will complete with the |
449 | /// same value returned by `task` after it has been scheduled. |
450 | /// |
451 | /// The `debugLabel` and `flow` are used to report the task to the [Timeline], |
452 | /// for use when profiling. |
453 | /// |
454 | /// ## Processing model |
455 | /// |
456 | /// Tasks will be executed between frames, in priority order, |
457 | /// excluding tasks that are skipped by the current |
458 | /// [schedulingStrategy]. Tasks should be short (as in, up to a |
459 | /// millisecond), so as to not cause the regular frame callbacks to |
460 | /// get delayed. |
461 | /// |
462 | /// If an animation is running, including, for instance, a [ProgressIndicator] |
463 | /// indicating that there are pending tasks, then tasks with a priority below |
464 | /// [Priority.animation] won't run (at least, not with the |
465 | /// [defaultSchedulingStrategy]; this can be configured using |
466 | /// [schedulingStrategy]). |
467 | Future<T> scheduleTask<T>( |
468 | TaskCallback<T> task, |
469 | Priority priority, { |
470 | String? debugLabel, |
471 | Flow? flow, |
472 | }) { |
473 | final bool isFirstTask = _taskQueue.isEmpty; |
474 | final _TaskEntry<T> entry = _TaskEntry<T>( |
475 | task, |
476 | priority.value, |
477 | debugLabel, |
478 | flow, |
479 | ); |
480 | _taskQueue.add(entry); |
481 | if (isFirstTask && !locked) { |
482 | _ensureEventLoopCallback(); |
483 | } |
484 | return entry.completer.future; |
485 | } |
486 | |
487 | @override |
488 | void unlocked() { |
489 | super.unlocked(); |
490 | if (_taskQueue.isNotEmpty) { |
491 | _ensureEventLoopCallback(); |
492 | } |
493 | } |
494 | |
495 | // Whether this scheduler already requested to be called from the event loop. |
496 | bool _hasRequestedAnEventLoopCallback = false; |
497 | |
498 | // Ensures that the scheduler services a task scheduled by |
499 | // [SchedulerBinding.scheduleTask]. |
500 | void _ensureEventLoopCallback() { |
501 | assert(!locked); |
502 | assert(_taskQueue.isNotEmpty); |
503 | if (_hasRequestedAnEventLoopCallback) { |
504 | return; |
505 | } |
506 | _hasRequestedAnEventLoopCallback = true; |
507 | Timer.run(_runTasks); |
508 | } |
509 | |
510 | // Scheduled by _ensureEventLoopCallback. |
511 | void _runTasks() { |
512 | _hasRequestedAnEventLoopCallback = false; |
513 | if (handleEventLoopCallback()) { |
514 | _ensureEventLoopCallback(); |
515 | } // runs next task when there's time |
516 | } |
517 | |
518 | /// Execute the highest-priority task, if it is of a high enough priority. |
519 | /// |
520 | /// Returns false if the scheduler is [locked], or if there are no tasks |
521 | /// remaining. |
522 | /// |
523 | /// Returns true otherwise, including when no task is executed due to priority |
524 | /// being too low. |
525 | @visibleForTesting |
526 | @pragma('vm:notify-debugger-on-exception' ) |
527 | bool handleEventLoopCallback() { |
528 | if (_taskQueue.isEmpty || locked) { |
529 | return false; |
530 | } |
531 | final _TaskEntry<dynamic> entry = _taskQueue.first; |
532 | if (schedulingStrategy(priority: entry.priority, scheduler: this)) { |
533 | try { |
534 | _taskQueue.removeFirst(); |
535 | entry.run(); |
536 | } catch (exception, exceptionStack) { |
537 | StackTrace? callbackStack; |
538 | assert(() { |
539 | callbackStack = entry.debugStack; |
540 | return true; |
541 | }()); |
542 | FlutterError.reportError(FlutterErrorDetails( |
543 | exception: exception, |
544 | stack: exceptionStack, |
545 | library: 'scheduler library' , |
546 | context: ErrorDescription('during a task callback' ), |
547 | informationCollector: (callbackStack == null) ? null : () { |
548 | return <DiagnosticsNode>[ |
549 | DiagnosticsStackTrace( |
550 | '\nThis exception was thrown in the context of a scheduler callback. ' |
551 | 'When the scheduler callback was _registered_ (as opposed to when the ' |
552 | 'exception was thrown), this was the stack' , |
553 | callbackStack, |
554 | ), |
555 | ]; |
556 | }, |
557 | )); |
558 | } |
559 | return _taskQueue.isNotEmpty; |
560 | } |
561 | return true; |
562 | } |
563 | |
564 | int _nextFrameCallbackId = 0; // positive |
565 | Map<int, _FrameCallbackEntry> _transientCallbacks = <int, _FrameCallbackEntry>{}; |
566 | final Set<int> _removedIds = HashSet<int>(); |
567 | |
568 | /// The current number of transient frame callbacks scheduled. |
569 | /// |
570 | /// This is reset to zero just before all the currently scheduled |
571 | /// transient callbacks are called, at the start of a frame. |
572 | /// |
573 | /// This number is primarily exposed so that tests can verify that |
574 | /// there are no unexpected transient callbacks still registered |
575 | /// after a test's resources have been gracefully disposed. |
576 | int get transientCallbackCount => _transientCallbacks.length; |
577 | |
578 | /// Schedules the given transient frame callback. |
579 | /// |
580 | /// Adds the given callback to the list of frame callbacks and ensures that a |
581 | /// frame is scheduled. |
582 | /// |
583 | /// If this is called during the frame's animation phase (when transient frame |
584 | /// callbacks are still being invoked), a new frame will be scheduled, and |
585 | /// `callback` will be called in the newly scheduled frame, not in the current |
586 | /// frame. |
587 | /// |
588 | /// If this is a one-off registration, ignore the `rescheduling` argument. |
589 | /// |
590 | /// If this is a callback that will be re-registered each time it fires, then |
591 | /// when you re-register the callback, set the `rescheduling` argument to |
592 | /// true. This has no effect in release builds, but in debug builds, it |
593 | /// ensures that the stack trace that is stored for this callback is the |
594 | /// original stack trace for when the callback was _first_ registered, rather |
595 | /// than the stack trace for when the callback is re-registered. This makes it |
596 | /// easier to track down the original reason that a particular callback was |
597 | /// called. If `rescheduling` is true, the call must be in the context of a |
598 | /// frame callback. |
599 | /// |
600 | /// Callbacks registered with this method can be canceled using |
601 | /// [cancelFrameCallbackWithId]. |
602 | /// |
603 | /// See also: |
604 | /// |
605 | /// * [WidgetsBinding.drawFrame], which explains the phases of each frame |
606 | /// for those apps that use Flutter widgets (and where transient frame |
607 | /// callbacks fit into those phases). |
608 | int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) { |
609 | scheduleFrame(); |
610 | _nextFrameCallbackId += 1; |
611 | _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling); |
612 | return _nextFrameCallbackId; |
613 | } |
614 | |
615 | /// Cancels the transient frame callback with the given [id]. |
616 | /// |
617 | /// Removes the given callback from the list of frame callbacks. If a frame |
618 | /// has been requested, this does not also cancel that request. |
619 | /// |
620 | /// Transient frame callbacks are those registered using |
621 | /// [scheduleFrameCallback]. |
622 | void cancelFrameCallbackWithId(int id) { |
623 | assert(id > 0); |
624 | _transientCallbacks.remove(id); |
625 | _removedIds.add(id); |
626 | } |
627 | |
628 | /// Asserts that there are no registered transient callbacks; if |
629 | /// there are, prints their locations and throws an exception. |
630 | /// |
631 | /// A transient frame callback is one that was registered with |
632 | /// [scheduleFrameCallback]. |
633 | /// |
634 | /// This is expected to be called at the end of tests (the |
635 | /// flutter_test framework does it automatically in normal cases). |
636 | /// |
637 | /// Call this method when you expect there to be no transient |
638 | /// callbacks registered, in an assert statement with a message that |
639 | /// you want printed when a transient callback is registered: |
640 | /// |
641 | /// ```dart |
642 | /// assert(SchedulerBinding.instance.debugAssertNoTransientCallbacks( |
643 | /// 'A leak of transient callbacks was detected while doing foo.' |
644 | /// )); |
645 | /// ``` |
646 | /// |
647 | /// Does nothing if asserts are disabled. Always returns true. |
648 | bool debugAssertNoTransientCallbacks(String reason) { |
649 | assert(() { |
650 | if (transientCallbackCount > 0) { |
651 | // We cache the values so that we can produce them later |
652 | // even if the information collector is called after |
653 | // the problem has been resolved. |
654 | final int count = transientCallbackCount; |
655 | final Map<int, _FrameCallbackEntry> callbacks = Map<int, _FrameCallbackEntry>.of(_transientCallbacks); |
656 | FlutterError.reportError(FlutterErrorDetails( |
657 | exception: reason, |
658 | library: 'scheduler library' , |
659 | informationCollector: () => <DiagnosticsNode>[ |
660 | if (count == 1) |
661 | // TODO(jacobr): I have added an extra line break in this case. |
662 | ErrorDescription( |
663 | 'There was one transient callback left. ' |
664 | 'The stack trace for when it was registered is as follows:' , |
665 | ) |
666 | else |
667 | ErrorDescription( |
668 | 'There were $count transient callbacks left. ' |
669 | 'The stack traces for when they were registered are as follows:' , |
670 | ), |
671 | for (final int id in callbacks.keys) |
672 | DiagnosticsStackTrace('── callback $id ──' , callbacks[id]!.debugStack, showSeparator: false), |
673 | ], |
674 | )); |
675 | } |
676 | return true; |
677 | }()); |
678 | return true; |
679 | } |
680 | |
681 | /// Asserts that there are no pending performance mode requests in debug mode. |
682 | /// |
683 | /// Throws a [FlutterError] if there are pending performance mode requests, |
684 | /// as this indicates a potential memory leak. |
685 | bool debugAssertNoPendingPerformanceModeRequests(String reason) { |
686 | assert(() { |
687 | if (_performanceMode != null) { |
688 | throw FlutterError(reason); |
689 | } |
690 | return true; |
691 | }()); |
692 | return true; |
693 | } |
694 | |
695 | /// Asserts that there is no artificial time dilation in debug mode. |
696 | /// |
697 | /// Throws a [FlutterError] if there are such dilation, as this will make |
698 | /// subsequent tests see dilation and thus flaky. |
699 | bool debugAssertNoTimeDilation(String reason) { |
700 | assert(() { |
701 | if (timeDilation != 1.0) { |
702 | throw FlutterError(reason); |
703 | } |
704 | return true; |
705 | }()); |
706 | return true; |
707 | } |
708 | |
709 | /// Prints the stack for where the current transient callback was registered. |
710 | /// |
711 | /// A transient frame callback is one that was registered with |
712 | /// [scheduleFrameCallback]. |
713 | /// |
714 | /// When called in debug more and in the context of a transient callback, this |
715 | /// function prints the stack trace from where the current transient callback |
716 | /// was registered (i.e. where it first called [scheduleFrameCallback]). |
717 | /// |
718 | /// When called in debug mode in other contexts, it prints a message saying |
719 | /// that this function was not called in the context a transient callback. |
720 | /// |
721 | /// In release mode, this function does nothing. |
722 | /// |
723 | /// To call this function, use the following code: |
724 | /// |
725 | /// ```dart |
726 | /// SchedulerBinding.debugPrintTransientCallbackRegistrationStack(); |
727 | /// ``` |
728 | static void debugPrintTransientCallbackRegistrationStack() { |
729 | assert(() { |
730 | if (_FrameCallbackEntry.debugCurrentCallbackStack != null) { |
731 | debugPrint('When the current transient callback was registered, this was the stack:' ); |
732 | debugPrint( |
733 | FlutterError.defaultStackFilter( |
734 | FlutterError.demangleStackTrace( |
735 | _FrameCallbackEntry.debugCurrentCallbackStack!, |
736 | ).toString().trimRight().split('\n' ), |
737 | ).join('\n' ), |
738 | ); |
739 | } else { |
740 | debugPrint('No transient callback is currently executing.' ); |
741 | } |
742 | return true; |
743 | }()); |
744 | } |
745 | |
746 | final List<FrameCallback> _persistentCallbacks = <FrameCallback>[]; |
747 | |
748 | /// Adds a persistent frame callback. |
749 | /// |
750 | /// Persistent callbacks are called after transient |
751 | /// (non-persistent) frame callbacks. |
752 | /// |
753 | /// Does *not* request a new frame. Conceptually, persistent frame |
754 | /// callbacks are observers of "begin frame" events. Since they are |
755 | /// executed after the transient frame callbacks they can drive the |
756 | /// rendering pipeline. |
757 | /// |
758 | /// Persistent frame callbacks cannot be unregistered. Once registered, they |
759 | /// are called for every frame for the lifetime of the application. |
760 | /// |
761 | /// See also: |
762 | /// |
763 | /// * [WidgetsBinding.drawFrame], which explains the phases of each frame |
764 | /// for those apps that use Flutter widgets (and where persistent frame |
765 | /// callbacks fit into those phases). |
766 | void addPersistentFrameCallback(FrameCallback callback) { |
767 | _persistentCallbacks.add(callback); |
768 | } |
769 | |
770 | final List<FrameCallback> _postFrameCallbacks = <FrameCallback>[]; |
771 | |
772 | /// Schedule a callback for the end of this frame. |
773 | /// |
774 | /// The provided callback is run immediately after a frame, just after the |
775 | /// persistent frame callbacks (which is when the main rendering pipeline has |
776 | /// been flushed). |
777 | /// |
778 | /// This method does *not* request a new frame. If a frame is already in |
779 | /// progress and the execution of post-frame callbacks has not yet begun, then |
780 | /// the registered callback is executed at the end of the current frame. |
781 | /// Otherwise, the registered callback is executed after the next frame |
782 | /// (whenever that may be, if ever). |
783 | /// |
784 | /// The callbacks are executed in the order in which they have been |
785 | /// added. |
786 | /// |
787 | /// Post-frame callbacks cannot be unregistered. They are called exactly once. |
788 | /// |
789 | /// In debug mode, if [debugTracePostFrameCallbacks] is set to true, then the |
790 | /// registered callback will show up in the timeline events chart, which can |
791 | /// be viewed in [DevTools](https://docs.flutter.dev/tools/devtools). |
792 | /// In that case, the `debugLabel` argument specifies the name of the callback |
793 | /// as it will appear in the timeline. In profile and release builds, |
794 | /// post-frame are never traced, and the `debugLabel` argument is ignored. |
795 | /// |
796 | /// See also: |
797 | /// |
798 | /// * [scheduleFrameCallback], which registers a callback for the start of |
799 | /// the next frame. |
800 | /// * [WidgetsBinding.drawFrame], which explains the phases of each frame |
801 | /// for those apps that use Flutter widgets (and where post frame |
802 | /// callbacks fit into those phases). |
803 | void addPostFrameCallback(FrameCallback callback, {String debugLabel = 'callback' }) { |
804 | assert(() { |
805 | if (debugTracePostFrameCallbacks) { |
806 | final FrameCallback originalCallback = callback; |
807 | callback = (Duration timeStamp) { |
808 | Timeline.startSync(debugLabel); |
809 | try { |
810 | originalCallback(timeStamp); |
811 | } finally { |
812 | Timeline.finishSync(); |
813 | } |
814 | }; |
815 | } |
816 | return true; |
817 | }()); |
818 | _postFrameCallbacks.add(callback); |
819 | } |
820 | |
821 | Completer<void>? _nextFrameCompleter; |
822 | |
823 | /// Returns a Future that completes after the frame completes. |
824 | /// |
825 | /// If this is called between frames, a frame is immediately scheduled if |
826 | /// necessary. If this is called during a frame, the Future completes after |
827 | /// the current frame. |
828 | /// |
829 | /// If the device's screen is currently turned off, this may wait a very long |
830 | /// time, since frames are not scheduled while the device's screen is turned |
831 | /// off. |
832 | Future<void> get endOfFrame { |
833 | if (_nextFrameCompleter == null) { |
834 | if (schedulerPhase == SchedulerPhase.idle) { |
835 | scheduleFrame(); |
836 | } |
837 | _nextFrameCompleter = Completer<void>(); |
838 | addPostFrameCallback((Duration timeStamp) { |
839 | _nextFrameCompleter!.complete(); |
840 | _nextFrameCompleter = null; |
841 | }, debugLabel: 'SchedulerBinding.completeFrame' ); |
842 | } |
843 | return _nextFrameCompleter!.future; |
844 | } |
845 | |
846 | /// Whether this scheduler has requested that [handleBeginFrame] be called soon. |
847 | bool get hasScheduledFrame => _hasScheduledFrame; |
848 | bool _hasScheduledFrame = false; |
849 | |
850 | /// The phase that the scheduler is currently operating under. |
851 | SchedulerPhase get schedulerPhase => _schedulerPhase; |
852 | SchedulerPhase _schedulerPhase = SchedulerPhase.idle; |
853 | |
854 | /// Whether frames are currently being scheduled when [scheduleFrame] is called. |
855 | /// |
856 | /// This value depends on the value of the [lifecycleState]. |
857 | bool get framesEnabled => _framesEnabled; |
858 | |
859 | bool _framesEnabled = true; |
860 | void _setFramesEnabledState(bool enabled) { |
861 | if (_framesEnabled == enabled) { |
862 | return; |
863 | } |
864 | _framesEnabled = enabled; |
865 | if (enabled) { |
866 | scheduleFrame(); |
867 | } |
868 | } |
869 | |
870 | /// Ensures callbacks for [PlatformDispatcher.onBeginFrame] and |
871 | /// [PlatformDispatcher.onDrawFrame] are registered. |
872 | @protected |
873 | void ensureFrameCallbacksRegistered() { |
874 | platformDispatcher.onBeginFrame ??= _handleBeginFrame; |
875 | platformDispatcher.onDrawFrame ??= _handleDrawFrame; |
876 | } |
877 | |
878 | /// Schedules a new frame using [scheduleFrame] if this object is not |
879 | /// currently producing a frame. |
880 | /// |
881 | /// Calling this method ensures that [handleDrawFrame] will eventually be |
882 | /// called, unless it's already in progress. |
883 | /// |
884 | /// This has no effect if [schedulerPhase] is |
885 | /// [SchedulerPhase.transientCallbacks] or [SchedulerPhase.midFrameMicrotasks] |
886 | /// (because a frame is already being prepared in that case), or |
887 | /// [SchedulerPhase.persistentCallbacks] (because a frame is actively being |
888 | /// rendered in that case). It will schedule a frame if the [schedulerPhase] |
889 | /// is [SchedulerPhase.idle] (in between frames) or |
890 | /// [SchedulerPhase.postFrameCallbacks] (after a frame). |
891 | void ensureVisualUpdate() { |
892 | switch (schedulerPhase) { |
893 | case SchedulerPhase.idle: |
894 | case SchedulerPhase.postFrameCallbacks: |
895 | scheduleFrame(); |
896 | return; |
897 | case SchedulerPhase.transientCallbacks: |
898 | case SchedulerPhase.midFrameMicrotasks: |
899 | case SchedulerPhase.persistentCallbacks: |
900 | return; |
901 | } |
902 | } |
903 | |
904 | /// If necessary, schedules a new frame by calling |
905 | /// [dart:ui.PlatformDispatcher.scheduleFrame]. |
906 | /// |
907 | /// After this is called, the engine will (eventually) call |
908 | /// [handleBeginFrame]. (This call might be delayed, e.g. if the device's |
909 | /// screen is turned off it will typically be delayed until the screen is on |
910 | /// and the application is visible.) Calling this during a frame forces |
911 | /// another frame to be scheduled, even if the current frame has not yet |
912 | /// completed. |
913 | /// |
914 | /// Scheduled frames are serviced when triggered by a "Vsync" signal provided |
915 | /// by the operating system. The "Vsync" signal, or vertical synchronization |
916 | /// signal, was historically related to the display refresh, at a time when |
917 | /// hardware physically moved a beam of electrons vertically between updates |
918 | /// of the display. The operation of contemporary hardware is somewhat more |
919 | /// subtle and complicated, but the conceptual "Vsync" refresh signal continue |
920 | /// to be used to indicate when applications should update their rendering. |
921 | /// |
922 | /// To have a stack trace printed to the console any time this function |
923 | /// schedules a frame, set [debugPrintScheduleFrameStacks] to true. |
924 | /// |
925 | /// See also: |
926 | /// |
927 | /// * [scheduleForcedFrame], which ignores the [lifecycleState] when |
928 | /// scheduling a frame. |
929 | /// * [scheduleWarmUpFrame], which ignores the "Vsync" signal entirely and |
930 | /// triggers a frame immediately. |
931 | void scheduleFrame() { |
932 | if (_hasScheduledFrame || !framesEnabled) { |
933 | return; |
934 | } |
935 | assert(() { |
936 | if (debugPrintScheduleFrameStacks) { |
937 | debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.' ); |
938 | } |
939 | return true; |
940 | }()); |
941 | ensureFrameCallbacksRegistered(); |
942 | platformDispatcher.scheduleFrame(); |
943 | _hasScheduledFrame = true; |
944 | } |
945 | |
946 | /// Schedules a new frame by calling |
947 | /// [dart:ui.PlatformDispatcher.scheduleFrame]. |
948 | /// |
949 | /// After this is called, the engine will call [handleBeginFrame], even if |
950 | /// frames would normally not be scheduled by [scheduleFrame] (e.g. even if |
951 | /// the device's screen is turned off). |
952 | /// |
953 | /// The framework uses this to force a frame to be rendered at the correct |
954 | /// size when the phone is rotated, so that a correctly-sized rendering is |
955 | /// available when the screen is turned back on. |
956 | /// |
957 | /// To have a stack trace printed to the console any time this function |
958 | /// schedules a frame, set [debugPrintScheduleFrameStacks] to true. |
959 | /// |
960 | /// Prefer using [scheduleFrame] unless it is imperative that a frame be |
961 | /// scheduled immediately, since using [scheduleForcedFrame] will cause |
962 | /// significantly higher battery usage when the device should be idle. |
963 | /// |
964 | /// Consider using [scheduleWarmUpFrame] instead if the goal is to update the |
965 | /// rendering as soon as possible (e.g. at application startup). |
966 | void scheduleForcedFrame() { |
967 | if (_hasScheduledFrame) { |
968 | return; |
969 | } |
970 | assert(() { |
971 | if (debugPrintScheduleFrameStacks) { |
972 | debugPrintStack(label: 'scheduleForcedFrame() called. Current phase is $schedulerPhase.' ); |
973 | } |
974 | return true; |
975 | }()); |
976 | ensureFrameCallbacksRegistered(); |
977 | platformDispatcher.scheduleFrame(); |
978 | _hasScheduledFrame = true; |
979 | } |
980 | |
981 | bool _warmUpFrame = false; |
982 | |
983 | /// Schedule a frame to run as soon as possible, rather than waiting for |
984 | /// the engine to request a frame in response to a system "Vsync" signal. |
985 | /// |
986 | /// This is used during application startup so that the first frame (which is |
987 | /// likely to be quite expensive) gets a few extra milliseconds to run. |
988 | /// |
989 | /// Locks events dispatching until the scheduled frame has completed. |
990 | /// |
991 | /// If a frame has already been scheduled with [scheduleFrame] or |
992 | /// [scheduleForcedFrame], this call may delay that frame. |
993 | /// |
994 | /// If any scheduled frame has already begun or if another |
995 | /// [scheduleWarmUpFrame] was already called, this call will be ignored. |
996 | /// |
997 | /// Prefer [scheduleFrame] to update the display in normal operation. |
998 | /// |
999 | /// ## Design discussion |
1000 | /// |
1001 | /// The Flutter engine prompts the framework to generate frames when it |
1002 | /// receives a request from the operating system (known for historical reasons |
1003 | /// as a vsync). However, this may not happen for several milliseconds after |
1004 | /// the app starts (or after a hot reload). To make use of the time between |
1005 | /// when the widget tree is first configured and when the engine requests an |
1006 | /// update, the framework schedules a _warm-up frame_. |
1007 | /// |
1008 | /// A warm-up frame may never actually render (as the engine did not request |
1009 | /// it and therefore does not have a valid context in which to paint), but it |
1010 | /// will cause the framework to go through the steps of building, laying out, |
1011 | /// and painting, which can together take several milliseconds. Thus, when the |
1012 | /// engine requests a real frame, much of the work will already have been |
1013 | /// completed, and the framework can generate the frame with minimal |
1014 | /// additional effort. |
1015 | /// |
1016 | /// Warm-up frames are scheduled by [runApp] on startup, and by |
1017 | /// [RendererBinding.performReassemble] during a hot reload. |
1018 | /// |
1019 | /// Warm-up frames are also scheduled when the framework is unblocked by a |
1020 | /// call to [RendererBinding.allowFirstFrame] (corresponding to a call to |
1021 | /// [RendererBinding.deferFirstFrame] that blocked the rendering). |
1022 | void scheduleWarmUpFrame() { |
1023 | if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle) { |
1024 | return; |
1025 | } |
1026 | |
1027 | _warmUpFrame = true; |
1028 | TimelineTask? debugTimelineTask; |
1029 | if (!kReleaseMode) { |
1030 | debugTimelineTask = TimelineTask()..start('Warm-up frame' ); |
1031 | } |
1032 | final bool hadScheduledFrame = _hasScheduledFrame; |
1033 | PlatformDispatcher.instance.scheduleWarmUpFrame( |
1034 | beginFrame: () { |
1035 | assert(_warmUpFrame); |
1036 | handleBeginFrame(null); |
1037 | }, |
1038 | drawFrame: () { |
1039 | assert(_warmUpFrame); |
1040 | handleDrawFrame(); |
1041 | // We call resetEpoch after this frame so that, in the hot reload case, |
1042 | // the very next frame pretends to have occurred immediately after this |
1043 | // warm-up frame. The warm-up frame's timestamp will typically be far in |
1044 | // the past (the time of the last real frame), so if we didn't reset the |
1045 | // epoch we would see a sudden jump from the old time in the warm-up frame |
1046 | // to the new time in the "real" frame. The biggest problem with this is |
1047 | // that implicit animations end up being triggered at the old time and |
1048 | // then skipping every frame and finishing in the new time. |
1049 | resetEpoch(); |
1050 | _warmUpFrame = false; |
1051 | if (hadScheduledFrame) { |
1052 | scheduleFrame(); |
1053 | } |
1054 | }, |
1055 | ); |
1056 | |
1057 | // Lock events so touch events etc don't insert themselves until the |
1058 | // scheduled frame has finished. |
1059 | lockEvents(() async { |
1060 | await endOfFrame; |
1061 | if (!kReleaseMode) { |
1062 | debugTimelineTask!.finish(); |
1063 | } |
1064 | }); |
1065 | } |
1066 | |
1067 | Duration? _firstRawTimeStampInEpoch; |
1068 | Duration _epochStart = Duration.zero; |
1069 | Duration _lastRawTimeStamp = Duration.zero; |
1070 | |
1071 | /// Prepares the scheduler for a non-monotonic change to how time stamps are |
1072 | /// calculated. |
1073 | /// |
1074 | /// Callbacks received from the scheduler assume that their time stamps are |
1075 | /// monotonically increasing. The raw time stamp passed to [handleBeginFrame] |
1076 | /// is monotonic, but the scheduler might adjust those time stamps to provide |
1077 | /// [timeDilation]. Without careful handling, these adjusts could cause time |
1078 | /// to appear to run backwards. |
1079 | /// |
1080 | /// The [resetEpoch] function ensures that the time stamps are monotonic by |
1081 | /// resetting the base time stamp used for future time stamp adjustments to the |
1082 | /// current value. For example, if the [timeDilation] decreases, rather than |
1083 | /// scaling down the [Duration] since the beginning of time, [resetEpoch] will |
1084 | /// ensure that we only scale down the duration since [resetEpoch] was called. |
1085 | /// |
1086 | /// Setting [timeDilation] calls [resetEpoch] automatically. You don't need to |
1087 | /// call [resetEpoch] yourself. |
1088 | void resetEpoch() { |
1089 | _epochStart = _adjustForEpoch(_lastRawTimeStamp); |
1090 | _firstRawTimeStampInEpoch = null; |
1091 | } |
1092 | |
1093 | /// Adjusts the given time stamp into the current epoch. |
1094 | /// |
1095 | /// This both offsets the time stamp to account for when the epoch started |
1096 | /// (both in raw time and in the epoch's own time line) and scales the time |
1097 | /// stamp to reflect the time dilation in the current epoch. |
1098 | /// |
1099 | /// These mechanisms together combine to ensure that the durations we give |
1100 | /// during frame callbacks are monotonically increasing. |
1101 | Duration _adjustForEpoch(Duration rawTimeStamp) { |
1102 | final Duration rawDurationSinceEpoch = _firstRawTimeStampInEpoch == null ? Duration.zero : rawTimeStamp - _firstRawTimeStampInEpoch!; |
1103 | return Duration(microseconds: (rawDurationSinceEpoch.inMicroseconds / timeDilation).round() + _epochStart.inMicroseconds); |
1104 | } |
1105 | |
1106 | /// The time stamp for the frame currently being processed. |
1107 | /// |
1108 | /// This is only valid while between the start of [handleBeginFrame] and the |
1109 | /// end of the corresponding [handleDrawFrame], i.e. while a frame is being |
1110 | /// produced. |
1111 | Duration get currentFrameTimeStamp { |
1112 | assert(_currentFrameTimeStamp != null); |
1113 | return _currentFrameTimeStamp!; |
1114 | } |
1115 | Duration? _currentFrameTimeStamp; |
1116 | |
1117 | /// The raw time stamp as provided by the engine to |
1118 | /// [dart:ui.PlatformDispatcher.onBeginFrame] for the frame currently being |
1119 | /// processed. |
1120 | /// |
1121 | /// Unlike [currentFrameTimeStamp], this time stamp is neither adjusted to |
1122 | /// offset when the epoch started nor scaled to reflect the [timeDilation] in |
1123 | /// the current epoch. |
1124 | /// |
1125 | /// On most platforms, this is a more or less arbitrary value, and should |
1126 | /// generally be ignored. On Fuchsia, this corresponds to the system-provided |
1127 | /// presentation time, and can be used to ensure that animations running in |
1128 | /// different processes are synchronized. |
1129 | Duration get currentSystemFrameTimeStamp { |
1130 | return _lastRawTimeStamp; |
1131 | } |
1132 | |
1133 | int _debugFrameNumber = 0; |
1134 | String? _debugBanner; |
1135 | |
1136 | // Whether the current engine frame needs to be postponed till after the |
1137 | // warm-up frame. |
1138 | // |
1139 | // Engine may begin a frame in the middle of the warm-up frame because the |
1140 | // warm-up frame is scheduled by timers while the engine frame is scheduled |
1141 | // by platform specific frame scheduler (e.g. `requestAnimationFrame` on the |
1142 | // web). When this happens, we let the warm-up frame finish, and postpone the |
1143 | // engine frame. |
1144 | bool _rescheduleAfterWarmUpFrame = false; |
1145 | |
1146 | void _handleBeginFrame(Duration rawTimeStamp) { |
1147 | if (_warmUpFrame) { |
1148 | // "begin frame" and "draw frame" must strictly alternate. Therefore |
1149 | // _rescheduleAfterWarmUpFrame cannot possibly be true here as it is |
1150 | // reset by _handleDrawFrame. |
1151 | assert(!_rescheduleAfterWarmUpFrame); |
1152 | _rescheduleAfterWarmUpFrame = true; |
1153 | return; |
1154 | } |
1155 | handleBeginFrame(rawTimeStamp); |
1156 | } |
1157 | |
1158 | void _handleDrawFrame() { |
1159 | if (_rescheduleAfterWarmUpFrame) { |
1160 | _rescheduleAfterWarmUpFrame = false; |
1161 | // Reschedule in a post-frame callback to allow the draw-frame phase of |
1162 | // the warm-up frame to finish. |
1163 | addPostFrameCallback((Duration timeStamp) { |
1164 | // Force an engine frame. |
1165 | // |
1166 | // We need to reset _hasScheduledFrame here because we cancelled the |
1167 | // original engine frame, and therefore did not run handleBeginFrame |
1168 | // who is responsible for resetting it. So if a frame callback set this |
1169 | // to true in the "begin frame" part of the warm-up frame, it will |
1170 | // still be true here and cause us to skip scheduling an engine frame. |
1171 | _hasScheduledFrame = false; |
1172 | scheduleFrame(); |
1173 | }, debugLabel: 'SchedulerBinding.scheduleFrame' ); |
1174 | return; |
1175 | } |
1176 | handleDrawFrame(); |
1177 | } |
1178 | |
1179 | final TimelineTask? _frameTimelineTask = kReleaseMode ? null : TimelineTask(); |
1180 | |
1181 | /// Called by the engine to prepare the framework to produce a new frame. |
1182 | /// |
1183 | /// This function calls all the transient frame callbacks registered by |
1184 | /// [scheduleFrameCallback]. It then returns, any scheduled microtasks are run |
1185 | /// (e.g. handlers for any [Future]s resolved by transient frame callbacks), |
1186 | /// and [handleDrawFrame] is called to continue the frame. |
1187 | /// |
1188 | /// If the given time stamp is null, the time stamp from the last frame is |
1189 | /// reused. |
1190 | /// |
1191 | /// To have a banner shown at the start of every frame in debug mode, set |
1192 | /// [debugPrintBeginFrameBanner] to true. The banner will be printed to the |
1193 | /// console using [debugPrint] and will contain the frame number (which |
1194 | /// increments by one for each frame), and the time stamp of the frame. If the |
1195 | /// given time stamp was null, then the string "warm-up frame" is shown |
1196 | /// instead of the time stamp. This allows frames eagerly pushed by the |
1197 | /// framework to be distinguished from those requested by the engine in |
1198 | /// response to the "Vsync" signal from the operating system. |
1199 | /// |
1200 | /// You can also show a banner at the end of every frame by setting |
1201 | /// [debugPrintEndFrameBanner] to true. This allows you to distinguish log |
1202 | /// statements printed during a frame from those printed between frames (e.g. |
1203 | /// in response to events or timers). |
1204 | void handleBeginFrame(Duration? rawTimeStamp) { |
1205 | _frameTimelineTask?.start('Frame' ); |
1206 | _firstRawTimeStampInEpoch ??= rawTimeStamp; |
1207 | _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp); |
1208 | if (rawTimeStamp != null) { |
1209 | _lastRawTimeStamp = rawTimeStamp; |
1210 | } |
1211 | |
1212 | assert(() { |
1213 | _debugFrameNumber += 1; |
1214 | |
1215 | if (debugPrintBeginFrameBanner || debugPrintEndFrameBanner) { |
1216 | final StringBuffer frameTimeStampDescription = StringBuffer(); |
1217 | if (rawTimeStamp != null) { |
1218 | _debugDescribeTimeStamp(_currentFrameTimeStamp!, frameTimeStampDescription); |
1219 | } else { |
1220 | frameTimeStampDescription.write('(warm-up frame)' ); |
1221 | } |
1222 | _debugBanner = '▄▄▄▄▄▄▄▄ Frame ${_debugFrameNumber.toString().padRight(7)} ${frameTimeStampDescription.toString().padLeft(18)} ▄▄▄▄▄▄▄▄' ; |
1223 | if (debugPrintBeginFrameBanner) { |
1224 | debugPrint(_debugBanner); |
1225 | } |
1226 | } |
1227 | return true; |
1228 | }()); |
1229 | |
1230 | assert(schedulerPhase == SchedulerPhase.idle); |
1231 | _hasScheduledFrame = false; |
1232 | try { |
1233 | // TRANSIENT FRAME CALLBACKS |
1234 | _frameTimelineTask?.start('Animate' ); |
1235 | _schedulerPhase = SchedulerPhase.transientCallbacks; |
1236 | final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks; |
1237 | _transientCallbacks = <int, _FrameCallbackEntry>{}; |
1238 | callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) { |
1239 | if (!_removedIds.contains(id)) { |
1240 | _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack); |
1241 | } |
1242 | }); |
1243 | _removedIds.clear(); |
1244 | } finally { |
1245 | _schedulerPhase = SchedulerPhase.midFrameMicrotasks; |
1246 | } |
1247 | } |
1248 | |
1249 | DartPerformanceMode? _performanceMode; |
1250 | int _numPerformanceModeRequests = 0; |
1251 | |
1252 | /// Request a specific [DartPerformanceMode]. |
1253 | /// |
1254 | /// Returns `null` if the request was not successful due to conflicting performance mode requests. |
1255 | /// Two requests are said to be in conflict if they are not of the same [DartPerformanceMode] type, |
1256 | /// and an explicit request for a performance mode has been made prior. |
1257 | /// |
1258 | /// Requestor is responsible for calling [PerformanceModeRequestHandle.dispose] when it no longer |
1259 | /// requires the performance mode. |
1260 | PerformanceModeRequestHandle? requestPerformanceMode(DartPerformanceMode mode) { |
1261 | // conflicting requests are not allowed. |
1262 | if (_performanceMode != null && _performanceMode != mode) { |
1263 | return null; |
1264 | } |
1265 | |
1266 | if (_performanceMode == mode) { |
1267 | assert(_numPerformanceModeRequests > 0); |
1268 | _numPerformanceModeRequests++; |
1269 | } else if (_performanceMode == null) { |
1270 | assert(_numPerformanceModeRequests == 0); |
1271 | _performanceMode = mode; |
1272 | _numPerformanceModeRequests = 1; |
1273 | } |
1274 | |
1275 | return PerformanceModeRequestHandle._(_disposePerformanceModeRequest); |
1276 | } |
1277 | |
1278 | /// Remove a request for a specific [DartPerformanceMode]. |
1279 | /// |
1280 | /// If all the pending requests have been disposed, the engine will revert to the |
1281 | /// [DartPerformanceMode.balanced] performance mode. |
1282 | void _disposePerformanceModeRequest() { |
1283 | _numPerformanceModeRequests--; |
1284 | if (_numPerformanceModeRequests == 0) { |
1285 | _performanceMode = null; |
1286 | PlatformDispatcher.instance.requestDartPerformanceMode(DartPerformanceMode.balanced); |
1287 | } |
1288 | } |
1289 | |
1290 | /// Returns the current [DartPerformanceMode] requested or `null` if no requests have |
1291 | /// been made. |
1292 | /// |
1293 | /// This is only supported in debug and profile modes, returns `null` in release mode. |
1294 | DartPerformanceMode? debugGetRequestedPerformanceMode() { |
1295 | if (!(kDebugMode || kProfileMode)) { |
1296 | return null; |
1297 | } else { |
1298 | return _performanceMode; |
1299 | } |
1300 | } |
1301 | |
1302 | /// Called by the engine to produce a new frame. |
1303 | /// |
1304 | /// This method is called immediately after [handleBeginFrame]. It calls all |
1305 | /// the callbacks registered by [addPersistentFrameCallback], which typically |
1306 | /// drive the rendering pipeline, and then calls the callbacks registered by |
1307 | /// [addPostFrameCallback]. |
1308 | /// |
1309 | /// See [handleBeginFrame] for a discussion about debugging hooks that may be |
1310 | /// useful when working with frame callbacks. |
1311 | void handleDrawFrame() { |
1312 | assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks); |
1313 | _frameTimelineTask?.finish(); // end the "Animate" phase |
1314 | try { |
1315 | // PERSISTENT FRAME CALLBACKS |
1316 | _schedulerPhase = SchedulerPhase.persistentCallbacks; |
1317 | for (final FrameCallback callback in List<FrameCallback>.of(_persistentCallbacks)) { |
1318 | _invokeFrameCallback(callback, _currentFrameTimeStamp!); |
1319 | } |
1320 | |
1321 | // POST-FRAME CALLBACKS |
1322 | _schedulerPhase = SchedulerPhase.postFrameCallbacks; |
1323 | final List<FrameCallback> localPostFrameCallbacks = |
1324 | List<FrameCallback>.of(_postFrameCallbacks); |
1325 | _postFrameCallbacks.clear(); |
1326 | if (!kReleaseMode) { |
1327 | FlutterTimeline.startSync('POST_FRAME' ); |
1328 | } |
1329 | try { |
1330 | for (final FrameCallback callback in localPostFrameCallbacks) { |
1331 | _invokeFrameCallback(callback, _currentFrameTimeStamp!); |
1332 | } |
1333 | } finally { |
1334 | if (!kReleaseMode) { |
1335 | FlutterTimeline.finishSync(); |
1336 | } |
1337 | } |
1338 | } finally { |
1339 | _schedulerPhase = SchedulerPhase.idle; |
1340 | _frameTimelineTask?.finish(); // end the Frame |
1341 | assert(() { |
1342 | if (debugPrintEndFrameBanner) { |
1343 | debugPrint('▀' * _debugBanner!.length); |
1344 | } |
1345 | _debugBanner = null; |
1346 | return true; |
1347 | }()); |
1348 | _currentFrameTimeStamp = null; |
1349 | } |
1350 | } |
1351 | |
1352 | void _profileFramePostEvent(FrameTiming frameTiming) { |
1353 | postEvent('Flutter.Frame' , <String, dynamic>{ |
1354 | 'number' : frameTiming.frameNumber, |
1355 | 'startTime' : frameTiming.timestampInMicroseconds(FramePhase.buildStart), |
1356 | 'elapsed' : frameTiming.totalSpan.inMicroseconds, |
1357 | 'build' : frameTiming.buildDuration.inMicroseconds, |
1358 | 'raster' : frameTiming.rasterDuration.inMicroseconds, |
1359 | 'vsyncOverhead' : frameTiming.vsyncOverhead.inMicroseconds, |
1360 | }); |
1361 | } |
1362 | |
1363 | static void _debugDescribeTimeStamp(Duration timeStamp, StringBuffer buffer) { |
1364 | if (timeStamp.inDays > 0) { |
1365 | buffer.write(' ${timeStamp.inDays}d ' ); |
1366 | } |
1367 | if (timeStamp.inHours > 0) { |
1368 | buffer.write(' ${timeStamp.inHours - timeStamp.inDays * Duration.hoursPerDay}h ' ); |
1369 | } |
1370 | if (timeStamp.inMinutes > 0) { |
1371 | buffer.write(' ${timeStamp.inMinutes - timeStamp.inHours * Duration.minutesPerHour}m ' ); |
1372 | } |
1373 | if (timeStamp.inSeconds > 0) { |
1374 | buffer.write(' ${timeStamp.inSeconds - timeStamp.inMinutes * Duration.secondsPerMinute}s ' ); |
1375 | } |
1376 | buffer.write(' ${timeStamp.inMilliseconds - timeStamp.inSeconds * Duration.millisecondsPerSecond}' ); |
1377 | final int microseconds = timeStamp.inMicroseconds - timeStamp.inMilliseconds * Duration.microsecondsPerMillisecond; |
1378 | if (microseconds > 0) { |
1379 | buffer.write('. ${microseconds.toString().padLeft(3, "0" )}' ); |
1380 | } |
1381 | buffer.write('ms' ); |
1382 | } |
1383 | |
1384 | // Calls the given [callback] with [timestamp] as argument. |
1385 | // |
1386 | // Wraps the callback in a try/catch and forwards any error to |
1387 | // [debugSchedulerExceptionHandler], if set. If not set, prints |
1388 | // the error. |
1389 | @pragma('vm:notify-debugger-on-exception' ) |
1390 | void _invokeFrameCallback(FrameCallback callback, Duration timeStamp, [ StackTrace? callbackStack ]) { |
1391 | assert(_FrameCallbackEntry.debugCurrentCallbackStack == null); |
1392 | assert(() { |
1393 | _FrameCallbackEntry.debugCurrentCallbackStack = callbackStack; |
1394 | return true; |
1395 | }()); |
1396 | try { |
1397 | callback(timeStamp); |
1398 | } catch (exception, exceptionStack) { |
1399 | FlutterError.reportError(FlutterErrorDetails( |
1400 | exception: exception, |
1401 | stack: exceptionStack, |
1402 | library: 'scheduler library' , |
1403 | context: ErrorDescription('during a scheduler callback' ), |
1404 | informationCollector: (callbackStack == null) ? null : () { |
1405 | return <DiagnosticsNode>[ |
1406 | DiagnosticsStackTrace( |
1407 | '\nThis exception was thrown in the context of a scheduler callback. ' |
1408 | 'When the scheduler callback was _registered_ (as opposed to when the ' |
1409 | 'exception was thrown), this was the stack' , |
1410 | callbackStack, |
1411 | ), |
1412 | ]; |
1413 | }, |
1414 | )); |
1415 | } |
1416 | assert(() { |
1417 | _FrameCallbackEntry.debugCurrentCallbackStack = null; |
1418 | return true; |
1419 | }()); |
1420 | } |
1421 | } |
1422 | |
1423 | /// The default [SchedulingStrategy] for [SchedulerBinding.schedulingStrategy]. |
1424 | /// |
1425 | /// If there are any frame callbacks registered, only runs tasks with |
1426 | /// a [Priority] of [Priority.animation] or higher. Otherwise, runs |
1427 | /// all tasks. |
1428 | bool defaultSchedulingStrategy({ required int priority, required SchedulerBinding scheduler }) { |
1429 | if (scheduler.transientCallbackCount > 0) { |
1430 | return priority >= Priority.animation.value; |
1431 | } |
1432 | return true; |
1433 | } |
1434 | |