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