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_Profiling).toBe(null);
52
+ });
53
+ return;
54
+ }
55
+
56
+ beforeEach(() => {
57
+ jest.resetModules();
58
+ jest.mock('scheduler', () => require('scheduler/unstable_mock'));
59
+ Scheduler = require('scheduler');
60
+
61
+ sharedProfilingArray = new Int32Array(
62
+ Scheduler.unstable_Profiling.sharedProfilingBuffer,
63
+ );
64
+
65
+ // runWithPriority = Scheduler.unstable_runWithPriority;
66
+ ImmediatePriority = Scheduler.unstable_ImmediatePriority;
67
+ UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
68
+ NormalPriority = Scheduler.unstable_NormalPriority;
69
+ LowPriority = Scheduler.unstable_LowPriority;
70
+ IdlePriority = Scheduler.unstable_IdlePriority;
71
+ scheduleCallback = Scheduler.unstable_scheduleCallback;
72
+ cancelCallback = Scheduler.unstable_cancelCallback;
73
+ // wrapCallback = Scheduler.unstable_wrapCallback;
74
+ // getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel;
75
+ // shouldYield = Scheduler.unstable_shouldYield;
76
+ });
77
+
78
+ const PRIORITY = 0;
79
+ const CURRENT_TASK_ID = 1;
80
+ const CURRENT_RUN_ID = 2;
81
+ const QUEUE_SIZE = 3;
82
+
83
+ afterEach(() => {
84
+ if (sharedProfilingArray[QUEUE_SIZE] !== 0) {
85
+ throw Error(
86
+ 'Test exited, but the shared profiling buffer indicates that a task ' +
87
+ 'is still running',
88
+ );
89
+ }
90
+ });
91
+
92
+ const TaskStartEvent = 1;
93
+ const TaskCompleteEvent = 2;
94
+ const TaskErrorEvent = 3;
95
+ const TaskCancelEvent = 4;
96
+ const TaskRunEvent = 5;
97
+ const TaskYieldEvent = 6;
98
+ const SchedulerSuspendEvent = 7;
99
+ const SchedulerResumeEvent = 8;
100
+
101
+ function stopProfilingAndPrintFlamegraph() {
102
+ const eventLog = new Int32Array(
103
+ Scheduler.unstable_Profiling.stopLoggingProfilingEvents(),
104
+ );
105
+
106
+ const tasks = new Map();
107
+ const mainThreadRuns = [];
108
+
109
+ let isSuspended = true;
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
+ if (isSuspended) {
136
+ throw Error('Task cannot Complete outside the work loop.');
137
+ }
138
+ const taskId = eventLog[i + 2];
139
+ const task = tasks.get(taskId);
140
+ if (task === undefined) {
141
+ throw Error('Task does not exist.');
142
+ }
143
+ task.end = time;
144
+ task.exitStatus = 'completed';
145
+ i += 3;
146
+ break;
147
+ }
148
+ case TaskErrorEvent: {
149
+ if (isSuspended) {
150
+ throw Error('Task cannot Error outside the work loop.');
151
+ }
152
+ const taskId = eventLog[i + 2];
153
+ const task = tasks.get(taskId);
154
+ if (task === undefined) {
155
+ throw Error('Task does not exist.');
156
+ }
157
+ task.end = time;
158
+ task.exitStatus = 'errored';
159
+ i += 3;
160
+ break;
161
+ }
162
+ case TaskCancelEvent: {
163
+ const taskId = eventLog[i + 2];
164
+ const task = tasks.get(taskId);
165
+ if (task === undefined) {
166
+ throw Error('Task does not exist.');
167
+ }
168
+ task.end = time;
169
+ task.exitStatus = 'canceled';
170
+ i += 3;
171
+ break;
172
+ }
173
+ case TaskRunEvent:
174
+ case TaskYieldEvent: {
175
+ if (isSuspended) {
176
+ throw Error('Task cannot Run or Yield outside the work loop.');
177
+ }
178
+ const taskId = eventLog[i + 2];
179
+ const task = tasks.get(taskId);
180
+ if (task === undefined) {
181
+ throw Error('Task does not exist.');
182
+ }
183
+ task.runs.push(time);
184
+ i += 4;
185
+ break;
186
+ }
187
+ case SchedulerSuspendEvent: {
188
+ if (isSuspended) {
189
+ throw Error('Scheduler cannot Suspend outside the work loop.');
190
+ }
191
+ isSuspended = true;
192
+ mainThreadRuns.push(time);
193
+ i += 3;
194
+ break;
195
+ }
196
+ case SchedulerResumeEvent: {
197
+ if (!isSuspended) {
198
+ throw Error('Scheduler cannot Resume inside the work loop.');
199
+ }
200
+ isSuspended = false;
201
+ mainThreadRuns.push(time);
202
+ i += 3;
203
+ break;
204
+ }
205
+ default: {
206
+ throw Error('Unknown instruction type: ' + instruction);
207
+ }
208
+ }
209
+ }
210
+
211
+ // Now we can render the tasks as a flamegraph.
212
+ const labelColumnWidth = 30;
213
+ const msPerChar = 50;
214
+
215
+ let result = '';
216
+
217
+ const mainThreadLabelColumn = '!!! Main thread ';
218
+ let mainThreadTimelineColumn = '';
219
+ let isMainThreadBusy = true;
220
+ for (const time of mainThreadRuns) {
221
+ const index = time / msPerChar;
222
+ mainThreadTimelineColumn += (isMainThreadBusy ? '█' : '░').repeat(
223
+ index - mainThreadTimelineColumn.length,
224
+ );
225
+ isMainThreadBusy = !isMainThreadBusy;
226
+ }
227
+ result += `${mainThreadLabelColumn}│${mainThreadTimelineColumn}\n`;
228
+
229
+ const tasksByPriority = Array.from(tasks.values()).sort(
230
+ (t1, t2) => t1.priorityLevel - t2.priorityLevel,
231
+ );
232
+
233
+ for (const task of tasksByPriority) {
234
+ let label = task.label;
235
+ if (label === undefined) {
236
+ label = 'Task';
237
+ }
238
+ let labelColumn = `Task ${task.id} [${priorityLevelToString(
239
+ task.priorityLevel,
240
+ )}]`;
241
+ labelColumn += ' '.repeat(labelColumnWidth - labelColumn.length - 1);
242
+
243
+ // Add empty space up until the start mark
244
+ let timelineColumn = ' '.repeat(task.start / msPerChar);
245
+
246
+ let isRunning = false;
247
+ for (const time of task.runs) {
248
+ const index = time / msPerChar;
249
+ timelineColumn += (isRunning ? '█' : '░').repeat(
250
+ index - timelineColumn.length,
251
+ );
252
+ isRunning = !isRunning;
253
+ }
254
+
255
+ const endIndex = task.end / msPerChar;
256
+ timelineColumn += (isRunning ? '█' : '░').repeat(
257
+ endIndex - timelineColumn.length,
258
+ );
259
+
260
+ if (task.exitStatus !== 'completed') {
261
+ timelineColumn += `🡐 ${task.exitStatus}`;
262
+ }
263
+
264
+ result += `${labelColumn}│${timelineColumn}\n`;
265
+ }
266
+
267
+ return '\n' + result;
268
+ }
269
+
270
+ function getProfilingInfo() {
271
+ const queueSize = sharedProfilingArray[QUEUE_SIZE];
272
+ if (queueSize === 0) {
273
+ return 'Empty Queue';
274
+ }
275
+ const priorityLevel = sharedProfilingArray[PRIORITY];
276
+ if (priorityLevel === 0) {
277
+ return 'Suspended, Queue Size: ' + queueSize;
278
+ }
279
+ return (
280
+ `Task: ${sharedProfilingArray[CURRENT_TASK_ID]}, ` +
281
+ `Run: ${sharedProfilingArray[CURRENT_RUN_ID]}, ` +
282
+ `Priority: ${priorityLevelToString(priorityLevel)}, ` +
283
+ `Queue Size: ${sharedProfilingArray[QUEUE_SIZE]}`
284
+ );
285
+ }
286
+
287
+ it('creates a basic flamegraph', () => {
288
+ Scheduler.unstable_Profiling.startLoggingProfilingEvents();
289
+
290
+ Scheduler.unstable_advanceTime(100);
291
+ scheduleCallback(
292
+ NormalPriority,
293
+ () => {
294
+ Scheduler.unstable_advanceTime(300);
295
+ Scheduler.unstable_yieldValue(getProfilingInfo());
296
+ scheduleCallback(
297
+ UserBlockingPriority,
298
+ () => {
299
+ Scheduler.unstable_yieldValue(getProfilingInfo());
300
+ Scheduler.unstable_advanceTime(300);
301
+ },
302
+ {label: 'Bar'},
303
+ );
304
+ Scheduler.unstable_advanceTime(100);
305
+ Scheduler.unstable_yieldValue('Yield');
306
+ return () => {
307
+ Scheduler.unstable_yieldValue(getProfilingInfo());
308
+ Scheduler.unstable_advanceTime(300);
309
+ };
310
+ },
311
+ {label: 'Foo'},
312
+ );
313
+ expect(Scheduler).toFlushAndYieldThrough([
314
+ 'Task: 1, Run: 1, Priority: Normal, Queue Size: 1',
315
+ 'Yield',
316
+ ]);
317
+ Scheduler.unstable_advanceTime(100);
318
+ expect(Scheduler).toFlushAndYield([
319
+ 'Task: 2, Run: 2, Priority: User-blocking, Queue Size: 2',
320
+ 'Task: 1, Run: 3, Priority: Normal, Queue Size: 1',
321
+ ]);
322
+
323
+ expect(getProfilingInfo()).toEqual('Empty Queue');
324
+
325
+ expect(stopProfilingAndPrintFlamegraph()).toEqual(
326
+ `
327
+!!! Main thread │██░░░░░░░░██░░░░░░░░░░░░
328
+Task 2 [User-blocking] │ ░░░░██████
329
+Task 1 [Normal] │ ████████░░░░░░░░██████
330
+`,
331
+ );
332
+ });
333
+
334
+ it('marks when a task is canceled', () => {
335
+ Scheduler.unstable_Profiling.startLoggingProfilingEvents();
336
+
337
+ const task = scheduleCallback(NormalPriority, () => {
338
+ Scheduler.unstable_yieldValue(getProfilingInfo());
339
+ Scheduler.unstable_advanceTime(300);
340
+ Scheduler.unstable_yieldValue('Yield');
341
+ return () => {
342
+ Scheduler.unstable_yieldValue('Continuation');
343
+ Scheduler.unstable_advanceTime(200);
344
+ };
345
+ });
346
+
347
+ expect(Scheduler).toFlushAndYieldThrough([
348
+ 'Task: 1, Run: 1, Priority: Normal, Queue Size: 1',
349
+ 'Yield',
350
+ ]);
351
+ Scheduler.unstable_advanceTime(100);
352
+
353
+ cancelCallback(task);
354
+
355
+ Scheduler.unstable_advanceTime(1000);
356
+ expect(Scheduler).toFlushWithoutYielding();
357
+ expect(stopProfilingAndPrintFlamegraph()).toEqual(
358
+ `
359
+!!! Main thread │░░░░░░██████████████████████
360
+Task 1 [Normal] │██████░░🡐 canceled
361
+`,
362
+ );
363
+ });
364
+
365
+ it('marks when a task errors', () => {
366
+ Scheduler.unstable_Profiling.startLoggingProfilingEvents();
367
+
368
+ scheduleCallback(NormalPriority, () => {
369
+ Scheduler.unstable_advanceTime(300);
370
+ throw Error('Oops');
371
+ });
372
+
373
+ expect(Scheduler).toFlushAndThrow('Oops');
374
+ Scheduler.unstable_advanceTime(100);
375
+
376
+ Scheduler.unstable_advanceTime(1000);
377
+ expect(Scheduler).toFlushWithoutYielding();
378
+ expect(stopProfilingAndPrintFlamegraph()).toEqual(
379
+ `
380
+!!! Main thread │░░░░░░██████████████████████
381
+Task 1 [Normal] │██████🡐 errored
382
+`,
383
+ );
384
+ });
385
+
386
+ it('marks when multiple tasks are canceled', () => {
387
+ Scheduler.unstable_Profiling.startLoggingProfilingEvents();
388
+
389
+ const task1 = scheduleCallback(NormalPriority, () => {
390
+ Scheduler.unstable_yieldValue(getProfilingInfo());
391
+ Scheduler.unstable_advanceTime(300);
392
+ Scheduler.unstable_yieldValue('Yield');
393
+ return () => {
394
+ Scheduler.unstable_yieldValue('Continuation');
395
+ Scheduler.unstable_advanceTime(200);
396
+ };
397
+ });
398
+ const task2 = scheduleCallback(NormalPriority, () => {
399
+ Scheduler.unstable_yieldValue(getProfilingInfo());
400
+ Scheduler.unstable_advanceTime(300);
401
+ Scheduler.unstable_yieldValue('Yield');
402
+ return () => {
403
+ Scheduler.unstable_yieldValue('Continuation');
404
+ Scheduler.unstable_advanceTime(200);
405
+ };
406
+ });
407
+
408
+ expect(Scheduler).toFlushAndYieldThrough([
409
+ 'Task: 1, Run: 1, Priority: Normal, Queue Size: 2',
410
+ 'Yield',
411
+ ]);
412
+ Scheduler.unstable_advanceTime(100);
413
+
414
+ cancelCallback(task1);
415
+ cancelCallback(task2);
416
+
417
+ // Advance more time. This should not affect the size of the main
418
+ // thread row, since the Scheduler queue is empty.
419
+ Scheduler.unstable_advanceTime(1000);
420
+ expect(Scheduler).toFlushWithoutYielding();
421
+
422
+ // The main thread row should end when the callback is cancelled.
423
+ expect(stopProfilingAndPrintFlamegraph()).toEqual(
424
+ `
425
+!!! Main thread │░░░░░░██████████████████████
426
+Task 1 [Normal] │██████░░🡐 canceled
427
+Task 2 [Normal] │░░░░░░░░🡐 canceled
428
+`,
429
+ );
430
+ });
431
+
432
+ it('handles cancelling a task that already finished', () => {
433
+ Scheduler.unstable_Profiling.startLoggingProfilingEvents();
434
+
435
+ const task = scheduleCallback(NormalPriority, () => {
436
+ Scheduler.unstable_yieldValue('A');
437
+ Scheduler.unstable_advanceTime(1000);
438
+ });
439
+ expect(Scheduler).toFlushAndYield(['A']);
440
+ cancelCallback(task);
441
+ expect(stopProfilingAndPrintFlamegraph()).toEqual(
442
+ `
443
+!!! Main thread │░░░░░░░░░░░░░░░░░░░░
444
+Task 1 [Normal] │████████████████████
445
+`,
446
+ );
447
+ });
448
+
449
+ it('handles cancelling a task multiple times', () => {
450
+ Scheduler.unstable_Profiling.startLoggingProfilingEvents();
451
+
452
+ scheduleCallback(
453
+ NormalPriority,
454
+ () => {
455
+ Scheduler.unstable_yieldValue('A');
456
+ Scheduler.unstable_advanceTime(1000);
457
+ },
458
+ {label: 'A'},
459
+ );
460
+ Scheduler.unstable_advanceTime(200);
461
+ const task = scheduleCallback(
462
+ NormalPriority,
463
+ () => {
464
+ Scheduler.unstable_yieldValue('B');
465
+ Scheduler.unstable_advanceTime(1000);
466
+ },
467
+ {label: 'B'},
468
+ );
469
+ Scheduler.unstable_advanceTime(400);
470
+ cancelCallback(task);
471
+ cancelCallback(task);
472
+ cancelCallback(task);
473
+ expect(Scheduler).toFlushAndYield(['A']);
474
+ expect(stopProfilingAndPrintFlamegraph()).toEqual(
475
+ `
476
+!!! Main thread │████████████░░░░░░░░░░░░░░░░░░░░
477
+Task 1 [Normal] │░░░░░░░░░░░░████████████████████
478
+Task 2 [Normal] │ ░░░░░░░░🡐 canceled
479
+`,
480
+ );
481
+ });
482
+
483
+ it('handles cancelling a delayed task', () => {
484
+ Scheduler.unstable_Profiling.startLoggingProfilingEvents();
485
+ const task = scheduleCallback(
486
+ NormalPriority,
487
+ () => Scheduler.unstable_yieldValue('A'),
488
+ {delay: 1000},
489
+ );
490
+ cancelCallback(task);
491
+ expect(Scheduler).toFlushWithoutYielding();
492
+ expect(stopProfilingAndPrintFlamegraph()).toEqual(
493
+ `
494
+!!! Main thread │
495
+`,
496
+ );
497
+ });
498
+
499
+ it('resizes event log buffer if there are many events', () => {
500
+ const tasks = [];
501
+ for (let i = 0; i < 5000; i++) {
502
+ tasks.push(scheduleCallback(NormalPriority, () => {}));
503
+ }
504
+ expect(getProfilingInfo()).toEqual('Suspended, Queue Size: 5000');
505
+ tasks.forEach(task => cancelCallback(task));
506
+ expect(getProfilingInfo()).toEqual('Empty Queue');
507
+ });
508
+});