From a51382c52578cf9cb0fd2febc6f83b778efdd9e8 Mon Sep 17 00:00:00 2001 From: Jamie Wong Date: Sat, 23 Dec 2017 13:37:56 -0500 Subject: [PATCH] Hot reloading WIP --- application.tsx | 101 ++++++++++++++++++++++++++++++++++++++++++++++ flamechart.tsx | 3 +- package.json | 2 +- reloadable.tsx | 51 +++++++++++++++++++++++ speedscope.tsx | 105 +++++++++--------------------------------------- 5 files changed, 173 insertions(+), 89 deletions(-) create mode 100644 application.tsx create mode 100644 reloadable.tsx diff --git a/application.tsx b/application.tsx new file mode 100644 index 0000000..d4e9db0 --- /dev/null +++ b/application.tsx @@ -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
+ {flamechartToView && + } +
+ } +} + +const style = StyleSheet.create({ + root: { + width: '100vw', + height: '100vh', + overflow: 'hidden' + } +}) + diff --git a/flamechart.tsx b/flamechart.tsx index f51c8d6..a28ab15 100644 --- a/flamechart.tsx +++ b/flamechart.tsx @@ -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 { +export class FlamechartView extends ReloadableComponent { container: HTMLDivElement | null = null constructor() { diff --git a/package.json b/package.json index b21ae54..efa2092 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/reloadable.tsx b/reloadable.tsx new file mode 100644 index 0000000..aea9e7f --- /dev/null +++ b/reloadable.tsx @@ -0,0 +1,51 @@ +import {Component} from 'preact' + +interface SerializedComponent { + state: S + internal: I | null + serializedSubcomponents: {[key: string]: any} +} + +export abstract class ReloadableComponent extends Component { + serialize(): SerializedComponent { + 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) { + 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) + } +} + + diff --git a/speedscope.tsx b/speedscope.tsx index ccb4f28..986375b 100644 --- a/speedscope.tsx +++ b/speedscope.tsx @@ -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
- {flamechartToView && - } -
+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(, document.body) \ No newline at end of file +render(, document.body, document.body.lastElementChild || undefined) \ No newline at end of file