Hot reloading WIP

This commit is contained in:
Jamie Wong 2017-12-23 13:37:56 -05:00
parent c9f0cf83c1
commit a51382c525
5 changed files with 173 additions and 89 deletions

101
application.tsx Normal file
View File

@ -0,0 +1,101 @@
import {h, Component} from 'preact'
import {StyleSheet, css} from 'aphrodite'
import {ReloadableComponent} from './reloadable'
import {importFromBGFlameGraph} from './import/bg-flamegraph'
import {importFromStackprof} from './import/stackprof'
import {Profile} from './profile'
import {Flamechart, FlamechartView} from './flamechart'
const enum SortOrder {
CHRONO,
ALPHA
}
interface ApplicationState {
profile: Profile | null
flamechart: Flamechart | null
sortedFlamechart: Flamechart | null
sortOrder: SortOrder
}
export class Application extends ReloadableComponent<{}, ApplicationState, {}> {
constructor() {
super()
this.state = {
profile: null,
flamechart: null,
sortedFlamechart: null,
sortOrder: SortOrder.CHRONO
}
}
onDrop = (ev: DragEvent) => {
const file = ev.dataTransfer.files.item(0)
const reader = new FileReader
reader.addEventListener('loadend', () => {
const profile = file.name.endsWith('json') ? importFromStackprof(reader.result) : importFromBGFlameGraph(reader.result)
const flamechart = new Flamechart(profile)
const sortedFlamechart = new Flamechart(profile.sortedAlphabetically())
this.setState({profile, flamechart, sortedFlamechart})
})
reader.readAsText(file)
ev.preventDefault()
}
onDragOver = (ev: DragEvent) => {
ev.preventDefault()
}
onWindowKeyPress = (ev: KeyboardEvent) => {
if (ev.key == 'a') {
this.setState({
sortOrder: this.state.sortOrder === SortOrder.CHRONO ? SortOrder.ALPHA : SortOrder.CHRONO
})
}
}
onWindowResize = () => {
this.forceUpdate()
}
componentDidMount() {
window.addEventListener('resize', this.onWindowResize)
// TODO(jlfwong): for this to be safely embeddable, there'll need to be some
// way of specify event focus.
window.addEventListener('keypress', this.onWindowKeyPress)
}
componentWillUnmount() {
window.removeEventListener('resize', this.onWindowResize)
window.removeEventListener('keypress', this.onWindowKeyPress)
}
flamechartView: FlamechartView | null
flamechartRef = (view: FlamechartView | null) => this.flamechartView = view
subcomponents() {
return {
flamechart: this.flamechartView
}
}
render() {
const {flamechart, sortedFlamechart, sortOrder} = this.state
const flamechartToView = sortOrder == SortOrder.CHRONO ? flamechart : sortedFlamechart
return <div onDrop={this.onDrop} onDragOver={this.onDragOver} className={css(style.root)}>
{flamechartToView &&
<FlamechartView ref={this.flamechartRef} flamechart={flamechartToView} />}
</div>
}
}
const style = StyleSheet.create({
root: {
width: '100vw',
height: '100vh',
overflow: 'hidden'
}
})

View File

@ -1,5 +1,6 @@
import {h, Component} from 'preact'
import {StyleSheet, css} from 'aphrodite'
import {ReloadableComponent} from './reloadable'
import {Profile, Frame, CallTreeNode} from './profile'
import * as regl from 'regl'
@ -647,7 +648,7 @@ interface FlamechartViewState {
logicalSpaceMouse: Vec2
}
export class FlamechartView extends Component<FlamechartViewProps, FlamechartViewState> {
export class FlamechartView extends ReloadableComponent<FlamechartViewProps, FlamechartViewState> {
container: HTMLDivElement | null = null
constructor() {

View File

@ -5,7 +5,7 @@
"main": "index.js",
"scripts": {
"serve": "parcel -o dev.html -d dist/dev",
"release": "parcel build speedscope.tsx"
"release": "tsc --noEmit && parcel build speedscope.tsx"
},
"author": "",
"license": "MIT",

51
reloadable.tsx Normal file
View File

@ -0,0 +1,51 @@
import {Component} from 'preact'
interface SerializedComponent<S, I> {
state: S
internal: I | null
serializedSubcomponents: {[key: string]: any}
}
export abstract class ReloadableComponent<P, S, I> extends Component<P, S> {
serialize(): SerializedComponent<S, I> {
const serializedSubcomponents: {[key: string]: any} = Object.create(null)
const subcomponents = this.subcomponents()
for (const key in subcomponents) {
const val = subcomponents[key]
if (val && val instanceof ReloadableComponent) {
serializedSubcomponents[key] = val.serialize()
}
}
return {
state: this.state,
internal: this.serializeInternal(),
serializedSubcomponents,
}
}
rehydrate(serialized: SerializedComponent<S, I>) {
this.setState(serialized.state)
if (serialized.internal) {
this.rehydrateInternal(serialized.internal)
}
const subcomponents = this.subcomponents()
for (const key in subcomponents) {
const val = subcomponents[key]
const data = serialized.serializedSubcomponents[key]
if (data && val && val instanceof ReloadableComponent) {
subcomponents.serialize(data)
}
}
}
serializeInternal(): I | null {
return null
}
rehydrateInternal(internal: I) {}
subcomponents(): {[key: string]: any} {
return Object.create(null)
}
}

View File

@ -1,93 +1,24 @@
import {h, render, Component} from 'preact'
import {StyleSheet, css} from 'aphrodite'
import {h, render} from 'preact'
import {Application} from'./application'
import {importFromBGFlameGraph} from './import/bg-flamegraph'
import {importFromStackprof} from './import/stackprof'
import {Profile} from './profile'
import {Flamechart, FlamechartView} from './flamechart'
const enum SortOrder {
CHRONO,
ALPHA
}
interface ApplicationState {
profile: Profile | null
flamechart: Flamechart | null
sortedFlamechart: Flamechart | null
sortOrder: SortOrder
}
class Application extends Component<{}, ApplicationState> {
constructor() {
super()
this.state = {
profile: null,
flamechart: null,
sortedFlamechart: null,
sortOrder: SortOrder.CHRONO
let app: Application | null = null
const retained = (window as any)['__retained__'] as any
declare const module: any
if (module.hot) {
module.hot.dispose(() => {
if (app) {
(window as any)['__retained__'] = app.serialize()
}
}
})
module.hot.accept()
}
onDrop = (ev: DragEvent) => {
const file = ev.dataTransfer.files.item(0)
const reader = new FileReader
reader.addEventListener('loadend', () => {
const profile = file.name.endsWith('json') ? importFromStackprof(reader.result) : importFromBGFlameGraph(reader.result)
const flamechart = new Flamechart(profile)
const sortedFlamechart = new Flamechart(profile.sortedAlphabetically())
this.setState({profile, flamechart, sortedFlamechart})
})
reader.readAsText(file)
ev.preventDefault()
}
onDragOver = (ev: DragEvent) => {
ev.preventDefault()
}
onWindowKeyPress = (ev: KeyboardEvent) => {
if (ev.key == 'a') {
this.setState({
sortOrder: this.state.sortOrder === SortOrder.CHRONO ? SortOrder.ALPHA : SortOrder.CHRONO
})
}
}
onWindowResize = () => {
this.forceUpdate()
}
componentDidMount() {
window.addEventListener('resize', this.onWindowResize)
// TODO(jlfwong): for this to be safely embeddable, there'll need to be some
// way of specify event focus.
window.addEventListener('keypress', this.onWindowKeyPress)
}
componentWillUnmount() {
window.removeEventListener('resize', this.onWindowResize)
window.removeEventListener('keypress', this.onWindowKeyPress)
}
render() {
const {flamechart, sortedFlamechart, sortOrder} = this.state
const flamechartToView = sortOrder == SortOrder.CHRONO ? flamechart : sortedFlamechart
return <div onDrop={this.onDrop} onDragOver={this.onDragOver} className={css(style.root)}>
{flamechartToView &&
<FlamechartView flamechart={flamechartToView} />}
</div>
function ref(instance: Application | null) {
app = instance
if (instance && retained) {
console.log('rehydrating: ', retained)
instance.rehydrate(retained)
}
}
const style = StyleSheet.create({
root: {
width: '100vw',
height: '100vh',
overflow: 'hidden'
}
})
render(<Application />, document.body)
render(<Application ref={ref}/>, document.body, document.body.lastElementChild || undefined)