1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ * @jest-environment node
9
+ */
10
+
11
+/* eslint-disable no-for-of-loops/no-for-of-loops */
12
+
13
+'use strict';
14
+
15
+let Scheduler;
16
+let sharedProfilingArray;
17
+// let runWithPriority;
18
+let ImmediatePriority;
19
+let UserBlockingPriority;
20
+let NormalPriority;
21
+let LowPriority;
22
+let IdlePriority;
23
+let scheduleCallback;
24
+let cancelCallback;
25
+// let wrapCallback;
26
+// let getCurrentPriorityLevel;
27
+// let shouldYield;
28
+
29
+function priorityLevelToString(priorityLevel) {
30
+ switch (priorityLevel) {
31
+ case ImmediatePriority:
32
+ return 'Immediate';
33
+ case UserBlockingPriority:
34
+ return 'User-blocking';
35
+ case NormalPriority:
36
+ return 'Normal';
37
+ case LowPriority:
38
+ return 'Low';
39
+ case IdlePriority:
40
+ return 'Idle';
41
+ default:
42
+ return null;
43
+ }
44
+}
45
+
46
+describe('Scheduler', () => {
47
+ if (!__PROFILE__) {
48
+ // The tests in this suite only apply when profiling is on
49
+ it('profiling APIs are not available', () => {
50
+ Scheduler = require('scheduler');
51
+ expect(Scheduler.unstable_stopLoggingProfilingEvents).toBe(null);
52
+ expect(Scheduler.unstable_sharedProfilingBuffer).toBe(null);
53
+ });
54
+ return;
55
+ }
56
+
57
+ beforeEach(() => {
58
+ jest.resetModules();
59
+ jest.mock('scheduler', () => require('scheduler/unstable_mock'));
60
+ Scheduler = require('scheduler');
61
+
62
+ sharedProfilingArray = new Int32Array(
63
+ Scheduler.unstable_sharedProfilingBuffer,
64
+ );
65
+
66
+ // runWithPriority = Scheduler.unstable_runWithPriority;
67
+ ImmediatePriority = Scheduler.unstable_ImmediatePriority;
68
+ UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
69
+ NormalPriority = Scheduler.unstable_NormalPriority;
70
+ LowPriority = Scheduler.unstable_LowPriority;
71
+ IdlePriority = Scheduler.unstable_IdlePriority;
72
+ scheduleCallback = Scheduler.unstable_scheduleCallback;
73
+ cancelCallback = Scheduler.unstable_cancelCallback;
74
+ // wrapCallback = Scheduler.unstable_wrapCallback;
75
+ // getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel;
76
+ // shouldYield = Scheduler.unstable_shouldYield;
77
+ });
78
+
79
+ const PRIORITY = 0;
80
+ const CURRENT_TASK_ID = 1;
81
+ const CURRENT_RUN_ID = 2;
82
+ const QUEUE_SIZE = 3;
83
+
84
+ afterEach(() => {
85
+ if (sharedProfilingArray[QUEUE_SIZE] !== 0) {
86
+ throw Error(
87
+ 'Test exited, but the shared profiling buffer indicates that a task ' +
88
+ 'is still running',
89
+ );
90
+ }
91
+ });
92
+
93
+ const TaskStartEvent = 1;
94
+ const TaskCompleteEvent = 2;
95
+ const TaskErrorEvent = 3;
96
+ const TaskCancelEvent = 4;
97
+ const TaskRunEvent = 5;
98
+ const TaskYieldEvent = 6;
99
+ const SchedulerSuspendEvent = 7;
100
+ const SchedulerResumeEvent = 8;
101
+
102
+ function stopProfilingAndPrintFlamegraph() {
103
+ const eventLog = new Int32Array(
104
+ Scheduler.unstable_stopLoggingProfilingEvents(),
105
+ );
106
+
107
+ const tasks = new Map();
108
+ const mainThreadRuns = [];
109
+
110
+ let i = 0;
111
+ processLog: while (i < eventLog.length) {
112
+ const instruction = eventLog[i];
113
+ const time = eventLog[i + 1];
114
+ switch (instruction) {
115
+ case 0: {
116
+ break processLog;
117
+ }
118
+ case TaskStartEvent: {
119
+ const taskId = eventLog[i + 2];
120
+ const priorityLevel = eventLog[i + 3];
121
+ const task = {
122
+ id: taskId,
123
+ priorityLevel,
124
+ label: null,
125
+ start: time,
126
+ end: -1,
127
+ exitStatus: null,
128
+ runs: [],
129
+ };
130
+ tasks.set(taskId, task);
131
+ i += 4;
132
+ break;
133
+ }
134
+ case TaskCompleteEvent: {
135
+ const taskId = eventLog[i + 2];
136
+ const task = tasks.get(taskId);
137
+ if (task === undefined) {
138
+ throw Error('Task does not exist.');
139
+ }
140
+ task.end = time;
141
+ task.exitStatus = 'completed';
142
+ i += 3;
143
+ break;
144
+ }
145
+ case TaskErrorEvent: {
146
+ const taskId = eventLog[i + 2];
147
+ const task = tasks.get(taskId);
148
+ if (task === undefined) {
149
+ throw Error('Task does not exist.');
150
+ }
151
+ task.end = time;
152
+ task.exitStatus = 'errored';
153
+ i += 3;
154
+ break;
155
+ }
156
+ case TaskCancelEvent: {
157
+ const taskId = eventLog[i + 2];
158
+ const task = tasks.get(taskId);
159
+ if (task === undefined) {
160
+ throw Error('Task does not exist.');
161
+ }
162
+ task.end = time;
163
+ task.exitStatus = 'canceled';
164
+ i += 3;
165
+ break;
166
+ }
167
+ case TaskRunEvent:
168
+ case TaskYieldEvent: {
169
+ const taskId = eventLog[i + 2];
170
+ const task = tasks.get(taskId);
171
+ if (task === undefined) {
172
+ throw Error('Task does not exist.');
173
+ }
174
+ task.runs.push(time);
175
+ i += 4;
176
+ break;
177
+ }
178
+ case SchedulerSuspendEvent:
179
+ case SchedulerResumeEvent: {
180
+ mainThreadRuns.push(time);
181
+ i += 3;
182
+ break;
183
+ }
184
+ default: {
185
+ throw Error('Unknown instruction type: ' + instruction);
186
+ }
187
+ }
188
+ }
189
+
190
+ // Now we can render the tasks as a flamegraph.
191
+ const labelColumnWidth = 30;
192
+ const msPerChar = 50;
193
+
194
+ let result = '';
195
+
196
+ const mainThreadLabelColumn = '!!! Main thread ';
197
+ let mainThreadTimelineColumn = '';
198
+ let isMainThreadBusy = false;
199
+ for (const time of mainThreadRuns) {
200
+ const index = time / msPerChar;
201
+ mainThreadTimelineColumn += (isMainThreadBusy ? '█' : ' ').repeat(
202
+ index - mainThreadTimelineColumn.length,
203
+ );
204
+ isMainThreadBusy = !isMainThreadBusy;
205
+ }
206
+ result += `${mainThreadLabelColumn}│${mainThreadTimelineColumn}\n`;
207
+
208
+ const tasksByPriority = Array.from(tasks.values()).sort(
209
+ (t1, t2) => t1.priorityLevel - t2.priorityLevel,
210
+ );
211
+
212
+ for (const task of tasksByPriority) {
213
+ let label = task.label;
214
+ if (label === undefined) {
215
+ label = 'Task';
216
+ }
217
+ let labelColumn = `Task ${task.id} [${priorityLevelToString(
218
+ task.priorityLevel,
219
+ )}]`;
220
+ labelColumn += ' '.repeat(labelColumnWidth - labelColumn.length - 1);
221
+
222
+ // Add empty space up until the start mark
223
+ let timelineColumn = ' '.repeat(task.start / msPerChar);
224
+
225
+ let isRunning = false;
226
+ for (const time of task.runs) {
227
+ const index = time / msPerChar;
228
+ timelineColumn += (isRunning ? '█' : '░').repeat(
229
+ index - timelineColumn.length,
230
+ );
231
+ isRunning = !isRunning;
232
+ }
233
+
234
+ const endIndex = task.end / msPerChar;
235
+ timelineColumn += (isRunning ? '█' : '░').repeat(
236
+ endIndex - timelineColumn.length,
237
+ );
238
+
239
+ if (task.exitStatus !== 'completed') {
240
+ timelineColumn += `🡐 ${task.exitStatus}`;
241
+ }
242
+
243
+ result += `${labelColumn}│${timelineColumn}\n`;
244
+ }
245
+
246
+ return '\n' + result;
247
+ }
248
+
249
+ function getProfilingInfo() {
250
+ const queueSize = sharedProfilingArray[QUEUE_SIZE];
251
+ if (queueSize === 0) {
252
+ return 'Empty Queue';
253
+ }
254
+ const priorityLevel = sharedProfilingArray[PRIORITY];
255
+ if (priorityLevel === 0) {
256
+ return 'Suspended, Queue Size: ' + queueSize;
257
+ }
258
+ return (
259
+ `Task: ${sharedProfilingArray[CURRENT_TASK_ID]}, ` +
260
+ `Run: ${sharedProfilingArray[CURRENT_RUN_ID]}, ` +
261
+ `Priority: ${priorityLevelToString(priorityLevel)}, ` +
262
+ `Queue Size: ${sharedProfilingArray[QUEUE_SIZE]}`
263
+ );
264
+ }
265
+
266
+ it('creates a basic flamegraph', () => {
267
+ Scheduler.unstable_startLoggingProfilingEvents();
268
+
269
+ Scheduler.unstable_advanceTime(100);
270
+ scheduleCallback(
271
+ NormalPriority,
272
+ () => {
273
+ Scheduler.unstable_advanceTime(300);
274
+ Scheduler.unstable_yieldValue(getProfilingInfo());
275
+ scheduleCallback(
276
+ UserBlockingPriority,
277
+ () => {
278
+ Scheduler.unstable_yieldValue(getProfilingInfo());
279
+ Scheduler.unstable_advanceTime(300);
280
+ },
281
+ {label: 'Bar'},
282
+ );
283
+ Scheduler.unstable_advanceTime(100);
284
+ Scheduler.unstable_yieldValue('Yield');
285
+ return () => {
286
+ Scheduler.unstable_yieldValue(getProfilingInfo());
287
+ Scheduler.unstable_advanceTime(300);
288
+ };
289
+ },
290
+ {label: 'Foo'},
291
+ );
292
+ expect(Scheduler).toFlushAndYieldThrough([
293
+ 'Task: 1, Run: 1, Priority: Normal, Queue Size: 1',
294
+ 'Yield',
295
+ ]);
296
+ Scheduler.unstable_advanceTime(100);
297
+ expect(Scheduler).toFlushAndYield([
298
+ 'Task: 2, Run: 2, Priority: User-blocking, Queue Size: 2',
299
+ 'Task: 1, Run: 3, Priority: Normal, Queue Size: 1',
300
+ ]);
301
+
302
+ expect(getProfilingInfo()).toEqual('Empty Queue');
303
+
304
+ expect(stopProfilingAndPrintFlamegraph()).toEqual(
305
+ `
306
+!!! Main thread │ ██
307
+Task 2 [User-blocking] │ ░░░░██████
308
+Task 1 [Normal] │ ████████░░░░░░░░██████
309
+`,
310
+ );
311
+ });
312
+
313
+ it('marks when a task is canceled', () => {
314
+ Scheduler.unstable_startLoggingProfilingEvents();
315
+
316
+ const task = scheduleCallback(NormalPriority, () => {
317
+ Scheduler.unstable_yieldValue(getProfilingInfo());
318
+ Scheduler.unstable_advanceTime(300);
319
+ Scheduler.unstable_yieldValue('Yield');
320
+ return () => {
321
+ Scheduler.unstable_yieldValue('Continuation');
322
+ Scheduler.unstable_advanceTime(200);
323
+ };
324
+ });
325
+
326
+ expect(Scheduler).toFlushAndYieldThrough([
327
+ 'Task: 1, Run: 1, Priority: Normal, Queue Size: 1',
328
+ 'Yield',
329
+ ]);
330
+ Scheduler.unstable_advanceTime(100);
331
+
332
+ cancelCallback(task);
333
+
334
+ // Advance more time. This should not affect the size of the main
335
+ // thread row, since the Scheduler queue is empty.
336
+ Scheduler.unstable_advanceTime(1000);
337
+ expect(Scheduler).toFlushWithoutYielding();
338
+
339
+ // The main thread row should end when the callback is cancelled.
340
+ expect(stopProfilingAndPrintFlamegraph()).toEqual(
341
+ `
342
+!!! Main thread │ ██
343
+Task 1 [Normal] │██████░░🡐 canceled
344
+`,
345
+ );
346
+ });
347
+
348
+ it('marks when a task errors', () => {
349
+ Scheduler.unstable_startLoggingProfilingEvents();
350
+
351
+ scheduleCallback(NormalPriority, () => {
352
+ Scheduler.unstable_advanceTime(300);
353
+ throw Error('Oops');
354
+ });
355
+
356
+ expect(Scheduler).toFlushAndThrow('Oops');
357
+ Scheduler.unstable_advanceTime(100);
358
+
359
+ // Advance more time. This should not affect the size of the main
360
+ // thread row, since the Scheduler queue is empty.
361
+ Scheduler.unstable_advanceTime(1000);
362
+ expect(Scheduler).toFlushWithoutYielding();
363
+
364
+ // The main thread row should end when the callback is cancelled.
365
+ expect(stopProfilingAndPrintFlamegraph()).toEqual(
366
+ `
367
+!!! Main thread │
368
+Task 1 [Normal] │██████🡐 errored
369
+`,
370
+ );
371
+ });
372
+
373
+ it('handles cancelling a task that already finished', () => {
374
+ Scheduler.unstable_startLoggingProfilingEvents();
375
+
376
+ const task = scheduleCallback(NormalPriority, () => {
377
+ Scheduler.unstable_yieldValue('A');
378
+ Scheduler.unstable_advanceTime(1000);
379
+ });
380
+ expect(Scheduler).toFlushAndYield(['A']);
381
+ cancelCallback(task);
382
+ expect(stopProfilingAndPrintFlamegraph()).toEqual(
383
+ `
384
+!!! Main thread │
385
+Task 1 [Normal] │████████████████████
386
+`,
387
+ );
388
+ });
389
+
390
+ it('handles cancelling a task multiple times', () => {
391
+ Scheduler.unstable_startLoggingProfilingEvents();
392
+
393
+ scheduleCallback(
394
+ NormalPriority,
395
+ () => {
396
+ Scheduler.unstable_yieldValue('A');
397
+ Scheduler.unstable_advanceTime(1000);
398
+ },
399
+ {label: 'A'},
400
+ );
401
+ Scheduler.unstable_advanceTime(200);
402
+ const task = scheduleCallback(
403
+ NormalPriority,
404
+ () => {
405
+ Scheduler.unstable_yieldValue('B');
406
+ Scheduler.unstable_advanceTime(1000);
407
+ },
408
+ {label: 'B'},
409
+ );
410
+ Scheduler.unstable_advanceTime(400);
411
+ cancelCallback(task);
412
+ cancelCallback(task);
413
+ cancelCallback(task);
414
+ expect(Scheduler).toFlushAndYield(['A']);
415
+ expect(stopProfilingAndPrintFlamegraph()).toEqual(
416
+ `
417
+!!! Main thread │████████████
418
+Task 1 [Normal] │░░░░░░░░░░░░████████████████████
419
+Task 2 [Normal] │ ░░░░░░░░🡐 canceled
420
+`,
421
+ );
422
+ });
423
+
424
+ it('handles cancelling a delayed task', () => {
425
+ Scheduler.unstable_startLoggingProfilingEvents();
426
+ const task = scheduleCallback(
427
+ NormalPriority,
428
+ () => Scheduler.unstable_yieldValue('A'),
429
+ {delay: 1000},
430
+ );
431
+ cancelCallback(task);
432
+ expect(Scheduler).toFlushWithoutYielding();
433
+ expect(stopProfilingAndPrintFlamegraph()).toEqual(
434
+ `
435
+!!! Main thread │
436
+`,
437
+ );
438
+ });
439
+
440
+ it('resizes event log buffer if there are many events', () => {
441
+ const tasks = [];
442
+ for (let i = 0; i < 5000; i++) {
443
+ tasks.push(scheduleCallback(NormalPriority, () => {}));
444
+ }
445
+ expect(getProfilingInfo()).toEqual('Suspended, Queue Size: 5000');
446
+ tasks.forEach(task => cancelCallback(task));
447
+ expect(getProfilingInfo()).toEqual('Empty Queue');
448
+ });
449
+});