Skip to content

Add functionality to filter flame graphs by search query #503

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/app-state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const flattenRecursionAtom = new Atom<boolean>(false, 'flattenRecursion')
export const searchIsActiveAtom = new Atom<boolean>(false, 'searchIsActive')
export const searchQueryAtom = new Atom<string>('', 'searchQueryAtom')

// True if the flamechart should only show the matching frames when searching.
export const onlyMatchesAtom = new Atom<boolean>(false, 'onlyMatches')

// Which top-level view should be displayed
export const viewModeAtom = new Atom<ViewMode>(ViewMode.CHRONO_FLAME_CHART, 'viewMode')

Expand Down
1 change: 1 addition & 0 deletions src/lib/profile-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class ProfileSearchResults {
constructor(
readonly profile: Profile,
readonly searchQuery: string,
readonly onlyMatches: boolean,
) {}

private matches: Map<Frame, [number, number][] | null> | null = null
Expand Down
43 changes: 43 additions & 0 deletions src/lib/profile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {lastOf, KeyedSet} from './utils'
import {ValueFormatter, RawValueFormatter} from './value-formatters'
import {FileFormat} from './file-format-spec'
import {ProfileSearchResults} from './profile-search'

export interface FrameInfo {
key: string | number
Expand Down Expand Up @@ -196,6 +197,48 @@ export class Profile {
visit(this.groupedCalltreeRoot)
}

// Filter the calls of a traversal function (e.g. `forEachCall` or `forEachCallGrouped`)
// based on the results of a search.
filteredTraversal(
searchResults: ProfileSearchResults | null,
traversalFn: (
openFrame: (node: CallTreeNode, value: number) => void,
closeFrame: (node: CallTreeNode, value: number) => void,
) => void,
) {
if (searchResults == null || !searchResults.onlyMatches) {
return traversalFn
}
const traversal = (
openFrame: (node: CallTreeNode, value: number) => void,
closeFrame: (node: CallTreeNode, value: number) => void,
) => {
const matchingFrames = new Set<Frame>()
this.forEachFrame(frame => {
if (searchResults.getMatchForFrame(frame)) {
matchingFrames.add(frame)
}
})
if (matchingFrames.size === 0) {
return
}
// Use the original traversal function but filter callbacks
traversalFn(
(node, value) => {
if (matchingFrames.has(node.frame)) {
openFrame(node, value)
}
},
(node, value) => {
if (matchingFrames.has(node.frame)) {
closeFrame(node, value)
}
},
)
}
return traversal
}

forEachCallGrouped(
openFrame: (node: CallTreeNode, value: number) => void,
closeFrame: (node: CallTreeNode, value: number) => void,
Expand Down
8 changes: 8 additions & 0 deletions src/views/flamechart-search-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {Rect, Vec2} from '../lib/math'
import {h, createContext, ComponentChildren} from 'preact'
import {Flamechart} from '../lib/flamechart'
import {CallTreeNode} from '../lib/profile'
import {useAtom} from '../lib/atom'
import {onlyMatchesAtom} from '../app-state'

export const FlamechartSearchContext = createContext<FlamechartSearchData | null>(null)

Expand Down Expand Up @@ -65,6 +67,7 @@ export const FlamechartSearchContextProvider = ({

export const FlamechartSearchView = memo(() => {
const flamechartData = useContext(FlamechartSearchContext)
const onlyMatches = useAtom(onlyMatchesAtom)

// TODO(jlfwong): This pattern is pretty gross, but I really don't want values
// that can be undefined or null.
Expand All @@ -84,6 +87,10 @@ export const FlamechartSearchView = memo(() => {
return searchResults.indexOf(selectedNode)
}, [searchResults, selectedNode])

const toggleOnlyMatches = useCallback(() => {
onlyMatchesAtom.set(!onlyMatches)
}, [onlyMatches])

const selectAndZoomToMatch = useCallback(
(match: FlamechartSearchMatch) => {
if (!setSelectedNode) return
Expand Down Expand Up @@ -146,6 +153,7 @@ export const FlamechartSearchView = memo(() => {
numResults={numResults}
selectPrev={selectPrev}
selectNext={selectNext}
toggleOnlyMatches={toggleOnlyMatches}
/>
)
})
26 changes: 21 additions & 5 deletions src/views/flamechart-view-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
getFrameToColorBucket,
} from '../app-state/getters'
import {Vec2, Rect} from '../lib/math'
import {memo, useCallback} from 'preact/compat'
import {memo, useCallback, useContext} from 'preact/compat'
import {ActiveProfileState} from '../app-state/active-profile-state'
import {FlamechartSearchContextProvider} from './flamechart-search-view'
import {Theme, useTheme} from './themes/theme'
import {FlamechartID, FlamechartViewState} from '../app-state/profile-group'
import {profileGroupAtom} from '../app-state'
import {ProfileSearchContext} from './search-view'
import {ProfileSearchResults} from '../lib/profile-search'

interface FlamechartSetters {
setLogicalSpaceViewportSize: (logicalSpaceViewportSize: Vec2) => void
Expand Down Expand Up @@ -70,15 +72,19 @@ export const getChronoViewFlamechart = memoizeByShallowEquality(
({
profile,
getColorBucketForFrame,
searchResults,
}: {
profile: Profile
getColorBucketForFrame: (frame: Frame) => number
searchResults: ProfileSearchResults | null
}): Flamechart => {
return new Flamechart({
getTotalWeight: profile.getTotalWeight.bind(profile),
forEachCall: profile.forEachCall.bind(profile),
formatValue: profile.formatValue.bind(profile),
getColorBucketForFrame,
forEachCall: profile
.filteredTraversal(searchResults, profile.forEachCall.bind(profile))
.bind(profile),
})
},
)
Expand Down Expand Up @@ -115,13 +121,18 @@ export const ChronoFlamechartView = memo((props: FlamechartViewContainerProps) =
const {profile, chronoViewState} = activeProfileState

const theme = useTheme()
const profileSearchResults = useContext(ProfileSearchContext)

const canvasContext = getCanvasContext({theme, canvas: glCanvas})
const frameToColorBucket = getFrameToColorBucket(profile)
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
const getCSSColorForFrame = createGetCSSColorForFrame({theme, frameToColorBucket})

const flamechart = getChronoViewFlamechart({profile, getColorBucketForFrame})
const flamechart = getChronoViewFlamechart({
profile,
getColorBucketForFrame,
searchResults: profileSearchResults,
})
const flamechartRenderer = getChronoViewFlamechartRenderer({
canvasContext,
flamechart,
Expand Down Expand Up @@ -155,15 +166,19 @@ export const getLeftHeavyFlamechart = memoizeByShallowEquality(
({
profile,
getColorBucketForFrame,
searchResults,
}: {
profile: Profile
getColorBucketForFrame: (frame: Frame) => number
searchResults: ProfileSearchResults | null
}): Flamechart => {
return new Flamechart({
getTotalWeight: profile.getTotalNonIdleWeight.bind(profile),
forEachCall: profile.forEachCallGrouped.bind(profile),
formatValue: profile.formatValue.bind(profile),
getColorBucketForFrame,
forEachCall: profile
.filteredTraversal(searchResults, profile.forEachCallGrouped.bind(profile))
.bind(profile),
})
},
)
Expand All @@ -176,7 +191,7 @@ export const LeftHeavyFlamechartView = memo((ownProps: FlamechartViewContainerPr
const {profile, leftHeavyViewState} = activeProfileState

const theme = useTheme()

const profileSearchResults = useContext(ProfileSearchContext)
const canvasContext = getCanvasContext({theme, canvas: glCanvas})
const frameToColorBucket = getFrameToColorBucket(profile)
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
Expand All @@ -185,6 +200,7 @@ export const LeftHeavyFlamechartView = memo((ownProps: FlamechartViewContainerPr
const flamechart = getLeftHeavyFlamechart({
profile,
getColorBucketForFrame,
searchResults: profileSearchResults,
})
const flamechartRenderer = getLeftHeavyFlamechartRenderer({
canvasContext,
Expand Down
1 change: 1 addition & 0 deletions src/views/sandwich-search-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const SandwichSearchView = memo(() => {
numResults={numResults}
selectPrev={selectPrev}
selectNext={selectNext}
toggleOnlyMatches={null}
/>
)
})
15 changes: 11 additions & 4 deletions src/views/search-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {ProfileSearchResults} from '../lib/profile-search'
import {Profile} from '../lib/profile'
import {useActiveProfileState} from '../app-state/active-profile-state'
import {useTheme, withTheme} from './themes/theme'
import {searchIsActiveAtom, searchQueryAtom} from '../app-state'
import {searchIsActiveAtom, onlyMatchesAtom, searchQueryAtom} from '../app-state'
import {useAtom} from '../lib/atom'

function stopPropagation(ev: Event) {
Expand All @@ -21,13 +21,14 @@ export const ProfileSearchContextProvider = ({children}: {children: ComponentChi
const profile: Profile | null = activeProfileState ? activeProfileState.profile : null
const searchIsActive = useAtom(searchIsActiveAtom)
const searchQuery = useAtom(searchQueryAtom)
const onlyMatches = useAtom(onlyMatchesAtom)

const searchResults = useMemo(() => {
if (!profile || !searchIsActive || searchQuery.length === 0) {
return null
}
return new ProfileSearchResults(profile, searchQuery)
}, [searchIsActive, searchQuery, profile])
return new ProfileSearchResults(profile, searchQuery, onlyMatches)
}, [searchIsActive, searchQuery, profile, onlyMatches])

return (
<ProfileSearchContext.Provider value={searchResults}>{children}</ProfileSearchContext.Provider>
Expand All @@ -39,10 +40,11 @@ interface SearchViewProps {
numResults: number | null
selectNext: () => void
selectPrev: () => void
toggleOnlyMatches: (() => void) | null
}

export const SearchView = memo(
({numResults, resultIndex, selectNext, selectPrev}: SearchViewProps) => {
({numResults, resultIndex, selectNext, selectPrev, toggleOnlyMatches}: SearchViewProps) => {
const theme = useTheme()
const style = getStyle(theme)
const searchIsActive = useAtom(searchIsActiveAtom)
Expand Down Expand Up @@ -157,6 +159,11 @@ export const SearchView = memo(
<button className={css(style.icon, style.button)} onClick={selectNext}>
➡️
</button>
{toggleOnlyMatches && (
<button className={css(style.icon, style.button)} onClick={toggleOnlyMatches}>
🎯
</button>
)}
</Fragment>
)}
<svg
Expand Down