Merge pull request #17 from alangpierce/set-up-prettier

Set up Prettier and run it on the whole codebase
This commit is contained in:
Jamie Wong
2018-04-14 16:56:31 -07:00
committed by GitHub
31 changed files with 1936 additions and 759 deletions

13
.eslintrc.js Normal file
View File

@@ -0,0 +1,13 @@
module.exports = {
parser: 'typescript-eslint-parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
plugins: ['prettier'],
rules: {
'prettier/prettier': 'error',
},
};

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
node_modules
.cache
dist
.idea

3
.travis.yml Normal file
View File

@@ -0,0 +1,3 @@
language: node_js
node_js:
- '9'

View File

@@ -5,21 +5,20 @@ import {ReloadableComponent, SerializedComponent} from './reloadable'
import {importFromBGFlameGraph} from './import/bg-flamegraph'
import {importFromStackprof} from './import/stackprof'
import {importFromChromeTimeline, importFromChromeCPUProfile} from './import/chrome'
import { FlamechartRenderer } from './flamechart-renderer'
import { CanvasContext } from './canvas-context'
import {FlamechartRenderer} from './flamechart-renderer'
import {CanvasContext} from './canvas-context'
import {Profile, Frame} from './profile'
import {Flamechart} from './flamechart'
import { FlamechartView } from './flamechart-view'
import { FontFamily, FontSize, Colors } from './style'
import {FlamechartView} from './flamechart-view'
import {FontFamily, FontSize, Colors} from './style'
declare function require(x: string): any
const exampleProfileURL = require('./sample/perf-vertx-stacks-01-collapsed-all.txt')
const enum SortOrder {
CHRONO,
LEFT_HEAVY
LEFT_HEAVY,
}
interface ApplicationState {
@@ -56,7 +55,7 @@ function importProfile(contents: string, fileName: string): Profile | null {
// Second pass: Try to guess what file format it is based on structure
try {
const parsed = JSON.parse(contents)
if (Array.isArray(parsed) && parsed[parsed.length - 1].name === "CpuProfile") {
if (Array.isArray(parsed) && parsed[parsed.length - 1].name === 'CpuProfile') {
console.log('Importing as Chrome CPU Profile')
return importFromChromeTimeline(parsed)
} else if ('nodes' in parsed && 'samples' in parsed && 'timeDeltas' in parsed) {
@@ -97,31 +96,51 @@ export class Toolbar extends ReloadableComponent<ToolbarProps, void> {
render() {
const help = (
<div className={css(style.toolbarTab)}>
<a href="https://github.com/jlfwong/speedscope#usage" className={css(style.noLinkStyle)} target="_blank">
<a
href="https://github.com/jlfwong/speedscope#usage"
className={css(style.noLinkStyle)}
target="_blank"
>
<span className={css(style.emoji)}></span>Help
</a>
</div>
)
if (!this.props.profile) {
return <div className={css(style.toolbar)}>
<div className={css(style.toolbarLeft)}>{help}</div>
🔬speedscope
</div>
return (
<div className={css(style.toolbar)}>
<div className={css(style.toolbarLeft)}>{help}</div>
🔬speedscope
</div>
)
}
return <div className={css(style.toolbar)}>
<div className={css(style.toolbarLeft)}>
<div className={css(style.toolbarTab, this.props.sortOrder === SortOrder.CHRONO && style.toolbarTabActive)} onClick={this.setTimeOrder}>
<span className={css(style.emoji)}>🕰</span>Time Order
return (
<div className={css(style.toolbar)}>
<div className={css(style.toolbarLeft)}>
<div
className={css(
style.toolbarTab,
this.props.sortOrder === SortOrder.CHRONO && style.toolbarTabActive,
)}
onClick={this.setTimeOrder}
>
<span className={css(style.emoji)}>🕰</span>Time Order
</div>
<div
className={css(
style.toolbarTab,
this.props.sortOrder === SortOrder.LEFT_HEAVY && style.toolbarTabActive,
)}
onClick={this.setLeftHeavyOrder}
>
<span className={css(style.emoji)}></span>Left Heavy
</div>
{help}
</div>
<div className={css(style.toolbarTab, this.props.sortOrder === SortOrder.LEFT_HEAVY && style.toolbarTabActive)} onClick={this.setLeftHeavyOrder}>
<span className={css(style.emoji)}></span>Left Heavy
</div>
{help}
{this.props.profile.getName()}
<div className={css(style.toolbarRight)}>🔬speedscope</div>
</div>
{this.props.profile.getName()}
<div className={css(style.toolbarRight)}>🔬speedscope</div>
</div>
)
}
}
@@ -145,7 +164,7 @@ export class GLCanvas extends ReloadableComponent<GLCanvasProps, void> {
private maybeResize() {
if (!this.canvas) return
let { width, height } = this.canvas.getBoundingClientRect()
let {width, height} = this.canvas.getBoundingClientRect()
width = Math.floor(width) * window.devicePixelRatio
height = Math.floor(height) * window.devicePixelRatio
@@ -188,7 +207,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
flamechartRenderer: null,
sortedFlamechart: null,
sortedFlamechartRenderer: null,
sortOrder: SortOrder.CHRONO
sortOrder: SortOrder.CHRONO,
}
}
@@ -201,11 +220,11 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
rehydrate(serialized: SerializedComponent<ApplicationState>) {
super.rehydrate(serialized)
const { flamechart, sortedFlamechart } = serialized.state
const {flamechart, sortedFlamechart} = serialized.state
if (this.canvasContext && flamechart && sortedFlamechart) {
this.setState({
flamechartRenderer: new FlamechartRenderer(this.canvasContext, flamechart),
sortedFlamechartRenderer: new FlamechartRenderer(this.canvasContext, sortedFlamechart)
sortedFlamechartRenderer: new FlamechartRenderer(this.canvasContext, sortedFlamechart),
})
}
}
@@ -216,7 +235,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
console.time('import')
const profile = importProfile(contents, fileName)
if (profile == null) {
this.setState({ loading: false })
this.setState({loading: false})
// TODO(jlfwong): Make this a nicer overlay
alert('Unrecognized format! See documentation about supported formats.')
return
@@ -248,7 +267,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
getTotalWeight: profile.getTotalWeight.bind(profile),
forEachCall: profile.forEachCall.bind(profile),
formatValue: profile.formatValue.bind(profile),
getColorBucketForFrame
getColorBucketForFrame,
})
const flamechartRenderer = new FlamechartRenderer(this.canvasContext, flamechart)
@@ -256,29 +275,32 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
getTotalWeight: profile.getTotalNonIdleWeight.bind(profile),
forEachCall: profile.forEachCallGrouped.bind(profile),
formatValue: profile.formatValue.bind(profile),
getColorBucketForFrame
getColorBucketForFrame,
})
const sortedFlamechartRenderer = new FlamechartRenderer(this.canvasContext, sortedFlamechart)
console.timeEnd('import')
console.time('first setState')
this.setState({
profile,
flamechart,
flamechartRenderer,
sortedFlamechart,
sortedFlamechartRenderer,
loading: false
}, () => {
console.timeEnd('first setState')
})
this.setState(
{
profile,
flamechart,
flamechartRenderer,
sortedFlamechart,
sortedFlamechartRenderer,
loading: false,
},
() => {
console.timeEnd('first setState')
},
)
}
loadFromFile(file: File) {
this.setState({ loading: true }, () => {
this.setState({loading: true}, () => {
requestAnimationFrame(() => {
const reader = new FileReader
const reader = new FileReader()
reader.addEventListener('loadend', () => {
this.loadFromString(file.name, reader.result)
})
@@ -288,11 +310,13 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
}
loadExample = () => {
this.setState({ loading: true })
this.setState({loading: true})
const filename = 'perf-vertx-stacks-01-collapsed-all.txt'
fetch(exampleProfileURL).then(resp => resp.text()).then(data => {
this.loadFromString(filename, data)
})
fetch(exampleProfileURL)
.then(resp => resp.text())
.then(data => {
this.loadFromString(filename, data)
})
}
onDrop = (ev: DragEvent) => {
@@ -310,11 +334,11 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
onWindowKeyPress = (ev: KeyboardEvent) => {
if (ev.key === '1') {
this.setState({
sortOrder: SortOrder.CHRONO
sortOrder: SortOrder.CHRONO,
})
} else if (ev.key === '2') {
this.setState({
sortOrder: SortOrder.LEFT_HEAVY
sortOrder: SortOrder.LEFT_HEAVY,
})
}
}
@@ -328,10 +352,10 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
}
flamechartView: FlamechartView | null = null
flamechartRef = (view: FlamechartView | null) => this.flamechartView = view
flamechartRef = (view: FlamechartView | null) => (this.flamechartView = view)
subcomponents() {
return {
flamechart: this.flamechartView
flamechart: this.flamechartView,
}
}
@@ -343,38 +367,75 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
}
renderLanding() {
return <div className={css(style.landingContainer)}>
<div className={css(style.landingMessage)}>
<p className={css(style.landingP)}>👋 Hi there! Welcome to 🔬speedscope, an interactive{' '}
<a className={css(style.link)} href="http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html">flamegraph</a> visualizer.
Use it to help you make your software faster.</p>
<p className={css(style.landingP)}>Drag and drop a profile file onto this window to get started,
click the big blue button below to browse for a profile to explore, or{' '}
<a className={css(style.link)} onClick={this.loadExample}>click here</a>{' '}
to load an example profile.</p>
return (
<div className={css(style.landingContainer)}>
<div className={css(style.landingMessage)}>
<p className={css(style.landingP)}>
👋 Hi there! Welcome to 🔬speedscope, an interactive{' '}
<a
className={css(style.link)}
href="http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html"
>
flamegraph
</a>{' '}
visualizer. Use it to help you make your software faster.
</p>
<p className={css(style.landingP)}>
Drag and drop a profile file onto this window to get started, click the big blue button
below to browse for a profile to explore, or{' '}
<a className={css(style.link)} onClick={this.loadExample}>
click here
</a>{' '}
to load an example profile.
</p>
<div className={css(style.browseButtonContainer)}>
<input type="file" name="file" id="file" onChange={this.onFileSelect} className={css(style.hide)} />
<label for="file" className={css(style.browseButton)}>Browse</label>
<div className={css(style.browseButtonContainer)}>
<input
type="file"
name="file"
id="file"
onChange={this.onFileSelect}
className={css(style.hide)}
/>
<label for="file" className={css(style.browseButton)}>
Browse
</label>
</div>
<p className={css(style.landingP)}>
See the{' '}
<a
className={css(style.link)}
href="https://github.com/jlfwong/speedscope#usage"
target="_blank"
>
documentation
</a>{' '}
for information about supported file formats, keyboard shortcuts, and how to navigate
around the profile.
</p>
<p className={css(style.landingP)}>
speedscope is open source. Please{' '}
<a
className={css(style.link)}
target="_blank"
href="https://github.com/jlfwong/speedscope/issues"
>
report any issues on GitHub
</a>.
</p>
</div>
<p className={css(style.landingP)}>See the <a className={css(style.link)}
href="https://github.com/jlfwong/speedscope#usage" target="_blank">documentation</a> for
information about supported file formats, keyboard shortcuts, and how
to navigate around the profile.</p>
<p className={css(style.landingP)}>speedscope is open source.
Please <a className={css(style.link)} target="_blank" href="https://github.com/jlfwong/speedscope/issues">report any issues on GitHub</a>.</p>
</div>
</div>
)
}
renderLoadingBar() {
return <div className={css(style.loading)}></div>
return <div className={css(style.loading)} />
}
setSortOrder = (sortOrder: SortOrder) => {
this.setState({ sortOrder })
this.setState({sortOrder})
}
private canvasContext: CanvasContext | null = null
@@ -383,23 +444,36 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
}
render() {
const {flamechart, flamechartRenderer, sortedFlamechart, sortedFlamechartRenderer, sortOrder, loading} = this.state
const {
flamechart,
flamechartRenderer,
sortedFlamechart,
sortedFlamechartRenderer,
sortOrder,
loading,
} = this.state
const flamechartToView = sortOrder == SortOrder.CHRONO ? flamechart : sortedFlamechart
const flamechartRendererToUse = sortOrder == SortOrder.CHRONO ? flamechartRenderer : sortedFlamechartRenderer
const flamechartRendererToUse =
sortOrder == SortOrder.CHRONO ? flamechartRenderer : sortedFlamechartRenderer
return <div onDrop={this.onDrop} onDragOver={this.onDragOver} className={css(style.root)}>
<GLCanvas setCanvasContext={this.setCanvasContext} />
<Toolbar setSortOrder={this.setSortOrder} {...this.state} />
{loading ?
this.renderLoadingBar() :
this.canvasContext && flamechartToView && flamechartRendererToUse ?
return (
<div onDrop={this.onDrop} onDragOver={this.onDragOver} className={css(style.root)}>
<GLCanvas setCanvasContext={this.setCanvasContext} />
<Toolbar setSortOrder={this.setSortOrder} {...this.state} />
{loading ? (
this.renderLoadingBar()
) : this.canvasContext && flamechartToView && flamechartRendererToUse ? (
<FlamechartView
canvasContext={this.canvasContext}
flamechartRenderer={flamechartRendererToUse}
ref={this.flamechartRef}
flamechart={flamechartToView} /> :
this.renderLanding()}
</div>
flamechart={flamechartToView}
/>
) : (
this.renderLanding()
)}
</div>
)
}
}
@@ -409,23 +483,25 @@ const style = StyleSheet.create({
width: '100vw',
height: '100vh',
zIndex: -1,
pointerEvents: 'none'
pointerEvents: 'none',
},
loading: {
height: 3,
marginBottom: -3,
background: Colors.DARK_BLUE,
transformOrigin: '0% 50%',
animationName: [{
from: {
transform: `scaleX(0)`
animationName: [
{
from: {
transform: `scaleX(0)`,
},
to: {
transform: `scaleX(1)`,
},
},
to: {
transform: `scaleX(1)`
}
}],
animationTimingFunction: "cubic-bezier(0, 1, 0, 1)",
animationDuration: "30s"
],
animationTimingFunction: 'cubic-bezier(0, 1, 0, 1)',
animationDuration: '30s',
},
root: {
width: '100vw',
@@ -435,27 +511,27 @@ const style = StyleSheet.create({
flexDirection: 'column',
position: 'relative',
fontFamily: FontFamily.MONOSPACE,
lineHeight: '20px'
lineHeight: '20px',
},
landingContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flex: 1
flex: 1,
},
landingMessage: {
maxWidth: 600
maxWidth: 600,
},
landingP: {
marginBottom: 16
marginBottom: 16,
},
hide: {
display: 'none'
display: 'none',
},
browseButtonContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
justifyContent: 'center',
},
browseButton: {
marginBottom: 16,
@@ -467,12 +543,12 @@ const style = StyleSheet.create({
lineHeight: '72px',
background: Colors.DARK_BLUE,
color: 'white',
cursor: 'pointer'
cursor: 'pointer',
},
link: {
color: Colors.LIGHT_BLUE,
cursor: 'pointer',
textDecoration: 'none'
textDecoration: 'none',
},
toolbar: {
height: 18,
@@ -482,7 +558,7 @@ const style = StyleSheet.create({
fontFamily: FontFamily.MONOSPACE,
fontSize: FontSize.TITLE,
lineHeight: '18px',
userSelect: 'none'
userSelect: 'none',
},
toolbarLeft: {
position: 'absolute',
@@ -513,24 +589,23 @@ const style = StyleSheet.create({
marginLeft: 2,
':hover': {
background: Colors.GRAY,
cursor: 'pointer'
}
cursor: 'pointer',
},
},
toolbarTabActive: {
background: Colors.LIGHT_BLUE,
':hover': {
background: Colors.LIGHT_BLUE
}
background: Colors.LIGHT_BLUE,
},
},
noLinkStyle: {
textDecoration: 'none',
color: 'inherit'
color: 'inherit',
},
emoji: {
display: 'inline-block',
verticalAlign: 'middle',
paddingTop: '0px',
marginRight: '0.3em'
}
marginRight: '0.3em',
},
})

View File

@@ -1,11 +1,25 @@
import regl from 'regl'
import { RectangleBatchRenderer, RectangleBatch, RectangleBatchRendererProps } from './rectangle-batch-renderer';
import { ViewportRectangleRenderer, ViewportRectangleRendererProps } from './overlay-rectangle-renderer';
import { TextureCachedRenderer, TextureRenderer, TextureRendererProps } from './texture-catched-renderer'
import { StatsPanel } from './stats'
import {
RectangleBatchRenderer,
RectangleBatch,
RectangleBatchRendererProps,
} from './rectangle-batch-renderer'
import {
ViewportRectangleRenderer,
ViewportRectangleRendererProps,
} from './overlay-rectangle-renderer'
import {
TextureCachedRenderer,
TextureRenderer,
TextureRendererProps,
} from './texture-catched-renderer'
import {StatsPanel} from './stats'
import { Vec2, Rect } from './math';
import { FlamechartColorPassRenderer, FlamechartColorPassRenderProps } from './flamechart-color-pass-renderer';
import {Vec2, Rect} from './math'
import {
FlamechartColorPassRenderer,
FlamechartColorPassRenderProps,
} from './flamechart-color-pass-renderer'
type FrameCallback = () => void
@@ -19,25 +33,25 @@ export class CanvasContext {
private viewportRectangleRenderer: ViewportRectangleRenderer
private textureRenderer: TextureRenderer
private flamechartColorPassRenderer: FlamechartColorPassRenderer
private setViewportScope: regl.Command<{ physicalBounds: Rect }>
private setViewportScope: regl.Command<{physicalBounds: Rect}>
private setScissor: regl.Command<{}>
constructor(canvas: HTMLCanvasElement) {
this.gl = regl({
canvas: canvas,
attributes: {
antialias: false
antialias: false,
},
extensions: ['ANGLE_instanced_arrays', 'WEBGL_depth_texture'],
optionalExtensions: ['EXT_disjoint_timer_query'],
profile: true
profile: true,
})
;(window as any)['CanvasContext'] = this
this.rectangleBatchRenderer = new RectangleBatchRenderer(this.gl)
this.viewportRectangleRenderer = new ViewportRectangleRenderer(this.gl)
this.textureRenderer = new TextureRenderer(this.gl)
this.flamechartColorPassRenderer = new FlamechartColorPassRenderer(this.gl)
this.setScissor = this.gl({ scissor: { enable: true } })
this.setScissor = this.gl({scissor: {enable: true}})
this.setViewportScope = this.gl<SetViewportScopeProps>({
context: {
viewportX: (context: regl.Context, props: SetViewportScopeProps) => {
@@ -45,29 +59,35 @@ export class CanvasContext {
},
viewportY: (context: regl.Context, props: SetViewportScopeProps) => {
return props.physicalBounds.top()
}
},
},
viewport: (context, props) => {
const { physicalBounds } = props
const {physicalBounds} = props
return {
x: physicalBounds.left(),
y: window.devicePixelRatio * window.innerHeight - physicalBounds.top() - physicalBounds.height(),
y:
window.devicePixelRatio * window.innerHeight -
physicalBounds.top() -
physicalBounds.height(),
width: physicalBounds.width(),
height: physicalBounds.height()
height: physicalBounds.height(),
}
},
scissor: (context, props) => {
const { physicalBounds } = props
const {physicalBounds} = props
return {
enable: true,
box: {
x: physicalBounds.left(),
y: window.devicePixelRatio * window.innerHeight - physicalBounds.top() - physicalBounds.height(),
y:
window.devicePixelRatio * window.innerHeight -
physicalBounds.top() -
physicalBounds.height(),
width: physicalBounds.width(),
height: physicalBounds.height()
}
height: physicalBounds.height(),
},
}
}
},
})
}
@@ -92,7 +112,7 @@ export class CanvasContext {
private onBeforeFrame = (context: regl.Context) => {
this.setScissor(() => {
this.gl.clear({ color: [0, 0, 0, 0] })
this.gl.clear({color: [0, 0, 0, 0]})
})
this.tickNeeded = false
@@ -139,11 +159,11 @@ export class CanvasContext {
}): TextureCachedRenderer<T> {
return new TextureCachedRenderer(this.gl, {
...options,
textureRenderer: this.textureRenderer
textureRenderer: this.textureRenderer,
})
}
drawViewportRectangle(props: ViewportRectangleRendererProps){
drawViewportRectangle(props: ViewportRectangleRendererProps) {
this.viewportRectangleRenderer.render(props)
}
@@ -151,16 +171,16 @@ export class CanvasContext {
const bounds = el.getBoundingClientRect()
const physicalBounds = new Rect(
new Vec2(bounds.left * window.devicePixelRatio, bounds.top * window.devicePixelRatio),
new Vec2(bounds.width * window.devicePixelRatio, bounds.height * window.devicePixelRatio)
new Vec2(bounds.width * window.devicePixelRatio, bounds.height * window.devicePixelRatio),
)
this.setViewportScope({ physicalBounds }, cb)
this.setViewportScope({physicalBounds}, cb)
}
setViewport(physicalBounds: Rect, cb: (context: regl.Context) => void) {
this.setViewportScope({ physicalBounds }, cb)
this.setViewportScope({physicalBounds}, cb)
}
getMaxTextureSize() {
return this.gl.limits.maxTextureSize
}
}
}

View File

@@ -1,23 +1,32 @@
import {Frame} from './profile'
export class Color {
constructor(readonly r: number = 0, readonly g: number = 0, readonly b: number = 0, readonly a: number = 1) {}
constructor(
readonly r: number = 0,
readonly g: number = 0,
readonly b: number = 0,
readonly a: number = 1,
) {}
static fromLumaChromaHue(L: number, C: number, H: number) {
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_luma/chroma/hue
const hPrime = H / 60
const X = C * (1 - Math.abs(hPrime % 2 - 1))
const [R1, G1, B1] = (
hPrime < 1 ? [C, X, 0] :
hPrime < 2 ? [X, C, 0] :
hPrime < 3 ? [0, C, X] :
hPrime < 4 ? [0, X, C] :
hPrime < 5 ? [X, 0, C] :
[C, 0, X]
)
const [R1, G1, B1] =
hPrime < 1
? [C, X, 0]
: hPrime < 2
? [X, C, 0]
: hPrime < 3
? [0, C, X]
: hPrime < 4
? [0, X, C]
: hPrime < 5
? [X, 0, C]
: [C, 0, X]
const m = L - (0.30 * R1 + 0.59 * G1 + 0.11 * B1)
const m = L - (0.3 * R1 + 0.59 * G1 + 0.11 * B1)
return new Color(R1 + m, G1 + m, B1 + m, 1.0)
}
@@ -64,11 +73,13 @@ export class FrameColorGenerator {
const x = 2 * fract(100.0 * ratio) - 1
const L = 0.85 - 0.1 * x
const C = 0.20 + 0.1 * x
const C = 0.2 + 0.1 * x
const H = 360 * ratio
this.frameToColor.set(frames[i], Color.fromLumaChromaHue(L, C, H))
}
}
getColorForFrame(f: Frame) { return this.frameToColor.get(f) || new Color() }
}
getColorForFrame(f: Frame) {
return this.frameToColor.get(f) || new Color()
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
import regl from 'regl'
import { Vec2, Rect, AffineTransform } from './math'
import {Vec2, Rect, AffineTransform} from './math'
export interface FlamechartColorPassRenderProps {
rectInfoTexture: regl.Texture
@@ -102,7 +102,7 @@ export class FlamechartColorPassRenderer {
`,
depth: {
enable: false
enable: false,
},
attributes: {
@@ -114,18 +114,8 @@ export class FlamechartColorPassRenderer {
// | /|
// |/ |
// 2 +--+ 3
position: gl.buffer([
[-1, 1],
[1, 1],
[-1, -1],
[1, -1]
]),
uv: gl.buffer([
[0, 1],
[1, 1],
[0, 0],
[1, 0]
])
position: gl.buffer([[-1, 1], [1, 1], [-1, -1], [1, -1]]),
uv: gl.buffer([[0, 1], [1, 1], [0, 0], [1, 0]]),
},
count: 4,
@@ -135,37 +125,36 @@ export class FlamechartColorPassRenderer {
uniforms: {
colorTexture: (context, props) => props.rectInfoTexture,
uvTransform: (context, props) => {
const { srcRect, rectInfoTexture } = props
const {srcRect, rectInfoTexture} = props
const physicalToUV = AffineTransform.withTranslation(new Vec2(0, 1))
.times(AffineTransform.withScale(new Vec2(1, -1)))
.times(AffineTransform.betweenRects(
.times(
AffineTransform.betweenRects(
new Rect(Vec2.zero, new Vec2(rectInfoTexture.width, rectInfoTexture.height)),
Rect.unit
))
Rect.unit,
),
)
const uvRect = physicalToUV.transformRect(srcRect)
return AffineTransform.betweenRects(
Rect.unit,
uvRect,
).flatten()
return AffineTransform.betweenRects(Rect.unit, uvRect).flatten()
},
renderOutlines: (context, props) => {
return props.renderOutlines ? 1.0 : 0.0
},
uvSpacePixelSize: (context, props) => {
return Vec2.unit.dividedByPointwise(new Vec2(props.rectInfoTexture.width, props.rectInfoTexture.height)).flatten()
return Vec2.unit
.dividedByPointwise(new Vec2(props.rectInfoTexture.width, props.rectInfoTexture.height))
.flatten()
},
positionTransform: (context, props) => {
const { dstRect } = 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 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()
}
},
},
})
}

View File

@@ -1,13 +1,13 @@
import { h, Component } from 'preact'
import { css } from 'aphrodite'
import { Flamechart } from './flamechart'
import { Rect, Vec2, AffineTransform, clamp } from './math'
import { FlamechartRenderer } from "./flamechart-renderer"
import { cachedMeasureTextWidth } from "./utils";
import { style, Sizes } from "./flamechart-style";
import { FontFamily, FontSize, Colors } from "./style"
import { CanvasContext } from './canvas-context'
import { TextureCachedRenderer } from './texture-catched-renderer'
import {h, Component} from 'preact'
import {css} from 'aphrodite'
import {Flamechart} from './flamechart'
import {Rect, Vec2, AffineTransform, clamp} from './math'
import {FlamechartRenderer} from './flamechart-renderer'
import {cachedMeasureTextWidth} from './utils'
import {style, Sizes} from './flamechart-style'
import {FontFamily, FontSize, Colors} from './style'
import {CanvasContext} from './canvas-context'
import {TextureCachedRenderer} from './texture-catched-renderer'
const DEVICE_PIXEL_RATIO = window.devicePixelRatio
@@ -24,7 +24,7 @@ interface FlamechartMinimapViewProps {
enum DraggingMode {
DRAW_NEW_VIEWPORT,
TRANSLATE_VIEWPORT
TRANSLATE_VIEWPORT,
}
export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps, {}> {
@@ -39,7 +39,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
private physicalViewSize() {
return new Vec2(
this.overlayCanvas ? this.overlayCanvas.width : 0,
this.overlayCanvas ? this.overlayCanvas.height : 0
this.overlayCanvas ? this.overlayCanvas.height : 0,
)
}
@@ -50,7 +50,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
private configSpaceSize() {
return new Vec2(
this.props.flamechart.getTotalWeight(),
this.props.flamechart.getLayers().length
this.props.flamechart.getLayers().length,
)
}
@@ -59,7 +59,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
return AffineTransform.betweenRects(
new Rect(new Vec2(0, 0), this.configSpaceSize()),
new Rect(minimapOrigin, this.physicalViewSize().minus(minimapOrigin))
new Rect(minimapOrigin, this.physicalViewSize().minus(minimapOrigin)),
)
}
@@ -90,20 +90,20 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
}
return false
},
render: (props) => {
render: props => {
this.props.flamechartRenderer.render({
physicalSpaceDstRect: new Rect(
this.minimapOrigin(),
this.physicalViewSize().minus(this.minimapOrigin())
this.physicalViewSize().minus(this.minimapOrigin()),
),
configSpaceSrcRect: new Rect(new Vec2(0, 0), this.configSpaceSize()),
renderOutlines: false
renderOutlines: false,
})
}
},
})
}
this.props.canvasContext.renderInto(this.container, (context) => {
this.props.canvasContext.renderInto(this.container, context => {
// TODO(jlfwong): Switch back to the texture cached renderer once I figure out
// how to resize a framebuffer while another framebuffer is active. It seems
// to crash regl. I should submit a reduced repro case and hopefully get it fixed?
@@ -116,9 +116,9 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
configSpaceSrcRect: new Rect(new Vec2(0, 0), this.configSpaceSize()),
physicalSpaceDstRect: new Rect(
this.minimapOrigin(),
this.physicalViewSize().minus(this.minimapOrigin())
this.physicalViewSize().minus(this.minimapOrigin()),
),
renderOutlines: false
renderOutlines: false,
})
this.props.canvasContext.drawViewportRectangle({
configSpaceViewportRect: this.props.configSpaceViewportRect,
@@ -147,14 +147,18 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
// 1eN, 2eN, or 5eN for some N
// Ideally, we want an interval every 100 logical screen pixels
const logicalToConfig = (this.configSpaceToPhysicalViewSpace().inverted() || new AffineTransform()).times(this.logicalToPhysicalViewSpace())
const logicalToConfig = (
this.configSpaceToPhysicalViewSpace().inverted() || new AffineTransform()
).times(this.logicalToPhysicalViewSpace())
const targetInterval = logicalToConfig.transformVector(new Vec2(200, 1)).x
const physicalViewSpaceFrameHeight = Sizes.FRAME_HEIGHT * DEVICE_PIXEL_RATIO
const physicalViewSpaceFontSize = FontSize.LABEL * DEVICE_PIXEL_RATIO
const LABEL_PADDING_PX = (physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${FontFamily.MONOSPACE}`
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${
FontFamily.MONOSPACE
}`
ctx.textBaseline = 'top'
const minInterval = Math.pow(10, Math.floor(Math.log10(targetInterval)))
@@ -205,11 +209,13 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
private resizeOverlayCanvasIfNeeded() {
if (!this.overlayCanvas) return
let {width, height} = this.overlayCanvas.getBoundingClientRect()
{/*
{
/*
We render text at a higher resolution then scale down to
ensure we're rendering at 1:1 device pixel ratio.
This ensures our text is rendered crisply.
*/}
*/
}
width = Math.floor(width)
height = Math.floor(height)
@@ -219,8 +225,8 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
const scaledWidth = width * DEVICE_PIXEL_RATIO
const scaledHeight = height * DEVICE_PIXEL_RATIO
if (scaledWidth === this.overlayCanvas.width &&
scaledHeight === this.overlayCanvas.height) return
if (scaledWidth === this.overlayCanvas.width && scaledHeight === this.overlayCanvas.height)
return
this.overlayCanvas.width = scaledWidth
this.overlayCanvas.height = scaledHeight
@@ -252,7 +258,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
private maybeClearInteractionLock = () => {
if (this.interactionLock) {
if (!this.frameHadWheelEvent) {
this.framesWithoutWheelEvents++;
this.framesWithoutWheelEvents++
if (this.framesWithoutWheelEvents >= 2) {
this.interactionLock = null
this.framesWithoutWheelEvents = 0
@@ -275,11 +281,10 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
private zoom(multiplier: number) {
this.interactionLock = 'zoom'
const configSpaceViewport = this.props.configSpaceViewportRect
const configSpaceCenter = configSpaceViewport.origin.plus(configSpaceViewport.size.times(1/2))
const configSpaceCenter = configSpaceViewport.origin.plus(configSpaceViewport.size.times(1 / 2))
if (!configSpaceCenter) return
const zoomTransform = AffineTransform
.withTranslation(configSpaceCenter.times(-1))
const zoomTransform = AffineTransform.withTranslation(configSpaceCenter.times(-1))
.scaledBy(new Vec2(multiplier, 1))
.translatedBy(configSpaceCenter)
@@ -294,13 +299,13 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
const isZoom = ev.metaKey || ev.ctrlKey
if (isZoom && this.interactionLock !== 'pan') {
let multiplier = 1 + (ev.deltaY / 100)
let multiplier = 1 + ev.deltaY / 100
// On Chrome & Firefox, pinch-to-zoom maps to
// WheelEvent + Ctrl Key. We'll accelerate it in
// this case, since it feels a bit sluggish otherwise.
if (ev.ctrlKey) {
multiplier = 1 + (ev.deltaY / 40)
multiplier = 1 + ev.deltaY / 40
}
multiplier = clamp(multiplier, 0.1, 10.0)
@@ -310,13 +315,16 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
this.pan(new Vec2(ev.deltaX, ev.deltaY))
}
this.renderCanvas()
}
private configSpaceMouse(ev: MouseEvent): Vec2 | null {
const logicalSpaceMouse = this.windowToLogicalViewSpace().transformPosition(new Vec2(ev.clientX, ev.clientY))
const physicalSpaceMouse = this.logicalToPhysicalViewSpace().transformPosition(logicalSpaceMouse)
const logicalSpaceMouse = this.windowToLogicalViewSpace().transformPosition(
new Vec2(ev.clientX, ev.clientY),
)
const physicalSpaceMouse = this.logicalToPhysicalViewSpace().transformPosition(
logicalSpaceMouse,
)
return this.configSpaceToPhysicalViewSpace().inverseTransformPosition(physicalSpaceMouse)
}
@@ -331,7 +339,9 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
// If dragging starting inside the viewport rectangle,
// we'll move the existing viewport
this.draggingMode = DraggingMode.TRANSLATE_VIEWPORT
this.dragConfigSpaceViewportOffset = configSpaceMouse.minus(this.props.configSpaceViewportRect.origin)
this.dragConfigSpaceViewportOffset = configSpaceMouse.minus(
this.props.configSpaceViewportRect.origin,
)
} else {
// If dragging starts outside the the viewport rectangle,
// we'll start drawing a new viewport
@@ -353,7 +363,9 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
this.updateCursor(configSpaceMouse)
// Clamp the mouse position to avoid weird behavior when outside the canvas bounds
configSpaceMouse = new Rect(new Vec2(0, 0), this.configSpaceSize()).closestPointTo(configSpaceMouse)
configSpaceMouse = new Rect(new Vec2(0, 0), this.configSpaceSize()).closestPointTo(
configSpaceMouse,
)
if (this.draggingMode === DraggingMode.DRAW_NEW_VIEWPORT) {
const configStart = this.dragStartConfigSpaceMouse
@@ -366,16 +378,15 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
const width = right - left
const height = this.props.configSpaceViewportRect.height()
this.props.setConfigSpaceViewportRect(new Rect(
new Vec2(left, configEnd.y - height / 2),
new Vec2(width, height)
))
this.props.setConfigSpaceViewportRect(
new Rect(new Vec2(left, configEnd.y - height / 2), new Vec2(width, height)),
)
} else if (this.draggingMode === DraggingMode.TRANSLATE_VIEWPORT) {
if (!this.dragConfigSpaceViewportOffset) return
const newOrigin = configSpaceMouse.minus(this.dragConfigSpaceViewportOffset)
this.props.setConfigSpaceViewportRect(
this.props.configSpaceViewportRect.withOrigin(newOrigin)
this.props.configSpaceViewportRect.withOrigin(newOrigin),
)
}
}
@@ -428,11 +439,9 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
onWheel={this.onWheel}
onMouseDown={this.onMouseDown}
onMouseMove={this.onMouseMove}
className={css(style.minimap, style.vbox)} >
<canvas
width={1} height={1}
ref={this.overlayCanvasRef}
className={css(style.fill)} />
className={css(style.minimap, style.vbox)}
>
<canvas width={1} height={1} ref={this.overlayCanvasRef} className={css(style.fill)} />
</div>
)
}

View File

@@ -1,11 +1,11 @@
import regl from 'regl'
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 {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'
const MAX_BATCH_SIZE = 10000
@@ -23,18 +23,24 @@ class RowAtlas<K> {
wrapS: 'clamp',
wrapT: 'clamp',
})
this.framebuffer = canvasContext.gl.framebuffer({ color: [this.texture] })
this.framebuffer = canvasContext.gl.framebuffer({color: [this.texture]})
this.rowCache = new LRUCache(this.texture.height)
this.renderToFramebuffer = canvasContext.gl({
framebuffer: this.framebuffer
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 }
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()) {
@@ -50,10 +56,7 @@ class RowAtlas<K> {
}
}
writeToAtlasIfNeeded(
keys: K[],
render: (textureDstRect: Rect, key: K) => void
) {
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)
@@ -64,14 +67,11 @@ class RowAtlas<K> {
// 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)
)
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
physicalSpaceDstRect: textureRect,
})
render(textureRect, key)
}
@@ -84,17 +84,14 @@ class RowAtlas<K> {
return false
}
const textureRect = new Rect(
new Vec2(0, row),
new Vec2(this.texture.width, 1)
)
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
dstRect: dstRect,
})
return true
}
@@ -113,16 +110,26 @@ class RangeTreeLeafNode implements RangeTreeNode {
constructor(
private batch: RectangleBatch,
private bounds: Rect,
private numPrecedingRectanglesInRow: number
private numPrecedingRectanglesInRow: number,
) {
batch.uploadToGPU()
}
getBatch() { return this.batch }
getBounds() { return this.bounds }
getRectCount() { return this.batch.getRectCount() }
getChildren() { return this.children }
getParity() { return this.numPrecedingRectanglesInRow % 2 }
getBatch() {
return this.batch
}
getBounds() {
return this.bounds
}
getRectCount() {
return this.batch.getRectCount()
}
getChildren() {
return this.children
}
getParity() {
return this.numPrecedingRectanglesInRow % 2
}
forEachLeafNodeWithinBounds(configSpaceBounds: Rect, cb: (leaf: RangeTreeLeafNode) => void) {
if (!this.bounds.hasIntersectionWith(configSpaceBounds)) return
cb(this)
@@ -134,7 +141,7 @@ class RangeTreeInteriorNode implements RangeTreeNode {
private bounds: Rect
constructor(private children: RangeTreeNode[]) {
if (children.length === 0) {
throw new Error("Empty interior node")
throw new Error('Empty interior node')
}
let minLeft = Infinity
let maxRight = -Infinity
@@ -150,13 +157,19 @@ class RangeTreeInteriorNode implements RangeTreeNode {
}
this.bounds = new Rect(
new Vec2(minLeft, minTop),
new Vec2(maxRight - minLeft, maxBottom - minTop)
new Vec2(maxRight - minLeft, maxBottom - minTop),
)
}
getBounds() { return this.bounds }
getRectCount() { return this.rectCount }
getChildren() { return this.children }
getBounds() {
return this.bounds
}
getRectCount() {
return this.rectCount
}
getChildren() {
return this.children
}
forEachLeafNodeWithinBounds(configSpaceBounds: Rect, cb: (leaf: RangeTreeLeafNode) => void) {
if (!this.bounds.hasIntersectionWith(configSpaceBounds)) return
@@ -202,17 +215,20 @@ export class FlamechartRenderer {
for (let i = 0; i < layer.length; i++) {
const frame = layer[i]
if (batch.getRectCount() >= MAX_BATCH_SIZE) {
leafNodes.push(new RangeTreeLeafNode(batch, new Rect(
new Vec2(minLeft, stackDepth),
new Vec2(maxRight - minLeft, 1)
), rectCount))
leafNodes.push(
new RangeTreeLeafNode(
batch,
new Rect(new Vec2(minLeft, stackDepth), new Vec2(maxRight - minLeft, 1)),
rectCount,
),
)
minLeft = Infinity
maxRight = -Infinity
batch = canvasContext.createRectangleBatch()
}
const configSpaceBounds = new Rect(
new Vec2(frame.start, y),
new Vec2(frame.end - frame.start, 1)
new Vec2(frame.end - frame.start, 1),
)
minLeft = Math.min(minLeft, configSpaceBounds.left())
maxRight = Math.max(maxRight, configSpaceBounds.right())
@@ -225,24 +241,27 @@ export class FlamechartRenderer {
const color = new Color(
(1 + i % 255) / 256,
(1 + stackDepth % 255) / 256,
(1 + this.flamechart.getColorBucketForFrame(frame.node.frame)) / 256
(1 + this.flamechart.getColorBucketForFrame(frame.node.frame)) / 256,
)
batch.addRect(configSpaceBounds, color)
rectCount++
}
if (batch.getRectCount() > 0) {
leafNodes.push(new RangeTreeLeafNode(batch, new Rect(
new Vec2(minLeft, stackDepth),
new Vec2(maxRight - minLeft, 1)
), rectCount))
leafNodes.push(
new RangeTreeLeafNode(
batch,
new Rect(new Vec2(minLeft, stackDepth), new Vec2(maxRight - minLeft, 1)),
rectCount,
),
)
}
// TODO(jlfwong): Making this into a binary tree
// range than a tree of always-height-two might make this run faster
this.layers.push(new RangeTreeInteriorNode(leafNodes))
}
this.rectInfoTexture = this.canvasContext.gl.texture({ width: 1, height: 1 })
this.rectInfoTexture = this.canvasContext.gl.texture({width: 1, height: 1})
this.framebuffer = this.canvasContext.gl.framebuffer({
color: [this.rectInfoTexture],
})
@@ -255,21 +274,18 @@ export class FlamechartRenderer {
}
configSpaceBoundsForKey(key: FlamechartRowAtlasKey): Rect {
const { stackDepth, zoomLevel, index } = key
const {stackDepth, zoomLevel, index} = key
const configSpaceContentWidth = this.flamechart.getTotalWeight()
const width = configSpaceContentWidth / Math.pow(2, zoomLevel)
return new Rect(
new Vec2(width * index, stackDepth),
new Vec2(width, 1)
)
return new Rect(new Vec2(width * index, stackDepth), new Vec2(width, 1))
}
render(props: FlamechartRendererProps) {
const { configSpaceSrcRect, physicalSpaceDstRect } = props
const {configSpaceSrcRect, physicalSpaceDstRect} = props
const atlasKeysToRender: { stackDepth: number, zoomLevel: number, index: number }[] = []
const atlasKeysToRender: {stackDepth: number; zoomLevel: number; index: number}[] = []
// 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
@@ -282,7 +298,7 @@ export class FlamechartRenderer {
let zoomLevel = 0
while (true) {
const configSpaceBounds = this.configSpaceBoundsForKey({ stackDepth: 0, zoomLevel, index: 0 })
const configSpaceBounds = this.configSpaceBoundsForKey({stackDepth: 0, zoomLevel, index: 0})
const physicalBounds = configToPhysical.transformRect(configSpaceBounds)
if (physicalBounds.width() < this.rowAtlas.getResolution()) {
break
@@ -295,12 +311,16 @@ export class FlamechartRenderer {
const configSpaceContentWidth = this.flamechart.getTotalWeight()
const numAtlasEntriesPerLayer = Math.pow(2, zoomLevel)
const left = Math.floor(numAtlasEntriesPerLayer * configSpaceSrcRect.left() / configSpaceContentWidth)
const right = Math.ceil(numAtlasEntriesPerLayer * configSpaceSrcRect.right() / configSpaceContentWidth)
const left = Math.floor(
numAtlasEntriesPerLayer * configSpaceSrcRect.left() / configSpaceContentWidth,
)
const right = Math.ceil(
numAtlasEntriesPerLayer * configSpaceSrcRect.right() / configSpaceContentWidth,
)
for (let stackDepth = top; stackDepth < bottom; stackDepth++) {
for (let index = left; index <= right; index++) {
const key = this.getOrInsertKey({ stackDepth, zoomLevel, index })
const key = this.getOrInsertKey({stackDepth, zoomLevel, index})
const configSpaceBounds = this.configSpaceBoundsForKey(key)
if (!configSpaceBounds.hasIntersectionWith(configSpaceSrcRect)) continue
atlasKeysToRender.push(key)
@@ -314,13 +334,13 @@ export class FlamechartRenderer {
// Fill the cache
this.rowAtlas.writeToAtlasIfNeeded(keysToRenderCached, (textureDstRect, key) => {
const configSpaceBounds = this.configSpaceBoundsForKey(key)
this.layers[key.stackDepth].forEachLeafNodeWithinBounds(configSpaceBounds, (leaf) => {
this.layers[key.stackDepth].forEachLeafNodeWithinBounds(configSpaceBounds, leaf => {
this.canvasContext.drawRectangleBatch({
batch: leaf.getBatch(),
configSpaceSrcRect: configSpaceBounds,
physicalSpaceDstRect: textureDstRect,
parityMin: key.stackDepth % 2 == 0 ? 2 : 0,
parityOffset: leaf.getParity()
parityOffset: leaf.getParity(),
})
})
})
@@ -328,7 +348,10 @@ export class FlamechartRenderer {
this.framebuffer.resize(physicalSpaceDstRect.width(), physicalSpaceDstRect.height())
this.framebuffer.use(context => {
this.canvasContext.gl.clear({color: [0, 0, 0, 0]})
const viewportRect = new Rect(Vec2.zero, new Vec2(context.viewportWidth, context.viewportHeight))
const viewportRect = new Rect(
Vec2.zero,
new Vec2(context.viewportWidth, context.viewportHeight),
)
const configToViewport = AffineTransform.betweenRects(configSpaceSrcRect, viewportRect)
@@ -342,13 +365,13 @@ export class FlamechartRenderer {
for (let key of keysToRenderUncached) {
const configSpaceBounds = this.configSpaceBoundsForKey(key)
const physicalBounds = configToViewport.transformRect(configSpaceBounds)
this.layers[key.stackDepth].forEachLeafNodeWithinBounds(configSpaceBounds, (leaf) => {
this.layers[key.stackDepth].forEachLeafNodeWithinBounds(configSpaceBounds, leaf => {
this.canvasContext.drawRectangleBatch({
batch: leaf.getBatch(),
configSpaceSrcRect,
physicalSpaceDstRect: physicalBounds,
parityMin: key.stackDepth % 2 == 0 ? 2 : 0,
parityOffset: leaf.getParity()
parityOffset: leaf.getParity(),
})
})
}
@@ -356,9 +379,12 @@ export class FlamechartRenderer {
this.canvasContext.drawFlamechartColorPass({
rectInfoTexture: this.rectInfoTexture,
srcRect: new Rect(Vec2.zero, new Vec2(this.rectInfoTexture.width, this.rectInfoTexture.height)),
srcRect: new Rect(
Vec2.zero,
new Vec2(this.rectInfoTexture.width, this.rectInfoTexture.height),
),
dstRect: physicalSpaceDstRect,
renderOutlines: props.renderOutlines
renderOutlines: props.renderOutlines,
})
// Overlay the atlas on top of the canvas for debugging
@@ -370,4 +396,4 @@ export class FlamechartRenderer {
})
*/
}
}
}

View File

@@ -1,5 +1,5 @@
import {StyleSheet} from 'aphrodite'
import { FontFamily, FontSize, Colors } from './style'
import {FontFamily, FontSize, Colors} from './style'
const HOVERTIP_PADDING = 2
@@ -22,7 +22,7 @@ export const style = StyleSheet.create({
pointerEvents: 'none',
userSelect: 'none',
fontSize: FontSize.LABEL,
fontFamily: FontFamily.MONOSPACE
fontFamily: FontFamily.MONOSPACE,
},
hoverTipRow: {
textOverflow: 'ellipsis',
@@ -33,10 +33,10 @@ export const style = StyleSheet.create({
maxWidth: Sizes.TOOLTIP_WIDTH_MAX,
},
hoverCount: {
color: '#6FCF97'
color: '#6FCF97',
},
clip: {
overflow: 'hidden'
overflow: 'hidden',
},
vbox: {
display: 'flex',
@@ -48,13 +48,13 @@ export const style = StyleSheet.create({
height: '100%',
position: 'absolute',
left: 0,
top: 0
top: 0,
},
minimap: {
height: Sizes.MINIMAP_HEIGHT,
borderBottom: `${Sizes.SEPARATOR_HEIGHT}px solid ${Colors.MEDIUM_GRAY}`
borderBottom: `${Sizes.SEPARATOR_HEIGHT}px solid ${Colors.MEDIUM_GRAY}`,
},
panZoomView: {
flex: 1
flex: 1,
},
});
})

View File

@@ -2,24 +2,30 @@ import {h} from 'preact'
import {css} from 'aphrodite'
import {ReloadableComponent} from './reloadable'
import { CallTreeNode } from './profile'
import { Flamechart, FlamechartFrame } from './flamechart'
import {CallTreeNode} from './profile'
import {Flamechart, FlamechartFrame} from './flamechart'
import { Rect, Vec2, AffineTransform, clamp } from './math'
import { cachedMeasureTextWidth } from "./utils";
import { FlamechartMinimapView } from "./flamechart-minimap-view"
import {Rect, Vec2, AffineTransform, clamp} from './math'
import {cachedMeasureTextWidth} from './utils'
import {FlamechartMinimapView} from './flamechart-minimap-view'
import { style, Sizes } from './flamechart-style'
import { FontSize, FontFamily, Colors } from './style'
import { CanvasContext } from './canvas-context'
import { FlamechartRenderer } from './flamechart-renderer'
import {style, Sizes} from './flamechart-style'
import {FontSize, FontFamily, Colors} from './style'
import {CanvasContext} from './canvas-context'
import {FlamechartRenderer} from './flamechart-renderer'
interface FlamechartFrameLabel {
configSpaceBounds: Rect
node: CallTreeNode
}
function binarySearch(lo: number, hi: number, f: (val: number) => number, target: number, targetRangeSize = 1): [number, number] {
function binarySearch(
lo: number,
hi: number,
f: (val: number) => number,
target: number,
targetRangeSize = 1,
): [number, number] {
console.assert(!isNaN(targetRangeSize) && !isNaN(target))
while (true) {
if (hi - lo <= targetRangeSize) return [lo, hi]
@@ -41,9 +47,14 @@ function buildTrimmedText(text: string, length: number) {
function trimTextMid(ctx: CanvasRenderingContext2D, text: string, maxWidth: number) {
if (cachedMeasureTextWidth(ctx, text) <= maxWidth) return text
const [lo,] = binarySearch(0, text.length, (n) => {
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, n))
}, maxWidth)
const [lo] = binarySearch(
0,
text.length,
n => {
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, n))
},
maxWidth,
)
return buildTrimmedText(text, lo)
}
@@ -108,14 +119,14 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
private configSpaceSize() {
return new Vec2(
this.props.flamechart.getTotalWeight(),
this.props.flamechart.getLayers().length
this.props.flamechart.getLayers().length,
)
}
private physicalViewSize() {
return new Vec2(
this.overlayCanvas ? this.overlayCanvas.width : 0,
this.overlayCanvas ? this.overlayCanvas.height : 0
this.overlayCanvas ? this.overlayCanvas.height : 0,
)
}
@@ -124,7 +135,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
private configSpaceToPhysicalViewSpace() {
return AffineTransform.betweenRects(
this.props.configSpaceViewportRect,
new Rect(new Vec2(0, 0), this.physicalViewSize())
new Rect(new Vec2(0, 0), this.physicalViewSize()),
)
}
@@ -135,11 +146,13 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
private resizeOverlayCanvasIfNeeded() {
if (!this.overlayCanvas) return
let {width, height} = this.overlayCanvas.getBoundingClientRect()
{/*
{
/*
We render text at a higher resolution then scale down to
ensure we're rendering at 1:1 device pixel ratio.
This ensures our text is rendered crisply.
*/}
*/
}
width = Math.floor(width)
height = Math.floor(height)
@@ -149,8 +162,8 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
const scaledWidth = width * DEVICE_PIXEL_RATIO
const scaledHeight = height * DEVICE_PIXEL_RATIO
if (scaledWidth === this.overlayCanvas.width &&
scaledHeight === this.overlayCanvas.height) return
if (scaledWidth === this.overlayCanvas.width && scaledHeight === this.overlayCanvas.height)
return
this.overlayCanvas.width = scaledWidth
this.overlayCanvas.height = scaledHeight
@@ -177,27 +190,30 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
if (this.hoveredLabel) {
const physicalViewBounds = configToPhysical.transformRect(this.hoveredLabel.configSpaceBounds)
ctx.strokeRect(
Math.floor(physicalViewBounds.left()), Math.floor(physicalViewBounds.top()),
Math.floor(physicalViewBounds.width()), Math.floor(physicalViewBounds.height())
Math.floor(physicalViewBounds.left()),
Math.floor(physicalViewBounds.top()),
Math.floor(physicalViewBounds.width()),
Math.floor(physicalViewBounds.height()),
)
}
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${FontFamily.MONOSPACE}`
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${
FontFamily.MONOSPACE
}`
ctx.fillStyle = Colors.DARK_GRAY
ctx.textBaseline = 'top'
const minWidthToRender = cachedMeasureTextWidth(ctx, 'M' + ELLIPSIS + 'M')
const minConfigSpaceWidthToRender = (configToPhysical.inverseTransformVector(new Vec2(minWidthToRender, 0)) || new Vec2(0, 0)).x
const minConfigSpaceWidthToRender = (
configToPhysical.inverseTransformVector(new Vec2(minWidthToRender, 0)) || new Vec2(0, 0)
).x
const LABEL_PADDING_PX = (physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2
const PADDING_OFFSET = new Vec2(LABEL_PADDING_PX, LABEL_PADDING_PX)
const SIZE_OFFSET = new Vec2(2 * LABEL_PADDING_PX, 2 * LABEL_PADDING_PX)
const renderFrameLabelAndChildren = (frame: FlamechartFrame, depth = 0) => {
const width = frame.end - frame.start
const configSpaceBounds = new Rect(
new Vec2(frame.start, depth),
new Vec2(width, 1)
)
const configSpaceBounds = new Rect(new Vec2(frame.start, depth), new Vec2(width, 1))
if (width < minConfigSpaceWidthToRender) return
if (configSpaceBounds.left() > this.props.configSpaceViewportRect.right()) return
@@ -210,11 +226,16 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
if (physicalLabelBounds.left() < 0) {
physicalLabelBounds = physicalLabelBounds
.withOrigin(physicalLabelBounds.origin.withX(0))
.withSize(physicalLabelBounds.size.withX(physicalLabelBounds.size.x + physicalLabelBounds.left()))
.withSize(
physicalLabelBounds.size.withX(
physicalLabelBounds.size.x + physicalLabelBounds.left(),
),
)
}
if (physicalLabelBounds.right() > physicalViewSize.x) {
physicalLabelBounds = physicalLabelBounds
.withSize(physicalLabelBounds.size.withX(physicalViewSize.x - physicalLabelBounds.left()))
physicalLabelBounds = physicalLabelBounds.withSize(
physicalLabelBounds.size.withX(physicalViewSize.x - physicalLabelBounds.left()),
)
}
physicalLabelBounds = physicalLabelBounds
@@ -230,7 +251,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
}
}
for (let frame of (this.props.flamechart.getLayers()[0] || [])) {
for (let frame of this.props.flamechart.getLayers()[0] || []) {
renderFrameLabelAndChildren(frame)
}
@@ -241,7 +262,9 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
// 1eN, 2eN, or 5eN for some N
// Ideally, we want an interval every 100 logical screen pixels
const logicalToConfig = (this.configSpaceToPhysicalViewSpace().inverted() || new AffineTransform()).times(this.logicalToPhysicalViewSpace())
const logicalToConfig = (
this.configSpaceToPhysicalViewSpace().inverted() || new AffineTransform()
).times(this.logicalToPhysicalViewSpace())
const targetInterval = logicalToConfig.transformVector(new Vec2(200, 1)).x
const minInterval = Math.pow(10, Math.floor(Math.log10(targetInterval)))
@@ -274,25 +297,28 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
private updateConfigSpaceViewport(windowResized = false) {
if (!this.container) return
const bounds = this.container.getBoundingClientRect()
const { width, height } = bounds
const {width, height} = bounds
// Still initializing: don't resize yet
if (width < 2 || height < 2) return
if (this.lastBounds == null) {
this.setConfigSpaceViewportRect(new Rect(
new Vec2(0, -1),
new Vec2(this.configSpaceSize().x, height / this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT)
))
this.setConfigSpaceViewportRect(
new Rect(
new Vec2(0, -1),
new Vec2(this.configSpaceSize().x, height / this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT),
),
)
} else if (windowResized) {
// Resize the viewport rectangle to match the window size aspect
// ratio.
this.setConfigSpaceViewportRect(this.props.configSpaceViewportRect.withSize(
this.props.configSpaceViewportRect.size.timesPointwise(new Vec2(
width / this.lastBounds.width,
height / this.lastBounds.height
))
))
this.setConfigSpaceViewportRect(
this.props.configSpaceViewportRect.withSize(
this.props.configSpaceViewportRect.size.timesPointwise(
new Vec2(width / this.lastBounds.width, height / this.lastBounds.height),
),
),
)
}
this.lastBounds = bounds
}
@@ -308,11 +334,11 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
if (this.props.configSpaceViewportRect.isEmpty()) return
this.props.canvasContext.renderInto(this.container, (context) => {
this.props.canvasContext.renderInto(this.container, context => {
this.props.flamechartRenderer.render({
physicalSpaceDstRect: new Rect(Vec2.zero, this.physicalViewSize()),
configSpaceSrcRect: this.props.configSpaceViewportRect,
renderOutlines: true
renderOutlines: true,
})
})
}
@@ -333,7 +359,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
private maybeClearInteractionLock = () => {
if (this.interactionLock) {
if (!this.frameHadWheelEvent) {
this.framesWithoutWheelEvents++;
this.framesWithoutWheelEvents++
if (this.framesWithoutWheelEvents >= 2) {
this.interactionLock = null
this.framesWithoutWheelEvents = 0
@@ -367,12 +393,15 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
private zoom(logicalViewSpaceCenter: Vec2, multiplier: number) {
this.interactionLock = 'zoom'
const physicalCenter = this.logicalToPhysicalViewSpace().transformPosition(logicalViewSpaceCenter)
const configSpaceCenter = this.configSpaceToPhysicalViewSpace().inverseTransformPosition(physicalCenter)
const physicalCenter = this.logicalToPhysicalViewSpace().transformPosition(
logicalViewSpaceCenter,
)
const configSpaceCenter = this.configSpaceToPhysicalViewSpace().inverseTransformPosition(
physicalCenter,
)
if (!configSpaceCenter) return
const zoomTransform = AffineTransform
.withTranslation(configSpaceCenter.times(-1))
const zoomTransform = AffineTransform.withTranslation(configSpaceCenter.times(-1))
.scaledBy(new Vec2(multiplier, 1))
.translatedBy(configSpaceCenter)
@@ -404,7 +433,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
const hoveredBounds = this.hoveredLabel.configSpaceBounds
const viewportRect = new Rect(
hoveredBounds.origin.minus(new Vec2(0, 1)),
hoveredBounds.size.withY(this.props.configSpaceViewportRect.height())
hoveredBounds.size.withY(this.props.configSpaceViewportRect.height()),
)
this.props.setConfigSpaceViewportRect(viewportRect)
}
@@ -434,24 +463,25 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
}
this.hoveredLabel = null
const logicalViewSpaceMouse = new Vec2(ev.offsetX, ev.offsetY)
const physicalViewSpaceMouse = this.logicalToPhysicalViewSpace().transformPosition(logicalViewSpaceMouse)
const configSpaceMouse = this.configSpaceToPhysicalViewSpace().inverseTransformPosition(physicalViewSpaceMouse)
const physicalViewSpaceMouse = this.logicalToPhysicalViewSpace().transformPosition(
logicalViewSpaceMouse,
)
const configSpaceMouse = this.configSpaceToPhysicalViewSpace().inverseTransformPosition(
physicalViewSpaceMouse,
)
if (!configSpaceMouse) return
const setHoveredLabel = (frame: FlamechartFrame, depth = 0) => {
const width = frame.end - frame.start
const configSpaceBounds = new Rect(
new Vec2(frame.start, depth),
new Vec2(width, 1)
)
const configSpaceBounds = new Rect(new Vec2(frame.start, depth), new Vec2(width, 1))
if (configSpaceMouse.x < configSpaceBounds.left()) return null
if (configSpaceMouse.x > configSpaceBounds.right()) return null
if (configSpaceBounds.contains(configSpaceMouse)) {
this.hoveredLabel = {
configSpaceBounds,
node: frame.node
node: frame.node,
}
}
@@ -460,7 +490,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
}
}
for (let frame of (this.props.flamechart.getLayers()[0] || [])) {
for (let frame of this.props.flamechart.getLayers()[0] || []) {
setHoveredLabel(frame)
}
@@ -486,13 +516,13 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
const isZoom = ev.metaKey || ev.ctrlKey
if (isZoom && this.interactionLock !== 'pan') {
let multiplier = 1 + (ev.deltaY / 100)
let multiplier = 1 + ev.deltaY / 100
// On Chrome & Firefox, pinch-to-zoom maps to
// WheelEvent + Ctrl Key. We'll accelerate it in
// this case, since it feels a bit sluggish otherwise.
if (ev.ctrlKey) {
multiplier = 1 + (ev.deltaY / 40)
multiplier = 1 + ev.deltaY / 40
}
multiplier = clamp(multiplier, 0.1, 10.0)
@@ -528,8 +558,9 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
}
}
shouldComponentUpdate() { return false }
shouldComponentUpdate() {
return false
}
componentWillReceiveProps(nextProps: FlamechartPanZoomViewProps) {
if (this.props.flamechart !== nextProps.flamechart) {
this.renderCanvas()
@@ -557,11 +588,9 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
onMouseLeave={this.onMouseLeave}
onDblClick={this.onDblClick}
onWheel={this.onWheel}
ref={this.containerRef}>
<canvas
width={1} height={1}
ref={this.overlayCanvasRef}
className={css(style.fill)} />
ref={this.containerRef}
>
<canvas width={1} height={1} ref={this.overlayCanvasRef} className={css(style.fill)} />
</div>
)
}
@@ -587,37 +616,40 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
this.state = {
hoveredNode: null,
configSpaceViewportRect: Rect.empty,
logicalSpaceMouse: Vec2.zero
logicalSpaceMouse: Vec2.zero,
}
}
private configSpaceSize() {
return new Vec2(
this.props.flamechart.getTotalWeight(),
this.props.flamechart.getLayers().length
this.props.flamechart.getLayers().length,
)
}
private minConfigSpaceViewportRectWidth() {
return Math.min(this.props.flamechart.getTotalWeight(), 3 * this.props.flamechart.getMinFrameWidth());
return Math.min(
this.props.flamechart.getTotalWeight(),
3 * this.props.flamechart.getMinFrameWidth(),
)
}
private setConfigSpaceViewportRect = (viewportRect: Rect): void => {
const configSpaceOriginBounds = new Rect(
new Vec2(0, -1),
Vec2.max(new Vec2(0, 0), this.configSpaceSize().minus(viewportRect.size))
Vec2.max(new Vec2(0, 0), this.configSpaceSize().minus(viewportRect.size)),
)
const configSpaceSizeBounds = new Rect(
new Vec2(this.minConfigSpaceViewportRectWidth(), viewportRect.height()),
new Vec2(this.configSpaceSize().x, viewportRect.height())
new Vec2(this.configSpaceSize().x, viewportRect.height()),
)
this.setState({
configSpaceViewportRect: new Rect(
configSpaceOriginBounds.closestPointTo(viewportRect.origin),
configSpaceSizeBounds.closestPointTo(viewportRect.size)
)
configSpaceSizeBounds.closestPointTo(viewportRect.size),
),
})
}
@@ -629,8 +661,8 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
onNodeHover = (hoveredNode: CallTreeNode | null, logicalSpaceMouse: Vec2) => {
this.setState({
hoveredNode,
logicalSpaceMouse: logicalSpaceMouse.plus(new Vec2(0, Sizes.MINIMAP_HEIGHT))
});
logicalSpaceMouse: logicalSpaceMouse.plus(new Vec2(0, Sizes.MINIMAP_HEIGHT)),
})
}
formatValue(weight: number) {
@@ -649,7 +681,7 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
renderTooltip() {
if (!this.container) return null
const { hoveredNode, logicalSpaceMouse } = this.state
const {hoveredNode, logicalSpaceMouse} = this.state
if (!hoveredNode) return null
const {width, height} = this.container.getBoundingClientRect()
@@ -665,26 +697,30 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
if (logicalSpaceMouse.x + OFFSET_FROM_MOUSE + Sizes.TOOLTIP_WIDTH_MAX < width) {
positionStyle.left = logicalSpaceMouse.x + OFFSET_FROM_MOUSE
} else {
positionStyle.right = (width - logicalSpaceMouse.x) + 1
positionStyle.right = width - logicalSpaceMouse.x + 1
}
if (logicalSpaceMouse.y + OFFSET_FROM_MOUSE + Sizes.TOOLTIP_HEIGHT_MAX < height) {
positionStyle.top = logicalSpaceMouse.y + OFFSET_FROM_MOUSE
} else {
positionStyle.bottom = (height - logicalSpaceMouse.y) + 1
positionStyle.bottom = height - logicalSpaceMouse.y + 1
}
return (
<div className={css(style.hoverTip)} style={positionStyle}>
<div className={css(style.hoverTipRow)}>
<span className={css(style.hoverCount)}>{this.formatValue(hoveredNode.getTotalWeight())}</span>{' '}
<span className={css(style.hoverCount)}>
{this.formatValue(hoveredNode.getTotalWeight())}
</span>{' '}
{hoveredNode.frame.name}
</div>
</div>
)
}
containerRef = (container?: Element) => { this.container = container as HTMLDivElement || null }
containerRef = (container?: Element) => {
this.container = (container as HTMLDivElement) || null
}
panZoomView: FlamechartPanZoomView | null = null
panZoomRef = (view: FlamechartPanZoomView | null) => {
@@ -692,7 +728,7 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
}
subcomponents() {
return {
panZoom: this.panZoomView
panZoom: this.panZoomView,
}
}
@@ -705,7 +741,8 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
flamechart={this.props.flamechart}
flamechartRenderer={this.props.flamechartRenderer}
canvasContext={this.props.canvasContext}
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect} />
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect}
/>
<FlamechartPanZoomView
ref={this.panZoomRef}
canvasContext={this.props.canvasContext}
@@ -718,6 +755,6 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
/>
{this.renderTooltip()}
</div>
)
)
}
}

View File

@@ -1,6 +1,6 @@
import {Frame, CallTreeNode} from './profile'
import { lastOf } from './utils'
import {lastOf} from './utils'
export interface FlamechartFrame {
node: CallTreeNode
@@ -19,7 +19,7 @@ interface FlamechartDataSource {
forEachCall(
openFrame: (node: CallTreeNode, value: number) => void,
closeFrame: (value: number) => void
closeFrame: (value: number) => void,
): void
getColorBucketForFrame(f: Frame): number
@@ -31,11 +31,21 @@ export class Flamechart {
private totalWeight: number = 0
private minFrameWidth: number = 1
getTotalWeight() { return this.totalWeight }
getLayers() { return this.layers }
getColorBucketForFrame(frame: Frame) { return this.source.getColorBucketForFrame(frame) }
getMinFrameWidth() { return this.minFrameWidth }
formatValue(v: number) { return this.source.formatValue(v) }
getTotalWeight() {
return this.totalWeight
}
getLayers() {
return this.layers
}
getColorBucketForFrame(frame: Frame) {
return this.source.getColorBucketForFrame(frame)
}
getMinFrameWidth() {
return this.minFrameWidth
}
formatValue(v: number) {
return this.source.formatValue(v)
}
constructor(private source: FlamechartDataSource) {
const stack: FlamechartFrame[] = []
@@ -71,4 +81,4 @@ export class Flamechart {
if (!isFinite(this.minFrameWidth)) this.minFrameWidth = 1
}
}
}

View File

@@ -9,10 +9,10 @@ interface BGSample {
function parseBGFoldedStacks(contents: string): BGSample[] {
const samples: BGSample[] = []
contents.replace(/^(.*) (\d+)$/mg, (match: string, stack: string, n: string) => {
contents.replace(/^(.*) (\d+)$/gm, (match: string, stack: string, n: string) => {
samples.push({
stack: stack.split(';').map(name => ({key: name, name: name})),
duration: parseInt(n, 10)
duration: parseInt(n, 10),
})
return match
})
@@ -27,4 +27,4 @@ export function importFromBGFlameGraph(contents: string): Profile {
profile.appendSample(sample.stack, sample.duration)
}
return profile
}
}

View File

@@ -2,28 +2,28 @@ import {Profile, TimeFormatter, FrameInfo} from '../profile'
import {getOrInsert, lastOf} from '../utils'
interface TimelineEvent {
pid: number,
tid: number,
ts: number,
ph: string,
cat: string,
name: string,
dur: number,
tdur: number,
tts: number,
args: { [key: string]: any }
pid: number
tid: number
ts: number
ph: string
cat: string
name: string
dur: number
tdur: number
tts: number
args: {[key: string]: any}
}
interface PositionTickInfo {
line: number,
line: number
ticks: number
}
interface CPUProfileCallFrame {
columnNumber: number,
functionName: string,
lineNumber: number,
scriptId: string,
columnNumber: number
functionName: string
lineNumber: number
scriptId: string
url: string
}
@@ -37,10 +37,10 @@ interface CPUProfileNode {
}
interface CPUProfile {
startTime: number,
endTime: number,
nodes: CPUProfileNode[],
samples: number[],
startTime: number
endTime: number
nodes: CPUProfileNode[]
samples: number[]
timeDeltas: number[]
}
@@ -48,18 +48,18 @@ export function importFromChromeTimeline(events: TimelineEvent[]) {
// It seems like sometimes Chrome timeline files contain multiple CpuProfiles?
// For now, choose the first one in the list.
for (let event of events) {
if (event.name == "CpuProfile") {
if (event.name == 'CpuProfile') {
const chromeProfile = event.args.data.cpuProfile as CPUProfile
return importFromChromeCPUProfile(chromeProfile)
}
}
throw new Error("Could not find CPU profile in Timeline")
throw new Error('Could not find CPU profile in Timeline')
}
const callFrameToFrameInfo = new Map<CPUProfileCallFrame, FrameInfo>()
function frameInfoForCallFrame(callFrame: CPUProfileCallFrame) {
return getOrInsert(callFrameToFrameInfo, callFrame, (callFrame) => {
const name = callFrame.functionName || "(anonymous)"
return getOrInsert(callFrameToFrameInfo, callFrame, callFrame => {
const name = callFrame.functionName || '(anonymous)'
const file = callFrame.url
const line = callFrame.lineNumber
const col = callFrame.columnNumber
@@ -68,7 +68,7 @@ function frameInfoForCallFrame(callFrame: CPUProfileCallFrame) {
name,
file,
line,
col
col,
}
})
}
@@ -117,7 +117,7 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile) {
let value = 0
for (let i = 0; i < samples.length; i++) {
const timeDelta = timeDeltas[i+1] || 0
const timeDelta = timeDeltas[i + 1] || 0
const nodeId = samples[i]
let stackTop = nodeById.get(nodeId)
if (!stackTop) continue
@@ -130,7 +130,10 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile) {
for (
lca = stackTop;
lca && prevStack.indexOf(lca) === -1;
lca = lca.callFrame.functionName === "(garbage collector)" ? lastOf(prevStack) : lca.parent || null
lca =
lca.callFrame.functionName === '(garbage collector)'
? lastOf(prevStack)
: lca.parent || null
) {}
// Close frames that are no longer open
@@ -146,7 +149,10 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile) {
let node: CPUProfileNode | null = stackTop;
node && node != lca;
// Place GC calls on top of the previous call stack
node = node.callFrame.functionName === "(garbage collector)" ? lastOf(prevStack) : node.parent || null
node =
node.callFrame.functionName === '(garbage collector)'
? lastOf(prevStack)
: node.parent || null
) {
toOpen.push(node)
}
@@ -167,4 +173,4 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile) {
profile.setValueFormatter(new TimeFormatter('microseconds'))
return profile
}
}

View File

@@ -20,7 +20,7 @@ export function importFromStackprof(stackprofProfile: StackprofProfile): Profile
const {frames, raw, raw_timestamp_deltas} = stackprofProfile
let sampleIndex = 0
for (let i = 0; i < raw.length;) {
for (let i = 0; i < raw.length; ) {
const stackHeight = raw[i++]
const stack: FrameInfo[] = []
@@ -28,7 +28,7 @@ export function importFromStackprof(stackprofProfile: StackprofProfile): Profile
const id = raw[i++]
stack.push({
key: id,
...frames[id]
...frames[id],
})
}
const nSamples = raw[i++]
@@ -43,4 +43,4 @@ export function importFromStackprof(stackprofProfile: StackprofProfile): Profile
profile.setValueFormatter(new TimeFormatter('microseconds'))
return profile
}
}

View File

@@ -1,18 +1,24 @@
class ListNode<V> {
prev: ListNode<V> | null = null
next: ListNode<V> | null = null
constructor(readonly data: V) { }
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() { }
constructor() {}
getHead(): ListNode<V> | null { return this.head }
getTail(): ListNode<V> | null { return this.tail }
getSize(): number { return this.size }
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) {
@@ -97,7 +103,7 @@ export class LRUCache<K, V> {
private list = new List<K>()
private map = new Map<K, LRUCacheNode<K, V>>()
constructor(private capacity: number) { }
constructor(private capacity: number) {}
has(key: K): boolean {
return this.map.has(key)
@@ -115,9 +121,13 @@ export class LRUCache<K, V> {
return node ? node.value : null
}
getSize() { return this.list.getSize() }
getSize() {
return this.list.getSize()
}
getCapacity() { return this.capacity }
getCapacity() {
return this.capacity
}
insert(key: K, value: V) {
const node = this.map.get(key)
@@ -129,7 +139,7 @@ export class LRUCache<K, V> {
this.map.delete(this.list.pop()!.data)
}
const listNode = this.list.prepend(new ListNode(key))
this.map.set(key, { value, listNode })
this.map.set(key, {value, listNode})
}
getOrInsert(key: K, f: (key: K) => V): V {
@@ -149,4 +159,4 @@ export class LRUCache<K, V> {
this.map.delete(key)
return [key, value]
}
}
}

192
math.ts
View File

@@ -6,19 +6,43 @@ export function clamp(x: number, minVal: number, maxVal: number) {
export class Vec2 {
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) }
withX(x: number) {
return new Vec2(x, this.y)
}
withY(y: number) {
return new Vec2(this.x, y)
}
plus(other: Vec2) { return new Vec2(this.x + other.x, this.y + other.y) }
minus(other: Vec2) { return new Vec2(this.x - other.x, this.y - other.y) }
times(scalar: number) { return new Vec2(this.x * scalar, this.y * scalar) }
timesPointwise(other: Vec2) { return new Vec2(this.x * other.x, this.y * other.y) }
dividedByPointwise(other: Vec2) { return new Vec2(this.x / other.x, this.y / other.y) }
dot(other: Vec2) { return this.x * other.x + this.y * other.y }
equals(other: Vec2) { return this.x === other.x && this.y === other.y }
length2() { return this.dot(this) }
length() { return Math.sqrt(this.length2()) }
abs() { return new Vec2(Math.abs(this.x), Math.abs(this.y)) }
plus(other: Vec2) {
return new Vec2(this.x + other.x, this.y + other.y)
}
minus(other: Vec2) {
return new Vec2(this.x - other.x, this.y - other.y)
}
times(scalar: number) {
return new Vec2(this.x * scalar, this.y * scalar)
}
timesPointwise(other: Vec2) {
return new Vec2(this.x * other.x, this.y * other.y)
}
dividedByPointwise(other: Vec2) {
return new Vec2(this.x / other.x, this.y / other.y)
}
dot(other: Vec2) {
return this.x * other.x + this.y * other.y
}
equals(other: Vec2) {
return this.x === other.x && this.y === other.y
}
length2() {
return this.dot(this)
}
length() {
return Math.sqrt(this.length2())
}
abs() {
return new Vec2(Math.abs(this.x), Math.abs(this.y))
}
static min(a: Vec2, b: Vec2) {
return new Vec2(Math.min(a.x, b.x), Math.min(a.y, b.y))
@@ -31,52 +55,56 @@ export class Vec2 {
static zero = new Vec2(0, 0)
static unit = new Vec2(1, 1)
flatten(): [number, number] { return [this.x, this.y] }
flatten(): [number, number] {
return [this.x, this.y]
}
}
export class AffineTransform {
constructor(
readonly m00 = 1, readonly m01 = 0, readonly m02 = 0,
readonly m10 = 0, readonly m11 = 1, readonly m12 = 0
readonly m00 = 1,
readonly m01 = 0,
readonly m02 = 0,
readonly m10 = 0,
readonly m11 = 1,
readonly m12 = 0,
) {}
withScale(s: Vec2) {
let {
m00, m01, m02,
m10, m11, m12
} = this
let {m00, m01, m02, m10, m11, m12} = this
m00 = s.x
m11 = s.y
return new AffineTransform(m00, m01, m02, m10, m11, m12)
}
static withScale(s: Vec2) {
return (new AffineTransform).withScale(s)
return new AffineTransform().withScale(s)
}
scaledBy(s: Vec2) {
return AffineTransform.withScale(s).times(this)
}
getScale() {
return new Vec2(this.m00, this.m11)
}
scaledBy(s: Vec2) { return AffineTransform.withScale(s).times(this) }
getScale() { return new Vec2(this.m00, this.m11) }
withTranslation(t: Vec2) {
let {
m00, m01, m02,
m10, m11, m12
} = this
let {m00, m01, m02, m10, m11, m12} = this
m02 = t.x
m12 = t.y
return new AffineTransform(m00, m01, m02, m10, m11, m12)
}
static withTranslation(t: Vec2) {
return (new AffineTransform).withTranslation(t)
return new AffineTransform().withTranslation(t)
}
getTranslation() {
return new Vec2(this.m02, this.m12)
}
translatedBy(t: Vec2) {
return AffineTransform.withTranslation(t).times(this)
}
getTranslation() { return new Vec2(this.m02, this.m12) }
translatedBy(t: Vec2) { return AffineTransform.withTranslation(t).times(this) }
static betweenRects(from: Rect, to: Rect) {
return AffineTransform
.withTranslation(from.origin.times(-1))
.scaledBy(new Vec2(
to.size.x / from.size.x,
to.size.y / from.size.y
))
return AffineTransform.withTranslation(from.origin.times(-1))
.scaledBy(new Vec2(to.size.x / from.size.x, to.size.y / from.size.y))
.translatedBy(to.origin)
}
@@ -92,17 +120,19 @@ export class AffineTransform {
}
equals(other: AffineTransform) {
return this.m00 == other.m00 &&
this.m01 == other.m01 &&
this.m02 == other.m02 &&
this.m10 == other.m10 &&
this.m11 == other.m11 &&
this.m12 == other.m12;
return (
this.m00 == other.m00 &&
this.m01 == other.m01 &&
this.m02 == other.m02 &&
this.m10 == other.m10 &&
this.m11 == other.m11 &&
this.m12 == other.m12
)
}
timesScalar(s: number) {
const {m00, m01, m02, m10, m11, m12} = this
return new AffineTransform(s*m00, s*m01, s*m02, s*m10, s*m11, s*m12)
return new AffineTransform(s * m00, s * m01, s * m02, s * m10, s * m11, s * m12)
}
det() {
@@ -112,9 +142,7 @@ export class AffineTransform {
const m22 = 1
return (
m00 * (m11 * m22 - m12 * m21) -
m01 * (m10 * m22 - m12 * m20) +
m02 * (m10 * m21 - m11 * m20)
m00 * (m11 * m22 - m12 * m21) - m01 * (m10 * m22 - m12 * m20) + m02 * (m10 * m21 - m11 * m20)
)
}
@@ -149,10 +177,7 @@ export class AffineTransform {
}
transformVector(v: Vec2) {
return new Vec2(
v.x * this.m00 + v.y * this.m01,
v.x * this.m10 + v.y * this.m11
)
return new Vec2(v.x * this.m00 + v.y * this.m01, v.x * this.m10 + v.y * this.m11)
}
inverseTransformVector(v: Vec2): Vec2 | null {
@@ -164,7 +189,7 @@ export class AffineTransform {
transformPosition(v: Vec2) {
return new Vec2(
v.x * this.m00 + v.y * this.m01 + this.m02,
v.x * this.m10 + v.y * this.m11 + this.m12
v.x * this.m10 + v.y * this.m11 + this.m12,
)
}
@@ -189,7 +214,7 @@ export class AffineTransform {
return new Rect(origin, size)
}
inverseTransformRect(r: Rect): Rect | null{
inverseTransformRect(r: Rect): Rect | null {
const inv = this.inverted()
if (!inv) return null
return inv.transformRect(r)
@@ -197,6 +222,7 @@ export class AffineTransform {
flatten(): [number, number, number, number, number, number, number, number, number] {
// Flatten into GLSL format
// prettier-ignore
return [
this.m00, this.m10, 0,
this.m01, this.m11, 0,
@@ -206,35 +232,55 @@ export class AffineTransform {
}
export class Rect {
constructor(
readonly origin: Vec2,
readonly size: Vec2
) {}
constructor(readonly origin: Vec2, readonly size: Vec2) {}
isEmpty() { return this.width() == 0 || this.height() == 0 }
isEmpty() {
return this.width() == 0 || this.height() == 0
}
width() { return this.size.x }
height() { return this.size.y }
width() {
return this.size.x
}
height() {
return this.size.y
}
left() { return this.origin.x }
right() { return this.left() + this.width() }
top() { return this.origin.y }
bottom() { return this.top() + this.height() }
left() {
return this.origin.x
}
right() {
return this.left() + this.width()
}
top() {
return this.origin.y
}
bottom() {
return this.top() + this.height()
}
topLeft() { return this.origin }
topRight() { return this.origin.plus(new Vec2(this.width(), 0)) }
topLeft() {
return this.origin
}
topRight() {
return this.origin.plus(new Vec2(this.width(), 0))
}
bottomRight() { return this.origin.plus(this.size) }
bottomLeft() { return this.origin.plus(new Vec2(0, this.height())) }
bottomRight() {
return this.origin.plus(this.size)
}
bottomLeft() {
return this.origin.plus(new Vec2(0, this.height()))
}
withOrigin(origin: Vec2) { return new Rect(origin, this.size) }
withSize(size: Vec2) { return new Rect(this.origin, size) }
withOrigin(origin: Vec2) {
return new Rect(origin, this.size)
}
withSize(size: Vec2) {
return new Rect(this.origin, size)
}
closestPointTo(p: Vec2) {
return new Vec2(
clamp(p.x, this.left(), this.right()),
clamp(p.y, this.top(), this.bottom())
)
return new Vec2(clamp(p.x, this.left(), this.right()), clamp(p.y, this.top(), this.bottom()))
}
distanceFrom(p: Vec2) {
@@ -270,4 +316,4 @@ export class Rect {
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))
}
}

View File

@@ -1,5 +1,5 @@
import regl from 'regl'
import { AffineTransform, Rect } from './math'
import {AffineTransform, Rect} from './math'
export interface ViewportRectangleRendererProps {
configSpaceToPhysicalViewSpace: AffineTransform
@@ -66,12 +66,12 @@ export class ViewportRectangleRenderer {
srcRGB: 'src alpha',
srcAlpha: 'one',
dstRGB: 'one minus src alpha',
dstAlpha: 'one'
}
dstAlpha: 'one',
},
},
depth: {
enable: false
enable: false,
},
attributes: {
@@ -83,12 +83,7 @@ export class ViewportRectangleRenderer {
// | /|
// |/ |
// 2 +--+ 3
position: [
[-1, 1],
[1, 1],
[-1, -1],
[1, -1]
]
position: [[-1, 1], [1, 1], [-1, -1], [1, -1]],
},
uniforms: {
@@ -109,12 +104,12 @@ export class ViewportRectangleRenderer {
},
framebufferHeight: (context, props) => {
return context.framebufferHeight
}
},
},
primitive: 'triangle strip',
count: 4
count: 4,
})
}
@@ -122,6 +117,10 @@ export class ViewportRectangleRenderer {
this.command(props)
}
resetStats() { return Object.assign(this.command.stats, { cpuTime: 0, gpuTime: 0, count: 0 }) }
stats() { return this.command.stats }
}
resetStats() {
return Object.assign(this.command.stats, {cpuTime: 0, gpuTime: 0, count: 0})
}
stats() {
return this.command.stats
}
}

703
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,11 @@
"description": "",
"main": "index.js",
"scripts": {
"serve": "parcel index.html --open --no-autoinstall",
"deploy": "./deploy.sh"
"deploy": "./deploy.sh",
"prettier": "prettier --write './**/*.ts' './**/*.tsx'",
"lint": "eslint './**/*.ts' './**/*.tsx'",
"test": "npm run lint",
"serve": "parcel index.html --open --no-autoinstall"
},
"browserslist": [
"last 2 Chrome versions",
@@ -15,10 +18,14 @@
"license": "MIT",
"devDependencies": {
"aphrodite": "2.1.0",
"eslint": "^4.19.1",
"eslint-plugin-prettier": "^2.6.0",
"parcel-bundler": "1.7.0",
"preact": "8.2.7",
"prettier": "^1.12.0",
"regl": "1.3.1",
"typescript": "2.8.1",
"typescript-eslint-parser": "^14.0.0",
"uglify-es": "3.2.2"
}
}

7
prettier.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
bracketSpacing: false,
printWidth: 100,
semi: false,
singleQuote: true,
trailingComma: 'all',
};

View File

@@ -1,4 +1,4 @@
import { lastOf, getOrInsert } from './utils'
import {lastOf, getOrInsert} from './utils'
const demangleCppModule = import('./demangle-cpp')
// Force eager loading of the module
@@ -25,10 +25,18 @@ export interface FrameInfo {
export class HasWeights {
private selfWeight = 0
private totalWeight = 0
getSelfWeight() { return this.selfWeight }
getTotalWeight() { return this.totalWeight }
addToTotalWeight(delta: number) { this.totalWeight += delta }
addToSelfWeight(delta: number) { this.selfWeight += delta }
getSelfWeight() {
return this.selfWeight
}
getTotalWeight() {
return this.totalWeight
}
addToTotalWeight(delta: number) {
this.totalWeight += delta
}
addToSelfWeight(delta: number) {
this.selfWeight += delta
}
}
export class Frame extends HasWeights {
@@ -81,7 +89,7 @@ export class RawValueFormatter implements ValueFormatter {
}
export class TimeFormatter implements ValueFormatter {
private multiplier : number
private multiplier: number
constructor(unit: 'nanoseconds' | 'microseconds' | 'milliseconds' | 'seconds') {
if (unit === 'nanoseconds') this.multiplier = 1e-9
@@ -93,7 +101,7 @@ export class TimeFormatter implements ValueFormatter {
format(v: number) {
const s = v * this.multiplier
if (s / 1e0 >= 1) return `${s.toFixed(2)}s`
if (s / 1 >= 1) return `${s.toFixed(2)}s`
if (s / 1e-3 >= 1) return `${(s / 1e-3).toFixed(2)}ms`
if (s / 1e-6 >= 1) return `${(s / 1e-6).toFixed(2)}µs`
else return `${(s / 1e-9).toFixed(2)}ms`
@@ -120,20 +128,30 @@ export class Profile {
this.totalWeight = totalWeight
}
formatValue(v: number) { return this.valueFormatter.format(v) }
setValueFormatter(f: ValueFormatter) { this.valueFormatter = f }
formatValue(v: number) {
return this.valueFormatter.format(v)
}
setValueFormatter(f: ValueFormatter) {
this.valueFormatter = f
}
getName() { return this.name }
setName(name: string) { this.name = name }
getName() {
return this.name
}
setName(name: string) {
this.name = name
}
getTotalWeight() { return this.totalWeight }
getTotalWeight() {
return this.totalWeight
}
getTotalNonIdleWeight() {
return this.groupedCalltreeRoot.children.reduce((n, c) => n + c.getTotalWeight(), 0)
}
forEachCallGrouped(
openFrame: (node: CallTreeNode, value: number) => void,
closeFrame: (value: number) => void
closeFrame: (value: number) => void,
) {
function visit(node: CallTreeNode, start: number) {
if (node.frame !== rootFrame) {
@@ -143,9 +161,9 @@ export class Profile {
let childTime = 0
const children = [...node.children]
children.sort((a, b) => a.getTotalWeight() > b.getTotalWeight() ? -1 : 1)
children.sort((a, b) => (a.getTotalWeight() > b.getTotalWeight() ? -1 : 1))
children.forEach(function (child) {
children.forEach(function(child) {
visit(child, start + childTime)
childTime += child.getTotalWeight()
})
@@ -159,7 +177,7 @@ export class Profile {
forEachCall(
openFrame: (node: CallTreeNode, value: number) => void,
closeFrame: (value: number) => void
closeFrame: (value: number) => void,
) {
let prevStack: CallTreeNode[] = []
let value = 0
@@ -220,7 +238,9 @@ export class Profile {
for (let frameInfo of stack) {
const frame = getOrInsert(this.frames, frameInfo.key, () => new Frame(frameInfo))
const last = useAppendOrder ? lastOf(node.children) : node.children.find(c => c.frame === frame)
const last = useAppendOrder
? lastOf(node.children)
: node.children.find(c => c.frame === frame)
if (last && last.frame == frame) {
node = last
} else {
@@ -303,7 +323,9 @@ export class Profile {
}
}
const last = useAppendOrder ? lastOf(prevTop.children) : prevTop.children.find(c => c.frame === frame)
const last = useAppendOrder
? lastOf(prevTop.children)
: prevTop.children.find(c => c.frame === frame)
let node: CallTreeNode
if (last && last.frame == frame) {
node = last

View File

@@ -1,6 +1,6 @@
import regl from 'regl'
import { Rect, Vec2, AffineTransform } from './math'
import { Color } from './color'
import {Rect, Vec2, AffineTransform} from './math'
import {Color} from './color'
export class RectangleBatch {
private rectCapacity = 1000
@@ -10,19 +10,23 @@ export class RectangleBatch {
private configSpaceSizes = new Float32Array(this.rectCapacity * 2)
private colors = new Float32Array(this.rectCapacity * 3)
constructor(private gl: regl.Instance) { }
constructor(private gl: regl.Instance) {}
getRectCount() { return this.rectCount }
getRectCount() {
return this.rectCount
}
private configSpaceOffsetBuffer: regl.Buffer | null = null
getConfigSpaceOffsetBuffer() {
if (!this.configSpaceOffsetBuffer) this.configSpaceOffsetBuffer = this.gl.buffer(this.configSpaceOffsets)
if (!this.configSpaceOffsetBuffer)
this.configSpaceOffsetBuffer = this.gl.buffer(this.configSpaceOffsets)
return this.configSpaceOffsetBuffer
}
private configSpaceSizeBuffer: regl.Buffer | null = null
getConfigSpaceSizeBuffer() {
if (!this.configSpaceSizeBuffer) this.configSpaceSizeBuffer = this.gl.buffer(this.configSpaceSizes)
if (!this.configSpaceSizeBuffer)
this.configSpaceSizeBuffer = this.gl.buffer(this.configSpaceSizes)
return this.configSpaceSizeBuffer
}
@@ -105,11 +109,11 @@ export class RectangleBatchRenderer {
}
`,
depth: {
enable: false
},
depth: {
enable: false,
},
frag: `
frag: `
precision mediump float;
varying vec3 vColor;
varying float vParity;
@@ -121,12 +125,7 @@ export class RectangleBatchRenderer {
attributes: {
// Non-instanced attributes
corner: gl.buffer([
[0, 0],
[1, 0],
[0, 1],
[1, 1],
]),
corner: gl.buffer([[0, 0], [1, 0], [0, 1], [1, 1]]),
// Instanced attributes
configSpaceOffset: (context, props) => {
@@ -135,7 +134,7 @@ export class RectangleBatchRenderer {
offset: 0,
stride: 2 * 4,
size: 2,
divisor: 1
divisor: 1,
}
},
configSpaceSize: (context, props) => {
@@ -144,7 +143,7 @@ export class RectangleBatchRenderer {
offset: 0,
stride: 2 * 4,
size: 2,
divisor: 1
divisor: 1,
}
},
color: (context, props) => {
@@ -153,22 +152,23 @@ export class RectangleBatchRenderer {
offset: 0,
stride: 3 * 4,
size: 3,
divisor: 1
divisor: 1,
}
}
},
},
uniforms: {
configSpaceToNDC: (context, props) => {
const configToPhysical = AffineTransform.betweenRects(
props.configSpaceSrcRect,
props.physicalSpaceDstRect
props.physicalSpaceDstRect,
)
const viewportSize = new Vec2(context.viewportWidth, context.viewportHeight)
const physicalToNDC = AffineTransform.withTranslation(new Vec2(-1, 1))
.times(AffineTransform.withScale(new Vec2(2, -2).dividedByPointwise(viewportSize)))
const physicalToNDC = AffineTransform.withTranslation(new Vec2(-1, 1)).times(
AffineTransform.withScale(new Vec2(2, -2).dividedByPointwise(viewportSize)),
)
return physicalToNDC.times(configToPhysical).flatten()
},
@@ -179,7 +179,7 @@ export class RectangleBatchRenderer {
parityMin: (context, props) => {
return props.parityMin == null ? 0 : 1 + props.parityMin
}
},
},
instances: (context, props) => {
@@ -196,6 +196,10 @@ export class RectangleBatchRenderer {
this.command(props)
}
resetStats() { return Object.assign(this.command.stats, { cpuTime: 0, gpuTime: 0, count: 0 }) }
stats() { return this.command.stats }
}
resetStats() {
return Object.assign(this.command.stats, {cpuTime: 0, gpuTime: 0, count: 0})
}
stats() {
return this.command.stats
}
}

321
regl.d.ts vendored
View File

@@ -1,4 +1,4 @@
declare module "regl" {
declare module 'regl' {
interface InitializationOptions {
/** A reference to a WebGL rendering context. (Default created from canvas) */
gl?: WebGLRenderingContext
@@ -57,7 +57,24 @@ declare module "regl" {
export type vec3 = [number, number, number]
export type vec4 = [number, number, number, number]
export type mat3 = [number, number, number, number, number, number, number, number, number]
export type mat4 = [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]
export type mat4 = [
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number,
number
]
type GlslPrimitive = number | vec2 | vec3 | vec4 | mat3 | mat4
interface Tick {
@@ -68,9 +85,9 @@ declare module "regl" {
<P>(params: CommandOptions<P>): Command<P>
clear(args: {
color?: [number, number, number, number],
depth?: number,
stencil?: number,
color?: [number, number, number, number]
depth?: number
stencil?: number
}): void
// TODO(jlfwong): read()
@@ -127,10 +144,25 @@ declare module "regl" {
frame(callback: (context: Context) => void): Tick
}
type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array |
Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array
type TypedArray =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
type DrawMode = 'points' | 'lines' | 'line strip' | 'line loop' | 'triangles' | 'triangle strip' | 'triangle fan'
type DrawMode =
| 'points'
| 'lines'
| 'line strip'
| 'line loop'
| 'triangles'
| 'triangle strip'
| 'triangle fan'
interface Context {
tick: number
@@ -177,14 +209,49 @@ declare module "regl" {
}
type MagFilter = 'nearest' | 'linear'
type MinFilter = 'nearest' | 'linear' | 'mipmap' | 'linear mipmap linear' | 'nearest mipmap linear' | 'nearest mipmap nearest'
type MinFilter =
| 'nearest'
| 'linear'
| 'mipmap'
| 'linear mipmap linear'
| 'nearest mipmap linear'
| 'nearest mipmap nearest'
type WrapMode = 'repeat' | 'clamp' | 'mirror'
type TextureFormat = 'alpha' | 'luminance' | 'luminance alpha' | 'rgb' | 'rgba' | 'rgba4' | 'rgb5 a1' | 'rgb565' | 'srgb' | 'srgba' | 'depth' | 'depth stencil' | 'rgb s3tc dxt1' | 'rgb s3tc dxt5' | 'rgb atc' | 'rgba atc explicit alpha' | 'rgba atc interpolated alpha' | 'rgb pvrtc 4bppv1' | 'rgb pvrtc 2bppv1' | 'rgba pvrtc 4bppv1' | 'rgba pvrtc 2bppv1' | 'rgb etc1'
type TextureFormat =
| 'alpha'
| 'luminance'
| 'luminance alpha'
| 'rgb'
| 'rgba'
| 'rgba4'
| 'rgb5 a1'
| 'rgb565'
| 'srgb'
| 'srgba'
| 'depth'
| 'depth stencil'
| 'rgb s3tc dxt1'
| 'rgb s3tc dxt5'
| 'rgb atc'
| 'rgba atc explicit alpha'
| 'rgba atc interpolated alpha'
| 'rgb pvrtc 4bppv1'
| 'rgb pvrtc 2bppv1'
| 'rgba pvrtc 4bppv1'
| 'rgba pvrtc 2bppv1'
| 'rgb etc1'
type TextureType = 'uint8' | 'uint16' | 'float' | 'float32' | 'half float' | 'float16'
type ColorSpace = 'none' | 'browser'
type MipmapHint = "don't care" | 'dont care' | 'nice' | 'fast'
type TextureData = number[] | number[][] | TypedArray | HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | CanvasRenderingContext2D
type TextureData =
| number[]
| number[][]
| TypedArray
| HTMLImageElement
| HTMLVideoElement
| HTMLCanvasElement
| CanvasRenderingContext2D
interface TextureOptions {
width?: number
height?: number
@@ -233,7 +300,17 @@ declare module "regl" {
// TODO(jlfwong): Cubic frame buffers
interface RenderBufferOptions {
format?: 'rgba4' | 'rgb565' | 'rgb5 a1' | 'depth' | 'stencil' | 'depth stencil' | 'srgba' | 'rgba16f' | 'rgb16f' | 'rgba32f'
format?:
| 'rgba4'
| 'rgb565'
| 'rgb5 a1'
| 'depth'
| 'stencil'
| 'depth stencil'
| 'srgba'
| 'rgba16f'
| 'rgb16f'
| 'rgba32f'
width?: number
height?: number
shape?: [number, number]
@@ -256,7 +333,15 @@ declare module "regl" {
depth?: boolean | RenderBuffer | Texture
stencil?: boolean | RenderBuffer | Texture
depthStencil?: boolean | RenderBuffer | Texture
colorFormat?: 'rgba' | 'rgba4' | 'rgb565' | 'rgb5 a1' | 'rgb16f' | 'rgba16f' | 'rgba32f' | 'srgba'
colorFormat?:
| 'rgba'
| 'rgba4'
| 'rgb565'
| 'rgb5 a1'
| 'rgb16f'
| 'rgba16f'
| 'rgba32f'
| 'srgba'
colorType?: 'uint8' | 'half float' | 'float'
}
interface Framebuffer {
@@ -281,18 +366,60 @@ declare module "regl" {
size?: number
divisor?: number
}
type Attribute = AttributeOptions | Buffer | BufferArgs | { constant: number | vec2 | vec3 | vec4 | mat3 | mat4 }
type Attribute =
| AttributeOptions
| Buffer
| BufferArgs
| {constant: number | vec2 | vec3 | vec4 | mat3 | mat4}
interface Computed<P, T> {
(context: Context, props: P, batchId: number): T
}
type MaybeComputed<P, T> = Computed<P, T> | T
type DepthFunction = 'never' | 'always' | '<' | 'less' | '<=' | 'lequal' | '>' | 'greater' | '>=' | 'gequal' | '=' | 'equal' | '!=' | 'notequal'
type BlendFunction = 0 | 'zero' | 1 | 'one' | 'src color' | 'one minus src color' | 'src alpha' | 'one minus src alpha' | 'dst color' | 'one minus dst color' | 'dst alpha' | 'one minus dst alpha' | 'constant color' | 'one minus constant color' | 'one minus constant alpha' | 'src alpha saturate'
type DepthFunction =
| 'never'
| 'always'
| '<'
| 'less'
| '<='
| 'lequal'
| '>'
| 'greater'
| '>='
| 'gequal'
| '='
| 'equal'
| '!='
| 'notequal'
type BlendFunction =
| 0
| 'zero'
| 1
| 'one'
| 'src color'
| 'one minus src color'
| 'src alpha'
| 'one minus src alpha'
| 'dst color'
| 'one minus dst color'
| 'dst alpha'
| 'one minus dst alpha'
| 'constant color'
| 'one minus constant color'
| 'one minus constant alpha'
| 'src alpha saturate'
type BlendEquation = 'add' | 'subtract' | 'reverse subtract' | 'min' | 'max'
type StencilFunction = DepthFunction
type StencilOp = 'zero' | 'keep' | 'replace' | 'invert' | 'increment' | 'decrement' | 'increment wrap' | 'decrement wrap'
type StencilOp =
| 'zero'
| 'keep'
| 'replace'
| 'invert'
| 'increment'
| 'decrement'
| 'increment wrap'
| 'decrement wrap'
interface CommandOptions<P> {
/** Source code of vertex shader */
vert?: string
@@ -300,11 +427,11 @@ declare module "regl" {
/** Source code of fragment shader */
frag?: string
context?: { [contextName: string]: MaybeComputed<P, any> }
context?: {[contextName: string]: MaybeComputed<P, any>}
uniforms?: { [uniformName: string]: MaybeComputed<P, Uniform> }
uniforms?: {[uniformName: string]: MaybeComputed<P, Uniform>}
attributes?: { [attributeName: string]: MaybeComputed<P, Attribute> }
attributes?: {[attributeName: string]: MaybeComputed<P, Attribute>}
primitive?: DrawMode
@@ -324,51 +451,70 @@ declare module "regl" {
profile?: MaybeComputed<P, boolean>
depth?: MaybeComputed<P, {
enable?: boolean,
mask?: boolean,
func?: DepthFunction,
range?: [number, number]
}>
blend?: MaybeComputed<P, {
enable?: boolean,
func?: {
src: BlendFunction
dst: BlendFunction
} | {
srcRGB: BlendFunction
srcAlpha: BlendFunction
dstRGB: BlendFunction
dstAlpha: BlendFunction
},
equation?: BlendEquation | {
rgb: BlendEquation
alpha: BlendEquation
},
color?: vec4
}>
stencil?: MaybeComputed<P, {
enable?: boolean
mask?: number
func?: StencilFunction
opFront?: { fail: StencilOp, zfail: StencilOp, pass: StencilOp },
opBack?: { fail: StencilOp, zfail: StencilOp, pass: StencilOp },
}>
polygonOffset?: MaybeComputed<P, {
enable?: boolean
offset?: {
factor: number
units: number
depth?: MaybeComputed<
P,
{
enable?: boolean
mask?: boolean
func?: DepthFunction
range?: [number, number]
}
}>
>
cull?: MaybeComputed<P, {
enable?: boolean
face?: 'front' | 'back'
}>
blend?: MaybeComputed<
P,
{
enable?: boolean
func?:
| {
src: BlendFunction
dst: BlendFunction
}
| {
srcRGB: BlendFunction
srcAlpha: BlendFunction
dstRGB: BlendFunction
dstAlpha: BlendFunction
}
equation?:
| BlendEquation
| {
rgb: BlendEquation
alpha: BlendEquation
}
color?: vec4
}
>
stencil?: MaybeComputed<
P,
{
enable?: boolean
mask?: number
func?: StencilFunction
opFront?: {fail: StencilOp; zfail: StencilOp; pass: StencilOp}
opBack?: {fail: StencilOp; zfail: StencilOp; pass: StencilOp}
}
>
polygonOffset?: MaybeComputed<
P,
{
enable?: boolean
offset?: {
factor: number
units: number
}
}
>
cull?: MaybeComputed<
P,
{
enable?: boolean
face?: 'front' | 'back'
}
>
frontFace?: MaybeComputed<P, 'cw' | 'ccw'>
@@ -378,35 +524,46 @@ declare module "regl" {
colorMask?: MaybeComputed<P, [boolean, boolean, boolean, boolean]>
sample?: MaybeComputed<P, {
enable?: boolean
alpha?: boolean
coverage?: {
value: number
invert: boolean
sample?: MaybeComputed<
P,
{
enable?: boolean
alpha?: boolean
coverage?: {
value: number
invert: boolean
}
}
}>
>
scissor?: MaybeComputed<P, {
enable?: boolean
box?: {
scissor?: MaybeComputed<
P,
{
enable?: boolean
box?: {
x: number
y: number
width: number
height: number
}
}
>
viewport?: MaybeComputed<
P,
{
x: number
y: number
width: number
height: number
}
}>
viewport?: MaybeComputed<P, {
x: number
y: number
width: number
height: number
}>
>
}
function prop<P>(name: keyof P): (context: Context, props: P, batchId: number) => P[keyof P]
function context<P>(name: keyof Context): (context: Context, props: P, batchId: number) => Context[keyof Context]
function context<P>(
name: keyof Context,
): (context: Context, props: P, batchId: number) => Context[keyof Context]
interface Command<P> {
/** One shot rendering */

View File

@@ -38,5 +38,3 @@ export abstract class ReloadableComponent<P, S> extends Component<P, S> {
return Object.create(null)
}
}

View File

@@ -1,5 +1,5 @@
import {h, render} from 'preact'
import {Application} from'./application'
import {Application} from './application'
let app: Application | null = null
const retained = (window as any)['__retained__'] as any
@@ -7,7 +7,7 @@ declare const module: any
if (module.hot) {
module.hot.dispose(() => {
if (app) {
(window as any)['__retained__'] = app.serialize()
;(window as any)['__retained__'] = app.serialize()
}
})
module.hot.accept()
@@ -21,4 +21,4 @@ function ref(instance: Application | null) {
}
}
render(<Application ref={ref}/>, document.body, document.body.lastElementChild || undefined)
render(<Application ref={ref} />, document.body, document.body.lastElementChild || undefined)

101
stats.ts
View File

@@ -35,33 +35,32 @@ export class StatsPanel {
showPanel(id: number) {
for (var i = 0; i < this.container.children.length; i++) {
(this.container.children[i] as HTMLElement).style.display = i === id ? 'block' : 'none';
;(this.container.children[i] as HTMLElement).style.display = i === id ? 'block' : 'none'
}
this.shown = id;
this.shown = id
}
private beginTime: number = 0
begin() {
this.beginTime = ( performance || Date ).now();
this.beginTime = (performance || Date).now()
}
private frames = 0
private prevTime = 0
end() {
this.frames++;
var time = ( performance || Date ).now();
this.msPanel.update(time - this.beginTime, 200);
this.frames++
var time = (performance || Date).now()
this.msPanel.update(time - this.beginTime, 200)
if ( time >= this.prevTime + 1000 ) {
this.fpsPanel.update(( this.frames * 1000 ) / ( time - this.prevTime ), 100);
this.prevTime = time;
this.frames = 0;
if (time >= this.prevTime + 1000) {
this.fpsPanel.update(this.frames * 1000 / (time - this.prevTime), 100)
this.prevTime = time
this.frames = 0
}
}
}
const PR = Math.round( window.devicePixelRatio || 1 );
const PR = Math.round(window.devicePixelRatio || 1)
class Panel {
private min: number = Infinity
@@ -75,23 +74,23 @@ class Panel {
private GRAPH_X = 3 * PR
private GRAPH_Y = 15 * PR
private GRAPH_WIDTH = 74 * PR
private GRAPH_HEIGHT = 30 * PR;
private GRAPH_HEIGHT = 30 * PR
constructor(private name: string, private fg: string, private bg: string) {
this.canvas.width = this.WIDTH;
this.canvas.height = this.HEIGHT;
this.canvas.style.cssText = 'width:80px;height:48px';
this.canvas.width = this.WIDTH
this.canvas.height = this.HEIGHT
this.canvas.style.cssText = 'width:80px;height:48px'
this.context.font = 'bold ' + ( 9 * PR ) + 'px Helvetica,Arial,sans-serif';
this.context.textBaseline = 'top';
this.context.fillStyle = bg;
this.context.fillRect( 0, 0, this.WIDTH, this.HEIGHT );
this.context.fillStyle = fg;
this.context.font = 'bold ' + 9 * PR + 'px Helvetica,Arial,sans-serif'
this.context.textBaseline = 'top'
this.context.fillStyle = bg
this.context.fillRect(0, 0, this.WIDTH, this.HEIGHT)
this.context.fillStyle = fg
this.context.fillText(this.name, this.TEXT_X, this.TEXT_Y)
this.context.fillRect(this.GRAPH_X, this.GRAPH_Y, this.GRAPH_WIDTH, this.GRAPH_HEIGHT);
this.context.fillStyle = bg;
this.context.globalAlpha = 0.9;
this.context.fillRect(this.GRAPH_X, this.GRAPH_Y, this.GRAPH_WIDTH, this.GRAPH_HEIGHT);
this.context.fillRect(this.GRAPH_X, this.GRAPH_Y, this.GRAPH_WIDTH, this.GRAPH_HEIGHT)
this.context.fillStyle = bg
this.context.globalAlpha = 0.9
this.context.fillRect(this.GRAPH_X, this.GRAPH_Y, this.GRAPH_WIDTH, this.GRAPH_HEIGHT)
}
appendTo(el: HTMLElement) {
@@ -99,18 +98,44 @@ class Panel {
}
update(value: number, maxValue: number) {
this.min = Math.min(this.min, value );
this.max = Math.max(this.max, value );
this.min = Math.min(this.min, value)
this.max = Math.max(this.max, value)
this.context.fillStyle = this.bg;
this.context.globalAlpha = 1;
this.context.fillRect( 0, 0, this.WIDTH, this.GRAPH_Y );
this.context.fillStyle = this.fg;
this.context.fillText( Math.round( value ) + ' ' + name + ' (' + Math.round( this.min ) + '-' + Math.round( this.max ) + ')', this.TEXT_X, this.TEXT_Y );
this.context.drawImage( this.canvas, this.GRAPH_X + PR, this.GRAPH_Y, this.GRAPH_WIDTH - PR, this.GRAPH_HEIGHT, this.GRAPH_X, this.GRAPH_Y, this.GRAPH_WIDTH - PR, this.GRAPH_HEIGHT );
this.context.fillRect( this.GRAPH_X + this.GRAPH_WIDTH - PR, this.GRAPH_Y, PR, this.GRAPH_HEIGHT );
this.context.fillStyle = this.bg;
this.context.globalAlpha = 0.9;
this.context.fillRect( this.GRAPH_X + this.GRAPH_WIDTH - PR, this.GRAPH_Y, PR, Math.round( ( 1 - ( value / maxValue ) ) * this.GRAPH_HEIGHT ) );
this.context.fillStyle = this.bg
this.context.globalAlpha = 1
this.context.fillRect(0, 0, this.WIDTH, this.GRAPH_Y)
this.context.fillStyle = this.fg
this.context.fillText(
Math.round(value) +
' ' +
name +
' (' +
Math.round(this.min) +
'-' +
Math.round(this.max) +
')',
this.TEXT_X,
this.TEXT_Y,
)
this.context.drawImage(
this.canvas,
this.GRAPH_X + PR,
this.GRAPH_Y,
this.GRAPH_WIDTH - PR,
this.GRAPH_HEIGHT,
this.GRAPH_X,
this.GRAPH_Y,
this.GRAPH_WIDTH - PR,
this.GRAPH_HEIGHT,
)
this.context.fillRect(this.GRAPH_X + this.GRAPH_WIDTH - PR, this.GRAPH_Y, PR, this.GRAPH_HEIGHT)
this.context.fillStyle = this.bg
this.context.globalAlpha = 0.9
this.context.fillRect(
this.GRAPH_X + this.GRAPH_WIDTH - PR,
this.GRAPH_Y,
PR,
Math.round((1 - value / maxValue) * this.GRAPH_HEIGHT),
)
}
}
}

View File

@@ -1,18 +1,18 @@
export enum FontFamily {
MONOSPACE = "Courier, monospace"
MONOSPACE = 'Courier, monospace',
}
export enum FontSize {
LABEL = 10,
TITLE = 12,
BIG_BUTTON = 36
BIG_BUTTON = 36,
}
export enum Colors {
LIGHT_GRAY = "#C4C4C4",
MEDIUM_GRAY = "#BDBDBD",
LIGHT_GRAY = '#C4C4C4',
MEDIUM_GRAY = '#BDBDBD',
GRAY = '#666666',
DARK_GRAY = '#222222',
LIGHT_BLUE = '#56CCF2',
DARK_BLUE = '#2F80ED'
DARK_BLUE = '#2F80ED',
}

View File

@@ -1,5 +1,5 @@
import regl from 'regl'
import { Vec2, Rect, AffineTransform } from './math'
import {Vec2, Rect, AffineTransform} from './math'
export interface TextureRendererProps {
texture: regl.Texture
@@ -36,7 +36,7 @@ export class TextureRenderer {
`,
depth: {
enable: false
enable: false,
},
attributes: {
@@ -48,54 +48,41 @@ export class TextureRenderer {
// | /|
// |/ |
// 2 +--+ 3
position: gl.buffer([
[-1, 1],
[1, 1],
[-1, -1],
[1, -1]
]),
uv: gl.buffer([
[0, 1],
[1, 1],
[0, 0],
[1, 0]
])
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,
uvTransform: (context, props) => {
const { srcRect, texture } = props
const {srcRect, texture} = props
const physicalToUV = AffineTransform.withTranslation(new Vec2(0, 1))
.times(AffineTransform.withScale(new Vec2(1, -1)))
.times(AffineTransform.betweenRects(
.times(
AffineTransform.betweenRects(
new Rect(Vec2.zero, new Vec2(texture.width, texture.height)),
Rect.unit
))
Rect.unit,
),
)
const uvRect = physicalToUV.transformRect(srcRect)
return AffineTransform.betweenRects(
Rect.unit,
uvRect,
).flatten()
return AffineTransform.betweenRects(Rect.unit, uvRect).flatten()
},
positionTransform: (context, props) => {
const { dstRect } = 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 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',
count: 4
count: 4,
})
}
@@ -103,8 +90,12 @@ export class TextureRenderer {
this.command(props)
}
resetStats() { return Object.assign(this.command.stats, { cpuTime: 0, gpuTime: 0, count: 0 }) }
stats() { return this.command.stats }
resetStats() {
return Object.assign(this.command.stats, {cpuTime: 0, gpuTime: 0, count: 0})
}
stats() {
return this.command.stats
}
}
export interface TextureCachedRendererOptions<T> {
@@ -141,10 +132,13 @@ export class TextureCachedRenderer<T> {
render(props: T) {
this.withContext((context: regl.Context) => {
let needsRender = false
if (this.texture.width !== context.viewportWidth || this.texture.height !== context.viewportHeight) {
if (
this.texture.width !== context.viewportWidth ||
this.texture.height !== context.viewportHeight
) {
// TODO(jlfwong): Can probably just use this.framebuffer.resize
this.texture({ width: context.viewportWidth, height: context.viewportHeight })
this.framebuffer({ color: [this.texture] })
this.texture({width: context.viewportWidth, height: context.viewportHeight})
this.framebuffer({color: [this.texture]})
needsRender = true
} else if (this.lastRenderProps == null) {
needsRender = true
@@ -161,26 +155,29 @@ export class TextureCachedRenderer<T> {
x: 0,
y: 0,
width: context.viewportWidth,
height: context.viewportHeight
height: context.viewportHeight,
}
},
framebuffer: this.framebuffer
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))
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
dstRect: glViewportRect,
})
this.lastRenderProps = props
this.dirty = false
})
}
}
}

View File

@@ -1,5 +1,5 @@
export function lastOf<T>(ts: T[]): T | null {
return ts[ts.length-1] || null
return ts[ts.length - 1] || null
}
export function sortBy<T>(ts: T[], key: (t: T) => number | string): void {
@@ -26,7 +26,9 @@ export function* itMap<T, U>(it: Iterable<T>, f: (t: T) => U): Iterable<U> {
}
export function itForEach<T>(it: Iterable<T>, f: (t: T) => void): void {
for (let t of it) { f(t) }
for (let t of it) {
f(t)
}
}
export function itReduce<T, U>(it: Iterable<T>, f: (a: U, b: T) => U, init: U): U {
@@ -44,4 +46,4 @@ export function cachedMeasureTextWidth(ctx: CanvasRenderingContext2D, text: stri
measureTextCache.set(text, ctx.measureText(text).width)
}
return measureTextCache.get(text)!
}
}