From 893a54d0cde592e1cd77203efd597e3d961417ba Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Wed, 23 Dec 2015 16:10:39 -0800 Subject: [PATCH] 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 --- Libraries/Interaction/InteractionManager.js | 95 ++++-- Libraries/Interaction/TaskQueue.js | 148 +++++++++ .../__tests__/InteractionManager-test.js | 288 ++++++++++++++++++ .../__tests__/InteractionMixin-test.js | 44 +++ .../Interaction/__tests__/TaskQueue-test.js | 121 ++++++++ Libraries/Utilities/MessageQueue.js | 7 + 6 files changed, 673 insertions(+), 30 deletions(-) create mode 100644 Libraries/Interaction/TaskQueue.js create mode 100644 Libraries/Interaction/__tests__/InteractionManager-test.js create mode 100644 Libraries/Interaction/__tests__/InteractionMixin-test.js create mode 100644 Libraries/Interaction/__tests__/TaskQueue-test.js diff --git a/Libraries/Interaction/InteractionManager.js b/Libraries/Interaction/InteractionManager.js index fd4ab67c3..8fac8edb8 100644 --- a/Libraries/Interaction/InteractionManager.js +++ b/Libraries/Interaction/InteractionManager.js @@ -11,23 +11,19 @@ */ 'use strict'; -var ErrorUtils = require('ErrorUtils'); -var EventEmitter = require('EventEmitter'); -var Set = require('Set'); +const BatchedBridge = require('BatchedBridge'); +const EventEmitter = require('EventEmitter'); +const Set = require('Set'); +const TaskQueue = require('TaskQueue'); -var invariant = require('invariant'); -var keyMirror = require('keyMirror'); -var setImmediate = require('setImmediate'); +const invariant = require('invariant'); +const keyMirror = require('keyMirror'); +const setImmediate = require('setImmediate'); type Handle = number; +import type {Task} from 'TaskQueue'; -var _emitter = new EventEmitter(); -var _interactionSet = new Set(); -var _addInteractionSet = new Set(); -var _deleteInteractionSet = new Set(); -var _nextUpdateHandle = null; -var _queue = []; -var _inc = 0; +const _emitter = new EventEmitter(); /** * InteractionManager allows long-running work to be scheduled after any @@ -63,6 +59,20 @@ var _inc = 0; * InteractionManager.clearInteractionHandle(handle); * // 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 = { Events: keyMirror({ @@ -73,13 +83,14 @@ var InteractionManager = { /** * Schedule a function to run after all interactions have completed. */ - runAfterInteractions(callback: ?Function): Promise { + runAfterInteractions(task: ?Task): Promise { return new Promise(resolve => { - scheduleUpdate(); - if (callback) { - _queue.push(callback); + _scheduleUpdate(); + if (task) { + _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. */ createInteractionHandle(): Handle { - scheduleUpdate(); + _scheduleUpdate(); var handle = ++_inc; _addInteractionSet.add(handle); return handle; @@ -101,28 +112,49 @@ var InteractionManager = { !!handle, 'Must provide a handle to clear.' ); - scheduleUpdate(); + _scheduleUpdate(); _addInteractionSet.delete(handle); _deleteInteractionSet.add(handle); }, 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. */ -function scheduleUpdate() { +function _scheduleUpdate() { if (!_nextUpdateHandle) { - _nextUpdateHandle = setImmediate(processUpdate); + if (_deadline > 0) { + _nextUpdateHandle = setTimeout(_processUpdate, 0); + } else { + _nextUpdateHandle = setImmediate(_processUpdate); + } } } /** * Notify listeners, process queue, etc */ -function processUpdate() { - _nextUpdateHandle = null; +function _processUpdate() { + _nextUpdateHandle = 0; var interactionCount = _interactionSet.size; _addInteractionSet.forEach(handle => @@ -143,13 +175,16 @@ function processUpdate() { // process the queue regardless of a transition if (nextInteractionCount === 0) { - var queue = _queue; - _queue = []; - queue.forEach(callback => { - ErrorUtils.applyWithGuard(callback); - }); + while (_taskQueue.hasTasksToProcess()) { + _taskQueue.processNext(); + if (_deadline > 0 && + BatchedBridge.getEventLoopRunningTime() >= _deadline) { + // Hit deadline before processing all tasks, so process more later. + _scheduleUpdate(); + break; + } + } } - _addInteractionSet.clear(); _deleteInteractionSet.clear(); } diff --git a/Libraries/Interaction/TaskQueue.js b/Libraries/Interaction/TaskQueue.js new file mode 100644 index 000000000..8ef7a3704 --- /dev/null +++ b/Libraries/Interaction/TaskQueue.js @@ -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, popable: bool}>; + _onMoreTasks: () => void; + + _getCurrentQueue(): Array { + 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; diff --git a/Libraries/Interaction/__tests__/InteractionManager-test.js b/Libraries/Interaction/__tests__/InteractionManager-test.js new file mode 100644 index 000000000..f02701d7f --- /dev/null +++ b/Libraries/Interaction/__tests__/InteractionManager-test.js @@ -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(); + }); +}); diff --git a/Libraries/Interaction/__tests__/InteractionMixin-test.js b/Libraries/Interaction/__tests__/InteractionMixin-test.js new file mode 100644 index 000000000..c1cc7f54d --- /dev/null +++ b/Libraries/Interaction/__tests__/InteractionMixin-test.js @@ -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); + }); +}); diff --git a/Libraries/Interaction/__tests__/TaskQueue-test.js b/Libraries/Interaction/__tests__/TaskQueue-test.js new file mode 100644 index 000000000..9b36783db --- /dev/null +++ b/Libraries/Interaction/__tests__/TaskQueue-test.js @@ -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); + }); +}); diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index 43211c865..e95dcb385 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -53,6 +53,7 @@ class MessageQueue { this._callbacks = []; this._callbackID = 0; this._lastFlush = 0; + this._eventLoopStartTime = new Date().getTime(); [ 'invokeCallbackAndReturnFlushedQueue', @@ -109,6 +110,10 @@ class MessageQueue { return module; } + getEventLoopRunningTime() { + return new Date().getTime() - this._eventLoopStartTime; + } + /** * "Private" methods */ @@ -150,6 +155,7 @@ class MessageQueue { __callFunction(module, method, args) { this._lastFlush = new Date().getTime(); + this._eventLoopStartTime = this._lastFlush; if (isFinite(module)) { method = this._methodTable[module][method]; module = this._moduleTable[module]; @@ -171,6 +177,7 @@ class MessageQueue { __invokeCallback(cbID, args) { Systrace.beginEvent(`MessageQueue.invokeCallback(${cbID})`); this._lastFlush = new Date().getTime(); + this._eventLoopStartTime = this._lastFlush; let callback = this._callbacks[cbID]; if (!callback || __DEV__) { let debug = this._debugInfo[cbID >> 1];