mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
228 lines
5.7 KiB
TypeScript
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);
|
|
});
|
|
});
|