Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3b35d7b0f | ||
|
|
dfaefe54fd | ||
|
|
7514f4c0c9 |
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "speedscope",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"description": "",
|
||||
"repository": "jlfwong/speedscope",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -357,6 +357,7 @@ const style = StyleSheet.create({
|
||||
scrollView: {
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
flexGrow: 1,
|
||||
},
|
||||
tableView: {
|
||||
width: '100%',
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user