Compare commits

...
3 Commits
Author SHA1 Message Date
Jamie Wong c3b35d7b0f 1.8.0 2020-07-19 21:27:00 -07:00
Jamie Wong dfaefe54fd Implement search highlighting in time order & left heavy views (#297)
This implements the next step towards full featured search in speedscope: visual highlighting of matching search results in the time ordered & left heavy views. This doesn't yet add the ability to click prev/next to select the next matching element in the editor, but I'm still planning on doing something like that. I haven't figured out yet what I want the user experience to be like for that.

![speedscope-flamegraph-search](https://user-images.githubusercontent.com/150329/87898991-9ebba900-ca04-11ea-9bd9-31ad8d4c6d2a.gif)

This works towards fixing #38
2020-07-19 21:20:14 -07:00
Jamie Wong 7514f4c0c9 Fix performance issues for the caller/callee flamegraphs in the sandwich view (#296)
This fixes two unrelated problems which together caused performance issues in the sandwich view & made hover tooltips appear to be broken.

The first issue was caused by continuously priming the `requestAnimationFrame` loop when it should be a no-op, and the second issue was caused by using different cache keys when trying to access a memoized value in the caller & callee flamegraph components. This resulted in thrash, and especially bad performance because the cache miss was resulting in us re-allocating the WebGL framebuffer on every frame, which is unsurprisingly quite slow.

Fixes #212 
Fixes #155 
Fixes #74 (though this was maybe already fixed)
2020-07-18 22:37:15 -07:00
15 changed files with 475 additions and 59 deletions
+10
View File
@@ -1,5 +1,15 @@
## Unreleased
## [1.8.0] - 2020-07-19
### Added
- Added search highlighting in time order & left heavy views [[#297](https://github.com/jlfwong/speedscope/pull/297)]
### Fixed
- Fix performance issues for the caller/callee flamegraphs in the sandwich view [[#296](https://github.com/jlfwong/speedscope/pull/296)]
## [1.7.0] - 2020-07-13
### Added
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "speedscope",
"version": "1.7.0",
"version": "1.8.0",
"description": "",
"repository": "jlfwong/speedscope",
"main": "index.js",
+116
View File
@@ -0,0 +1,116 @@
import {buildTrimmedText, ELLIPSIS, remapRangesToTrimmedText} from './text-utils'
import {fuzzyMatchStrings} from './fuzzy-find'
function assertTrimmed(text: string, length: number, expectedTrimmed: string) {
expect(buildTrimmedText(text, length).trimmedString).toEqual(
expectedTrimmed.replace('...', ELLIPSIS),
)
}
test('buildTrimmedText', () => {
assertTrimmed('hello world', 1, '...')
assertTrimmed('hello world', 2, 'h...')
assertTrimmed('hello world', 3, 'h...d')
assertTrimmed('hello world', 4, 'he...d')
assertTrimmed('hello world', 10, 'hello...orld')
assertTrimmed('hello world', 11, 'hello world')
assertTrimmed('hello world', 100, 'hello world')
})
function highlightText(text: string, highlightedRanges: [number, number][]): string {
let last = 0
let highlighted = ''
for (let range of highlightedRanges) {
highlighted += `${text.slice(last, range[0])}[${text.slice(range[0], range[1])}]`
last = range[1]
}
highlighted += text.slice(last)
return highlighted
}
function assertTrimmedHighlight({
text,
pattern,
expectedHighlighted,
length,
expectedHighlightedTrimmed,
}: {
text: string
pattern: string
expectedHighlighted: string
length: number
expectedHighlightedTrimmed: string
}) {
const match = fuzzyMatchStrings(text, pattern)
const trimmed = buildTrimmedText(text, length)
if (!match) {
fail()
return
}
const matchedRangesForTrimmedText = remapRangesToTrimmedText(trimmed, match.matchedRanges)
const highlighted = highlightText(text, match.matchedRanges)
const highlightedTrimmed = highlightText(trimmed.trimmedString, matchedRangesForTrimmedText)
expect(highlighted).toEqual(expectedHighlighted)
expect(highlightedTrimmed).toEqual(expectedHighlightedTrimmed.replace('...', ELLIPSIS))
}
test('remapRangesToTrimmedText', () => {
assertTrimmedHighlight({
text: 'hello world',
pattern: 'he',
length: 4,
expectedHighlighted: '[he]llo world',
expectedHighlightedTrimmed: `[he]...d`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'o w',
length: 4,
expectedHighlighted: 'hell[o w]orld',
expectedHighlightedTrimmed: `he[...]d`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'ow',
length: 4,
expectedHighlighted: 'hell[o] [w]orld',
expectedHighlightedTrimmed: `he[...]d`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'hello',
length: 4,
expectedHighlighted: '[hello] world',
expectedHighlightedTrimmed: `[he...]d`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'hello world',
length: 4,
expectedHighlighted: '[hello world]',
expectedHighlightedTrimmed: `[he...d]`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'helloworld',
length: 4,
expectedHighlighted: '[hello] [world]',
expectedHighlightedTrimmed: `[he...][d]`,
})
assertTrimmedHighlight({
text: 'hello world',
pattern: 'world',
length: 4,
expectedHighlighted: 'hello [world]',
expectedHighlightedTrimmed: `he[...d]`,
})
})
+174 -8
View File
@@ -18,22 +18,188 @@ export function cachedMeasureTextWidth(ctx: CanvasRenderingContext2D, text: stri
return measureTextCache.get(text)!
}
function buildTrimmedText(text: string, length: number) {
const prefixLength = Math.floor(length / 2)
const prefix = text.substr(0, prefixLength)
const suffix = text.substr(text.length - prefixLength, prefixLength)
return prefix + ELLIPSIS + suffix
interface TrimmedTextResult {
trimmedString: string
trimmedLength: number
prefixLength: number
suffixLength: number
originalLength: number
originalString: string
}
export function trimTextMid(ctx: CanvasRenderingContext2D, text: string, maxWidth: number) {
if (cachedMeasureTextWidth(ctx, text) <= maxWidth) return text
// Trim text, placing an ellipsis in the middle, with a slight bias towards
// keeping text from the beginning rather than the end
export function buildTrimmedText(text: string, length: number): TrimmedTextResult {
if (text.length <= length) {
return {
trimmedString: text,
trimmedLength: text.length,
prefixLength: text.length,
suffixLength: 0,
originalString: text,
originalLength: text.length,
}
}
let prefixLength = Math.floor(length / 2)
const suffixLength = length - prefixLength - 1
const prefix = text.substr(0, prefixLength)
const suffix = text.substr(text.length - suffixLength, suffixLength)
const trimmedString = prefix + ELLIPSIS + suffix
return {
trimmedString,
trimmedLength: trimmedString.length,
prefixLength: prefix.length,
suffixLength: suffix.length,
originalString: text,
originalLength: text.length,
}
}
// Trim text to fit within the given number of pixels on the canvas
export function trimTextMid(
ctx: CanvasRenderingContext2D,
text: string,
maxWidth: number,
): TrimmedTextResult {
if (cachedMeasureTextWidth(ctx, text) <= maxWidth) {
return buildTrimmedText(text, text.length)
}
const [lo] = binarySearch(
0,
text.length,
n => {
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, n))
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, n).trimmedString)
},
maxWidth,
)
return buildTrimmedText(text, lo)
}
enum IndexTypeInTrimmed {
IN_PREFIX,
IN_SUFFIX,
ELIDED,
}
function getIndexTypeInTrimmed(result: TrimmedTextResult, index: number): IndexTypeInTrimmed {
if (index < result.prefixLength) {
return IndexTypeInTrimmed.IN_PREFIX
} else if (index < result.originalLength - result.suffixLength) {
return IndexTypeInTrimmed.ELIDED
} else {
return IndexTypeInTrimmed.IN_SUFFIX
}
}
export function remapRangesToTrimmedText(
trimmedText: TrimmedTextResult,
ranges: [number, number][],
): [number, number][] {
// We intentionally don't just re-run fuzzy matching on the trimmed
// text, beacuse if the search query is "helloWorld", the frame name
// is "application::helloWorld", and that gets trimmed down to
// "appl...oWorld", we still want "oWorld" to be highlighted, even
// though the string "appl...oWorld" is not matched by the query
// "helloWorld".
//
// There's a weird case to consider here: what if the trimmedText is
// also matched by the query, but results in a different match than
// the original query? Consider, e.g. the search string of "ab". The
// string "hello ab shabby" will be matched at the first "ab", but
// may be trimmed to "hello...shabby". In this case, should we
// highlight the "ab" hidden by the ellipsis, or the "ab" in
// "shabby"? The code below highlights the ellipsis so that the
// matched characters don't change as you zoom in and out.
const rangesToHighlightInTrimmedText: [number, number][] = []
const lengthLoss = trimmedText.originalLength - trimmedText.trimmedLength
let highlightedEllipsis = false
for (let [origStart, origEnd] of ranges) {
let startPosType = getIndexTypeInTrimmed(trimmedText, origStart)
let endPosType = getIndexTypeInTrimmed(trimmedText, origEnd - 1)
switch (startPosType) {
case IndexTypeInTrimmed.IN_PREFIX: {
switch (endPosType) {
case IndexTypeInTrimmed.IN_PREFIX: {
// The entire range fits in the prefix. Add it unmodified.
rangesToHighlightInTrimmedText.push([origStart, origEnd])
break
}
case IndexTypeInTrimmed.ELIDED: {
// The range starts in the prefix, but ends in the elided
// section. Add just the prefix + one char for the ellipsis.
rangesToHighlightInTrimmedText.push([
origStart,
origStart + trimmedText.prefixLength + 1,
])
highlightedEllipsis = true
break
}
case IndexTypeInTrimmed.IN_SUFFIX: {
// The range crosses from the prefix to the suffix.
// Highlight everything including the ellipsis.
rangesToHighlightInTrimmedText.push([origStart, origEnd - lengthLoss])
break
}
}
break
}
case IndexTypeInTrimmed.ELIDED: {
switch (endPosType) {
case IndexTypeInTrimmed.IN_PREFIX: {
// This should be impossible
throw new Error('Unexpected highlight range starts in elided and ends in prefix')
}
case IndexTypeInTrimmed.ELIDED: {
// The match starts & ends within the elided section.
if (!highlightedEllipsis) {
rangesToHighlightInTrimmedText.push([
trimmedText.prefixLength,
trimmedText.prefixLength + 1,
])
highlightedEllipsis = true
}
break
}
case IndexTypeInTrimmed.IN_SUFFIX: {
// The match starts in elided, but ends in suffix.
if (highlightedEllipsis) {
rangesToHighlightInTrimmedText.push([
trimmedText.trimmedLength - trimmedText.suffixLength,
origEnd - lengthLoss,
])
} else {
rangesToHighlightInTrimmedText.push([trimmedText.prefixLength, origEnd - lengthLoss])
highlightedEllipsis = true
}
break
}
}
break
}
case IndexTypeInTrimmed.IN_SUFFIX: {
switch (endPosType) {
case IndexTypeInTrimmed.IN_PREFIX: {
// This should be impossible
throw new Error('Unexpected highlight range starts in suffix and ends in prefix')
}
case IndexTypeInTrimmed.ELIDED: {
// This should be impossible
throw new Error('Unexpected highlight range starts in suffix and ends in elided')
break
}
case IndexTypeInTrimmed.IN_SUFFIX: {
// Match starts & ends in suffix
rangesToHighlightInTrimmedText.push([origStart - lengthLoss, origEnd - lengthLoss])
break
}
}
break
}
}
}
return rangesToHighlightInTrimmedText
}
-4
View File
@@ -40,10 +40,6 @@ export const getRowAtlas = memoizeByReference((canvasContext: CanvasContext) =>
)
})
export const getProfileWithRecursionFlattened = memoizeByReference((profile: Profile) =>
profile.getProfileWithRecursionFlattened(),
)
export const getProfileToView = memoizeByShallowEquality(
({profile, flattenRecursion}: {profile: Profile; flattenRecursion: boolean}): Profile => {
return flattenRecursion ? profile.getProfileWithRecursionFlattened() : profile
+6 -1
View File
@@ -13,7 +13,7 @@ import {
getFrameToColorBucket,
} from '../store/getters'
import {FlamechartID} from '../store/flamechart-view-state'
import {FlamechartWrapper} from './flamechart-wrapper'
import {FlamechartWrapper, useDummySearchProps} from './flamechart-wrapper'
import {useAppSelector} from '../store'
import {h} from 'preact'
import {memo} from 'preact/compat'
@@ -81,6 +81,11 @@ export const CalleeFlamegraphView = memo((ownProps: FlamechartViewContainerProps
// This overrides the setSelectedNode specified in useFlamechartSettesr
setSelectedNode={noop}
{...callerCallee.calleeFlamegraph}
/*
* TODO(jlfwong): When implementing search for the sandwich views,
* change these flags
* */
{...useDummySearchProps()}
/>
)
})
+94 -25
View File
@@ -1,13 +1,20 @@
import {Rect, AffineTransform, Vec2, clamp} from '../lib/math'
import {CallTreeNode} from '../lib/profile'
import {CallTreeNode, Frame} from '../lib/profile'
import {Flamechart, FlamechartFrame} from '../lib/flamechart'
import {CanvasContext} from '../gl/canvas-context'
import {FlamechartRenderer} from '../gl/flamechart-renderer'
import {Sizes, FontSize, Colors, FontFamily, commonStyle} from './style'
import {cachedMeasureTextWidth, ELLIPSIS, trimTextMid} from '../lib/text-utils'
import {
cachedMeasureTextWidth,
ELLIPSIS,
trimTextMid,
remapRangesToTrimmedText,
} from '../lib/text-utils'
import {style} from './flamechart-style'
import {h, Component} from 'preact'
import {css} from 'aphrodite'
import {memoizeByReference} from '../lib/utils'
import {FuzzyMatch, fuzzyMatchStrings} from '../lib/fuzzy-find'
interface FlamechartFrameLabel {
configSpaceBounds: Rect
@@ -47,7 +54,10 @@ export interface FlamechartPanZoomViewProps {
setConfigSpaceViewportRect: (rect: Rect) => void
logicalSpaceViewportSize: Vec2
setLogicalSpaceViewportBounds: (size: Vec2) => void
setLogicalSpaceViewportSize: (size: Vec2) => void
searchIsActive: boolean
searchQuery: string
}
export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps, {}> {
@@ -183,7 +193,6 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${FontFamily.MONOSPACE}`
ctx.textBaseline = 'alphabetic'
ctx.fillStyle = Colors.DARK_GRAY
const minWidthToRender = cachedMeasureTextWidth(ctx, 'M' + ELLIPSIS + 'M')
const minConfigSpaceWidthToRender = (
@@ -192,6 +201,15 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
const LABEL_PADDING_PX = 5 * window.devicePixelRatio
const memoizedFuzzyMatch = memoizeByReference((frame: Frame): FuzzyMatch | null => {
return fuzzyMatchStrings(frame.name, this.props.searchQuery)
})
const frameMatchesSearchQuery = (frame: Frame): FuzzyMatch | null => {
if (!this.props.searchIsActive) return null
if (this.props.searchQuery.length === 0) return null
return memoizedFuzzyMatch(frame)
}
const renderFrameLabelAndChildren = (frame: FlamechartFrame, depth = 0) => {
const width = frame.end - frame.start
const y = this.props.renderInverted ? this.configSpaceSize().y - 1 - depth : depth
@@ -226,16 +244,52 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
}
if (physicalLabelBounds.width() > minWidthToRender) {
const match = frameMatchesSearchQuery(frame.node.frame)
const trimmedText = trimTextMid(
ctx,
frame.node.frame.name,
physicalLabelBounds.width() - 2 * LABEL_PADDING_PX,
)
if (match) {
const rangesToHighlightInTrimmedText = remapRangesToTrimmedText(
trimmedText,
match.matchedRanges,
)
// Once we have the character ranges to highlight, we need to
// actually do the highlighting.
let lastEndIndex = 0
let left = physicalLabelBounds.left() + LABEL_PADDING_PX
ctx.fillStyle = Colors.YELLOW
ctx.beginPath()
const padding = (physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2 - 2
for (let [startIndex, endIndex] of rangesToHighlightInTrimmedText) {
left += ctx.measureText(trimmedText.trimmedString.substring(lastEndIndex, startIndex))
.width
const highlightWidth = ctx.measureText(
trimmedText.trimmedString.substring(startIndex, endIndex),
).width
ctx.rect(
left,
physicalLabelBounds.top() + padding,
highlightWidth,
physicalViewSpaceFrameHeight - 2 * padding,
)
left += highlightWidth
lastEndIndex = endIndex
}
ctx.fill()
}
// Note that this is specifying the position of the starting text
// baseline.
ctx.fillStyle = Colors.DARK_GRAY
ctx.fillText(
trimmedText,
trimmedText.trimmedString,
physicalLabelBounds.left() + LABEL_PADDING_PX,
Math.round(
physicalLabelBounds.bottom() -
@@ -259,8 +313,9 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
const minConfigSpaceWidthToRenderOutline = (
configToPhysical.inverseTransformVector(new Vec2(1, 0)) || new Vec2(0, 0)
).x
const renderIndirectlySelectedFrameOutlines = (frame: FlamechartFrame, depth = 0) => {
if (!this.props.selectedNode) return
const renderSpecialFrameOutlines = (frame: FlamechartFrame, depth = 0) => {
if (!this.props.selectedNode && !this.props.searchIsActive) return
const width = frame.end - frame.start
const y = this.props.renderInverted ? this.configSpaceSize().y - 1 - depth : depth
const configSpaceBounds = new Rect(new Vec2(frame.start, y), new Vec2(width, 1))
@@ -271,25 +326,30 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
if (configSpaceBounds.top() > this.props.configSpaceViewportRect.bottom()) return
if (configSpaceBounds.hasIntersectionWith(this.props.configSpaceViewportRect)) {
const physicalRectBounds = configToPhysical.transformRect(configSpaceBounds)
let outlineColor: string | null = null
if (frame.node.frame === this.props.selectedNode.frame) {
if (this.props.selectedNode != null && frame.node.frame === this.props.selectedNode.frame) {
if (frame.node === this.props.selectedNode) {
if (ctx.strokeStyle !== Colors.DARK_BLUE) {
ctx.stroke()
ctx.beginPath()
ctx.strokeStyle = Colors.DARK_BLUE
}
} else {
if (ctx.strokeStyle !== Colors.PALE_DARK_BLUE) {
ctx.stroke()
ctx.beginPath()
ctx.strokeStyle = Colors.PALE_DARK_BLUE
}
outlineColor = Colors.DARK_BLUE
} else if (ctx.strokeStyle !== Colors.PALE_DARK_BLUE) {
outlineColor = Colors.PALE_DARK_BLUE
}
} else {
if (frameMatchesSearchQuery(frame.node.frame)) {
outlineColor = Colors.YELLOW
}
}
// Identify the flamechart frames with a function that matches the
// selected flamechart frame.
if (outlineColor != null) {
if (ctx.strokeStyle !== outlineColor) {
// If the outline color changed, stroke the existing path
// constructed by previous ctx.rect calls, then update the stroke
// style before drawing the next one.
ctx.stroke()
ctx.beginPath()
ctx.strokeStyle = outlineColor
}
const physicalRectBounds = configToPhysical.transformRect(configSpaceBounds)
ctx.rect(
Math.round(physicalRectBounds.left() + 1 + frameOutlineWidth / 2),
Math.round(physicalRectBounds.top() + 1 + frameOutlineWidth / 2),
@@ -300,13 +360,13 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
}
for (let child of frame.children) {
renderIndirectlySelectedFrameOutlines(child, depth + 1)
renderSpecialFrameOutlines(child, depth + 1)
}
}
ctx.beginPath()
for (let frame of this.props.flamechart.getLayers()[0] || []) {
renderIndirectlySelectedFrameOutlines(frame)
renderSpecialFrameOutlines(frame)
}
ctx.stroke()
@@ -396,7 +456,11 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
),
)
}
this.props.setLogicalSpaceViewportBounds(new Vec2(width, height))
const newSize = new Vec2(width, height)
if (!newSize.equals(logicalSpaceViewportSize)) {
this.props.setLogicalSpaceViewportSize(newSize)
}
}
onWindowResize = () => {
@@ -686,6 +750,11 @@ export class FlamechartPanZoomView extends Component<FlamechartPanZoomViewProps,
if (this.props.flamechart !== nextProps.flamechart) {
this.hoveredLabel = null
this.renderCanvas()
} else if (
this.props.searchQuery !== nextProps.searchQuery ||
this.props.searchIsActive !== nextProps.searchIsActive
) {
this.renderCanvas()
} else if (this.props.selectedNode !== nextProps.selectedNode) {
this.renderCanvas()
} else if (this.props.configSpaceViewportRect !== nextProps.configSpaceViewportRect) {
+19
View File
@@ -18,6 +18,8 @@ import {ActiveProfileState} from './application'
import {Vec2, Rect} from '../lib/math'
import {actions} from '../store/actions'
import {memo} from 'preact/compat'
import {useAppSelector} from '../store'
import {SearchViewProps} from './search-view'
interface FlamechartSetters {
setLogicalSpaceViewportSize: (logicalSpaceViewportSize: Vec2) => void
@@ -64,9 +66,24 @@ export type FlamechartViewProps = {
flamechartRenderer: FlamechartRenderer
renderInverted: boolean
getCSSColorForFrame: (frame: Frame) => string
searchIsActive: boolean
searchQuery: string
setSearchQuery: (query: string) => void
setSearchIsActive: (active: boolean) => void
} & FlamechartSetters &
FlamechartViewState
const {setSearchQuery, setSearchIsActive} = actions
function useSearchViewProps(): SearchViewProps {
return {
searchIsActive: useAppSelector(state => state.searchIsActive, []),
setSearchQuery: useActionCreator(setSearchQuery, []),
searchQuery: useAppSelector(state => state.searchQuery, []),
setSearchIsActive: useActionCreator(setSearchIsActive, []),
}
}
export const getChronoViewFlamechart = memoizeByShallowEquality(
({
profile,
@@ -134,6 +151,7 @@ export const ChronoFlamechartView = memo((props: FlamechartViewContainerProps) =
canvasContext={canvasContext}
getCSSColorForFrame={getCSSColorForFrame}
{...useFlamechartSetters(FlamechartID.CHRONO, index)}
{...useSearchViewProps()}
{...chronoViewState}
/>
)
@@ -185,6 +203,7 @@ export const LeftHeavyFlamechartView = memo((ownProps: FlamechartViewContainerPr
canvasContext={canvasContext}
getCSSColorForFrame={getCSSColorForFrame}
{...useFlamechartSetters(FlamechartID.LEFT_HEAVY, index)}
{...useSearchViewProps()}
{...leftHeavyViewState}
/>
)
+10 -1
View File
@@ -14,6 +14,7 @@ import {FlamechartPanZoomView} from './flamechart-pan-zoom-view'
import {Hovertip} from './hovertip'
import {FlamechartViewProps} from './flamechart-view-container'
import {StatelessComponent} from '../lib/typed-redux'
import {SearchView} from './search-view'
export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
private configSpaceSize() {
@@ -113,7 +114,15 @@ export class FlamechartView extends StatelessComponent<FlamechartViewProps> {
configSpaceViewportRect={this.props.configSpaceViewportRect}
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect}
logicalSpaceViewportSize={this.props.logicalSpaceViewportSize}
setLogicalSpaceViewportBounds={this.setLogicalSpaceViewportSize}
setLogicalSpaceViewportSize={this.setLogicalSpaceViewportSize}
searchQuery={this.props.searchQuery}
searchIsActive={this.props.searchIsActive}
/>
<SearchView
searchQuery={this.props.searchQuery}
searchIsActive={this.props.searchIsActive}
setSearchQuery={this.props.setSearchQuery}
setSearchIsActive={this.props.setSearchIsActive}
/>
{this.renderTooltip()}
{this.props.selectedNode && (
+14 -1
View File
@@ -8,6 +8,17 @@ import {noop, formatPercent} from '../lib/utils'
import {Hovertip} from './hovertip'
import {FlamechartViewProps} from './flamechart-view-container'
import {StatelessComponent} from '../lib/typed-redux'
import {useCallback} from 'preact/hooks'
import {SearchViewProps} from './search-view'
export function useDummySearchProps(): SearchViewProps {
return {
searchIsActive: false,
searchQuery: '',
setSearchQuery: useCallback((q: string) => {}, []),
setSearchIsActive: useCallback((v: boolean) => {}, []),
}
}
export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
private clampViewportToFlamegraph(viewportRect: Rect) {
@@ -83,7 +94,9 @@ export class FlamechartWrapper extends StatelessComponent<FlamechartViewProps> {
canvasContext={this.props.canvasContext}
renderInverted={this.props.renderInverted}
logicalSpaceViewportSize={this.props.logicalSpaceViewportSize}
setLogicalSpaceViewportBounds={this.setLogicalSpaceViewportSize}
setLogicalSpaceViewportSize={this.setLogicalSpaceViewportSize}
searchIsActive={this.props.searchIsActive}
searchQuery={this.props.searchQuery}
/>
{this.renderTooltip()}
</div>
@@ -10,12 +10,11 @@ import {
getCanvasContext,
createGetColorBucketForFrame,
createGetCSSColorForFrame,
getProfileWithRecursionFlattened,
getFrameToColorBucket,
} from '../store/getters'
import {FlamechartID} from '../store/flamechart-view-state'
import {useAppSelector} from '../store'
import {FlamechartWrapper} from './flamechart-wrapper'
import {FlamechartWrapper, useDummySearchProps} from './flamechart-wrapper'
import {h} from 'preact'
import {memo} from 'preact/compat'
@@ -65,8 +64,6 @@ export const InvertedCallerFlamegraphView = memo((ownProps: FlamechartViewContai
if (!callerCallee) throw new Error('callerCallee missing')
const {selectedFrame} = callerCallee
profile = flattenRecursion ? getProfileWithRecursionFlattened(profile) : profile
const frameToColorBucket = getFrameToColorBucket(profile)
const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket)
const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket)
@@ -93,6 +90,11 @@ export const InvertedCallerFlamegraphView = memo((ownProps: FlamechartViewContai
// This overrides the setSelectedNode specified in useFlamechartSettesr
setSelectedNode={noop}
{...callerCallee.invertedCallerFlamegraph}
/*
* TODO(jlfwong): When implementing search for the sandwich views,
* change these flags
* */
{...useDummySearchProps()}
/>
)
})
+1
View File
@@ -357,6 +357,7 @@ const style = StyleSheet.create({
scrollView: {
overflowY: 'auto',
overflowX: 'hidden',
flexGrow: 1,
},
tableView: {
width: '100%',
-1
View File
@@ -50,7 +50,6 @@ export const ScrollableListView = ({
requestAnimationFrame(() => {
setViewportSize(viewport.getBoundingClientRect()[widthOrHeight])
if (initialScroll.current != null) {
console.log('executing initial scroll to ', initialScroll.current)
viewport.scrollTo({[leftOrTop]: initialScroll.current})
initialScroll.current = null
}
+23 -13
View File
@@ -8,11 +8,11 @@ function stopPropagation(ev: Event) {
ev.stopPropagation()
}
interface SearchViewProps {
export interface SearchViewProps {
searchQuery: string
searchIsActive: boolean
setSearchQuery: (query: string | null) => void
setSearchQuery: (query: string) => void
setSearchIsActive: (active: boolean) => void
}
@@ -36,6 +36,18 @@ export const SearchView = memo(
if (ev.key === 'Escape') {
setSearchIsActive(false)
}
if (ev.key == 'f' && (ev.metaKey || ev.ctrlKey)) {
if (inputRef.current) {
// If the input is already focused, select all
inputRef.current.select()
}
// It seems like when an input is focused, the browser find menu pops
// up without this line. It seems like it's not sufficient to only
// preventDefault in the window keydown handler.
ev.preventDefault()
}
},
[setSearchIsActive],
)
@@ -48,10 +60,17 @@ export const SearchView = memo(
ev.preventDefault()
if (inputRef.current) {
// If the search box is already open, then re-select it.
// If the search box is already open, then re-select it immediately.
inputRef.current.select()
} else {
// Otherwise, focus the search, then focus the input on the next
// frame, when the search box should have mounted.
setSearchIsActive(true)
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.select()
}
})
}
}
}
@@ -62,15 +81,6 @@ export const SearchView = memo(
}
}, [setSearchIsActive])
const focusInput = useCallback((node: HTMLInputElement | null) => {
if (node) {
requestAnimationFrame(() => {
node.select()
})
}
inputRef.current = node
}, [])
const close = useCallback(() => setSearchIsActive(false), [setSearchIsActive])
if (!searchIsActive) return null
@@ -85,7 +95,7 @@ export const SearchView = memo(
onKeyDown={onKeyDown}
onKeyUp={stopPropagation}
onKeyPress={stopPropagation}
ref={focusInput}
ref={inputRef}
/>
<svg
+1
View File
@@ -22,6 +22,7 @@ export enum Colors {
PALE_DARK_BLUE = '#8EB7ED',
GREEN = '#6FCF97',
TRANSPARENT_GREEN = 'rgba(111, 207, 151, 0.2)',
YELLOW = '#FEDC62',
}
export enum Sizes {