Add yieldy, chained async task support to InteractionManager
Summary: Default behavior should be unchanged. If we queue up a bunch of expensive tasks during an interaction, the default `InteractionManager` behavior would execute them all in one synchronous loop at the end the JS event loop via one `setImmediate` call, blocking the JS thread the entire time. The `setDeadline` addition in this diff enables an option to only execute tasks until the `eventLoopRunningTime` is hit (added to MessageQueue/BatchedBridge), allowing the queue execution to be paused if an interaction starts in between tasks, making the app more responsive. Additionally, if a task ends up generating a bunch of additional tasks asynchronously, the previous implementation would execute these new tasks after already scheduled tasks. This is often fine, but I want it to fully resolve async tasks and all their dependencies before making progress in the rest of the queue, so I added support for `type PromiseTask = {gen: () => Promise}` to do just this. It works by building a stack of queues each time a `PromiseTask` is started, and pops them off the stack once they are resolved and the queues are processed. I also pulled all of the actual queue logic out of `InteractionManager` and into a new `TaskQueue` class to isolate concerns a bit. public Reviewed By: josephsavona Differential Revision: D2754311 fb-gh-sync-id: bfd6d0c54e6410cb261aa1d2c5024dd91a3959e6
This commit is contained in:
parent
f7de9678d4
commit
893a54d0cd
|
@ -11,23 +11,19 @@
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var ErrorUtils = require('ErrorUtils');
|
const BatchedBridge = require('BatchedBridge');
|
||||||
var EventEmitter = require('EventEmitter');
|
const EventEmitter = require('EventEmitter');
|
||||||
var Set = require('Set');
|
const Set = require('Set');
|
||||||
|
const TaskQueue = require('TaskQueue');
|
||||||
|
|
||||||
var invariant = require('invariant');
|
const invariant = require('invariant');
|
||||||
var keyMirror = require('keyMirror');
|
const keyMirror = require('keyMirror');
|
||||||
var setImmediate = require('setImmediate');
|
const setImmediate = require('setImmediate');
|
||||||
|
|
||||||
type Handle = number;
|
type Handle = number;
|
||||||
|
import type {Task} from 'TaskQueue';
|
||||||
|
|
||||||
var _emitter = new EventEmitter();
|
const _emitter = new EventEmitter();
|
||||||
var _interactionSet = new Set();
|
|
||||||
var _addInteractionSet = new Set();
|
|
||||||
var _deleteInteractionSet = new Set();
|
|
||||||
var _nextUpdateHandle = null;
|
|
||||||
var _queue = [];
|
|
||||||
var _inc = 0;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* InteractionManager allows long-running work to be scheduled after any
|
* InteractionManager allows long-running work to be scheduled after any
|
||||||
|
@ -63,6 +59,20 @@ var _inc = 0;
|
||||||
* InteractionManager.clearInteractionHandle(handle);
|
* InteractionManager.clearInteractionHandle(handle);
|
||||||
* // queued tasks run if all handles were cleared
|
* // queued tasks run if all handles were cleared
|
||||||
* ```
|
* ```
|
||||||
|
*
|
||||||
|
* `runAfterInteractions` takes either a plain callback function, or a
|
||||||
|
* `PromiseTask` object with a `gen` method that returns a `Promise`. If a
|
||||||
|
* `PromiseTask` is supplied, then it is fully resolved (including asynchronous
|
||||||
|
* dependencies that also schedule more tasks via `runAfterInteractions`) before
|
||||||
|
* starting on the next task that might have been queued up synchronously
|
||||||
|
* earlier.
|
||||||
|
*
|
||||||
|
* By default, queued tasks are executed together in a loop in one
|
||||||
|
* `setImmediate` batch. If `setDeadline` is called with a positive number, then
|
||||||
|
* tasks will only be executed until the deadline (in terms of js event loop run
|
||||||
|
* time) approaches, at which point execution will yield via setTimeout,
|
||||||
|
* allowing events such as touches to start interactions and block queued tasks
|
||||||
|
* from executing, making apps more responsive.
|
||||||
*/
|
*/
|
||||||
var InteractionManager = {
|
var InteractionManager = {
|
||||||
Events: keyMirror({
|
Events: keyMirror({
|
||||||
|
@ -73,13 +83,14 @@ var InteractionManager = {
|
||||||
/**
|
/**
|
||||||
* Schedule a function to run after all interactions have completed.
|
* Schedule a function to run after all interactions have completed.
|
||||||
*/
|
*/
|
||||||
runAfterInteractions(callback: ?Function): Promise {
|
runAfterInteractions(task: ?Task): Promise {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
scheduleUpdate();
|
_scheduleUpdate();
|
||||||
if (callback) {
|
if (task) {
|
||||||
_queue.push(callback);
|
_taskQueue.enqueue(task);
|
||||||
}
|
}
|
||||||
_queue.push(resolve);
|
const name = task && task.name || '?';
|
||||||
|
_taskQueue.enqueue({run: resolve, name: 'resolve ' + name});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -87,7 +98,7 @@ var InteractionManager = {
|
||||||
* Notify manager that an interaction has started.
|
* Notify manager that an interaction has started.
|
||||||
*/
|
*/
|
||||||
createInteractionHandle(): Handle {
|
createInteractionHandle(): Handle {
|
||||||
scheduleUpdate();
|
_scheduleUpdate();
|
||||||
var handle = ++_inc;
|
var handle = ++_inc;
|
||||||
_addInteractionSet.add(handle);
|
_addInteractionSet.add(handle);
|
||||||
return handle;
|
return handle;
|
||||||
|
@ -101,28 +112,49 @@ var InteractionManager = {
|
||||||
!!handle,
|
!!handle,
|
||||||
'Must provide a handle to clear.'
|
'Must provide a handle to clear.'
|
||||||
);
|
);
|
||||||
scheduleUpdate();
|
_scheduleUpdate();
|
||||||
_addInteractionSet.delete(handle);
|
_addInteractionSet.delete(handle);
|
||||||
_deleteInteractionSet.add(handle);
|
_deleteInteractionSet.add(handle);
|
||||||
},
|
},
|
||||||
|
|
||||||
addListener: _emitter.addListener.bind(_emitter),
|
addListener: _emitter.addListener.bind(_emitter),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A positive number will use setTimeout to schedule any tasks after the
|
||||||
|
* eventLoopRunningTime hits the deadline value, otherwise all tasks will be
|
||||||
|
* executed in one setImmediate batch (default).
|
||||||
|
*/
|
||||||
|
setDeadline(deadline: number) {
|
||||||
|
_deadline = deadline;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _interactionSet = new Set();
|
||||||
|
const _addInteractionSet = new Set();
|
||||||
|
const _deleteInteractionSet = new Set();
|
||||||
|
const _taskQueue = new TaskQueue({onMoreTasks: _scheduleUpdate});
|
||||||
|
let _nextUpdateHandle = 0;
|
||||||
|
let _inc = 0;
|
||||||
|
let _deadline = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule an asynchronous update to the interaction state.
|
* Schedule an asynchronous update to the interaction state.
|
||||||
*/
|
*/
|
||||||
function scheduleUpdate() {
|
function _scheduleUpdate() {
|
||||||
if (!_nextUpdateHandle) {
|
if (!_nextUpdateHandle) {
|
||||||
_nextUpdateHandle = setImmediate(processUpdate);
|
if (_deadline > 0) {
|
||||||
|
_nextUpdateHandle = setTimeout(_processUpdate, 0);
|
||||||
|
} else {
|
||||||
|
_nextUpdateHandle = setImmediate(_processUpdate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify listeners, process queue, etc
|
* Notify listeners, process queue, etc
|
||||||
*/
|
*/
|
||||||
function processUpdate() {
|
function _processUpdate() {
|
||||||
_nextUpdateHandle = null;
|
_nextUpdateHandle = 0;
|
||||||
|
|
||||||
var interactionCount = _interactionSet.size;
|
var interactionCount = _interactionSet.size;
|
||||||
_addInteractionSet.forEach(handle =>
|
_addInteractionSet.forEach(handle =>
|
||||||
|
@ -143,13 +175,16 @@ function processUpdate() {
|
||||||
|
|
||||||
// process the queue regardless of a transition
|
// process the queue regardless of a transition
|
||||||
if (nextInteractionCount === 0) {
|
if (nextInteractionCount === 0) {
|
||||||
var queue = _queue;
|
while (_taskQueue.hasTasksToProcess()) {
|
||||||
_queue = [];
|
_taskQueue.processNext();
|
||||||
queue.forEach(callback => {
|
if (_deadline > 0 &&
|
||||||
ErrorUtils.applyWithGuard(callback);
|
BatchedBridge.getEventLoopRunningTime() >= _deadline) {
|
||||||
});
|
// Hit deadline before processing all tasks, so process more later.
|
||||||
|
_scheduleUpdate();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_addInteractionSet.clear();
|
_addInteractionSet.clear();
|
||||||
_deleteInteractionSet.clear();
|
_deleteInteractionSet.clear();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @providesModule TaskQueue
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const ErrorUtils = require('ErrorUtils');
|
||||||
|
|
||||||
|
const invariant = require('invariant');
|
||||||
|
|
||||||
|
type SimpleTask = {
|
||||||
|
name: string;
|
||||||
|
run: () => void;
|
||||||
|
};
|
||||||
|
type PromiseTask = {
|
||||||
|
name: string;
|
||||||
|
gen: () => Promise;
|
||||||
|
};
|
||||||
|
export type Task = Function | SimpleTask | PromiseTask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TaskQueue - A system for queueing and executing a mix of simple callbacks and
|
||||||
|
* trees of dependent tasks based on Promises. No tasks are executed unless
|
||||||
|
* `processNext` is called.
|
||||||
|
*
|
||||||
|
* `enqueue` takes a Task object with either a simple `run` callback, or a
|
||||||
|
* `gen` function that returns a `Promise` and puts it in the queue. If a gen
|
||||||
|
* function is supplied, then the promise it returns will block execution of
|
||||||
|
* tasks already in the queue until it resolves. This can be used to make sure
|
||||||
|
* the first task is fully resolved (including asynchronous dependencies that
|
||||||
|
* also schedule more tasks via `enqueue`) before starting on the next task that
|
||||||
|
* might have been queued up earlier. The `onMoreTasks` constructor argument is
|
||||||
|
* used to inform the owner that an async task has resolved and that the queue
|
||||||
|
* should be processed again.
|
||||||
|
*
|
||||||
|
* Note: Tasks are only actually executed with explicit calls to `processNext`.
|
||||||
|
*/
|
||||||
|
class TaskQueue {
|
||||||
|
/**
|
||||||
|
* TaskQueue instances are self contained and independent, so multiple tasks
|
||||||
|
* of varying semantics and priority can operate together.
|
||||||
|
*
|
||||||
|
* `onMoreTasks` is invoked when `PromiseTask`s resolve if there are more
|
||||||
|
* tasks to process.
|
||||||
|
*/
|
||||||
|
constructor({onMoreTasks}: {onMoreTasks: () => void}) {
|
||||||
|
this._onMoreTasks = onMoreTasks;
|
||||||
|
this._queueStack = [{tasks: [], popable: false}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a task to the queue. It is recommended to name your tasks for easier
|
||||||
|
* async debugging. Tasks will not be executed until `processNext` is called
|
||||||
|
* explicitly.
|
||||||
|
*/
|
||||||
|
enqueue(task: Task): void {
|
||||||
|
this._getCurrentQueue().push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if `processNext` should be called.
|
||||||
|
*
|
||||||
|
* @returns {boolean} Returns true if there are tasks that are ready to be
|
||||||
|
* processed with `processNext`, or returns false if there are no more tasks
|
||||||
|
* to be processed right now, although there may be tasks in the queue that
|
||||||
|
* are blocked by earlier `PromiseTask`s that haven't resolved yet.
|
||||||
|
* `onMoreTasks` will be called after each `PromiseTask` resolves if there are
|
||||||
|
* tasks ready to run at that point.
|
||||||
|
*/
|
||||||
|
hasTasksToProcess(): bool {
|
||||||
|
return this._getCurrentQueue().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the next task in the queue.
|
||||||
|
*/
|
||||||
|
processNext(): void {
|
||||||
|
let queue = this._getCurrentQueue();
|
||||||
|
if (queue.length) {
|
||||||
|
const task = queue.shift();
|
||||||
|
try {
|
||||||
|
if (task.gen) {
|
||||||
|
this._genPromise((task: any)); // Rather than annoying tagged union
|
||||||
|
} else if (task.run) {
|
||||||
|
task.run();
|
||||||
|
} else {
|
||||||
|
invariant(
|
||||||
|
typeof task === 'function',
|
||||||
|
'Expected Function, SimpleTask, or PromiseTask, but got: ' +
|
||||||
|
JSON.stringify(task)
|
||||||
|
);
|
||||||
|
task();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
e.message = 'TaskQueue: Error with task' + (task.name || ' ') + ': ' +
|
||||||
|
e.message;
|
||||||
|
ErrorUtils.reportError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_queueStack: Array<{tasks: Array<Task>, popable: bool}>;
|
||||||
|
_onMoreTasks: () => void;
|
||||||
|
|
||||||
|
_getCurrentQueue(): Array<Task> {
|
||||||
|
const stackIdx = this._queueStack.length - 1;
|
||||||
|
const queue = this._queueStack[stackIdx];
|
||||||
|
if (queue.popable &&
|
||||||
|
queue.tasks.length === 0 &&
|
||||||
|
this._queueStack.length > 1) {
|
||||||
|
this._queueStack.pop();
|
||||||
|
return this._getCurrentQueue();
|
||||||
|
} else {
|
||||||
|
return queue.tasks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_genPromise(task: PromiseTask) {
|
||||||
|
// Each async task pushes it's own queue onto the queue stack. This
|
||||||
|
// effectively defers execution of previously queued tasks until the promise
|
||||||
|
// resolves, at which point we allow the new queue to be popped, which
|
||||||
|
// happens once it is fully processed.
|
||||||
|
this._queueStack.push({tasks: [], popable: false});
|
||||||
|
const stackIdx = this._queueStack.length - 1;
|
||||||
|
ErrorUtils.applyWithGuard(task.gen)
|
||||||
|
.then(() => {
|
||||||
|
this._queueStack[stackIdx].popable = true;
|
||||||
|
this.hasTasksToProcess() && this._onMoreTasks();
|
||||||
|
})
|
||||||
|
.catch((ex) => {
|
||||||
|
console.warn(
|
||||||
|
'TaskQueue: Error resolving Promise in task ' + task.name,
|
||||||
|
ex
|
||||||
|
);
|
||||||
|
throw ex;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = TaskQueue;
|
|
@ -0,0 +1,288 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2004-present Facebook. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
jest
|
||||||
|
.dontMock('InteractionManager')
|
||||||
|
.dontMock('TaskQueue')
|
||||||
|
.dontMock('invariant');
|
||||||
|
|
||||||
|
function expectToBeCalledOnce(fn) {
|
||||||
|
expect(fn.mock.calls.length).toBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('InteractionManager', () => {
|
||||||
|
let InteractionManager;
|
||||||
|
let interactionStart;
|
||||||
|
let interactionComplete;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModuleRegistry();
|
||||||
|
InteractionManager = require('InteractionManager');
|
||||||
|
|
||||||
|
interactionStart = jest.genMockFunction();
|
||||||
|
interactionComplete = jest.genMockFunction();
|
||||||
|
|
||||||
|
InteractionManager.addListener(
|
||||||
|
InteractionManager.Events.interactionStart,
|
||||||
|
interactionStart
|
||||||
|
);
|
||||||
|
InteractionManager.addListener(
|
||||||
|
InteractionManager.Events.interactionComplete,
|
||||||
|
interactionComplete
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when clearing an undefined handle', () => {
|
||||||
|
expect(() => InteractionManager.clearInteractionHandle()).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('notifies asynchronously when interaction starts', () => {
|
||||||
|
InteractionManager.createInteractionHandle();
|
||||||
|
expect(interactionStart).not.toBeCalled();
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(interactionStart).toBeCalled();
|
||||||
|
expect(interactionComplete).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('notifies asynchronously when interaction stops', () => {
|
||||||
|
var handle = InteractionManager.createInteractionHandle();
|
||||||
|
jest.runAllTimers();
|
||||||
|
interactionStart.mockClear();
|
||||||
|
InteractionManager.clearInteractionHandle(handle);
|
||||||
|
expect(interactionComplete).not.toBeCalled();
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(interactionStart).not.toBeCalled();
|
||||||
|
expect(interactionComplete).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not notify when started & stopped in same event loop', () => {
|
||||||
|
var handle = InteractionManager.createInteractionHandle();
|
||||||
|
InteractionManager.clearInteractionHandle(handle);
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(interactionStart).not.toBeCalled();
|
||||||
|
expect(interactionComplete).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not notify when going from two -> one active interactions', () => {
|
||||||
|
InteractionManager.createInteractionHandle();
|
||||||
|
var handle = InteractionManager.createInteractionHandle();
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
interactionStart.mockClear();
|
||||||
|
interactionComplete.mockClear();
|
||||||
|
|
||||||
|
InteractionManager.clearInteractionHandle(handle);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(interactionStart).not.toBeCalled();
|
||||||
|
expect(interactionComplete).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runs tasks asynchronously when there are interactions', () => {
|
||||||
|
var task = jest.genMockFunction();
|
||||||
|
InteractionManager.runAfterInteractions(task);
|
||||||
|
expect(task).not.toBeCalled();
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(task).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runs tasks when interactions complete', () => {
|
||||||
|
var task = jest.genMockFunction();
|
||||||
|
var handle = InteractionManager.createInteractionHandle();
|
||||||
|
InteractionManager.runAfterInteractions(task);
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
InteractionManager.clearInteractionHandle(handle);
|
||||||
|
expect(task).not.toBeCalled();
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(task).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not run tasks twice', () => {
|
||||||
|
var task1 = jest.genMockFunction();
|
||||||
|
var task2 = jest.genMockFunction();
|
||||||
|
InteractionManager.runAfterInteractions(task1);
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
InteractionManager.runAfterInteractions(task2);
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expectToBeCalledOnce(task1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runs tasks added while processing previous tasks', () => {
|
||||||
|
var task1 = jest.genMockFunction().mockImplementation(() => {
|
||||||
|
InteractionManager.runAfterInteractions(task2);
|
||||||
|
});
|
||||||
|
var task2 = jest.genMockFunction();
|
||||||
|
|
||||||
|
InteractionManager.runAfterInteractions(task1);
|
||||||
|
expect(task2).not.toBeCalled();
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(task1).toBeCalled();
|
||||||
|
expect(task2).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('promise tasks', () => {
|
||||||
|
let InteractionManager;
|
||||||
|
let BatchedBridge;
|
||||||
|
let sequenceId;
|
||||||
|
function createSequenceTask(expectedSequenceId) {
|
||||||
|
return jest.genMockFunction().mockImplementation(() => {
|
||||||
|
expect(++sequenceId).toBe(expectedSequenceId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModuleRegistry();
|
||||||
|
InteractionManager = require('InteractionManager');
|
||||||
|
BatchedBridge = require('BatchedBridge');
|
||||||
|
sequenceId = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should run a basic promise task', () => {
|
||||||
|
const task1 = jest.genMockFunction().mockImplementation(() => {
|
||||||
|
expect(++sequenceId).toBe(1);
|
||||||
|
return new Promise(resolve => resolve());
|
||||||
|
});
|
||||||
|
InteractionManager.runAfterInteractions({gen: task1, name: 'gen1'});
|
||||||
|
jest.runAllTimers();
|
||||||
|
expectToBeCalledOnce(task1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nested promises', () => {
|
||||||
|
const task1 = jest.genMockFunction().mockImplementation(() => {
|
||||||
|
expect(++sequenceId).toBe(1);
|
||||||
|
return new Promise(resolve => {
|
||||||
|
InteractionManager.runAfterInteractions({gen: task2, name: 'gen2'})
|
||||||
|
.then(resolve);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const task2 = jest.genMockFunction().mockImplementation(() => {
|
||||||
|
expect(++sequenceId).toBe(2);
|
||||||
|
return new Promise(resolve => resolve());
|
||||||
|
});
|
||||||
|
InteractionManager.runAfterInteractions({gen: task1, name: 'gen1'});
|
||||||
|
jest.runAllTimers();
|
||||||
|
expectToBeCalledOnce(task1);
|
||||||
|
expectToBeCalledOnce(task2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pause promise tasks during interactions then resume', () => {
|
||||||
|
const task1 = createSequenceTask(1);
|
||||||
|
const task2 = jest.genMockFunction().mockImplementation(() => {
|
||||||
|
expect(++sequenceId).toBe(2);
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
InteractionManager.runAfterInteractions(task3).then(resolve);
|
||||||
|
}, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const task3 = createSequenceTask(3);
|
||||||
|
InteractionManager.runAfterInteractions(task1);
|
||||||
|
InteractionManager.runAfterInteractions({gen: task2, name: 'gen2'});
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
expectToBeCalledOnce(task1);
|
||||||
|
expectToBeCalledOnce(task2);
|
||||||
|
const handle = InteractionManager.createInteractionHandle();
|
||||||
|
jest.runAllTimers();
|
||||||
|
jest.runAllTimers(); // Just to be sure...
|
||||||
|
expect(task3).not.toBeCalled();
|
||||||
|
InteractionManager.clearInteractionHandle(handle);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expectToBeCalledOnce(task3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute tasks in loop within deadline', () => {
|
||||||
|
InteractionManager.setDeadline(100);
|
||||||
|
BatchedBridge.getEventLoopRunningTime.mockReturnValue(10);
|
||||||
|
const task1 = createSequenceTask(1);
|
||||||
|
const task2 = createSequenceTask(2);
|
||||||
|
InteractionManager.runAfterInteractions(task1);
|
||||||
|
InteractionManager.runAfterInteractions(task2);
|
||||||
|
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
|
||||||
|
expectToBeCalledOnce(task1);
|
||||||
|
expectToBeCalledOnce(task2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute tasks one at a time if deadline exceeded', () => {
|
||||||
|
InteractionManager.setDeadline(100);
|
||||||
|
BatchedBridge.getEventLoopRunningTime.mockReturnValue(200);
|
||||||
|
const task1 = createSequenceTask(1);
|
||||||
|
const task2 = createSequenceTask(2);
|
||||||
|
InteractionManager.runAfterInteractions(task1);
|
||||||
|
InteractionManager.runAfterInteractions(task2);
|
||||||
|
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
|
||||||
|
expectToBeCalledOnce(task1);
|
||||||
|
expect(task2).not.toBeCalled();
|
||||||
|
|
||||||
|
jest.runOnlyPendingTimers(); // resolve1
|
||||||
|
jest.runOnlyPendingTimers(); // task2
|
||||||
|
|
||||||
|
expectToBeCalledOnce(task2);
|
||||||
|
});
|
||||||
|
|
||||||
|
const bigAsyncTest = () => {
|
||||||
|
const task1 = createSequenceTask(1);
|
||||||
|
const task2 = jest.genMockFunction().mockImplementation(() => {
|
||||||
|
expect(++sequenceId).toBe(2);
|
||||||
|
return new Promise(resolve => {
|
||||||
|
InteractionManager.runAfterInteractions(task3);
|
||||||
|
setTimeout(() => {
|
||||||
|
InteractionManager.runAfterInteractions({gen: task4, name: 'gen4'})
|
||||||
|
.then(resolve);
|
||||||
|
}, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const task3 = createSequenceTask(3);
|
||||||
|
const task4 = jest.genMockFunction().mockImplementation(() => {
|
||||||
|
expect(++sequenceId).toBe(4);
|
||||||
|
return new Promise(resolve => {
|
||||||
|
InteractionManager.runAfterInteractions(task5).then(resolve);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const task5 = createSequenceTask(5);
|
||||||
|
const task6 = createSequenceTask(6);
|
||||||
|
|
||||||
|
InteractionManager.runAfterInteractions(task1);
|
||||||
|
InteractionManager.runAfterInteractions({gen: task2, name: 'gen2'});
|
||||||
|
InteractionManager.runAfterInteractions(task6);
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
// runAllTimers doesn't actually run all timers with nested timer functions
|
||||||
|
// inside Promises, so we have to call it extra times.
|
||||||
|
jest.runAllTimers();
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expectToBeCalledOnce(task1);
|
||||||
|
expectToBeCalledOnce(task2);
|
||||||
|
expectToBeCalledOnce(task3);
|
||||||
|
expectToBeCalledOnce(task4);
|
||||||
|
expectToBeCalledOnce(task5);
|
||||||
|
expectToBeCalledOnce(task6);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('resolves async tasks recusively before other queued tasks', () => {
|
||||||
|
bigAsyncTest();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should also work with a deadline', () => {
|
||||||
|
InteractionManager.setDeadline(100);
|
||||||
|
BatchedBridge.getEventLoopRunningTime.mockReturnValue(200);
|
||||||
|
bigAsyncTest();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,44 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2004-present Facebook. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
jest.dontMock('InteractionMixin');
|
||||||
|
|
||||||
|
describe('InteractionMixin', () => {
|
||||||
|
var InteractionManager;
|
||||||
|
var InteractionMixin;
|
||||||
|
var component;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModuleRegistry();
|
||||||
|
InteractionManager = require('InteractionManager');
|
||||||
|
InteractionMixin = require('InteractionMixin');
|
||||||
|
|
||||||
|
component = Object.create(InteractionMixin);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start interactions', () => {
|
||||||
|
var timeout = 123;
|
||||||
|
component.createInteractionHandle(timeout);
|
||||||
|
expect(InteractionManager.createInteractionHandle).toBeCalled(timeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should end interactions', () => {
|
||||||
|
var handle = {};
|
||||||
|
component.clearInteractionHandle(handle);
|
||||||
|
expect(InteractionManager.clearInteractionHandle).toBeCalledWith(handle);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should schedule tasks', () => {
|
||||||
|
var task = jest.genMockFunction();
|
||||||
|
component.runAfterInteractions(task);
|
||||||
|
expect(InteractionManager.runAfterInteractions).toBeCalledWith(task);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should end unfinished interactions in componentWillUnmount', () => {
|
||||||
|
var handle = component.createInteractionHandle();
|
||||||
|
component.componentWillUnmount();
|
||||||
|
expect(InteractionManager.clearInteractionHandle).toBeCalledWith(handle);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,121 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2004-present Facebook. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
jest.dontMock('TaskQueue');
|
||||||
|
|
||||||
|
function expectToBeCalledOnce(fn) {
|
||||||
|
expect(fn.mock.calls.length).toBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTaskQueue(taskQueue) {
|
||||||
|
do {
|
||||||
|
jest.runAllTimers();
|
||||||
|
taskQueue.processNext();
|
||||||
|
jest.runAllTimers();
|
||||||
|
} while (taskQueue.hasTasksToProcess())
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('TaskQueue', () => {
|
||||||
|
let taskQueue;
|
||||||
|
let onMoreTasks;
|
||||||
|
let sequenceId;
|
||||||
|
function createSequenceTask(expectedSequenceId) {
|
||||||
|
return jest.genMockFunction().mockImplementation(() => {
|
||||||
|
expect(++sequenceId).toBe(expectedSequenceId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModuleRegistry();
|
||||||
|
onMoreTasks = jest.genMockFunction();
|
||||||
|
const TaskQueue = require('TaskQueue');
|
||||||
|
taskQueue = new TaskQueue({onMoreTasks});
|
||||||
|
sequenceId = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should run a basic task', () => {
|
||||||
|
const task1 = createSequenceTask(1);
|
||||||
|
taskQueue.enqueue({run: task1, name: 'run1'});
|
||||||
|
expect(taskQueue.hasTasksToProcess()).toBe(true);
|
||||||
|
taskQueue.processNext();
|
||||||
|
expectToBeCalledOnce(task1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle blocking promise task', () => {
|
||||||
|
const task1 = jest.genMockFunction().mockImplementation(() => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(++sequenceId).toBe(1);
|
||||||
|
resolve();
|
||||||
|
}, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const task2 = createSequenceTask(2);
|
||||||
|
taskQueue.enqueue({gen: task1, name: 'gen1'});
|
||||||
|
taskQueue.enqueue({run: task2, name: 'run2'});
|
||||||
|
|
||||||
|
taskQueue.processNext();
|
||||||
|
|
||||||
|
expectToBeCalledOnce(task1);
|
||||||
|
expect(task2).not.toBeCalled();
|
||||||
|
expect(onMoreTasks).not.toBeCalled();
|
||||||
|
expect(taskQueue.hasTasksToProcess()).toBe(false);
|
||||||
|
|
||||||
|
clearTaskQueue(taskQueue);
|
||||||
|
|
||||||
|
expectToBeCalledOnce(onMoreTasks);
|
||||||
|
expectToBeCalledOnce(task2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nested simple tasks', () => {
|
||||||
|
const task1 = jest.genMockFunction().mockImplementation(() => {
|
||||||
|
expect(++sequenceId).toBe(1);
|
||||||
|
taskQueue.enqueue({run: task3, name: 'run3'});
|
||||||
|
});
|
||||||
|
const task2 = createSequenceTask(2);
|
||||||
|
const task3 = createSequenceTask(3);
|
||||||
|
taskQueue.enqueue({run: task1, name: 'run1'});
|
||||||
|
taskQueue.enqueue({run: task2, name: 'run2'}); // not blocked by task 1
|
||||||
|
|
||||||
|
clearTaskQueue(taskQueue);
|
||||||
|
|
||||||
|
expectToBeCalledOnce(task1);
|
||||||
|
expectToBeCalledOnce(task2);
|
||||||
|
expectToBeCalledOnce(task3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nested promises', () => {
|
||||||
|
const task1 = jest.genMockFunction().mockImplementation(() => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(++sequenceId).toBe(1);
|
||||||
|
taskQueue.enqueue({gen: task2, name: 'gen2'});
|
||||||
|
taskQueue.enqueue({run: resolve, name: 'resolve1'});
|
||||||
|
}, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const task2 = jest.genMockFunction().mockImplementation(() => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(++sequenceId).toBe(2);
|
||||||
|
taskQueue.enqueue({run: task3, name: 'run3'});
|
||||||
|
taskQueue.enqueue({run: resolve, name: 'resolve2'});
|
||||||
|
}, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const task3 = createSequenceTask(3);
|
||||||
|
const task4 = createSequenceTask(4);
|
||||||
|
taskQueue.enqueue({gen: task1, name: 'gen1'});
|
||||||
|
taskQueue.enqueue({run: task4, name: 'run4'}); // blocked by task 1 promise
|
||||||
|
|
||||||
|
clearTaskQueue(taskQueue);
|
||||||
|
|
||||||
|
expectToBeCalledOnce(task1);
|
||||||
|
expectToBeCalledOnce(task2);
|
||||||
|
expectToBeCalledOnce(task3);
|
||||||
|
expectToBeCalledOnce(task4);
|
||||||
|
});
|
||||||
|
});
|
|
@ -53,6 +53,7 @@ class MessageQueue {
|
||||||
this._callbacks = [];
|
this._callbacks = [];
|
||||||
this._callbackID = 0;
|
this._callbackID = 0;
|
||||||
this._lastFlush = 0;
|
this._lastFlush = 0;
|
||||||
|
this._eventLoopStartTime = new Date().getTime();
|
||||||
|
|
||||||
[
|
[
|
||||||
'invokeCallbackAndReturnFlushedQueue',
|
'invokeCallbackAndReturnFlushedQueue',
|
||||||
|
@ -109,6 +110,10 @@ class MessageQueue {
|
||||||
return module;
|
return module;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEventLoopRunningTime() {
|
||||||
|
return new Date().getTime() - this._eventLoopStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Private" methods
|
* "Private" methods
|
||||||
*/
|
*/
|
||||||
|
@ -150,6 +155,7 @@ class MessageQueue {
|
||||||
|
|
||||||
__callFunction(module, method, args) {
|
__callFunction(module, method, args) {
|
||||||
this._lastFlush = new Date().getTime();
|
this._lastFlush = new Date().getTime();
|
||||||
|
this._eventLoopStartTime = this._lastFlush;
|
||||||
if (isFinite(module)) {
|
if (isFinite(module)) {
|
||||||
method = this._methodTable[module][method];
|
method = this._methodTable[module][method];
|
||||||
module = this._moduleTable[module];
|
module = this._moduleTable[module];
|
||||||
|
@ -171,6 +177,7 @@ class MessageQueue {
|
||||||
__invokeCallback(cbID, args) {
|
__invokeCallback(cbID, args) {
|
||||||
Systrace.beginEvent(`MessageQueue.invokeCallback(${cbID})`);
|
Systrace.beginEvent(`MessageQueue.invokeCallback(${cbID})`);
|
||||||
this._lastFlush = new Date().getTime();
|
this._lastFlush = new Date().getTime();
|
||||||
|
this._eventLoopStartTime = this._lastFlush;
|
||||||
let callback = this._callbacks[cbID];
|
let callback = this._callbacks[cbID];
|
||||||
if (!callback || __DEV__) {
|
if (!callback || __DEV__) {
|
||||||
let debug = this._debugInfo[cbID >> 1];
|
let debug = this._debugInfo[cbID >> 1];
|
||||||
|
|
Loading…
Reference in New Issue