[Fiber] Fix infinite loop in scheduler and add more tests (#8172)

* Make test more complete

* Add a failing test for scheduling in reverse order

It hangs forever because we don't clear next pointer when unscheduling a root. Therefore, when it's scheduled again, it brings all its previous chain with it, potentially creating a cycle.

* Clear the next pointer when unscheduling a root

Fixes a potential infinite cycle when we reschedule a root.

* Add new tests to Fiber test tracker
This commit is contained in:
Dan Abramov
2016-11-01 17:44:57 +00:00
committed by GitHub
parent ab02a03cad
commit 1a49b5f563
3 changed files with 64 additions and 4 deletions
+5
View File
@@ -794,6 +794,11 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js
* handles isMounted when an unmount is deferred
* finds no node before insertion and correct node before deletion
src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js
* schedules multiple roots
* works on deferred roots in the order they were scheduled
* works on roots with animation priority before deferred work
src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
* can update child nodes of a host instance
* can update child nodes of a fragment
@@ -77,14 +77,22 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
function findNextUnitOfWork() {
// Clear out roots with no more work on them.
while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) {
// Unschedule this root.
nextScheduledRoot.isScheduled = false;
// Read the next pointer now.
// We need to clear it in case this root gets scheduled again later.
const next = nextScheduledRoot.nextScheduledRoot;
nextScheduledRoot.nextScheduledRoot = null;
// Exit if we cleared all the roots and there's no work to do.
if (nextScheduledRoot === lastScheduledRoot) {
nextScheduledRoot = null;
lastScheduledRoot = null;
nextPriorityLevel = NoWork;
return null;
}
nextScheduledRoot = nextScheduledRoot.nextScheduledRoot;
// Continue with the next root.
// If there's no work on it, it will get unscheduled too.
nextScheduledRoot = next;
}
let root = nextScheduledRoot;
let highestPriorityRoot = null;
@@ -70,6 +70,36 @@ describe('ReactIncrementalScheduling', () => {
expect(ReactNoop.getChildren('a')).toEqual([span('a:6')]);
});
it('works on deferred roots in the order they were scheduled', () => {
ReactNoop.renderToRootWithID(<span prop="a:1" />, 'a');
ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b');
ReactNoop.renderToRootWithID(<span prop="c:1" />, 'c');
ReactNoop.flush();
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:1')]);
// Schedule deferred work in the reverse order
ReactNoop.renderToRootWithID(<span prop="c:2" />, 'c');
ReactNoop.renderToRootWithID(<span prop="b:2" />, 'b');
// Ensure it starts in the order it was scheduled
ReactNoop.flushDeferredPri(15);
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
// Schedule last bit of work, it will get processed the last
ReactNoop.renderToRootWithID(<span prop="a:2" />, 'a');
// Keep performing work in the order it was scheduled
ReactNoop.flushDeferredPri(15);
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
ReactNoop.flushDeferredPri(15);
expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
});
it('works on roots with animation priority before deferred work', () => {
ReactNoop.renderToRootWithID(<span prop="a:1" />, 'a');
ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b');
@@ -85,7 +115,8 @@ describe('ReactIncrementalScheduling', () => {
ReactNoop.renderToRootWithID(<span prop="b:2" />, 'b');
ReactNoop.renderToRootWithID(<span prop="c:2" />, 'c');
});
// Handle roots with animation work first
// We're flushing deferred work
// Still, roots with animation work are handled first
ReactNoop.flushDeferredPri(15);
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
@@ -94,10 +125,26 @@ describe('ReactIncrementalScheduling', () => {
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
// Then handle deferred root
// More deferred and animation work just got scheduled!
ReactNoop.renderToRootWithID(<span prop="c:3" />, 'c');
ReactNoop.performAnimationWork(() => {
ReactNoop.renderToRootWithID(<span prop="b:3" />, 'b');
});
// Animation is still handled first
ReactNoop.flushDeferredPri(15);
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:3')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
// Finally we handle deferred root in the order it was scheduled
ReactNoop.flushDeferredPri(15);
expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:3')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
ReactNoop.flushDeferredPri(15);
expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:3')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:3')]);
});
});