mirror of
https://github.com/jlfwong/speedscope.git
synced 2024-12-26 20:04:37 +03:00
Basic horizontal axis labelling
This commit is contained in:
parent
cf36b8e630
commit
2188025165
@ -60,7 +60,8 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
const sortedFlamechart = new Flamechart({
|
||||
getTotalWeight: profile.getTotalNonIdleWeight.bind(profile),
|
||||
forEachCall: profile.forEachCallGrouped.bind(profile),
|
||||
forEachFrame: profile.forEachFrame.bind(profile)
|
||||
formatValue: profile.formatValue.bind(profile),
|
||||
forEachFrame: profile.forEachFrame.bind(profile),
|
||||
})
|
||||
this.setState({ profile, flamechart, sortedFlamechart }, () => {
|
||||
console.timeEnd('import')
|
||||
|
@ -5,8 +5,9 @@ import { css } from 'aphrodite'
|
||||
import { Flamechart } from './flamechart'
|
||||
import { Rect, Vec2, AffineTransform } from './math'
|
||||
import { rectangleBatchRenderer, RectangleBatchRendererProps } from "./rectangle-batch-renderer"
|
||||
import { atMostOnceAFrame } from "./utils";
|
||||
import { style } from "./flamechart-style";
|
||||
import { atMostOnceAFrame, cachedMeasureTextWidth } from "./utils";
|
||||
import { style, Sizes } from "./flamechart-style";
|
||||
import { FontFamily, FontSize, Colors } from "./style"
|
||||
|
||||
const DEVICE_PIXEL_RATIO = window.devicePixelRatio
|
||||
|
||||
@ -18,12 +19,15 @@ interface FlamechartMinimapViewProps {
|
||||
|
||||
export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps, {}> {
|
||||
renderer: ReglCommand<RectangleBatchRendererProps> | null = null
|
||||
overlayRenderer: ReglCommand<OverlayRectangleRendererProps> | null = null
|
||||
viewportRectRenderer: ReglCommand<OverlayRectangleRendererProps> | null = null
|
||||
|
||||
ctx: WebGLRenderingContext | null = null
|
||||
regl: regl.ReglCommandConstructor | null = null
|
||||
canvas: HTMLCanvasElement | null = null
|
||||
|
||||
overlayCanvas: HTMLCanvasElement | null = null
|
||||
overlayCtx: CanvasRenderingContext2D | null = null
|
||||
|
||||
private physicalViewSize() {
|
||||
return new Vec2(
|
||||
this.canvas ? this.canvas.width : 0,
|
||||
@ -39,9 +43,11 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
}
|
||||
|
||||
private configSpaceToPhysicalViewSpace() {
|
||||
const minimapOrigin = new Vec2(0, Sizes.FRAME_HEIGHT * DEVICE_PIXEL_RATIO)
|
||||
|
||||
return AffineTransform.betweenRects(
|
||||
new Rect(new Vec2(0, 0), this.configSpaceSize()),
|
||||
new Rect(new Vec2(0, 0), this.physicalViewSize())
|
||||
new Rect(minimapOrigin, this.physicalViewSize().minus(minimapOrigin))
|
||||
)
|
||||
}
|
||||
|
||||
@ -55,7 +61,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
}
|
||||
|
||||
private renderRects() {
|
||||
if (!this.renderer || !this.canvas || !this.overlayRenderer) return
|
||||
if (!this.renderer || !this.canvas || !this.viewportRectRenderer) return
|
||||
this.resizeCanvasIfNeeded()
|
||||
|
||||
const configSpaceToNDC = this.physicalViewSpaceToNDC().times(this.configSpaceToPhysicalViewSpace())
|
||||
@ -65,13 +71,66 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
physicalSize: this.physicalViewSize()
|
||||
})
|
||||
|
||||
this.overlayRenderer({
|
||||
this.viewportRectRenderer({
|
||||
configSpaceViewportRect: this.props.configSpaceViewportRect,
|
||||
configSpaceToPhysicalViewSpace: this.configSpaceToPhysicalViewSpace(),
|
||||
physicalSize: this.physicalViewSize()
|
||||
})
|
||||
}
|
||||
|
||||
private renderOverlays() {
|
||||
const ctx = this.overlayCtx
|
||||
if (!ctx) return
|
||||
const physicalViewSize = this.physicalViewSize()
|
||||
ctx.clearRect(0, 0, physicalViewSize.x, physicalViewSize.y)
|
||||
|
||||
this.resizeOverlayCanvasIfNeeded()
|
||||
|
||||
const configToPhysical = this.configSpaceToPhysicalViewSpace()
|
||||
|
||||
const left = 0
|
||||
const right = this.configSpaceSize().x
|
||||
|
||||
// We want about 10 gridlines to be visible, and want the unit to be
|
||||
// 1eN, 2eN, or 5eN for some N
|
||||
|
||||
// Ideally, we want an interval every 100 logical screen pixels
|
||||
const logicalToConfig = (this.configSpaceToPhysicalViewSpace().inverted() || new AffineTransform()).times(this.logicalToPhysicalViewSpace())
|
||||
const targetInterval = logicalToConfig.transformVector(new Vec2(200, 1)).x
|
||||
|
||||
const physicalViewSpaceFrameHeight = Sizes.FRAME_HEIGHT * DEVICE_PIXEL_RATIO
|
||||
const physicalViewSpaceFontSize = FontSize.LABEL * DEVICE_PIXEL_RATIO
|
||||
const LABEL_PADDING_PX = (physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2
|
||||
|
||||
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${FontFamily.MONOSPACE}`
|
||||
ctx.textBaseline = 'top'
|
||||
|
||||
const minInterval = Math.pow(10, Math.floor(Math.log10(targetInterval)))
|
||||
let interval = minInterval
|
||||
|
||||
if (targetInterval / interval > 5) {
|
||||
interval *= 5
|
||||
} else if (targetInterval / interval > 2) {
|
||||
interval *= 2
|
||||
}
|
||||
|
||||
{
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
|
||||
ctx.fillRect(0, 0, physicalViewSize.x, physicalViewSpaceFrameHeight)
|
||||
|
||||
ctx.fillStyle = Colors.GRAY
|
||||
for (let x = Math.ceil(left / interval) * interval; x < right; x += interval) {
|
||||
// TODO(jlfwong): Ensure that labels do not overlap
|
||||
const pos = Math.round(configToPhysical.transformPosition(new Vec2(x, 0)).x)
|
||||
const labelText = this.props.flamechart.formatValue(x)
|
||||
const textWidth = Math.ceil(cachedMeasureTextWidth(ctx, labelText))
|
||||
|
||||
ctx.fillText(labelText, pos - textWidth - LABEL_PADDING_PX, LABEL_PADDING_PX)
|
||||
ctx.fillRect(pos, 0, 1, physicalViewSize.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: FlamechartMinimapViewProps) {
|
||||
if (this.props.flamechart !== nextProps.flamechart) {
|
||||
this.renderer = null
|
||||
@ -101,6 +160,30 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
this.ctx.viewport(0, 0, width, height)
|
||||
}
|
||||
|
||||
private resizeOverlayCanvasIfNeeded() {
|
||||
if (!this.overlayCanvas) return
|
||||
let {width, height} = this.overlayCanvas.getBoundingClientRect()
|
||||
{/*
|
||||
We render text at a higher resolution then scale down to
|
||||
ensure we're rendering at 1:1 device pixel ratio.
|
||||
This ensures our text is rendered crisply.
|
||||
*/}
|
||||
width = Math.floor(width)
|
||||
height = Math.floor(height)
|
||||
|
||||
// Still initializing: don't resize yet
|
||||
if (width === 0 || height === 0) return
|
||||
|
||||
const scaledWidth = width * DEVICE_PIXEL_RATIO
|
||||
const scaledHeight = height * DEVICE_PIXEL_RATIO
|
||||
|
||||
if (scaledWidth === this.overlayCanvas.width &&
|
||||
scaledHeight === this.overlayCanvas.height) return
|
||||
|
||||
this.overlayCanvas.width = scaledWidth
|
||||
this.overlayCanvas.height = scaledHeight
|
||||
}
|
||||
|
||||
private renderCanvas = atMostOnceAFrame(() => {
|
||||
if (!this.canvas || this.canvas.getBoundingClientRect().width < 2) {
|
||||
// If the canvas is still tiny, it means browser layout hasn't had
|
||||
@ -115,6 +198,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
depth: 1
|
||||
})
|
||||
this.renderRects()
|
||||
this.renderOverlays()
|
||||
}
|
||||
})
|
||||
|
||||
@ -236,7 +320,18 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
}
|
||||
|
||||
this.renderer = rectangleBatchRenderer(this.regl, configSpaceRects, colors, 0)
|
||||
this.overlayRenderer = overlayRectangleRenderer(this.regl);
|
||||
this.viewportRectRenderer = viewportRectangleRenderer(this.regl);
|
||||
}
|
||||
|
||||
private overlayCanvasRef = (element?: Element) => {
|
||||
if (element) {
|
||||
this.overlayCanvas = element as HTMLCanvasElement
|
||||
this.overlayCtx = this.overlayCanvas.getContext('2d')
|
||||
this.renderCanvas()
|
||||
} else {
|
||||
this.overlayCanvas = null
|
||||
this.overlayCtx = null
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -258,6 +353,10 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
width={1} height={1}
|
||||
ref={this.canvasRef}
|
||||
className={css(style.fill)} />
|
||||
<canvas
|
||||
width={1} height={1}
|
||||
ref={this.overlayCanvasRef}
|
||||
className={css(style.fill)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -269,7 +368,7 @@ export interface OverlayRectangleRendererProps {
|
||||
physicalSize: Vec2
|
||||
}
|
||||
|
||||
export const overlayRectangleRenderer = (regl: regl.ReglCommandConstructor) => {
|
||||
export const viewportRectangleRenderer = (regl: regl.ReglCommandConstructor) => {
|
||||
return regl<OverlayRectangleRendererProps>({
|
||||
vert: `
|
||||
attribute vec2 position;
|
||||
|
@ -9,12 +9,12 @@ import * as regl from 'regl'
|
||||
import { vec3, ReglCommand, ReglCommandConstructor } from 'regl'
|
||||
|
||||
import { Rect, Vec2, AffineTransform, clamp } from './math'
|
||||
import { atMostOnceAFrame } from "./utils";
|
||||
import { atMostOnceAFrame, cachedMeasureTextWidth } from "./utils";
|
||||
import { rectangleBatchRenderer, RectangleBatchRendererProps } from "./rectangle-batch-renderer"
|
||||
import { FlamechartMinimapView } from "./flamechart-minimap-view"
|
||||
|
||||
import { style, Sizes } from './flamechart-style'
|
||||
import { FontSize, FontFamily } from './style'
|
||||
import { FontSize, FontFamily, Colors } from './style'
|
||||
|
||||
interface FlamechartFrameLabel {
|
||||
configSpaceBounds: Rect
|
||||
@ -41,14 +41,6 @@ function buildTrimmedText(text: string, length: number) {
|
||||
return prefix + ELLIPSIS + suffix
|
||||
}
|
||||
|
||||
const measureTextCache = new Map<string, number>()
|
||||
function cachedMeasureTextWidth(ctx: CanvasRenderingContext2D, text: string): number {
|
||||
if (!measureTextCache.has(text)) {
|
||||
measureTextCache.set(text, ctx.measureText(text).width)
|
||||
}
|
||||
return measureTextCache.get(text)!
|
||||
}
|
||||
|
||||
function trimTextMid(ctx: CanvasRenderingContext2D, text: string, maxWidth: number) {
|
||||
if (cachedMeasureTextWidth(ctx, text) <= maxWidth) return text
|
||||
const [lo,] = binarySearch(0, text.length, (n) => {
|
||||
@ -117,7 +109,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
const layer = layers[i]
|
||||
for (let flamechartFrame of layer) {
|
||||
const configSpaceBounds = new Rect(
|
||||
new Vec2(flamechartFrame.start, i),
|
||||
new Vec2(flamechartFrame.start, i+1),
|
||||
new Vec2(flamechartFrame.end - flamechartFrame.start, 1)
|
||||
)
|
||||
configSpaceRects.push(configSpaceBounds)
|
||||
@ -214,10 +206,9 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
|
||||
this.overlayCanvas.width = scaledWidth
|
||||
this.overlayCanvas.height = scaledHeight
|
||||
|
||||
}
|
||||
|
||||
private renderLabels() {
|
||||
private renderOverlays() {
|
||||
const ctx = this.overlayCtx
|
||||
if (!ctx) return
|
||||
this.resizeOverlayCanvasIfNeeded()
|
||||
@ -245,12 +236,13 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
}
|
||||
|
||||
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${FontFamily.MONOSPACE}`
|
||||
ctx.fillStyle = 'rgba(80, 70, 70, 1)'
|
||||
ctx.fillStyle = Colors.GRAY
|
||||
ctx.textBaseline = 'top'
|
||||
|
||||
const minWidthToRender = cachedMeasureTextWidth(ctx, 'M' + ELLIPSIS + 'M')
|
||||
const LABEL_PADDING_PX = (physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2
|
||||
|
||||
for (let label of this.labels) {
|
||||
const LABEL_PADDING_PX = 2 * DEVICE_PIXEL_RATIO
|
||||
let physicalLabelBounds = configToPhysical.transformRect(label.configSpaceBounds)
|
||||
|
||||
physicalLabelBounds = physicalLabelBounds
|
||||
@ -275,6 +267,41 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
const trimmedText = trimTextMid(ctx, label.node.frame.name, physicalLabelBounds.width())
|
||||
ctx.fillText(trimmedText, physicalLabelBounds.left(), physicalLabelBounds.top())
|
||||
}
|
||||
|
||||
const left = this.props.configSpaceViewportRect.left()
|
||||
const right = this.props.configSpaceViewportRect.right()
|
||||
|
||||
// We want about 10 gridlines to be visible, and want the unit to be
|
||||
// 1eN, 2eN, or 5eN for some N
|
||||
|
||||
// Ideally, we want an interval every 100 logical screen pixels
|
||||
const logicalToConfig = (this.configSpaceToPhysicalViewSpace().inverted() || new AffineTransform()).times(this.logicalToPhysicalViewSpace())
|
||||
const targetInterval = logicalToConfig.transformVector(new Vec2(200, 1)).x
|
||||
|
||||
const minInterval = Math.pow(10, Math.floor(Math.log10(targetInterval)))
|
||||
let interval = minInterval
|
||||
|
||||
if (targetInterval / interval > 5) {
|
||||
interval *= 5
|
||||
} else if (targetInterval / interval > 2) {
|
||||
interval *= 2
|
||||
}
|
||||
|
||||
{
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
|
||||
ctx.fillRect(0, 0, physicalViewSize.x, physicalViewSpaceFrameHeight)
|
||||
|
||||
ctx.fillStyle = Colors.GRAY
|
||||
for (let x = Math.ceil(left / interval) * interval; x < right; x += interval) {
|
||||
// TODO(jlfwong): Ensure that labels do not overlap
|
||||
const pos = Math.round(configToPhysical.transformPosition(new Vec2(x, 0)).x)
|
||||
const labelText = this.props.flamechart.formatValue(x)
|
||||
const textWidth = cachedMeasureTextWidth(ctx, labelText)
|
||||
|
||||
ctx.fillText(labelText, pos - textWidth - 5, 2)
|
||||
ctx.fillRect(pos, 0, 1, physicalViewSize.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private resizeCanvasIfNeeded(windowResized = false) {
|
||||
@ -372,7 +399,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
} else {
|
||||
if (!this.renderer) this.preprocess(this.props.flamechart)
|
||||
this.renderRects()
|
||||
this.renderLabels()
|
||||
this.renderOverlays()
|
||||
}
|
||||
})
|
||||
|
||||
@ -581,9 +608,9 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
});
|
||||
}
|
||||
|
||||
formatTime(timeInNs: number) {
|
||||
const totalTimeNs = this.props.flamechart.getTotalWeight()
|
||||
return `${(timeInNs / 1000).toFixed(2)}ms (${(100 * timeInNs/totalTimeNs).toFixed()}%)`
|
||||
formatValue(weight: number) {
|
||||
const totalWeight = this.props.flamechart.getTotalWeight()
|
||||
return `${this.props.flamechart.formatValue(weight)} (${(100 * weight/totalWeight).toFixed()}%)`
|
||||
}
|
||||
|
||||
renderTooltip() {
|
||||
@ -617,7 +644,7 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
return (
|
||||
<div className={css(style.hoverTip)} style={positionStyle}>
|
||||
<div className={css(style.hoverTipRow)}>
|
||||
<span className={css(style.hoverCount)}>{this.formatTime(hoveredNode.getTotalTime())}</span>{' '}
|
||||
<span className={css(style.hoverCount)}>{this.formatValue(hoveredNode.getTotalWeight())}</span>{' '}
|
||||
{hoveredNode.frame.name}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,6 +15,8 @@ type StackLayer = FlamechartFrame[]
|
||||
interface FlamechartDataSource {
|
||||
getTotalWeight(): number
|
||||
|
||||
formatValue(v: number): string
|
||||
|
||||
forEachCall(
|
||||
openFrame: (node: CallTreeNode, value: number) => void,
|
||||
closeFrame: (value: number) => void
|
||||
@ -35,6 +37,7 @@ export class Flamechart {
|
||||
getLayers() { return this.layers }
|
||||
getFrameColors() { return this.frameColors }
|
||||
getMinFrameWidth() { return this.minFrameWidth }
|
||||
formatValue(v: number) { return this.source.formatValue(v) }
|
||||
|
||||
// TODO(jlfwong): Move this a color generation file
|
||||
private selectFrameColors(source: FlamechartDataSource) {
|
||||
@ -132,6 +135,7 @@ export class Flamechart {
|
||||
console.assert(stack.length > 0)
|
||||
const stackTop = stack.pop()!
|
||||
stackTop.end = value
|
||||
if (stackTop.end - stackTop.start === 0) return
|
||||
const layerIndex = stack.length
|
||||
while (this.layers.length <= layerIndex) this.layers.push([])
|
||||
this.layers[layerIndex].push(stackTop)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Profile, FrameInfo} from '../profile'
|
||||
import {Profile, TimeFormatter, FrameInfo} from '../profile'
|
||||
|
||||
interface TimelineEvent {
|
||||
pid: number,
|
||||
@ -106,5 +106,7 @@ export function importFromChrome(events: TimelineEvent[]) {
|
||||
|
||||
profile.appendSample(stack, timeDelta)
|
||||
}
|
||||
|
||||
profile.setValueFormatter(new TimeFormatter('us'))
|
||||
return profile
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
// https://github.com/tmm1/stackprof
|
||||
|
||||
import {Profile, FrameInfo} from '../profile'
|
||||
import {Profile, TimeFormatter, FrameInfo} from '../profile'
|
||||
|
||||
interface StackprofFrame {
|
||||
name: string
|
||||
@ -41,5 +41,6 @@ export function importFromStackprof(stackprofProfile: StackprofProfile): Profile
|
||||
profile.appendSample(stack, sampleDuration)
|
||||
}
|
||||
|
||||
profile.setValueFormatter(new TimeFormatter('us'))
|
||||
return profile
|
||||
}
|
47
profile.ts
47
profile.ts
@ -21,8 +21,8 @@ export interface FrameInfo {
|
||||
export class HasWeights {
|
||||
private selfWeight = 0
|
||||
private totalWeight = 0
|
||||
getSelfTime() { return this.selfWeight }
|
||||
getTotalTime() { return this.totalWeight }
|
||||
getSelfWeight() { return this.selfWeight }
|
||||
getTotalWeight() { return this.totalWeight }
|
||||
addToTotalWeight(delta: number) { this.totalWeight += delta }
|
||||
addToSelfWeight(delta: number) { this.selfWeight += delta }
|
||||
}
|
||||
@ -71,6 +71,36 @@ const rootFrame = new Frame({
|
||||
name: '(speedscope root)',
|
||||
})
|
||||
|
||||
export interface ValueFormatter {
|
||||
format(v: number): string
|
||||
}
|
||||
|
||||
export class RawValueFormatter implements ValueFormatter {
|
||||
format(v: number) {
|
||||
return v.toLocaleString()
|
||||
}
|
||||
}
|
||||
|
||||
export class TimeFormatter implements ValueFormatter {
|
||||
private multiplier : number
|
||||
|
||||
constructor(unit: 'ns' | 'us' | 'ms' | 's' = 'ns') {
|
||||
if (unit === 'ns') this.multiplier = 1e-9
|
||||
else if (unit === 'us') this.multiplier = 1e-6
|
||||
else if (unit === 'ms') this.multiplier = 1e-3
|
||||
else this.multiplier = 1
|
||||
}
|
||||
|
||||
format(v: number) {
|
||||
const s = v * this.multiplier
|
||||
|
||||
if (s / 1e0 > 1) return `${s.toFixed(2)}s`
|
||||
if (s / 1e-3 > 1) return `${(s / 1e-3).toFixed(2)}ms`
|
||||
if (s / 1e-6 > 1) return `${(s / 1e-6).toFixed(2)}us`
|
||||
else return `${(s / 1e-9).toFixed(2)}ms`
|
||||
}
|
||||
}
|
||||
|
||||
export class Profile {
|
||||
private totalWeight: number
|
||||
|
||||
@ -83,13 +113,18 @@ export class Profile {
|
||||
private samples: CallTreeNode[] = []
|
||||
private weights: number[] = []
|
||||
|
||||
private valueFormatter: ValueFormatter = new RawValueFormatter()
|
||||
|
||||
constructor(totalWeight: number) {
|
||||
this.totalWeight = totalWeight
|
||||
}
|
||||
|
||||
formatValue(v: number) { return this.valueFormatter.format(v) }
|
||||
setValueFormatter(f: ValueFormatter) { this.valueFormatter = f }
|
||||
|
||||
getTotalWeight() { return this.totalWeight }
|
||||
getTotalNonIdleWeight() {
|
||||
return this.groupedCalltreeRoot.children.reduce((n, c) => n + c.getTotalTime(), 0)
|
||||
return this.groupedCalltreeRoot.children.reduce((n, c) => n + c.getTotalWeight(), 0)
|
||||
}
|
||||
|
||||
forEachCallGrouped(
|
||||
@ -104,15 +139,15 @@ export class Profile {
|
||||
let childTime = 0
|
||||
|
||||
const children = [...node.children]
|
||||
children.sort((a, b) => a.getTotalTime() > b.getTotalTime() ? -1 : 1)
|
||||
children.sort((a, b) => a.getTotalWeight() > b.getTotalWeight() ? -1 : 1)
|
||||
|
||||
children.forEach(function (child) {
|
||||
visit(child, start + childTime)
|
||||
childTime += child.getTotalTime()
|
||||
childTime += child.getTotalWeight()
|
||||
})
|
||||
|
||||
if (node.frame !== rootFrame) {
|
||||
closeFrame(start + node.getTotalTime())
|
||||
closeFrame(start + node.getTotalWeight())
|
||||
}
|
||||
}
|
||||
visit(this.groupedCalltreeRoot, 0)
|
||||
|
3
style.ts
3
style.ts
@ -8,6 +8,7 @@ export enum FontSize {
|
||||
}
|
||||
|
||||
export enum Colors {
|
||||
LIGHT_GRAY = "#C4C4C4",
|
||||
MEDIUM_GRAY = "#BDBDBD",
|
||||
LIGHT_GRAY = "#C4C4C4"
|
||||
GRAY = '#666666'
|
||||
}
|
||||
|
9
utils.ts
9
utils.ts
@ -13,4 +13,13 @@ export function atMostOnceAFrame<F extends Function>(fn: F) {
|
||||
|
||||
export function lastOf<T>(ts: T[]): T | null {
|
||||
return ts[ts.length-1] || null
|
||||
}
|
||||
|
||||
// NOTE: This blindly assumes the same result across contexts.
|
||||
const measureTextCache = new Map<string, number>()
|
||||
export function cachedMeasureTextWidth(ctx: CanvasRenderingContext2D, text: string): number {
|
||||
if (!measureTextCache.has(text)) {
|
||||
measureTextCache.set(text, ctx.measureText(text).width)
|
||||
}
|
||||
return measureTextCache.get(text)!
|
||||
}
|
Loading…
Reference in New Issue
Block a user