@@ -30,10 +30,26 @@ typedef bool SchedulingStrategy({ int priority, Scheduler scheduler });
3030///
3131/// Combines the task and its priority.
3232class _TaskEntry {
33+ const _TaskEntry (this .task, this .priority);
3334 final VoidCallback task;
3435 final int priority;
36+ }
3537
36- const _TaskEntry (this .task, this .priority);
38+ class _FrameCallbackEntry {
39+ _FrameCallbackEntry (this .callback, { bool rescheduling: false }) {
40+ assert (() {
41+ if (rescheduling) {
42+ assert (currentCallbackStack != null );
43+ stack = currentCallbackStack;
44+ } else {
45+ stack = StackTrace .current;
46+ }
47+ return true ;
48+ });
49+ }
50+ static StackTrace currentCallbackStack;
51+ final FrameCallback callback;
52+ StackTrace stack;
3753}
3854
3955class Priority {
@@ -141,7 +157,7 @@ abstract class Scheduler extends BindingBase {
141157 }
142158
143159 int _nextFrameCallbackId = 0 ; // positive
144- Map <int , FrameCallback > _transientCallbacks = < int , FrameCallback > {};
160+ Map <int , _FrameCallbackEntry > _transientCallbacks = < int , _FrameCallbackEntry > {};
145161 final Set <int > _removedIds = new HashSet <int >();
146162
147163 int get transientCallbackCount => _transientCallbacks.length;
@@ -150,9 +166,14 @@ abstract class Scheduler extends BindingBase {
150166 ///
151167 /// Adds the given callback to the list of frame-callbacks and ensures that a
152168 /// frame is scheduled.
153- int scheduleFrameCallback (FrameCallback callback) {
169+ ///
170+ /// If `rescheduling` is true, the call must be in the context of a
171+ /// frame callback, and for debugging purposes the stack trace
172+ /// stored for this callback will be the same stack trace as for the
173+ /// current callback.
174+ int scheduleFrameCallback (FrameCallback callback, { bool rescheduling: false }) {
154175 _ensureBeginFrameCallback ();
155- return addFrameCallback (callback);
176+ return addFrameCallback (callback, rescheduling : rescheduling );
156177 }
157178
158179 /// Adds a frame callback.
@@ -162,9 +183,18 @@ abstract class Scheduler extends BindingBase {
162183 ///
163184 /// The registered callbacks are executed in the order in which they have been
164185 /// registered.
165- int addFrameCallback (FrameCallback callback) {
186+ ///
187+ /// Callbacks registered with this method will not be invoked until
188+ /// a frame is requested. To register a callback and ensure that a
189+ /// frame is immediately scheduled, use [scheduleFrameCallback] .
190+ ///
191+ /// If `rescheduling` is true, the call must be in the context of a
192+ /// frame callback, and for debugging purposes the stack trace
193+ /// stored for this callback will be the same stack trace as for the
194+ /// current callback.
195+ int addFrameCallback (FrameCallback callback, { bool rescheduling: false }) {
166196 _nextFrameCallbackId += 1 ;
167- _transientCallbacks[_nextFrameCallbackId] = callback;
197+ _transientCallbacks[_nextFrameCallbackId] = new _FrameCallbackEntry ( callback, rescheduling : rescheduling) ;
168198 return _nextFrameCallbackId;
169199 }
170200
@@ -217,11 +247,11 @@ abstract class Scheduler extends BindingBase {
217247 void _invokeTransientFrameCallbacks (Duration timeStamp) {
218248 Timeline .startSync ('Animate' );
219249 assert (_debugInFrame);
220- Map <int , FrameCallback > callbacks = _transientCallbacks;
221- _transientCallbacks = new Map <int , FrameCallback >();
222- callbacks.forEach ((int id, FrameCallback callback ) {
250+ Map <int , _FrameCallbackEntry > callbacks = _transientCallbacks;
251+ _transientCallbacks = new Map <int , _FrameCallbackEntry >();
252+ callbacks.forEach ((int id, _FrameCallbackEntry callbackEntry ) {
223253 if (! _removedIds.contains (id))
224- invokeFrameCallback (callback, timeStamp);
254+ invokeFrameCallback (callbackEntry. callback, timeStamp, callbackEntry.stack );
225255 });
226256 _removedIds.clear ();
227257 Timeline .finishSync ();
@@ -264,18 +294,68 @@ abstract class Scheduler extends BindingBase {
264294 /// Wraps the callback in a try/catch and forwards any error to
265295 /// [debugSchedulerExceptionHandler] , if set. If not set, then simply prints
266296 /// the error.
267- void invokeFrameCallback (FrameCallback callback, Duration timeStamp) {
297+ ///
298+ /// Must not be called reentrantly from within a frame callback.
299+ void invokeFrameCallback (FrameCallback callback, Duration timeStamp, [ StackTrace stack ]) {
268300 assert (callback != null );
301+ assert (_FrameCallbackEntry .currentCallbackStack == null );
302+ assert (() { _FrameCallbackEntry .currentCallbackStack = stack; return true ; });
269303 try {
270304 callback (timeStamp);
271305 } catch (exception, stack) {
272306 FlutterError .reportError (new FlutterErrorDetails (
273307 exception: exception,
274308 stack: stack,
275309 library: 'scheduler library' ,
276- context: 'during a scheduler callback'
310+ context: 'during a scheduler callback' ,
311+ informationCollector: (stack == null ) ? null : (StringBuffer information) {
312+ information.writeln ('When this callback was registered, this was the stack:\n $stack ' );
313+ }
277314 ));
278315 }
316+ assert (() { _FrameCallbackEntry .currentCallbackStack = null ; return true ; });
317+ }
318+
319+ /// Asserts that there are no registered transient callbacks; if
320+ /// there are, prints their locations and throws an exception.
321+ ///
322+ /// This is expected to be called at the end of tests (the
323+ /// flutter_test framework does it automatically in normal cases).
324+ ///
325+ /// To invoke this method, call it, when you expect there to be no
326+ /// transient callbacks registered, in an assert statement with a
327+ /// message that you want printed when a transient callback is
328+ /// registered, as follows:
329+ ///
330+ /// ```dart
331+ /// assert(Scheduler.instance.debugAssertNoTransientCallbacks(
332+ /// 'A leak of transient callbacks was detected while doing foo.'
333+ /// ));
334+ /// ```
335+ ///
336+ /// Does nothing if asserts are disabled. Always returns true.
337+ bool debugAssertNoTransientCallbacks (String reason) {
338+ assert (() {
339+ if (transientCallbackCount > 0 ) {
340+ FlutterError .reportError (new FlutterErrorDetails (
341+ exception: reason,
342+ library: 'scheduler library' ,
343+ informationCollector: (StringBuffer information) {
344+ information.writeln (
345+ 'There ${ transientCallbackCount == 1 ? "was one transient callback" : "were $transientCallbackCount transient callbacks" } '
346+ 'left. The stack traces for when they were registered are as follows:'
347+ );
348+ for (int id in _transientCallbacks.keys) {
349+ _FrameCallbackEntry entry = _transientCallbacks[id];
350+ information.writeln ('-- callback $id --' );
351+ information.writeln (entry.stack);
352+ }
353+ }
354+ ));
355+ }
356+ return true ;
357+ });
358+ return true ;
279359 }
280360
281361 /// Ensures that the scheduler is woken by the event loop.
0 commit comments