From cebadfabfe1337e28bc91bbc5f4cde6fdab34ded Mon Sep 17 00:00:00 2001 From: JonasBa Date: Wed, 9 Feb 2022 09:23:49 +0100 Subject: [PATCH 1/7] feat(flamegraph): add view select menu --- .../profiling/flamegraphZoomView.stories.js | 23 +++++++- .../profiling/FlamegraphViewSelectMenu.tsx | 58 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 static/app/components/profiling/FlamegraphViewSelectMenu.tsx diff --git a/docs-ui/stories/components/profiling/flamegraphZoomView.stories.js b/docs-ui/stories/components/profiling/flamegraphZoomView.stories.js index a7ab1186778691..3903e911391a2d 100644 --- a/docs-ui/stories/components/profiling/flamegraphZoomView.stories.js +++ b/docs-ui/stories/components/profiling/flamegraphZoomView.stories.js @@ -1,5 +1,6 @@ import * as React from 'react'; +import {FlamegraphViewSelectMenu} from 'sentry/components/profiling/FlamegraphViewSelectMenu'; import {FlamegraphZoomView} from 'sentry/components/profiling/FlamegraphZoomView'; import {FlamegraphZoomViewMinimap} from 'sentry/components/profiling/FlamegraphZoomViewMinimap'; import {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler'; @@ -16,8 +17,16 @@ export default { export const EventedTrace = () => { const canvasPoolManager = new CanvasPoolManager(); + const [view, setView] = React.useState({inverted: false, leftHeavy: false}); + const profiles = importProfile(trace); - const flamegraph = new Flamegraph(profiles.profiles[0]); + + const flamegraph = new Flamegraph( + profiles.profiles[0], + 0, + view.inverted, + view.leftHeavy + ); return (
{ overscrollBehavior: 'contain', }} > +
+ { + setView({...view, leftHeavy: s === 'left heavy'}); + }} + onViewChange={v => { + setView({...view, inverted: v === 'bottom up'}); + }} + /> +
void; + onSortingChange: (sorting: FlamegraphViewSelectMenuProps['sorting']) => void; +} + +function FlamegraphViewSelectMenu({ + view, + onViewChange, + sorting, + onSortingChange, +}: FlamegraphViewSelectMenuProps): React.ReactElement { + return ( + + + + + + + ); +} + +const ViewSelectMenu = styled('div')` + padding: 0 0; + flex: 1; + height: 100%; +`; + +export {FlamegraphViewSelectMenu}; From e1a46309265c76c1a25064477ab2f54af59f7c9e Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 10 Feb 2022 09:15:34 +0100 Subject: [PATCH 2/7] feat(profiling): use buttonbar --- .../profiling/FlamegraphViewSelectMenu.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/static/app/components/profiling/FlamegraphViewSelectMenu.tsx b/static/app/components/profiling/FlamegraphViewSelectMenu.tsx index 9a28052378bfb8..36a728780f7d49 100644 --- a/static/app/components/profiling/FlamegraphViewSelectMenu.tsx +++ b/static/app/components/profiling/FlamegraphViewSelectMenu.tsx @@ -1,6 +1,5 @@ -import styled from '@emotion/styled'; - import Button from 'sentry/components/button'; +import ButtonBar from 'sentry/components/buttonBar'; interface FlamegraphViewSelectMenuProps { view: 'top down' | 'bottom up'; @@ -16,7 +15,7 @@ function FlamegraphViewSelectMenu({ onSortingChange, }: FlamegraphViewSelectMenuProps): React.ReactElement { return ( - + - + ); } -const ViewSelectMenu = styled('div')` - padding: 0 0; - flex: 1; - height: 100%; -`; - export {FlamegraphViewSelectMenu}; From ac15883c1eee3e8e23ad0a55b4bb7b5cd964790e Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 10 Feb 2022 11:17:05 +0100 Subject: [PATCH 3/7] feat(profiling): add search --- .../profiling/flamegraphZoomView.stories.js | 5 + .../components/profiling/FlamegraphSearch.tsx | 243 ++++++++++++++++++ .../profiling/FlamegraphZoomView.tsx | 206 +++++++-------- .../app/utils/profiling/canvasScheduler.tsx | 19 +- .../app/utils/profiling/validators/regExp.tsx | 13 + 5 files changed, 382 insertions(+), 104 deletions(-) create mode 100644 static/app/components/profiling/FlamegraphSearch.tsx create mode 100644 static/app/utils/profiling/validators/regExp.tsx diff --git a/docs-ui/stories/components/profiling/flamegraphZoomView.stories.js b/docs-ui/stories/components/profiling/flamegraphZoomView.stories.js index 3903e911391a2d..5840e37c870560 100644 --- a/docs-ui/stories/components/profiling/flamegraphZoomView.stories.js +++ b/docs-ui/stories/components/profiling/flamegraphZoomView.stories.js @@ -1,5 +1,6 @@ import * as React from 'react'; +import {FlamegraphSearch} from 'sentry/components/profiling/FlamegraphSearch'; import {FlamegraphViewSelectMenu} from 'sentry/components/profiling/FlamegraphViewSelectMenu'; import {FlamegraphZoomView} from 'sentry/components/profiling/FlamegraphZoomView'; import {FlamegraphZoomViewMinimap} from 'sentry/components/profiling/FlamegraphZoomViewMinimap'; @@ -67,6 +68,10 @@ export const EventedTrace = () => { canvasPoolManager={canvasPoolManager} flamegraphTheme={LightFlamegraphTheme} /> +
); diff --git a/static/app/components/profiling/FlamegraphSearch.tsx b/static/app/components/profiling/FlamegraphSearch.tsx new file mode 100644 index 00000000000000..574fe9a3529688 --- /dev/null +++ b/static/app/components/profiling/FlamegraphSearch.tsx @@ -0,0 +1,243 @@ +import * as React from 'react'; +import styled from '@emotion/styled'; +import * as Sentry from '@sentry/react'; +import Fuse from 'fuse.js'; + +import space from 'sentry/styles/space'; +import {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler'; +import {Flamegraph} from 'sentry/utils/profiling/flamegraph'; +import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame'; +import {isRegExpString, parseRegExp} from 'sentry/utils/profiling/validators/regExp'; + +function uniqueFrameKey(frame: FlamegraphFrame): string { + return `${frame.frame.key + String(frame.start)}`; +} + +const numericSort = ( + a: null | undefined | number, + b: null | undefined | number, + direction: 'asc' | 'desc' +): number => { + if (a === b) { + return 0; + } + if (a === null || a === undefined) { + return 1; + } + if (b === null || b === undefined) { + return -1; + } + + return direction === 'asc' ? a - b : b - a; +}; + +interface FlamegraphSearchProps { + flamegraphs: Flamegraph[]; + placement: 'top' | 'bottom'; + canvasPoolManager: CanvasPoolManager; +} + +function FlamegraphSearch({ + flamegraphs, + canvasPoolManager, +}: FlamegraphSearchProps): React.ReactElement | null { + const ref = React.useRef(null); + + const [open, setOpen] = React.useState(false); + const [selectedNode, setSelectedNode] = React.useState(); + const [searchResults, setSearchResults] = React.useState< + Record + >({}); + + const allFrames = React.useMemo(() => { + return flamegraphs.reduce( + (acc: FlamegraphFrame[], graph) => acc.concat(graph.frames), + [] + ); + }, [flamegraphs]); + + const searchIndex = React.useMemo(() => { + return new Fuse(allFrames, { + keys: ['frame.name'], + threshold: 0.3, + includeMatches: true, + }); + }, [allFrames.length]); + + const onZoomIntoFrame = React.useCallback( + (frame: FlamegraphFrame) => { + canvasPoolManager.dispatch('zoomIntoFrame', [frame]); + setSelectedNode(frame); + }, + [canvasPoolManager] + ); + + const handleSearchInput = React.useCallback( + (evt: React.ChangeEvent) => { + const query = evt.currentTarget.value; + const results: Record = {}; + + if (!query) { + setSearchResults(results); + canvasPoolManager.dispatch('searchResults', [results]); + return; + } + + if (isRegExpString(query)) { + const [_, lookup, flags] = parseRegExp(query) ?? []; + + try { + if (!lookup) { + throw new Error('Invalid RegExp'); + } + + const userQuery = new RegExp(lookup, flags || 'g'); + + for (let i = 0; i < allFrames.length; i++) { + const frame = allFrames[i]; + + if (userQuery.test(frame.frame.name.trim())) { + results[ + `${ + frame.frame.name + + (frame.frame.file ? frame.frame.file : '') + + String(frame.start) + }` + ] = frame; + } + } + } catch (e) { + Sentry.captureMessage(e.message); + } + } else { + const fuseResults = searchIndex + .search(query) + .sort((a, b) => numericSort(a.item.start, b.item.start, 'asc')); + + for (const frame of fuseResults) { + results[ + `${ + frame.item.frame.name + + (frame.item.frame.file ? frame.item.frame.file : '') + + String(frame.item.start) + }` + ] = frame.item; + } + } + + setSearchResults(results); + canvasPoolManager.dispatch('searchResults', [results]); + }, + [searchIndex, frames, canvasPoolManager] + ); + + const onNextSearchClick = React.useCallback(() => { + const frames = Object.values(searchResults).sort((a, b) => + a.start === b.start + ? numericSort(a.depth, b.depth, 'asc') + : numericSort(a.start, b.start, 'asc') + ); + if (!frames.length) { + return undefined; + } + + if (!selectedNode) { + return onZoomIntoFrame(frames[0] ?? null); + } + + const index = frames.findIndex( + f => uniqueFrameKey(f) === uniqueFrameKey(selectedNode) + ); + + if (index + 1 > frames.length - 1) { + return onZoomIntoFrame(frames[0]); + } + return onZoomIntoFrame(frames[index + 1]); + }, [selectedNode, searchResults, onZoomIntoFrame]); + + const onPreviousSearchClick = React.useCallback(() => { + const frames = Object.values(searchResults).sort((a, b) => + a.start === b.start + ? numericSort(a.depth, b.depth, 'asc') + : numericSort(a.start, b.start, 'asc') + ); + if (!frames.length) { + return undefined; + } + + if (!selectedNode) { + return onZoomIntoFrame(frames[0] ?? null); + } + const index = frames.findIndex( + f => uniqueFrameKey(f) === uniqueFrameKey(selectedNode) + ); + + if (index - 1 < 0) { + return onZoomIntoFrame(frames[frames.length - 1]); + } + return onZoomIntoFrame(frames[index - 1]); + }, [selectedNode, searchResults, onZoomIntoFrame]); + + const onCmdF = React.useCallback( + (evt: KeyboardEvent) => { + if (evt.key === 'f' && evt.metaKey) { + evt.preventDefault(); + if (open) { + ref.current?.focus(); + } else { + setOpen(true); + } + } + if (evt.key === 'Escape') { + setSearchResults({}); + setOpen(false); + } + }, + [open, setSearchResults] + ); + + const onKeyDown = React.useCallback( + (evt: React.KeyboardEvent) => { + if (evt.key === 'Escape') { + setSearchResults({}); + setOpen(false); + } + if (evt.key === 'ArrowDown') { + evt.preventDefault(); + onNextSearchClick(); + } + if (evt.key === 'ArrowUp') { + evt.preventDefault(); + onPreviousSearchClick(); + } + }, + [onNextSearchClick, onPreviousSearchClick, setSearchResults] + ); + + React.useEffect(() => { + document.addEventListener('keydown', onCmdF); + + return () => { + document.removeEventListener('keydown', onCmdF); + }; + }, [onCmdF]); + + return open ? ( + + ) : null; +} + +const Input = styled('input')` + position: absolute; + left: 50%; + top: ${space(4)}; + transform: translateX(-50%); +`; + +export {FlamegraphSearch}; diff --git a/static/app/components/profiling/FlamegraphZoomView.tsx b/static/app/components/profiling/FlamegraphZoomView.tsx index 0cc1006fb50274..7ed1334988315f 100644 --- a/static/app/components/profiling/FlamegraphZoomView.tsx +++ b/static/app/components/profiling/FlamegraphZoomView.tsx @@ -22,7 +22,6 @@ interface FlamegraphZoomViewProps { flamegraph: Flamegraph | DifferentialFlamegraph; flamegraphTheme: FlamegraphTheme; highlightRecursion: boolean; - searchResults: Record; showSelectedNodeStack?: boolean; } @@ -30,7 +29,6 @@ function FlamegraphZoomView({ flamegraph, canvasPoolManager, colorCoding, - searchResults, flamegraphTheme, highlightRecursion, }: FlamegraphZoomViewProps): React.ReactElement { @@ -47,39 +45,39 @@ function FlamegraphZoomView({ const flamegraphRenderer = useMemoWithPrevious( previousRenderer => { - if (flamegraphCanvasRef === null) { - return null; - } + if (flamegraphCanvasRef) { + const renderer = new FlamegraphRenderer( + flamegraphCanvasRef, + flamegraph, + flamegraphTheme, + flamegraph.inverted + ? vec2.fromValues(0, 0) + : vec2.fromValues( + 0, + flamegraphTheme.SIZES.TIMELINE_HEIGHT * window.devicePixelRatio + ), + {draw_border: true} + ); - const renderer = new FlamegraphRenderer( - flamegraphCanvasRef, - flamegraph, - flamegraphTheme, - flamegraph.inverted - ? vec2.fromValues(0, 0) - : vec2.fromValues( - 0, - flamegraphTheme.SIZES.TIMELINE_HEIGHT * window.devicePixelRatio + if (flamegraph.inverted) { + canvasPoolManager.dispatch('setConfigView', [ + renderer.configView.translateY( + renderer.configSpace.height - renderer.configView.height + 1 ), - {draw_border: true} - ); - - if (flamegraph.inverted) { - canvasPoolManager.dispatch('setConfigView', [ - renderer.configView.translateY( - renderer.configSpace.height - renderer.configView.height + 1 - ), - ]); - } - - // If the flamegraph name is the same as before, then the user probably changed the way they want - // to visualize the flamegraph. In those cases we want preserve the previous config view so - // that users dont lose their state. E.g. clicking on invert flamegraph still shows you the same - // flamegraph you were looking at before, just inverted instead of zooming out completely - if (previousRenderer?.flamegraph.name === renderer.flamegraph.name) { - renderer.setConfigView(previousRenderer.configView); + ]); + } + + // If the flamegraph name is the same as before, then the user probably changed the way they want + // to visualize the flamegraph. In those cases we want preserve the previous config view so + // that users dont lose their state. E.g. clicking on invert flamegraph still shows you the same + // flamegraph you were looking at before, just inverted instead of zooming out completely + if (previousRenderer?.flamegraph.name === renderer.flamegraph.name) { + renderer.setConfigView(previousRenderer.configView); + } + return renderer; } - return renderer; + // If we have no renderer, then the canvas is not initialize yet and we cannot initialize the renderer + return null; }, [ flamegraphCanvasRef, @@ -121,72 +119,54 @@ function FlamegraphZoomView({ setGridRenderer(newGridRenderer); function clearOverlayCanvas() { - // We are doing this because of typescript, in reality we are creating a scope - // where flamegraphRenderer is not null (see check on L86) - if (flamegraphRenderer === null) { - return; + if (flamegraphRenderer) { + newTextRenderer.context.clearRect( + 0, + 0, + flamegraphRenderer!.physicalSpace.width, + flamegraphRenderer!.physicalSpace.height + ); } - newTextRenderer.context.clearRect( - 0, - 0, - flamegraphRenderer!.physicalSpace.width, - flamegraphRenderer!.physicalSpace.height - ); } function drawText() { - // We are doing this because of typescript, in reality we are creating a scope - // where flamegraphRenderer is not null (see check on L86) - if (flamegraphRenderer === null) { - return; + if (flamegraphRenderer) { + newTextRenderer.draw( + flamegraphRenderer.configView, + flamegraphRenderer.configSpace, + flamegraphRenderer.configToPhysicalSpace + ); } - newTextRenderer.draw( - flamegraphRenderer.configView, - flamegraphRenderer.configSpace, - flamegraphRenderer.configToPhysicalSpace - ); } function drawRectangles() { - // We are doing this because of typescript, in reality we are creating a scope - // where flamegraphRenderer is not null (see check on L86) - if (flamegraphRenderer === null) { - return; + if (flamegraphRenderer) { + flamegraphRenderer.draw(null); } - flamegraphRenderer.draw(searchResults); } function drawGrid() { - // We are doing this because of typescript, in reality we are creating a scope - // where flamegraphRenderer is not null (see check on L86) - if (flamegraphRenderer === null) { - return; + if (flamegraphRenderer) { + newGridRenderer.draw( + flamegraphRenderer.configView, + flamegraphRenderer.physicalSpace, + flamegraphRenderer.configToPhysicalSpace + ); } - newGridRenderer.draw( - flamegraphRenderer.configView, - flamegraphRenderer.physicalSpace, - flamegraphRenderer.configToPhysicalSpace - ); } function onConfigViewChange(rect: Rect) { - // We are doing this because of typescript, in reality we are creating a scope - // where flamegraphRenderer is not null (see check on L86) - if (flamegraphRenderer === null) { - return; + if (flamegraphRenderer) { + flamegraphRenderer.setConfigView(rect); + newScheduler.draw(); } - flamegraphRenderer.setConfigView(rect); - newScheduler.draw(); } function onTransformConfigView(mat: mat3) { - // We are doing this because of typescript, in reality we are creating a scope - // where flamegraphRenderer is not null (see check on L86) - if (flamegraphRenderer === null) { - return; + if (flamegraphRenderer) { + flamegraphRenderer.transformConfigView(mat); + newScheduler.draw(); } - flamegraphRenderer.transformConfigView(mat); - newScheduler.draw(); } newScheduler.on('setConfigView', onConfigViewChange); @@ -206,6 +186,7 @@ function FlamegraphZoomView({ .translate(0, 0) .withWidth(flamegraph.configSpace.width) ); + setConfigSpaceCursor(null); newScheduler.draw(); }); @@ -224,8 +205,26 @@ function FlamegraphZoomView({ flamegraphRenderer.configView.height ) ); - setSelectedNode(frame); + setConfigSpaceCursor(null); + setSelectedNode(frame); + + newScheduler.draw(); + }); + + newScheduler.on('searchResults', (results: Record) => { + newScheduler.unregisterBeforeFrameCallback(drawRectangles); + + function newDrawRectangles() { + if (flamegraphRenderer) { + flamegraphRenderer.draw(results); + } + } + newScheduler.registerBeforeFrameCallback(newDrawRectangles); + + newScheduler.onDispose(() => + newScheduler.unregisterBeforeFrameCallback(newDrawRectangles) + ); newScheduler.draw(); }); @@ -248,10 +247,13 @@ function FlamegraphZoomView({ canvasPoolManager.registerScheduler(newScheduler); - return function () { + return () => { setScheduler(null); - // @TODO we can probably keep the grid renderer - setGridRenderer(null); + newScheduler.unregisterBeforeFrameCallback(clearOverlayCanvas); + newScheduler.unregisterBeforeFrameCallback(drawRectangles); + newScheduler.unregisterAfterFrameCallback(drawText); + newScheduler.unregisterAfterFrameCallback(drawGrid); + canvasPoolManager.unregisterScheduler(newScheduler); observer.disconnect(); }; @@ -262,27 +264,28 @@ function FlamegraphZoomView({ flamegraphCanvasRef, flamegraphOverlayCanvasRef, flamegraphRenderer, - searchResults, ]); - const selectedFrameRenderer = React.useMemo(() => { - if (!flamegraphOverlayCanvasRef) { - return null; - } - return new SelectedFrameRenderer(flamegraphOverlayCanvasRef); - }, [flamegraphOverlayCanvasRef, flamegraph, flamegraphTheme]); + const selectedFrameRenderer = React.useMemo( + () => + flamegraphOverlayCanvasRef + ? new SelectedFrameRenderer(flamegraphOverlayCanvasRef) + : null, + [flamegraphOverlayCanvasRef, flamegraph, flamegraphTheme] + ); const [selectedNode, setSelectedNode] = React.useState(null); - const [configSpaceCursor, setConfigSpaceCursor] = React.useState< [number, number] | null >(null); - const hoveredNode = React.useMemo(() => { - if (!configSpaceCursor || !flamegraphRenderer) { - return null; - } - return flamegraphRenderer.getHoveredNode(configSpaceCursor); - }, [configSpaceCursor, flamegraphRenderer]); + + const hoveredNode = React.useMemo( + () => + configSpaceCursor && flamegraphRenderer + ? flamegraphRenderer.getHoveredNode(configSpaceCursor) + : null, + [configSpaceCursor, flamegraphRenderer] + ); React.useEffect(() => { if ( @@ -387,7 +390,7 @@ function FlamegraphZoomView({ return; } - // Only dispatch the zoom action if the new clicked node is not the same as the old selected node + // Only dispatch the zoom action if the new clicked node is not the same as the old selected node. // This essentialy tracks double click action on a rectangle if (hoveredNode && selectedNode && hoveredNode === selectedNode) { canvasPoolManager.dispatch('zoomIntoFrame', [hoveredNode]); @@ -401,10 +404,7 @@ function FlamegraphZoomView({ const onCanvasMouseMove = React.useCallback( (evt: React.MouseEvent) => { - if (!flamegraphRenderer) { - return; - } - if (!flamegraphRenderer.frames.length) { + if (!flamegraphRenderer?.frames.length) { return; } @@ -423,7 +423,7 @@ function FlamegraphZoomView({ const zoom = React.useCallback( (evt: WheelEvent) => { - if (!flamegraphRenderer || !flamegraphRenderer.frames.length) { + if (!flamegraphRenderer?.frames.length) { return; } @@ -456,7 +456,7 @@ function FlamegraphZoomView({ const scroll = React.useCallback( (evt: WheelEvent) => { - if (!flamegraphRenderer || !flamegraphRenderer.frames.length) { + if (!flamegraphRenderer?.frames.length) { return; } @@ -507,7 +507,7 @@ function FlamegraphZoomView({ flamegraphCanvasRef.addEventListener('wheel', onCanvasWheel); - return function () { + return () => { flamegraphCanvasRef.removeEventListener('wheel', onCanvasWheel); }; }, [flamegraphCanvasRef, zoom, scroll]); diff --git a/static/app/utils/profiling/canvasScheduler.tsx b/static/app/utils/profiling/canvasScheduler.tsx index c8c48f29914a49..db1f93d8755cf3 100644 --- a/static/app/utils/profiling/canvasScheduler.tsx +++ b/static/app/utils/profiling/canvasScheduler.tsx @@ -1,6 +1,7 @@ import {mat3} from 'gl-matrix'; import {Rect} from './gl/utils'; +import {FlamegraphFrame} from './flamegraphFrame'; type DrawFn = () => void; type ArgumentTypes = F extends (...args: infer A) => any ? A : never; @@ -10,6 +11,7 @@ export interface FlamegraphEvents { selectedNode: (frame: any | null) => void; setConfigView: (configView: Rect) => void; transformConfigView: (transform: mat3) => void; + searchResults: (results: Record) => void; zoomIntoFrame: (frame: any) => void; } @@ -18,16 +20,27 @@ type EventStore = {[K in keyof FlamegraphEvents]: Set}; export class CanvasScheduler { beforeFrameCallbacks: Set = new Set(); afterFrameCallbacks: Set = new Set(); + + onDisposeCallbacks: Set<() => void> = new Set(); requestAnimationFrame: number | null = null; events: EventStore = { - setConfigView: new Set(), transformConfigView: new Set(), zoomIntoFrame: new Set(), selectedNode: new Set(), resetZoom: new Set(), + searchResults: new Set(), + setConfigView: new Set(), }; + onDispose(cb: () => void): void { + if (this.onDisposeCallbacks.has(cb)) { + return; + } + + this.onDisposeCallbacks.add(cb); + } + on(eventName: K, cb: FlamegraphEvents[K]): void { const set = this.events[eventName] as unknown as Set; if (set.has(cb)) { @@ -81,6 +94,10 @@ export class CanvasScheduler { } dispose(): void { + for (const cb of this.onDisposeCallbacks) { + this.onDisposeCallbacks.delete(cb); + cb(); + } for (const type in this.events) { this.events[type].clear(); } diff --git a/static/app/utils/profiling/validators/regExp.tsx b/static/app/utils/profiling/validators/regExp.tsx new file mode 100644 index 00000000000000..0fe4b4b307fafb --- /dev/null +++ b/static/app/utils/profiling/validators/regExp.tsx @@ -0,0 +1,13 @@ +const REG_EXP = /(.*)\/([dgimsuy])/; + +export const parseRegExp = (string: string): RegExpMatchArray | null => { + return string.match(REG_EXP); +}; + +export const isRegExpString = (string?: string): boolean => { + if (!string?.trim().length) { + return false; + } + + return REG_EXP.test(string); +}; From 1870aa3cc36f679b611831ce7d003dd0b386e170 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Thu, 10 Feb 2022 10:24:52 +0000 Subject: [PATCH 4/7] style(lint): Auto commit lint changes --- static/app/components/profiling/FlamegraphSearch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/profiling/FlamegraphSearch.tsx b/static/app/components/profiling/FlamegraphSearch.tsx index 574fe9a3529688..e90528ac1fabdf 100644 --- a/static/app/components/profiling/FlamegraphSearch.tsx +++ b/static/app/components/profiling/FlamegraphSearch.tsx @@ -32,9 +32,9 @@ const numericSort = ( }; interface FlamegraphSearchProps { + canvasPoolManager: CanvasPoolManager; flamegraphs: Flamegraph[]; placement: 'top' | 'bottom'; - canvasPoolManager: CanvasPoolManager; } function FlamegraphSearch({ From 7ddc2f3ce7db5ce4d4492731eb2156a4fce08253 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Thu, 10 Feb 2022 10:26:22 +0000 Subject: [PATCH 5/7] style(lint): Auto commit lint changes --- static/app/utils/profiling/canvasScheduler.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/utils/profiling/canvasScheduler.tsx b/static/app/utils/profiling/canvasScheduler.tsx index db1f93d8755cf3..d616a251757d47 100644 --- a/static/app/utils/profiling/canvasScheduler.tsx +++ b/static/app/utils/profiling/canvasScheduler.tsx @@ -8,10 +8,10 @@ type ArgumentTypes = F extends (...args: infer A) => any ? A : never; export interface FlamegraphEvents { resetZoom: () => void; + searchResults: (results: Record) => void; selectedNode: (frame: any | null) => void; setConfigView: (configView: Rect) => void; transformConfigView: (transform: mat3) => void; - searchResults: (results: Record) => void; zoomIntoFrame: (frame: any) => void; } From 9c92b29e5b3268a5e7f46cc07bc9f2bca0a93947 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Mon, 14 Feb 2022 13:31:21 +0100 Subject: [PATCH 6/7] feat(profiling): decouple search fn --- .../components/profiling/FlamegraphSearch.tsx | 104 ++++++++++-------- 1 file changed, 60 insertions(+), 44 deletions(-) diff --git a/static/app/components/profiling/FlamegraphSearch.tsx b/static/app/components/profiling/FlamegraphSearch.tsx index e90528ac1fabdf..0a066ddeecefc9 100644 --- a/static/app/components/profiling/FlamegraphSearch.tsx +++ b/static/app/components/profiling/FlamegraphSearch.tsx @@ -13,6 +13,63 @@ function uniqueFrameKey(frame: FlamegraphFrame): string { return `${frame.frame.key + String(frame.start)}`; } +function frameSearch( + query: string, + frames: ReadonlyArray, + index: Fuse< + FlamegraphFrame, + {includeMatches: true; keys: 'frame.name'[]; threshold: number} + > +): Record { + const results = {}; + if (isRegExpString(query)) { + const [_, lookup, flags] = parseRegExp(query) ?? []; + + try { + if (!lookup) { + throw new Error('Invalid RegExp'); + } + + for (let i = 0; i < frames.length; i++) { + const frame = frames[i]; + + if (new RegExp(lookup, flags ?? 'g').test(frame.frame.name.trim())) { + results[ + `${ + frame.frame.name + + (frame.frame.file ? frame.frame.file : '') + + String(frame.start) + }` + ] = frame; + } + } + + return results; + } catch (e) { + Sentry.captureMessage(e.message); + return results; + } + } + + const fuseResults = index + .search(query) + .sort((a, b) => numericSort(a.item.start, b.item.start, 'asc')); + + for (let i = 0; i < fuseResults.length; i++) { + const frame = fuseResults[i]; + + results[ + `${ + frame.item.frame.name + + (frame.item.frame.file ? frame.item.frame.file : '') + + String(frame.item.start) + }` + ] = frame.item; + } + + return results; +} + const numericSort = ( a: null | undefined | number, b: null | undefined | number, @@ -75,55 +132,14 @@ function FlamegraphSearch({ const handleSearchInput = React.useCallback( (evt: React.ChangeEvent) => { const query = evt.currentTarget.value; - const results: Record = {}; if (!query) { - setSearchResults(results); - canvasPoolManager.dispatch('searchResults', [results]); + setSearchResults({}); + canvasPoolManager.dispatch('searchResults', [{}]); return; } - if (isRegExpString(query)) { - const [_, lookup, flags] = parseRegExp(query) ?? []; - - try { - if (!lookup) { - throw new Error('Invalid RegExp'); - } - - const userQuery = new RegExp(lookup, flags || 'g'); - - for (let i = 0; i < allFrames.length; i++) { - const frame = allFrames[i]; - - if (userQuery.test(frame.frame.name.trim())) { - results[ - `${ - frame.frame.name + - (frame.frame.file ? frame.frame.file : '') + - String(frame.start) - }` - ] = frame; - } - } - } catch (e) { - Sentry.captureMessage(e.message); - } - } else { - const fuseResults = searchIndex - .search(query) - .sort((a, b) => numericSort(a.item.start, b.item.start, 'asc')); - - for (const frame of fuseResults) { - results[ - `${ - frame.item.frame.name + - (frame.item.frame.file ? frame.item.frame.file : '') + - String(frame.item.start) - }` - ] = frame.item; - } - } + const results = frameSearch(query, allFrames, searchIndex); setSearchResults(results); canvasPoolManager.dispatch('searchResults', [results]); From aaf8e38c18c4a5f1ee9730a2005ac018838f6f23 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 15 Feb 2022 09:25:11 +0100 Subject: [PATCH 7/7] fix(flamegraphsearch): missing hook deps --- static/app/components/profiling/FlamegraphSearch.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/static/app/components/profiling/FlamegraphSearch.tsx b/static/app/components/profiling/FlamegraphSearch.tsx index 0a066ddeecefc9..5b87cabde5698a 100644 --- a/static/app/components/profiling/FlamegraphSearch.tsx +++ b/static/app/components/profiling/FlamegraphSearch.tsx @@ -43,12 +43,11 @@ function frameSearch( ] = frame; } } - - return results; } catch (e) { Sentry.captureMessage(e.message); - return results; } + + return results; } const fuseResults = index @@ -119,7 +118,7 @@ function FlamegraphSearch({ threshold: 0.3, includeMatches: true, }); - }, [allFrames.length]); + }, [allFrames]); const onZoomIntoFrame = React.useCallback( (frame: FlamegraphFrame) => { @@ -144,7 +143,7 @@ function FlamegraphSearch({ setSearchResults(results); canvasPoolManager.dispatch('searchResults', [results]); }, - [searchIndex, frames, canvasPoolManager] + [searchIndex, frames, canvasPoolManager, allFrames] ); const onNextSearchClick = React.useCallback(() => {