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:
Spencer Ahrens 2015-12-23 16:10:39 -08:00 committed by facebook-github-bot-7
parent f7de9678d4
commit 893a54d0cd
6 changed files with 673 additions and 30 deletions

View File

@ -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();
}

View File

@ -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;

View File

@ -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();
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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];