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:
Alan Pierce
2018-04-14 20:38:04 -07:00
committed by Jamie Wong
parent 702d8cf5b4
commit 7c1118a425
3 changed files with 80 additions and 4 deletions
+2
View File
@@ -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
View File
@@ -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,
+28
View File
@@ -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 {}
}
}