// Benchmarks allow us to make changes and evaluate performance before the code gets shipped to production. // They can be used to make performance improvements or to test impact of newly added functionality. // Run with: yarn run ts-node --project ./config/tsconfig.benchmark.json -r tsconfig-paths/register static/app/utils/profiling/renderers/textRenderer.benchmark.ts import benchmarkjs from 'benchmark'; import maxBy from 'lodash/maxBy'; import {initializeLocale} from 'sentry/bootstrap/initializeLocale'; import {FlamegraphSearch} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphSearch'; import {TextRenderer} from 'sentry/utils/profiling/renderers/textRenderer'; import {Flamegraph} from '../flamegraph'; import {LightFlamegraphTheme} from '../flamegraph/flamegraphTheme'; import {Rect, transformMatrixBetweenRect} from '../gl/utils'; import androidTrace from '../profile/formats/android/trace.json'; import ios from '../profile/formats/ios/trace.json'; import typescriptTrace from '../profile/formats/typescript/trace.json'; import {importProfile} from '../profile/importProfile'; import {FlamegraphFrame, getFlamegraphFrameSearchId} from './../flamegraphFrame'; // This logs an error which is annoying to see in the outputs initializeLocale({} as any); // We dont compare benchmark results, as we are testing a single version of the code, so we run this as a baseline, // store the results somewhere locally and then compare the results with the new version of our code. function benchmark(name: string, callback: () => void) { const suite = new benchmarkjs.Suite(); suite .add(name, callback) .on('cycle', event => { // well, we need to see the results somewhere // eslint-disable-next-line console.log(event.target.toString()); }) .on('error', event => { // If something goes wrong, fail early throw event; }); suite.run({async: false}); } global.window = {devicePixelRatio: 1}; const makeDrawFullScreen = (renderer: TextRenderer, flamegraph: Flamegraph) => { const configView = new Rect( 0, 0, flamegraph.configSpace.width, // 50% width 1000 ).withY(0); const transform = transformMatrixBetweenRect(configView, new Rect(0, 0, 1000, 1000)); return (searchResults?: FlamegraphSearch) => { renderer.draw(flamegraph.configSpace, transform, searchResults); }; }; const makeDrawCenterScreen = (renderer: TextRenderer, flamegraph: Flamegraph) => { const configView = new Rect( flamegraph.configSpace.width * 0.25, // 25% to left 0, flamegraph.configSpace.width * 0.5, // 50% width 1000 ).withY(0); const transform = transformMatrixBetweenRect(configView, new Rect(0, 0, 1000, 1000)); return (searchResults?: FlamegraphSearch) => { renderer.draw(configView, transform, searchResults); }; }; const makeDrawRightSideOfScreen = (renderer: TextRenderer, flamegraph: Flamegraph) => { const configView = new Rect( flamegraph.configSpace.width * 0.75, // 75% to left 0, flamegraph.configSpace.width * 0.25, // 25% width 1000 ).withY(0); const transform = transformMatrixBetweenRect(configView, new Rect(0, 0, 1000, 1000)); return (searchResults?: FlamegraphSearch) => { renderer.draw(configView, transform, searchResults); }; }; const tsProfile = importProfile(typescriptTrace as any, ''); const tsFlamegraph = new Flamegraph(tsProfile.profiles[0], 0, { inverted: false, leftHeavy: false, }); const androidProfile = importProfile(androidTrace as any, ''); const androidFlamegraph = new Flamegraph( androidProfile.profiles[androidProfile.activeProfileIndex] as any, 0, { inverted: false, leftHeavy: false, } ); const iosProfile = importProfile(ios as any, ''); const iosFlamegraph = new Flamegraph( iosProfile.profiles[iosProfile.activeProfileIndex] as any, 0, { inverted: false, leftHeavy: false, } ); const makeTextRenderer = flamegraph => new TextRenderer( { clientWidth: 1000, clientHeight: 1000, clientLeft: 0, clientTop: 0, getContext: () => { return { fillRect: () => {}, fillText: () => {}, measureText: (t: string) => { return { width: t.length, }; }, }; }, }, flamegraph, LightFlamegraphTheme ); interface FramePartitionData { count: number; frames: Set; } const makeSearchResults = (flamegraph: Flamegraph): FlamegraphSearch => { const framesPartitionedByWords = flamegraph.frames.reduce((acc, frame) => { const words = frame.frame.name.split(' '); words.forEach(w => { if (!acc[w]) { acc[w] = { count: 0, frames: new Set(), }; } const node = acc[w]; node.count++; node.frames.add(frame); }); return acc; }, {} as Record); const [word, data] = maxBy( Object.entries(framesPartitionedByWords), ([_, partitionData]) => { return partitionData.frames.size; } )!; return { results: Array.from(data.frames.values()).reduce((acc, frame) => { acc[getFlamegraphFrameSearchId(frame)] = frame; return acc; }, {}), query: word, }; }; const suite = (name: string, textRenderer: TextRenderer, flamegraph: Flamegraph) => { const results = makeSearchResults(flamegraph); benchmark(`${name} (full profile)`, () => makeDrawFullScreen(textRenderer, flamegraph)(new Map()) ); benchmark(`${name} (center half)`, () => makeDrawCenterScreen(textRenderer, flamegraph)(new Map()) ); benchmark(`${name} (right quarter)`, () => makeDrawRightSideOfScreen(textRenderer, flamegraph)(new Map()) ); benchmark( `${name} (full profile, w/ search matching ${flamegraph.frames.length} of ${flamegraph.frames.length})`, () => makeDrawFullScreen(textRenderer, flamegraph)(results) ); benchmark( `${name} (center half, w/ search ${flamegraph.frames.length} of ${flamegraph.frames.length})`, () => makeDrawCenterScreen(textRenderer, flamegraph)(results) ); benchmark( `${name} (right quarter, w/ search ${flamegraph.frames.length} of ${flamegraph.frames.length})`, () => makeDrawRightSideOfScreen(textRenderer, flamegraph)(results) ); }; suite('typescript', makeTextRenderer(tsFlamegraph), tsFlamegraph); suite('android', makeTextRenderer(androidFlamegraph), androidFlamegraph); suite('ios', makeTextRenderer(iosFlamegraph), iosFlamegraph);