Set up Prettier and run it on the whole codebase

* Install prettier, set up the config file, and run it on all ts and tsx files.
* Install eslint and configure it with just eslint-plugin-prettier to check to
  make sure that prettier has been run.
* Add a basic .travis.yml that runs eslint.

There are other style things that might be nice to enforce with ESLint/TSLint,
like using const, import order, etc, but this commit just focuses on prettier,
which gets most of the way there.

One annoying issue for now is that typescript-eslint-parser gives a big warning
message since they haven't updated to officially support TypeScript 2.8 yet. We
aren't even using any ESLint rules that need the parser, but if we don't include
it, ESLint will crash. TS2.8 support is hopefully coming really soon, though:
https://github.com/eslint/typescript-eslint-parser/pull/454

As for the prettier config specifically, see https://prettier.io/docs/en/options.html
for the available options.

Config settings that seem non-controversial:

Semicolons: You don't use semicolons. (I prefer semicolons, but either way is fine.)

Quote style: Looks like you consistently use single quotes outside JSX and double
quotes in JSX, which is the `singleQuote: true` option.

Config settings worth discussion:

Line width: You don't have a specific max. I put 100 since I think it's a good number
for people (like both of us, probably) who find 80 a bit cramped. (At Benchling we use
110.) Prettier has a big red warning box recommending 80, but I still prefer 100ish.

Bracket spacing: This is `{foo}` vs `{ foo }` for imports, exports, object literals,
and destructuring. Looks like you're inconsistent but lean toward spaces. I personally
really dislike bracket spacing (it feels inconsistent with arrays and function calls),
but I'm certainly fine with it and Prettier has it enabled by default, so I kept it
enabled.

Trailing comma style: Options are "no trailing commas", "trailing commas for
everything exception function calls and parameter lists", and "trailing commas
everywhere". TypeScript can handle trailing commas everywhere, so there isn't a
concern with tooling. You're inconsistent, and it looks like you tend to not have
trailing commas, but I think it's probably best to just have them everywhere, so I
enabled them.

JSX Brackets: You're inconsistent about this, I think. I'd prefer to just keep the
default and wrap the `>` to the next line.

Arrow function parens: I only found two cases of arrow functions with one param
(both `c => c.frame === frame`), and both omitted the parens, so I kept the
default of omitting parens. This makes it mildly more annoying to add a typescript
type or additional param, which is a possible reason for always requiring parens.

Everything else is non-configurable, although it's possible some places would be
better with a `// prettier-ignore` comment (but I usually try to avoid those).
This commit is contained in:
Alan Pierce 2018-04-14 08:17:59 -07:00
parent 298056a9ef
commit 1bcb88670b
31 changed files with 1908 additions and 737 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

@ -1,25 +1,24 @@
import {h} from 'preact'
import {StyleSheet, css} from 'aphrodite'
import {ReloadableComponent, SerializedComponent} from './reloadable'
import { h } from 'preact'
import { StyleSheet, css } from 'aphrodite'
import { ReloadableComponent, SerializedComponent } from './reloadable'
import {importFromBGFlameGraph} from './import/bg-flamegraph'
import {importFromStackprof} from './import/stackprof'
import {importFromChromeTimeline, importFromChromeCPUProfile} from './import/chrome'
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 {Profile, Frame} from './profile'
import {Flamechart} from './flamechart'
import { Profile, Frame } from './profile'
import { Flamechart } from './flamechart'
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>
)
}
}
@ -188,7 +207,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
flamechartRenderer: null,
sortedFlamechart: null,
sortedFlamechartRenderer: null,
sortOrder: SortOrder.CHRONO
sortOrder: SortOrder.CHRONO,
}
}
@ -205,7 +224,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
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),
})
}
}
@ -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 }, () => {
requestAnimationFrame(() => {
const reader = new FileReader
const reader = new FileReader()
reader.addEventListener('loadend', () => {
this.loadFromString(file.name, reader.result)
})
@ -290,9 +312,11 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
loadExample = () => {
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,34 +367,71 @@ 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) => {
@ -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 {
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
@ -26,11 +40,11 @@ export class CanvasContext {
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)
@ -45,15 +59,18 @@ export class CanvasContext {
},
viewportY: (context: regl.Context, props: SetViewportScopeProps) => {
return props.physicalBounds.top()
}
},
},
viewport: (context, 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) => {
@ -62,12 +79,15 @@ export class CanvasContext {
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(),
},
}
}
},
})
}
@ -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,7 +171,7 @@ 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)
}
@ -163,4 +183,4 @@ export class CanvasContext {
getMaxTextureSize() {
return this.gl.limits.maxTextureSize
}
}
}

View File

@ -1,23 +1,32 @@
import {Frame} from './profile'
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

@ -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,
@ -138,34 +128,33 @@ export class FlamechartColorPassRenderer {
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 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

@ -2,10 +2,10 @@ 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 { 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'
@ -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)))
@ -204,12 +208,14 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
private resizeOverlayCanvasIfNeeded() {
if (!this.overlayCanvas) return
let {width, height} = this.overlayCanvas.getBoundingClientRect()
{/*
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 { CanvasContext } from './canvas-context'
import { Vec2, Rect, AffineTransform } from './math'
import { LRUCache } from './lru-cache'
import { Color } from './color'
import { getOrInsert } from './utils';
import { getOrInsert } from './utils'
const MAX_BATCH_SIZE = 10000
@ -26,15 +26,21 @@ class RowAtlas<K> {
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,17 +241,20 @@ 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
@ -260,16 +279,13 @@ export class FlamechartRenderer {
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 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
@ -295,8 +311,12 @@ 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++) {
@ -314,21 +334,24 @@ 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(),
})
})
})
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))
this.canvasContext.gl.clear({ color: [0, 0, 0, 0] })
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,4 +1,4 @@
import {StyleSheet} from 'aphrodite'
import { StyleSheet } from 'aphrodite'
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

@ -1,13 +1,13 @@
import {h} from 'preact'
import {css} from 'aphrodite'
import {ReloadableComponent} from './reloadable'
import { h } from 'preact'
import { css } from 'aphrodite'
import { ReloadableComponent } from './reloadable'
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 { cachedMeasureTextWidth } from './utils'
import { FlamechartMinimapView } from './flamechart-minimap-view'
import { style, Sizes } from './flamechart-style'
import { FontSize, FontFamily, Colors } from './style'
@ -19,7 +19,13 @@ interface FlamechartFrameLabel {
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()),
)
}
@ -134,12 +145,14 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
private resizeOverlayCanvasIfNeeded() {
if (!this.overlayCanvas) return
let {width, height} = this.overlayCanvas.getBoundingClientRect()
{/*
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)))
@ -280,19 +303,22 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
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)
@ -507,7 +537,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
onWindowKeyPress = (ev: KeyboardEvent) => {
if (!this.container) return
const {width, height} = this.container.getBoundingClientRect()
const { width, height } = this.container.getBoundingClientRect()
if (ev.key === '=' || ev.key === '+') {
this.zoom(new Vec2(width / 2, height / 2), 0.5)
@ -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) {
@ -652,7 +684,7 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
const { hoveredNode, logicalSpaceMouse } = this.state
if (!hoveredNode) return null
const {width, height} = this.container.getBoundingClientRect()
const { width, height } = this.container.getBoundingClientRect()
const positionStyle: {
left?: number
@ -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,4 +1,4 @@
import {Frame, CallTreeNode} from './profile'
import { Frame, CallTreeNode } from './profile'
import { lastOf } from './utils'
@ -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

@ -1,6 +1,6 @@
// https://github.com/brendangregg/FlameGraph#2-fold-stacks
import {Profile, FrameInfo} from '../profile'
import { Profile, FrameInfo } from '../profile'
interface BGSample {
stack: FrameInfo[]
@ -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)
stack: stack.split(';').map(name => ({ key: name, name: name })),
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

@ -1,29 +1,29 @@
import {Profile, TimeFormatter, FrameInfo} from '../profile'
import {getOrInsert, lastOf} from '../utils'
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,
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

@ -1,6 +1,6 @@
// https://github.com/tmm1/stackprof
import {Profile, TimeFormatter, FrameInfo} from '../profile'
import { Profile, TimeFormatter, FrameInfo } from '../profile'
interface StackprofFrame {
name: string
@ -9,7 +9,7 @@ interface StackprofFrame {
}
export interface StackprofProfile {
frames: {[number: string]: StackprofFrame}
frames: { [number: string]: StackprofFrame }
raw: number[]
raw_timestamp_deltas: number[]
}
@ -18,9 +18,9 @@ export function importFromStackprof(stackprofProfile: StackprofProfile): Profile
const duration = stackprofProfile.raw_timestamp_deltas.reduce((a, b) => a + b, 0)
const profile = new Profile(duration)
const {frames, raw, raw_timestamp_deltas} = stackprofProfile
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)
@ -149,4 +159,4 @@ export class LRUCache<K, V> {
this.map.delete(key)
return [key, value]
}
}
}

203
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,34 +120,34 @@ 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)
const { m00, m01, m02, m10, m11, m12 } = this
return new AffineTransform(s * m00, s * m01, s * m02, s * m10, s * m11, s * m12)
}
det() {
const {m00, m01, m02, m10, m11, m12} = this
const { m00, m01, m02, m10, m11, m12 } = this
const m20 = 0
const m21 = 0
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)
)
}
adj() {
const {m00, m01, m02, m10, m11, m12} = this
const { m00, m01, m02, m10, m11, m12 } = this
const m20 = 0
const m21 = 0
const m22 = 1
@ -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,44 +222,60 @@ export class AffineTransform {
flatten(): [number, number, number, number, number, number, number, number, number] {
// Flatten into GLSL format
return [
this.m00, this.m10, 0,
this.m01, this.m11, 0,
this.m02, this.m12, 1,
]
return [this.m00, this.m10, 0, this.m01, this.m11, 0, this.m02, this.m12, 1]
}
}
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 +311,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

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

6
prettier.config.js Normal file
View File

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

View File

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

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

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

@ -1,13 +1,13 @@
import {Component} from 'preact'
import { Component } from 'preact'
export interface SerializedComponent<S> {
state: S
serializedSubcomponents: {[key: string]: any}
serializedSubcomponents: { [key: string]: any }
}
export abstract class ReloadableComponent<P, S> extends Component<P, S> {
serialize(): SerializedComponent<S> {
const serializedSubcomponents: {[key: string]: any} = Object.create(null)
const serializedSubcomponents: { [key: string]: any } = Object.create(null)
const subcomponents = this.subcomponents()
for (const key in subcomponents) {
@ -34,9 +34,7 @@ export abstract class ReloadableComponent<P, S> extends Component<P, S> {
}
})
}
subcomponents(): {[key: string]: any} {
subcomponents(): { [key: string]: any } {
return Object.create(null)
}
}

View File

@ -1,5 +1,5 @@
import {h, render} from 'preact'
import {Application} from'./application'
import { h, render } from 'preact'
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

@ -36,7 +36,7 @@ export class TextureRenderer {
`,
depth: {
enable: false
enable: false,
},
attributes: {
@ -48,18 +48,8 @@ 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: {
@ -68,34 +58,31 @@ export class TextureRenderer {
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 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> {
@ -127,7 +118,7 @@ export class TextureCachedRenderer<T> {
this.textureRenderer = options.textureRenderer
this.texture = gl.texture(1, 1)
this.framebuffer = gl.framebuffer({color: [this.texture]})
this.framebuffer = gl.framebuffer({ color: [this.texture] })
this.withContext = gl({})
}
@ -141,7 +132,10 @@ 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] })
@ -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.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)!
}
}