Files
SpeedScope/src/lib/profile-search.ts
T
Jamie Wong 9452aeae82 Provide prev/next buttons to cycle through search results, make search results more visually prominent (#304)
This PR addresses two key pieces of feedback provided on search in #38 

1. Make the search results more visually prominent
2. Make it easier to find the matches by having some way of jumping to next

For the visual prominence facet, I switched from yellow outlines to orange backgrounds.

|Before|After|
|-|-|
|![image](https://user-images.githubusercontent.com/150329/89276105-14746700-d5f8-11ea-9c9d-1dfdfc3bd6d7.png)|![image](https://user-images.githubusercontent.com/150329/89276070-045c8780-d5f8-11ea-8664-9da0af569cec.png)|

For the easy identification portion, I added prev/next buttons to each view, which can also be operated by hitting Enter for next or Shift+Enter for previous.

![Kapture 2020-08-04 at 2 16 57](https://user-images.githubusercontent.com/150329/89276542-a1b7bb80-d5f8-11ea-8642-a172a6561734.gif)
2020-08-04 02:20:26 -07:00

91 lines
2.7 KiB
TypeScript

import {Profile, Frame, CallTreeNode} from './profile'
import {FuzzyMatch, fuzzyMatchStrings} from './fuzzy-find'
import {Flamechart, FlamechartFrame} from './flamechart'
import {Rect, Vec2} from './math'
export enum FlamechartType {
CHRONO_FLAME_CHART,
LEFT_HEAVY_FLAME_GRAPH,
}
// A utility class for storing cached search results to avoid recomputation when
// the search results & profile did not change.
export class ProfileSearchResults {
constructor(readonly profile: Profile, readonly searchQuery: string) {}
private matches: Map<Frame, FuzzyMatch> | null = null
getMatchForFrame(frame: Frame): FuzzyMatch | null {
if (!this.matches) {
this.matches = new Map()
this.profile.forEachFrame(frame => {
const match = fuzzyMatchStrings(frame.name, this.searchQuery)
if (match == null) return
this.matches!.set(frame, match)
})
}
return this.matches.get(frame) || null
}
}
export interface FlamechartSearchMatch {
configSpaceBounds: Rect
node: CallTreeNode
}
interface CachedFlamechartResult {
matches: FlamechartSearchMatch[]
indexForNode: Map<CallTreeNode, number>
}
export class FlamechartSearchResults {
constructor(readonly flamechart: Flamechart, readonly profileResults: ProfileSearchResults) {}
private matches: CachedFlamechartResult | null = null
private getResults(): CachedFlamechartResult {
if (this.matches == null) {
const matches: FlamechartSearchMatch[] = []
const indexForNode = new Map<CallTreeNode, number>()
const visit = (frame: FlamechartFrame, depth: number) => {
const {node} = frame
if (this.profileResults.getMatchForFrame(node.frame)) {
const configSpaceBounds = new Rect(
new Vec2(frame.start, depth),
new Vec2(frame.end - frame.start, 1),
)
indexForNode.set(node, matches.length)
matches.push({configSpaceBounds, node})
}
frame.children.forEach(child => {
visit(child, depth + 1)
})
}
const layers = this.flamechart.getLayers()
if (layers.length > 0) {
layers[0].forEach(frame => visit(frame, 0))
}
this.matches = {matches, indexForNode}
}
return this.matches
}
count(): number {
return this.getResults().matches.length
}
indexOf(node: CallTreeNode): number | null {
const result = this.getResults().indexForNode.get(node)
return result === undefined ? null : result
}
at(index: number): FlamechartSearchMatch {
const matches = this.getResults().matches
if (index < 0 || index >= matches.length) {
throw new Error(`Index ${index} out of bounds in list of ${matches.length} matches.`)
}
return matches[index]
}
}