Row atlas sort of working?
This commit is contained in:
+4
-7
@@ -18,6 +18,7 @@ export class CanvasContext {
|
||||
private viewportRectangleRenderer: ViewportRectangleRenderer
|
||||
private textureRenderer: TextureRenderer
|
||||
private setViewportScope: regl.Command<{ physicalBounds: Rect }>
|
||||
private setScissor: regl.Command<{}>
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
this.gl = regl({
|
||||
@@ -33,7 +34,7 @@ export class CanvasContext {
|
||||
this.rectangleBatchRenderer = new RectangleBatchRenderer(this.gl)
|
||||
this.viewportRectangleRenderer = new ViewportRectangleRenderer(this.gl)
|
||||
this.textureRenderer = new TextureRenderer(this.gl)
|
||||
|
||||
this.setScissor = this.gl({ scissor: { enable: true } })
|
||||
this.setViewportScope = this.gl<SetViewportScopeProps>({
|
||||
context: {
|
||||
viewportX: (context: regl.Context, props: SetViewportScopeProps) => {
|
||||
@@ -87,9 +88,7 @@ export class CanvasContext {
|
||||
private statsPanel: StatsPanel | null = this.perfDebug ? new StatsPanel() : null
|
||||
|
||||
private onBeforeFrame = (context: regl.Context) => {
|
||||
this.gl({
|
||||
scissor: { enable: false }
|
||||
})(() => {
|
||||
this.setScissor(() => {
|
||||
this.gl.clear({ color: [0, 0, 0, 0] })
|
||||
})
|
||||
|
||||
@@ -120,9 +119,7 @@ export class CanvasContext {
|
||||
}
|
||||
|
||||
drawTexture(props: TextureRendererProps) {
|
||||
this.gl({})((context: regl.Context) => {
|
||||
this.textureRenderer.render(context, props)
|
||||
})
|
||||
this.textureRenderer.render(props)
|
||||
}
|
||||
|
||||
createRectangleBatch(): RectangleBatch {
|
||||
|
||||
@@ -5,6 +5,159 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>speedscope</title>
|
||||
<script>
|
||||
// https://github.com/evanw/webgl-recorder
|
||||
false && (function() {
|
||||
var getContext = HTMLCanvasElement.prototype.getContext;
|
||||
var requestAnimationFrame = window.requestAnimationFrame;
|
||||
var frameSincePageLoad = 0;
|
||||
|
||||
function countFrames() {
|
||||
frameSincePageLoad++;
|
||||
requestAnimationFrame(countFrames);
|
||||
}
|
||||
|
||||
window.requestAnimationFrame = function() {
|
||||
return requestAnimationFrame.apply(window, arguments);
|
||||
};
|
||||
|
||||
HTMLCanvasElement.prototype.getContext = function(type) {
|
||||
var canvas = this;
|
||||
var context = getContext.apply(canvas, arguments);
|
||||
|
||||
if (type === 'webgl' || type === 'experimental-webgl') {
|
||||
var oldWidth = canvas.width;
|
||||
var oldHeight = canvas.height;
|
||||
var oldFrameCount = frameSincePageLoad;
|
||||
var trace = [];
|
||||
var variables = {};
|
||||
var fakeContext = {
|
||||
trace: trace,
|
||||
compileTrace: compileTrace,
|
||||
downloadTrace: downloadTrace,
|
||||
};
|
||||
|
||||
trace.push(' gl.canvas.width = ' + oldWidth + ';');
|
||||
trace.push(' gl.canvas.height = ' + oldHeight + ';');
|
||||
|
||||
function compileTrace() {
|
||||
var text = 'function* render(gl) {\n';
|
||||
text += ' // Recorded using https://github.com/evanw/webgl-recorder\n';
|
||||
for (var key in variables) {
|
||||
text += ' var ' + key + 's = [];\n';
|
||||
}
|
||||
text += trace.join('\n');
|
||||
text += '\n}\n';
|
||||
return text;
|
||||
}
|
||||
|
||||
function downloadTrace() {
|
||||
var text = compileTrace();
|
||||
var link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(new Blob([text], {type: 'application/javascript'}));
|
||||
link.download = 'trace.js';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function getVariable(value) {
|
||||
if (value instanceof WebGLActiveInfo ||
|
||||
value instanceof WebGLBuffer ||
|
||||
value instanceof WebGLFramebuffer ||
|
||||
value instanceof WebGLProgram ||
|
||||
value instanceof WebGLRenderbuffer ||
|
||||
value instanceof WebGLShader ||
|
||||
value instanceof WebGLShaderPrecisionFormat ||
|
||||
value instanceof WebGLTexture ||
|
||||
value instanceof WebGLUniformLocation) {
|
||||
var name = value.constructor.name;
|
||||
var list = variables[name] || (variables[name] = []);
|
||||
var index = list.indexOf(value);
|
||||
|
||||
if (index === -1) {
|
||||
index = list.length;
|
||||
list.push(value);
|
||||
}
|
||||
|
||||
return name + 's[' + index + ']';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
console.timeStamp('start')
|
||||
var start = performance.now()
|
||||
for (var key in context) {
|
||||
var value = context[key];
|
||||
|
||||
if (typeof value === 'function') {
|
||||
fakeContext[key] = function(key, value) {
|
||||
return function() {
|
||||
trace.push(`// ${performance.now() - start}`)
|
||||
var result = value.apply(context, arguments);
|
||||
var args = [];
|
||||
|
||||
if (frameSincePageLoad !== oldFrameCount) {
|
||||
oldFrameCount = frameSincePageLoad;
|
||||
trace.push(' yield;');
|
||||
}
|
||||
|
||||
if (canvas.width !== oldWidth || canvas.height !== oldHeight) {
|
||||
oldWidth = canvas.width;
|
||||
oldHeight = canvas.height;
|
||||
trace.push(' gl.canvas.width = ' + oldWidth + ';');
|
||||
trace.push(' gl.canvas.height = ' + oldHeight + ';');
|
||||
}
|
||||
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var arg = arguments[i];
|
||||
|
||||
if (typeof arg === 'number' || typeof arg === 'boolean' || typeof arg === 'string' || arg === null) {
|
||||
args.push(JSON.stringify(arg));
|
||||
}
|
||||
|
||||
else if (ArrayBuffer.isView(arg)) {
|
||||
args.push('new ' + arg.constructor.name + '([' + Array.prototype.slice.call(arg) + '])');
|
||||
}
|
||||
|
||||
else {
|
||||
var variable = getVariable(arg);
|
||||
if (variable !== null) {
|
||||
args.push(variable);
|
||||
}
|
||||
|
||||
else {
|
||||
console.log('unsupported value:', arg);
|
||||
args.push('null');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var text = 'gl.' + key + '(' + args.join(', ') + ');';
|
||||
var variable = getVariable(result);
|
||||
if (variable !== null) text = variable + ' = ' + text;
|
||||
trace.push(' ' + text);
|
||||
|
||||
return result;
|
||||
};
|
||||
}(key, value);
|
||||
}
|
||||
|
||||
else {
|
||||
fakeContext[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return fakeContext;
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
countFrames();
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="reset.css">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
|
||||
|
||||
@@ -99,7 +99,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
}
|
||||
|
||||
this.props.canvasContext.renderInto(this.container, (context) => {
|
||||
this.cachedRenderer!.render(context, {
|
||||
this.cachedRenderer!.render({
|
||||
physicalSize: this.physicalViewSize()
|
||||
})
|
||||
this.props.canvasContext.drawViewportRectangle({
|
||||
|
||||
+196
-145
@@ -3,15 +3,99 @@ 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'
|
||||
|
||||
const MAX_BATCH_SIZE = 10000
|
||||
|
||||
class RowAtlas<K> {
|
||||
private texture: regl.Texture
|
||||
private framebuffer: regl.Framebuffer
|
||||
private renderToFramebuffer: regl.Command<{}>
|
||||
private rowCache: LRUCache<K, number>
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
has(key: K) { return this.rowCache.has(key) }
|
||||
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!
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
)
|
||||
|
||||
render(textureRect, key)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
getMinLeft(): number
|
||||
getMaxRight(): number
|
||||
getBounds(): Rect
|
||||
getRectCount(): number
|
||||
getChildren(): RangeTreeNode[]
|
||||
forEachBatchWithinBounds(configSpaceViewport: Rect, cb: (batch: RectangleBatch) => void): void
|
||||
forEachLeafNodeWithinBounds(configSpaceBounds: Rect, cb: (leaf: RangeTreeLeafNode) => void): void
|
||||
}
|
||||
|
||||
class RangeTreeLeafNode implements RangeTreeNode {
|
||||
@@ -19,46 +103,54 @@ class RangeTreeLeafNode implements RangeTreeNode {
|
||||
|
||||
constructor(
|
||||
private batch: RectangleBatch,
|
||||
private minLeft: number,
|
||||
private maxRight: number
|
||||
) {}
|
||||
private bounds: Rect
|
||||
) {
|
||||
batch.uploadToGPU()
|
||||
}
|
||||
|
||||
getMinLeft() { return this.minLeft }
|
||||
getMaxRight() { return this.maxRight }
|
||||
getBatch() { return this.batch }
|
||||
getBounds() { return this.bounds }
|
||||
getRectCount() { return this.batch.getRectCount() }
|
||||
getChildren() { return this.children }
|
||||
forEachBatchWithinBounds(configSpaceViewport: Rect, cb: (batch: RectangleBatch) => void) {
|
||||
if (this.maxRight < configSpaceViewport.left()) return
|
||||
if (this.minLeft > configSpaceViewport.right()) return
|
||||
cb(this.batch)
|
||||
|
||||
// TODO(jlfwong): Remove this line. This is here for now to artificially
|
||||
// decrease rendering performance to try various optimizations.
|
||||
cb(this.batch)
|
||||
forEachLeafNodeWithinBounds(configSpaceBounds: Rect, cb: (leaf: RangeTreeLeafNode) => void) {
|
||||
if (!this.bounds.hasIntersectionWith(configSpaceBounds)) return
|
||||
cb(this)
|
||||
}
|
||||
}
|
||||
|
||||
class RangeTreeInteriorNode implements RangeTreeNode {
|
||||
private rectCount: number = 0
|
||||
private bounds: Rect
|
||||
constructor(private children: RangeTreeNode[]) {
|
||||
if (children.length === 0) {
|
||||
throw new Error("Empty interior node")
|
||||
}
|
||||
let minLeft = Infinity
|
||||
let maxRight = -Infinity
|
||||
let minTop = Infinity
|
||||
let maxBottom = -Infinity
|
||||
for (let child of children) {
|
||||
this.rectCount += child.getRectCount()
|
||||
const bounds = child.getBounds()
|
||||
minLeft = Math.min(minLeft, bounds.left())
|
||||
maxRight = Math.max(maxRight, bounds.right())
|
||||
minTop = Math.min(minTop, bounds.top())
|
||||
maxBottom = Math.max(maxBottom, bounds.bottom())
|
||||
}
|
||||
this.bounds = new Rect(
|
||||
new Vec2(minLeft, minTop),
|
||||
new Vec2(maxRight - minLeft, maxBottom - minTop)
|
||||
)
|
||||
}
|
||||
|
||||
getMinLeft() { return this.children[0].getMinLeft() }
|
||||
getMaxRight() { return this.children[this.children.length - 1].getMaxRight() }
|
||||
getBounds() { return this.bounds }
|
||||
getRectCount() { return this.rectCount }
|
||||
getChildren() { return this.children }
|
||||
forEachBatchWithinBounds(configSpaceViewport: Rect, cb: (batch: RectangleBatch) => void) {
|
||||
// if (this.getMaxRight() < configSpaceViewport.left()) return
|
||||
// if (this.getMinLeft() > configSpaceViewport.right()) return
|
||||
|
||||
forEachLeafNodeWithinBounds(configSpaceBounds: Rect, cb: (leaf: RangeTreeLeafNode) => void) {
|
||||
if (!this.bounds.hasIntersectionWith(configSpaceBounds)) return
|
||||
for (let child of this.children) {
|
||||
child.forEachBatchWithinBounds(configSpaceViewport, cb)
|
||||
child.forEachLeafNodeWithinBounds(configSpaceBounds, cb)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,149 +160,108 @@ export interface FlamechartRendererProps {
|
||||
physicalSpaceDstRect: Rect
|
||||
}
|
||||
|
||||
class BoundedLayer {
|
||||
private rootNode: RangeTreeNode
|
||||
constructor(
|
||||
private canvasContext: CanvasContext,
|
||||
flamechart: Flamechart,
|
||||
private stackDepth: number
|
||||
) {
|
||||
const leafNodes: RangeTreeLeafNode[] = []
|
||||
|
||||
let minLeft = Infinity
|
||||
let maxRight = -Infinity
|
||||
let batch = canvasContext.createRectangleBatch()
|
||||
|
||||
for (let frame of flamechart.getLayers()[stackDepth]) {
|
||||
if (batch.getRectCount() >= MAX_BATCH_SIZE) {
|
||||
leafNodes.push(new RangeTreeLeafNode(batch, minLeft, maxRight))
|
||||
minLeft = Infinity
|
||||
maxRight = -Infinity
|
||||
batch = canvasContext.createRectangleBatch()
|
||||
}
|
||||
const configSpaceBounds = new Rect(
|
||||
new Vec2(frame.start, stackDepth + 1),
|
||||
new Vec2(frame.end - frame.start, 1)
|
||||
)
|
||||
minLeft = Math.min(minLeft, configSpaceBounds.left())
|
||||
maxRight = Math.max(maxRight, configSpaceBounds.right())
|
||||
const color = flamechart.getColorForFrame(frame.node.frame)
|
||||
batch.addRect(configSpaceBounds, color)
|
||||
}
|
||||
|
||||
if (batch.getRectCount() > 0) {
|
||||
leafNodes.push(new RangeTreeLeafNode(batch, minLeft, maxRight))
|
||||
}
|
||||
|
||||
// TODO(jlfwong): Probably want this to be a binary tree
|
||||
this.rootNode = new RangeTreeInteriorNode(leafNodes)
|
||||
}
|
||||
|
||||
render(props: FlamechartRendererProps) {
|
||||
const configSpaceTop = this.stackDepth + 1
|
||||
const configSpaceBottom = configSpaceTop + 1
|
||||
|
||||
const { configSpaceSrcRect, physicalSpaceDstRect } = props
|
||||
if (configSpaceTop > configSpaceSrcRect.bottom()) {
|
||||
// Entire layer is below the config space bounds
|
||||
return
|
||||
}
|
||||
|
||||
if (configSpaceBottom < configSpaceSrcRect.top()) {
|
||||
// Entire layer is above the config space bounds
|
||||
return
|
||||
}
|
||||
|
||||
this.rootNode.forEachBatchWithinBounds(configSpaceSrcRect, batch => {
|
||||
this.canvasContext.drawRectangleBatch({
|
||||
configSpaceSrcRect,
|
||||
physicalSpaceDstRect,
|
||||
batch
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class FlamechartRenderer {
|
||||
private layers: BoundedLayer[] = []
|
||||
private texture: regl.Texture | null = null
|
||||
private root: RangeTreeNode
|
||||
private rowAtlas: RowAtlas<RangeTreeLeafNode>
|
||||
|
||||
private getConfigSpaceSize() {
|
||||
return new Vec2(this.flamechart.getTotalWeight(), this.flamechart.getLayers().length)
|
||||
}
|
||||
|
||||
private getConfigSpaceContentRect() {
|
||||
return new Rect(new Vec2(), this.getConfigSpaceSize())
|
||||
}
|
||||
|
||||
constructor(private canvasContext: CanvasContext, private flamechart: Flamechart) {
|
||||
constructor(private canvasContext: CanvasContext, flamechart: Flamechart) {
|
||||
const nLayers = flamechart.getLayers().length
|
||||
const maxTextureSize = canvasContext.getMaxTextureSize()
|
||||
this.rowAtlas = new RowAtlas(canvasContext)
|
||||
|
||||
if (nLayers > maxTextureSize) {
|
||||
throw new Error(`This profile has more than ${maxTextureSize} layers!`)
|
||||
}
|
||||
const layers: RangeTreeNode[] = []
|
||||
|
||||
for (let i = 0; i < nLayers; i++) {
|
||||
this.layers.push(new BoundedLayer(canvasContext, flamechart, i))
|
||||
}
|
||||
for (let stackDepth = 0; stackDepth < nLayers; stackDepth++) {
|
||||
const leafNodes: RangeTreeLeafNode[] = []
|
||||
const y = stackDepth + 1
|
||||
|
||||
this.texture = canvasContext.gl.texture({
|
||||
width: canvasContext.getMaxTextureSize(),
|
||||
height: nLayers,
|
||||
wrapS: 'clamp',
|
||||
wrapT: 'clamp',
|
||||
})
|
||||
const fbo = canvasContext.gl.framebuffer({ color: [this.texture] })
|
||||
let minLeft = Infinity
|
||||
let maxRight = -Infinity
|
||||
let batch = canvasContext.createRectangleBatch()
|
||||
|
||||
const configSpaceSrcRect = this.getConfigSpaceContentRect()
|
||||
|
||||
canvasContext.gl({
|
||||
viewport: (context, props) => {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: canvasContext.getMaxTextureSize(),
|
||||
height: nLayers
|
||||
for (let frame of flamechart.getLayers()[stackDepth]) {
|
||||
if (batch.getRectCount() >= MAX_BATCH_SIZE) {
|
||||
leafNodes.push(new RangeTreeLeafNode(batch, new Rect(
|
||||
new Vec2(minLeft, stackDepth),
|
||||
new Vec2(maxRight - minLeft, 1)
|
||||
)))
|
||||
minLeft = Infinity
|
||||
maxRight = -Infinity
|
||||
batch = canvasContext.createRectangleBatch()
|
||||
}
|
||||
},
|
||||
framebuffer: fbo
|
||||
})((context: regl.Context) => {
|
||||
const physicalSpaceDstRect = new Rect(
|
||||
new Vec2(),
|
||||
new Vec2(context.viewportWidth, context.viewportHeight)
|
||||
)
|
||||
for (let layer of this.layers) {
|
||||
layer.render({ configSpaceSrcRect, physicalSpaceDstRect })
|
||||
const configSpaceBounds = new Rect(
|
||||
new Vec2(frame.start, y),
|
||||
new Vec2(frame.end - frame.start, 1)
|
||||
)
|
||||
minLeft = Math.min(minLeft, configSpaceBounds.left())
|
||||
maxRight = Math.max(maxRight, configSpaceBounds.right())
|
||||
const color = flamechart.getColorForFrame(frame.node.frame)
|
||||
batch.addRect(configSpaceBounds, color)
|
||||
}
|
||||
})
|
||||
|
||||
fbo.destroy()
|
||||
if (batch.getRectCount() > 0) {
|
||||
leafNodes.push(new RangeTreeLeafNode(batch, new Rect(
|
||||
new Vec2(minLeft, stackDepth),
|
||||
new Vec2(maxRight - minLeft, 1)
|
||||
)))
|
||||
}
|
||||
|
||||
// TODO(jlfwong): Probably want this to be a binary tree
|
||||
layers.push(new RangeTreeInteriorNode(leafNodes))
|
||||
}
|
||||
this.root = new RangeTreeInteriorNode(layers)
|
||||
}
|
||||
|
||||
render(props: FlamechartRendererProps) {
|
||||
if (!this.texture) return
|
||||
|
||||
const { configSpaceSrcRect, physicalSpaceDstRect } = props
|
||||
const content = this.getConfigSpaceContentRect()
|
||||
|
||||
const textureRect = new Rect(
|
||||
new Vec2(), new Vec2(this.texture.width, this.texture.height)
|
||||
)
|
||||
let renderedBatchCount = 0
|
||||
let cacheCapacity = this.rowAtlas.getCapacity()
|
||||
|
||||
const configToTexture = AffineTransform.betweenRects(content, textureRect)
|
||||
const physicalSpaceSrcRect = configToTexture.transformRect(configSpaceSrcRect)
|
||||
const cachedLeaves: RangeTreeLeafNode[] = []
|
||||
const uncachedLeaves: RangeTreeLeafNode[] = []
|
||||
|
||||
this.canvasContext.drawTexture({
|
||||
texture: this.texture,
|
||||
srcRect: physicalSpaceSrcRect,
|
||||
dstRect: physicalSpaceDstRect
|
||||
this.root.forEachLeafNodeWithinBounds(configSpaceSrcRect, leaf => {
|
||||
// We want to avoid rendering more batches to the cache than
|
||||
// the capacity fo the cache to prevent LRU cache thrash. Imagine
|
||||
// the capacity is 2 and you render 4 items via the cache. Every time
|
||||
// you do this, you end up evicting and populating the cache on all 4 items,
|
||||
// which is even more expensive than not using a cache at all! Instead,
|
||||
// we'll cache the first 2 entries in that case, and re-use that cache each time,
|
||||
// while rendering the final 2 items without use of the cache.
|
||||
// An exception here is if the node is already in the cache!
|
||||
let useCache = renderedBatchCount++ < cacheCapacity || this.rowAtlas.has(leaf)
|
||||
|
||||
if (useCache) {
|
||||
cachedLeaves.push(leaf)
|
||||
} else {
|
||||
uncachedLeaves.push(leaf)
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
for (let layer of this.layers) {
|
||||
layer.render(props)
|
||||
this.rowAtlas.writeToAtlasIfNeeded(cachedLeaves, (textureDstRect, leaf) => {
|
||||
this.canvasContext.drawRectangleBatch({
|
||||
batch: leaf.getBatch(),
|
||||
configSpaceSrcRect: leaf.getBounds(),
|
||||
physicalSpaceDstRect: textureDstRect
|
||||
})
|
||||
})
|
||||
|
||||
const configToPhysical = AffineTransform.betweenRects(configSpaceSrcRect, physicalSpaceDstRect)
|
||||
for (let leaf of cachedLeaves) {
|
||||
const configSpaceLeafBounds = leaf.getBounds()
|
||||
const physicalLeafBounds = configToPhysical.transformRect(configSpaceLeafBounds)
|
||||
if (!this.rowAtlas.renderViaAtlas(leaf, physicalLeafBounds)) {
|
||||
console.error('Failed to render from cache')
|
||||
}
|
||||
}
|
||||
|
||||
for (let leaf of uncachedLeaves) {
|
||||
this.canvasContext.drawRectangleBatch({
|
||||
batch: leaf.getBatch(),
|
||||
configSpaceSrcRect,
|
||||
physicalSpaceDstRect
|
||||
})
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -310,7 +310,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
|
||||
this.props.canvasContext.renderInto(this.container, () => {
|
||||
this.props.flamechartRenderer.render({
|
||||
physicalSpaceDstRect: new Rect(new Vec2(), this.physicalViewSize()),
|
||||
physicalSpaceDstRect: new Rect(Vec2.zero, this.physicalViewSize()),
|
||||
configSpaceSrcRect: this.props.configSpaceViewportRect
|
||||
})
|
||||
})
|
||||
@@ -463,7 +463,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
|
||||
private onMouseLeave = (ev: MouseEvent) => {
|
||||
this.hoveredLabel = null
|
||||
this.props.setNodeHover(null, new Vec2())
|
||||
this.props.setNodeHover(null, Vec2.zero)
|
||||
this.renderCanvas()
|
||||
}
|
||||
|
||||
@@ -573,8 +573,8 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
super()
|
||||
this.state = {
|
||||
hoveredNode: null,
|
||||
configSpaceViewportRect: new Rect(),
|
||||
logicalSpaceMouse: new Vec2()
|
||||
configSpaceViewportRect: Rect.empty,
|
||||
logicalSpaceMouse: Vec2.zero
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
class ListNode<V> {
|
||||
prev: ListNode<V> | null = null
|
||||
next: ListNode<V> | null = null
|
||||
constructor(readonly data: V) { }
|
||||
}
|
||||
|
||||
export class List<V> {
|
||||
private head: ListNode<V> | null = null
|
||||
private tail: ListNode<V> | null = null
|
||||
private size: number = 0
|
||||
constructor() { }
|
||||
|
||||
getHead(): ListNode<V> | null { return this.head }
|
||||
getTail(): ListNode<V> | null { return this.tail }
|
||||
getSize(): number { return this.size }
|
||||
|
||||
append(node: ListNode<V>): void {
|
||||
if (!this.tail) {
|
||||
this.head = this.tail = node
|
||||
} else {
|
||||
this.tail.next = node
|
||||
node.prev = this.tail
|
||||
this.tail = node
|
||||
}
|
||||
this.size++
|
||||
}
|
||||
|
||||
prepend(node: ListNode<V>): ListNode<V> {
|
||||
if (!this.head) {
|
||||
this.head = this.tail = node
|
||||
} else {
|
||||
this.head.prev = node
|
||||
node.next = this.head
|
||||
this.head = node
|
||||
}
|
||||
this.size++
|
||||
return node
|
||||
}
|
||||
|
||||
pop(): ListNode<V> | null {
|
||||
if (!this.tail) {
|
||||
return null
|
||||
} else {
|
||||
const ret = this.tail
|
||||
if (ret.prev) {
|
||||
this.tail = ret.prev
|
||||
this.tail.next = null
|
||||
} else {
|
||||
this.head = this.tail = null
|
||||
}
|
||||
this.size--
|
||||
ret.prev = null
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
dequeue(): ListNode<V> | null {
|
||||
if (!this.head) {
|
||||
return null
|
||||
} else {
|
||||
const ret = this.head
|
||||
if (ret.next) {
|
||||
this.head = ret.next
|
||||
this.head.prev = null
|
||||
} else {
|
||||
this.head = this.tail = null
|
||||
}
|
||||
this.size--
|
||||
ret.next = null
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
remove(node: ListNode<V>): void {
|
||||
if (node.prev == null) {
|
||||
this.dequeue()
|
||||
} else if (node.next == null) {
|
||||
this.pop()
|
||||
} else {
|
||||
// Neither first nor last, should be safe to just link
|
||||
// neighbours.
|
||||
node.next.prev = node.prev
|
||||
node.prev.next = node.next
|
||||
node.next = null
|
||||
node.prev = null
|
||||
}
|
||||
this.size--
|
||||
}
|
||||
}
|
||||
|
||||
interface LRUCacheNode<K, V> {
|
||||
value: V
|
||||
listNode: ListNode<K>
|
||||
}
|
||||
|
||||
export class LRUCache<K, V> {
|
||||
private list = new List<K>()
|
||||
private map = new Map<K, LRUCacheNode<K, V>>()
|
||||
|
||||
constructor(private capacity: number) { }
|
||||
|
||||
has(key: K): boolean {
|
||||
return this.map.has(key)
|
||||
}
|
||||
|
||||
get(key: K): V | null {
|
||||
const node = this.map.get(key)
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
// Bring node to the front of the list
|
||||
this.list.remove(node.listNode)
|
||||
this.list.prepend(node.listNode)
|
||||
|
||||
return node ? node.value : null
|
||||
}
|
||||
|
||||
getSize() { return this.list.getSize() }
|
||||
|
||||
getCapacity() { return this.capacity }
|
||||
|
||||
insert(key: K, value: V) {
|
||||
const node = this.map.get(key)
|
||||
if (node) {
|
||||
this.list.remove(node.listNode)
|
||||
}
|
||||
// Evict old entries when out of capacity
|
||||
while (this.list.getSize() >= this.capacity) {
|
||||
this.map.delete(this.list.pop()!.data)
|
||||
}
|
||||
const listNode = this.list.prepend(new ListNode(key))
|
||||
this.map.set(key, { value, listNode })
|
||||
}
|
||||
|
||||
getOrInsert(key: K, f: (key: K) => V): V {
|
||||
let value = this.get(key)
|
||||
if (value == null) {
|
||||
value = f(key)
|
||||
this.insert(key, value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
removeLRU(): [K, V] | null {
|
||||
const oldest = this.list.pop()
|
||||
if (!oldest) return null
|
||||
const key = oldest.data
|
||||
const value = this.map.get(key)!.value
|
||||
this.map.delete(key)
|
||||
return [key, value]
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ export function clamp(x: number, minVal: number, maxVal: number) {
|
||||
}
|
||||
|
||||
export class Vec2 {
|
||||
constructor(readonly x = 0, readonly y = 0) {}
|
||||
constructor(readonly x: number, readonly y: number) {}
|
||||
withX(x: number) { return new Vec2(x, this.y) }
|
||||
withY(y: number) { return new Vec2(this.x, y) }
|
||||
|
||||
@@ -28,6 +28,9 @@ export class Vec2 {
|
||||
return new Vec2(Math.max(a.x, b.x), Math.max(a.y, b.y))
|
||||
}
|
||||
|
||||
static zero = new Vec2(0, 0)
|
||||
static unit = new Vec2(1, 1)
|
||||
|
||||
flatten(): [number, number] { return [this.x, this.y] }
|
||||
}
|
||||
|
||||
@@ -204,8 +207,8 @@ export class AffineTransform {
|
||||
|
||||
export class Rect {
|
||||
constructor(
|
||||
readonly origin = new Vec2(),
|
||||
readonly size = new Vec2()
|
||||
readonly origin: Vec2,
|
||||
readonly size: Vec2
|
||||
) {}
|
||||
|
||||
isEmpty() { return this.width() == 0 || this.height() == 0 }
|
||||
@@ -263,4 +266,8 @@ export class Rect {
|
||||
equals(other: Rect) {
|
||||
return this.origin.equals(other.origin) && this.size.equals(other.size)
|
||||
}
|
||||
|
||||
static empty = new Rect(Vec2.zero, Vec2.zero)
|
||||
static unit = new Rect(Vec2.zero, Vec2.unit)
|
||||
static NDC = new Rect(new Vec2(-1, -1), new Vec2(2, 2))
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"serve": "parcel -o dev.html",
|
||||
"serve": "parcel --no-hmr -o dev.html",
|
||||
"release": "tsc --noEmit && parcel build speedscope.tsx"
|
||||
},
|
||||
"author": "",
|
||||
|
||||
+90
-83
@@ -1,5 +1,5 @@
|
||||
import * as regl from 'regl'
|
||||
import { Vec2, Rect } from './math'
|
||||
import { Vec2, Rect, AffineTransform } from './math'
|
||||
|
||||
export class TextureRendererProps {
|
||||
texture: regl.Texture
|
||||
@@ -12,13 +12,16 @@ export class TextureRenderer {
|
||||
constructor(gl: regl.Instance) {
|
||||
this.command = gl({
|
||||
vert: `
|
||||
uniform mat3 uvTransform;
|
||||
uniform mat3 positionTransform;
|
||||
|
||||
attribute vec2 position;
|
||||
attribute vec2 uv;
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position, 0, 1);
|
||||
vUv = (uvTransform * vec3(uv, 1)).xy;
|
||||
gl_Position = vec4((positionTransform * vec3(position, 1)).xy, 0, 1);
|
||||
}
|
||||
`,
|
||||
frag: `
|
||||
@@ -45,48 +48,49 @@ export class TextureRenderer {
|
||||
// | /|
|
||||
// |/ |
|
||||
// 2 +--+ 3
|
||||
position: (context, props) => {
|
||||
const { dstRect } = props
|
||||
|
||||
const width = context.viewportWidth
|
||||
const height = context.viewportHeight
|
||||
|
||||
const left = 2 * (dstRect.left() / width) - 1
|
||||
const right = 2 * (dstRect.right() / width) - 1
|
||||
|
||||
const top = -(2 * (dstRect.top() / height) - 1)
|
||||
const bottom = -(2 * (dstRect.bottom() / height) - 1)
|
||||
|
||||
return [
|
||||
[left, top],
|
||||
[right, top],
|
||||
[left, bottom],
|
||||
[right, bottom]
|
||||
]
|
||||
},
|
||||
uv: (context, props) => {
|
||||
const { srcRect } = props
|
||||
|
||||
const width = props.texture.width
|
||||
const height = props.texture.height
|
||||
|
||||
const left = srcRect.left() / width
|
||||
const right = srcRect.right() / width
|
||||
|
||||
const top = 1 - srcRect.top() / height
|
||||
const bottom = 1 - srcRect.bottom() / height
|
||||
|
||||
return [
|
||||
[left, top],
|
||||
[right, top],
|
||||
[left, bottom],
|
||||
[right, bottom]
|
||||
]
|
||||
}
|
||||
position: gl.buffer([
|
||||
[-1, 1],
|
||||
[1, 1],
|
||||
[-1, -1],
|
||||
[1, -1]
|
||||
]),
|
||||
uv: gl.buffer([
|
||||
[0, 1],
|
||||
[1, 1],
|
||||
[0, 0],
|
||||
[1, 0]
|
||||
])
|
||||
},
|
||||
|
||||
uniforms: {
|
||||
texture: (context, props) => props.texture
|
||||
texture: (context, props) => props.texture,
|
||||
uvTransform: (context, props) => {
|
||||
const { srcRect, texture } = props
|
||||
const physicalToUV = AffineTransform.withTranslation(new Vec2(0, 1))
|
||||
.times(AffineTransform.withScale(new Vec2(1, -1)))
|
||||
.times(AffineTransform.betweenRects(
|
||||
new Rect(Vec2.zero, new Vec2(texture.width, texture.height)),
|
||||
Rect.unit
|
||||
))
|
||||
const uvRect = physicalToUV.transformRect(srcRect)
|
||||
return AffineTransform.betweenRects(
|
||||
Rect.unit,
|
||||
uvRect,
|
||||
).flatten()
|
||||
},
|
||||
positionTransform: (context, props) => {
|
||||
const { dstRect } = props
|
||||
|
||||
const viewportSize = new Vec2(context.viewportWidth, context.viewportHeight)
|
||||
|
||||
const physicalToNDC = AffineTransform.withScale(new Vec2(1, -1))
|
||||
.times(AffineTransform.betweenRects(
|
||||
new Rect(Vec2.zero, viewportSize),
|
||||
Rect.NDC)
|
||||
)
|
||||
const ndcRect = physicalToNDC.transformRect(dstRect)
|
||||
return AffineTransform.betweenRects(Rect.NDC, ndcRect).flatten()
|
||||
}
|
||||
},
|
||||
|
||||
primitive: 'triangle strip',
|
||||
@@ -95,11 +99,10 @@ export class TextureRenderer {
|
||||
})
|
||||
}
|
||||
|
||||
render(context: regl.Context, props: TextureRendererProps) {
|
||||
render(props: TextureRendererProps) {
|
||||
this.command(props)
|
||||
}
|
||||
|
||||
|
||||
resetStats() { return Object.assign(this.command.stats, { cpuTime: 0, gpuTime: 0, count: 0 }) }
|
||||
stats() { return this.command.stats }
|
||||
}
|
||||
@@ -116,6 +119,7 @@ export class TextureCachedRenderer<T> {
|
||||
private texture: regl.Texture
|
||||
private framebuffer: regl.Framebuffer
|
||||
private textureRenderer: TextureRenderer
|
||||
private withContext: regl.Command<{}>
|
||||
|
||||
constructor(private gl: regl.Instance, options: TextureCachedRendererOptions<T>) {
|
||||
this.renderUncached = options.render
|
||||
@@ -124,6 +128,7 @@ export class TextureCachedRenderer<T> {
|
||||
|
||||
this.texture = gl.texture(1, 1)
|
||||
this.framebuffer = gl.framebuffer({color: [this.texture]})
|
||||
this.withContext = gl({})
|
||||
}
|
||||
|
||||
private lastRenderProps: T | null = null
|
||||
@@ -133,47 +138,49 @@ export class TextureCachedRenderer<T> {
|
||||
this.dirty = true
|
||||
}
|
||||
|
||||
render(context: regl.Context, props: T) {
|
||||
let needsRender = false
|
||||
if (this.texture.width !== context.viewportWidth || this.texture.height !== context.viewportHeight) {
|
||||
this.texture({ width: context.viewportWidth, height: context.viewportHeight })
|
||||
this.framebuffer({ color: [this.texture] })
|
||||
needsRender = true
|
||||
} else if (this.lastRenderProps == null) {
|
||||
needsRender = true
|
||||
} else if (this.shouldUpdate(this.lastRenderProps, props)) {
|
||||
needsRender = true
|
||||
} else if (this.dirty) {
|
||||
needsRender = true
|
||||
}
|
||||
render(props: T) {
|
||||
this.withContext((context: regl.Context) => {
|
||||
let needsRender = false
|
||||
if (this.texture.width !== context.viewportWidth || this.texture.height !== context.viewportHeight) {
|
||||
this.texture({ width: context.viewportWidth, height: context.viewportHeight })
|
||||
this.framebuffer({ color: [this.texture] })
|
||||
needsRender = true
|
||||
} else if (this.lastRenderProps == null) {
|
||||
needsRender = true
|
||||
} else if (this.shouldUpdate(this.lastRenderProps, props)) {
|
||||
needsRender = true
|
||||
} else if (this.dirty) {
|
||||
needsRender = true
|
||||
}
|
||||
|
||||
if (needsRender) {
|
||||
// Render to texture
|
||||
this.gl({
|
||||
viewport: (context, props) => {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: context.viewportWidth,
|
||||
height: context.viewportHeight
|
||||
}
|
||||
},
|
||||
framebuffer: this.framebuffer
|
||||
})(() => {
|
||||
this.gl.clear({color: [0, 0, 0, 0]})
|
||||
this.renderUncached(props)
|
||||
if (needsRender) {
|
||||
// Render to texture
|
||||
this.gl({
|
||||
viewport: (context, props) => {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: context.viewportWidth,
|
||||
height: context.viewportHeight
|
||||
}
|
||||
},
|
||||
framebuffer: this.framebuffer
|
||||
})(() => {
|
||||
this.gl.clear({color: [0, 0, 0, 0]})
|
||||
this.renderUncached(props)
|
||||
})
|
||||
}
|
||||
|
||||
const glViewportRect = new Rect(Vec2.zero, new Vec2(context.viewportWidth, context.viewportHeight))
|
||||
|
||||
// Render from texture
|
||||
this.textureRenderer.render({
|
||||
texture: this.texture,
|
||||
srcRect: glViewportRect,
|
||||
dstRect: glViewportRect
|
||||
})
|
||||
}
|
||||
|
||||
const glViewportRect = new Rect(new Vec2(), new Vec2(context.viewportWidth, context.viewportHeight))
|
||||
|
||||
// Render from texture
|
||||
this.textureRenderer.render(context, {
|
||||
texture: this.texture,
|
||||
srcRect: glViewportRect,
|
||||
dstRect: glViewportRect
|
||||
this.lastRenderProps = props
|
||||
this.dirty = false
|
||||
})
|
||||
this.lastRenderProps = props
|
||||
this.dirty = false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user