Skip to content

Commit

Permalink
Expose shared array buffer with profiling info
Browse files Browse the repository at this point in the history
Array contains

- the priority Scheduler is currently running
- the size of the queue
- the id of the currently running task
  • Loading branch information
acdlite committed Aug 13, 2019
1 parent 161aba9 commit 917d899
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 10 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ module.exports = {
],

globals: {
SharedArrayBuffer: true,

spyOnDev: true,
spyOnDevAndProd: true,
spyOnProd: true,
Expand Down
4 changes: 4 additions & 0 deletions packages/scheduler/npm/umd/scheduler.development.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,9 @@
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_UserBlockingPriority;
},
get unstable_sharedProfilingBuffer() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_getFirstCallbackNode;
},
});
});
4 changes: 4 additions & 0 deletions packages/scheduler/npm/umd/scheduler.production.min.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,9 @@
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_UserBlockingPriority;
},
get unstable_sharedProfilingBuffer() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_getFirstCallbackNode;
},
});
});
4 changes: 4 additions & 0 deletions packages/scheduler/npm/umd/scheduler.profiling.min.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,9 @@
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_UserBlockingPriority;
},
get unstable_sharedProfilingBuffer() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_getFirstCallbackNode;
},
});
});
30 changes: 25 additions & 5 deletions packages/scheduler/src/Scheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import {
enableSchedulerDebugging,
enableSharedProfilingBuffer,
enableProfiling,
} from './SchedulerFeatureFlags';
import {
Expand All @@ -33,6 +34,7 @@ import {
IdlePriority,
} from './SchedulerPriorities';
import {
sharedProfilingBuffer,
markTaskRun,
markTaskYield,
markTaskCompleted,
Expand Down Expand Up @@ -83,6 +85,10 @@ function requestHostCallbackWithProfiling(cb) {
}
}

// Expose a shared array buffer that contains profiling information.
export const unstable_sharedProfilingBuffer =
enableProfiling && enableSharedProfilingBuffer ? sharedProfilingBuffer : null;

const requestHostCallback = enableProfiling
? requestHostCallbackWithProfiling
: requestHostCallbackWithoutProfiling;
Expand All @@ -96,7 +102,10 @@ function flushTask(task, callback, currentTime) {
markTaskYield(task);
return continuationCallback;
} else {
markTaskCompleted(task);
if (enableProfiling) {
markTaskCompleted(task);
task.isQueued = false;
}
return null;
}
}
Expand All @@ -113,7 +122,10 @@ function advanceTimers(currentTime) {
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
markTaskStart(timer);
if (enableProfiling) {
markTaskStart(timer);
timer.isQueued = true;
}
} else {
// Remaining timers are pending.
return;
Expand Down Expand Up @@ -201,7 +213,10 @@ function flushWork(hasTimeRemaining, initialTime) {
}
} catch (error) {
if (currentTask !== null) {
markTaskErrored(currentTask);
if (enableProfiling) {
markTaskErrored(currentTask);
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
Expand Down Expand Up @@ -332,6 +347,7 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
};

if (enableProfiling) {
newTask.isQueued = false;
if (typeof options === 'object' && options !== null) {
newTask.label = label;
}
Expand All @@ -355,7 +371,10 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
markTaskStart(newTask);
if (enableProfiling) {
markTaskStart(newTask);
newTask.isQueued = true;
}
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
Expand Down Expand Up @@ -384,8 +403,9 @@ function unstable_getFirstCallbackNode() {
}

function unstable_cancelCallback(task) {
if (enableProfiling && task.callback !== null) {
if (enableProfiling && task.isQueued) {
markTaskCanceled(task);
task.isQueued = false;
}
if (task !== null && task === peek(taskQueue)) {
pop(taskQueue);
Expand Down
1 change: 1 addition & 0 deletions packages/scheduler/src/SchedulerFeatureFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export const requestTimerEventBeforeFirstFrame = false;
export const enableMessageLoopImplementation = false;
export const enableProfiling = __PROFILE__;
export const enableUserTimingAPI = false;
export const enableSharedProfilingBuffer = false;
3 changes: 2 additions & 1 deletion packages/scheduler/src/SchedulerPriorities.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
* @flow
*/

export type PriorityLevel = 1 | 2 | 3 | 4 | 5;
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;
Expand Down
53 changes: 52 additions & 1 deletion packages/scheduler/src/SchedulerProfiling.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import type {PriorityLevel} from './SchedulerPriorities';
import {
enableProfiling,
enableUserTimingAPI as enableUserTimingAPIFeatureFlag,
enableSharedProfilingBuffer,
} from './SchedulerFeatureFlags';

import {NoPriority} from './SchedulerPriorities';

const enableUserTimingAPI =
enableUserTimingAPIFeatureFlag &&
typeof performance !== 'undefined' &&
Expand All @@ -22,8 +25,35 @@ const enableUserTimingAPI =
let runIdCounter: number = 0;
let mainThreadIdCounter: number = 0;

const length = 3;
const size = Int32Array.BYTES_PER_ELEMENT * length;
export const sharedProfilingBuffer =
// $FlowFixMe Flow doesn't know about SharedArrayBuffer
typeof SharedArrayBuffer === 'function'
? new SharedArrayBuffer(size)
: // $FlowFixMe Flow doesn't know about ArrayBuffer
new ArrayBuffer(size);
const profilingInfo = enableSharedProfilingBuffer
? new Int32Array(sharedProfilingBuffer)
: null;

const PRIORITY = 0;
const CURRENT_TASK_ID = 1;
const QUEUE_SIZE = 2;

if (enableSharedProfilingBuffer && profilingInfo !== null) {
profilingInfo[PRIORITY] = NoPriority;
// This is maintained with a counter, because the size of the priority queue
// array might include canceled tasks.
profilingInfo[QUEUE_SIZE] = 0;
profilingInfo[CURRENT_TASK_ID] = 0;
}

export function markTaskStart(task: {id: number}) {
if (enableProfiling) {
if (enableSharedProfilingBuffer && profilingInfo !== null) {
profilingInfo[QUEUE_SIZE]++;
}
if (enableUserTimingAPI) {
// Use extra field to track if delayed task starts.
const taskStartMark = `SchedulerTask-${task.id}-Start`;
Expand All @@ -39,6 +69,11 @@ export function markTaskCompleted(task: {
label?: string,
}) {
if (enableProfiling) {
if (enableSharedProfilingBuffer && profilingInfo !== null) {
profilingInfo[PRIORITY] = NoPriority;
profilingInfo[CURRENT_TASK_ID] = 0;
profilingInfo[QUEUE_SIZE]--;
}
if (enableUserTimingAPI) {
const info = JSON.stringify({
priorityLevel: task.priorityLevel,
Expand All @@ -58,6 +93,9 @@ export function markTaskCanceled(task: {
label?: string,
}) {
if (enableProfiling) {
if (enableSharedProfilingBuffer && profilingInfo !== null) {
profilingInfo[QUEUE_SIZE]--;
}
if (enableUserTimingAPI) {
const info = JSON.stringify({
priorityLevel: task.priorityLevel,
Expand All @@ -77,6 +115,11 @@ export function markTaskErrored(task: {
label?: string,
}) {
if (enableProfiling) {
if (enableSharedProfilingBuffer && profilingInfo !== null) {
profilingInfo[PRIORITY] = NoPriority;
profilingInfo[CURRENT_TASK_ID] = 0;
profilingInfo[QUEUE_SIZE]--;
}
if (enableUserTimingAPI) {
const info = JSON.stringify({
priorityLevel: task.priorityLevel,
Expand All @@ -90,8 +133,12 @@ export function markTaskErrored(task: {
}
}

export function markTaskRun(task: {id: number}) {
export function markTaskRun(task: {id: number, priorityLevel: PriorityLevel}) {
if (enableProfiling) {
if (enableSharedProfilingBuffer && profilingInfo !== null) {
profilingInfo[PRIORITY] = task.priorityLevel;
profilingInfo[CURRENT_TASK_ID] = task.id;
}
if (enableUserTimingAPI) {
runIdCounter++;
const runMark = `SchedulerTask-${task.id}-Run-${runIdCounter}`;
Expand All @@ -103,6 +150,10 @@ export function markTaskRun(task: {id: number}) {

export function markTaskYield(task: {id: number}) {
if (enableProfiling) {
if (enableSharedProfilingBuffer && profilingInfo !== null) {
profilingInfo[PRIORITY] = NoPriority;
profilingInfo[CURRENT_TASK_ID] = 0;
}
if (enableUserTimingAPI) {
const yieldMark = `SchedulerTask-${task.id}-Yield-${runIdCounter}`;
performance.mark(yieldMark);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
'use strict';

let Scheduler;
let sharedProfilingArray;
// let runWithPriority;
let ImmediatePriority;
let UserBlockingPriority;
Expand Down Expand Up @@ -51,8 +52,13 @@ describe('Scheduler', () => {
performance = global.performance = createUserTimingPolyfill();

require('scheduler/src/SchedulerFeatureFlags').enableUserTimingAPI = true;
require('scheduler/src/SchedulerFeatureFlags').enableSharedProfilingBuffer = true;
Scheduler = require('scheduler');

sharedProfilingArray = new Int32Array(
Scheduler.unstable_sharedProfilingBuffer,
);

// runWithPriority = Scheduler.unstable_runWithPriority;
ImmediatePriority = Scheduler.unstable_ImmediatePriority;
UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
Expand All @@ -68,6 +74,12 @@ describe('Scheduler', () => {

afterEach(() => {
performance.assertAllUserTimingsAreCleared();
if (sharedProfilingArray[2] !== 0) {
throw Error(
'Test exited, but the shared profiling buffer indicates that a task ' +
'is still running',
);
}
});

function createUserTimingPolyfill() {
Expand Down Expand Up @@ -248,6 +260,42 @@ describe('Scheduler', () => {
};
}

const PRIORITY = 0;
const CURRENT_TASK_ID = 1;
const QUEUE_SIZE = 2;
function getProfilingInfo() {
const queueSize = sharedProfilingArray[QUEUE_SIZE];

let priorityStr;
switch (sharedProfilingArray[PRIORITY]) {
case 0: {
return queueSize !== 0
? 'Suspended, Queue Size: ' + queueSize
: 'Empty Queue';
}
case 1:
priorityStr = 'Immediate';
break;
case 2:
priorityStr = 'User-Blocking';
break;
case 3:
priorityStr = 'Normal';
break;
case 4:
priorityStr = 'Low';
break;
case 5:
priorityStr = 'Idle';
break;
}
return `Current Task: ${
sharedProfilingArray[QUEUE_SIZE]
}, Priority: ${priorityStr}, Queue Size: ${
sharedProfilingArray[CURRENT_TASK_ID]
}`;
}

if (!__DEV__) {
// The tests in this suite are dev only
it("empty test so Jest doesn't complain that there are no tests in this file", () => {});
Expand All @@ -260,24 +308,36 @@ describe('Scheduler', () => {
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(['Yield']);
expect(Scheduler).toFlushAndYieldThrough([
'Current Task: 1, Priority: Normal, Queue Size: 1',
'Yield',
]);
Scheduler.unstable_advanceTime(100);
expect(Scheduler).toFlushWithoutYielding();
expect(Scheduler).toFlushAndYield([
'Current Task: 2, Priority: User-Blocking, Queue Size: 2',
'Current Task: 1, Priority: Normal, Queue Size: 1',
]);

expect(getProfilingInfo()).toEqual('Empty Queue');

expect(performance.printUserTimings()).toEqual(
`
!!! Main thread │ ██
Expand All @@ -289,6 +349,7 @@ describe('Scheduler', () => {

it('marks when a task is canceled', () => {
const task = scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue(getProfilingInfo());
Scheduler.unstable_advanceTime(300);
Scheduler.unstable_yieldValue('Yield');
return () => {
Expand All @@ -297,7 +358,10 @@ describe('Scheduler', () => {
};
});

expect(Scheduler).toFlushAndYieldThrough(['Yield']);
expect(Scheduler).toFlushAndYieldThrough([
'Current Task: 1, Priority: Normal, Queue Size: 1',
'Yield',
]);
Scheduler.unstable_advanceTime(100);

cancelCallback(task);
Expand Down
Loading

0 comments on commit 917d899

Please sign in to comment.