Skip to content

Commit c1d0a56

Browse files
feat: Add "long tasks" internal message (#1506)
1 parent f473350 commit c1d0a56

File tree

4 files changed

+333
-7
lines changed

4 files changed

+333
-7
lines changed

src/common/wrap/wrap-function.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export function createWrapperWithEmitter (emitter, always) {
7676
var originalThis
7777
var ctx
7878
var result
79+
let thrownError
7980

8081
try {
8182
originalThis = this
@@ -93,17 +94,31 @@ export function createWrapperWithEmitter (emitter, always) {
9394
// Warning: start events may mutate args!
9495
safeEmit(prefix + 'start', [args, originalThis, methodName], ctx, bubble)
9596

97+
const fnStartTime = performance.now()
98+
let fnEndTime = fnStartTime
9699
try {
97100
result = fn.apply(originalThis, args)
101+
fnEndTime = performance.now()
98102
return result
99103
} catch (err) {
104+
fnEndTime = performance.now()
100105
safeEmit(prefix + 'err', [args, originalThis, err], ctx, bubble)
101-
102106
// rethrow error so we don't effect execution by observing.
103-
throw err
107+
thrownError = err
108+
throw thrownError
104109
} finally {
105-
// happens no matter what.
106-
safeEmit(prefix + 'end', [args, originalThis, result], ctx, bubble)
110+
const duration = fnEndTime - fnStartTime
111+
const task = {
112+
duration,
113+
isLongTask: duration >= 50,
114+
methodName,
115+
thrownError
116+
// could add more properties here later if needed by downstream features
117+
}
118+
// standalone long task message
119+
if (task.isLongTask) safeEmit('long-task', [task], ctx, bubble)
120+
// -end message also includes the task execution info
121+
safeEmit(prefix + 'end', [args, originalThis, result, task], ctx, bubble)
107122
}
108123
}
109124
}

tests/assets/long-tasks.html

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<!DOCTYPE html>
2+
<!--
3+
Copyright 2020 New Relic Corporation.
4+
PDX-License-Identifier: Apache-2.0
5+
-->
6+
<html>
7+
<head>
8+
<title>RUM Unit Test</title>
9+
{init}{config}
10+
<script>
11+
NREUM.init.harvest.interval = 30;
12+
NREUM.init.logging.enabled = false;
13+
</script>
14+
{loader}
15+
</head>
16+
<body>
17+
<div>This page executes lots of different FNs that take a short or long amount of syncronous time to execute</div>
18+
<pre>
19+
The operations behave like this:
20+
21+
click (anywhere)!
22+
23+
document.addEventListener (click)
24+
---- SHORT TASK
25+
---- LONG TASK (with setTimeout -- should resolve after short tasks but still be detected!)
26+
---- SHORT TASK (x3)
27+
---- XHR send
28+
---- XHR load callback (Should have an error tied to the long task)
29+
------- SHORT TASK
30+
------- LONG TASK
31+
------- SHORT TASK
32+
------- Promise constructor
33+
----------- LONG TASK
34+
----------- SHORT TASK
35+
------- Promise then callback (async)
36+
----------- SHORT TASK
37+
----------- LONG TASK
38+
39+
Due to the way syncronous tasks block each other in JS, you may see no activity and then
40+
multiple long tasks resolving at the "same time", since they cascade like dominoes when syncronous
41+
</pre>
42+
<hr />
43+
<div id="content"></div>
44+
<script type="text/javascript">
45+
document.addEventListener(
46+
"click",
47+
function test () {
48+
const contents = document.querySelector("#content");
49+
updateDOM("Document Clicked!", true);
50+
simulatedShortTask("document addEventListener");
51+
52+
var xhr = new XMLHttpRequest();
53+
xhr.open("POST", "/echo");
54+
xhr.send("123");
55+
56+
simulatedLongTask("document addEventListener", true); // LONG TASK AFFECTING DOC AEL CB
57+
58+
simulatedShortTask("document addEventListener");
59+
simulatedShortTask("document addEventListener");
60+
simulatedShortTask("document addEventListener");
61+
62+
xhr.addEventListener(
63+
"load",
64+
function () {
65+
simulatedShortTask("xhr addEventListener");
66+
simulatedLongTask("xhr addEventListener"); // LONG TASK AFFECTING XHR LOAD CB
67+
simulatedShortTask("xhr addEventListener");
68+
69+
a = new Promise((resolve, reject) => {
70+
simulatedLongTask("promise constructor"); // LONG TASK AFFECTING PROMISE CONSTRUCTOR
71+
simulatedShortTask("promise constructor");
72+
resolve();
73+
});
74+
75+
a.then(() => {
76+
simulatedShortTask("promise.then");
77+
simulatedLongTask("promise.then"); // LONG TASK AFFECTING PROMISE THEN CB
78+
});
79+
80+
throw new Error("I threw an error in the XHR load callback! Fix ME!");
81+
},
82+
true
83+
);
84+
85+
function simulatedShortTask(name) {
86+
const start = performance.now();
87+
updateDOM(name + " task started");
88+
let i = 0;
89+
const loops = getRandomInt(10000, 1000000);
90+
while (i < loops) {
91+
i++;
92+
}
93+
const end = performance.now()
94+
updateDOM(name + " duration: " + (end-start).toFixed(4), "green");
95+
}
96+
97+
function simulatedLongTask(name, useSetTimeout) {
98+
const start = performance.now();
99+
updateDOM(name + " task started");
100+
// so as to not let the slow task block rendering for demo purposes
101+
102+
const executor = useSetTimeout
103+
? (fn) => setTimeout(fn, 0)
104+
: function slowHandler(fn) {
105+
fn();
106+
};
107+
108+
executor(reallySlowFn);
109+
110+
function reallySlowFn() {
111+
let i = 0;
112+
const loops = getRandomInt(100000000, 5000000000);
113+
while (i < loops) {
114+
i++;
115+
}
116+
const end = performance.now()
117+
updateDOM("Long task (" + name + ") duration: " + (end-start).toFixed(4), "red");
118+
}
119+
}
120+
121+
window.location.hash = Math.random();
122+
123+
function updateDOM(text, color) {
124+
const elem = document.createElement("div");
125+
elem.innerHTML = text;
126+
elem.style.color = color || "black";
127+
contents.appendChild(elem);
128+
}
129+
function getRandomInt(min, max) {
130+
return Math.ceil(Math.floor(Math.random() * (max - min + 1)) + min);
131+
}
132+
},
133+
false
134+
);
135+
</script>
136+
</body>
137+
</html>

tests/components/api.test.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,12 @@ describe('API tests', () => {
804804
expectHandled(SUPPORTABILITY_METRIC_CHANNEL, ['API/wrapLogger/called'])
805805

806806
expectEmitted('wrap-logger-start', [expect.any(Array), expect.any(Object), 'myObservedLogger'])
807-
expectEmitted('wrap-logger-end', [['test1'], expect.any(Object), undefined])
807+
expectEmitted('wrap-logger-end', [['test1'], expect.any(Object), undefined, expect.objectContaining({
808+
duration: expect.any(Number),
809+
isLongTask: false,
810+
methodName: 'myObservedLogger',
811+
thrownError: undefined
812+
})])
808813

809814
expectHandled(SUPPORTABILITY_METRIC_CHANNEL, ['API/logging/info/called'])
810815
expectHandled('log', [expect.any(Number), 'test1', {}, 'INFO', undefined])
@@ -832,7 +837,12 @@ describe('API tests', () => {
832837
expectHandled(SUPPORTABILITY_METRIC_CHANNEL, ['API/wrapLogger/called'])
833838

834839
expectEmitted('wrap-logger-start', [expect.any(Array), expect.any(Object), randomMethodName])
835-
expectEmitted('wrap-logger-end', [['test1'], expect.any(Object), undefined])
840+
expectEmitted('wrap-logger-end', [['test1'], expect.any(Object), undefined, expect.objectContaining({
841+
duration: expect.any(Number),
842+
isLongTask: false,
843+
methodName: randomMethodName,
844+
thrownError: undefined
845+
})])
836846

837847
expectHandled(SUPPORTABILITY_METRIC_CHANNEL, ['API/logging/warn/called'])
838848
expectHandled('log', [expect.any(Number), 'test1', {}, 'warn', undefined])
@@ -853,7 +863,12 @@ describe('API tests', () => {
853863
expectHandled(SUPPORTABILITY_METRIC_CHANNEL, ['API/wrapLogger/called'])
854864

855865
expectEmitted('wrap-logger-start', [expect.any(Array), expect.any(Object), randomMethodName])
856-
expectEmitted('wrap-logger-end', [['test1', { test2: 2 }, ['test3'], true, 1], expect.any(Object), undefined])
866+
expectEmitted('wrap-logger-end', [['test1', { test2: 2 }, ['test3'], true, 1], expect.any(Object), undefined, expect.objectContaining({
867+
duration: expect.any(Number),
868+
isLongTask: false,
869+
methodName: randomMethodName,
870+
thrownError: undefined
871+
})])
857872
})
858873

859874
test('wrapped function should still behave as intended', () => {

tests/components/wrappings/wrap-function.test.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,163 @@ describe('Wrap function', () => {
174174
})
175175
outer()
176176
})
177+
178+
describe('long tasks', () => {
179+
let onLongTask, onShortTask
180+
afterEach(async () => {
181+
if (onLongTask) {
182+
baseEE.removeEventListener('long-task', onLongTask)
183+
onLongTask = null
184+
}
185+
if (onShortTask) {
186+
baseEE.removeEventListener('short-task', onShortTask)
187+
onShortTask = null
188+
}
189+
})
190+
191+
test('does not emit long task event if fn takes less than 50ms', (done) => {
192+
onLongTask = function () {
193+
seenLongTask = true // set flag to check if long task event was emitted
194+
}
195+
onShortTask = function (_, __, ____, task) {
196+
expectShortTask(task)
197+
expect(seenLongTask).toEqual(false) // no long task event emitted
198+
done()
199+
}
200+
let seenLongTask = false
201+
baseEE.on('long-task', onLongTask)
202+
203+
baseEE.on('short-end', onShortTask)
204+
205+
const shortTask = wrapFn(() => {}, 'short-')
206+
shortTask(thisCtx)
207+
})
208+
209+
test('emits long task message if fn takes more than 50ms', (done) => {
210+
onLongTask = function (task) {
211+
expectLongTask(task)
212+
done()
213+
}
214+
215+
const reallySlowFn = function reallySlowFn () {
216+
slowFn(100000000, 1000000000) // this should take more than 50ms
217+
}
218+
219+
const longTask = wrapFn(reallySlowFn, 'long-', thisCtx, 'reallySlowFn', true)
220+
baseEE.on('long-task', onLongTask)
221+
222+
longTask(thisCtx)
223+
})
224+
225+
test('emits long task message if fn errors out', (done) => {
226+
const err = new Error('TEST')
227+
onLongTask = function (task) {
228+
expectLongTask(task, err)
229+
done()
230+
}
231+
232+
const reallySlowFn = function reallySlowFn () {
233+
slowFn(100000000, 1000000000) // this should take more than 50ms
234+
throw err // this should throw an error
235+
}
236+
237+
const longTask = wrapFn(reallySlowFn, 'long-', thisCtx, 'reallySlowFn', true)
238+
baseEE.on('long-task', onLongTask)
239+
240+
try {
241+
expect(longTask(thisCtx)).throws('TEST') // should throw an error
242+
} catch (e) {
243+
// We expect the error to be thrown, so we catch it here to avoid failing the test.
244+
// The long task event should still be emitted and handled by onLongTask.
245+
}
246+
})
247+
248+
test('finished fn message also has task info', (done) => {
249+
onShortTask = function (_, __, ___, task) {
250+
expectShortTask(task)
251+
seenTasks.short = true
252+
isDone()
253+
}
254+
onLongTask = function (_, __, ___, task) {
255+
expectLongTask(task)
256+
seenTasks.long = true
257+
isDone()
258+
}
259+
260+
const reallySlowFn = function reallySlowFn () {
261+
slowFn(100000000, 1000000000) // this should take more than 50ms
262+
}
263+
264+
const seenTasks = { short: false, long: false }
265+
266+
const shortTask = wrapFn(() => {}, 'short-')
267+
const longTask = wrapFn(reallySlowFn, 'long-', thisCtx, 'reallySlowFn', true)
268+
269+
baseEE.on('short-end', onShortTask)
270+
271+
baseEE.on('long-end', onLongTask)
272+
273+
function isDone () {
274+
if (seenTasks.short && seenTasks.long) {
275+
done() // both tasks emitted
276+
}
277+
}
278+
279+
shortTask(thisCtx) // this should not emit long task
280+
longTask(thisCtx)
281+
})
282+
283+
test('nested long tasks emit twice', (done) => {
284+
const seenLongTasks = { fn1: false, fn2: false, calls: 0 }
285+
onLongTask = function (task) {
286+
expectLongTask(task)
287+
seenLongTasks[task.methodName] = true
288+
seenLongTasks.calls++
289+
if (seenLongTasks.calls === 2 && seenLongTasks.fn1 && seenLongTasks.fn2) {
290+
done() // both long tasks emitted
291+
}
292+
}
293+
294+
baseEE.on('long-task', onLongTask)
295+
296+
const fn2 = function fn2 (ctx) {
297+
return slowFn()
298+
}
299+
const wrappedFn2 = wrapFn(fn2, 'fn2-', thisCtx, 'fn2', true)
300+
301+
const fn1 = function fn1 (ctx) {
302+
slowFn()
303+
return wrappedFn2(ctx)
304+
}
305+
const longTask = wrapFn(fn1, 'fn1-', thisCtx, 'fn1', true)
306+
longTask(thisCtx)
307+
})
308+
})
309+
310+
function expectLongTask (task, error) {
311+
expect(task).toBeDefined() // task info is passed
312+
expect(task.duration).toBeGreaterThanOrEqual(50) // can tell that task took less than 50ms
313+
expect(task.isLongTask).toEqual(true) // task is marked as short task
314+
if (error) expect(task.thrownError).toEqual(error) // task has thrown error
315+
else expect(task.thrownError).toBeUndefined() // task has not thrown error
316+
}
317+
function expectShortTask (task, error) {
318+
expect(task).toBeDefined() // task info is passed
319+
expect(task.duration).toBeLessThan(50) // can tell that task took less than 50ms
320+
expect(task.isLongTask).toEqual(false) // task is marked as short task
321+
if (error) expect(task.thrownError).toEqual(error) // task has thrown error
322+
else expect(task.thrownError).toBeUndefined() // task has not thrown error
323+
}
324+
325+
function slowFn (from = 100000000, to = 1000000000) {
326+
let i = 0
327+
const loops = getRandomInt(from, to)
328+
while (i < loops) {
329+
i++
330+
}
331+
function getRandomInt (min, max) {
332+
return Math.ceil(Math.floor(Math.random() * (max - min + 1)) + min)
333+
}
334+
return i
335+
}
177336
})

0 commit comments

Comments
 (0)