Add profileURL and title hash parameters (#19)
On init, we check the hash fragment for these parameters and load the URL. We always show a loading state in that case rather than the landing screen. When determining the title, an explicitly-specified title takes precedence, otherwise we use the filename. I also added an error state, currently only used for my new code, but possibly there could be a more robust or widespread error handling approach in the future.
This commit is contained in:
@@ -11,6 +11,8 @@ Given raw profiling data, speedscope allows you to interactively explore the dat
|
||||
# Usage
|
||||
Visit https://jlfwong.github.io/speedscope/, then either browse to find a profile file or drag-and-drop one onto the page. The profiles are not uploaded anywhere -- the application is totally in-browser.
|
||||
|
||||
To load a specific profile by URL, you can append a hash fragment like `#profileURL=[URL-encoded profile URL]&title=[URL-encoded custom title]`. Note that the server hosting the profile must have CORS configured to allow AJAX requests from speedscope.
|
||||
|
||||
## Supported file formats:
|
||||
1. The folded stack format output by the FlameGraph scripts do: https://github.com/brendangregg/FlameGraph#2-fold-stacks. Example: https://github.com/jlfwong/speedscope/blob/master/sample/perf-vertx-stacks-01-collapsed-all.txt
|
||||
2. The timeline format output by Chrome developer tools: https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference#save.
|
||||
|
||||
+50
-4
@@ -12,6 +12,7 @@ import {Profile, Frame} from './profile'
|
||||
import {Flamechart} from './flamechart'
|
||||
import {FlamechartView} from './flamechart-view'
|
||||
import {FontFamily, FontSize, Colors} from './style'
|
||||
import {getHashParams, HashParams} from './hash-params'
|
||||
|
||||
declare function require(x: string): any
|
||||
const exampleProfileURL = require('./sample/perf-vertx-stacks-01-collapsed-all.txt')
|
||||
@@ -29,6 +30,7 @@ interface ApplicationState {
|
||||
sortedFlamechartRenderer: FlamechartRenderer | null
|
||||
sortOrder: SortOrder
|
||||
loading: boolean
|
||||
error: boolean
|
||||
}
|
||||
|
||||
interface ToolbarProps extends ApplicationState {
|
||||
@@ -198,10 +200,16 @@ export class GLCanvas extends ReloadableComponent<GLCanvasProps, void> {
|
||||
}
|
||||
|
||||
export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
hashParams: HashParams
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.hashParams = getHashParams()
|
||||
this.state = {
|
||||
loading: false,
|
||||
// Start out at a loading state if we know that we'll immediately be fetching a profile to
|
||||
// view.
|
||||
loading: this.hashParams.profileURL != null,
|
||||
error: false,
|
||||
profile: null,
|
||||
flamechart: null,
|
||||
flamechartRenderer: null,
|
||||
@@ -243,8 +251,9 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
|
||||
await profile.demangle()
|
||||
|
||||
profile.setName(fileName)
|
||||
document.title = `${fileName} - speedscope`
|
||||
const title = this.hashParams.title || fileName
|
||||
profile.setName(title)
|
||||
document.title = `${title} - speedscope`
|
||||
|
||||
const frames: Frame[] = []
|
||||
profile.forEachFrame(f => frames.push(f))
|
||||
@@ -345,6 +354,24 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('keypress', this.onWindowKeyPress)
|
||||
this.maybeLoadHashParamProfile()
|
||||
}
|
||||
|
||||
async maybeLoadHashParamProfile() {
|
||||
try {
|
||||
if (this.hashParams.profileURL) {
|
||||
const response = await fetch(this.hashParams.profileURL)
|
||||
const profile = await response.text()
|
||||
let filename = new URL(this.hashParams.profileURL).pathname
|
||||
if (filename.includes('/')) {
|
||||
filename = filename.slice(filename.lastIndexOf('/') + 1)
|
||||
}
|
||||
await this.loadFromString(filename, profile)
|
||||
}
|
||||
} catch (e) {
|
||||
this.setState({error: true})
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -430,6 +457,15 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
)
|
||||
}
|
||||
|
||||
renderError() {
|
||||
return (
|
||||
<div className={css(style.error)}>
|
||||
<div>😿 Something went wrong.</div>
|
||||
<div>Check the JS console for more details.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderLoadingBar() {
|
||||
return <div className={css(style.loading)} />
|
||||
}
|
||||
@@ -451,6 +487,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
sortedFlamechartRenderer,
|
||||
sortOrder,
|
||||
loading,
|
||||
error,
|
||||
} = this.state
|
||||
const flamechartToView = sortOrder == SortOrder.CHRONO ? flamechart : sortedFlamechart
|
||||
const flamechartRendererToUse =
|
||||
@@ -460,7 +497,9 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
||||
<div onDrop={this.onDrop} onDragOver={this.onDragOver} className={css(style.root)}>
|
||||
<GLCanvas setCanvasContext={this.setCanvasContext} />
|
||||
<Toolbar setSortOrder={this.setSortOrder} {...this.state} />
|
||||
{loading ? (
|
||||
{error ? (
|
||||
this.renderError()
|
||||
) : loading ? (
|
||||
this.renderLoadingBar()
|
||||
) : this.canvasContext && flamechartToView && flamechartRendererToUse ? (
|
||||
<FlamechartView
|
||||
@@ -485,6 +524,13 @@ const style = StyleSheet.create({
|
||||
zIndex: -1,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
error: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
loading: {
|
||||
height: 3,
|
||||
marginBottom: -3,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
export interface HashParams {
|
||||
profileURL?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export function getHashParams(): HashParams {
|
||||
try {
|
||||
const hashContents = window.location.hash
|
||||
if (!hashContents.startsWith('#')) {
|
||||
return {}
|
||||
}
|
||||
const components = hashContents.substr(1).split('&')
|
||||
const result: HashParams = {}
|
||||
for (const component of components) {
|
||||
const [key, value] = component.split('=')
|
||||
if (key === 'profileURL') {
|
||||
result.profileURL = decodeURIComponent(value)
|
||||
} else if (key === 'title') {
|
||||
result.title = decodeURIComponent(value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
} catch (e) {
|
||||
console.error(`Error when loading hash fragment.`)
|
||||
console.error(e)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user