Files
2024-02-15 17:01:50 +00:00

228 lines
5.7 KiB
TypeScript

import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode';
import {Frame} from 'sentry/utils/profiling/frame';
import {Profile} from 'sentry/utils/profiling/profile/profile';
// Test utils to keep the tests code dry
export const f = (name: string, key: number, in_app: boolean = true) =>
new Frame({name, key, is_application: in_app});
export const c = (fr: Frame) => new CallTreeNode(fr, null);
export const firstCallee = (node: CallTreeNode) => node.children[0];
export const nthCallee = (node: CallTreeNode, n: number) => {
const child = node.children[n];
if (!child) {
throw new Error('Child not found');
}
return child;
};
export const makeTestingBoilerplate = () => {
const timings: [Frame['name'], string][] = [];
const openSpy = jest.fn();
const closeSpy = jest.fn();
// We need to wrap the spy fn because they are not allowed to reference external variables
const open = (node, value) => {
timings.push([node.frame.name, 'open']);
openSpy(node, value);
};
// We need to wrap the spy fn because they are not allowed to reference external variables
const close = (node, val) => {
timings.push([node.frame.name, 'close']);
closeSpy(node, val);
};
return {open, close, timings, openSpy, closeSpy};
};
// Since it's easy to make mistakes or accidentally assign parents to the wrong nodes, this utility fn
// will format the stack samples as a tree string so it's more human friendly.
export const _logExpectedStack = (samples: Profile['samples']): string => {
const head = `
Samples follow a top-down chronological order\n\n`;
const tail = `\n
----------------------->
stack top -> stack bottom`;
const final: string[] = [];
const visit = (node: CallTreeNode, str: string[]) => {
str.push(`${node.frame.name}`);
if (node.parent) {
visit(node.parent, str);
}
};
for (const stackTop of samples) {
const str = [];
visit(stackTop, str);
final.push(str.join(' -> '));
}
return `${head}${final.join('\n')}${tail}`;
};
describe('Profile', () => {
it('Empty profile duration is not infinity', () => {
const profile = Profile.Empty;
expect(profile.duration).toEqual(1000);
expect(profile.minFrameDuration).toEqual(1000);
});
it('forEach - iterates over a single sample', () => {
const profile = new Profile({
duration: 1000,
startedAt: 0,
endedAt: 1000,
name: 'profile',
unit: 'ms',
threadId: 0,
type: 'flamechart',
});
// Frames
const f0 = f('f0', 0);
const f1 = f('f1', 1);
// Call tree nodes
const s0 = c(f0);
const s1 = c(f1);
s0.parent = s1;
profile.samples = [s0];
const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();
profile.forEach(open, close);
expect(timings).toEqual([
['f1', 'open'],
['f0', 'open'],
['f0', 'close'],
['f1', 'close'],
]);
expect(openSpy).toHaveBeenCalledTimes(2);
expect(closeSpy).toHaveBeenCalledTimes(2);
});
it('forEach - opens new frames when stack is shared', () => {
const profile = new Profile({
duration: 1000,
startedAt: 0,
endedAt: 1000,
name: 'profile',
unit: 'ms',
threadId: 0,
type: 'flamechart',
});
// Frames
const f0 = f('f0', 0);
const f1 = f('f1', 1);
const f2 = f('f2', 1);
// Call tree nodes
const s0 = c(f0);
const s1 = c(f1);
const s2 = c(f2);
s1.parent = s0;
s2.parent = s1;
profile.samples = [s0, s1, s2];
const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();
profile.forEach(open, close);
expect(timings).toEqual([
['f0', 'open'],
['f1', 'open'],
['f2', 'open'],
['f2', 'close'],
['f1', 'close'],
['f0', 'close'],
]);
expect(openSpy).toHaveBeenCalledTimes(3);
expect(closeSpy).toHaveBeenCalledTimes(3);
});
it('forEach - closes frames one by one when stack is shared', () => {
const profile = new Profile({
duration: 1000,
startedAt: 0,
endedAt: 1000,
name: 'profile',
unit: 'ms',
threadId: 0,
type: 'flamechart',
});
// Instantiate frames
const f0 = f('f0', 0);
const f1 = f('f1', 1);
const f2 = f('f2', 2);
// Instantiate call tree nodes
const s0 = c(f2);
const s1 = c(f1);
const s2 = c(f0);
profile.samples = [s0, s1, s2];
const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();
profile.forEach(open, close);
expect(timings).toEqual([
['f2', 'open'],
['f2', 'close'],
['f1', 'open'],
['f1', 'close'],
['f0', 'open'],
['f0', 'close'],
]);
expect(openSpy).toHaveBeenCalledTimes(3);
expect(closeSpy).toHaveBeenCalledTimes(3);
});
// In JS land, the stack can be idle which is not the case in other runtimes, e.g. in mobile
// the program main is always running, so make sure we support "holes" in the samples
it('forEach - supports an idle stack', () => {
const profile = new Profile({
duration: 1000,
startedAt: 0,
endedAt: 1000,
name: 'profile',
unit: 'ms',
threadId: 0,
type: 'flamechart',
});
// Instantiate frames
const f0 = f('f0', 0);
// Instantiate call tree nodes
const s0 = c(f0);
const s1 = c(Frame.Root);
profile.samples = [s0, s1, s0];
const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();
profile.forEach(open, close);
expect(timings).toEqual([
['f0', 'open'],
['f0', 'close'],
['f0', 'open'],
['f0', 'close'],
]);
expect(openSpy).toHaveBeenCalledTimes(2);
expect(closeSpy).toHaveBeenCalledTimes(2);
});
});