Merge pull request #17 from alangpierce/set-up-prettier
Set up Prettier and run it on the whole codebase
This commit is contained in:
13
.eslintrc.js
Normal file
13
.eslintrc.js
Normal 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
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
.cache
|
||||
dist
|
||||
.idea
|
||||
|
||||
3
.travis.yml
Normal file
3
.travis.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '9'
|
||||
297
application.tsx
297
application.tsx
@@ -5,21 +5,20 @@ import {ReloadableComponent, SerializedComponent} from './reloadable'
|
||||
import {importFromBGFlameGraph} from './import/bg-flamegraph'
|
||||
import {importFromStackprof} from './import/stackprof'
|
||||
import {importFromChromeTimeline, importFromChromeCPUProfile} from './import/chrome'
|
||||
import { FlamechartRenderer } from './flamechart-renderer'
|
||||
import { CanvasContext } from './canvas-context'
|
||||
import {FlamechartRenderer} from './flamechart-renderer'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
|
||||
import {Profile, Frame} from './profile'
|
||||
import {Flamechart} from './flamechart'
|
||||
import { FlamechartView } from './flamechart-view'
|
||||
import { FontFamily, FontSize, Colors } from './style'
|
||||
|
||||
import {FlamechartView} from './flamechart-view'
|
||||
import {FontFamily, FontSize, Colors} from './style'
|
||||
|
||||
declare function require(x: string): any
|
||||
const exampleProfileURL = require('./sample/perf-vertx-stacks-01-collapsed-all.txt')
|
||||
|
||||
const enum SortOrder {
|
||||
CHRONO,
|
||||
LEFT_HEAVY
|
||||
LEFT_HEAVY,
|
||||
}
|
||||
|
||||
interface ApplicationState {
|
||||
@@ -56,7 +55,7 @@ function importProfile(contents: string, fileName: string): Profile | null {
|
||||
// Second pass: Try to guess what file format it is based on structure
|
||||
try {
|
||||
const parsed = JSON.parse(contents)
|
||||
if (Array.isArray(parsed) && parsed[parsed.length - 1].name === "CpuProfile") {
|
||||
if (Array.isArray(parsed) && parsed[parsed.length - 1].name === 'CpuProfile') {
|
||||
console.log('Importing as Chrome CPU Profile')
|
||||
return importFromChromeTimeline(parsed)
|
||||
} else if ('nodes' in parsed && 'samples' in parsed && 'timeDeltas' in parsed) {
|
||||
@@ -97,31 +96,51 @@ export class Toolbar extends ReloadableComponent<ToolbarProps, void> {
|
||||
render() {
|
||||
const help = (
|
||||
<div className={css(style.toolbarTab)}>
|
||||
<a href="https://github.com/jlfwong/speedscope#usage" className={css(style.noLinkStyle)} target="_blank">
|
||||
<a
|
||||
href="https://github.com/jlfwong/speedscope#usage"
|
||||
className={css(style.noLinkStyle)}
|
||||
target="_blank"
|
||||
>
|
||||
<span className={css(style.emoji)}>❓</span>Help
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (!this.props.profile) {
|
||||
return <div className={css(style.toolbar)}>
|
||||
<div className={css(style.toolbarLeft)}>{help}</div>
|
||||
🔬speedscope
|
||||
</div>
|
||||
return (
|
||||
<div className={css(style.toolbar)}>
|
||||
<div className={css(style.toolbarLeft)}>{help}</div>
|
||||
🔬speedscope
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className={css(style.toolbar)}>
|
||||
<div className={css(style.toolbarLeft)}>
|
||||
<div className={css(style.toolbarTab, this.props.sortOrder === SortOrder.CHRONO && style.toolbarTabActive)} onClick={this.setTimeOrder}>
|
||||
<span className={css(style.emoji)}>🕰</span>Time Order
|
||||
return (
|
||||
<div className={css(style.toolbar)}>
|
||||
<div className={css(style.toolbarLeft)}>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
this.props.sortOrder === SortOrder.CHRONO && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={this.setTimeOrder}
|
||||
>
|
||||
<span className={css(style.emoji)}>🕰</span>Time Order
|
||||
</div>
|
||||
<div
|
||||
className={css(
|
||||
style.toolbarTab,
|
||||
this.props.sortOrder === SortOrder.LEFT_HEAVY && style.toolbarTabActive,
|
||||
)}
|
||||
onClick={this.setLeftHeavyOrder}
|
||||
>
|
||||
<span className={css(style.emoji)}>⬅️</span>Left Heavy
|
||||
</div>
|
||||
{help}
|
||||
</div>
|
||||
<div className={css(style.toolbarTab, this.props.sortOrder === SortOrder.LEFT_HEAVY && style.toolbarTabActive)} onClick={this.setLeftHeavyOrder}>
|
||||
<span className={css(style.emoji)}>⬅️</span>Left Heavy
|
||||
</div>
|
||||
{help}
|
||||
{this.props.profile.getName()}
|
||||
<div className={css(style.toolbarRight)}>🔬speedscope</div>
|
||||
</div>
|
||||
{this.props.profile.getName()}
|
||||
<div className={css(style.toolbarRight)}>🔬speedscope</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +164,7 @@ export class GLCanvas extends ReloadableComponent<GLCanvasProps, void> {
|
||||
|
||||
private maybeResize() {
|
||||
if (!this.canvas) return
|
||||
let { width, height } = this.canvas.getBoundingClientRect()
|
||||
let {width, height} = this.canvas.getBoundingClientRect()
|
||||
width = Math.floor(width) * window.devicePixelRatio
|
||||
height = Math.floor(height) * window.devicePixelRatio
|
||||
|
||||
@@ -188,7 +207,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
flamechartRenderer: null,
|
||||
sortedFlamechart: null,
|
||||
sortedFlamechartRenderer: null,
|
||||
sortOrder: SortOrder.CHRONO
|
||||
sortOrder: SortOrder.CHRONO,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,11 +220,11 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
|
||||
rehydrate(serialized: SerializedComponent<ApplicationState>) {
|
||||
super.rehydrate(serialized)
|
||||
const { flamechart, sortedFlamechart } = serialized.state
|
||||
const {flamechart, sortedFlamechart} = serialized.state
|
||||
if (this.canvasContext && flamechart && sortedFlamechart) {
|
||||
this.setState({
|
||||
flamechartRenderer: new FlamechartRenderer(this.canvasContext, flamechart),
|
||||
sortedFlamechartRenderer: new FlamechartRenderer(this.canvasContext, sortedFlamechart)
|
||||
sortedFlamechartRenderer: new FlamechartRenderer(this.canvasContext, sortedFlamechart),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -216,7 +235,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
console.time('import')
|
||||
const profile = importProfile(contents, fileName)
|
||||
if (profile == null) {
|
||||
this.setState({ loading: false })
|
||||
this.setState({loading: false})
|
||||
// TODO(jlfwong): Make this a nicer overlay
|
||||
alert('Unrecognized format! See documentation about supported formats.')
|
||||
return
|
||||
@@ -248,7 +267,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
getTotalWeight: profile.getTotalWeight.bind(profile),
|
||||
forEachCall: profile.forEachCall.bind(profile),
|
||||
formatValue: profile.formatValue.bind(profile),
|
||||
getColorBucketForFrame
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const flamechartRenderer = new FlamechartRenderer(this.canvasContext, flamechart)
|
||||
|
||||
@@ -256,29 +275,32 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
getTotalWeight: profile.getTotalNonIdleWeight.bind(profile),
|
||||
forEachCall: profile.forEachCallGrouped.bind(profile),
|
||||
formatValue: profile.formatValue.bind(profile),
|
||||
getColorBucketForFrame
|
||||
getColorBucketForFrame,
|
||||
})
|
||||
const sortedFlamechartRenderer = new FlamechartRenderer(this.canvasContext, sortedFlamechart)
|
||||
|
||||
console.timeEnd('import')
|
||||
|
||||
console.time('first setState')
|
||||
this.setState({
|
||||
profile,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
sortedFlamechart,
|
||||
sortedFlamechartRenderer,
|
||||
loading: false
|
||||
}, () => {
|
||||
console.timeEnd('first setState')
|
||||
})
|
||||
this.setState(
|
||||
{
|
||||
profile,
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
sortedFlamechart,
|
||||
sortedFlamechartRenderer,
|
||||
loading: false,
|
||||
},
|
||||
() => {
|
||||
console.timeEnd('first setState')
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
loadFromFile(file: File) {
|
||||
this.setState({ loading: true }, () => {
|
||||
this.setState({loading: true}, () => {
|
||||
requestAnimationFrame(() => {
|
||||
const reader = new FileReader
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener('loadend', () => {
|
||||
this.loadFromString(file.name, reader.result)
|
||||
})
|
||||
@@ -288,11 +310,13 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
}
|
||||
|
||||
loadExample = () => {
|
||||
this.setState({ loading: true })
|
||||
this.setState({loading: true})
|
||||
const filename = 'perf-vertx-stacks-01-collapsed-all.txt'
|
||||
fetch(exampleProfileURL).then(resp => resp.text()).then(data => {
|
||||
this.loadFromString(filename, data)
|
||||
})
|
||||
fetch(exampleProfileURL)
|
||||
.then(resp => resp.text())
|
||||
.then(data => {
|
||||
this.loadFromString(filename, data)
|
||||
})
|
||||
}
|
||||
|
||||
onDrop = (ev: DragEvent) => {
|
||||
@@ -310,11 +334,11 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
onWindowKeyPress = (ev: KeyboardEvent) => {
|
||||
if (ev.key === '1') {
|
||||
this.setState({
|
||||
sortOrder: SortOrder.CHRONO
|
||||
sortOrder: SortOrder.CHRONO,
|
||||
})
|
||||
} else if (ev.key === '2') {
|
||||
this.setState({
|
||||
sortOrder: SortOrder.LEFT_HEAVY
|
||||
sortOrder: SortOrder.LEFT_HEAVY,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -328,10 +352,10 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
}
|
||||
|
||||
flamechartView: FlamechartView | null = null
|
||||
flamechartRef = (view: FlamechartView | null) => this.flamechartView = view
|
||||
flamechartRef = (view: FlamechartView | null) => (this.flamechartView = view)
|
||||
subcomponents() {
|
||||
return {
|
||||
flamechart: this.flamechartView
|
||||
flamechart: this.flamechartView,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,38 +367,75 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
}
|
||||
|
||||
renderLanding() {
|
||||
return <div className={css(style.landingContainer)}>
|
||||
<div className={css(style.landingMessage)}>
|
||||
<p className={css(style.landingP)}>👋 Hi there! Welcome to 🔬speedscope, an interactive{' '}
|
||||
<a className={css(style.link)} href="http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html">flamegraph</a> visualizer.
|
||||
Use it to help you make your software faster.</p>
|
||||
<p className={css(style.landingP)}>Drag and drop a profile file onto this window to get started,
|
||||
click the big blue button below to browse for a profile to explore, or{' '}
|
||||
<a className={css(style.link)} onClick={this.loadExample}>click here</a>{' '}
|
||||
to load an example profile.</p>
|
||||
return (
|
||||
<div className={css(style.landingContainer)}>
|
||||
<div className={css(style.landingMessage)}>
|
||||
<p className={css(style.landingP)}>
|
||||
👋 Hi there! Welcome to 🔬speedscope, an interactive{' '}
|
||||
<a
|
||||
className={css(style.link)}
|
||||
href="http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html"
|
||||
>
|
||||
flamegraph
|
||||
</a>{' '}
|
||||
visualizer. Use it to help you make your software faster.
|
||||
</p>
|
||||
<p className={css(style.landingP)}>
|
||||
Drag and drop a profile file onto this window to get started, click the big blue button
|
||||
below to browse for a profile to explore, or{' '}
|
||||
<a className={css(style.link)} onClick={this.loadExample}>
|
||||
click here
|
||||
</a>{' '}
|
||||
to load an example profile.
|
||||
</p>
|
||||
|
||||
<div className={css(style.browseButtonContainer)}>
|
||||
<input type="file" name="file" id="file" onChange={this.onFileSelect} className={css(style.hide)} />
|
||||
<label for="file" className={css(style.browseButton)}>Browse</label>
|
||||
<div className={css(style.browseButtonContainer)}>
|
||||
<input
|
||||
type="file"
|
||||
name="file"
|
||||
id="file"
|
||||
onChange={this.onFileSelect}
|
||||
className={css(style.hide)}
|
||||
/>
|
||||
<label for="file" className={css(style.browseButton)}>
|
||||
Browse
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p className={css(style.landingP)}>
|
||||
See the{' '}
|
||||
<a
|
||||
className={css(style.link)}
|
||||
href="https://github.com/jlfwong/speedscope#usage"
|
||||
target="_blank"
|
||||
>
|
||||
documentation
|
||||
</a>{' '}
|
||||
for information about supported file formats, keyboard shortcuts, and how to navigate
|
||||
around the profile.
|
||||
</p>
|
||||
|
||||
<p className={css(style.landingP)}>
|
||||
speedscope is open source. Please{' '}
|
||||
<a
|
||||
className={css(style.link)}
|
||||
target="_blank"
|
||||
href="https://github.com/jlfwong/speedscope/issues"
|
||||
>
|
||||
report any issues on GitHub
|
||||
</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className={css(style.landingP)}>See the <a className={css(style.link)}
|
||||
href="https://github.com/jlfwong/speedscope#usage" target="_blank">documentation</a> for
|
||||
information about supported file formats, keyboard shortcuts, and how
|
||||
to navigate around the profile.</p>
|
||||
|
||||
<p className={css(style.landingP)}>speedscope is open source.
|
||||
Please <a className={css(style.link)} target="_blank" href="https://github.com/jlfwong/speedscope/issues">report any issues on GitHub</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderLoadingBar() {
|
||||
return <div className={css(style.loading)}></div>
|
||||
return <div className={css(style.loading)} />
|
||||
}
|
||||
|
||||
setSortOrder = (sortOrder: SortOrder) => {
|
||||
this.setState({ sortOrder })
|
||||
this.setState({sortOrder})
|
||||
}
|
||||
|
||||
private canvasContext: CanvasContext | null = null
|
||||
@@ -383,23 +444,36 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {flamechart, flamechartRenderer, sortedFlamechart, sortedFlamechartRenderer, sortOrder, loading} = this.state
|
||||
const {
|
||||
flamechart,
|
||||
flamechartRenderer,
|
||||
sortedFlamechart,
|
||||
sortedFlamechartRenderer,
|
||||
sortOrder,
|
||||
loading,
|
||||
} = this.state
|
||||
const flamechartToView = sortOrder == SortOrder.CHRONO ? flamechart : sortedFlamechart
|
||||
const flamechartRendererToUse = sortOrder == SortOrder.CHRONO ? flamechartRenderer : sortedFlamechartRenderer
|
||||
const flamechartRendererToUse =
|
||||
sortOrder == SortOrder.CHRONO ? flamechartRenderer : sortedFlamechartRenderer
|
||||
|
||||
return <div onDrop={this.onDrop} onDragOver={this.onDragOver} className={css(style.root)}>
|
||||
<GLCanvas setCanvasContext={this.setCanvasContext} />
|
||||
<Toolbar setSortOrder={this.setSortOrder} {...this.state} />
|
||||
{loading ?
|
||||
this.renderLoadingBar() :
|
||||
this.canvasContext && flamechartToView && flamechartRendererToUse ?
|
||||
return (
|
||||
<div onDrop={this.onDrop} onDragOver={this.onDragOver} className={css(style.root)}>
|
||||
<GLCanvas setCanvasContext={this.setCanvasContext} />
|
||||
<Toolbar setSortOrder={this.setSortOrder} {...this.state} />
|
||||
{loading ? (
|
||||
this.renderLoadingBar()
|
||||
) : this.canvasContext && flamechartToView && flamechartRendererToUse ? (
|
||||
<FlamechartView
|
||||
canvasContext={this.canvasContext}
|
||||
flamechartRenderer={flamechartRendererToUse}
|
||||
ref={this.flamechartRef}
|
||||
flamechart={flamechartToView} /> :
|
||||
this.renderLanding()}
|
||||
</div>
|
||||
flamechart={flamechartToView}
|
||||
/>
|
||||
) : (
|
||||
this.renderLanding()
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,23 +483,25 @@ const style = StyleSheet.create({
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
zIndex: -1,
|
||||
pointerEvents: 'none'
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
loading: {
|
||||
height: 3,
|
||||
marginBottom: -3,
|
||||
background: Colors.DARK_BLUE,
|
||||
transformOrigin: '0% 50%',
|
||||
animationName: [{
|
||||
from: {
|
||||
transform: `scaleX(0)`
|
||||
animationName: [
|
||||
{
|
||||
from: {
|
||||
transform: `scaleX(0)`,
|
||||
},
|
||||
to: {
|
||||
transform: `scaleX(1)`,
|
||||
},
|
||||
},
|
||||
to: {
|
||||
transform: `scaleX(1)`
|
||||
}
|
||||
}],
|
||||
animationTimingFunction: "cubic-bezier(0, 1, 0, 1)",
|
||||
animationDuration: "30s"
|
||||
],
|
||||
animationTimingFunction: 'cubic-bezier(0, 1, 0, 1)',
|
||||
animationDuration: '30s',
|
||||
},
|
||||
root: {
|
||||
width: '100vw',
|
||||
@@ -435,27 +511,27 @@ const style = StyleSheet.create({
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
fontFamily: FontFamily.MONOSPACE,
|
||||
lineHeight: '20px'
|
||||
lineHeight: '20px',
|
||||
},
|
||||
landingContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1
|
||||
flex: 1,
|
||||
},
|
||||
landingMessage: {
|
||||
maxWidth: 600
|
||||
maxWidth: 600,
|
||||
},
|
||||
landingP: {
|
||||
marginBottom: 16
|
||||
marginBottom: 16,
|
||||
},
|
||||
hide: {
|
||||
display: 'none'
|
||||
display: 'none',
|
||||
},
|
||||
browseButtonContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
justifyContent: 'center',
|
||||
},
|
||||
browseButton: {
|
||||
marginBottom: 16,
|
||||
@@ -467,12 +543,12 @@ const style = StyleSheet.create({
|
||||
lineHeight: '72px',
|
||||
background: Colors.DARK_BLUE,
|
||||
color: 'white',
|
||||
cursor: 'pointer'
|
||||
cursor: 'pointer',
|
||||
},
|
||||
link: {
|
||||
color: Colors.LIGHT_BLUE,
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'none'
|
||||
textDecoration: 'none',
|
||||
},
|
||||
toolbar: {
|
||||
height: 18,
|
||||
@@ -482,7 +558,7 @@ const style = StyleSheet.create({
|
||||
fontFamily: FontFamily.MONOSPACE,
|
||||
fontSize: FontSize.TITLE,
|
||||
lineHeight: '18px',
|
||||
userSelect: 'none'
|
||||
userSelect: 'none',
|
||||
},
|
||||
toolbarLeft: {
|
||||
position: 'absolute',
|
||||
@@ -513,24 +589,23 @@ const style = StyleSheet.create({
|
||||
marginLeft: 2,
|
||||
':hover': {
|
||||
background: Colors.GRAY,
|
||||
cursor: 'pointer'
|
||||
}
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
toolbarTabActive: {
|
||||
background: Colors.LIGHT_BLUE,
|
||||
':hover': {
|
||||
background: Colors.LIGHT_BLUE
|
||||
}
|
||||
background: Colors.LIGHT_BLUE,
|
||||
},
|
||||
},
|
||||
noLinkStyle: {
|
||||
textDecoration: 'none',
|
||||
color: 'inherit'
|
||||
color: 'inherit',
|
||||
},
|
||||
emoji: {
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
paddingTop: '0px',
|
||||
marginRight: '0.3em'
|
||||
}
|
||||
marginRight: '0.3em',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
import regl from 'regl'
|
||||
import { RectangleBatchRenderer, RectangleBatch, RectangleBatchRendererProps } from './rectangle-batch-renderer';
|
||||
import { ViewportRectangleRenderer, ViewportRectangleRendererProps } from './overlay-rectangle-renderer';
|
||||
import { TextureCachedRenderer, TextureRenderer, TextureRendererProps } from './texture-catched-renderer'
|
||||
import { StatsPanel } from './stats'
|
||||
import {
|
||||
RectangleBatchRenderer,
|
||||
RectangleBatch,
|
||||
RectangleBatchRendererProps,
|
||||
} from './rectangle-batch-renderer'
|
||||
import {
|
||||
ViewportRectangleRenderer,
|
||||
ViewportRectangleRendererProps,
|
||||
} from './overlay-rectangle-renderer'
|
||||
import {
|
||||
TextureCachedRenderer,
|
||||
TextureRenderer,
|
||||
TextureRendererProps,
|
||||
} from './texture-catched-renderer'
|
||||
import {StatsPanel} from './stats'
|
||||
|
||||
import { Vec2, Rect } from './math';
|
||||
import { FlamechartColorPassRenderer, FlamechartColorPassRenderProps } from './flamechart-color-pass-renderer';
|
||||
import {Vec2, Rect} from './math'
|
||||
import {
|
||||
FlamechartColorPassRenderer,
|
||||
FlamechartColorPassRenderProps,
|
||||
} from './flamechart-color-pass-renderer'
|
||||
|
||||
type FrameCallback = () => void
|
||||
|
||||
@@ -19,25 +33,25 @@ export class CanvasContext {
|
||||
private viewportRectangleRenderer: ViewportRectangleRenderer
|
||||
private textureRenderer: TextureRenderer
|
||||
private flamechartColorPassRenderer: FlamechartColorPassRenderer
|
||||
private setViewportScope: regl.Command<{ physicalBounds: Rect }>
|
||||
private setViewportScope: regl.Command<{physicalBounds: Rect}>
|
||||
private setScissor: regl.Command<{}>
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
this.gl = regl({
|
||||
canvas: canvas,
|
||||
attributes: {
|
||||
antialias: false
|
||||
antialias: false,
|
||||
},
|
||||
extensions: ['ANGLE_instanced_arrays', 'WEBGL_depth_texture'],
|
||||
optionalExtensions: ['EXT_disjoint_timer_query'],
|
||||
profile: true
|
||||
profile: true,
|
||||
})
|
||||
;(window as any)['CanvasContext'] = this
|
||||
this.rectangleBatchRenderer = new RectangleBatchRenderer(this.gl)
|
||||
this.viewportRectangleRenderer = new ViewportRectangleRenderer(this.gl)
|
||||
this.textureRenderer = new TextureRenderer(this.gl)
|
||||
this.flamechartColorPassRenderer = new FlamechartColorPassRenderer(this.gl)
|
||||
this.setScissor = this.gl({ scissor: { enable: true } })
|
||||
this.setScissor = this.gl({scissor: {enable: true}})
|
||||
this.setViewportScope = this.gl<SetViewportScopeProps>({
|
||||
context: {
|
||||
viewportX: (context: regl.Context, props: SetViewportScopeProps) => {
|
||||
@@ -45,29 +59,35 @@ export class CanvasContext {
|
||||
},
|
||||
viewportY: (context: regl.Context, props: SetViewportScopeProps) => {
|
||||
return props.physicalBounds.top()
|
||||
}
|
||||
},
|
||||
},
|
||||
viewport: (context, props) => {
|
||||
const { physicalBounds } = props
|
||||
const {physicalBounds} = props
|
||||
return {
|
||||
x: physicalBounds.left(),
|
||||
y: window.devicePixelRatio * window.innerHeight - physicalBounds.top() - physicalBounds.height(),
|
||||
y:
|
||||
window.devicePixelRatio * window.innerHeight -
|
||||
physicalBounds.top() -
|
||||
physicalBounds.height(),
|
||||
width: physicalBounds.width(),
|
||||
height: physicalBounds.height()
|
||||
height: physicalBounds.height(),
|
||||
}
|
||||
},
|
||||
scissor: (context, props) => {
|
||||
const { physicalBounds } = props
|
||||
const {physicalBounds} = props
|
||||
return {
|
||||
enable: true,
|
||||
box: {
|
||||
x: physicalBounds.left(),
|
||||
y: window.devicePixelRatio * window.innerHeight - physicalBounds.top() - physicalBounds.height(),
|
||||
y:
|
||||
window.devicePixelRatio * window.innerHeight -
|
||||
physicalBounds.top() -
|
||||
physicalBounds.height(),
|
||||
width: physicalBounds.width(),
|
||||
height: physicalBounds.height()
|
||||
}
|
||||
height: physicalBounds.height(),
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -92,7 +112,7 @@ export class CanvasContext {
|
||||
|
||||
private onBeforeFrame = (context: regl.Context) => {
|
||||
this.setScissor(() => {
|
||||
this.gl.clear({ color: [0, 0, 0, 0] })
|
||||
this.gl.clear({color: [0, 0, 0, 0]})
|
||||
})
|
||||
|
||||
this.tickNeeded = false
|
||||
@@ -139,11 +159,11 @@ export class CanvasContext {
|
||||
}): TextureCachedRenderer<T> {
|
||||
return new TextureCachedRenderer(this.gl, {
|
||||
...options,
|
||||
textureRenderer: this.textureRenderer
|
||||
textureRenderer: this.textureRenderer,
|
||||
})
|
||||
}
|
||||
|
||||
drawViewportRectangle(props: ViewportRectangleRendererProps){
|
||||
drawViewportRectangle(props: ViewportRectangleRendererProps) {
|
||||
this.viewportRectangleRenderer.render(props)
|
||||
}
|
||||
|
||||
@@ -151,16 +171,16 @@ export class CanvasContext {
|
||||
const bounds = el.getBoundingClientRect()
|
||||
const physicalBounds = new Rect(
|
||||
new Vec2(bounds.left * window.devicePixelRatio, bounds.top * window.devicePixelRatio),
|
||||
new Vec2(bounds.width * window.devicePixelRatio, bounds.height * window.devicePixelRatio)
|
||||
new Vec2(bounds.width * window.devicePixelRatio, bounds.height * window.devicePixelRatio),
|
||||
)
|
||||
this.setViewportScope({ physicalBounds }, cb)
|
||||
this.setViewportScope({physicalBounds}, cb)
|
||||
}
|
||||
|
||||
setViewport(physicalBounds: Rect, cb: (context: regl.Context) => void) {
|
||||
this.setViewportScope({ physicalBounds }, cb)
|
||||
this.setViewportScope({physicalBounds}, cb)
|
||||
}
|
||||
|
||||
getMaxTextureSize() {
|
||||
return this.gl.limits.maxTextureSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
color.ts
37
color.ts
@@ -1,23 +1,32 @@
|
||||
import {Frame} from './profile'
|
||||
|
||||
export class Color {
|
||||
constructor(readonly r: number = 0, readonly g: number = 0, readonly b: number = 0, readonly a: number = 1) {}
|
||||
constructor(
|
||||
readonly r: number = 0,
|
||||
readonly g: number = 0,
|
||||
readonly b: number = 0,
|
||||
readonly a: number = 1,
|
||||
) {}
|
||||
|
||||
static fromLumaChromaHue(L: number, C: number, H: number) {
|
||||
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_luma/chroma/hue
|
||||
|
||||
const hPrime = H / 60
|
||||
const X = C * (1 - Math.abs(hPrime % 2 - 1))
|
||||
const [R1, G1, B1] = (
|
||||
hPrime < 1 ? [C, X, 0] :
|
||||
hPrime < 2 ? [X, C, 0] :
|
||||
hPrime < 3 ? [0, C, X] :
|
||||
hPrime < 4 ? [0, X, C] :
|
||||
hPrime < 5 ? [X, 0, C] :
|
||||
[C, 0, X]
|
||||
)
|
||||
const [R1, G1, B1] =
|
||||
hPrime < 1
|
||||
? [C, X, 0]
|
||||
: hPrime < 2
|
||||
? [X, C, 0]
|
||||
: hPrime < 3
|
||||
? [0, C, X]
|
||||
: hPrime < 4
|
||||
? [0, X, C]
|
||||
: hPrime < 5
|
||||
? [X, 0, C]
|
||||
: [C, 0, X]
|
||||
|
||||
const m = L - (0.30 * R1 + 0.59 * G1 + 0.11 * B1)
|
||||
const m = L - (0.3 * R1 + 0.59 * G1 + 0.11 * B1)
|
||||
|
||||
return new Color(R1 + m, G1 + m, B1 + m, 1.0)
|
||||
}
|
||||
@@ -64,11 +73,13 @@ export class FrameColorGenerator {
|
||||
const x = 2 * fract(100.0 * ratio) - 1
|
||||
|
||||
const L = 0.85 - 0.1 * x
|
||||
const C = 0.20 + 0.1 * x
|
||||
const C = 0.2 + 0.1 * x
|
||||
const H = 360 * ratio
|
||||
this.frameToColor.set(frames[i], Color.fromLumaChromaHue(L, C, H))
|
||||
}
|
||||
}
|
||||
|
||||
getColorForFrame(f: Frame) { return this.frameToColor.get(f) || new Color() }
|
||||
}
|
||||
getColorForFrame(f: Frame) {
|
||||
return this.frameToColor.get(f) || new Color()
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
import regl from 'regl'
|
||||
import { Vec2, Rect, AffineTransform } from './math'
|
||||
import {Vec2, Rect, AffineTransform} from './math'
|
||||
|
||||
export interface FlamechartColorPassRenderProps {
|
||||
rectInfoTexture: regl.Texture
|
||||
@@ -102,7 +102,7 @@ export class FlamechartColorPassRenderer {
|
||||
`,
|
||||
|
||||
depth: {
|
||||
enable: false
|
||||
enable: false,
|
||||
},
|
||||
|
||||
attributes: {
|
||||
@@ -114,18 +114,8 @@ export class FlamechartColorPassRenderer {
|
||||
// | /|
|
||||
// |/ |
|
||||
// 2 +--+ 3
|
||||
position: gl.buffer([
|
||||
[-1, 1],
|
||||
[1, 1],
|
||||
[-1, -1],
|
||||
[1, -1]
|
||||
]),
|
||||
uv: gl.buffer([
|
||||
[0, 1],
|
||||
[1, 1],
|
||||
[0, 0],
|
||||
[1, 0]
|
||||
])
|
||||
position: gl.buffer([[-1, 1], [1, 1], [-1, -1], [1, -1]]),
|
||||
uv: gl.buffer([[0, 1], [1, 1], [0, 0], [1, 0]]),
|
||||
},
|
||||
|
||||
count: 4,
|
||||
@@ -135,37 +125,36 @@ export class FlamechartColorPassRenderer {
|
||||
uniforms: {
|
||||
colorTexture: (context, props) => props.rectInfoTexture,
|
||||
uvTransform: (context, props) => {
|
||||
const { srcRect, rectInfoTexture } = props
|
||||
const {srcRect, rectInfoTexture} = props
|
||||
const physicalToUV = AffineTransform.withTranslation(new Vec2(0, 1))
|
||||
.times(AffineTransform.withScale(new Vec2(1, -1)))
|
||||
.times(AffineTransform.betweenRects(
|
||||
.times(
|
||||
AffineTransform.betweenRects(
|
||||
new Rect(Vec2.zero, new Vec2(rectInfoTexture.width, rectInfoTexture.height)),
|
||||
Rect.unit
|
||||
))
|
||||
Rect.unit,
|
||||
),
|
||||
)
|
||||
const uvRect = physicalToUV.transformRect(srcRect)
|
||||
return AffineTransform.betweenRects(
|
||||
Rect.unit,
|
||||
uvRect,
|
||||
).flatten()
|
||||
return AffineTransform.betweenRects(Rect.unit, uvRect).flatten()
|
||||
},
|
||||
renderOutlines: (context, props) => {
|
||||
return props.renderOutlines ? 1.0 : 0.0
|
||||
},
|
||||
uvSpacePixelSize: (context, props) => {
|
||||
return Vec2.unit.dividedByPointwise(new Vec2(props.rectInfoTexture.width, props.rectInfoTexture.height)).flatten()
|
||||
return Vec2.unit
|
||||
.dividedByPointwise(new Vec2(props.rectInfoTexture.width, props.rectInfoTexture.height))
|
||||
.flatten()
|
||||
},
|
||||
positionTransform: (context, props) => {
|
||||
const { dstRect } = props
|
||||
const {dstRect} = props
|
||||
const viewportSize = new Vec2(context.viewportWidth, context.viewportHeight)
|
||||
|
||||
const physicalToNDC = AffineTransform.withScale(new Vec2(1, -1))
|
||||
.times(AffineTransform.betweenRects(
|
||||
new Rect(Vec2.zero, viewportSize),
|
||||
Rect.NDC)
|
||||
)
|
||||
const physicalToNDC = AffineTransform.withScale(new Vec2(1, -1)).times(
|
||||
AffineTransform.betweenRects(new Rect(Vec2.zero, viewportSize), Rect.NDC),
|
||||
)
|
||||
const ndcRect = physicalToNDC.transformRect(dstRect)
|
||||
return AffineTransform.betweenRects(Rect.NDC, ndcRect).flatten()
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { h, Component } from 'preact'
|
||||
import { css } from 'aphrodite'
|
||||
import { Flamechart } from './flamechart'
|
||||
import { Rect, Vec2, AffineTransform, clamp } from './math'
|
||||
import { FlamechartRenderer } from "./flamechart-renderer"
|
||||
import { cachedMeasureTextWidth } from "./utils";
|
||||
import { style, Sizes } from "./flamechart-style";
|
||||
import { FontFamily, FontSize, Colors } from "./style"
|
||||
import { CanvasContext } from './canvas-context'
|
||||
import { TextureCachedRenderer } from './texture-catched-renderer'
|
||||
import {h, Component} from 'preact'
|
||||
import {css} from 'aphrodite'
|
||||
import {Flamechart} from './flamechart'
|
||||
import {Rect, Vec2, AffineTransform, clamp} from './math'
|
||||
import {FlamechartRenderer} from './flamechart-renderer'
|
||||
import {cachedMeasureTextWidth} from './utils'
|
||||
import {style, Sizes} from './flamechart-style'
|
||||
import {FontFamily, FontSize, Colors} from './style'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
import {TextureCachedRenderer} from './texture-catched-renderer'
|
||||
|
||||
const DEVICE_PIXEL_RATIO = window.devicePixelRatio
|
||||
|
||||
@@ -24,7 +24,7 @@ interface FlamechartMinimapViewProps {
|
||||
|
||||
enum DraggingMode {
|
||||
DRAW_NEW_VIEWPORT,
|
||||
TRANSLATE_VIEWPORT
|
||||
TRANSLATE_VIEWPORT,
|
||||
}
|
||||
|
||||
export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps, {}> {
|
||||
@@ -39,7 +39,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
private physicalViewSize() {
|
||||
return new Vec2(
|
||||
this.overlayCanvas ? this.overlayCanvas.width : 0,
|
||||
this.overlayCanvas ? this.overlayCanvas.height : 0
|
||||
this.overlayCanvas ? this.overlayCanvas.height : 0,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
private configSpaceSize() {
|
||||
return new Vec2(
|
||||
this.props.flamechart.getTotalWeight(),
|
||||
this.props.flamechart.getLayers().length
|
||||
this.props.flamechart.getLayers().length,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
|
||||
return AffineTransform.betweenRects(
|
||||
new Rect(new Vec2(0, 0), this.configSpaceSize()),
|
||||
new Rect(minimapOrigin, this.physicalViewSize().minus(minimapOrigin))
|
||||
new Rect(minimapOrigin, this.physicalViewSize().minus(minimapOrigin)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -90,20 +90,20 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
}
|
||||
return false
|
||||
},
|
||||
render: (props) => {
|
||||
render: props => {
|
||||
this.props.flamechartRenderer.render({
|
||||
physicalSpaceDstRect: new Rect(
|
||||
this.minimapOrigin(),
|
||||
this.physicalViewSize().minus(this.minimapOrigin())
|
||||
this.physicalViewSize().minus(this.minimapOrigin()),
|
||||
),
|
||||
configSpaceSrcRect: new Rect(new Vec2(0, 0), this.configSpaceSize()),
|
||||
renderOutlines: false
|
||||
renderOutlines: false,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
this.props.canvasContext.renderInto(this.container, (context) => {
|
||||
this.props.canvasContext.renderInto(this.container, context => {
|
||||
// TODO(jlfwong): Switch back to the texture cached renderer once I figure out
|
||||
// how to resize a framebuffer while another framebuffer is active. It seems
|
||||
// to crash regl. I should submit a reduced repro case and hopefully get it fixed?
|
||||
@@ -116,9 +116,9 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
configSpaceSrcRect: new Rect(new Vec2(0, 0), this.configSpaceSize()),
|
||||
physicalSpaceDstRect: new Rect(
|
||||
this.minimapOrigin(),
|
||||
this.physicalViewSize().minus(this.minimapOrigin())
|
||||
this.physicalViewSize().minus(this.minimapOrigin()),
|
||||
),
|
||||
renderOutlines: false
|
||||
renderOutlines: false,
|
||||
})
|
||||
this.props.canvasContext.drawViewportRectangle({
|
||||
configSpaceViewportRect: this.props.configSpaceViewportRect,
|
||||
@@ -147,14 +147,18 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
// 1eN, 2eN, or 5eN for some N
|
||||
|
||||
// Ideally, we want an interval every 100 logical screen pixels
|
||||
const logicalToConfig = (this.configSpaceToPhysicalViewSpace().inverted() || new AffineTransform()).times(this.logicalToPhysicalViewSpace())
|
||||
const logicalToConfig = (
|
||||
this.configSpaceToPhysicalViewSpace().inverted() || new AffineTransform()
|
||||
).times(this.logicalToPhysicalViewSpace())
|
||||
const targetInterval = logicalToConfig.transformVector(new Vec2(200, 1)).x
|
||||
|
||||
const physicalViewSpaceFrameHeight = Sizes.FRAME_HEIGHT * DEVICE_PIXEL_RATIO
|
||||
const physicalViewSpaceFontSize = FontSize.LABEL * DEVICE_PIXEL_RATIO
|
||||
const LABEL_PADDING_PX = (physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2
|
||||
|
||||
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${FontFamily.MONOSPACE}`
|
||||
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${
|
||||
FontFamily.MONOSPACE
|
||||
}`
|
||||
ctx.textBaseline = 'top'
|
||||
|
||||
const minInterval = Math.pow(10, Math.floor(Math.log10(targetInterval)))
|
||||
@@ -205,11 +209,13 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
private resizeOverlayCanvasIfNeeded() {
|
||||
if (!this.overlayCanvas) return
|
||||
let {width, height} = this.overlayCanvas.getBoundingClientRect()
|
||||
{/*
|
||||
{
|
||||
/*
|
||||
We render text at a higher resolution then scale down to
|
||||
ensure we're rendering at 1:1 device pixel ratio.
|
||||
This ensures our text is rendered crisply.
|
||||
*/}
|
||||
*/
|
||||
}
|
||||
width = Math.floor(width)
|
||||
height = Math.floor(height)
|
||||
|
||||
@@ -219,8 +225,8 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
const scaledWidth = width * DEVICE_PIXEL_RATIO
|
||||
const scaledHeight = height * DEVICE_PIXEL_RATIO
|
||||
|
||||
if (scaledWidth === this.overlayCanvas.width &&
|
||||
scaledHeight === this.overlayCanvas.height) return
|
||||
if (scaledWidth === this.overlayCanvas.width && scaledHeight === this.overlayCanvas.height)
|
||||
return
|
||||
|
||||
this.overlayCanvas.width = scaledWidth
|
||||
this.overlayCanvas.height = scaledHeight
|
||||
@@ -252,7 +258,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
private maybeClearInteractionLock = () => {
|
||||
if (this.interactionLock) {
|
||||
if (!this.frameHadWheelEvent) {
|
||||
this.framesWithoutWheelEvents++;
|
||||
this.framesWithoutWheelEvents++
|
||||
if (this.framesWithoutWheelEvents >= 2) {
|
||||
this.interactionLock = null
|
||||
this.framesWithoutWheelEvents = 0
|
||||
@@ -275,11 +281,10 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
private zoom(multiplier: number) {
|
||||
this.interactionLock = 'zoom'
|
||||
const configSpaceViewport = this.props.configSpaceViewportRect
|
||||
const configSpaceCenter = configSpaceViewport.origin.plus(configSpaceViewport.size.times(1/2))
|
||||
const configSpaceCenter = configSpaceViewport.origin.plus(configSpaceViewport.size.times(1 / 2))
|
||||
if (!configSpaceCenter) return
|
||||
|
||||
const zoomTransform = AffineTransform
|
||||
.withTranslation(configSpaceCenter.times(-1))
|
||||
const zoomTransform = AffineTransform.withTranslation(configSpaceCenter.times(-1))
|
||||
.scaledBy(new Vec2(multiplier, 1))
|
||||
.translatedBy(configSpaceCenter)
|
||||
|
||||
@@ -294,13 +299,13 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
const isZoom = ev.metaKey || ev.ctrlKey
|
||||
|
||||
if (isZoom && this.interactionLock !== 'pan') {
|
||||
let multiplier = 1 + (ev.deltaY / 100)
|
||||
let multiplier = 1 + ev.deltaY / 100
|
||||
|
||||
// On Chrome & Firefox, pinch-to-zoom maps to
|
||||
// WheelEvent + Ctrl Key. We'll accelerate it in
|
||||
// this case, since it feels a bit sluggish otherwise.
|
||||
if (ev.ctrlKey) {
|
||||
multiplier = 1 + (ev.deltaY / 40)
|
||||
multiplier = 1 + ev.deltaY / 40
|
||||
}
|
||||
|
||||
multiplier = clamp(multiplier, 0.1, 10.0)
|
||||
@@ -310,13 +315,16 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
this.pan(new Vec2(ev.deltaX, ev.deltaY))
|
||||
}
|
||||
|
||||
|
||||
this.renderCanvas()
|
||||
}
|
||||
|
||||
private configSpaceMouse(ev: MouseEvent): Vec2 | null {
|
||||
const logicalSpaceMouse = this.windowToLogicalViewSpace().transformPosition(new Vec2(ev.clientX, ev.clientY))
|
||||
const physicalSpaceMouse = this.logicalToPhysicalViewSpace().transformPosition(logicalSpaceMouse)
|
||||
const logicalSpaceMouse = this.windowToLogicalViewSpace().transformPosition(
|
||||
new Vec2(ev.clientX, ev.clientY),
|
||||
)
|
||||
const physicalSpaceMouse = this.logicalToPhysicalViewSpace().transformPosition(
|
||||
logicalSpaceMouse,
|
||||
)
|
||||
return this.configSpaceToPhysicalViewSpace().inverseTransformPosition(physicalSpaceMouse)
|
||||
}
|
||||
|
||||
@@ -331,7 +339,9 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
// If dragging starting inside the viewport rectangle,
|
||||
// we'll move the existing viewport
|
||||
this.draggingMode = DraggingMode.TRANSLATE_VIEWPORT
|
||||
this.dragConfigSpaceViewportOffset = configSpaceMouse.minus(this.props.configSpaceViewportRect.origin)
|
||||
this.dragConfigSpaceViewportOffset = configSpaceMouse.minus(
|
||||
this.props.configSpaceViewportRect.origin,
|
||||
)
|
||||
} else {
|
||||
// If dragging starts outside the the viewport rectangle,
|
||||
// we'll start drawing a new viewport
|
||||
@@ -353,7 +363,9 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
this.updateCursor(configSpaceMouse)
|
||||
|
||||
// Clamp the mouse position to avoid weird behavior when outside the canvas bounds
|
||||
configSpaceMouse = new Rect(new Vec2(0, 0), this.configSpaceSize()).closestPointTo(configSpaceMouse)
|
||||
configSpaceMouse = new Rect(new Vec2(0, 0), this.configSpaceSize()).closestPointTo(
|
||||
configSpaceMouse,
|
||||
)
|
||||
|
||||
if (this.draggingMode === DraggingMode.DRAW_NEW_VIEWPORT) {
|
||||
const configStart = this.dragStartConfigSpaceMouse
|
||||
@@ -366,16 +378,15 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
const width = right - left
|
||||
const height = this.props.configSpaceViewportRect.height()
|
||||
|
||||
this.props.setConfigSpaceViewportRect(new Rect(
|
||||
new Vec2(left, configEnd.y - height / 2),
|
||||
new Vec2(width, height)
|
||||
))
|
||||
this.props.setConfigSpaceViewportRect(
|
||||
new Rect(new Vec2(left, configEnd.y - height / 2), new Vec2(width, height)),
|
||||
)
|
||||
} else if (this.draggingMode === DraggingMode.TRANSLATE_VIEWPORT) {
|
||||
if (!this.dragConfigSpaceViewportOffset) return
|
||||
|
||||
const newOrigin = configSpaceMouse.minus(this.dragConfigSpaceViewportOffset)
|
||||
this.props.setConfigSpaceViewportRect(
|
||||
this.props.configSpaceViewportRect.withOrigin(newOrigin)
|
||||
this.props.configSpaceViewportRect.withOrigin(newOrigin),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -428,11 +439,9 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
||||
onWheel={this.onWheel}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onMouseMove={this.onMouseMove}
|
||||
className={css(style.minimap, style.vbox)} >
|
||||
<canvas
|
||||
width={1} height={1}
|
||||
ref={this.overlayCanvasRef}
|
||||
className={css(style.fill)} />
|
||||
className={css(style.minimap, style.vbox)}
|
||||
>
|
||||
<canvas width={1} height={1} ref={this.overlayCanvasRef} className={css(style.fill)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import regl from 'regl'
|
||||
import { Flamechart } from './flamechart'
|
||||
import { RectangleBatch } from './rectangle-batch-renderer'
|
||||
import { CanvasContext } from './canvas-context';
|
||||
import { Vec2, Rect, AffineTransform } from './math'
|
||||
import { LRUCache } from './lru-cache'
|
||||
import { Color } from './color'
|
||||
import { getOrInsert } from './utils';
|
||||
import {Flamechart} from './flamechart'
|
||||
import {RectangleBatch} from './rectangle-batch-renderer'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
import {Vec2, Rect, AffineTransform} from './math'
|
||||
import {LRUCache} from './lru-cache'
|
||||
import {Color} from './color'
|
||||
import {getOrInsert} from './utils'
|
||||
|
||||
const MAX_BATCH_SIZE = 10000
|
||||
|
||||
@@ -23,18 +23,24 @@ class RowAtlas<K> {
|
||||
wrapS: 'clamp',
|
||||
wrapT: 'clamp',
|
||||
})
|
||||
this.framebuffer = canvasContext.gl.framebuffer({ color: [this.texture] })
|
||||
this.framebuffer = canvasContext.gl.framebuffer({color: [this.texture]})
|
||||
this.rowCache = new LRUCache(this.texture.height)
|
||||
this.renderToFramebuffer = canvasContext.gl({
|
||||
framebuffer: this.framebuffer
|
||||
framebuffer: this.framebuffer,
|
||||
})
|
||||
this.clearLineBatch = canvasContext.createRectangleBatch()
|
||||
this.clearLineBatch.addRect(Rect.unit, new Color(0, 0, 0, 0))
|
||||
}
|
||||
|
||||
has(key: K) { return this.rowCache.has(key) }
|
||||
getResolution() { return this.texture.width }
|
||||
getCapacity() { return this.texture.height }
|
||||
has(key: K) {
|
||||
return this.rowCache.has(key)
|
||||
}
|
||||
getResolution() {
|
||||
return this.texture.width
|
||||
}
|
||||
getCapacity() {
|
||||
return this.texture.height
|
||||
}
|
||||
|
||||
private allocateLine(key: K): number {
|
||||
if (this.rowCache.getSize() < this.rowCache.getCapacity()) {
|
||||
@@ -50,10 +56,7 @@ class RowAtlas<K> {
|
||||
}
|
||||
}
|
||||
|
||||
writeToAtlasIfNeeded(
|
||||
keys: K[],
|
||||
render: (textureDstRect: Rect, key: K) => void
|
||||
) {
|
||||
writeToAtlasIfNeeded(keys: K[], render: (textureDstRect: Rect, key: K) => void) {
|
||||
this.renderToFramebuffer((context: regl.Context) => {
|
||||
for (let key of keys) {
|
||||
let row = this.rowCache.get(key)
|
||||
@@ -64,14 +67,11 @@ class RowAtlas<K> {
|
||||
// Not cached -- we'll have to actually render
|
||||
row = this.allocateLine(key)
|
||||
|
||||
const textureRect = new Rect(
|
||||
new Vec2(0, row),
|
||||
new Vec2(this.texture.width, 1)
|
||||
)
|
||||
const textureRect = new Rect(new Vec2(0, row), new Vec2(this.texture.width, 1))
|
||||
this.canvasContext.drawRectangleBatch({
|
||||
batch: this.clearLineBatch,
|
||||
configSpaceSrcRect: Rect.unit,
|
||||
physicalSpaceDstRect: textureRect
|
||||
physicalSpaceDstRect: textureRect,
|
||||
})
|
||||
render(textureRect, key)
|
||||
}
|
||||
@@ -84,17 +84,14 @@ class RowAtlas<K> {
|
||||
return false
|
||||
}
|
||||
|
||||
const textureRect = new Rect(
|
||||
new Vec2(0, row),
|
||||
new Vec2(this.texture.width, 1)
|
||||
)
|
||||
const textureRect = new Rect(new Vec2(0, row), new Vec2(this.texture.width, 1))
|
||||
|
||||
// At this point, we have the row in cache, and we can
|
||||
// paint directly from it into the framebuffer.
|
||||
this.canvasContext.drawTexture({
|
||||
texture: this.texture,
|
||||
srcRect: textureRect,
|
||||
dstRect: dstRect
|
||||
dstRect: dstRect,
|
||||
})
|
||||
return true
|
||||
}
|
||||
@@ -113,16 +110,26 @@ class RangeTreeLeafNode implements RangeTreeNode {
|
||||
constructor(
|
||||
private batch: RectangleBatch,
|
||||
private bounds: Rect,
|
||||
private numPrecedingRectanglesInRow: number
|
||||
private numPrecedingRectanglesInRow: number,
|
||||
) {
|
||||
batch.uploadToGPU()
|
||||
}
|
||||
|
||||
getBatch() { return this.batch }
|
||||
getBounds() { return this.bounds }
|
||||
getRectCount() { return this.batch.getRectCount() }
|
||||
getChildren() { return this.children }
|
||||
getParity() { return this.numPrecedingRectanglesInRow % 2 }
|
||||
getBatch() {
|
||||
return this.batch
|
||||
}
|
||||
getBounds() {
|
||||
return this.bounds
|
||||
}
|
||||
getRectCount() {
|
||||
return this.batch.getRectCount()
|
||||
}
|
||||
getChildren() {
|
||||
return this.children
|
||||
}
|
||||
getParity() {
|
||||
return this.numPrecedingRectanglesInRow % 2
|
||||
}
|
||||
forEachLeafNodeWithinBounds(configSpaceBounds: Rect, cb: (leaf: RangeTreeLeafNode) => void) {
|
||||
if (!this.bounds.hasIntersectionWith(configSpaceBounds)) return
|
||||
cb(this)
|
||||
@@ -134,7 +141,7 @@ class RangeTreeInteriorNode implements RangeTreeNode {
|
||||
private bounds: Rect
|
||||
constructor(private children: RangeTreeNode[]) {
|
||||
if (children.length === 0) {
|
||||
throw new Error("Empty interior node")
|
||||
throw new Error('Empty interior node')
|
||||
}
|
||||
let minLeft = Infinity
|
||||
let maxRight = -Infinity
|
||||
@@ -150,13 +157,19 @@ class RangeTreeInteriorNode implements RangeTreeNode {
|
||||
}
|
||||
this.bounds = new Rect(
|
||||
new Vec2(minLeft, minTop),
|
||||
new Vec2(maxRight - minLeft, maxBottom - minTop)
|
||||
new Vec2(maxRight - minLeft, maxBottom - minTop),
|
||||
)
|
||||
}
|
||||
|
||||
getBounds() { return this.bounds }
|
||||
getRectCount() { return this.rectCount }
|
||||
getChildren() { return this.children }
|
||||
getBounds() {
|
||||
return this.bounds
|
||||
}
|
||||
getRectCount() {
|
||||
return this.rectCount
|
||||
}
|
||||
getChildren() {
|
||||
return this.children
|
||||
}
|
||||
|
||||
forEachLeafNodeWithinBounds(configSpaceBounds: Rect, cb: (leaf: RangeTreeLeafNode) => void) {
|
||||
if (!this.bounds.hasIntersectionWith(configSpaceBounds)) return
|
||||
@@ -202,17 +215,20 @@ export class FlamechartRenderer {
|
||||
for (let i = 0; i < layer.length; i++) {
|
||||
const frame = layer[i]
|
||||
if (batch.getRectCount() >= MAX_BATCH_SIZE) {
|
||||
leafNodes.push(new RangeTreeLeafNode(batch, new Rect(
|
||||
new Vec2(minLeft, stackDepth),
|
||||
new Vec2(maxRight - minLeft, 1)
|
||||
), rectCount))
|
||||
leafNodes.push(
|
||||
new RangeTreeLeafNode(
|
||||
batch,
|
||||
new Rect(new Vec2(minLeft, stackDepth), new Vec2(maxRight - minLeft, 1)),
|
||||
rectCount,
|
||||
),
|
||||
)
|
||||
minLeft = Infinity
|
||||
maxRight = -Infinity
|
||||
batch = canvasContext.createRectangleBatch()
|
||||
}
|
||||
const configSpaceBounds = new Rect(
|
||||
new Vec2(frame.start, y),
|
||||
new Vec2(frame.end - frame.start, 1)
|
||||
new Vec2(frame.end - frame.start, 1),
|
||||
)
|
||||
minLeft = Math.min(minLeft, configSpaceBounds.left())
|
||||
maxRight = Math.max(maxRight, configSpaceBounds.right())
|
||||
@@ -225,24 +241,27 @@ export class FlamechartRenderer {
|
||||
const color = new Color(
|
||||
(1 + i % 255) / 256,
|
||||
(1 + stackDepth % 255) / 256,
|
||||
(1 + this.flamechart.getColorBucketForFrame(frame.node.frame)) / 256
|
||||
(1 + this.flamechart.getColorBucketForFrame(frame.node.frame)) / 256,
|
||||
)
|
||||
batch.addRect(configSpaceBounds, color)
|
||||
rectCount++
|
||||
}
|
||||
|
||||
if (batch.getRectCount() > 0) {
|
||||
leafNodes.push(new RangeTreeLeafNode(batch, new Rect(
|
||||
new Vec2(minLeft, stackDepth),
|
||||
new Vec2(maxRight - minLeft, 1)
|
||||
), rectCount))
|
||||
leafNodes.push(
|
||||
new RangeTreeLeafNode(
|
||||
batch,
|
||||
new Rect(new Vec2(minLeft, stackDepth), new Vec2(maxRight - minLeft, 1)),
|
||||
rectCount,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO(jlfwong): Making this into a binary tree
|
||||
// range than a tree of always-height-two might make this run faster
|
||||
this.layers.push(new RangeTreeInteriorNode(leafNodes))
|
||||
}
|
||||
this.rectInfoTexture = this.canvasContext.gl.texture({ width: 1, height: 1 })
|
||||
this.rectInfoTexture = this.canvasContext.gl.texture({width: 1, height: 1})
|
||||
this.framebuffer = this.canvasContext.gl.framebuffer({
|
||||
color: [this.rectInfoTexture],
|
||||
})
|
||||
@@ -255,21 +274,18 @@ export class FlamechartRenderer {
|
||||
}
|
||||
|
||||
configSpaceBoundsForKey(key: FlamechartRowAtlasKey): Rect {
|
||||
const { stackDepth, zoomLevel, index } = key
|
||||
const {stackDepth, zoomLevel, index} = key
|
||||
const configSpaceContentWidth = this.flamechart.getTotalWeight()
|
||||
|
||||
const width = configSpaceContentWidth / Math.pow(2, zoomLevel)
|
||||
|
||||
return new Rect(
|
||||
new Vec2(width * index, stackDepth),
|
||||
new Vec2(width, 1)
|
||||
)
|
||||
return new Rect(new Vec2(width * index, stackDepth), new Vec2(width, 1))
|
||||
}
|
||||
|
||||
render(props: FlamechartRendererProps) {
|
||||
const { configSpaceSrcRect, physicalSpaceDstRect } = props
|
||||
const {configSpaceSrcRect, physicalSpaceDstRect} = props
|
||||
|
||||
const atlasKeysToRender: { stackDepth: number, zoomLevel: number, index: number }[] = []
|
||||
const atlasKeysToRender: {stackDepth: number; zoomLevel: number; index: number}[] = []
|
||||
|
||||
// We want to render the lowest resolution we can while still guaranteeing that the
|
||||
// atlas line is higher resolution than its corresponding destination rectangle on
|
||||
@@ -282,7 +298,7 @@ export class FlamechartRenderer {
|
||||
|
||||
let zoomLevel = 0
|
||||
while (true) {
|
||||
const configSpaceBounds = this.configSpaceBoundsForKey({ stackDepth: 0, zoomLevel, index: 0 })
|
||||
const configSpaceBounds = this.configSpaceBoundsForKey({stackDepth: 0, zoomLevel, index: 0})
|
||||
const physicalBounds = configToPhysical.transformRect(configSpaceBounds)
|
||||
if (physicalBounds.width() < this.rowAtlas.getResolution()) {
|
||||
break
|
||||
@@ -295,12 +311,16 @@ export class FlamechartRenderer {
|
||||
|
||||
const configSpaceContentWidth = this.flamechart.getTotalWeight()
|
||||
const numAtlasEntriesPerLayer = Math.pow(2, zoomLevel)
|
||||
const left = Math.floor(numAtlasEntriesPerLayer * configSpaceSrcRect.left() / configSpaceContentWidth)
|
||||
const right = Math.ceil(numAtlasEntriesPerLayer * configSpaceSrcRect.right() / configSpaceContentWidth)
|
||||
const left = Math.floor(
|
||||
numAtlasEntriesPerLayer * configSpaceSrcRect.left() / configSpaceContentWidth,
|
||||
)
|
||||
const right = Math.ceil(
|
||||
numAtlasEntriesPerLayer * configSpaceSrcRect.right() / configSpaceContentWidth,
|
||||
)
|
||||
|
||||
for (let stackDepth = top; stackDepth < bottom; stackDepth++) {
|
||||
for (let index = left; index <= right; index++) {
|
||||
const key = this.getOrInsertKey({ stackDepth, zoomLevel, index })
|
||||
const key = this.getOrInsertKey({stackDepth, zoomLevel, index})
|
||||
const configSpaceBounds = this.configSpaceBoundsForKey(key)
|
||||
if (!configSpaceBounds.hasIntersectionWith(configSpaceSrcRect)) continue
|
||||
atlasKeysToRender.push(key)
|
||||
@@ -314,13 +334,13 @@ export class FlamechartRenderer {
|
||||
// Fill the cache
|
||||
this.rowAtlas.writeToAtlasIfNeeded(keysToRenderCached, (textureDstRect, key) => {
|
||||
const configSpaceBounds = this.configSpaceBoundsForKey(key)
|
||||
this.layers[key.stackDepth].forEachLeafNodeWithinBounds(configSpaceBounds, (leaf) => {
|
||||
this.layers[key.stackDepth].forEachLeafNodeWithinBounds(configSpaceBounds, leaf => {
|
||||
this.canvasContext.drawRectangleBatch({
|
||||
batch: leaf.getBatch(),
|
||||
configSpaceSrcRect: configSpaceBounds,
|
||||
physicalSpaceDstRect: textureDstRect,
|
||||
parityMin: key.stackDepth % 2 == 0 ? 2 : 0,
|
||||
parityOffset: leaf.getParity()
|
||||
parityOffset: leaf.getParity(),
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -328,7 +348,10 @@ export class FlamechartRenderer {
|
||||
this.framebuffer.resize(physicalSpaceDstRect.width(), physicalSpaceDstRect.height())
|
||||
this.framebuffer.use(context => {
|
||||
this.canvasContext.gl.clear({color: [0, 0, 0, 0]})
|
||||
const viewportRect = new Rect(Vec2.zero, new Vec2(context.viewportWidth, context.viewportHeight))
|
||||
const viewportRect = new Rect(
|
||||
Vec2.zero,
|
||||
new Vec2(context.viewportWidth, context.viewportHeight),
|
||||
)
|
||||
|
||||
const configToViewport = AffineTransform.betweenRects(configSpaceSrcRect, viewportRect)
|
||||
|
||||
@@ -342,13 +365,13 @@ export class FlamechartRenderer {
|
||||
for (let key of keysToRenderUncached) {
|
||||
const configSpaceBounds = this.configSpaceBoundsForKey(key)
|
||||
const physicalBounds = configToViewport.transformRect(configSpaceBounds)
|
||||
this.layers[key.stackDepth].forEachLeafNodeWithinBounds(configSpaceBounds, (leaf) => {
|
||||
this.layers[key.stackDepth].forEachLeafNodeWithinBounds(configSpaceBounds, leaf => {
|
||||
this.canvasContext.drawRectangleBatch({
|
||||
batch: leaf.getBatch(),
|
||||
configSpaceSrcRect,
|
||||
physicalSpaceDstRect: physicalBounds,
|
||||
parityMin: key.stackDepth % 2 == 0 ? 2 : 0,
|
||||
parityOffset: leaf.getParity()
|
||||
parityOffset: leaf.getParity(),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -356,9 +379,12 @@ export class FlamechartRenderer {
|
||||
|
||||
this.canvasContext.drawFlamechartColorPass({
|
||||
rectInfoTexture: this.rectInfoTexture,
|
||||
srcRect: new Rect(Vec2.zero, new Vec2(this.rectInfoTexture.width, this.rectInfoTexture.height)),
|
||||
srcRect: new Rect(
|
||||
Vec2.zero,
|
||||
new Vec2(this.rectInfoTexture.width, this.rectInfoTexture.height),
|
||||
),
|
||||
dstRect: physicalSpaceDstRect,
|
||||
renderOutlines: props.renderOutlines
|
||||
renderOutlines: props.renderOutlines,
|
||||
})
|
||||
|
||||
// Overlay the atlas on top of the canvas for debugging
|
||||
@@ -370,4 +396,4 @@ export class FlamechartRenderer {
|
||||
})
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {StyleSheet} from 'aphrodite'
|
||||
import { FontFamily, FontSize, Colors } from './style'
|
||||
import {FontFamily, FontSize, Colors} from './style'
|
||||
|
||||
const HOVERTIP_PADDING = 2
|
||||
|
||||
@@ -22,7 +22,7 @@ export const style = StyleSheet.create({
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
fontSize: FontSize.LABEL,
|
||||
fontFamily: FontFamily.MONOSPACE
|
||||
fontFamily: FontFamily.MONOSPACE,
|
||||
},
|
||||
hoverTipRow: {
|
||||
textOverflow: 'ellipsis',
|
||||
@@ -33,10 +33,10 @@ export const style = StyleSheet.create({
|
||||
maxWidth: Sizes.TOOLTIP_WIDTH_MAX,
|
||||
},
|
||||
hoverCount: {
|
||||
color: '#6FCF97'
|
||||
color: '#6FCF97',
|
||||
},
|
||||
clip: {
|
||||
overflow: 'hidden'
|
||||
overflow: 'hidden',
|
||||
},
|
||||
vbox: {
|
||||
display: 'flex',
|
||||
@@ -48,13 +48,13 @@ export const style = StyleSheet.create({
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0
|
||||
top: 0,
|
||||
},
|
||||
minimap: {
|
||||
height: Sizes.MINIMAP_HEIGHT,
|
||||
borderBottom: `${Sizes.SEPARATOR_HEIGHT}px solid ${Colors.MEDIUM_GRAY}`
|
||||
borderBottom: `${Sizes.SEPARATOR_HEIGHT}px solid ${Colors.MEDIUM_GRAY}`,
|
||||
},
|
||||
panZoomView: {
|
||||
flex: 1
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -2,24 +2,30 @@ import {h} from 'preact'
|
||||
import {css} from 'aphrodite'
|
||||
import {ReloadableComponent} from './reloadable'
|
||||
|
||||
import { CallTreeNode } from './profile'
|
||||
import { Flamechart, FlamechartFrame } from './flamechart'
|
||||
import {CallTreeNode} from './profile'
|
||||
import {Flamechart, FlamechartFrame} from './flamechart'
|
||||
|
||||
import { Rect, Vec2, AffineTransform, clamp } from './math'
|
||||
import { cachedMeasureTextWidth } from "./utils";
|
||||
import { FlamechartMinimapView } from "./flamechart-minimap-view"
|
||||
import {Rect, Vec2, AffineTransform, clamp} from './math'
|
||||
import {cachedMeasureTextWidth} from './utils'
|
||||
import {FlamechartMinimapView} from './flamechart-minimap-view'
|
||||
|
||||
import { style, Sizes } from './flamechart-style'
|
||||
import { FontSize, FontFamily, Colors } from './style'
|
||||
import { CanvasContext } from './canvas-context'
|
||||
import { FlamechartRenderer } from './flamechart-renderer'
|
||||
import {style, Sizes} from './flamechart-style'
|
||||
import {FontSize, FontFamily, Colors} from './style'
|
||||
import {CanvasContext} from './canvas-context'
|
||||
import {FlamechartRenderer} from './flamechart-renderer'
|
||||
|
||||
interface FlamechartFrameLabel {
|
||||
configSpaceBounds: Rect
|
||||
node: CallTreeNode
|
||||
}
|
||||
|
||||
function binarySearch(lo: number, hi: number, f: (val: number) => number, target: number, targetRangeSize = 1): [number, number] {
|
||||
function binarySearch(
|
||||
lo: number,
|
||||
hi: number,
|
||||
f: (val: number) => number,
|
||||
target: number,
|
||||
targetRangeSize = 1,
|
||||
): [number, number] {
|
||||
console.assert(!isNaN(targetRangeSize) && !isNaN(target))
|
||||
while (true) {
|
||||
if (hi - lo <= targetRangeSize) return [lo, hi]
|
||||
@@ -41,9 +47,14 @@ function buildTrimmedText(text: string, length: number) {
|
||||
|
||||
function trimTextMid(ctx: CanvasRenderingContext2D, text: string, maxWidth: number) {
|
||||
if (cachedMeasureTextWidth(ctx, text) <= maxWidth) return text
|
||||
const [lo,] = binarySearch(0, text.length, (n) => {
|
||||
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, n))
|
||||
}, maxWidth)
|
||||
const [lo] = binarySearch(
|
||||
0,
|
||||
text.length,
|
||||
n => {
|
||||
return cachedMeasureTextWidth(ctx, buildTrimmedText(text, n))
|
||||
},
|
||||
maxWidth,
|
||||
)
|
||||
return buildTrimmedText(text, lo)
|
||||
}
|
||||
|
||||
@@ -108,14 +119,14 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
private configSpaceSize() {
|
||||
return new Vec2(
|
||||
this.props.flamechart.getTotalWeight(),
|
||||
this.props.flamechart.getLayers().length
|
||||
this.props.flamechart.getLayers().length,
|
||||
)
|
||||
}
|
||||
|
||||
private physicalViewSize() {
|
||||
return new Vec2(
|
||||
this.overlayCanvas ? this.overlayCanvas.width : 0,
|
||||
this.overlayCanvas ? this.overlayCanvas.height : 0
|
||||
this.overlayCanvas ? this.overlayCanvas.height : 0,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -124,7 +135,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
private configSpaceToPhysicalViewSpace() {
|
||||
return AffineTransform.betweenRects(
|
||||
this.props.configSpaceViewportRect,
|
||||
new Rect(new Vec2(0, 0), this.physicalViewSize())
|
||||
new Rect(new Vec2(0, 0), this.physicalViewSize()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -135,11 +146,13 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
private resizeOverlayCanvasIfNeeded() {
|
||||
if (!this.overlayCanvas) return
|
||||
let {width, height} = this.overlayCanvas.getBoundingClientRect()
|
||||
{/*
|
||||
{
|
||||
/*
|
||||
We render text at a higher resolution then scale down to
|
||||
ensure we're rendering at 1:1 device pixel ratio.
|
||||
This ensures our text is rendered crisply.
|
||||
*/}
|
||||
*/
|
||||
}
|
||||
width = Math.floor(width)
|
||||
height = Math.floor(height)
|
||||
|
||||
@@ -149,8 +162,8 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
const scaledWidth = width * DEVICE_PIXEL_RATIO
|
||||
const scaledHeight = height * DEVICE_PIXEL_RATIO
|
||||
|
||||
if (scaledWidth === this.overlayCanvas.width &&
|
||||
scaledHeight === this.overlayCanvas.height) return
|
||||
if (scaledWidth === this.overlayCanvas.width && scaledHeight === this.overlayCanvas.height)
|
||||
return
|
||||
|
||||
this.overlayCanvas.width = scaledWidth
|
||||
this.overlayCanvas.height = scaledHeight
|
||||
@@ -177,27 +190,30 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
if (this.hoveredLabel) {
|
||||
const physicalViewBounds = configToPhysical.transformRect(this.hoveredLabel.configSpaceBounds)
|
||||
ctx.strokeRect(
|
||||
Math.floor(physicalViewBounds.left()), Math.floor(physicalViewBounds.top()),
|
||||
Math.floor(physicalViewBounds.width()), Math.floor(physicalViewBounds.height())
|
||||
Math.floor(physicalViewBounds.left()),
|
||||
Math.floor(physicalViewBounds.top()),
|
||||
Math.floor(physicalViewBounds.width()),
|
||||
Math.floor(physicalViewBounds.height()),
|
||||
)
|
||||
}
|
||||
|
||||
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${FontFamily.MONOSPACE}`
|
||||
ctx.font = `${physicalViewSpaceFontSize}px/${physicalViewSpaceFrameHeight}px ${
|
||||
FontFamily.MONOSPACE
|
||||
}`
|
||||
ctx.fillStyle = Colors.DARK_GRAY
|
||||
ctx.textBaseline = 'top'
|
||||
|
||||
const minWidthToRender = cachedMeasureTextWidth(ctx, 'M' + ELLIPSIS + 'M')
|
||||
const minConfigSpaceWidthToRender = (configToPhysical.inverseTransformVector(new Vec2(minWidthToRender, 0)) || new Vec2(0, 0)).x
|
||||
const minConfigSpaceWidthToRender = (
|
||||
configToPhysical.inverseTransformVector(new Vec2(minWidthToRender, 0)) || new Vec2(0, 0)
|
||||
).x
|
||||
const LABEL_PADDING_PX = (physicalViewSpaceFrameHeight - physicalViewSpaceFontSize) / 2
|
||||
const PADDING_OFFSET = new Vec2(LABEL_PADDING_PX, LABEL_PADDING_PX)
|
||||
const SIZE_OFFSET = new Vec2(2 * LABEL_PADDING_PX, 2 * LABEL_PADDING_PX)
|
||||
|
||||
const renderFrameLabelAndChildren = (frame: FlamechartFrame, depth = 0) => {
|
||||
const width = frame.end - frame.start
|
||||
const configSpaceBounds = new Rect(
|
||||
new Vec2(frame.start, depth),
|
||||
new Vec2(width, 1)
|
||||
)
|
||||
const configSpaceBounds = new Rect(new Vec2(frame.start, depth), new Vec2(width, 1))
|
||||
|
||||
if (width < minConfigSpaceWidthToRender) return
|
||||
if (configSpaceBounds.left() > this.props.configSpaceViewportRect.right()) return
|
||||
@@ -210,11 +226,16 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
if (physicalLabelBounds.left() < 0) {
|
||||
physicalLabelBounds = physicalLabelBounds
|
||||
.withOrigin(physicalLabelBounds.origin.withX(0))
|
||||
.withSize(physicalLabelBounds.size.withX(physicalLabelBounds.size.x + physicalLabelBounds.left()))
|
||||
.withSize(
|
||||
physicalLabelBounds.size.withX(
|
||||
physicalLabelBounds.size.x + physicalLabelBounds.left(),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (physicalLabelBounds.right() > physicalViewSize.x) {
|
||||
physicalLabelBounds = physicalLabelBounds
|
||||
.withSize(physicalLabelBounds.size.withX(physicalViewSize.x - physicalLabelBounds.left()))
|
||||
physicalLabelBounds = physicalLabelBounds.withSize(
|
||||
physicalLabelBounds.size.withX(physicalViewSize.x - physicalLabelBounds.left()),
|
||||
)
|
||||
}
|
||||
|
||||
physicalLabelBounds = physicalLabelBounds
|
||||
@@ -230,7 +251,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
}
|
||||
}
|
||||
|
||||
for (let frame of (this.props.flamechart.getLayers()[0] || [])) {
|
||||
for (let frame of this.props.flamechart.getLayers()[0] || []) {
|
||||
renderFrameLabelAndChildren(frame)
|
||||
}
|
||||
|
||||
@@ -241,7 +262,9 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
// 1eN, 2eN, or 5eN for some N
|
||||
|
||||
// Ideally, we want an interval every 100 logical screen pixels
|
||||
const logicalToConfig = (this.configSpaceToPhysicalViewSpace().inverted() || new AffineTransform()).times(this.logicalToPhysicalViewSpace())
|
||||
const logicalToConfig = (
|
||||
this.configSpaceToPhysicalViewSpace().inverted() || new AffineTransform()
|
||||
).times(this.logicalToPhysicalViewSpace())
|
||||
const targetInterval = logicalToConfig.transformVector(new Vec2(200, 1)).x
|
||||
|
||||
const minInterval = Math.pow(10, Math.floor(Math.log10(targetInterval)))
|
||||
@@ -274,25 +297,28 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
private updateConfigSpaceViewport(windowResized = false) {
|
||||
if (!this.container) return
|
||||
const bounds = this.container.getBoundingClientRect()
|
||||
const { width, height } = bounds
|
||||
const {width, height} = bounds
|
||||
|
||||
// Still initializing: don't resize yet
|
||||
if (width < 2 || height < 2) return
|
||||
|
||||
if (this.lastBounds == null) {
|
||||
this.setConfigSpaceViewportRect(new Rect(
|
||||
new Vec2(0, -1),
|
||||
new Vec2(this.configSpaceSize().x, height / this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT)
|
||||
))
|
||||
this.setConfigSpaceViewportRect(
|
||||
new Rect(
|
||||
new Vec2(0, -1),
|
||||
new Vec2(this.configSpaceSize().x, height / this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT),
|
||||
),
|
||||
)
|
||||
} else if (windowResized) {
|
||||
// Resize the viewport rectangle to match the window size aspect
|
||||
// ratio.
|
||||
this.setConfigSpaceViewportRect(this.props.configSpaceViewportRect.withSize(
|
||||
this.props.configSpaceViewportRect.size.timesPointwise(new Vec2(
|
||||
width / this.lastBounds.width,
|
||||
height / this.lastBounds.height
|
||||
))
|
||||
))
|
||||
this.setConfigSpaceViewportRect(
|
||||
this.props.configSpaceViewportRect.withSize(
|
||||
this.props.configSpaceViewportRect.size.timesPointwise(
|
||||
new Vec2(width / this.lastBounds.width, height / this.lastBounds.height),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
this.lastBounds = bounds
|
||||
}
|
||||
@@ -308,11 +334,11 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
|
||||
if (this.props.configSpaceViewportRect.isEmpty()) return
|
||||
|
||||
this.props.canvasContext.renderInto(this.container, (context) => {
|
||||
this.props.canvasContext.renderInto(this.container, context => {
|
||||
this.props.flamechartRenderer.render({
|
||||
physicalSpaceDstRect: new Rect(Vec2.zero, this.physicalViewSize()),
|
||||
configSpaceSrcRect: this.props.configSpaceViewportRect,
|
||||
renderOutlines: true
|
||||
renderOutlines: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -333,7 +359,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
private maybeClearInteractionLock = () => {
|
||||
if (this.interactionLock) {
|
||||
if (!this.frameHadWheelEvent) {
|
||||
this.framesWithoutWheelEvents++;
|
||||
this.framesWithoutWheelEvents++
|
||||
if (this.framesWithoutWheelEvents >= 2) {
|
||||
this.interactionLock = null
|
||||
this.framesWithoutWheelEvents = 0
|
||||
@@ -367,12 +393,15 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
private zoom(logicalViewSpaceCenter: Vec2, multiplier: number) {
|
||||
this.interactionLock = 'zoom'
|
||||
|
||||
const physicalCenter = this.logicalToPhysicalViewSpace().transformPosition(logicalViewSpaceCenter)
|
||||
const configSpaceCenter = this.configSpaceToPhysicalViewSpace().inverseTransformPosition(physicalCenter)
|
||||
const physicalCenter = this.logicalToPhysicalViewSpace().transformPosition(
|
||||
logicalViewSpaceCenter,
|
||||
)
|
||||
const configSpaceCenter = this.configSpaceToPhysicalViewSpace().inverseTransformPosition(
|
||||
physicalCenter,
|
||||
)
|
||||
if (!configSpaceCenter) return
|
||||
|
||||
const zoomTransform = AffineTransform
|
||||
.withTranslation(configSpaceCenter.times(-1))
|
||||
const zoomTransform = AffineTransform.withTranslation(configSpaceCenter.times(-1))
|
||||
.scaledBy(new Vec2(multiplier, 1))
|
||||
.translatedBy(configSpaceCenter)
|
||||
|
||||
@@ -404,7 +433,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
const hoveredBounds = this.hoveredLabel.configSpaceBounds
|
||||
const viewportRect = new Rect(
|
||||
hoveredBounds.origin.minus(new Vec2(0, 1)),
|
||||
hoveredBounds.size.withY(this.props.configSpaceViewportRect.height())
|
||||
hoveredBounds.size.withY(this.props.configSpaceViewportRect.height()),
|
||||
)
|
||||
this.props.setConfigSpaceViewportRect(viewportRect)
|
||||
}
|
||||
@@ -434,24 +463,25 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
}
|
||||
this.hoveredLabel = null
|
||||
const logicalViewSpaceMouse = new Vec2(ev.offsetX, ev.offsetY)
|
||||
const physicalViewSpaceMouse = this.logicalToPhysicalViewSpace().transformPosition(logicalViewSpaceMouse)
|
||||
const configSpaceMouse = this.configSpaceToPhysicalViewSpace().inverseTransformPosition(physicalViewSpaceMouse)
|
||||
const physicalViewSpaceMouse = this.logicalToPhysicalViewSpace().transformPosition(
|
||||
logicalViewSpaceMouse,
|
||||
)
|
||||
const configSpaceMouse = this.configSpaceToPhysicalViewSpace().inverseTransformPosition(
|
||||
physicalViewSpaceMouse,
|
||||
)
|
||||
|
||||
if (!configSpaceMouse) return
|
||||
|
||||
const setHoveredLabel = (frame: FlamechartFrame, depth = 0) => {
|
||||
const width = frame.end - frame.start
|
||||
const configSpaceBounds = new Rect(
|
||||
new Vec2(frame.start, depth),
|
||||
new Vec2(width, 1)
|
||||
)
|
||||
const configSpaceBounds = new Rect(new Vec2(frame.start, depth), new Vec2(width, 1))
|
||||
if (configSpaceMouse.x < configSpaceBounds.left()) return null
|
||||
if (configSpaceMouse.x > configSpaceBounds.right()) return null
|
||||
|
||||
if (configSpaceBounds.contains(configSpaceMouse)) {
|
||||
this.hoveredLabel = {
|
||||
configSpaceBounds,
|
||||
node: frame.node
|
||||
node: frame.node,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,7 +490,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
}
|
||||
}
|
||||
|
||||
for (let frame of (this.props.flamechart.getLayers()[0] || [])) {
|
||||
for (let frame of this.props.flamechart.getLayers()[0] || []) {
|
||||
setHoveredLabel(frame)
|
||||
}
|
||||
|
||||
@@ -486,13 +516,13 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
const isZoom = ev.metaKey || ev.ctrlKey
|
||||
|
||||
if (isZoom && this.interactionLock !== 'pan') {
|
||||
let multiplier = 1 + (ev.deltaY / 100)
|
||||
let multiplier = 1 + ev.deltaY / 100
|
||||
|
||||
// On Chrome & Firefox, pinch-to-zoom maps to
|
||||
// WheelEvent + Ctrl Key. We'll accelerate it in
|
||||
// this case, since it feels a bit sluggish otherwise.
|
||||
if (ev.ctrlKey) {
|
||||
multiplier = 1 + (ev.deltaY / 40)
|
||||
multiplier = 1 + ev.deltaY / 40
|
||||
}
|
||||
|
||||
multiplier = clamp(multiplier, 0.1, 10.0)
|
||||
@@ -528,8 +558,9 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
shouldComponentUpdate() { return false }
|
||||
shouldComponentUpdate() {
|
||||
return false
|
||||
}
|
||||
componentWillReceiveProps(nextProps: FlamechartPanZoomViewProps) {
|
||||
if (this.props.flamechart !== nextProps.flamechart) {
|
||||
this.renderCanvas()
|
||||
@@ -557,11 +588,9 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onDblClick={this.onDblClick}
|
||||
onWheel={this.onWheel}
|
||||
ref={this.containerRef}>
|
||||
<canvas
|
||||
width={1} height={1}
|
||||
ref={this.overlayCanvasRef}
|
||||
className={css(style.fill)} />
|
||||
ref={this.containerRef}
|
||||
>
|
||||
<canvas width={1} height={1} ref={this.overlayCanvasRef} className={css(style.fill)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -587,37 +616,40 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
this.state = {
|
||||
hoveredNode: null,
|
||||
configSpaceViewportRect: Rect.empty,
|
||||
logicalSpaceMouse: Vec2.zero
|
||||
logicalSpaceMouse: Vec2.zero,
|
||||
}
|
||||
}
|
||||
|
||||
private configSpaceSize() {
|
||||
return new Vec2(
|
||||
this.props.flamechart.getTotalWeight(),
|
||||
this.props.flamechart.getLayers().length
|
||||
this.props.flamechart.getLayers().length,
|
||||
)
|
||||
}
|
||||
|
||||
private minConfigSpaceViewportRectWidth() {
|
||||
return Math.min(this.props.flamechart.getTotalWeight(), 3 * this.props.flamechart.getMinFrameWidth());
|
||||
return Math.min(
|
||||
this.props.flamechart.getTotalWeight(),
|
||||
3 * this.props.flamechart.getMinFrameWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
private setConfigSpaceViewportRect = (viewportRect: Rect): void => {
|
||||
const configSpaceOriginBounds = new Rect(
|
||||
new Vec2(0, -1),
|
||||
Vec2.max(new Vec2(0, 0), this.configSpaceSize().minus(viewportRect.size))
|
||||
Vec2.max(new Vec2(0, 0), this.configSpaceSize().minus(viewportRect.size)),
|
||||
)
|
||||
|
||||
const configSpaceSizeBounds = new Rect(
|
||||
new Vec2(this.minConfigSpaceViewportRectWidth(), viewportRect.height()),
|
||||
new Vec2(this.configSpaceSize().x, viewportRect.height())
|
||||
new Vec2(this.configSpaceSize().x, viewportRect.height()),
|
||||
)
|
||||
|
||||
this.setState({
|
||||
configSpaceViewportRect: new Rect(
|
||||
configSpaceOriginBounds.closestPointTo(viewportRect.origin),
|
||||
configSpaceSizeBounds.closestPointTo(viewportRect.size)
|
||||
)
|
||||
configSpaceSizeBounds.closestPointTo(viewportRect.size),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -629,8 +661,8 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
onNodeHover = (hoveredNode: CallTreeNode | null, logicalSpaceMouse: Vec2) => {
|
||||
this.setState({
|
||||
hoveredNode,
|
||||
logicalSpaceMouse: logicalSpaceMouse.plus(new Vec2(0, Sizes.MINIMAP_HEIGHT))
|
||||
});
|
||||
logicalSpaceMouse: logicalSpaceMouse.plus(new Vec2(0, Sizes.MINIMAP_HEIGHT)),
|
||||
})
|
||||
}
|
||||
|
||||
formatValue(weight: number) {
|
||||
@@ -649,7 +681,7 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
renderTooltip() {
|
||||
if (!this.container) return null
|
||||
|
||||
const { hoveredNode, logicalSpaceMouse } = this.state
|
||||
const {hoveredNode, logicalSpaceMouse} = this.state
|
||||
if (!hoveredNode) return null
|
||||
|
||||
const {width, height} = this.container.getBoundingClientRect()
|
||||
@@ -665,26 +697,30 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
if (logicalSpaceMouse.x + OFFSET_FROM_MOUSE + Sizes.TOOLTIP_WIDTH_MAX < width) {
|
||||
positionStyle.left = logicalSpaceMouse.x + OFFSET_FROM_MOUSE
|
||||
} else {
|
||||
positionStyle.right = (width - logicalSpaceMouse.x) + 1
|
||||
positionStyle.right = width - logicalSpaceMouse.x + 1
|
||||
}
|
||||
|
||||
if (logicalSpaceMouse.y + OFFSET_FROM_MOUSE + Sizes.TOOLTIP_HEIGHT_MAX < height) {
|
||||
positionStyle.top = logicalSpaceMouse.y + OFFSET_FROM_MOUSE
|
||||
} else {
|
||||
positionStyle.bottom = (height - logicalSpaceMouse.y) + 1
|
||||
positionStyle.bottom = height - logicalSpaceMouse.y + 1
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css(style.hoverTip)} style={positionStyle}>
|
||||
<div className={css(style.hoverTipRow)}>
|
||||
<span className={css(style.hoverCount)}>{this.formatValue(hoveredNode.getTotalWeight())}</span>{' '}
|
||||
<span className={css(style.hoverCount)}>
|
||||
{this.formatValue(hoveredNode.getTotalWeight())}
|
||||
</span>{' '}
|
||||
{hoveredNode.frame.name}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
containerRef = (container?: Element) => { this.container = container as HTMLDivElement || null }
|
||||
containerRef = (container?: Element) => {
|
||||
this.container = (container as HTMLDivElement) || null
|
||||
}
|
||||
|
||||
panZoomView: FlamechartPanZoomView | null = null
|
||||
panZoomRef = (view: FlamechartPanZoomView | null) => {
|
||||
@@ -692,7 +728,7 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
}
|
||||
subcomponents() {
|
||||
return {
|
||||
panZoom: this.panZoomView
|
||||
panZoom: this.panZoomView,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -705,7 +741,8 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
flamechart={this.props.flamechart}
|
||||
flamechartRenderer={this.props.flamechartRenderer}
|
||||
canvasContext={this.props.canvasContext}
|
||||
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect} />
|
||||
setConfigSpaceViewportRect={this.setConfigSpaceViewportRect}
|
||||
/>
|
||||
<FlamechartPanZoomView
|
||||
ref={this.panZoomRef}
|
||||
canvasContext={this.props.canvasContext}
|
||||
@@ -718,6 +755,6 @@ export class FlamechartView extends ReloadableComponent<FlamechartViewProps, Fla
|
||||
/>
|
||||
{this.renderTooltip()}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Frame, CallTreeNode} from './profile'
|
||||
|
||||
import { lastOf } from './utils'
|
||||
import {lastOf} from './utils'
|
||||
|
||||
export interface FlamechartFrame {
|
||||
node: CallTreeNode
|
||||
@@ -19,7 +19,7 @@ interface FlamechartDataSource {
|
||||
|
||||
forEachCall(
|
||||
openFrame: (node: CallTreeNode, value: number) => void,
|
||||
closeFrame: (value: number) => void
|
||||
closeFrame: (value: number) => void,
|
||||
): void
|
||||
|
||||
getColorBucketForFrame(f: Frame): number
|
||||
@@ -31,11 +31,21 @@ export class Flamechart {
|
||||
private totalWeight: number = 0
|
||||
private minFrameWidth: number = 1
|
||||
|
||||
getTotalWeight() { return this.totalWeight }
|
||||
getLayers() { return this.layers }
|
||||
getColorBucketForFrame(frame: Frame) { return this.source.getColorBucketForFrame(frame) }
|
||||
getMinFrameWidth() { return this.minFrameWidth }
|
||||
formatValue(v: number) { return this.source.formatValue(v) }
|
||||
getTotalWeight() {
|
||||
return this.totalWeight
|
||||
}
|
||||
getLayers() {
|
||||
return this.layers
|
||||
}
|
||||
getColorBucketForFrame(frame: Frame) {
|
||||
return this.source.getColorBucketForFrame(frame)
|
||||
}
|
||||
getMinFrameWidth() {
|
||||
return this.minFrameWidth
|
||||
}
|
||||
formatValue(v: number) {
|
||||
return this.source.formatValue(v)
|
||||
}
|
||||
|
||||
constructor(private source: FlamechartDataSource) {
|
||||
const stack: FlamechartFrame[] = []
|
||||
@@ -71,4 +81,4 @@ export class Flamechart {
|
||||
|
||||
if (!isFinite(this.minFrameWidth)) this.minFrameWidth = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ interface BGSample {
|
||||
|
||||
function parseBGFoldedStacks(contents: string): BGSample[] {
|
||||
const samples: BGSample[] = []
|
||||
contents.replace(/^(.*) (\d+)$/mg, (match: string, stack: string, n: string) => {
|
||||
contents.replace(/^(.*) (\d+)$/gm, (match: string, stack: string, n: string) => {
|
||||
samples.push({
|
||||
stack: stack.split(';').map(name => ({key: name, name: name})),
|
||||
duration: parseInt(n, 10)
|
||||
duration: parseInt(n, 10),
|
||||
})
|
||||
return match
|
||||
})
|
||||
@@ -27,4 +27,4 @@ export function importFromBGFlameGraph(contents: string): Profile {
|
||||
profile.appendSample(sample.stack, sample.duration)
|
||||
}
|
||||
return profile
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,28 +2,28 @@ import {Profile, TimeFormatter, FrameInfo} from '../profile'
|
||||
import {getOrInsert, lastOf} from '../utils'
|
||||
|
||||
interface TimelineEvent {
|
||||
pid: number,
|
||||
tid: number,
|
||||
ts: number,
|
||||
ph: string,
|
||||
cat: string,
|
||||
name: string,
|
||||
dur: number,
|
||||
tdur: number,
|
||||
tts: number,
|
||||
args: { [key: string]: any }
|
||||
pid: number
|
||||
tid: number
|
||||
ts: number
|
||||
ph: string
|
||||
cat: string
|
||||
name: string
|
||||
dur: number
|
||||
tdur: number
|
||||
tts: number
|
||||
args: {[key: string]: any}
|
||||
}
|
||||
|
||||
interface PositionTickInfo {
|
||||
line: number,
|
||||
line: number
|
||||
ticks: number
|
||||
}
|
||||
|
||||
interface CPUProfileCallFrame {
|
||||
columnNumber: number,
|
||||
functionName: string,
|
||||
lineNumber: number,
|
||||
scriptId: string,
|
||||
columnNumber: number
|
||||
functionName: string
|
||||
lineNumber: number
|
||||
scriptId: string
|
||||
url: string
|
||||
}
|
||||
|
||||
@@ -37,10 +37,10 @@ interface CPUProfileNode {
|
||||
}
|
||||
|
||||
interface CPUProfile {
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
nodes: CPUProfileNode[],
|
||||
samples: number[],
|
||||
startTime: number
|
||||
endTime: number
|
||||
nodes: CPUProfileNode[]
|
||||
samples: number[]
|
||||
timeDeltas: number[]
|
||||
}
|
||||
|
||||
@@ -48,18 +48,18 @@ export function importFromChromeTimeline(events: TimelineEvent[]) {
|
||||
// It seems like sometimes Chrome timeline files contain multiple CpuProfiles?
|
||||
// For now, choose the first one in the list.
|
||||
for (let event of events) {
|
||||
if (event.name == "CpuProfile") {
|
||||
if (event.name == 'CpuProfile') {
|
||||
const chromeProfile = event.args.data.cpuProfile as CPUProfile
|
||||
return importFromChromeCPUProfile(chromeProfile)
|
||||
}
|
||||
}
|
||||
throw new Error("Could not find CPU profile in Timeline")
|
||||
throw new Error('Could not find CPU profile in Timeline')
|
||||
}
|
||||
|
||||
const callFrameToFrameInfo = new Map<CPUProfileCallFrame, FrameInfo>()
|
||||
function frameInfoForCallFrame(callFrame: CPUProfileCallFrame) {
|
||||
return getOrInsert(callFrameToFrameInfo, callFrame, (callFrame) => {
|
||||
const name = callFrame.functionName || "(anonymous)"
|
||||
return getOrInsert(callFrameToFrameInfo, callFrame, callFrame => {
|
||||
const name = callFrame.functionName || '(anonymous)'
|
||||
const file = callFrame.url
|
||||
const line = callFrame.lineNumber
|
||||
const col = callFrame.columnNumber
|
||||
@@ -68,7 +68,7 @@ function frameInfoForCallFrame(callFrame: CPUProfileCallFrame) {
|
||||
name,
|
||||
file,
|
||||
line,
|
||||
col
|
||||
col,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -117,7 +117,7 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile) {
|
||||
|
||||
let value = 0
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
const timeDelta = timeDeltas[i+1] || 0
|
||||
const timeDelta = timeDeltas[i + 1] || 0
|
||||
const nodeId = samples[i]
|
||||
let stackTop = nodeById.get(nodeId)
|
||||
if (!stackTop) continue
|
||||
@@ -130,7 +130,10 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile) {
|
||||
for (
|
||||
lca = stackTop;
|
||||
lca && prevStack.indexOf(lca) === -1;
|
||||
lca = lca.callFrame.functionName === "(garbage collector)" ? lastOf(prevStack) : lca.parent || null
|
||||
lca =
|
||||
lca.callFrame.functionName === '(garbage collector)'
|
||||
? lastOf(prevStack)
|
||||
: lca.parent || null
|
||||
) {}
|
||||
|
||||
// Close frames that are no longer open
|
||||
@@ -146,7 +149,10 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile) {
|
||||
let node: CPUProfileNode | null = stackTop;
|
||||
node && node != lca;
|
||||
// Place GC calls on top of the previous call stack
|
||||
node = node.callFrame.functionName === "(garbage collector)" ? lastOf(prevStack) : node.parent || null
|
||||
node =
|
||||
node.callFrame.functionName === '(garbage collector)'
|
||||
? lastOf(prevStack)
|
||||
: node.parent || null
|
||||
) {
|
||||
toOpen.push(node)
|
||||
}
|
||||
@@ -167,4 +173,4 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile) {
|
||||
|
||||
profile.setValueFormatter(new TimeFormatter('microseconds'))
|
||||
return profile
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export function importFromStackprof(stackprofProfile: StackprofProfile): Profile
|
||||
|
||||
const {frames, raw, raw_timestamp_deltas} = stackprofProfile
|
||||
let sampleIndex = 0
|
||||
for (let i = 0; i < raw.length;) {
|
||||
for (let i = 0; i < raw.length; ) {
|
||||
const stackHeight = raw[i++]
|
||||
|
||||
const stack: FrameInfo[] = []
|
||||
@@ -28,7 +28,7 @@ export function importFromStackprof(stackprofProfile: StackprofProfile): Profile
|
||||
const id = raw[i++]
|
||||
stack.push({
|
||||
key: id,
|
||||
...frames[id]
|
||||
...frames[id],
|
||||
})
|
||||
}
|
||||
const nSamples = raw[i++]
|
||||
@@ -43,4 +43,4 @@ export function importFromStackprof(stackprofProfile: StackprofProfile): Profile
|
||||
|
||||
profile.setValueFormatter(new TimeFormatter('microseconds'))
|
||||
return profile
|
||||
}
|
||||
}
|
||||
|
||||
30
lru-cache.ts
30
lru-cache.ts
@@ -1,18 +1,24 @@
|
||||
class ListNode<V> {
|
||||
prev: ListNode<V> | null = null
|
||||
next: ListNode<V> | null = null
|
||||
constructor(readonly data: V) { }
|
||||
constructor(readonly data: V) {}
|
||||
}
|
||||
|
||||
export class List<V> {
|
||||
private head: ListNode<V> | null = null
|
||||
private tail: ListNode<V> | null = null
|
||||
private size: number = 0
|
||||
constructor() { }
|
||||
constructor() {}
|
||||
|
||||
getHead(): ListNode<V> | null { return this.head }
|
||||
getTail(): ListNode<V> | null { return this.tail }
|
||||
getSize(): number { return this.size }
|
||||
getHead(): ListNode<V> | null {
|
||||
return this.head
|
||||
}
|
||||
getTail(): ListNode<V> | null {
|
||||
return this.tail
|
||||
}
|
||||
getSize(): number {
|
||||
return this.size
|
||||
}
|
||||
|
||||
append(node: ListNode<V>): void {
|
||||
if (!this.tail) {
|
||||
@@ -97,7 +103,7 @@ export class LRUCache<K, V> {
|
||||
private list = new List<K>()
|
||||
private map = new Map<K, LRUCacheNode<K, V>>()
|
||||
|
||||
constructor(private capacity: number) { }
|
||||
constructor(private capacity: number) {}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this.map.has(key)
|
||||
@@ -115,9 +121,13 @@ export class LRUCache<K, V> {
|
||||
return node ? node.value : null
|
||||
}
|
||||
|
||||
getSize() { return this.list.getSize() }
|
||||
getSize() {
|
||||
return this.list.getSize()
|
||||
}
|
||||
|
||||
getCapacity() { return this.capacity }
|
||||
getCapacity() {
|
||||
return this.capacity
|
||||
}
|
||||
|
||||
insert(key: K, value: V) {
|
||||
const node = this.map.get(key)
|
||||
@@ -129,7 +139,7 @@ export class LRUCache<K, V> {
|
||||
this.map.delete(this.list.pop()!.data)
|
||||
}
|
||||
const listNode = this.list.prepend(new ListNode(key))
|
||||
this.map.set(key, { value, listNode })
|
||||
this.map.set(key, {value, listNode})
|
||||
}
|
||||
|
||||
getOrInsert(key: K, f: (key: K) => V): V {
|
||||
@@ -149,4 +159,4 @@ export class LRUCache<K, V> {
|
||||
this.map.delete(key)
|
||||
return [key, value]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
192
math.ts
192
math.ts
@@ -6,19 +6,43 @@ export function clamp(x: number, minVal: number, maxVal: number) {
|
||||
|
||||
export class Vec2 {
|
||||
constructor(readonly x: number, readonly y: number) {}
|
||||
withX(x: number) { return new Vec2(x, this.y) }
|
||||
withY(y: number) { return new Vec2(this.x, y) }
|
||||
withX(x: number) {
|
||||
return new Vec2(x, this.y)
|
||||
}
|
||||
withY(y: number) {
|
||||
return new Vec2(this.x, y)
|
||||
}
|
||||
|
||||
plus(other: Vec2) { return new Vec2(this.x + other.x, this.y + other.y) }
|
||||
minus(other: Vec2) { return new Vec2(this.x - other.x, this.y - other.y) }
|
||||
times(scalar: number) { return new Vec2(this.x * scalar, this.y * scalar) }
|
||||
timesPointwise(other: Vec2) { return new Vec2(this.x * other.x, this.y * other.y) }
|
||||
dividedByPointwise(other: Vec2) { return new Vec2(this.x / other.x, this.y / other.y) }
|
||||
dot(other: Vec2) { return this.x * other.x + this.y * other.y }
|
||||
equals(other: Vec2) { return this.x === other.x && this.y === other.y }
|
||||
length2() { return this.dot(this) }
|
||||
length() { return Math.sqrt(this.length2()) }
|
||||
abs() { return new Vec2(Math.abs(this.x), Math.abs(this.y)) }
|
||||
plus(other: Vec2) {
|
||||
return new Vec2(this.x + other.x, this.y + other.y)
|
||||
}
|
||||
minus(other: Vec2) {
|
||||
return new Vec2(this.x - other.x, this.y - other.y)
|
||||
}
|
||||
times(scalar: number) {
|
||||
return new Vec2(this.x * scalar, this.y * scalar)
|
||||
}
|
||||
timesPointwise(other: Vec2) {
|
||||
return new Vec2(this.x * other.x, this.y * other.y)
|
||||
}
|
||||
dividedByPointwise(other: Vec2) {
|
||||
return new Vec2(this.x / other.x, this.y / other.y)
|
||||
}
|
||||
dot(other: Vec2) {
|
||||
return this.x * other.x + this.y * other.y
|
||||
}
|
||||
equals(other: Vec2) {
|
||||
return this.x === other.x && this.y === other.y
|
||||
}
|
||||
length2() {
|
||||
return this.dot(this)
|
||||
}
|
||||
length() {
|
||||
return Math.sqrt(this.length2())
|
||||
}
|
||||
abs() {
|
||||
return new Vec2(Math.abs(this.x), Math.abs(this.y))
|
||||
}
|
||||
|
||||
static min(a: Vec2, b: Vec2) {
|
||||
return new Vec2(Math.min(a.x, b.x), Math.min(a.y, b.y))
|
||||
@@ -31,52 +55,56 @@ export class Vec2 {
|
||||
static zero = new Vec2(0, 0)
|
||||
static unit = new Vec2(1, 1)
|
||||
|
||||
flatten(): [number, number] { return [this.x, this.y] }
|
||||
flatten(): [number, number] {
|
||||
return [this.x, this.y]
|
||||
}
|
||||
}
|
||||
|
||||
export class AffineTransform {
|
||||
constructor(
|
||||
readonly m00 = 1, readonly m01 = 0, readonly m02 = 0,
|
||||
readonly m10 = 0, readonly m11 = 1, readonly m12 = 0
|
||||
readonly m00 = 1,
|
||||
readonly m01 = 0,
|
||||
readonly m02 = 0,
|
||||
readonly m10 = 0,
|
||||
readonly m11 = 1,
|
||||
readonly m12 = 0,
|
||||
) {}
|
||||
|
||||
withScale(s: Vec2) {
|
||||
let {
|
||||
m00, m01, m02,
|
||||
m10, m11, m12
|
||||
} = this
|
||||
let {m00, m01, m02, m10, m11, m12} = this
|
||||
m00 = s.x
|
||||
m11 = s.y
|
||||
return new AffineTransform(m00, m01, m02, m10, m11, m12)
|
||||
}
|
||||
static withScale(s: Vec2) {
|
||||
return (new AffineTransform).withScale(s)
|
||||
return new AffineTransform().withScale(s)
|
||||
}
|
||||
scaledBy(s: Vec2) {
|
||||
return AffineTransform.withScale(s).times(this)
|
||||
}
|
||||
getScale() {
|
||||
return new Vec2(this.m00, this.m11)
|
||||
}
|
||||
scaledBy(s: Vec2) { return AffineTransform.withScale(s).times(this) }
|
||||
getScale() { return new Vec2(this.m00, this.m11) }
|
||||
|
||||
withTranslation(t: Vec2) {
|
||||
let {
|
||||
m00, m01, m02,
|
||||
m10, m11, m12
|
||||
} = this
|
||||
let {m00, m01, m02, m10, m11, m12} = this
|
||||
m02 = t.x
|
||||
m12 = t.y
|
||||
return new AffineTransform(m00, m01, m02, m10, m11, m12)
|
||||
}
|
||||
static withTranslation(t: Vec2) {
|
||||
return (new AffineTransform).withTranslation(t)
|
||||
return new AffineTransform().withTranslation(t)
|
||||
}
|
||||
getTranslation() {
|
||||
return new Vec2(this.m02, this.m12)
|
||||
}
|
||||
translatedBy(t: Vec2) {
|
||||
return AffineTransform.withTranslation(t).times(this)
|
||||
}
|
||||
getTranslation() { return new Vec2(this.m02, this.m12) }
|
||||
translatedBy(t: Vec2) { return AffineTransform.withTranslation(t).times(this) }
|
||||
|
||||
static betweenRects(from: Rect, to: Rect) {
|
||||
return AffineTransform
|
||||
.withTranslation(from.origin.times(-1))
|
||||
.scaledBy(new Vec2(
|
||||
to.size.x / from.size.x,
|
||||
to.size.y / from.size.y
|
||||
))
|
||||
return AffineTransform.withTranslation(from.origin.times(-1))
|
||||
.scaledBy(new Vec2(to.size.x / from.size.x, to.size.y / from.size.y))
|
||||
.translatedBy(to.origin)
|
||||
}
|
||||
|
||||
@@ -92,17 +120,19 @@ export class AffineTransform {
|
||||
}
|
||||
|
||||
equals(other: AffineTransform) {
|
||||
return this.m00 == other.m00 &&
|
||||
this.m01 == other.m01 &&
|
||||
this.m02 == other.m02 &&
|
||||
this.m10 == other.m10 &&
|
||||
this.m11 == other.m11 &&
|
||||
this.m12 == other.m12;
|
||||
return (
|
||||
this.m00 == other.m00 &&
|
||||
this.m01 == other.m01 &&
|
||||
this.m02 == other.m02 &&
|
||||
this.m10 == other.m10 &&
|
||||
this.m11 == other.m11 &&
|
||||
this.m12 == other.m12
|
||||
)
|
||||
}
|
||||
|
||||
timesScalar(s: number) {
|
||||
const {m00, m01, m02, m10, m11, m12} = this
|
||||
return new AffineTransform(s*m00, s*m01, s*m02, s*m10, s*m11, s*m12)
|
||||
return new AffineTransform(s * m00, s * m01, s * m02, s * m10, s * m11, s * m12)
|
||||
}
|
||||
|
||||
det() {
|
||||
@@ -112,9 +142,7 @@ export class AffineTransform {
|
||||
const m22 = 1
|
||||
|
||||
return (
|
||||
m00 * (m11 * m22 - m12 * m21) -
|
||||
m01 * (m10 * m22 - m12 * m20) +
|
||||
m02 * (m10 * m21 - m11 * m20)
|
||||
m00 * (m11 * m22 - m12 * m21) - m01 * (m10 * m22 - m12 * m20) + m02 * (m10 * m21 - m11 * m20)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -149,10 +177,7 @@ export class AffineTransform {
|
||||
}
|
||||
|
||||
transformVector(v: Vec2) {
|
||||
return new Vec2(
|
||||
v.x * this.m00 + v.y * this.m01,
|
||||
v.x * this.m10 + v.y * this.m11
|
||||
)
|
||||
return new Vec2(v.x * this.m00 + v.y * this.m01, v.x * this.m10 + v.y * this.m11)
|
||||
}
|
||||
|
||||
inverseTransformVector(v: Vec2): Vec2 | null {
|
||||
@@ -164,7 +189,7 @@ export class AffineTransform {
|
||||
transformPosition(v: Vec2) {
|
||||
return new Vec2(
|
||||
v.x * this.m00 + v.y * this.m01 + this.m02,
|
||||
v.x * this.m10 + v.y * this.m11 + this.m12
|
||||
v.x * this.m10 + v.y * this.m11 + this.m12,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -189,7 +214,7 @@ export class AffineTransform {
|
||||
return new Rect(origin, size)
|
||||
}
|
||||
|
||||
inverseTransformRect(r: Rect): Rect | null{
|
||||
inverseTransformRect(r: Rect): Rect | null {
|
||||
const inv = this.inverted()
|
||||
if (!inv) return null
|
||||
return inv.transformRect(r)
|
||||
@@ -197,6 +222,7 @@ export class AffineTransform {
|
||||
|
||||
flatten(): [number, number, number, number, number, number, number, number, number] {
|
||||
// Flatten into GLSL format
|
||||
// prettier-ignore
|
||||
return [
|
||||
this.m00, this.m10, 0,
|
||||
this.m01, this.m11, 0,
|
||||
@@ -206,35 +232,55 @@ export class AffineTransform {
|
||||
}
|
||||
|
||||
export class Rect {
|
||||
constructor(
|
||||
readonly origin: Vec2,
|
||||
readonly size: Vec2
|
||||
) {}
|
||||
constructor(readonly origin: Vec2, readonly size: Vec2) {}
|
||||
|
||||
isEmpty() { return this.width() == 0 || this.height() == 0 }
|
||||
isEmpty() {
|
||||
return this.width() == 0 || this.height() == 0
|
||||
}
|
||||
|
||||
width() { return this.size.x }
|
||||
height() { return this.size.y }
|
||||
width() {
|
||||
return this.size.x
|
||||
}
|
||||
height() {
|
||||
return this.size.y
|
||||
}
|
||||
|
||||
left() { return this.origin.x }
|
||||
right() { return this.left() + this.width() }
|
||||
top() { return this.origin.y }
|
||||
bottom() { return this.top() + this.height() }
|
||||
left() {
|
||||
return this.origin.x
|
||||
}
|
||||
right() {
|
||||
return this.left() + this.width()
|
||||
}
|
||||
top() {
|
||||
return this.origin.y
|
||||
}
|
||||
bottom() {
|
||||
return this.top() + this.height()
|
||||
}
|
||||
|
||||
topLeft() { return this.origin }
|
||||
topRight() { return this.origin.plus(new Vec2(this.width(), 0)) }
|
||||
topLeft() {
|
||||
return this.origin
|
||||
}
|
||||
topRight() {
|
||||
return this.origin.plus(new Vec2(this.width(), 0))
|
||||
}
|
||||
|
||||
bottomRight() { return this.origin.plus(this.size) }
|
||||
bottomLeft() { return this.origin.plus(new Vec2(0, this.height())) }
|
||||
bottomRight() {
|
||||
return this.origin.plus(this.size)
|
||||
}
|
||||
bottomLeft() {
|
||||
return this.origin.plus(new Vec2(0, this.height()))
|
||||
}
|
||||
|
||||
withOrigin(origin: Vec2) { return new Rect(origin, this.size) }
|
||||
withSize(size: Vec2) { return new Rect(this.origin, size) }
|
||||
withOrigin(origin: Vec2) {
|
||||
return new Rect(origin, this.size)
|
||||
}
|
||||
withSize(size: Vec2) {
|
||||
return new Rect(this.origin, size)
|
||||
}
|
||||
|
||||
closestPointTo(p: Vec2) {
|
||||
return new Vec2(
|
||||
clamp(p.x, this.left(), this.right()),
|
||||
clamp(p.y, this.top(), this.bottom())
|
||||
)
|
||||
return new Vec2(clamp(p.x, this.left(), this.right()), clamp(p.y, this.top(), this.bottom()))
|
||||
}
|
||||
|
||||
distanceFrom(p: Vec2) {
|
||||
@@ -270,4 +316,4 @@ export class Rect {
|
||||
static empty = new Rect(Vec2.zero, Vec2.zero)
|
||||
static unit = new Rect(Vec2.zero, Vec2.unit)
|
||||
static NDC = new Rect(new Vec2(-1, -1), new Vec2(2, 2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import regl from 'regl'
|
||||
import { AffineTransform, Rect } from './math'
|
||||
import {AffineTransform, Rect} from './math'
|
||||
|
||||
export interface ViewportRectangleRendererProps {
|
||||
configSpaceToPhysicalViewSpace: AffineTransform
|
||||
@@ -66,12 +66,12 @@ export class ViewportRectangleRenderer {
|
||||
srcRGB: 'src alpha',
|
||||
srcAlpha: 'one',
|
||||
dstRGB: 'one minus src alpha',
|
||||
dstAlpha: 'one'
|
||||
}
|
||||
dstAlpha: 'one',
|
||||
},
|
||||
},
|
||||
|
||||
depth: {
|
||||
enable: false
|
||||
enable: false,
|
||||
},
|
||||
|
||||
attributes: {
|
||||
@@ -83,12 +83,7 @@ export class ViewportRectangleRenderer {
|
||||
// | /|
|
||||
// |/ |
|
||||
// 2 +--+ 3
|
||||
position: [
|
||||
[-1, 1],
|
||||
[1, 1],
|
||||
[-1, -1],
|
||||
[1, -1]
|
||||
]
|
||||
position: [[-1, 1], [1, 1], [-1, -1], [1, -1]],
|
||||
},
|
||||
|
||||
uniforms: {
|
||||
@@ -109,12 +104,12 @@ export class ViewportRectangleRenderer {
|
||||
},
|
||||
framebufferHeight: (context, props) => {
|
||||
return context.framebufferHeight
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
primitive: 'triangle strip',
|
||||
|
||||
count: 4
|
||||
count: 4,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -122,6 +117,10 @@ export class ViewportRectangleRenderer {
|
||||
this.command(props)
|
||||
}
|
||||
|
||||
resetStats() { return Object.assign(this.command.stats, { cpuTime: 0, gpuTime: 0, count: 0 }) }
|
||||
stats() { return this.command.stats }
|
||||
}
|
||||
resetStats() {
|
||||
return Object.assign(this.command.stats, {cpuTime: 0, gpuTime: 0, count: 0})
|
||||
}
|
||||
stats() {
|
||||
return this.command.stats
|
||||
}
|
||||
}
|
||||
|
||||
703
package-lock.json
generated
703
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -4,8 +4,11 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"serve": "parcel index.html --open --no-autoinstall",
|
||||
"deploy": "./deploy.sh"
|
||||
"deploy": "./deploy.sh",
|
||||
"prettier": "prettier --write './**/*.ts' './**/*.tsx'",
|
||||
"lint": "eslint './**/*.ts' './**/*.tsx'",
|
||||
"test": "npm run lint",
|
||||
"serve": "parcel index.html --open --no-autoinstall"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 Chrome versions",
|
||||
@@ -15,10 +18,14 @@
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"aphrodite": "2.1.0",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-plugin-prettier": "^2.6.0",
|
||||
"parcel-bundler": "1.7.0",
|
||||
"preact": "8.2.7",
|
||||
"prettier": "^1.12.0",
|
||||
"regl": "1.3.1",
|
||||
"typescript": "2.8.1",
|
||||
"typescript-eslint-parser": "^14.0.0",
|
||||
"uglify-es": "3.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
7
prettier.config.js
Normal file
7
prettier.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
bracketSpacing: false,
|
||||
printWidth: 100,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
};
|
||||
58
profile.ts
58
profile.ts
@@ -1,4 +1,4 @@
|
||||
import { lastOf, getOrInsert } from './utils'
|
||||
import {lastOf, getOrInsert} from './utils'
|
||||
const demangleCppModule = import('./demangle-cpp')
|
||||
|
||||
// Force eager loading of the module
|
||||
@@ -25,10 +25,18 @@ export interface FrameInfo {
|
||||
export class HasWeights {
|
||||
private selfWeight = 0
|
||||
private totalWeight = 0
|
||||
getSelfWeight() { return this.selfWeight }
|
||||
getTotalWeight() { return this.totalWeight }
|
||||
addToTotalWeight(delta: number) { this.totalWeight += delta }
|
||||
addToSelfWeight(delta: number) { this.selfWeight += delta }
|
||||
getSelfWeight() {
|
||||
return this.selfWeight
|
||||
}
|
||||
getTotalWeight() {
|
||||
return this.totalWeight
|
||||
}
|
||||
addToTotalWeight(delta: number) {
|
||||
this.totalWeight += delta
|
||||
}
|
||||
addToSelfWeight(delta: number) {
|
||||
this.selfWeight += delta
|
||||
}
|
||||
}
|
||||
|
||||
export class Frame extends HasWeights {
|
||||
@@ -81,7 +89,7 @@ export class RawValueFormatter implements ValueFormatter {
|
||||
}
|
||||
|
||||
export class TimeFormatter implements ValueFormatter {
|
||||
private multiplier : number
|
||||
private multiplier: number
|
||||
|
||||
constructor(unit: 'nanoseconds' | 'microseconds' | 'milliseconds' | 'seconds') {
|
||||
if (unit === 'nanoseconds') this.multiplier = 1e-9
|
||||
@@ -93,7 +101,7 @@ export class TimeFormatter implements ValueFormatter {
|
||||
format(v: number) {
|
||||
const s = v * this.multiplier
|
||||
|
||||
if (s / 1e0 >= 1) return `${s.toFixed(2)}s`
|
||||
if (s / 1 >= 1) return `${s.toFixed(2)}s`
|
||||
if (s / 1e-3 >= 1) return `${(s / 1e-3).toFixed(2)}ms`
|
||||
if (s / 1e-6 >= 1) return `${(s / 1e-6).toFixed(2)}µs`
|
||||
else return `${(s / 1e-9).toFixed(2)}ms`
|
||||
@@ -120,20 +128,30 @@ export class Profile {
|
||||
this.totalWeight = totalWeight
|
||||
}
|
||||
|
||||
formatValue(v: number) { return this.valueFormatter.format(v) }
|
||||
setValueFormatter(f: ValueFormatter) { this.valueFormatter = f }
|
||||
formatValue(v: number) {
|
||||
return this.valueFormatter.format(v)
|
||||
}
|
||||
setValueFormatter(f: ValueFormatter) {
|
||||
this.valueFormatter = f
|
||||
}
|
||||
|
||||
getName() { return this.name }
|
||||
setName(name: string) { this.name = name }
|
||||
getName() {
|
||||
return this.name
|
||||
}
|
||||
setName(name: string) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
getTotalWeight() { return this.totalWeight }
|
||||
getTotalWeight() {
|
||||
return this.totalWeight
|
||||
}
|
||||
getTotalNonIdleWeight() {
|
||||
return this.groupedCalltreeRoot.children.reduce((n, c) => n + c.getTotalWeight(), 0)
|
||||
}
|
||||
|
||||
forEachCallGrouped(
|
||||
openFrame: (node: CallTreeNode, value: number) => void,
|
||||
closeFrame: (value: number) => void
|
||||
closeFrame: (value: number) => void,
|
||||
) {
|
||||
function visit(node: CallTreeNode, start: number) {
|
||||
if (node.frame !== rootFrame) {
|
||||
@@ -143,9 +161,9 @@ export class Profile {
|
||||
let childTime = 0
|
||||
|
||||
const children = [...node.children]
|
||||
children.sort((a, b) => a.getTotalWeight() > b.getTotalWeight() ? -1 : 1)
|
||||
children.sort((a, b) => (a.getTotalWeight() > b.getTotalWeight() ? -1 : 1))
|
||||
|
||||
children.forEach(function (child) {
|
||||
children.forEach(function(child) {
|
||||
visit(child, start + childTime)
|
||||
childTime += child.getTotalWeight()
|
||||
})
|
||||
@@ -159,7 +177,7 @@ export class Profile {
|
||||
|
||||
forEachCall(
|
||||
openFrame: (node: CallTreeNode, value: number) => void,
|
||||
closeFrame: (value: number) => void
|
||||
closeFrame: (value: number) => void,
|
||||
) {
|
||||
let prevStack: CallTreeNode[] = []
|
||||
let value = 0
|
||||
@@ -220,7 +238,9 @@ export class Profile {
|
||||
|
||||
for (let frameInfo of stack) {
|
||||
const frame = getOrInsert(this.frames, frameInfo.key, () => new Frame(frameInfo))
|
||||
const last = useAppendOrder ? lastOf(node.children) : node.children.find(c => c.frame === frame)
|
||||
const last = useAppendOrder
|
||||
? lastOf(node.children)
|
||||
: node.children.find(c => c.frame === frame)
|
||||
if (last && last.frame == frame) {
|
||||
node = last
|
||||
} else {
|
||||
@@ -303,7 +323,9 @@ export class Profile {
|
||||
}
|
||||
}
|
||||
|
||||
const last = useAppendOrder ? lastOf(prevTop.children) : prevTop.children.find(c => c.frame === frame)
|
||||
const last = useAppendOrder
|
||||
? lastOf(prevTop.children)
|
||||
: prevTop.children.find(c => c.frame === frame)
|
||||
let node: CallTreeNode
|
||||
if (last && last.frame == frame) {
|
||||
node = last
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import regl from 'regl'
|
||||
import { Rect, Vec2, AffineTransform } from './math'
|
||||
import { Color } from './color'
|
||||
import {Rect, Vec2, AffineTransform} from './math'
|
||||
import {Color} from './color'
|
||||
|
||||
export class RectangleBatch {
|
||||
private rectCapacity = 1000
|
||||
@@ -10,19 +10,23 @@ export class RectangleBatch {
|
||||
private configSpaceSizes = new Float32Array(this.rectCapacity * 2)
|
||||
private colors = new Float32Array(this.rectCapacity * 3)
|
||||
|
||||
constructor(private gl: regl.Instance) { }
|
||||
constructor(private gl: regl.Instance) {}
|
||||
|
||||
getRectCount() { return this.rectCount }
|
||||
getRectCount() {
|
||||
return this.rectCount
|
||||
}
|
||||
|
||||
private configSpaceOffsetBuffer: regl.Buffer | null = null
|
||||
getConfigSpaceOffsetBuffer() {
|
||||
if (!this.configSpaceOffsetBuffer) this.configSpaceOffsetBuffer = this.gl.buffer(this.configSpaceOffsets)
|
||||
if (!this.configSpaceOffsetBuffer)
|
||||
this.configSpaceOffsetBuffer = this.gl.buffer(this.configSpaceOffsets)
|
||||
return this.configSpaceOffsetBuffer
|
||||
}
|
||||
|
||||
private configSpaceSizeBuffer: regl.Buffer | null = null
|
||||
getConfigSpaceSizeBuffer() {
|
||||
if (!this.configSpaceSizeBuffer) this.configSpaceSizeBuffer = this.gl.buffer(this.configSpaceSizes)
|
||||
if (!this.configSpaceSizeBuffer)
|
||||
this.configSpaceSizeBuffer = this.gl.buffer(this.configSpaceSizes)
|
||||
return this.configSpaceSizeBuffer
|
||||
}
|
||||
|
||||
@@ -105,11 +109,11 @@ export class RectangleBatchRenderer {
|
||||
}
|
||||
`,
|
||||
|
||||
depth: {
|
||||
enable: false
|
||||
},
|
||||
depth: {
|
||||
enable: false,
|
||||
},
|
||||
|
||||
frag: `
|
||||
frag: `
|
||||
precision mediump float;
|
||||
varying vec3 vColor;
|
||||
varying float vParity;
|
||||
@@ -121,12 +125,7 @@ export class RectangleBatchRenderer {
|
||||
|
||||
attributes: {
|
||||
// Non-instanced attributes
|
||||
corner: gl.buffer([
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
[0, 1],
|
||||
[1, 1],
|
||||
]),
|
||||
corner: gl.buffer([[0, 0], [1, 0], [0, 1], [1, 1]]),
|
||||
|
||||
// Instanced attributes
|
||||
configSpaceOffset: (context, props) => {
|
||||
@@ -135,7 +134,7 @@ export class RectangleBatchRenderer {
|
||||
offset: 0,
|
||||
stride: 2 * 4,
|
||||
size: 2,
|
||||
divisor: 1
|
||||
divisor: 1,
|
||||
}
|
||||
},
|
||||
configSpaceSize: (context, props) => {
|
||||
@@ -144,7 +143,7 @@ export class RectangleBatchRenderer {
|
||||
offset: 0,
|
||||
stride: 2 * 4,
|
||||
size: 2,
|
||||
divisor: 1
|
||||
divisor: 1,
|
||||
}
|
||||
},
|
||||
color: (context, props) => {
|
||||
@@ -153,22 +152,23 @@ export class RectangleBatchRenderer {
|
||||
offset: 0,
|
||||
stride: 3 * 4,
|
||||
size: 3,
|
||||
divisor: 1
|
||||
divisor: 1,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
uniforms: {
|
||||
configSpaceToNDC: (context, props) => {
|
||||
const configToPhysical = AffineTransform.betweenRects(
|
||||
props.configSpaceSrcRect,
|
||||
props.physicalSpaceDstRect
|
||||
props.physicalSpaceDstRect,
|
||||
)
|
||||
|
||||
const viewportSize = new Vec2(context.viewportWidth, context.viewportHeight)
|
||||
|
||||
const physicalToNDC = AffineTransform.withTranslation(new Vec2(-1, 1))
|
||||
.times(AffineTransform.withScale(new Vec2(2, -2).dividedByPointwise(viewportSize)))
|
||||
const physicalToNDC = AffineTransform.withTranslation(new Vec2(-1, 1)).times(
|
||||
AffineTransform.withScale(new Vec2(2, -2).dividedByPointwise(viewportSize)),
|
||||
)
|
||||
|
||||
return physicalToNDC.times(configToPhysical).flatten()
|
||||
},
|
||||
@@ -179,7 +179,7 @@ export class RectangleBatchRenderer {
|
||||
|
||||
parityMin: (context, props) => {
|
||||
return props.parityMin == null ? 0 : 1 + props.parityMin
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
instances: (context, props) => {
|
||||
@@ -196,6 +196,10 @@ export class RectangleBatchRenderer {
|
||||
this.command(props)
|
||||
}
|
||||
|
||||
resetStats() { return Object.assign(this.command.stats, { cpuTime: 0, gpuTime: 0, count: 0 }) }
|
||||
stats() { return this.command.stats }
|
||||
}
|
||||
resetStats() {
|
||||
return Object.assign(this.command.stats, {cpuTime: 0, gpuTime: 0, count: 0})
|
||||
}
|
||||
stats() {
|
||||
return this.command.stats
|
||||
}
|
||||
}
|
||||
|
||||
321
regl.d.ts
vendored
321
regl.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
declare module "regl" {
|
||||
declare module 'regl' {
|
||||
interface InitializationOptions {
|
||||
/** A reference to a WebGL rendering context. (Default created from canvas) */
|
||||
gl?: WebGLRenderingContext
|
||||
@@ -57,7 +57,24 @@ declare module "regl" {
|
||||
export type vec3 = [number, number, number]
|
||||
export type vec4 = [number, number, number, number]
|
||||
export type mat3 = [number, number, number, number, number, number, number, number, number]
|
||||
export type mat4 = [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]
|
||||
export type mat4 = [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number
|
||||
]
|
||||
type GlslPrimitive = number | vec2 | vec3 | vec4 | mat3 | mat4
|
||||
|
||||
interface Tick {
|
||||
@@ -68,9 +85,9 @@ declare module "regl" {
|
||||
<P>(params: CommandOptions<P>): Command<P>
|
||||
|
||||
clear(args: {
|
||||
color?: [number, number, number, number],
|
||||
depth?: number,
|
||||
stencil?: number,
|
||||
color?: [number, number, number, number]
|
||||
depth?: number
|
||||
stencil?: number
|
||||
}): void
|
||||
|
||||
// TODO(jlfwong): read()
|
||||
@@ -127,10 +144,25 @@ declare module "regl" {
|
||||
frame(callback: (context: Context) => void): Tick
|
||||
}
|
||||
|
||||
type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array |
|
||||
Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array
|
||||
type TypedArray =
|
||||
| Int8Array
|
||||
| Uint8Array
|
||||
| Uint8ClampedArray
|
||||
| Int16Array
|
||||
| Uint16Array
|
||||
| Int32Array
|
||||
| Uint32Array
|
||||
| Float32Array
|
||||
| Float64Array
|
||||
|
||||
type DrawMode = 'points' | 'lines' | 'line strip' | 'line loop' | 'triangles' | 'triangle strip' | 'triangle fan'
|
||||
type DrawMode =
|
||||
| 'points'
|
||||
| 'lines'
|
||||
| 'line strip'
|
||||
| 'line loop'
|
||||
| 'triangles'
|
||||
| 'triangle strip'
|
||||
| 'triangle fan'
|
||||
|
||||
interface Context {
|
||||
tick: number
|
||||
@@ -177,14 +209,49 @@ declare module "regl" {
|
||||
}
|
||||
|
||||
type MagFilter = 'nearest' | 'linear'
|
||||
type MinFilter = 'nearest' | 'linear' | 'mipmap' | 'linear mipmap linear' | 'nearest mipmap linear' | 'nearest mipmap nearest'
|
||||
type MinFilter =
|
||||
| 'nearest'
|
||||
| 'linear'
|
||||
| 'mipmap'
|
||||
| 'linear mipmap linear'
|
||||
| 'nearest mipmap linear'
|
||||
| 'nearest mipmap nearest'
|
||||
type WrapMode = 'repeat' | 'clamp' | 'mirror'
|
||||
type TextureFormat = 'alpha' | 'luminance' | 'luminance alpha' | 'rgb' | 'rgba' | 'rgba4' | 'rgb5 a1' | 'rgb565' | 'srgb' | 'srgba' | 'depth' | 'depth stencil' | 'rgb s3tc dxt1' | 'rgb s3tc dxt5' | 'rgb atc' | 'rgba atc explicit alpha' | 'rgba atc interpolated alpha' | 'rgb pvrtc 4bppv1' | 'rgb pvrtc 2bppv1' | 'rgba pvrtc 4bppv1' | 'rgba pvrtc 2bppv1' | 'rgb etc1'
|
||||
type TextureFormat =
|
||||
| 'alpha'
|
||||
| 'luminance'
|
||||
| 'luminance alpha'
|
||||
| 'rgb'
|
||||
| 'rgba'
|
||||
| 'rgba4'
|
||||
| 'rgb5 a1'
|
||||
| 'rgb565'
|
||||
| 'srgb'
|
||||
| 'srgba'
|
||||
| 'depth'
|
||||
| 'depth stencil'
|
||||
| 'rgb s3tc dxt1'
|
||||
| 'rgb s3tc dxt5'
|
||||
| 'rgb atc'
|
||||
| 'rgba atc explicit alpha'
|
||||
| 'rgba atc interpolated alpha'
|
||||
| 'rgb pvrtc 4bppv1'
|
||||
| 'rgb pvrtc 2bppv1'
|
||||
| 'rgba pvrtc 4bppv1'
|
||||
| 'rgba pvrtc 2bppv1'
|
||||
| 'rgb etc1'
|
||||
type TextureType = 'uint8' | 'uint16' | 'float' | 'float32' | 'half float' | 'float16'
|
||||
type ColorSpace = 'none' | 'browser'
|
||||
type MipmapHint = "don't care" | 'dont care' | 'nice' | 'fast'
|
||||
|
||||
type TextureData = number[] | number[][] | TypedArray | HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | CanvasRenderingContext2D
|
||||
type TextureData =
|
||||
| number[]
|
||||
| number[][]
|
||||
| TypedArray
|
||||
| HTMLImageElement
|
||||
| HTMLVideoElement
|
||||
| HTMLCanvasElement
|
||||
| CanvasRenderingContext2D
|
||||
interface TextureOptions {
|
||||
width?: number
|
||||
height?: number
|
||||
@@ -233,7 +300,17 @@ declare module "regl" {
|
||||
// TODO(jlfwong): Cubic frame buffers
|
||||
|
||||
interface RenderBufferOptions {
|
||||
format?: 'rgba4' | 'rgb565' | 'rgb5 a1' | 'depth' | 'stencil' | 'depth stencil' | 'srgba' | 'rgba16f' | 'rgb16f' | 'rgba32f'
|
||||
format?:
|
||||
| 'rgba4'
|
||||
| 'rgb565'
|
||||
| 'rgb5 a1'
|
||||
| 'depth'
|
||||
| 'stencil'
|
||||
| 'depth stencil'
|
||||
| 'srgba'
|
||||
| 'rgba16f'
|
||||
| 'rgb16f'
|
||||
| 'rgba32f'
|
||||
width?: number
|
||||
height?: number
|
||||
shape?: [number, number]
|
||||
@@ -256,7 +333,15 @@ declare module "regl" {
|
||||
depth?: boolean | RenderBuffer | Texture
|
||||
stencil?: boolean | RenderBuffer | Texture
|
||||
depthStencil?: boolean | RenderBuffer | Texture
|
||||
colorFormat?: 'rgba' | 'rgba4' | 'rgb565' | 'rgb5 a1' | 'rgb16f' | 'rgba16f' | 'rgba32f' | 'srgba'
|
||||
colorFormat?:
|
||||
| 'rgba'
|
||||
| 'rgba4'
|
||||
| 'rgb565'
|
||||
| 'rgb5 a1'
|
||||
| 'rgb16f'
|
||||
| 'rgba16f'
|
||||
| 'rgba32f'
|
||||
| 'srgba'
|
||||
colorType?: 'uint8' | 'half float' | 'float'
|
||||
}
|
||||
interface Framebuffer {
|
||||
@@ -281,18 +366,60 @@ declare module "regl" {
|
||||
size?: number
|
||||
divisor?: number
|
||||
}
|
||||
type Attribute = AttributeOptions | Buffer | BufferArgs | { constant: number | vec2 | vec3 | vec4 | mat3 | mat4 }
|
||||
type Attribute =
|
||||
| AttributeOptions
|
||||
| Buffer
|
||||
| BufferArgs
|
||||
| {constant: number | vec2 | vec3 | vec4 | mat3 | mat4}
|
||||
|
||||
interface Computed<P, T> {
|
||||
(context: Context, props: P, batchId: number): T
|
||||
}
|
||||
type MaybeComputed<P, T> = Computed<P, T> | T
|
||||
|
||||
type DepthFunction = 'never' | 'always' | '<' | 'less' | '<=' | 'lequal' | '>' | 'greater' | '>=' | 'gequal' | '=' | 'equal' | '!=' | 'notequal'
|
||||
type BlendFunction = 0 | 'zero' | 1 | 'one' | 'src color' | 'one minus src color' | 'src alpha' | 'one minus src alpha' | 'dst color' | 'one minus dst color' | 'dst alpha' | 'one minus dst alpha' | 'constant color' | 'one minus constant color' | 'one minus constant alpha' | 'src alpha saturate'
|
||||
type DepthFunction =
|
||||
| 'never'
|
||||
| 'always'
|
||||
| '<'
|
||||
| 'less'
|
||||
| '<='
|
||||
| 'lequal'
|
||||
| '>'
|
||||
| 'greater'
|
||||
| '>='
|
||||
| 'gequal'
|
||||
| '='
|
||||
| 'equal'
|
||||
| '!='
|
||||
| 'notequal'
|
||||
type BlendFunction =
|
||||
| 0
|
||||
| 'zero'
|
||||
| 1
|
||||
| 'one'
|
||||
| 'src color'
|
||||
| 'one minus src color'
|
||||
| 'src alpha'
|
||||
| 'one minus src alpha'
|
||||
| 'dst color'
|
||||
| 'one minus dst color'
|
||||
| 'dst alpha'
|
||||
| 'one minus dst alpha'
|
||||
| 'constant color'
|
||||
| 'one minus constant color'
|
||||
| 'one minus constant alpha'
|
||||
| 'src alpha saturate'
|
||||
type BlendEquation = 'add' | 'subtract' | 'reverse subtract' | 'min' | 'max'
|
||||
type StencilFunction = DepthFunction
|
||||
type StencilOp = 'zero' | 'keep' | 'replace' | 'invert' | 'increment' | 'decrement' | 'increment wrap' | 'decrement wrap'
|
||||
type StencilOp =
|
||||
| 'zero'
|
||||
| 'keep'
|
||||
| 'replace'
|
||||
| 'invert'
|
||||
| 'increment'
|
||||
| 'decrement'
|
||||
| 'increment wrap'
|
||||
| 'decrement wrap'
|
||||
interface CommandOptions<P> {
|
||||
/** Source code of vertex shader */
|
||||
vert?: string
|
||||
@@ -300,11 +427,11 @@ declare module "regl" {
|
||||
/** Source code of fragment shader */
|
||||
frag?: string
|
||||
|
||||
context?: { [contextName: string]: MaybeComputed<P, any> }
|
||||
context?: {[contextName: string]: MaybeComputed<P, any>}
|
||||
|
||||
uniforms?: { [uniformName: string]: MaybeComputed<P, Uniform> }
|
||||
uniforms?: {[uniformName: string]: MaybeComputed<P, Uniform>}
|
||||
|
||||
attributes?: { [attributeName: string]: MaybeComputed<P, Attribute> }
|
||||
attributes?: {[attributeName: string]: MaybeComputed<P, Attribute>}
|
||||
|
||||
primitive?: DrawMode
|
||||
|
||||
@@ -324,51 +451,70 @@ declare module "regl" {
|
||||
|
||||
profile?: MaybeComputed<P, boolean>
|
||||
|
||||
depth?: MaybeComputed<P, {
|
||||
enable?: boolean,
|
||||
mask?: boolean,
|
||||
func?: DepthFunction,
|
||||
range?: [number, number]
|
||||
}>
|
||||
|
||||
blend?: MaybeComputed<P, {
|
||||
enable?: boolean,
|
||||
func?: {
|
||||
src: BlendFunction
|
||||
dst: BlendFunction
|
||||
} | {
|
||||
srcRGB: BlendFunction
|
||||
srcAlpha: BlendFunction
|
||||
dstRGB: BlendFunction
|
||||
dstAlpha: BlendFunction
|
||||
},
|
||||
equation?: BlendEquation | {
|
||||
rgb: BlendEquation
|
||||
alpha: BlendEquation
|
||||
},
|
||||
color?: vec4
|
||||
}>
|
||||
|
||||
stencil?: MaybeComputed<P, {
|
||||
enable?: boolean
|
||||
mask?: number
|
||||
func?: StencilFunction
|
||||
opFront?: { fail: StencilOp, zfail: StencilOp, pass: StencilOp },
|
||||
opBack?: { fail: StencilOp, zfail: StencilOp, pass: StencilOp },
|
||||
}>
|
||||
|
||||
polygonOffset?: MaybeComputed<P, {
|
||||
enable?: boolean
|
||||
offset?: {
|
||||
factor: number
|
||||
units: number
|
||||
depth?: MaybeComputed<
|
||||
P,
|
||||
{
|
||||
enable?: boolean
|
||||
mask?: boolean
|
||||
func?: DepthFunction
|
||||
range?: [number, number]
|
||||
}
|
||||
}>
|
||||
>
|
||||
|
||||
cull?: MaybeComputed<P, {
|
||||
enable?: boolean
|
||||
face?: 'front' | 'back'
|
||||
}>
|
||||
blend?: MaybeComputed<
|
||||
P,
|
||||
{
|
||||
enable?: boolean
|
||||
func?:
|
||||
| {
|
||||
src: BlendFunction
|
||||
dst: BlendFunction
|
||||
}
|
||||
| {
|
||||
srcRGB: BlendFunction
|
||||
srcAlpha: BlendFunction
|
||||
dstRGB: BlendFunction
|
||||
dstAlpha: BlendFunction
|
||||
}
|
||||
equation?:
|
||||
| BlendEquation
|
||||
| {
|
||||
rgb: BlendEquation
|
||||
alpha: BlendEquation
|
||||
}
|
||||
color?: vec4
|
||||
}
|
||||
>
|
||||
|
||||
stencil?: MaybeComputed<
|
||||
P,
|
||||
{
|
||||
enable?: boolean
|
||||
mask?: number
|
||||
func?: StencilFunction
|
||||
opFront?: {fail: StencilOp; zfail: StencilOp; pass: StencilOp}
|
||||
opBack?: {fail: StencilOp; zfail: StencilOp; pass: StencilOp}
|
||||
}
|
||||
>
|
||||
|
||||
polygonOffset?: MaybeComputed<
|
||||
P,
|
||||
{
|
||||
enable?: boolean
|
||||
offset?: {
|
||||
factor: number
|
||||
units: number
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
cull?: MaybeComputed<
|
||||
P,
|
||||
{
|
||||
enable?: boolean
|
||||
face?: 'front' | 'back'
|
||||
}
|
||||
>
|
||||
|
||||
frontFace?: MaybeComputed<P, 'cw' | 'ccw'>
|
||||
|
||||
@@ -378,35 +524,46 @@ declare module "regl" {
|
||||
|
||||
colorMask?: MaybeComputed<P, [boolean, boolean, boolean, boolean]>
|
||||
|
||||
sample?: MaybeComputed<P, {
|
||||
enable?: boolean
|
||||
alpha?: boolean
|
||||
coverage?: {
|
||||
value: number
|
||||
invert: boolean
|
||||
sample?: MaybeComputed<
|
||||
P,
|
||||
{
|
||||
enable?: boolean
|
||||
alpha?: boolean
|
||||
coverage?: {
|
||||
value: number
|
||||
invert: boolean
|
||||
}
|
||||
}
|
||||
}>
|
||||
>
|
||||
|
||||
scissor?: MaybeComputed<P, {
|
||||
enable?: boolean
|
||||
box?: {
|
||||
scissor?: MaybeComputed<
|
||||
P,
|
||||
{
|
||||
enable?: boolean
|
||||
box?: {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
viewport?: MaybeComputed<
|
||||
P,
|
||||
{
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
}>
|
||||
|
||||
viewport?: MaybeComputed<P, {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}>
|
||||
>
|
||||
}
|
||||
|
||||
function prop<P>(name: keyof P): (context: Context, props: P, batchId: number) => P[keyof P]
|
||||
function context<P>(name: keyof Context): (context: Context, props: P, batchId: number) => Context[keyof Context]
|
||||
function context<P>(
|
||||
name: keyof Context,
|
||||
): (context: Context, props: P, batchId: number) => Context[keyof Context]
|
||||
|
||||
interface Command<P> {
|
||||
/** One shot rendering */
|
||||
|
||||
@@ -38,5 +38,3 @@ export abstract class ReloadableComponent<P, S> extends Component<P, S> {
|
||||
return Object.create(null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {h, render} from 'preact'
|
||||
import {Application} from'./application'
|
||||
import {Application} from './application'
|
||||
|
||||
let app: Application | null = null
|
||||
const retained = (window as any)['__retained__'] as any
|
||||
@@ -7,7 +7,7 @@ declare const module: any
|
||||
if (module.hot) {
|
||||
module.hot.dispose(() => {
|
||||
if (app) {
|
||||
(window as any)['__retained__'] = app.serialize()
|
||||
;(window as any)['__retained__'] = app.serialize()
|
||||
}
|
||||
})
|
||||
module.hot.accept()
|
||||
@@ -21,4 +21,4 @@ function ref(instance: Application | null) {
|
||||
}
|
||||
}
|
||||
|
||||
render(<Application ref={ref}/>, document.body, document.body.lastElementChild || undefined)
|
||||
render(<Application ref={ref} />, document.body, document.body.lastElementChild || undefined)
|
||||
|
||||
101
stats.ts
101
stats.ts
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
style.ts
10
style.ts
@@ -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',
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import regl from 'regl'
|
||||
import { Vec2, Rect, AffineTransform } from './math'
|
||||
import {Vec2, Rect, AffineTransform} from './math'
|
||||
|
||||
export interface TextureRendererProps {
|
||||
texture: regl.Texture
|
||||
@@ -36,7 +36,7 @@ export class TextureRenderer {
|
||||
`,
|
||||
|
||||
depth: {
|
||||
enable: false
|
||||
enable: false,
|
||||
},
|
||||
|
||||
attributes: {
|
||||
@@ -48,54 +48,41 @@ export class TextureRenderer {
|
||||
// | /|
|
||||
// |/ |
|
||||
// 2 +--+ 3
|
||||
position: gl.buffer([
|
||||
[-1, 1],
|
||||
[1, 1],
|
||||
[-1, -1],
|
||||
[1, -1]
|
||||
]),
|
||||
uv: gl.buffer([
|
||||
[0, 1],
|
||||
[1, 1],
|
||||
[0, 0],
|
||||
[1, 0]
|
||||
])
|
||||
position: gl.buffer([[-1, 1], [1, 1], [-1, -1], [1, -1]]),
|
||||
uv: gl.buffer([[0, 1], [1, 1], [0, 0], [1, 0]]),
|
||||
},
|
||||
|
||||
uniforms: {
|
||||
texture: (context, props) => props.texture,
|
||||
uvTransform: (context, props) => {
|
||||
const { srcRect, texture } = props
|
||||
const {srcRect, texture} = props
|
||||
const physicalToUV = AffineTransform.withTranslation(new Vec2(0, 1))
|
||||
.times(AffineTransform.withScale(new Vec2(1, -1)))
|
||||
.times(AffineTransform.betweenRects(
|
||||
.times(
|
||||
AffineTransform.betweenRects(
|
||||
new Rect(Vec2.zero, new Vec2(texture.width, texture.height)),
|
||||
Rect.unit
|
||||
))
|
||||
Rect.unit,
|
||||
),
|
||||
)
|
||||
const uvRect = physicalToUV.transformRect(srcRect)
|
||||
return AffineTransform.betweenRects(
|
||||
Rect.unit,
|
||||
uvRect,
|
||||
).flatten()
|
||||
return AffineTransform.betweenRects(Rect.unit, uvRect).flatten()
|
||||
},
|
||||
positionTransform: (context, props) => {
|
||||
const { dstRect } = props
|
||||
const {dstRect} = props
|
||||
|
||||
const viewportSize = new Vec2(context.viewportWidth, context.viewportHeight)
|
||||
|
||||
const physicalToNDC = AffineTransform.withScale(new Vec2(1, -1))
|
||||
.times(AffineTransform.betweenRects(
|
||||
new Rect(Vec2.zero, viewportSize),
|
||||
Rect.NDC)
|
||||
)
|
||||
const physicalToNDC = AffineTransform.withScale(new Vec2(1, -1)).times(
|
||||
AffineTransform.betweenRects(new Rect(Vec2.zero, viewportSize), Rect.NDC),
|
||||
)
|
||||
const ndcRect = physicalToNDC.transformRect(dstRect)
|
||||
return AffineTransform.betweenRects(Rect.NDC, ndcRect).flatten()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
primitive: 'triangle strip',
|
||||
|
||||
count: 4
|
||||
count: 4,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -103,8 +90,12 @@ export class TextureRenderer {
|
||||
this.command(props)
|
||||
}
|
||||
|
||||
resetStats() { return Object.assign(this.command.stats, { cpuTime: 0, gpuTime: 0, count: 0 }) }
|
||||
stats() { return this.command.stats }
|
||||
resetStats() {
|
||||
return Object.assign(this.command.stats, {cpuTime: 0, gpuTime: 0, count: 0})
|
||||
}
|
||||
stats() {
|
||||
return this.command.stats
|
||||
}
|
||||
}
|
||||
|
||||
export interface TextureCachedRendererOptions<T> {
|
||||
@@ -141,10 +132,13 @@ export class TextureCachedRenderer<T> {
|
||||
render(props: T) {
|
||||
this.withContext((context: regl.Context) => {
|
||||
let needsRender = false
|
||||
if (this.texture.width !== context.viewportWidth || this.texture.height !== context.viewportHeight) {
|
||||
if (
|
||||
this.texture.width !== context.viewportWidth ||
|
||||
this.texture.height !== context.viewportHeight
|
||||
) {
|
||||
// TODO(jlfwong): Can probably just use this.framebuffer.resize
|
||||
this.texture({ width: context.viewportWidth, height: context.viewportHeight })
|
||||
this.framebuffer({ color: [this.texture] })
|
||||
this.texture({width: context.viewportWidth, height: context.viewportHeight})
|
||||
this.framebuffer({color: [this.texture]})
|
||||
needsRender = true
|
||||
} else if (this.lastRenderProps == null) {
|
||||
needsRender = true
|
||||
@@ -161,26 +155,29 @@ export class TextureCachedRenderer<T> {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: context.viewportWidth,
|
||||
height: context.viewportHeight
|
||||
height: context.viewportHeight,
|
||||
}
|
||||
},
|
||||
framebuffer: this.framebuffer
|
||||
framebuffer: this.framebuffer,
|
||||
})(() => {
|
||||
this.gl.clear({color: [0, 0, 0, 0]})
|
||||
this.renderUncached(props)
|
||||
})
|
||||
}
|
||||
|
||||
const glViewportRect = new Rect(Vec2.zero, new Vec2(context.viewportWidth, context.viewportHeight))
|
||||
const glViewportRect = new Rect(
|
||||
Vec2.zero,
|
||||
new Vec2(context.viewportWidth, context.viewportHeight),
|
||||
)
|
||||
|
||||
// Render from texture
|
||||
this.textureRenderer.render({
|
||||
texture: this.texture,
|
||||
srcRect: glViewportRect,
|
||||
dstRect: glViewportRect
|
||||
dstRect: glViewportRect,
|
||||
})
|
||||
this.lastRenderProps = props
|
||||
this.dirty = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
utils.ts
8
utils.ts
@@ -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)!
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user