Skip to content

fix: Tasks scheduled through 'SchedulerBinding.instance.scheduleTask'… - #82781

Closed
Nayuta403 wants to merge 16 commits into
flutter:masterfrom
Nayuta403:patch-1
Closed

Nayuta403 wants to merge 16 commits into
flutter:masterfrom
Nayuta403:patch-1

Conversation

@Nayuta403

Copy link
Copy Markdown
Contributor

Related Issues

#82016

Description

This PR is for solving that tasks scheduled through 'SchedulerBinding.instance.scheduleTask' were not executed.
Before modification, if a task has a priority of idle, it will not be scheduled when the on-screen animation ends.
The problem is that once the 'handleEventLoopCallback()' returns false, the _TaskQueue will not be scheduled again until the next scheduleTask.
Therefore, when 'handleEventLoopCallback()' returns false, it is determined whether the queue is empty or not to continue scheduling.
To avoid frequent scheduling, wait until the next frame when 'handleEventLoopCallback() 'returns false (because I assume that by default the return value of 'handleEventLoopCallback()' does not change within the same frame).

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@flutter-dashboard flutter-dashboard Bot added the framework flutter/packages/flutter repository. See also f: labels. label May 18, 2021
@google-cla google-cla Bot added the cla: yes label May 18, 2021
@goderbauer

Copy link
Copy Markdown
Member

Can you take a look at the failing tests?

@Nayuta403

Copy link
Copy Markdown
Contributor Author

After debugging, I found out why a single test passes, but the entire file fails . Due to the following test cases of _hasRequestedAnEventLoopCallback turned out to be true, so my test cases timeout happened

test('2 calls to scheduleWarmUpFrame just schedules it once', () {
final List<VoidCallback> timerQueueTasks = <VoidCallback>[];
bool taskExecuted = false;
runZoned<void>(
() {
// Run it twice without processing the queued tasks.
scheduler.scheduleWarmUpFrame();
scheduler.scheduleWarmUpFrame();
scheduler.scheduleTask(() { taskExecuted = true; }, Priority.touch);
},
zoneSpecification: ZoneSpecification(
createTimer: (Zone self, ZoneDelegate parent, Zone zone, Duration duration, void Function() f) {
// Don't actually run the tasks, just record that it was scheduled.
timerQueueTasks.add(f);
return DummyTimer();
},
),
);
// scheduleWarmUpFrame scheduled 2 Timers, scheduleTask scheduled 0 because
// events are locked.
expect(timerQueueTasks.length, 2);
expect(taskExecuted, false);
// Run the timers so that the scheduler is no longer in warm-up state.
for (final VoidCallback timer in timerQueueTasks) {
timer();
}
});

Here is my debugging process

I just kept

test('2 calls to scheduleWarmUpFrame just schedules it once', () {
final List<VoidCallback> timerQueueTasks = <VoidCallback>[];
bool taskExecuted = false;
runZoned<void>(
() {
// Run it twice without processing the queued tasks.
scheduler.scheduleWarmUpFrame();
scheduler.scheduleWarmUpFrame();
scheduler.scheduleTask(() { taskExecuted = true; }, Priority.touch);
},
zoneSpecification: ZoneSpecification(
createTimer: (Zone self, ZoneDelegate parent, Zone zone, Duration duration, void Function() f) {
// Don't actually run the tasks, just record that it was scheduled.
timerQueueTasks.add(f);
return DummyTimer();
},
),
);
// scheduleWarmUpFrame scheduled 2 Timers, scheduleTask scheduled 0 because
// events are locked.
expect(timerQueueTasks.length, 2);
expect(taskExecuted, false);
// Run the timers so that the scheduler is no longer in warm-up state.
for (final VoidCallback timer in timerQueueTasks) {
timer();
}
});

and my test cases, and added log at the start and end of the task

The problem is that when the above test finishes, _ensureEventLoopCallback() is called, but _runTasks in Timer.run(_runTasks) are not executed. This led to the _hasRequestedAnEventLoopCallback is true, and _taskQueue is not empty. In executed after my test, _ensureEventLoopCallback () with _hasRequestedAnEventLoopCallback jam, so _runTasks hasn't been scheduling, never task execution. My test is waiting for the task to execute, so the timeout occurs

image

I dont know why _runTasks in Timer.run(_runTasks) is not executed in the above example, what do you think
@goderbauer

@dnfield

dnfield commented May 28, 2021

Copy link
Copy Markdown
Contributor

Sounds like we need some cleanup method at the end of each test

@Nayuta403

Nayuta403 commented May 29, 2021 •

Copy link
Copy Markdown
Contributor Author

Yeah,I think there are two problems One is Why is _ensureEventLoopCallback() called after this test is complete, and _runTasks in Timer.run (_runTasks) not executed. The second is how to set the reset method, if at all.

@Nayuta403

Copy link
Copy Markdown
Contributor Author

Let me add, the results of the run with that test case removed

image

Comment thread packages/flutter/lib/src/scheduler/binding.dart Outdated
Comment thread packages/flutter/lib/src/scheduler/binding.dart Outdated
Comment thread packages/flutter/lib/src/scheduler/binding.dart Outdated
);
controller.forward();

// If the task is not scheduled round-robin, this code will block the test, test will be timeout

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having this time out is not great. Can this run a a fake async zone where we can control time to ensure that the task will execute after the expected time has elapsed?

@Nayuta403 Nayuta403 Jun 22, 2021 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have fixed the other issues, but when writing the test using the FakeAsync Zone, the test failed. After debugging, I found that the Timer. run in FakeAsync will be immediately added to the _timers set, and the tasks in the set will be processed synchronously while _firetimersWhile, so the EventLoop will schedule and add the tasks in a cycle. No other code will continue to run. But this is not the same as actually running on the device.

The following related code:

class FakeAsync {

  /// Creates a new timer controlled by `this` that fires [callback] after
  /// [duration] (or every [duration] if [periodic] is `true`).
  Timer _createTimer(Duration duration, Function callback, bool periodic) {
    var timer = FakeTimer._(duration, callback, periodic, this);
    _timers.add(timer);
    return timer;
  }
}

  /// Invoke the callback for each timer until [predicate] returns `false` for
  /// the next timer that would be fired.
  ///
  /// Microtasks are flushed before and after each timer is fired. Before each
  /// timer fires, [_elapsed] is updated to the appropriate duration.
  void _fireTimersWhile(bool Function(FakeTimer timer) predicate) {
    flushMicrotasks();
    for (;;) {
      if (_timers.isEmpty) break;

      var timer = minBy(_timers, (FakeTimer timer) => timer._nextCall)!;
      if (!predicate(timer)) break;

      _elapseTo(timer._nextCall);
      timer._fire();
      flushMicrotasks();
    }
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc  when you have time~ thanks  @goderbauer @Hixie

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully understand what you mean by this. It looks like the timeout I was concerned about is now gone from the test?

@Nayuta403 Nayuta403 Jul 18, 2021 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is not an EventLoop, timeout appears in the following line of code
await scheduler.scheduleTask(() => taskExecuted = true, Priority.idle);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because in the original code, once 'schedulingStrategy' has returned the fase, it will not be executed regardless of whether there is a queue left in the _taskQueue, so this task will never be executed, causing the test to time out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to use FakeAsync to control the animation time, but the Timer.run in FakeAsync is a synchronous queue, which causes the system to execute Timer.run(_runTask) in a loop, thus timed out. So I can only write test cases in this way in the end. If you have better ideas, can you give me some suggestions

@flutter-dashboard

Copy link
Copy Markdown

This pull request executed golden file tests, but it has not been updated in a while (20+ days). Test results from Gold expire after as many days, so this pull request will need to be updated with a fresh commit in order to get results from Gold.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

_hasRequestedAnEventLoopCallback = false;
if (handleEventLoopCallback())
_ensureEventLoopCallback(); // runs next task when there's time
if (handleEventLoopCallback() || _taskQueue.isNotEmpty) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this can be simplified since there isn't really a case where handleEventLoopCallback returns true and _taskQueue.isNotEmpty is false, which means only _taskQueue.isNotEmpty decides here whether we queue up another event loop callback.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this doesn't look correct: If handleEventLoopCallback returns false because we're locked (see line 457) we are not allowed to call _ensureEventLoopCallback because of the assert(!locked) in that method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I also think it doesn't look correct with the previous change. After I thought it over and over again, I think the problem is inside the handleEventLoopCallback.
Because
image

According to the source code, The handleEventLoopCallback returns false in three cases:

  • _taskQueue.isEmpty || locked
  • schedulingStrategy() return true, and _taskQueue becomes empty after performing a task
  • schedulingStrategy() return false , although _taskQueue is not empty

I think the third case is problematic, because going to the third logic says the _taskQueue is not empty and locked is false . This indicates that a task still needs to be executed, except that this time the schedulingStrategy is not satisfied. It should return true to continue the _ensureEventLoopCallback like this.

image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems incorrect to me, since it can cause tasks to fire even if the locked is true.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was something wrong with my previous revision, you can check out my latest comment .
https://github.com/flutter/flutter/pull/82781/files#r671808835

I will revert the previous changes. And I think the problem is just that handleEventLoopCallback .

If locked is true, this method will return fasle at the first step and the event will not be executed
image


/// When a scheduling error occurs, resets the relative state of EventLoop.
@visibleForTesting
void resetEventLoop() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call this debugResetEventLoop to make it clear that this shouldn't be called by an app and make it only do something when asserts are enabled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'll try to change it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done you can review it

Comment thread packages/flutter/test/scheduler/scheduler_test.dart Outdated
/// When a scheduling error occurs, resets the relative state of EventLoop.
@visibleForTesting
void resetEventLoop() {
_hasRequestedAnEventLoopCallback = false;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is odd though, because the scheduled Timer.run would still execute...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This flag _hasRequestedAnEventLoopCallback was corrected by the previous test error, as you can see from my previous debugging procedure

#82781 (comment)

);
controller.forward();

// If the task is not scheduled round-robin, this code will block the test, test will be timeout

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully understand what you mean by this. It looks like the timeout I was concerned about is now gone from the test?

@goderbauer

Copy link
Copy Markdown
Member

Can you rebase this to the latest master to get the checks to pass?

@Nayuta403

Nayuta403 commented Jul 18, 2021 •

Copy link
Copy Markdown
Contributor Author

Should I re-open a PR after the discussion?

@Nayuta403

Copy link
Copy Markdown
Contributor Author

Can you rebase this to the latest master to get the checks to pass?

@goderbauer have time to cc

@Nayuta403
Nayuta403 requested a review from goderbauer July 28, 2021 15:25
@goderbauer

Copy link
Copy Markdown
Member

All you have to do to rebase (and get the tests running again) is probably a git pull upstream master --rebase followed by git push origin patch-1.

@dnfield

dnfield commented Aug 16, 2021

Copy link
Copy Markdown
Contributor

It looks like this needs a rebase/merge again.


test('The task was successfully executed after the animation', () async {
scheduler.schedulingStrategy = defaultSchedulingStrategy;
scheduler.debugResetEventLoop();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove this right? you get a new scheduler for each test.

It'd be best not to expose new API if we can avoid it.

@Nayuta403 Nayuta403 Aug 17, 2021 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have try to new scheduler , but it's TestSchedulerBinding class with BindingBase. So it can only initialize once. In the end, I had to do it this way

import 'scheduler_tester.dart';

class TestSchedulerBinding extends BindingBase with SchedulerBinding, ServicesBinding {
class TestSchedulerBinding extends BindingBase with SchedulerBinding, ServicesBinding, SemanticsBinding {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need semantics binding here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I have used the animation in the test, it needs the SemanticsBinding .

@dnfield dnfield left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comments. I'm sorry it took me so long to get to this, I was OOO for a bit.

@Nayuta403 Nayuta403 left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your reply @dnfield , this PR has been going on for so long that I almost forgot it 🥲. I'm going to rebase the master and modify the handleEventLoopCallback


test('The task was successfully executed after the animation', () async {
scheduler.schedulingStrategy = defaultSchedulingStrategy;
scheduler.debugResetEventLoop();

@Nayuta403 Nayuta403 Aug 17, 2021 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have try to new scheduler , but it's TestSchedulerBinding class with BindingBase. So it can only initialize once. In the end, I had to do it this way

import 'scheduler_tester.dart';

class TestSchedulerBinding extends BindingBase with SchedulerBinding, ServicesBinding {
class TestSchedulerBinding extends BindingBase with SchedulerBinding, ServicesBinding, SemanticsBinding {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I have used the animation in the test, it needs the SemanticsBinding .

_hasRequestedAnEventLoopCallback = false;
if (handleEventLoopCallback())
_ensureEventLoopCallback(); // runs next task when there's time
if (handleEventLoopCallback() || _taskQueue.isNotEmpty) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was something wrong with my previous revision, you can check out my latest comment .
https://github.com/flutter/flutter/pull/82781/files#r671808835

I will revert the previous changes. And I think the problem is just that handleEventLoopCallback .

If locked is true, this method will return fasle at the first step and the event will not be executed
image

@dnfield

dnfield commented Aug 17, 2021

Copy link
Copy Markdown
Contributor

I think we're hitting an impasse here. I'm going to close this PR to get it off of review queues for now - we can discuss this further on Discord if you want.

This PR in its current state cannot be merged. We cannot accept a test that relies on real timers running for a certain amount of time - it will not be clear if a timeout is due to a test failure or if infra is flaky, and the test will likely get ignored or disabled or deleted if it starts failing. It's also not clear to me that it's safe to remove the "locked" mechanism from the scheduler binding, which this PR wants to do.

@dnfield dnfield closed this Aug 17, 2021
@dnfield

dnfield commented Aug 17, 2021

Copy link
Copy Markdown
Contributor

Again I appreciate the time you've put into this, and I'm sorry for the length of time it took to get a review.

@Nayuta403

Copy link
Copy Markdown
Contributor Author

OK. Thanks again for your reply @dnfield ,I think the problem is pretty much fixed, but the test case does need to be reconsidered.I'll launch PR after I've perfected the test case,Thank you for your patient reply. ~

@Nayuta403
Nayuta403 deleted the patch-1 branch May 14, 2022 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants