Skip to content

Commit 56ac7a8

Browse files
committed
Add support for Chrome's Async Stack Tagging API
https://developer.chrome.com/blog/devtools-modern-web-debugging/#linked-stack-traces This enables the Chrome developer tools to link the stack traces of the original event scheduling and the eventual execution on the runloop. This is available in Chrome 106 and above. To protect production performance, this is disabled-by-default, but can be enabled by setting `Backburner.ASYNC_STACKS = true`. Applications/frameworks could choose to enable this by default in development modes.
1 parent af77b18 commit 56ac7a8

File tree

7 files changed

+151
-22
lines changed

7 files changed

+151
-22
lines changed

bench/benches/schedule-flush.js

+16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ function prodSetup() {
88
};
99
}
1010

11+
function asyncStackSetup() {
12+
var backburner = new this.Backburner(["sync", "actions", "routerTransitions", "render", "afterRender", "destroy", "rsvpAfter"]);
13+
backburner.ASYNC_STACKS = true;
14+
15+
var target = {
16+
someMethod: function() { }
17+
};
18+
}
19+
1120
function debugSetup() {
1221
var backburner = new this.Backburner(["sync", "actions", "routerTransitions", "render", "afterRender", "destroy", "rsvpAfter"]);
1322
backburner.DEBUG = true;
@@ -77,6 +86,13 @@ base.forEach(item => {
7786
scenarios.push(prodItem);
7887
});
7988

89+
base.forEach(item => {
90+
let debugItem = Object.assign({}, item);
91+
debugItem.name = `ASYNC_STACKS - ${debugItem.name}`;
92+
debugItem.setup = asyncStackSetup;
93+
scenarios.push(debugItem);
94+
});
95+
8096
base.forEach(item => {
8197
let debugItem = Object.assign({}, item);
8298
debugItem.name = `DEBUG - ${debugItem.name}`;

lib/backburner/deferred-action-queues.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default class DeferredActionQueues {
3030
* @param {Any} stack
3131
* @return queue
3232
*/
33-
public schedule(queueName: string, target: any, method: any, args: any, onceFlag: boolean, stack: any) {
33+
public schedule(queueName: string, target: any, method: any, args: any, onceFlag: boolean, stack: any, consoleTask: any) {
3434
let queues = this.queues;
3535
let queue = queues[queueName];
3636

@@ -45,9 +45,9 @@ export default class DeferredActionQueues {
4545
this.queueNameIndex = 0;
4646

4747
if (onceFlag) {
48-
return queue.pushUnique(target, method, args, stack);
48+
return queue.pushUnique(target, method, args, stack, consoleTask);
4949
} else {
50-
return queue.push(target, method, args, stack);
50+
return queue.push(target, method, args, stack, consoleTask);
5151
}
5252
}
5353

lib/backburner/queue.ts

+22-9
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ import { IQueueItem } from './interfaces';
22
import {
33
findItem,
44
getOnError,
5-
getQueueItems
5+
getQueueItems,
6+
QUEUE_ITEM_LENGTH
67
} from './utils';
78

89
export const enum QUEUE_STATE {
910
Pause = 1
1011
}
1112

12-
const QUEUE_ITEM_LENGTH = 4;
13-
1413
export default class Queue {
1514
private name: string;
1615
private globalOptions: any;
@@ -28,7 +27,7 @@ export default class Queue {
2827

2928
public stackFor(index) {
3029
if (index < this._queue.length) {
31-
let entry = this._queue[index * 3 + QUEUE_ITEM_LENGTH];
30+
let entry = this._queue[(index * QUEUE_ITEM_LENGTH) + 3];
3231
if (entry) {
3332
return entry.stack;
3433
} else {
@@ -37,12 +36,20 @@ export default class Queue {
3736
}
3837
}
3938

39+
public consoleTaskFor(index, inQueueBeingFlushed = false) {
40+
let q = inQueueBeingFlushed ? this._queueBeingFlushed : this._queue;
41+
if (index < q.length) {
42+
return q[(index * QUEUE_ITEM_LENGTH) + 4];
43+
}
44+
}
45+
4046
public flush(sync?: Boolean) {
4147
let { before, after } = this.options;
4248
let target;
4349
let method;
4450
let args;
4551
let errorRecordedForStack;
52+
let consoleTask;
4653

4754
this.targetQueues.clear();
4855
if (this._queueBeingFlushed.length === 0) {
@@ -84,7 +91,12 @@ export default class Queue {
8491
target = queueItems[i];
8592
args = queueItems[i + 2];
8693
errorRecordedForStack = queueItems[i + 3]; // Debugging assistance
87-
invoke(target, method, args, onError, errorRecordedForStack);
94+
consoleTask = queueItems[i + 4];
95+
if(consoleTask){
96+
consoleTask.run(invoke.bind(this, target, method, args, onError, errorRecordedForStack))
97+
}else{
98+
invoke(target, method, args, onError, errorRecordedForStack)
99+
}
88100
}
89101

90102
if (this.index !== this._queueBeingFlushed.length &&
@@ -138,8 +150,8 @@ export default class Queue {
138150
return false;
139151
}
140152

141-
public push(target, method, args, stack): { queue: Queue, target, method } {
142-
this._queue.push(target, method, args, stack);
153+
public push(target, method, args, stack, consoleTask): { queue: Queue, target, method } {
154+
this._queue.push(target, method, args, stack, consoleTask);
143155

144156
return {
145157
queue: this,
@@ -148,7 +160,7 @@ export default class Queue {
148160
};
149161
}
150162

151-
public pushUnique(target, method, args, stack): { queue: Queue, target, method } {
163+
public pushUnique(target, method, args, stack, consoleTask): { queue: Queue, target, method } {
152164
let localQueueMap = this.targetQueues.get(target);
153165

154166
if (localQueueMap === undefined) {
@@ -158,12 +170,13 @@ export default class Queue {
158170

159171
let index = localQueueMap.get(method);
160172
if (index === undefined) {
161-
let queueIndex = this._queue.push(target, method, args, stack) - QUEUE_ITEM_LENGTH;
173+
let queueIndex = this._queue.push(target, method, args, stack, consoleTask) - QUEUE_ITEM_LENGTH;
162174
localQueueMap.set(method, queueIndex);
163175
} else {
164176
let queue = this._queue;
165177
queue[index + 2] = args; // replace args
166178
queue[index + 3] = stack; // replace stack
179+
queue[index + 3] = consoleTask; // replace consoleTask
167180
}
168181

169182
return {

lib/backburner/utils.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ const enum QueueItemPosition {
1111
stack
1212
}
1313

14-
export const TIMERS_OFFSET = 6;
14+
export const QUEUE_ITEM_LENGTH = 5;
15+
export const TIMERS_OFFSET = 7;
1516

1617
export function isCoercableNumber(suspect) {
1718
let type = typeof suspect;
@@ -25,7 +26,7 @@ export function getOnError(options) {
2526
export function findItem(target, method, collection) {
2627
let index = -1;
2728

28-
for (let i = 0, l = collection.length; i < l; i += 4) {
29+
for (let i = 0, l = collection.length; i < l; i += QUEUE_ITEM_LENGTH) {
2930
if (collection[i] === target && collection[i + 1] === method) {
3031
index = i;
3132
break;
@@ -38,7 +39,7 @@ export function findItem(target, method, collection) {
3839
export function findTimerItem(target, method, collection) {
3940
let index = -1;
4041

41-
for (let i = 2, l = collection.length; i < l; i += 6) {
42+
for (let i = 2, l = collection.length; i < l; i += TIMERS_OFFSET) {
4243
if (collection[i] === target && collection[i + 1] === method) {
4344
index = i - 2;
4445
break;

lib/index.ts

+30-7
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ const noop = function() {};
3232

3333
const DISABLE_SCHEDULE = Object.freeze([]);
3434

35+
interface ConsoleWithCreateTask extends Console {
36+
createTask(name: string): ConsoleTask;
37+
}
38+
39+
interface ConsoleTask {
40+
run<T>(f: () => T): T;
41+
}
42+
3543
function parseArgs(...args: any[]);
3644
function parseArgs() {
3745
let length = arguments.length;
@@ -163,6 +171,7 @@ export default class Backburner {
163171
public static buildNext = buildNext;
164172

165173
public DEBUG = false;
174+
public ASYNC_STACKS = false;
166175

167176
public currentInstance: DeferredActionQueues | null = null;
168177

@@ -371,7 +380,8 @@ export default class Backburner {
371380
scheduleCount++;
372381
let [target, method, args] = parseArgs(..._args);
373382
let stack = this.DEBUG ? new Error() : undefined;
374-
return this._ensureInstance().schedule(queueName, target, method, args, false, stack);
383+
let consoleTask = this.createTask(queueName, method);
384+
return this._ensureInstance().schedule(queueName, target, method, args, false, stack, consoleTask);
375385
}
376386

377387
/*
@@ -385,7 +395,8 @@ export default class Backburner {
385395
public scheduleIterable(queueName: string, iterable: () => Iterable) {
386396
scheduleIterableCount++;
387397
let stack = this.DEBUG ? new Error() : undefined;
388-
return this._ensureInstance().schedule(queueName, null, iteratorDrain, [iterable], false, stack);
398+
let consoleTask = this.createTask(queueName, null);
399+
return this._ensureInstance().schedule(queueName, null, iteratorDrain, [iterable], false, stack, consoleTask);
389400
}
390401

391402
/**
@@ -406,7 +417,8 @@ export default class Backburner {
406417
scheduleOnceCount++;
407418
let [target, method, args] = parseArgs(..._args);
408419
let stack = this.DEBUG ? new Error() : undefined;
409-
return this._ensureInstance().schedule(queueName, target, method, args, true, stack);
420+
let consoleTask = this.createTask(queueName, method);
421+
return this._ensureInstance().schedule(queueName, target, method, args, true, stack, consoleTask);
410422
}
411423

412424
/**
@@ -525,7 +537,8 @@ export default class Backburner {
525537
_timers[argIndex] = args;
526538
} else {
527539
let stack = this._timers[index + 5];
528-
this._timers.splice(i, 0, executeAt, timerId, target, method, args, stack);
540+
let consoleTask = this._timers[index + 6];
541+
this._timers.splice(i, 0, executeAt, timerId, target, method, args, stack, consoleTask);
529542
this._timers.splice(index, TIMERS_OFFSET);
530543
}
531544

@@ -666,16 +679,17 @@ export default class Backburner {
666679

667680
private _later(target, method, args, wait) {
668681
let stack = this.DEBUG ? new Error() : undefined;
682+
let consoleTask = this.createTask("(timer)", method);
669683
let executeAt = this._platform.now() + wait;
670684
let id = UUID++;
671685

672686
if (this._timers.length === 0) {
673-
this._timers.push(executeAt, id, target, method, args, stack);
687+
this._timers.push(executeAt, id, target, method, args, stack, consoleTask);
674688
this._installTimerTimeout();
675689
} else {
676690
// find position to insert
677691
let i = searchTimer(executeAt, this._timers);
678-
this._timers.splice(i, 0, executeAt, id, target, method, args, stack);
692+
this._timers.splice(i, 0, executeAt, id, target, method, args, stack, consoleTask);
679693

680694
// always reinstall since it could be out of sync
681695
this._reinstallTimerTimeout();
@@ -741,7 +755,8 @@ export default class Backburner {
741755
let target = timers[i + 2];
742756
let method = timers[i + 3];
743757
let stack = timers[i + 5];
744-
this.currentInstance!.schedule(defaultQueue, target, method, args, false, stack);
758+
let consoleTask = timers[i + 6];
759+
this.currentInstance!.schedule(defaultQueue, target, method, args, false, stack, consoleTask);
745760
}
746761
}
747762

@@ -792,4 +807,12 @@ export default class Backburner {
792807

793808
this._autorun = true;
794809
}
810+
811+
private createTask(queueName, method){
812+
if(this.ASYNC_STACKS && console["createTask"]){
813+
return (<ConsoleWithCreateTask>console).createTask(
814+
`runloop ${queueName} | ${method?.name || "<anonymous>"}`
815+
);
816+
}
817+
}
795818
}

tests/async-stack-test.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import Backburner from 'backburner';
2+
3+
const skipIfNotSupported = !!console["createTask"] ? QUnit.test : QUnit.skip;
4+
5+
QUnit.module('tests/async_stacks');
6+
7+
QUnit.test("schedule - does not affect normal behaviour", function(assert) {
8+
let bb = new Backburner(['one']);
9+
let callCount = 0;
10+
11+
bb.run(() => {
12+
bb.schedule("one", () => callCount += 1)
13+
bb.schedule("one", () => callCount += 1)
14+
});
15+
assert.strictEqual(callCount, 2, "schedule works correctly with ASYNC_STACKS disabled");
16+
17+
bb.ASYNC_STACKS = true;
18+
19+
bb.run(() => {
20+
bb.schedule("one", () => callCount += 1)
21+
bb.schedule("one", () => callCount += 1)
22+
});
23+
assert.strictEqual(callCount, 4, "schedule works correctly with ASYNC_STACKS enabled");
24+
});
25+
26+
skipIfNotSupported('schedule - ASYNC_STACKS flag enables async stack tagging', function(assert) {
27+
let bb = new Backburner(['one']);
28+
29+
bb.schedule('one', () => {});
30+
31+
assert.true(bb.currentInstance && (bb.currentInstance.queues.one.consoleTaskFor(0) === undefined), 'No consoleTask is stored');
32+
33+
bb.ASYNC_STACKS = true;
34+
35+
bb.schedule('one', () => {});
36+
37+
const task = bb.currentInstance && bb.currentInstance.queues.one.consoleTaskFor(1);
38+
assert.true(!!task?.run, 'consoleTask is stored in queue');
39+
});
40+
41+
QUnit.test("later - ASYNC_STACKS does not affect normal behaviour", function(assert) {
42+
let bb = new Backburner(['one']);
43+
let done = assert.async();
44+
bb.ASYNC_STACKS = true;
45+
46+
bb.later(() => {
47+
assert.true(true, "timer called")
48+
done()
49+
});
50+
});
51+
52+
53+
skipIfNotSupported('later - skips async stack when ASYNC_STACKS is false', function(assert) {
54+
let done = assert.async();
55+
let bb = new Backburner(['one']);
56+
57+
bb.later(() => {
58+
const task = bb.currentInstance && bb.currentInstance.queues.one.consoleTaskFor(0, true);
59+
assert.true(bb.currentInstance && (bb.currentInstance.queues.one.consoleTaskFor(0, true) === undefined), 'consoleTask is not stored')
60+
done();
61+
});
62+
});
63+
64+
65+
skipIfNotSupported('later - ASYNC_STACKS flag enables async stack tagging', function(assert) {
66+
let done = assert.async();
67+
let bb = new Backburner(['one']);
68+
bb.ASYNC_STACKS = true;
69+
70+
bb.later(() => {
71+
const task = bb.currentInstance && bb.currentInstance.queues.one.consoleTaskFor(0, true);
72+
assert.true(!!task?.run, 'consoleTask is stored in timer queue and then passed to runloop queue')
73+
done();
74+
});
75+
});

tests/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import './async-stack-test';
12
import './autorun-test';
23
import './bb-has-timers-test';
34
import './build-next-test';

0 commit comments

Comments
 (0)