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:
Jamie Wong 2018-06-21 13:56:58 -07:00 committed by GitHub
parent aaac0ad7e3
commit d659eb0159
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 374 additions and 164 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"editor.formatOnSave": true,
}

View File

@ -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}

View File

@ -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)

View File

@ -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

View File

@ -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', () => {

View File

@ -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
View 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
}
}

View 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

View File

@ -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()
}

View File

@ -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)

View File

@ -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)