[Scheduler] Profiling features (#16145)
* [Scheduler] Mark user-timing events Marks when Scheduler starts and stops running a task. Also marks when a task is initially scheduled, and when Scheduler is waiting for a callback, which can't be inferred from a sample-based JavaScript CPU profile alone. The plan is to use the user-timing events to build a Scheduler profiler that shows how the lifetime of tasks interact with each other and with unscheduled main thread work. The test suite works by printing an text representation of a Scheduler flamegraph. * Expose shared array buffer with profiling info Array contains - the priority Scheduler is currently running - the size of the queue - the id of the currently running task * Replace user-timing calls with event log Events are written to an array buffer using a custom instruction format. For now, this is only meant to be used during page start up, before the profiler worker has a chance to start up. Once the worker is ready, call `stopLoggingProfilerEvents` to return the log up to that point, then send the array buffer to the worker. Then switch to the sampling based approach. * Record the current run ID Each synchronous block of Scheduler work is given a unique run ID. This is different than a task ID because a single task will have more than one run if it yields with a continuation.
This commit is contained in:
parent
56636353d8
commit
a34ca7bce6
|
@ -140,6 +140,8 @@ module.exports = {
|
||||||
],
|
],
|
||||||
|
|
||||||
globals: {
|
globals: {
|
||||||
|
SharedArrayBuffer: true,
|
||||||
|
|
||||||
spyOnDev: true,
|
spyOnDev: true,
|
||||||
spyOnDevAndProd: true,
|
spyOnDevAndProd: true,
|
||||||
spyOnProd: true,
|
spyOnProd: true,
|
||||||
|
|
|
@ -136,6 +136,7 @@ describe('ReactDebugFiberPerf', () => {
|
||||||
require('shared/ReactFeatureFlags').enableProfilerTimer = false;
|
require('shared/ReactFeatureFlags').enableProfilerTimer = false;
|
||||||
require('shared/ReactFeatureFlags').replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
require('shared/ReactFeatureFlags').replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
||||||
require('shared/ReactFeatureFlags').debugRenderPhaseSideEffectsForStrictMode = false;
|
require('shared/ReactFeatureFlags').debugRenderPhaseSideEffectsForStrictMode = false;
|
||||||
|
require('scheduler/src/SchedulerFeatureFlags').enableProfiling = false;
|
||||||
|
|
||||||
// Import after the polyfill is set up:
|
// Import after the polyfill is set up:
|
||||||
React = require('react');
|
React = require('react');
|
||||||
|
|
|
@ -110,6 +110,20 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unstable_startLoggingProfilingEvents() {
|
||||||
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_startLoggingProfilingEvents.apply(
|
||||||
|
this,
|
||||||
|
arguments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unstable_stopLoggingProfilingEvents() {
|
||||||
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_stopLoggingProfilingEvents.apply(
|
||||||
|
this,
|
||||||
|
arguments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
unstable_now: unstable_now,
|
unstable_now: unstable_now,
|
||||||
unstable_scheduleCallback: unstable_scheduleCallback,
|
unstable_scheduleCallback: unstable_scheduleCallback,
|
||||||
|
@ -124,6 +138,8 @@
|
||||||
unstable_pauseExecution: unstable_pauseExecution,
|
unstable_pauseExecution: unstable_pauseExecution,
|
||||||
unstable_getFirstCallbackNode: unstable_getFirstCallbackNode,
|
unstable_getFirstCallbackNode: unstable_getFirstCallbackNode,
|
||||||
unstable_forceFrameRate: unstable_forceFrameRate,
|
unstable_forceFrameRate: unstable_forceFrameRate,
|
||||||
|
unstable_startLoggingProfilingEvents: unstable_startLoggingProfilingEvents,
|
||||||
|
unstable_stopLoggingProfilingEvents: unstable_stopLoggingProfilingEvents,
|
||||||
get unstable_IdlePriority() {
|
get unstable_IdlePriority() {
|
||||||
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||||
.Scheduler.unstable_IdlePriority;
|
.Scheduler.unstable_IdlePriority;
|
||||||
|
@ -144,5 +160,9 @@
|
||||||
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||||
.Scheduler.unstable_UserBlockingPriority;
|
.Scheduler.unstable_UserBlockingPriority;
|
||||||
},
|
},
|
||||||
|
get unstable_sharedProfilingBuffer() {
|
||||||
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||||
|
.Scheduler.unstable_getFirstCallbackNode;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -104,6 +104,20 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unstable_startLoggingProfilingEvents() {
|
||||||
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_startLoggingProfilingEvents.apply(
|
||||||
|
this,
|
||||||
|
arguments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unstable_stopLoggingProfilingEvents() {
|
||||||
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_stopLoggingProfilingEvents.apply(
|
||||||
|
this,
|
||||||
|
arguments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
unstable_now: unstable_now,
|
unstable_now: unstable_now,
|
||||||
unstable_scheduleCallback: unstable_scheduleCallback,
|
unstable_scheduleCallback: unstable_scheduleCallback,
|
||||||
|
@ -118,6 +132,8 @@
|
||||||
unstable_pauseExecution: unstable_pauseExecution,
|
unstable_pauseExecution: unstable_pauseExecution,
|
||||||
unstable_getFirstCallbackNode: unstable_getFirstCallbackNode,
|
unstable_getFirstCallbackNode: unstable_getFirstCallbackNode,
|
||||||
unstable_forceFrameRate: unstable_forceFrameRate,
|
unstable_forceFrameRate: unstable_forceFrameRate,
|
||||||
|
unstable_startLoggingProfilingEvents: unstable_startLoggingProfilingEvents,
|
||||||
|
unstable_stopLoggingProfilingEvents: unstable_stopLoggingProfilingEvents,
|
||||||
get unstable_IdlePriority() {
|
get unstable_IdlePriority() {
|
||||||
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||||
.Scheduler.unstable_IdlePriority;
|
.Scheduler.unstable_IdlePriority;
|
||||||
|
@ -138,5 +154,9 @@
|
||||||
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||||
.Scheduler.unstable_UserBlockingPriority;
|
.Scheduler.unstable_UserBlockingPriority;
|
||||||
},
|
},
|
||||||
|
get unstable_sharedProfilingBuffer() {
|
||||||
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||||
|
.Scheduler.unstable_getFirstCallbackNode;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -104,6 +104,20 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unstable_startLoggingProfilingEvents() {
|
||||||
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_startLoggingProfilingEvents.apply(
|
||||||
|
this,
|
||||||
|
arguments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unstable_stopLoggingProfilingEvents() {
|
||||||
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_stopLoggingProfilingEvents.apply(
|
||||||
|
this,
|
||||||
|
arguments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
unstable_now: unstable_now,
|
unstable_now: unstable_now,
|
||||||
unstable_scheduleCallback: unstable_scheduleCallback,
|
unstable_scheduleCallback: unstable_scheduleCallback,
|
||||||
|
@ -118,6 +132,8 @@
|
||||||
unstable_pauseExecution: unstable_pauseExecution,
|
unstable_pauseExecution: unstable_pauseExecution,
|
||||||
unstable_getFirstCallbackNode: unstable_getFirstCallbackNode,
|
unstable_getFirstCallbackNode: unstable_getFirstCallbackNode,
|
||||||
unstable_forceFrameRate: unstable_forceFrameRate,
|
unstable_forceFrameRate: unstable_forceFrameRate,
|
||||||
|
unstable_startLoggingProfilingEvents: unstable_startLoggingProfilingEvents,
|
||||||
|
unstable_stopLoggingProfilingEvents: unstable_stopLoggingProfilingEvents,
|
||||||
get unstable_IdlePriority() {
|
get unstable_IdlePriority() {
|
||||||
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||||
.Scheduler.unstable_IdlePriority;
|
.Scheduler.unstable_IdlePriority;
|
||||||
|
@ -138,5 +154,9 @@
|
||||||
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||||
.Scheduler.unstable_UserBlockingPriority;
|
.Scheduler.unstable_UserBlockingPriority;
|
||||||
},
|
},
|
||||||
|
get unstable_sharedProfilingBuffer() {
|
||||||
|
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||||
|
.Scheduler.unstable_getFirstCallbackNode;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,10 +8,14 @@
|
||||||
|
|
||||||
/* eslint-disable no-var */
|
/* eslint-disable no-var */
|
||||||
|
|
||||||
import {enableSchedulerDebugging} from './SchedulerFeatureFlags';
|
|
||||||
import {
|
import {
|
||||||
requestHostCallback,
|
enableSchedulerDebugging,
|
||||||
|
enableProfiling,
|
||||||
|
} from './SchedulerFeatureFlags';
|
||||||
|
import {
|
||||||
|
requestHostCallback as requestHostCallbackWithoutProfiling,
|
||||||
requestHostTimeout,
|
requestHostTimeout,
|
||||||
|
cancelHostCallback,
|
||||||
cancelHostTimeout,
|
cancelHostTimeout,
|
||||||
shouldYieldToHost,
|
shouldYieldToHost,
|
||||||
getCurrentTime,
|
getCurrentTime,
|
||||||
|
@ -21,11 +25,26 @@ import {
|
||||||
import {push, pop, peek} from './SchedulerMinHeap';
|
import {push, pop, peek} from './SchedulerMinHeap';
|
||||||
|
|
||||||
// TODO: Use symbols?
|
// TODO: Use symbols?
|
||||||
var ImmediatePriority = 1;
|
import {
|
||||||
var UserBlockingPriority = 2;
|
ImmediatePriority,
|
||||||
var NormalPriority = 3;
|
UserBlockingPriority,
|
||||||
var LowPriority = 4;
|
NormalPriority,
|
||||||
var IdlePriority = 5;
|
LowPriority,
|
||||||
|
IdlePriority,
|
||||||
|
} from './SchedulerPriorities';
|
||||||
|
import {
|
||||||
|
sharedProfilingBuffer,
|
||||||
|
markTaskRun,
|
||||||
|
markTaskYield,
|
||||||
|
markTaskCompleted,
|
||||||
|
markTaskCanceled,
|
||||||
|
markTaskErrored,
|
||||||
|
markSchedulerSuspended,
|
||||||
|
markSchedulerUnsuspended,
|
||||||
|
markTaskStart,
|
||||||
|
stopLoggingProfilingEvents,
|
||||||
|
startLoggingProfilingEvents,
|
||||||
|
} from './SchedulerProfiling';
|
||||||
|
|
||||||
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
|
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
|
||||||
// Math.pow(2, 30) - 1
|
// Math.pow(2, 30) - 1
|
||||||
|
@ -60,15 +79,17 @@ var isPerformingWork = false;
|
||||||
var isHostCallbackScheduled = false;
|
var isHostCallbackScheduled = false;
|
||||||
var isHostTimeoutScheduled = false;
|
var isHostTimeoutScheduled = false;
|
||||||
|
|
||||||
function flushTask(task, callback, currentTime) {
|
function requestHostCallbackWithProfiling(cb, time) {
|
||||||
currentPriorityLevel = task.priorityLevel;
|
if (enableProfiling) {
|
||||||
var didUserCallbackTimeout = task.expirationTime <= currentTime;
|
markSchedulerSuspended(time);
|
||||||
var continuationCallback = callback(didUserCallbackTimeout);
|
requestHostCallbackWithoutProfiling(cb);
|
||||||
return typeof continuationCallback === 'function'
|
}
|
||||||
? continuationCallback
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestHostCallback = enableProfiling
|
||||||
|
? requestHostCallbackWithProfiling
|
||||||
|
: requestHostCallbackWithoutProfiling;
|
||||||
|
|
||||||
function advanceTimers(currentTime) {
|
function advanceTimers(currentTime) {
|
||||||
// Check for tasks that are no longer delayed and add them to the queue.
|
// Check for tasks that are no longer delayed and add them to the queue.
|
||||||
let timer = peek(timerQueue);
|
let timer = peek(timerQueue);
|
||||||
|
@ -81,6 +102,10 @@ function advanceTimers(currentTime) {
|
||||||
pop(timerQueue);
|
pop(timerQueue);
|
||||||
timer.sortIndex = timer.expirationTime;
|
timer.sortIndex = timer.expirationTime;
|
||||||
push(taskQueue, timer);
|
push(taskQueue, timer);
|
||||||
|
if (enableProfiling) {
|
||||||
|
markTaskStart(timer);
|
||||||
|
timer.isQueued = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Remaining timers are pending.
|
// Remaining timers are pending.
|
||||||
return;
|
return;
|
||||||
|
@ -96,7 +121,7 @@ function handleTimeout(currentTime) {
|
||||||
if (!isHostCallbackScheduled) {
|
if (!isHostCallbackScheduled) {
|
||||||
if (peek(taskQueue) !== null) {
|
if (peek(taskQueue) !== null) {
|
||||||
isHostCallbackScheduled = true;
|
isHostCallbackScheduled = true;
|
||||||
requestHostCallback(flushWork);
|
requestHostCallback(flushWork, currentTime);
|
||||||
} else {
|
} else {
|
||||||
const firstTimer = peek(timerQueue);
|
const firstTimer = peek(timerQueue);
|
||||||
if (firstTimer !== null) {
|
if (firstTimer !== null) {
|
||||||
|
@ -107,6 +132,10 @@ function handleTimeout(currentTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function flushWork(hasTimeRemaining, initialTime) {
|
function flushWork(hasTimeRemaining, initialTime) {
|
||||||
|
if (isHostCallbackScheduled) {
|
||||||
|
markSchedulerUnsuspended(initialTime);
|
||||||
|
}
|
||||||
|
|
||||||
// We'll need a host callback the next time work is scheduled.
|
// We'll need a host callback the next time work is scheduled.
|
||||||
isHostCallbackScheduled = false;
|
isHostCallbackScheduled = false;
|
||||||
if (isHostTimeoutScheduled) {
|
if (isHostTimeoutScheduled) {
|
||||||
|
@ -135,15 +164,24 @@ function flushWork(hasTimeRemaining, initialTime) {
|
||||||
const callback = currentTask.callback;
|
const callback = currentTask.callback;
|
||||||
if (callback !== null) {
|
if (callback !== null) {
|
||||||
currentTask.callback = null;
|
currentTask.callback = null;
|
||||||
const continuation = flushTask(currentTask, callback, currentTime);
|
currentPriorityLevel = currentTask.priorityLevel;
|
||||||
if (continuation !== null) {
|
const didUserCallbackTimeout =
|
||||||
currentTask.callback = continuation;
|
currentTask.expirationTime <= currentTime;
|
||||||
|
markTaskRun(currentTask, currentTime);
|
||||||
|
const continuationCallback = callback(didUserCallbackTimeout);
|
||||||
|
currentTime = getCurrentTime();
|
||||||
|
if (typeof continuationCallback === 'function') {
|
||||||
|
currentTask.callback = continuationCallback;
|
||||||
|
markTaskYield(currentTask, currentTime);
|
||||||
} else {
|
} else {
|
||||||
|
if (enableProfiling) {
|
||||||
|
markTaskCompleted(currentTask, currentTime);
|
||||||
|
currentTask.isQueued = false;
|
||||||
|
}
|
||||||
if (currentTask === peek(taskQueue)) {
|
if (currentTask === peek(taskQueue)) {
|
||||||
pop(taskQueue);
|
pop(taskQueue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentTime = getCurrentTime();
|
|
||||||
advanceTimers(currentTime);
|
advanceTimers(currentTime);
|
||||||
} else {
|
} else {
|
||||||
pop(taskQueue);
|
pop(taskQueue);
|
||||||
|
@ -152,6 +190,8 @@ function flushWork(hasTimeRemaining, initialTime) {
|
||||||
}
|
}
|
||||||
// Return whether there's additional work
|
// Return whether there's additional work
|
||||||
if (currentTask !== null) {
|
if (currentTask !== null) {
|
||||||
|
markSchedulerSuspended(currentTime);
|
||||||
|
isHostCallbackScheduled = true;
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
let firstTimer = peek(timerQueue);
|
let firstTimer = peek(timerQueue);
|
||||||
|
@ -160,6 +200,18 @@ function flushWork(hasTimeRemaining, initialTime) {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (currentTask !== null) {
|
||||||
|
if (enableProfiling) {
|
||||||
|
const currentTime = getCurrentTime();
|
||||||
|
markTaskErrored(currentTask, currentTime);
|
||||||
|
currentTask.isQueued = false;
|
||||||
|
}
|
||||||
|
if (currentTask === peek(taskQueue)) {
|
||||||
|
pop(taskQueue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
currentTask = null;
|
currentTask = null;
|
||||||
currentPriorityLevel = previousPriorityLevel;
|
currentPriorityLevel = previousPriorityLevel;
|
||||||
|
@ -250,6 +302,8 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
|
||||||
|
|
||||||
var startTime;
|
var startTime;
|
||||||
var timeout;
|
var timeout;
|
||||||
|
// TODO: Expose the current label when profiling, somehow
|
||||||
|
// var label;
|
||||||
if (typeof options === 'object' && options !== null) {
|
if (typeof options === 'object' && options !== null) {
|
||||||
var delay = options.delay;
|
var delay = options.delay;
|
||||||
if (typeof delay === 'number' && delay > 0) {
|
if (typeof delay === 'number' && delay > 0) {
|
||||||
|
@ -261,6 +315,12 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
|
||||||
typeof options.timeout === 'number'
|
typeof options.timeout === 'number'
|
||||||
? options.timeout
|
? options.timeout
|
||||||
: timeoutForPriorityLevel(priorityLevel);
|
: timeoutForPriorityLevel(priorityLevel);
|
||||||
|
// if (enableProfiling) {
|
||||||
|
// var _label = options.label;
|
||||||
|
// if (typeof _label === 'string') {
|
||||||
|
// label = _label;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
} else {
|
} else {
|
||||||
timeout = timeoutForPriorityLevel(priorityLevel);
|
timeout = timeoutForPriorityLevel(priorityLevel);
|
||||||
startTime = currentTime;
|
startTime = currentTime;
|
||||||
|
@ -269,7 +329,7 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
|
||||||
var expirationTime = startTime + timeout;
|
var expirationTime = startTime + timeout;
|
||||||
|
|
||||||
var newTask = {
|
var newTask = {
|
||||||
id: taskIdCounter++,
|
id: ++taskIdCounter,
|
||||||
callback,
|
callback,
|
||||||
priorityLevel,
|
priorityLevel,
|
||||||
startTime,
|
startTime,
|
||||||
|
@ -277,6 +337,13 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
|
||||||
sortIndex: -1,
|
sortIndex: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (enableProfiling) {
|
||||||
|
newTask.isQueued = false;
|
||||||
|
// if (typeof options === 'object' && options !== null) {
|
||||||
|
// newTask.label = label;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
if (startTime > currentTime) {
|
if (startTime > currentTime) {
|
||||||
// This is a delayed task.
|
// This is a delayed task.
|
||||||
newTask.sortIndex = startTime;
|
newTask.sortIndex = startTime;
|
||||||
|
@ -295,11 +362,15 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
|
||||||
} else {
|
} else {
|
||||||
newTask.sortIndex = expirationTime;
|
newTask.sortIndex = expirationTime;
|
||||||
push(taskQueue, newTask);
|
push(taskQueue, newTask);
|
||||||
|
if (enableProfiling) {
|
||||||
|
markTaskStart(newTask, currentTime);
|
||||||
|
newTask.isQueued = true;
|
||||||
|
}
|
||||||
// Schedule a host callback, if needed. If we're already performing work,
|
// Schedule a host callback, if needed. If we're already performing work,
|
||||||
// wait until the next time we yield.
|
// wait until the next time we yield.
|
||||||
if (!isHostCallbackScheduled && !isPerformingWork) {
|
if (!isHostCallbackScheduled && !isPerformingWork) {
|
||||||
isHostCallbackScheduled = true;
|
isHostCallbackScheduled = true;
|
||||||
requestHostCallback(flushWork);
|
requestHostCallback(flushWork, currentTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,7 +385,12 @@ function unstable_continueExecution() {
|
||||||
isSchedulerPaused = false;
|
isSchedulerPaused = false;
|
||||||
if (!isHostCallbackScheduled && !isPerformingWork) {
|
if (!isHostCallbackScheduled && !isPerformingWork) {
|
||||||
isHostCallbackScheduled = true;
|
isHostCallbackScheduled = true;
|
||||||
requestHostCallback(flushWork);
|
if (enableProfiling) {
|
||||||
|
const currentTime = getCurrentTime();
|
||||||
|
requestHostCallbackWithProfiling(flushWork, currentTime);
|
||||||
|
} else {
|
||||||
|
requestHostCallback(flushWork);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,10 +399,26 @@ function unstable_getFirstCallbackNode() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function unstable_cancelCallback(task) {
|
function unstable_cancelCallback(task) {
|
||||||
// Null out the callback to indicate the task has been canceled. (Can't remove
|
if (enableProfiling && task.isQueued) {
|
||||||
// from the queue because you can't remove arbitrary nodes from an array based
|
const currentTime = getCurrentTime();
|
||||||
// heap, only the first one.)
|
markTaskCanceled(task, currentTime);
|
||||||
task.callback = null;
|
task.isQueued = false;
|
||||||
|
}
|
||||||
|
if (task !== null && task === peek(taskQueue)) {
|
||||||
|
pop(taskQueue);
|
||||||
|
if (enableProfiling && !isPerformingWork && taskQueue.length === 0) {
|
||||||
|
// The queue is now empty.
|
||||||
|
const currentTime = getCurrentTime();
|
||||||
|
markSchedulerUnsuspended(currentTime);
|
||||||
|
isHostCallbackScheduled = false;
|
||||||
|
cancelHostCallback();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Null out the callback to indicate the task has been canceled. (Can't
|
||||||
|
// remove from the queue because you can't remove arbitrary nodes from an
|
||||||
|
// array based heap, only the first one.)
|
||||||
|
task.callback = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function unstable_getCurrentPriorityLevel() {
|
function unstable_getCurrentPriorityLevel() {
|
||||||
|
@ -370,3 +462,16 @@ export {
|
||||||
getCurrentTime as unstable_now,
|
getCurrentTime as unstable_now,
|
||||||
forceFrameRate as unstable_forceFrameRate,
|
forceFrameRate as unstable_forceFrameRate,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const unstable_startLoggingProfilingEvents = enableProfiling
|
||||||
|
? startLoggingProfilingEvents
|
||||||
|
: null;
|
||||||
|
|
||||||
|
export const unstable_stopLoggingProfilingEvents = enableProfiling
|
||||||
|
? stopLoggingProfilingEvents
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Expose a shared array buffer that contains profiling information.
|
||||||
|
export const unstable_sharedProfilingBuffer = enableProfiling
|
||||||
|
? sharedProfilingBuffer
|
||||||
|
: null;
|
||||||
|
|
|
@ -11,3 +11,4 @@ export const enableIsInputPending = false;
|
||||||
export const requestIdleCallbackBeforeFirstFrame = false;
|
export const requestIdleCallbackBeforeFirstFrame = false;
|
||||||
export const requestTimerEventBeforeFirstFrame = false;
|
export const requestTimerEventBeforeFirstFrame = false;
|
||||||
export const enableMessageLoopImplementation = false;
|
export const enableMessageLoopImplementation = false;
|
||||||
|
export const enableProfiling = __PROFILE__;
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;
|
||||||
|
|
||||||
|
// TODO: Use symbols?
|
||||||
|
export const NoPriority = 0;
|
||||||
|
export const ImmediatePriority = 1;
|
||||||
|
export const UserBlockingPriority = 2;
|
||||||
|
export const NormalPriority = 3;
|
||||||
|
export const LowPriority = 4;
|
||||||
|
export const IdlePriority = 5;
|
|
@ -0,0 +1,210 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {PriorityLevel} from './SchedulerPriorities';
|
||||||
|
import {enableProfiling} from './SchedulerFeatureFlags';
|
||||||
|
|
||||||
|
import {NoPriority} from './SchedulerPriorities';
|
||||||
|
|
||||||
|
let runIdCounter: number = 0;
|
||||||
|
let mainThreadIdCounter: number = 0;
|
||||||
|
|
||||||
|
const profilingStateSize = 4;
|
||||||
|
export const sharedProfilingBuffer =
|
||||||
|
// $FlowFixMe Flow doesn't know about SharedArrayBuffer
|
||||||
|
typeof SharedArrayBuffer === 'function'
|
||||||
|
? new SharedArrayBuffer(profilingStateSize * Int32Array.BYTES_PER_ELEMENT)
|
||||||
|
: // $FlowFixMe Flow doesn't know about ArrayBuffer
|
||||||
|
new ArrayBuffer(profilingStateSize * Int32Array.BYTES_PER_ELEMENT);
|
||||||
|
|
||||||
|
const profilingState = enableProfiling
|
||||||
|
? new Int32Array(sharedProfilingBuffer)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const PRIORITY = 0;
|
||||||
|
const CURRENT_TASK_ID = 1;
|
||||||
|
const CURRENT_RUN_ID = 2;
|
||||||
|
const QUEUE_SIZE = 3;
|
||||||
|
|
||||||
|
if (enableProfiling && profilingState !== null) {
|
||||||
|
profilingState[PRIORITY] = NoPriority;
|
||||||
|
// This is maintained with a counter, because the size of the priority queue
|
||||||
|
// array might include canceled tasks.
|
||||||
|
profilingState[QUEUE_SIZE] = 0;
|
||||||
|
profilingState[CURRENT_TASK_ID] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const INITIAL_EVENT_LOG_SIZE = 1000;
|
||||||
|
|
||||||
|
let eventLogSize = 0;
|
||||||
|
let eventLogBuffer = null;
|
||||||
|
let eventLog = null;
|
||||||
|
let eventLogIndex = 0;
|
||||||
|
|
||||||
|
const TaskStartEvent = 1;
|
||||||
|
const TaskCompleteEvent = 2;
|
||||||
|
const TaskErrorEvent = 3;
|
||||||
|
const TaskCancelEvent = 4;
|
||||||
|
const TaskRunEvent = 5;
|
||||||
|
const TaskYieldEvent = 6;
|
||||||
|
const SchedulerSuspendEvent = 7;
|
||||||
|
const SchedulerResumeEvent = 8;
|
||||||
|
|
||||||
|
function logEvent(entries) {
|
||||||
|
if (eventLog !== null) {
|
||||||
|
const offset = eventLogIndex;
|
||||||
|
eventLogIndex += entries.length;
|
||||||
|
if (eventLogIndex + 1 > eventLogSize) {
|
||||||
|
eventLogSize = eventLogIndex + 1;
|
||||||
|
const newEventLog = new Int32Array(
|
||||||
|
eventLogSize * Int32Array.BYTES_PER_ELEMENT,
|
||||||
|
);
|
||||||
|
newEventLog.set(eventLog);
|
||||||
|
eventLogBuffer = newEventLog.buffer;
|
||||||
|
eventLog = newEventLog;
|
||||||
|
}
|
||||||
|
eventLog.set(entries, offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startLoggingProfilingEvents(): void {
|
||||||
|
eventLogSize = INITIAL_EVENT_LOG_SIZE;
|
||||||
|
eventLogBuffer = new ArrayBuffer(eventLogSize * Int32Array.BYTES_PER_ELEMENT);
|
||||||
|
eventLog = new Int32Array(eventLogBuffer);
|
||||||
|
eventLogIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopLoggingProfilingEvents(): ArrayBuffer | null {
|
||||||
|
const buffer = eventLogBuffer;
|
||||||
|
eventLogBuffer = eventLog = null;
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markTaskStart(
|
||||||
|
task: {id: number, priorityLevel: PriorityLevel},
|
||||||
|
time: number,
|
||||||
|
) {
|
||||||
|
if (enableProfiling) {
|
||||||
|
if (profilingState !== null) {
|
||||||
|
profilingState[QUEUE_SIZE]++;
|
||||||
|
}
|
||||||
|
if (eventLog !== null) {
|
||||||
|
logEvent([TaskStartEvent, time, task.id, task.priorityLevel]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markTaskCompleted(
|
||||||
|
task: {
|
||||||
|
id: number,
|
||||||
|
priorityLevel: PriorityLevel,
|
||||||
|
},
|
||||||
|
time: number,
|
||||||
|
) {
|
||||||
|
if (enableProfiling) {
|
||||||
|
if (profilingState !== null) {
|
||||||
|
profilingState[PRIORITY] = NoPriority;
|
||||||
|
profilingState[CURRENT_TASK_ID] = 0;
|
||||||
|
profilingState[QUEUE_SIZE]--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventLog !== null) {
|
||||||
|
logEvent([TaskCompleteEvent, time, task.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markTaskCanceled(
|
||||||
|
task: {
|
||||||
|
id: number,
|
||||||
|
priorityLevel: PriorityLevel,
|
||||||
|
},
|
||||||
|
time: number,
|
||||||
|
) {
|
||||||
|
if (enableProfiling) {
|
||||||
|
if (profilingState !== null) {
|
||||||
|
profilingState[QUEUE_SIZE]--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventLog !== null) {
|
||||||
|
logEvent([TaskCancelEvent, time, task.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markTaskErrored(
|
||||||
|
task: {
|
||||||
|
id: number,
|
||||||
|
priorityLevel: PriorityLevel,
|
||||||
|
},
|
||||||
|
time: number,
|
||||||
|
) {
|
||||||
|
if (enableProfiling) {
|
||||||
|
if (profilingState !== null) {
|
||||||
|
profilingState[PRIORITY] = NoPriority;
|
||||||
|
profilingState[CURRENT_TASK_ID] = 0;
|
||||||
|
profilingState[QUEUE_SIZE]--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventLog !== null) {
|
||||||
|
logEvent([TaskErrorEvent, time, task.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markTaskRun(
|
||||||
|
task: {id: number, priorityLevel: PriorityLevel},
|
||||||
|
time: number,
|
||||||
|
) {
|
||||||
|
if (enableProfiling) {
|
||||||
|
runIdCounter++;
|
||||||
|
|
||||||
|
if (profilingState !== null) {
|
||||||
|
profilingState[PRIORITY] = task.priorityLevel;
|
||||||
|
profilingState[CURRENT_TASK_ID] = task.id;
|
||||||
|
profilingState[CURRENT_RUN_ID] = runIdCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventLog !== null) {
|
||||||
|
logEvent([TaskRunEvent, time, task.id, runIdCounter]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markTaskYield(task: {id: number}, time: number) {
|
||||||
|
if (enableProfiling) {
|
||||||
|
if (profilingState !== null) {
|
||||||
|
profilingState[PRIORITY] = NoPriority;
|
||||||
|
profilingState[CURRENT_TASK_ID] = 0;
|
||||||
|
profilingState[CURRENT_RUN_ID] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventLog !== null) {
|
||||||
|
logEvent([TaskYieldEvent, time, task.id, runIdCounter]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markSchedulerSuspended(time: number) {
|
||||||
|
if (enableProfiling) {
|
||||||
|
mainThreadIdCounter++;
|
||||||
|
|
||||||
|
if (eventLog !== null) {
|
||||||
|
logEvent([SchedulerSuspendEvent, time, mainThreadIdCounter]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markSchedulerUnsuspended(time: number) {
|
||||||
|
if (enableProfiling) {
|
||||||
|
if (eventLog !== null) {
|
||||||
|
logEvent([SchedulerResumeEvent, time, mainThreadIdCounter]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,11 +59,15 @@ describe('SchedulerDOM', () => {
|
||||||
runPostMessageCallbacks(config);
|
runPostMessageCallbacks(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
let frameSize = 33;
|
let frameSize;
|
||||||
let startOfLatestFrame = 0;
|
let startOfLatestFrame;
|
||||||
let currentTime = 0;
|
let currentTime;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
frameSize = 33;
|
||||||
|
startOfLatestFrame = 0;
|
||||||
|
currentTime = 0;
|
||||||
|
|
||||||
delete global.performance;
|
delete global.performance;
|
||||||
global.requestAnimationFrame = function(cb) {
|
global.requestAnimationFrame = function(cb) {
|
||||||
return rAFCallbacks.push(() => {
|
return rAFCallbacks.push(() => {
|
||||||
|
|
|
@ -0,0 +1,449 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @emails react-core
|
||||||
|
* @jest-environment node
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let Scheduler;
|
||||||
|
let sharedProfilingArray;
|
||||||
|
// let runWithPriority;
|
||||||
|
let ImmediatePriority;
|
||||||
|
let UserBlockingPriority;
|
||||||
|
let NormalPriority;
|
||||||
|
let LowPriority;
|
||||||
|
let IdlePriority;
|
||||||
|
let scheduleCallback;
|
||||||
|
let cancelCallback;
|
||||||
|
// let wrapCallback;
|
||||||
|
// let getCurrentPriorityLevel;
|
||||||
|
// let shouldYield;
|
||||||
|
|
||||||
|
function priorityLevelToString(priorityLevel) {
|
||||||
|
switch (priorityLevel) {
|
||||||
|
case ImmediatePriority:
|
||||||
|
return 'Immediate';
|
||||||
|
case UserBlockingPriority:
|
||||||
|
return 'User-blocking';
|
||||||
|
case NormalPriority:
|
||||||
|
return 'Normal';
|
||||||
|
case LowPriority:
|
||||||
|
return 'Low';
|
||||||
|
case IdlePriority:
|
||||||
|
return 'Idle';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Scheduler', () => {
|
||||||
|
if (!__PROFILE__) {
|
||||||
|
// The tests in this suite only apply when profiling is on
|
||||||
|
it('profiling APIs are not available', () => {
|
||||||
|
Scheduler = require('scheduler');
|
||||||
|
expect(Scheduler.unstable_stopLoggingProfilingEvents).toBe(null);
|
||||||
|
expect(Scheduler.unstable_sharedProfilingBuffer).toBe(null);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.mock('scheduler', () => require('scheduler/unstable_mock'));
|
||||||
|
Scheduler = require('scheduler');
|
||||||
|
|
||||||
|
sharedProfilingArray = new Int32Array(
|
||||||
|
Scheduler.unstable_sharedProfilingBuffer,
|
||||||
|
);
|
||||||
|
|
||||||
|
// runWithPriority = Scheduler.unstable_runWithPriority;
|
||||||
|
ImmediatePriority = Scheduler.unstable_ImmediatePriority;
|
||||||
|
UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
|
||||||
|
NormalPriority = Scheduler.unstable_NormalPriority;
|
||||||
|
LowPriority = Scheduler.unstable_LowPriority;
|
||||||
|
IdlePriority = Scheduler.unstable_IdlePriority;
|
||||||
|
scheduleCallback = Scheduler.unstable_scheduleCallback;
|
||||||
|
cancelCallback = Scheduler.unstable_cancelCallback;
|
||||||
|
// wrapCallback = Scheduler.unstable_wrapCallback;
|
||||||
|
// getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel;
|
||||||
|
// shouldYield = Scheduler.unstable_shouldYield;
|
||||||
|
});
|
||||||
|
|
||||||
|
const PRIORITY = 0;
|
||||||
|
const CURRENT_TASK_ID = 1;
|
||||||
|
const CURRENT_RUN_ID = 2;
|
||||||
|
const QUEUE_SIZE = 3;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (sharedProfilingArray[QUEUE_SIZE] !== 0) {
|
||||||
|
throw Error(
|
||||||
|
'Test exited, but the shared profiling buffer indicates that a task ' +
|
||||||
|
'is still running',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const TaskStartEvent = 1;
|
||||||
|
const TaskCompleteEvent = 2;
|
||||||
|
const TaskErrorEvent = 3;
|
||||||
|
const TaskCancelEvent = 4;
|
||||||
|
const TaskRunEvent = 5;
|
||||||
|
const TaskYieldEvent = 6;
|
||||||
|
const SchedulerSuspendEvent = 7;
|
||||||
|
const SchedulerResumeEvent = 8;
|
||||||
|
|
||||||
|
function stopProfilingAndPrintFlamegraph() {
|
||||||
|
const eventLog = new Int32Array(
|
||||||
|
Scheduler.unstable_stopLoggingProfilingEvents(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const tasks = new Map();
|
||||||
|
const mainThreadRuns = [];
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
processLog: while (i < eventLog.length) {
|
||||||
|
const instruction = eventLog[i];
|
||||||
|
const time = eventLog[i + 1];
|
||||||
|
switch (instruction) {
|
||||||
|
case 0: {
|
||||||
|
break processLog;
|
||||||
|
}
|
||||||
|
case TaskStartEvent: {
|
||||||
|
const taskId = eventLog[i + 2];
|
||||||
|
const priorityLevel = eventLog[i + 3];
|
||||||
|
const task = {
|
||||||
|
id: taskId,
|
||||||
|
priorityLevel,
|
||||||
|
label: null,
|
||||||
|
start: time,
|
||||||
|
end: -1,
|
||||||
|
exitStatus: null,
|
||||||
|
runs: [],
|
||||||
|
};
|
||||||
|
tasks.set(taskId, task);
|
||||||
|
i += 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TaskCompleteEvent: {
|
||||||
|
const taskId = eventLog[i + 2];
|
||||||
|
const task = tasks.get(taskId);
|
||||||
|
if (task === undefined) {
|
||||||
|
throw Error('Task does not exist.');
|
||||||
|
}
|
||||||
|
task.end = time;
|
||||||
|
task.exitStatus = 'completed';
|
||||||
|
i += 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TaskErrorEvent: {
|
||||||
|
const taskId = eventLog[i + 2];
|
||||||
|
const task = tasks.get(taskId);
|
||||||
|
if (task === undefined) {
|
||||||
|
throw Error('Task does not exist.');
|
||||||
|
}
|
||||||
|
task.end = time;
|
||||||
|
task.exitStatus = 'errored';
|
||||||
|
i += 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TaskCancelEvent: {
|
||||||
|
const taskId = eventLog[i + 2];
|
||||||
|
const task = tasks.get(taskId);
|
||||||
|
if (task === undefined) {
|
||||||
|
throw Error('Task does not exist.');
|
||||||
|
}
|
||||||
|
task.end = time;
|
||||||
|
task.exitStatus = 'canceled';
|
||||||
|
i += 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TaskRunEvent:
|
||||||
|
case TaskYieldEvent: {
|
||||||
|
const taskId = eventLog[i + 2];
|
||||||
|
const task = tasks.get(taskId);
|
||||||
|
if (task === undefined) {
|
||||||
|
throw Error('Task does not exist.');
|
||||||
|
}
|
||||||
|
task.runs.push(time);
|
||||||
|
i += 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SchedulerSuspendEvent:
|
||||||
|
case SchedulerResumeEvent: {
|
||||||
|
mainThreadRuns.push(time);
|
||||||
|
i += 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw Error('Unknown instruction type: ' + instruction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we can render the tasks as a flamegraph.
|
||||||
|
const labelColumnWidth = 30;
|
||||||
|
const msPerChar = 50;
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
const mainThreadLabelColumn = '!!! Main thread ';
|
||||||
|
let mainThreadTimelineColumn = '';
|
||||||
|
let isMainThreadBusy = false;
|
||||||
|
for (const time of mainThreadRuns) {
|
||||||
|
const index = time / msPerChar;
|
||||||
|
mainThreadTimelineColumn += (isMainThreadBusy ? '█' : ' ').repeat(
|
||||||
|
index - mainThreadTimelineColumn.length,
|
||||||
|
);
|
||||||
|
isMainThreadBusy = !isMainThreadBusy;
|
||||||
|
}
|
||||||
|
result += `${mainThreadLabelColumn}│${mainThreadTimelineColumn}\n`;
|
||||||
|
|
||||||
|
const tasksByPriority = Array.from(tasks.values()).sort(
|
||||||
|
(t1, t2) => t1.priorityLevel - t2.priorityLevel,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const task of tasksByPriority) {
|
||||||
|
let label = task.label;
|
||||||
|
if (label === undefined) {
|
||||||
|
label = 'Task';
|
||||||
|
}
|
||||||
|
let labelColumn = `Task ${task.id} [${priorityLevelToString(
|
||||||
|
task.priorityLevel,
|
||||||
|
)}]`;
|
||||||
|
labelColumn += ' '.repeat(labelColumnWidth - labelColumn.length - 1);
|
||||||
|
|
||||||
|
// Add empty space up until the start mark
|
||||||
|
let timelineColumn = ' '.repeat(task.start / msPerChar);
|
||||||
|
|
||||||
|
let isRunning = false;
|
||||||
|
for (const time of task.runs) {
|
||||||
|
const index = time / msPerChar;
|
||||||
|
timelineColumn += (isRunning ? '█' : '░').repeat(
|
||||||
|
index - timelineColumn.length,
|
||||||
|
);
|
||||||
|
isRunning = !isRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endIndex = task.end / msPerChar;
|
||||||
|
timelineColumn += (isRunning ? '█' : '░').repeat(
|
||||||
|
endIndex - timelineColumn.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (task.exitStatus !== 'completed') {
|
||||||
|
timelineColumn += `🡐 ${task.exitStatus}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
result += `${labelColumn}│${timelineColumn}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '\n' + result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProfilingInfo() {
|
||||||
|
const queueSize = sharedProfilingArray[QUEUE_SIZE];
|
||||||
|
if (queueSize === 0) {
|
||||||
|
return 'Empty Queue';
|
||||||
|
}
|
||||||
|
const priorityLevel = sharedProfilingArray[PRIORITY];
|
||||||
|
if (priorityLevel === 0) {
|
||||||
|
return 'Suspended, Queue Size: ' + queueSize;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
`Task: ${sharedProfilingArray[CURRENT_TASK_ID]}, ` +
|
||||||
|
`Run: ${sharedProfilingArray[CURRENT_RUN_ID]}, ` +
|
||||||
|
`Priority: ${priorityLevelToString(priorityLevel)}, ` +
|
||||||
|
`Queue Size: ${sharedProfilingArray[QUEUE_SIZE]}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('creates a basic flamegraph', () => {
|
||||||
|
Scheduler.unstable_startLoggingProfilingEvents();
|
||||||
|
|
||||||
|
Scheduler.unstable_advanceTime(100);
|
||||||
|
scheduleCallback(
|
||||||
|
NormalPriority,
|
||||||
|
() => {
|
||||||
|
Scheduler.unstable_advanceTime(300);
|
||||||
|
Scheduler.unstable_yieldValue(getProfilingInfo());
|
||||||
|
scheduleCallback(
|
||||||
|
UserBlockingPriority,
|
||||||
|
() => {
|
||||||
|
Scheduler.unstable_yieldValue(getProfilingInfo());
|
||||||
|
Scheduler.unstable_advanceTime(300);
|
||||||
|
},
|
||||||
|
{label: 'Bar'},
|
||||||
|
);
|
||||||
|
Scheduler.unstable_advanceTime(100);
|
||||||
|
Scheduler.unstable_yieldValue('Yield');
|
||||||
|
return () => {
|
||||||
|
Scheduler.unstable_yieldValue(getProfilingInfo());
|
||||||
|
Scheduler.unstable_advanceTime(300);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{label: 'Foo'},
|
||||||
|
);
|
||||||
|
expect(Scheduler).toFlushAndYieldThrough([
|
||||||
|
'Task: 1, Run: 1, Priority: Normal, Queue Size: 1',
|
||||||
|
'Yield',
|
||||||
|
]);
|
||||||
|
Scheduler.unstable_advanceTime(100);
|
||||||
|
expect(Scheduler).toFlushAndYield([
|
||||||
|
'Task: 2, Run: 2, Priority: User-blocking, Queue Size: 2',
|
||||||
|
'Task: 1, Run: 3, Priority: Normal, Queue Size: 1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(getProfilingInfo()).toEqual('Empty Queue');
|
||||||
|
|
||||||
|
expect(stopProfilingAndPrintFlamegraph()).toEqual(
|
||||||
|
`
|
||||||
|
!!! Main thread │ ██
|
||||||
|
Task 2 [User-blocking] │ ░░░░██████
|
||||||
|
Task 1 [Normal] │ ████████░░░░░░░░██████
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks when a task is canceled', () => {
|
||||||
|
Scheduler.unstable_startLoggingProfilingEvents();
|
||||||
|
|
||||||
|
const task = scheduleCallback(NormalPriority, () => {
|
||||||
|
Scheduler.unstable_yieldValue(getProfilingInfo());
|
||||||
|
Scheduler.unstable_advanceTime(300);
|
||||||
|
Scheduler.unstable_yieldValue('Yield');
|
||||||
|
return () => {
|
||||||
|
Scheduler.unstable_yieldValue('Continuation');
|
||||||
|
Scheduler.unstable_advanceTime(200);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Scheduler).toFlushAndYieldThrough([
|
||||||
|
'Task: 1, Run: 1, Priority: Normal, Queue Size: 1',
|
||||||
|
'Yield',
|
||||||
|
]);
|
||||||
|
Scheduler.unstable_advanceTime(100);
|
||||||
|
|
||||||
|
cancelCallback(task);
|
||||||
|
|
||||||
|
// Advance more time. This should not affect the size of the main
|
||||||
|
// thread row, since the Scheduler queue is empty.
|
||||||
|
Scheduler.unstable_advanceTime(1000);
|
||||||
|
expect(Scheduler).toFlushWithoutYielding();
|
||||||
|
|
||||||
|
// The main thread row should end when the callback is cancelled.
|
||||||
|
expect(stopProfilingAndPrintFlamegraph()).toEqual(
|
||||||
|
`
|
||||||
|
!!! Main thread │ ██
|
||||||
|
Task 1 [Normal] │██████░░🡐 canceled
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks when a task errors', () => {
|
||||||
|
Scheduler.unstable_startLoggingProfilingEvents();
|
||||||
|
|
||||||
|
scheduleCallback(NormalPriority, () => {
|
||||||
|
Scheduler.unstable_advanceTime(300);
|
||||||
|
throw Error('Oops');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Scheduler).toFlushAndThrow('Oops');
|
||||||
|
Scheduler.unstable_advanceTime(100);
|
||||||
|
|
||||||
|
// Advance more time. This should not affect the size of the main
|
||||||
|
// thread row, since the Scheduler queue is empty.
|
||||||
|
Scheduler.unstable_advanceTime(1000);
|
||||||
|
expect(Scheduler).toFlushWithoutYielding();
|
||||||
|
|
||||||
|
// The main thread row should end when the callback is cancelled.
|
||||||
|
expect(stopProfilingAndPrintFlamegraph()).toEqual(
|
||||||
|
`
|
||||||
|
!!! Main thread │
|
||||||
|
Task 1 [Normal] │██████🡐 errored
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles cancelling a task that already finished', () => {
|
||||||
|
Scheduler.unstable_startLoggingProfilingEvents();
|
||||||
|
|
||||||
|
const task = scheduleCallback(NormalPriority, () => {
|
||||||
|
Scheduler.unstable_yieldValue('A');
|
||||||
|
Scheduler.unstable_advanceTime(1000);
|
||||||
|
});
|
||||||
|
expect(Scheduler).toFlushAndYield(['A']);
|
||||||
|
cancelCallback(task);
|
||||||
|
expect(stopProfilingAndPrintFlamegraph()).toEqual(
|
||||||
|
`
|
||||||
|
!!! Main thread │
|
||||||
|
Task 1 [Normal] │████████████████████
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles cancelling a task multiple times', () => {
|
||||||
|
Scheduler.unstable_startLoggingProfilingEvents();
|
||||||
|
|
||||||
|
scheduleCallback(
|
||||||
|
NormalPriority,
|
||||||
|
() => {
|
||||||
|
Scheduler.unstable_yieldValue('A');
|
||||||
|
Scheduler.unstable_advanceTime(1000);
|
||||||
|
},
|
||||||
|
{label: 'A'},
|
||||||
|
);
|
||||||
|
Scheduler.unstable_advanceTime(200);
|
||||||
|
const task = scheduleCallback(
|
||||||
|
NormalPriority,
|
||||||
|
() => {
|
||||||
|
Scheduler.unstable_yieldValue('B');
|
||||||
|
Scheduler.unstable_advanceTime(1000);
|
||||||
|
},
|
||||||
|
{label: 'B'},
|
||||||
|
);
|
||||||
|
Scheduler.unstable_advanceTime(400);
|
||||||
|
cancelCallback(task);
|
||||||
|
cancelCallback(task);
|
||||||
|
cancelCallback(task);
|
||||||
|
expect(Scheduler).toFlushAndYield(['A']);
|
||||||
|
expect(stopProfilingAndPrintFlamegraph()).toEqual(
|
||||||
|
`
|
||||||
|
!!! Main thread │████████████
|
||||||
|
Task 1 [Normal] │░░░░░░░░░░░░████████████████████
|
||||||
|
Task 2 [Normal] │ ░░░░░░░░🡐 canceled
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles cancelling a delayed task', () => {
|
||||||
|
Scheduler.unstable_startLoggingProfilingEvents();
|
||||||
|
const task = scheduleCallback(
|
||||||
|
NormalPriority,
|
||||||
|
() => Scheduler.unstable_yieldValue('A'),
|
||||||
|
{delay: 1000},
|
||||||
|
);
|
||||||
|
cancelCallback(task);
|
||||||
|
expect(Scheduler).toFlushWithoutYielding();
|
||||||
|
expect(stopProfilingAndPrintFlamegraph()).toEqual(
|
||||||
|
`
|
||||||
|
!!! Main thread │
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resizes event log buffer if there are many events', () => {
|
||||||
|
const tasks = [];
|
||||||
|
for (let i = 0; i < 5000; i++) {
|
||||||
|
tasks.push(scheduleCallback(NormalPriority, () => {}));
|
||||||
|
}
|
||||||
|
expect(getProfilingInfo()).toEqual('Suspended, Queue Size: 5000');
|
||||||
|
tasks.forEach(task => cancelCallback(task));
|
||||||
|
expect(getProfilingInfo()).toEqual('Empty Queue');
|
||||||
|
});
|
||||||
|
});
|
|
@ -13,3 +13,5 @@ export const {
|
||||||
requestTimerEventBeforeFirstFrame,
|
requestTimerEventBeforeFirstFrame,
|
||||||
enableMessageLoopImplementation,
|
enableMessageLoopImplementation,
|
||||||
} = require('SchedulerFeatureFlags');
|
} = require('SchedulerFeatureFlags');
|
||||||
|
|
||||||
|
export const enableProfiling = __PROFILE__;
|
||||||
|
|
|
@ -53,8 +53,9 @@ if (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const initialTime = Date.now();
|
||||||
getCurrentTime = function() {
|
getCurrentTime = function() {
|
||||||
return Date.now();
|
return Date.now() - initialTime;
|
||||||
};
|
};
|
||||||
requestHostCallback = function(cb) {
|
requestHostCallback = function(cb) {
|
||||||
if (_callback !== null) {
|
if (_callback !== null) {
|
||||||
|
@ -111,10 +112,15 @@ if (
|
||||||
typeof requestIdleCallback === 'function' &&
|
typeof requestIdleCallback === 'function' &&
|
||||||
typeof cancelIdleCallback === 'function';
|
typeof cancelIdleCallback === 'function';
|
||||||
|
|
||||||
getCurrentTime =
|
if (
|
||||||
typeof performance === 'object' && typeof performance.now === 'function'
|
typeof performance === 'object' &&
|
||||||
? () => performance.now()
|
typeof performance.now === 'function'
|
||||||
: () => Date.now();
|
) {
|
||||||
|
getCurrentTime = () => performance.now();
|
||||||
|
} else {
|
||||||
|
const initialTime = Date.now();
|
||||||
|
getCurrentTime = () => Date.now() - initialTime;
|
||||||
|
}
|
||||||
|
|
||||||
let isRAFLoopRunning = false;
|
let isRAFLoopRunning = false;
|
||||||
let isMessageLoopRunning = false;
|
let isMessageLoopRunning = false;
|
||||||
|
|
|
@ -411,7 +411,13 @@ const bundles = [
|
||||||
|
|
||||||
/******* React Scheduler (experimental) *******/
|
/******* React Scheduler (experimental) *******/
|
||||||
{
|
{
|
||||||
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
|
bundleTypes: [
|
||||||
|
NODE_DEV,
|
||||||
|
NODE_PROD,
|
||||||
|
FB_WWW_DEV,
|
||||||
|
FB_WWW_PROD,
|
||||||
|
FB_WWW_PROFILING,
|
||||||
|
],
|
||||||
moduleType: ISOMORPHIC,
|
moduleType: ISOMORPHIC,
|
||||||
entry: 'scheduler',
|
entry: 'scheduler',
|
||||||
global: 'Scheduler',
|
global: 'Scheduler',
|
||||||
|
|
|
@ -21,6 +21,11 @@ module.exports = {
|
||||||
process: true,
|
process: true,
|
||||||
setImmediate: true,
|
setImmediate: true,
|
||||||
Buffer: true,
|
Buffer: true,
|
||||||
|
|
||||||
|
// Scheduler profiling
|
||||||
|
SharedArrayBuffer: true,
|
||||||
|
Int32Array: true,
|
||||||
|
ArrayBuffer: true,
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 5,
|
ecmaVersion: 5,
|
||||||
|
|
|
@ -22,6 +22,11 @@ module.exports = {
|
||||||
// Node.js Server Rendering
|
// Node.js Server Rendering
|
||||||
setImmediate: true,
|
setImmediate: true,
|
||||||
Buffer: true,
|
Buffer: true,
|
||||||
|
|
||||||
|
// Scheduler profiling
|
||||||
|
SharedArrayBuffer: true,
|
||||||
|
Int32Array: true,
|
||||||
|
ArrayBuffer: true,
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 5,
|
ecmaVersion: 5,
|
||||||
|
|
|
@ -21,6 +21,11 @@ module.exports = {
|
||||||
// Fabric. See https://github.com/facebook/react/pull/15490
|
// Fabric. See https://github.com/facebook/react/pull/15490
|
||||||
// for more information
|
// for more information
|
||||||
nativeFabricUIManager: true,
|
nativeFabricUIManager: true,
|
||||||
|
|
||||||
|
// Scheduler profiling
|
||||||
|
SharedArrayBuffer: true,
|
||||||
|
Int32Array: true,
|
||||||
|
ArrayBuffer: true,
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 5,
|
ecmaVersion: 5,
|
||||||
|
|
|
@ -24,6 +24,11 @@ module.exports = {
|
||||||
define: true,
|
define: true,
|
||||||
require: true,
|
require: true,
|
||||||
global: true,
|
global: true,
|
||||||
|
|
||||||
|
// Scheduler profiling
|
||||||
|
SharedArrayBuffer: true,
|
||||||
|
Int32Array: true,
|
||||||
|
ArrayBuffer: true,
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 5,
|
ecmaVersion: 5,
|
||||||
|
|
Loading…
Reference in New Issue