speedscope/application.tsx

524 lines
16 KiB
TypeScript
Raw Normal View History

2018-01-07 22:46:44 +03:00
import {h} from 'preact'
2017-12-23 21:37:56 +03:00
import {StyleSheet, css} from 'aphrodite'
2018-01-24 10:47:17 +03:00
import {ReloadableComponent, SerializedComponent} from './reloadable'
2017-12-23 21:37:56 +03:00
import {importFromBGFlameGraph} from './import/bg-flamegraph'
import {importFromStackprof} from './import/stackprof'
2018-01-08 06:50:27 +03:00
import {importFromChromeTimeline, importFromChromeCPUProfile} from './import/chrome'
2018-01-25 21:17:17 +03:00
import { FlamechartRenderer } from './flamechart-renderer'
2018-01-23 20:59:14 +03:00
import { CanvasContext } from './canvas-context'
2017-12-23 21:37:56 +03:00
2018-01-08 05:36:23 +03:00
import {Profile, Frame} from './profile'
2017-12-26 20:59:42 +03:00
import {Flamechart} from './flamechart'
import { FlamechartView } from './flamechart-view'
2018-01-07 22:45:05 +03:00
import { FontFamily, FontSize, Colors } from './style'
2017-12-23 21:37:56 +03:00
const enum SortOrder {
CHRONO,
2018-01-07 04:31:48 +03:00
LEFT_HEAVY
2017-12-23 21:37:56 +03:00
}
interface ApplicationState {
profile: Profile | null
flamechart: Flamechart | null
2018-01-25 21:17:17 +03:00
flamechartRenderer: FlamechartRenderer | null
2017-12-23 21:37:56 +03:00
sortedFlamechart: Flamechart | null
2018-01-25 21:17:17 +03:00
sortedFlamechartRenderer: FlamechartRenderer | null
2017-12-23 21:37:56 +03:00
sortOrder: SortOrder
2018-01-19 21:50:22 +03:00
loading: boolean
2017-12-23 21:37:56 +03:00
}
2018-01-08 03:18:41 +03:00
interface ToolbarProps extends ApplicationState {
setSortOrder(order: SortOrder): void
}
function importProfile(contents: string, fileName: string): Profile | null {
try {
// First pass: Check known file format names to infer the file type
if (fileName.endsWith('.cpuprofile')) {
console.log('Importing as Chrome CPU Profile')
return importFromChromeCPUProfile(JSON.parse(contents))
} else if (fileName.endsWith('.chrome.json') || /Profile-\d{8}T\d{6}/.exec(fileName)) {
console.log('Importing as Chrome Timeline')
return importFromChromeTimeline(JSON.parse(contents))
} else if (fileName.endsWith('.stackprof.json')) {
console.log('Importing as stackprof profile')
return importFromStackprof(JSON.parse(contents))
} else if (fileName.endsWith('.txt')) {
console.log('Importing as collapsed stack format')
return importFromBGFlameGraph(contents)
}
// 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") {
console.log('Importing as Chrome CPU Profile')
return importFromChromeTimeline(parsed)
} else if ('nodes' in parsed && 'samples' in parsed && 'timeDeltas' in parsed) {
console.log('Importing as Chrome Timeline')
return importFromChromeCPUProfile(parsed)
} else if ('mode' in parsed && 'frames' in parsed) {
console.log('Importing as stackprof profile')
return importFromStackprof(parsed)
}
} catch (e) {
// Format is not JSON
// If every line ends with a space followed by a number, it's probably
// the collapsed stack format.
const lineCount = contents.split(/\n/).length
if (lineCount > 1 && lineCount === contents.split(/ \d+\n/).length) {
console.log('Importing as collapsed stack format')
return importFromBGFlameGraph(contents)
}
}
return null
} catch (e) {
console.error(e)
return null
}
}
2018-01-08 03:18:41 +03:00
export class Toolbar extends ReloadableComponent<ToolbarProps, void> {
setTimeOrder = () => {
this.props.setSortOrder(SortOrder.CHRONO)
}
setLeftHeavyOrder = () => {
this.props.setSortOrder(SortOrder.LEFT_HEAVY)
}
render() {
2018-01-08 03:18:41 +03:00
const help = (
<div className={css(style.toolbarTab)}>
<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)}>
2018-01-08 03:18:41 +03:00
<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>
{this.props.profile.getName()}
<div className={css(style.toolbarRight)}>🔬speedscope</div>
</div>
}
}
2018-01-22 22:22:24 +03:00
interface GLCanvasProps {
2018-01-23 20:59:14 +03:00
setCanvasContext(canvasContext: CanvasContext | null): void
2018-01-22 22:22:24 +03:00
}
export class GLCanvas extends ReloadableComponent<GLCanvasProps, void> {
2018-01-23 20:59:14 +03:00
private canvas: HTMLCanvasElement | null = null
private canvasContext: CanvasContext | null = null
2018-01-22 22:22:24 +03:00
private ref = (canvas?: Element) => {
if (canvas instanceof HTMLCanvasElement) {
this.canvas = canvas
2018-01-23 20:59:14 +03:00
this.canvasContext = new CanvasContext(canvas)
2018-01-22 22:22:24 +03:00
} else {
2018-01-23 20:59:14 +03:00
this.canvas = null
this.canvasContext = null
2018-01-22 22:22:24 +03:00
}
2018-01-23 20:59:14 +03:00
this.props.setCanvasContext(this.canvasContext)
2018-01-22 22:22:24 +03:00
}
private maybeResize() {
2018-01-23 20:59:14 +03:00
if (!this.canvas) return
2018-01-22 22:22:24 +03:00
let { width, height } = this.canvas.getBoundingClientRect()
width = Math.floor(width) * window.devicePixelRatio
height = Math.floor(height) * window.devicePixelRatio
// Still initializing: don't resize yet
if (width < 4 || height < 4) return
const oldWidth = this.canvas.width
const oldHeight = this.canvas.height
// Already at the right size
if (width === oldWidth && height === oldHeight) return
this.canvas.width = width
this.canvas.height = height
}
onWindowResize = () => {
this.maybeResize()
window.addEventListener('resize', this.onWindowResize)
}
componentDidMount() {
window.addEventListener('resize', this.onWindowResize)
requestAnimationFrame(() => this.maybeResize())
}
componentWillUnmount() {
window.removeEventListener('resize', this.onWindowResize)
}
render() {
return <canvas className={css(style.glCanvasView)} ref={this.ref} width={1} height={1} />
}
}
export class Application extends ReloadableComponent<{}, ApplicationState> {
2017-12-23 21:37:56 +03:00
constructor() {
super()
this.state = {
2018-01-19 21:50:22 +03:00
loading: false,
2017-12-23 21:37:56 +03:00
profile: null,
flamechart: null,
2018-01-25 21:17:17 +03:00
flamechartRenderer: null,
2017-12-23 21:37:56 +03:00
sortedFlamechart: null,
2018-01-25 21:17:17 +03:00
sortedFlamechartRenderer: null,
2017-12-23 21:37:56 +03:00
sortOrder: SortOrder.CHRONO
}
}
2018-01-24 10:47:17 +03:00
serialize() {
const result = super.serialize()
2018-01-25 21:17:17 +03:00
delete result.state.flamechartRenderer
delete result.state.sortedFlamechartRenderer
2018-01-24 10:47:17 +03:00
return result
}
rehydrate(serialized: SerializedComponent<ApplicationState>) {
super.rehydrate(serialized)
const { flamechart, sortedFlamechart } = serialized.state
if (this.canvasContext && flamechart && sortedFlamechart) {
this.setState({
2018-01-25 21:17:17 +03:00
flamechartRenderer: new FlamechartRenderer(this.canvasContext, flamechart),
sortedFlamechartRenderer: new FlamechartRenderer(this.canvasContext, sortedFlamechart)
2018-01-24 10:47:17 +03:00
})
}
}
2018-01-07 22:45:05 +03:00
loadFromString(fileName: string, contents: string) {
2018-01-23 20:59:14 +03:00
if (!this.canvasContext) return
2018-01-22 22:22:24 +03:00
2018-01-07 22:45:05 +03:00
console.time('import')
const profile = importProfile(contents, fileName)
if (profile == null) {
2018-01-19 21:50:22 +03:00
this.setState({ loading: false })
// TODO(jlfwong): Make this a nicer overlay
alert('Unrecognized format! See documentation about supported formats.')
return
}
2018-01-08 03:18:41 +03:00
profile.setName(fileName)
document.title = `${fileName} - speedscope`
2018-01-08 05:36:23 +03:00
const frames: Frame[] = []
profile.forEachFrame(f => frames.push(f))
2018-01-30 12:51:02 +03:00
function key(f: Frame) {
return (f.file || '') + f.name
}
function compare(a: Frame, b: Frame) {
return key(a) > key(b) ? 1 : -1
}
frames.sort(compare)
const frameToColorBucket = new Map<Frame, number>()
for (let i = 0; i < frames.length; i++) {
frameToColorBucket.set(frames[i], Math.floor(255 * i / frames.length))
}
function getColorBucketForFrame(frame: Frame) {
return frameToColorBucket.get(frame) || 0
}
2018-01-08 05:36:23 +03:00
const flamechart = new Flamechart({
getTotalWeight: profile.getTotalWeight.bind(profile),
forEachCall: profile.forEachCall.bind(profile),
formatValue: profile.formatValue.bind(profile),
2018-01-30 12:51:02 +03:00
getColorBucketForFrame
2018-01-08 05:36:23 +03:00
})
2018-01-25 21:17:17 +03:00
const flamechartRenderer = new FlamechartRenderer(this.canvasContext, flamechart)
2018-01-08 05:36:23 +03:00
2018-01-07 22:45:05 +03:00
const sortedFlamechart = new Flamechart({
getTotalWeight: profile.getTotalNonIdleWeight.bind(profile),
forEachCall: profile.forEachCallGrouped.bind(profile),
formatValue: profile.formatValue.bind(profile),
2018-01-30 12:51:02 +03:00
getColorBucketForFrame
2018-01-07 22:45:05 +03:00
})
2018-01-25 21:17:17 +03:00
const sortedFlamechartRenderer = new FlamechartRenderer(this.canvasContext, sortedFlamechart)
2018-01-22 22:22:24 +03:00
console.timeEnd('import')
2018-01-08 05:36:23 +03:00
2018-01-17 09:27:31 +03:00
console.time('first setState')
2018-01-22 22:22:24 +03:00
this.setState({
profile,
flamechart,
2018-01-25 21:17:17 +03:00
flamechartRenderer,
2018-01-22 22:22:24 +03:00
sortedFlamechart,
2018-01-25 21:17:17 +03:00
sortedFlamechartRenderer,
2018-01-22 22:22:24 +03:00
loading: false
}, () => {
2018-01-17 09:27:31 +03:00
console.timeEnd('first setState')
2018-01-07 22:45:05 +03:00
})
}
loadFromFile(file: File) {
2018-01-19 21:50:22 +03:00
this.setState({ loading: true }, () => {
requestAnimationFrame(() => {
const reader = new FileReader
reader.addEventListener('loadend', () => {
this.loadFromString(file.name, reader.result)
})
reader.readAsText(file)
})
2017-12-23 21:37:56 +03:00
})
2018-01-07 22:45:05 +03:00
}
loadExample = () => {
2018-01-19 21:50:22 +03:00
this.setState({ loading: true })
2018-01-07 22:45:05 +03:00
fetch('dist/perf-vertx-stacks-01-collapsed-all.txt').then(resp => resp.text()).then(data => {
this.loadFromString('perf-vertx-stacks-01-collapsed-all.txt', data)
})
}
onDrop = (ev: DragEvent) => {
this.loadFromFile(ev.dataTransfer.files.item(0))
2017-12-23 21:37:56 +03:00
ev.preventDefault()
}
onDragOver = (ev: DragEvent) => {
ev.preventDefault()
}
onWindowKeyPress = (ev: KeyboardEvent) => {
2018-01-07 04:31:48 +03:00
if (ev.key === '1') {
2017-12-23 21:37:56 +03:00
this.setState({
2018-01-07 04:31:48 +03:00
sortOrder: SortOrder.CHRONO
})
} else if (ev.key === '2') {
this.setState({
sortOrder: SortOrder.LEFT_HEAVY
2017-12-23 21:37:56 +03:00
})
}
}
componentDidMount() {
window.addEventListener('keypress', this.onWindowKeyPress)
}
componentWillUnmount() {
window.removeEventListener('keypress', this.onWindowKeyPress)
}
flamechartView: FlamechartView | null
flamechartRef = (view: FlamechartView | null) => this.flamechartView = view
subcomponents() {
return {
flamechart: this.flamechartView
}
}
2018-01-07 22:45:05 +03:00
onFileSelect = (ev: Event) => {
this.loadFromFile((ev.target as HTMLInputElement).files!.item(0))
}
renderLanding() {
return <div className={css(style.landingContainer)}>
<div className={css(style.landingMessage)}>
2018-01-08 06:50:27 +03:00
<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>
2018-01-07 22:45:05 +03:00
<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>
<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>
}
2018-01-19 21:50:22 +03:00
renderLoadingBar() {
return <div className={css(style.loading)}></div>
}
2018-01-08 03:18:41 +03:00
setSortOrder = (sortOrder: SortOrder) => {
this.setState({ sortOrder })
}
2018-01-23 20:59:14 +03:00
private canvasContext: CanvasContext | null = null
private setCanvasContext = (canvasContext: CanvasContext | null) => {
this.canvasContext = canvasContext
2018-01-22 22:22:24 +03:00
}
2017-12-23 21:37:56 +03:00
render() {
2018-01-25 21:17:17 +03:00
const {flamechart, flamechartRenderer, sortedFlamechart, sortedFlamechartRenderer, sortOrder, loading} = this.state
2017-12-23 21:37:56 +03:00
const flamechartToView = sortOrder == SortOrder.CHRONO ? flamechart : sortedFlamechart
2018-01-25 21:17:17 +03:00
const flamechartRendererToUse = sortOrder == SortOrder.CHRONO ? flamechartRenderer : sortedFlamechartRenderer
2017-12-23 21:37:56 +03:00
return <div onDrop={this.onDrop} onDragOver={this.onDragOver} className={css(style.root)}>
2018-01-23 20:59:14 +03:00
<GLCanvas setCanvasContext={this.setCanvasContext} />
2018-01-08 03:18:41 +03:00
<Toolbar setSortOrder={this.setSortOrder} {...this.state} />
2018-01-19 21:50:22 +03:00
{loading ?
this.renderLoadingBar() :
2018-01-25 21:17:17 +03:00
this.canvasContext && flamechartToView && flamechartRendererToUse ?
2018-01-22 22:22:24 +03:00
<FlamechartView
2018-01-23 20:59:14 +03:00
canvasContext={this.canvasContext}
2018-01-25 21:17:17 +03:00
flamechartRenderer={flamechartRendererToUse}
2018-01-22 22:22:24 +03:00
ref={this.flamechartRef}
flamechart={flamechartToView} /> :
2018-01-19 21:50:22 +03:00
this.renderLanding()}
2017-12-23 21:37:56 +03:00
</div>
}
}
const style = StyleSheet.create({
2018-01-22 22:22:24 +03:00
glCanvasView: {
position: 'absolute',
width: '100vw',
height: '100vh',
zIndex: -1,
pointerEvents: 'none'
},
2018-01-19 21:50:22 +03:00
loading: {
height: 3,
marginBottom: -3,
background: Colors.DARK_BLUE,
transformOrigin: '0% 50%',
animationName: [{
from: {
transform: `scaleX(0)`
},
to: {
transform: `scaleX(1)`
}
}],
animationTimingFunction: "cubic-bezier(0, 1, 0, 1)",
animationDuration: "30s"
},
2017-12-23 21:37:56 +03:00
root: {
width: '100vw',
height: '100vh',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
position: 'relative',
2018-01-08 06:50:27 +03:00
fontFamily: FontFamily.MONOSPACE,
lineHeight: '20px'
},
2018-01-07 22:45:05 +03:00
landingContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flex: 1
},
landingMessage: {
maxWidth: 600
},
landingP: {
marginBottom: 16
},
hide: {
display: 'none'
},
browseButtonContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
},
browseButton: {
marginBottom: 16,
height: 72,
flex: 1,
maxWidth: 256,
textAlign: 'center',
fontSize: FontSize.BIG_BUTTON,
lineHeight: '72px',
background: Colors.DARK_BLUE,
color: 'white',
cursor: 'pointer'
},
link: {
color: Colors.LIGHT_BLUE,
cursor: 'pointer',
textDecoration: 'none'
2018-01-08 03:18:41 +03:00
},
toolbar: {
height: 18,
background: 'black',
color: 'white',
textAlign: 'center',
fontFamily: FontFamily.MONOSPACE,
fontSize: FontSize.TITLE,
lineHeight: '18px',
userSelect: 'none'
},
toolbarLeft: {
position: 'absolute',
height: 18,
overflow: 'hidden',
top: 0,
left: 0,
marginRight: 2,
textAlign: 'left',
},
toolbarRight: {
height: 18,
overflow: 'hidden',
position: 'absolute',
top: 0,
right: 0,
marginRight: 2,
textAlign: 'right',
},
toolbarTab: {
background: Colors.DARK_GRAY,
marginTop: 2,
height: 16,
lineHeight: '16px',
paddingLeft: 2,
paddingRight: 8,
display: 'inline-block',
marginLeft: 2,
':hover': {
background: Colors.GRAY,
cursor: 'pointer'
}
},
toolbarTabActive: {
background: Colors.LIGHT_BLUE,
':hover': {
background: Colors.LIGHT_BLUE
}
},
noLinkStyle: {
textDecoration: 'none',
color: 'inherit'
},
emoji: {
display: 'inline-block',
verticalAlign: 'middle',
paddingTop: '0px',
marginRight: '0.3em'
2017-12-23 21:37:56 +03:00
}
})