mirror of
https://github.com/jlfwong/speedscope.git
synced 2024-11-22 12:53:23 +03:00
Add a hotkey to flatten recursion (#68)
This makes the left-heavy view much more useful since recursive calls are all collapsed together. Press `r` to activate Fixes #37
This commit is contained in:
parent
aaac0ad7e3
commit
d659eb0159
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
}
|
102
application.tsx
102
application.tsx
@ -13,7 +13,7 @@ import {
|
||||
FileSystemDirectoryEntry,
|
||||
} from './import/instruments'
|
||||
|
||||
import {FlamechartRenderer} from './flamechart-renderer'
|
||||
import {FlamechartRenderer, FlamechartRowAtlasKey} from './flamechart-renderer'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
|
||||
import {Profile, Frame} from './profile'
|
||||
@ -24,6 +24,7 @@ import {getHashParams, HashParams} from './hash-params'
|
||||
import {ProfileTableView, SortMethod, SortField, SortDirection} from './profile-table-view'
|
||||
import {triangle} from './utils'
|
||||
import {Color} from './color'
|
||||
import {RowAtlas} from './row-atlas'
|
||||
|
||||
declare function require(x: string): any
|
||||
const exampleProfileURL = require('./sample/profiles/stackcollapse/perf-vertx-stacks-01-collapsed-all.txt')
|
||||
@ -36,6 +37,8 @@ const enum ViewMode {
|
||||
|
||||
interface ApplicationState {
|
||||
profile: Profile | null
|
||||
activeProfile: Profile | null
|
||||
flattenRecursion: boolean
|
||||
|
||||
chronoFlamechart: Flamechart | null
|
||||
chronoFlamechartRenderer: FlamechartRenderer | null
|
||||
@ -255,6 +258,8 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
dragActive: false,
|
||||
error: false,
|
||||
profile: null,
|
||||
activeProfile: null,
|
||||
flattenRecursion: false,
|
||||
|
||||
chronoFlamechart: null,
|
||||
chronoFlamechartRenderer: null,
|
||||
@ -281,11 +286,16 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
rehydrate(serialized: SerializedComponent<ApplicationState>) {
|
||||
super.rehydrate(serialized)
|
||||
const {chronoFlamechart, leftHeavyFlamegraph} = serialized.state
|
||||
if (this.canvasContext && chronoFlamechart && leftHeavyFlamegraph) {
|
||||
if (this.canvasContext && this.rowAtlas && chronoFlamechart && leftHeavyFlamegraph) {
|
||||
this.setState({
|
||||
chronoFlamechartRenderer: new FlamechartRenderer(this.canvasContext, chronoFlamechart),
|
||||
chronoFlamechartRenderer: new FlamechartRenderer(
|
||||
this.canvasContext,
|
||||
this.rowAtlas,
|
||||
chronoFlamechart,
|
||||
),
|
||||
leftHeavyFlamegraphRenderer: new FlamechartRenderer(
|
||||
this.canvasContext,
|
||||
this.rowAtlas,
|
||||
leftHeavyFlamegraph,
|
||||
),
|
||||
})
|
||||
@ -296,7 +306,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
await new Promise(resolve => this.setState({loading: true}, resolve))
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
if (!this.canvasContext) return
|
||||
if (!this.canvasContext || !this.rowAtlas) return
|
||||
|
||||
console.time('import')
|
||||
|
||||
@ -316,13 +326,21 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('PROFILE', profile)
|
||||
|
||||
await profile.demangle()
|
||||
|
||||
const title = this.hashParams.title || profile.getName()
|
||||
profile.setName(title)
|
||||
document.title = `${title} - speedscope`
|
||||
|
||||
await this.setActiveProfile(profile)
|
||||
|
||||
console.timeEnd('import')
|
||||
this.setState({profile})
|
||||
}
|
||||
|
||||
async setActiveProfile(profile: Profile) {
|
||||
if (!this.canvasContext || !this.rowAtlas) return
|
||||
|
||||
document.title = `${profile.getName()} - speedscope`
|
||||
|
||||
const frames: Frame[] = []
|
||||
profile.forEachFrame(f => frames.push(f))
|
||||
@ -333,12 +351,12 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
return key(a) > key(b) ? 1 : -1
|
||||
}
|
||||
frames.sort(compare)
|
||||
const frameToColorBucket = new Map<Frame, number>()
|
||||
const frameToColorBucket = new Map<string | number, number>()
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
frameToColorBucket.set(frames[i], Math.floor(255 * i / frames.length))
|
||||
frameToColorBucket.set(frames[i].key, Math.floor(255 * i / frames.length))
|
||||
}
|
||||
function getColorBucketForFrame(frame: Frame) {
|
||||
return frameToColorBucket.get(frame) || 0
|
||||
return frameToColorBucket.get(frame.key) || 0
|
||||
}
|
||||
|
||||
const chronoFlamechart = new Flamechart({
|
||||
@ -347,7 +365,11 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
formatValue: profile.formatValue.bind(profile),
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const chronoFlamechartRenderer = new FlamechartRenderer(this.canvasContext, chronoFlamechart)
|
||||
const chronoFlamechartRenderer = new FlamechartRenderer(
|
||||
this.canvasContext,
|
||||
this.rowAtlas,
|
||||
chronoFlamechart,
|
||||
)
|
||||
|
||||
const leftHeavyFlamegraph = new Flamechart({
|
||||
getTotalWeight: profile.getTotalNonIdleWeight.bind(profile),
|
||||
@ -357,28 +379,26 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
})
|
||||
const leftHeavyFlamegraphRenderer = new FlamechartRenderer(
|
||||
this.canvasContext,
|
||||
this.rowAtlas,
|
||||
leftHeavyFlamegraph,
|
||||
)
|
||||
|
||||
console.timeEnd('import')
|
||||
await new Promise(resolve => {
|
||||
this.setState(
|
||||
{
|
||||
activeProfile: profile,
|
||||
|
||||
console.time('first setState')
|
||||
this.setState(
|
||||
{
|
||||
profile,
|
||||
chronoFlamechart,
|
||||
chronoFlamechartRenderer,
|
||||
|
||||
chronoFlamechart,
|
||||
chronoFlamechartRenderer,
|
||||
leftHeavyFlamegraph,
|
||||
leftHeavyFlamegraphRenderer,
|
||||
|
||||
leftHeavyFlamegraph,
|
||||
leftHeavyFlamegraphRenderer,
|
||||
|
||||
loading: false,
|
||||
},
|
||||
() => {
|
||||
console.timeEnd('first setState')
|
||||
},
|
||||
)
|
||||
loading: false,
|
||||
},
|
||||
resolve,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
loadFromFile(file: File) {
|
||||
@ -388,8 +408,12 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener('loadend', () => {
|
||||
const profile = importProfile(file.name, reader.result)
|
||||
if (profile) resolve(profile)
|
||||
else reject()
|
||||
if (profile) {
|
||||
if (!profile.getName()) {
|
||||
profile.setName(file.name)
|
||||
}
|
||||
resolve(profile)
|
||||
} else reject()
|
||||
})
|
||||
reader.readAsText(file)
|
||||
}),
|
||||
@ -450,6 +474,16 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
this.setState({
|
||||
viewMode: ViewMode.TABLE_VIEW,
|
||||
})
|
||||
} else if (ev.key === 'r') {
|
||||
const {flattenRecursion, profile} = this.state
|
||||
if (!profile) return
|
||||
if (flattenRecursion) {
|
||||
this.setActiveProfile(profile)
|
||||
this.setState({flattenRecursion: false})
|
||||
} else {
|
||||
this.setActiveProfile(profile.flattenRecursion())
|
||||
this.setState({flattenRecursion: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -586,8 +620,14 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
}
|
||||
|
||||
private canvasContext: CanvasContext | null = null
|
||||
private rowAtlas: RowAtlas<FlamechartRowAtlasKey> | null = null
|
||||
private setCanvasContext = (canvasContext: CanvasContext | null) => {
|
||||
this.canvasContext = canvasContext
|
||||
if (canvasContext) {
|
||||
this.rowAtlas = new RowAtlas(canvasContext)
|
||||
} else {
|
||||
this.rowAtlas = null
|
||||
}
|
||||
}
|
||||
|
||||
getCSSColorForFrame = (frame: Frame): string => {
|
||||
@ -614,7 +654,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
return this.renderLoadingBar()
|
||||
}
|
||||
|
||||
if (!this.state.profile) {
|
||||
if (!this.state.activeProfile) {
|
||||
return this.renderLanding()
|
||||
}
|
||||
|
||||
@ -654,7 +694,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
case ViewMode.TABLE_VIEW: {
|
||||
return (
|
||||
<ProfileTableView
|
||||
profile={this.state.profile}
|
||||
profile={this.state.activeProfile}
|
||||
getCSSColorForFrame={this.getCSSColorForFrame}
|
||||
sortMethod={this.state.tableSortMethod}
|
||||
setSortMethod={this.setTableSortMethod}
|
||||
|
@ -3,100 +3,12 @@ import {Flamechart} from './flamechart'
|
||||
import {RectangleBatch} from './rectangle-batch-renderer'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
import {Vec2, Rect, AffineTransform} from './math'
|
||||
import {LRUCache} from './lru-cache'
|
||||
import {Color} from './color'
|
||||
import {getOrInsert} from './utils'
|
||||
import {KeyedSet} from './utils'
|
||||
import {RowAtlas} from './row-atlas'
|
||||
|
||||
const MAX_BATCH_SIZE = 10000
|
||||
|
||||
class RowAtlas<K> {
|
||||
texture: regl.Texture
|
||||
private framebuffer: regl.Framebuffer
|
||||
private renderToFramebuffer: regl.Command<{}>
|
||||
private rowCache: LRUCache<K, number>
|
||||
private clearLineBatch: RectangleBatch
|
||||
|
||||
constructor(private canvasContext: CanvasContext) {
|
||||
this.texture = canvasContext.gl.texture({
|
||||
width: Math.min(canvasContext.getMaxTextureSize(), 4096),
|
||||
height: Math.min(canvasContext.getMaxTextureSize(), 1024),
|
||||
wrapS: 'clamp',
|
||||
wrapT: 'clamp',
|
||||
})
|
||||
this.framebuffer = canvasContext.gl.framebuffer({color: [this.texture]})
|
||||
this.rowCache = new LRUCache(this.texture.height)
|
||||
this.renderToFramebuffer = canvasContext.gl({
|
||||
framebuffer: this.framebuffer,
|
||||
})
|
||||
this.clearLineBatch = canvasContext.createRectangleBatch()
|
||||
this.clearLineBatch.addRect(Rect.unit, new Color(0, 0, 0, 0))
|
||||
}
|
||||
|
||||
has(key: K) {
|
||||
return this.rowCache.has(key)
|
||||
}
|
||||
getResolution() {
|
||||
return this.texture.width
|
||||
}
|
||||
getCapacity() {
|
||||
return this.texture.height
|
||||
}
|
||||
|
||||
private allocateLine(key: K): number {
|
||||
if (this.rowCache.getSize() < this.rowCache.getCapacity()) {
|
||||
// Not in cache, but cache isn't full
|
||||
const row = this.rowCache.getSize()
|
||||
this.rowCache.insert(key, row)
|
||||
return row
|
||||
} else {
|
||||
// Not in cache, and cache is full. Evict something.
|
||||
const [, row] = this.rowCache.removeLRU()!
|
||||
this.rowCache.insert(key, row)
|
||||
return row
|
||||
}
|
||||
}
|
||||
|
||||
writeToAtlasIfNeeded(keys: K[], render: (textureDstRect: Rect, key: K) => void) {
|
||||
this.renderToFramebuffer((context: regl.Context) => {
|
||||
for (let key of keys) {
|
||||
let row = this.rowCache.get(key)
|
||||
if (row != null) {
|
||||
// Already cached!
|
||||
continue
|
||||
}
|
||||
// Not cached -- we'll have to actually render
|
||||
row = this.allocateLine(key)
|
||||
|
||||
const textureRect = new Rect(new Vec2(0, row), new Vec2(this.texture.width, 1))
|
||||
this.canvasContext.drawRectangleBatch({
|
||||
batch: this.clearLineBatch,
|
||||
configSpaceSrcRect: Rect.unit,
|
||||
physicalSpaceDstRect: textureRect,
|
||||
})
|
||||
render(textureRect, key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
renderViaAtlas(key: K, dstRect: Rect): boolean {
|
||||
let row = this.rowCache.get(key)
|
||||
if (row == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const textureRect = new Rect(new Vec2(0, row), new Vec2(this.texture.width, 1))
|
||||
|
||||
// At this point, we have the row in cache, and we can
|
||||
// paint directly from it into the framebuffer.
|
||||
this.canvasContext.drawTexture({
|
||||
texture: this.texture,
|
||||
srcRect: textureRect,
|
||||
dstRect: dstRect,
|
||||
})
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
interface RangeTreeNode {
|
||||
getBounds(): Rect
|
||||
getRectCount(): number
|
||||
@ -185,21 +97,41 @@ export interface FlamechartRendererProps {
|
||||
renderOutlines: boolean
|
||||
}
|
||||
|
||||
interface FlamechartRowAtlasKey {
|
||||
interface FlamechartRowAtlasKeyInfo {
|
||||
stackDepth: number
|
||||
zoomLevel: number
|
||||
index: number
|
||||
}
|
||||
|
||||
export class FlamechartRowAtlasKey {
|
||||
readonly stackDepth: number
|
||||
readonly zoomLevel: number
|
||||
readonly index: number
|
||||
|
||||
get key() {
|
||||
return `${this.stackDepth}_${this.index}_${this.zoomLevel}`
|
||||
}
|
||||
private constructor(options: FlamechartRowAtlasKeyInfo) {
|
||||
this.stackDepth = options.stackDepth
|
||||
this.zoomLevel = options.zoomLevel
|
||||
this.index = options.index
|
||||
}
|
||||
static getOrInsert(set: KeyedSet<FlamechartRowAtlasKey>, info: FlamechartRowAtlasKeyInfo) {
|
||||
return set.getOrInsert(new FlamechartRowAtlasKey(info))
|
||||
}
|
||||
}
|
||||
|
||||
export class FlamechartRenderer {
|
||||
private layers: RangeTreeNode[] = []
|
||||
private rowAtlas: RowAtlas<FlamechartRowAtlasKey>
|
||||
private rectInfoTexture: regl.Texture
|
||||
private framebuffer: regl.Framebuffer
|
||||
|
||||
constructor(private canvasContext: CanvasContext, private flamechart: Flamechart) {
|
||||
constructor(
|
||||
private canvasContext: CanvasContext,
|
||||
private rowAtlas: RowAtlas<FlamechartRowAtlasKey>,
|
||||
private flamechart: Flamechart,
|
||||
) {
|
||||
const nLayers = flamechart.getLayers().length
|
||||
this.rowAtlas = new RowAtlas(canvasContext)
|
||||
for (let stackDepth = 0; stackDepth < nLayers; stackDepth++) {
|
||||
const leafNodes: RangeTreeLeafNode[] = []
|
||||
const y = stackDepth
|
||||
@ -267,11 +199,7 @@ export class FlamechartRenderer {
|
||||
})
|
||||
}
|
||||
|
||||
private atlasKeys = new Map<string, FlamechartRowAtlasKey>()
|
||||
getOrInsertKey(key: FlamechartRowAtlasKey): FlamechartRowAtlasKey {
|
||||
const hash = `${key.stackDepth}_${key.index}_${key.zoomLevel}`
|
||||
return getOrInsert(this.atlasKeys, hash, () => key)
|
||||
}
|
||||
private atlasKeys = new KeyedSet<FlamechartRowAtlasKey>()
|
||||
|
||||
configSpaceBoundsForKey(key: FlamechartRowAtlasKey): Rect {
|
||||
const {stackDepth, zoomLevel, index} = key
|
||||
@ -285,7 +213,7 @@ export class FlamechartRenderer {
|
||||
render(props: FlamechartRendererProps) {
|
||||
const {configSpaceSrcRect, physicalSpaceDstRect} = props
|
||||
|
||||
const atlasKeysToRender: {stackDepth: number; zoomLevel: number; index: number}[] = []
|
||||
const atlasKeysToRender: FlamechartRowAtlasKey[] = []
|
||||
|
||||
// We want to render the lowest resolution we can while still guaranteeing that the
|
||||
// atlas line is higher resolution than its corresponding destination rectangle on
|
||||
@ -298,7 +226,12 @@ export class FlamechartRenderer {
|
||||
|
||||
let zoomLevel = 0
|
||||
while (true) {
|
||||
const configSpaceBounds = this.configSpaceBoundsForKey({stackDepth: 0, zoomLevel, index: 0})
|
||||
const key = FlamechartRowAtlasKey.getOrInsert(this.atlasKeys, {
|
||||
stackDepth: 0,
|
||||
zoomLevel,
|
||||
index: 0,
|
||||
})
|
||||
const configSpaceBounds = this.configSpaceBoundsForKey(key)
|
||||
const physicalBounds = configToPhysical.transformRect(configSpaceBounds)
|
||||
if (physicalBounds.width() < this.rowAtlas.getResolution()) {
|
||||
break
|
||||
@ -320,7 +253,11 @@ export class FlamechartRenderer {
|
||||
|
||||
for (let stackDepth = top; stackDepth < bottom; stackDepth++) {
|
||||
for (let index = left; index <= right; index++) {
|
||||
const key = this.getOrInsertKey({stackDepth, zoomLevel, index})
|
||||
const key = FlamechartRowAtlasKey.getOrInsert(this.atlasKeys, {
|
||||
stackDepth,
|
||||
zoomLevel,
|
||||
index,
|
||||
})
|
||||
const configSpaceBounds = this.configSpaceBoundsForKey(key)
|
||||
if (!configSpaceBounds.hasIntersectionWith(configSpaceSrcRect)) continue
|
||||
atlasKeysToRender.push(key)
|
||||
|
@ -19,7 +19,7 @@ interface FlamechartDataSource {
|
||||
|
||||
forEachCall(
|
||||
openFrame: (node: CallTreeNode, value: number) => void,
|
||||
closeFrame: (value: number) => void,
|
||||
closeFrame: (node: CallTreeNode, value: number) => void,
|
||||
): void
|
||||
|
||||
getColorBucketForFrame(f: Frame): number
|
||||
@ -65,7 +65,7 @@ export class Flamechart {
|
||||
}
|
||||
|
||||
this.minFrameWidth = Infinity
|
||||
const closeFrame = (value: number) => {
|
||||
const closeFrame = (node: CallTreeNode, value: number) => {
|
||||
console.assert(stack.length > 0)
|
||||
const stackTop = stack.pop()!
|
||||
stackTop.end = value
|
||||
|
@ -29,8 +29,8 @@ function verifyProfile(profile: Profile) {
|
||||
|
||||
let stackList: string[] = []
|
||||
const curStack: (number | string)[] = []
|
||||
|
||||
let lastValue = 0
|
||||
|
||||
function openFrame(node: CallTreeNode, value: number) {
|
||||
if (lastValue != value) {
|
||||
stackList.push(curStack.map(k => `${k}`).join(';'))
|
||||
@ -39,7 +39,7 @@ function verifyProfile(profile: Profile) {
|
||||
curStack.push(node.frame.key)
|
||||
}
|
||||
|
||||
function closeFrame(value: number) {
|
||||
function closeFrame(node: CallTreeNode, value: number) {
|
||||
if (lastValue != value) {
|
||||
stackList.push(curStack.map(k => `${k}`).join(';'))
|
||||
lastValue = value
|
||||
@ -62,11 +62,11 @@ function verifyProfile(profile: Profile) {
|
||||
'a',
|
||||
])
|
||||
|
||||
lastValue = 0
|
||||
stackList = []
|
||||
profile.forEachCallGrouped(openFrame, closeFrame)
|
||||
expect(stackList).toEqual([
|
||||
// prettier-ignore
|
||||
'',
|
||||
'a;b;e',
|
||||
'a;b;b',
|
||||
'a;b;c',
|
||||
@ -74,6 +74,36 @@ function verifyProfile(profile: Profile) {
|
||||
'a;b',
|
||||
'a',
|
||||
])
|
||||
|
||||
const flattened = profile.flattenRecursion()
|
||||
|
||||
lastValue = 0
|
||||
stackList = []
|
||||
flattened.forEachCall(openFrame, closeFrame)
|
||||
expect(stackList).toEqual([
|
||||
// prettier-ignore
|
||||
'a',
|
||||
'a;b',
|
||||
'a;b;d',
|
||||
'a;b;c',
|
||||
'',
|
||||
'a',
|
||||
'a;b',
|
||||
'a;b;e',
|
||||
'a',
|
||||
])
|
||||
|
||||
lastValue = 0
|
||||
stackList = []
|
||||
flattened.forEachCallGrouped(openFrame, closeFrame)
|
||||
expect(stackList).toEqual([
|
||||
// prettier-ignore
|
||||
'a;b;e',
|
||||
'a;b;c',
|
||||
'a;b;d',
|
||||
'a;b',
|
||||
'a',
|
||||
])
|
||||
}
|
||||
|
||||
test('StackListProfileBuilder', () => {
|
||||
|
85
profile.ts
85
profile.ts
@ -1,4 +1,4 @@
|
||||
import {lastOf, getOrInsert} from './utils'
|
||||
import {lastOf, KeyedSet} from './utils'
|
||||
import {ValueFormatter, RawValueFormatter} from './value-formatters'
|
||||
const demangleCppModule = import('./demangle-cpp')
|
||||
|
||||
@ -57,7 +57,7 @@ export class Frame extends HasWeights {
|
||||
// Column in the file
|
||||
col?: number
|
||||
|
||||
constructor(info: FrameInfo) {
|
||||
private constructor(info: FrameInfo) {
|
||||
super()
|
||||
this.key = info.key
|
||||
this.name = info.name
|
||||
@ -65,13 +65,22 @@ export class Frame extends HasWeights {
|
||||
this.line = info.line
|
||||
this.col = info.col
|
||||
}
|
||||
|
||||
static root = new Frame({
|
||||
key: '(speedscope root)',
|
||||
name: '(speedscope root)',
|
||||
})
|
||||
|
||||
static getOrInsert(set: KeyedSet<Frame>, info: FrameInfo) {
|
||||
return set.getOrInsert(new Frame(info))
|
||||
}
|
||||
}
|
||||
|
||||
export class CallTreeNode extends HasWeights {
|
||||
children: CallTreeNode[] = []
|
||||
|
||||
isRoot() {
|
||||
return this.frame === rootFrame
|
||||
return this.frame === Frame.root
|
||||
}
|
||||
|
||||
constructor(readonly frame: Frame, readonly parent: CallTreeNode | null) {
|
||||
@ -79,19 +88,14 @@ export class CallTreeNode extends HasWeights {
|
||||
}
|
||||
}
|
||||
|
||||
const rootFrame = new Frame({
|
||||
key: '(speedscope root)',
|
||||
name: '(speedscope root)',
|
||||
})
|
||||
|
||||
export class Profile {
|
||||
protected name: string = ''
|
||||
|
||||
protected totalWeight: number
|
||||
|
||||
protected frames = new Map<string | number, Frame>()
|
||||
protected appendOrderCalltreeRoot = new CallTreeNode(rootFrame, null)
|
||||
protected groupedCalltreeRoot = new CallTreeNode(rootFrame, null)
|
||||
protected frames = new KeyedSet<Frame>()
|
||||
protected appendOrderCalltreeRoot = new CallTreeNode(Frame.root, null)
|
||||
protected groupedCalltreeRoot = new CallTreeNode(Frame.root, null)
|
||||
|
||||
// List of references to CallTreeNodes at the top of the
|
||||
// stack at the time of the sample.
|
||||
@ -135,10 +139,10 @@ export class Profile {
|
||||
|
||||
forEachCallGrouped(
|
||||
openFrame: (node: CallTreeNode, value: number) => void,
|
||||
closeFrame: (value: number) => void,
|
||||
closeFrame: (node: CallTreeNode, value: number) => void,
|
||||
) {
|
||||
function visit(node: CallTreeNode, start: number) {
|
||||
if (node.frame !== rootFrame) {
|
||||
if (node.frame !== Frame.root) {
|
||||
openFrame(node, start)
|
||||
}
|
||||
|
||||
@ -152,8 +156,8 @@ export class Profile {
|
||||
childTime += child.getTotalWeight()
|
||||
})
|
||||
|
||||
if (node.frame !== rootFrame) {
|
||||
closeFrame(start + node.getTotalWeight())
|
||||
if (node.frame !== Frame.root) {
|
||||
closeFrame(node, start + node.getTotalWeight())
|
||||
}
|
||||
}
|
||||
visit(this.groupedCalltreeRoot, 0)
|
||||
@ -161,7 +165,7 @@ export class Profile {
|
||||
|
||||
forEachCall(
|
||||
openFrame: (node: CallTreeNode, value: number) => void,
|
||||
closeFrame: (value: number) => void,
|
||||
closeFrame: (node: CallTreeNode, value: number) => void,
|
||||
) {
|
||||
let prevStack: CallTreeNode[] = []
|
||||
let value = 0
|
||||
@ -175,21 +179,21 @@ export class Profile {
|
||||
// so hopefully this isn't much of a problem
|
||||
for (
|
||||
lca = stackTop;
|
||||
lca && lca.frame != rootFrame && prevStack.indexOf(lca) === -1;
|
||||
lca && lca.frame != Frame.root && prevStack.indexOf(lca) === -1;
|
||||
lca = lca.parent
|
||||
) {}
|
||||
|
||||
// Close frames that are no longer open
|
||||
while (prevStack.length > 0 && lastOf(prevStack) != lca) {
|
||||
prevStack.pop()
|
||||
closeFrame(value)
|
||||
const node = prevStack.pop()!
|
||||
closeFrame(node, value)
|
||||
}
|
||||
|
||||
// Open frames that are now becoming open
|
||||
const toOpen: CallTreeNode[] = []
|
||||
for (
|
||||
let node: CallTreeNode | null = stackTop;
|
||||
node && node.frame != rootFrame && node != lca;
|
||||
node && node.frame != Frame.root && node != lca;
|
||||
node = node.parent
|
||||
) {
|
||||
toOpen.push(node)
|
||||
@ -206,7 +210,7 @@ export class Profile {
|
||||
|
||||
// Close frames that are open at the end of the trace
|
||||
for (let i = prevStack.length - 1; i >= 0; i--) {
|
||||
closeFrame(value)
|
||||
closeFrame(prevStack[i], value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,11 +218,42 @@ export class Profile {
|
||||
this.frames.forEach(fn)
|
||||
}
|
||||
|
||||
flattenRecursion(): Profile {
|
||||
const builder = new CallTreeProfileBuilder()
|
||||
|
||||
const stack: (CallTreeNode | null)[] = []
|
||||
const framesInStack = new Set<Frame>()
|
||||
|
||||
function openFrame(node: CallTreeNode, value: number) {
|
||||
if (framesInStack.has(node.frame)) {
|
||||
stack.push(null)
|
||||
} else {
|
||||
framesInStack.add(node.frame)
|
||||
stack.push(node)
|
||||
builder.enterFrame(node.frame, value)
|
||||
}
|
||||
}
|
||||
function closeFrame(node: CallTreeNode, value: number) {
|
||||
const stackTop = stack.pop()
|
||||
if (stackTop) {
|
||||
framesInStack.delete(stackTop.frame)
|
||||
builder.leaveFrame(stackTop.frame, value)
|
||||
}
|
||||
}
|
||||
|
||||
this.forEachCall(openFrame, closeFrame)
|
||||
|
||||
const flattenedProfile = builder.build()
|
||||
flattenedProfile.name = this.name
|
||||
flattenedProfile.valueFormatter = this.valueFormatter
|
||||
return flattenedProfile
|
||||
}
|
||||
|
||||
// Demangle symbols for readability
|
||||
async demangle() {
|
||||
let demangleCpp: ((name: string) => string) | null = null
|
||||
|
||||
for (let frame of this.frames.values()) {
|
||||
for (let frame of this.frames) {
|
||||
// This function converts a mangled C++ name such as "__ZNK7Support6ColorFeqERKS0_"
|
||||
// into a human-readable symbol (in this case "Support::ColorF::==(Support::ColorF&)")
|
||||
if (frame.name.startsWith('__Z')) {
|
||||
@ -239,7 +274,7 @@ export class StackListProfileBuilder extends Profile {
|
||||
let framesInStack = new Set<Frame>()
|
||||
|
||||
for (let frameInfo of stack) {
|
||||
const frame = getOrInsert(this.frames, frameInfo.key, () => new Frame(frameInfo))
|
||||
const frame = Frame.getOrInsert(this.frames, frameInfo)
|
||||
const last = useAppendOrder
|
||||
? lastOf(node.children)
|
||||
: node.children.find(c => c.frame === frame)
|
||||
@ -346,7 +381,7 @@ export class CallTreeProfileBuilder extends Profile {
|
||||
}
|
||||
}
|
||||
enterFrame(frameInfo: FrameInfo, value: number) {
|
||||
const frame = getOrInsert(this.frames, frameInfo.key, () => new Frame(frameInfo))
|
||||
const frame = Frame.getOrInsert(this.frames, frameInfo)
|
||||
this.addWeightsToFrames(value)
|
||||
this._enterFrame(frame, value, true)
|
||||
this._enterFrame(frame, value, false)
|
||||
@ -374,7 +409,7 @@ export class CallTreeProfileBuilder extends Profile {
|
||||
}
|
||||
|
||||
leaveFrame(frameInfo: FrameInfo, value: number) {
|
||||
const frame = getOrInsert(this.frames, frameInfo.key, () => new Frame(frameInfo))
|
||||
const frame = Frame.getOrInsert(this.frames, frameInfo)
|
||||
this.addWeightsToFrames(value)
|
||||
|
||||
this._leaveFrame(frame, value, true)
|
||||
|
94
row-atlas.ts
Normal file
94
row-atlas.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import regl from 'regl'
|
||||
import {LRUCache} from './lru-cache'
|
||||
import {RectangleBatch} from './rectangle-batch-renderer'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
import {Rect, Vec2} from './math'
|
||||
import {Color} from './color'
|
||||
|
||||
export class RowAtlas<K> {
|
||||
texture: regl.Texture
|
||||
private framebuffer: regl.Framebuffer
|
||||
private renderToFramebuffer: regl.Command<{}>
|
||||
private rowCache: LRUCache<K, number>
|
||||
private clearLineBatch: RectangleBatch
|
||||
|
||||
constructor(private canvasContext: CanvasContext) {
|
||||
this.texture = canvasContext.gl.texture({
|
||||
width: Math.min(canvasContext.getMaxTextureSize(), 4096),
|
||||
height: Math.min(canvasContext.getMaxTextureSize(), 4096),
|
||||
wrapS: 'clamp',
|
||||
wrapT: 'clamp',
|
||||
})
|
||||
this.framebuffer = canvasContext.gl.framebuffer({color: [this.texture]})
|
||||
this.rowCache = new LRUCache(this.texture.height)
|
||||
this.renderToFramebuffer = canvasContext.gl({
|
||||
framebuffer: this.framebuffer,
|
||||
})
|
||||
this.clearLineBatch = canvasContext.createRectangleBatch()
|
||||
this.clearLineBatch.addRect(Rect.unit, new Color(0, 0, 0, 0))
|
||||
}
|
||||
|
||||
has(key: K) {
|
||||
return this.rowCache.has(key)
|
||||
}
|
||||
getResolution() {
|
||||
return this.texture.width
|
||||
}
|
||||
getCapacity() {
|
||||
return this.texture.height
|
||||
}
|
||||
|
||||
private allocateLine(key: K): number {
|
||||
if (this.rowCache.getSize() < this.rowCache.getCapacity()) {
|
||||
// Not in cache, but cache isn't full
|
||||
const row = this.rowCache.getSize()
|
||||
this.rowCache.insert(key, row)
|
||||
return row
|
||||
} else {
|
||||
// Not in cache, and cache is full. Evict something.
|
||||
const [, row] = this.rowCache.removeLRU()!
|
||||
this.rowCache.insert(key, row)
|
||||
return row
|
||||
}
|
||||
}
|
||||
|
||||
writeToAtlasIfNeeded(keys: K[], render: (textureDstRect: Rect, key: K) => void) {
|
||||
this.renderToFramebuffer((context: regl.Context) => {
|
||||
for (let key of keys) {
|
||||
let row = this.rowCache.get(key)
|
||||
if (row != null) {
|
||||
// Already cached!
|
||||
continue
|
||||
}
|
||||
// Not cached -- we'll have to actually render
|
||||
row = this.allocateLine(key)
|
||||
|
||||
const textureRect = new Rect(new Vec2(0, row), new Vec2(this.texture.width, 1))
|
||||
this.canvasContext.drawRectangleBatch({
|
||||
batch: this.clearLineBatch,
|
||||
configSpaceSrcRect: Rect.unit,
|
||||
physicalSpaceDstRect: textureRect,
|
||||
})
|
||||
render(textureRect, key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
renderViaAtlas(key: K, dstRect: Rect): boolean {
|
||||
let row = this.rowCache.get(key)
|
||||
if (row == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const textureRect = new Rect(new Vec2(0, row), new Vec2(this.texture.width, 1))
|
||||
|
||||
// At this point, we have the row in cache, and we can
|
||||
// paint directly from it into the framebuffer.
|
||||
this.canvasContext.drawTexture({
|
||||
texture: this.texture,
|
||||
srcRect: textureRect,
|
||||
dstRect: dstRect,
|
||||
})
|
||||
return true
|
||||
}
|
||||
}
|
10
sample/profiles/stackcollapse/recursion.txt
Normal file
10
sample/profiles/stackcollapse/recursion.txt
Normal file
@ -0,0 +1,10 @@
|
||||
a;a 1
|
||||
b;b 1
|
||||
a;b 1
|
||||
a;b;a;b 1
|
||||
a;b;a;c 1
|
||||
a;b;a;b;a;b 1
|
||||
a;b;a;b;c 1
|
||||
a;b;a;b 1
|
||||
a;b;a;c 1
|
||||
a;b;a;b;c 1
|
@ -30,7 +30,7 @@ export function dumpProfile(profile: Profile): any {
|
||||
curStack.push(node.frame.name)
|
||||
}
|
||||
|
||||
function closeFrame(value: number) {
|
||||
function closeFrame(node: CallTreeNode, value: number) {
|
||||
maybeEmit(value)
|
||||
curStack.pop()
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
itReduce,
|
||||
zeroPad,
|
||||
formatPercent,
|
||||
KeyedSet,
|
||||
} from './utils'
|
||||
|
||||
test('sortBy', () => {
|
||||
@ -25,6 +26,31 @@ test('getOrInsert', () => {
|
||||
expect(m.get('x')).toBe(1)
|
||||
})
|
||||
|
||||
class ValueType {
|
||||
private constructor(readonly a: string, readonly num: number) {}
|
||||
get key() {
|
||||
return `${this.a}_${this.num}`
|
||||
}
|
||||
static getOrInsert(set: KeyedSet<ValueType>, a: string, num: number) {
|
||||
return set.getOrInsert(new ValueType(a, num))
|
||||
}
|
||||
}
|
||||
|
||||
test('KeyedSet', () => {
|
||||
const set = new KeyedSet<ValueType>()
|
||||
|
||||
const x1 = ValueType.getOrInsert(set, 'x', 1)
|
||||
const x2 = ValueType.getOrInsert(set, 'x', 1)
|
||||
const y = ValueType.getOrInsert(set, 'y', 1)
|
||||
|
||||
expect(x1).toBe(x2)
|
||||
expect(y).not.toBe(x1)
|
||||
|
||||
const set2 = new KeyedSet<ValueType>()
|
||||
const x3 = ValueType.getOrInsert(set2, 'x', 1)
|
||||
expect(x1).not.toBe(x3)
|
||||
})
|
||||
|
||||
test('getOrElse', () => {
|
||||
const m = new Map<string, number>()
|
||||
expect(getOrElse(m, 'hello', k => k.length)).toBe(5)
|
||||
|
35
utils.ts
35
utils.ts
@ -26,6 +26,41 @@ export function getOrThrow<K, V>(map: Map<K, V>, k: K): V {
|
||||
return map.get(k)!
|
||||
}
|
||||
|
||||
// Intended to be used to de-duplicate objects based on a key property. This
|
||||
// allows value comparisons to be done efficiently and for the returned objects
|
||||
// to be used intuitively in Map objects.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// export class Frame {
|
||||
// private constructor(readonly file: string, readonly name: string) {}
|
||||
// get key() { return `${this.file}:${this.name}` }
|
||||
// static getOrInsert(set: KeyedSet<Frame>, file: string, name: string) {
|
||||
// return set.getOrInsert(set, new Frame(file, name))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
export interface HasKey {
|
||||
readonly key: string | number
|
||||
}
|
||||
export class KeyedSet<T extends HasKey> implements Iterable<T> {
|
||||
private map = new Map<string | number, T>()
|
||||
|
||||
getOrInsert(t: T): T {
|
||||
const key = t.key
|
||||
const existing = this.map.get(key)
|
||||
if (existing) return existing
|
||||
this.map.set(key, t)
|
||||
return t
|
||||
}
|
||||
forEach(fn: (t: T) => void) {
|
||||
this.map.forEach(fn)
|
||||
}
|
||||
[Symbol.iterator]() {
|
||||
return this.map.values()
|
||||
}
|
||||
}
|
||||
|
||||
export function* itMap<T, U>(it: Iterable<T>, f: (t: T) => U): Iterable<U> {
|
||||
for (let t of it) {
|
||||
yield f(t)
|
||||
|
Loading…
Reference in New Issue
Block a user