mirror of
https://github.com/jlfwong/speedscope.git
synced 2024-11-22 22:14:25 +03:00
Speed up color genration
This commit is contained in:
parent
ebb1627f76
commit
8e3997d81f
@ -6,10 +6,11 @@ import {importFromBGFlameGraph} from './import/bg-flamegraph'
|
|||||||
import {importFromStackprof} from './import/stackprof'
|
import {importFromStackprof} from './import/stackprof'
|
||||||
import {importFromChrome} from './import/chrome'
|
import {importFromChrome} from './import/chrome'
|
||||||
|
|
||||||
import {Profile} from './profile'
|
import {Profile, Frame} from './profile'
|
||||||
import {Flamechart} from './flamechart'
|
import {Flamechart} from './flamechart'
|
||||||
import { FlamechartView } from './flamechart-view'
|
import { FlamechartView } from './flamechart-view'
|
||||||
import { FontFamily, FontSize, Colors } from './style'
|
import { FontFamily, FontSize, Colors } from './style'
|
||||||
|
import { FrameColorGenerator } from './color'
|
||||||
|
|
||||||
const enum SortOrder {
|
const enum SortOrder {
|
||||||
CHRONO,
|
CHRONO,
|
||||||
@ -91,13 +92,25 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
|||||||
const profile = fileName.endsWith('json') ? this.importJSON(JSON.parse(contents)) : importFromBGFlameGraph(contents)
|
const profile = fileName.endsWith('json') ? this.importJSON(JSON.parse(contents)) : importFromBGFlameGraph(contents)
|
||||||
profile.setName(fileName)
|
profile.setName(fileName)
|
||||||
document.title = `${fileName} - speedscope`
|
document.title = `${fileName} - speedscope`
|
||||||
const flamechart = new Flamechart(profile)
|
|
||||||
|
const frames: Frame[] = []
|
||||||
|
profile.forEachFrame(f => frames.push(f))
|
||||||
|
const colorGenerator = new FrameColorGenerator(frames)
|
||||||
|
|
||||||
|
const flamechart = new Flamechart({
|
||||||
|
getTotalWeight: profile.getTotalWeight.bind(profile),
|
||||||
|
forEachCall: profile.forEachCall.bind(profile),
|
||||||
|
formatValue: profile.formatValue.bind(profile),
|
||||||
|
getColorForFrame: colorGenerator.getColorForFrame.bind(colorGenerator)
|
||||||
|
})
|
||||||
|
|
||||||
const sortedFlamechart = new Flamechart({
|
const sortedFlamechart = new Flamechart({
|
||||||
getTotalWeight: profile.getTotalNonIdleWeight.bind(profile),
|
getTotalWeight: profile.getTotalNonIdleWeight.bind(profile),
|
||||||
forEachCall: profile.forEachCallGrouped.bind(profile),
|
forEachCall: profile.forEachCallGrouped.bind(profile),
|
||||||
formatValue: profile.formatValue.bind(profile),
|
formatValue: profile.formatValue.bind(profile),
|
||||||
forEachFrame: profile.forEachFrame.bind(profile),
|
getColorForFrame: colorGenerator.getColorForFrame.bind(colorGenerator)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.setState({ profile, flamechart, sortedFlamechart }, () => {
|
this.setState({ profile, flamechart, sortedFlamechart }, () => {
|
||||||
console.timeEnd('import')
|
console.timeEnd('import')
|
||||||
})
|
})
|
||||||
@ -188,7 +201,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {profile, flamechart, sortedFlamechart, sortOrder} = this.state
|
const {flamechart, sortedFlamechart, sortOrder} = this.state
|
||||||
const flamechartToView = sortOrder == SortOrder.CHRONO ? flamechart : sortedFlamechart
|
const flamechartToView = sortOrder == SortOrder.CHRONO ? flamechart : sortedFlamechart
|
||||||
|
|
||||||
return <div onDrop={this.onDrop} onDragOver={this.onDragOver} className={css(style.root)}>
|
return <div onDrop={this.onDrop} onDragOver={this.onDragOver} className={css(style.root)}>
|
||||||
|
73
color.ts
Normal file
73
color.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import {Frame} from './profile'
|
||||||
|
|
||||||
|
export class Color {
|
||||||
|
constructor(readonly r: number = 0, readonly g: number = 0, readonly b: number = 0, readonly a: number = 1) {}
|
||||||
|
|
||||||
|
static fromLumaChromaHue(L: number, C: number, H: number) {
|
||||||
|
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_luma/chroma/hue
|
||||||
|
|
||||||
|
const hPrime = H / 60
|
||||||
|
const X = C * (1 - Math.abs(hPrime % 2 - 1))
|
||||||
|
const [R1, G1, B1] = (
|
||||||
|
hPrime < 1 ? [C, X, 0] :
|
||||||
|
hPrime < 2 ? [X, C, 0] :
|
||||||
|
hPrime < 3 ? [0, C, X] :
|
||||||
|
hPrime < 4 ? [0, X, C] :
|
||||||
|
hPrime < 5 ? [X, 0, C] :
|
||||||
|
[C, 0, X]
|
||||||
|
)
|
||||||
|
|
||||||
|
const m = L - (0.30 * R1 + 0.59 * G1 + 0.11 * B1)
|
||||||
|
|
||||||
|
return new Color(R1 + m, G1 + m, B1 + m, 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fract(x: number) {
|
||||||
|
return x - Math.floor(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FrameColorGenerator {
|
||||||
|
private frameToColor = new Map<Frame, Color>()
|
||||||
|
|
||||||
|
constructor(frames: Frame[]) {
|
||||||
|
// Make a copy so we can mutate it
|
||||||
|
frames = [...frames]
|
||||||
|
|
||||||
|
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 cumulativeScores: number[] = []
|
||||||
|
let lastScore = 0
|
||||||
|
for (let i = 0; i < frames.length; i++) {
|
||||||
|
const score = lastScore + Math.abs(compare(frames[i], frames[(i + 1) % frames.length]))
|
||||||
|
cumulativeScores.push(score)
|
||||||
|
lastScore = score
|
||||||
|
}
|
||||||
|
|
||||||
|
// We now have a sorted list of frames s.t. frames with similar
|
||||||
|
// file paths and method names are clustered together.
|
||||||
|
//
|
||||||
|
// Now, to assign them colors, we map normalized cumulative
|
||||||
|
// score values onto the full range of hue values.
|
||||||
|
const totalScore = cumulativeScores[cumulativeScores.length - 1] || 1
|
||||||
|
for (let i = 0; i < cumulativeScores.length; i++) {
|
||||||
|
const ratio = cumulativeScores[i] / totalScore
|
||||||
|
const x = 2 * fract(100.0 * ratio) - 1
|
||||||
|
|
||||||
|
const L = 0.85 - 0.1 * x
|
||||||
|
const C = 0.20 + 0.1 * x
|
||||||
|
const H = 360 * ratio
|
||||||
|
this.frameToColor.set(frames[i], Color.fromLumaChromaHue(L, C, H))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getColorForFrame(f: Frame) { return this.frameToColor.get(f) || new Color() }
|
||||||
|
}
|
@ -308,8 +308,6 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
|||||||
const colors: vec3[] = []
|
const colors: vec3[] = []
|
||||||
|
|
||||||
const layers = flamechart.getLayers()
|
const layers = flamechart.getLayers()
|
||||||
const frameColors = flamechart.getFrameColors()
|
|
||||||
|
|
||||||
for (let i = 0; i < layers.length; i++) {
|
for (let i = 0; i < layers.length; i++) {
|
||||||
const layer = layers[i]
|
const layer = layers[i]
|
||||||
for (let flamechartFrame of layer) {
|
for (let flamechartFrame of layer) {
|
||||||
@ -318,7 +316,8 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
|
|||||||
new Vec2(flamechartFrame.end - flamechartFrame.start, 1)
|
new Vec2(flamechartFrame.end - flamechartFrame.start, 1)
|
||||||
)
|
)
|
||||||
configSpaceRects.push(configSpaceBounds)
|
configSpaceRects.push(configSpaceBounds)
|
||||||
colors.push(frameColors.get(flamechartFrame.node.frame) || [0, 0, 0])
|
const color = flamechart.getColorForFrame(flamechartFrame.node.frame)
|
||||||
|
colors.push([color.r, color.g, color.b])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +102,6 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
|||||||
const colors: vec3[] = []
|
const colors: vec3[] = []
|
||||||
|
|
||||||
const layers = flamechart.getLayers()
|
const layers = flamechart.getLayers()
|
||||||
const frameColors = flamechart.getFrameColors()
|
|
||||||
|
|
||||||
this.labels = []
|
this.labels = []
|
||||||
for (let i = 0; i < layers.length; i++) {
|
for (let i = 0; i < layers.length; i++) {
|
||||||
@ -113,7 +112,8 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
|
|||||||
new Vec2(flamechartFrame.end - flamechartFrame.start, 1)
|
new Vec2(flamechartFrame.end - flamechartFrame.start, 1)
|
||||||
)
|
)
|
||||||
configSpaceRects.push(configSpaceBounds)
|
configSpaceRects.push(configSpaceBounds)
|
||||||
colors.push(frameColors.get(flamechartFrame.node.frame) || [0, 0, 0])
|
const color = flamechart.getColorForFrame(flamechartFrame.node.frame)
|
||||||
|
colors.push([color.r, color.g, color.b])
|
||||||
|
|
||||||
this.labels.push({
|
this.labels.push({
|
||||||
configSpaceBounds,
|
configSpaceBounds,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {Frame, CallTreeNode} from './profile'
|
import {Frame, CallTreeNode} from './profile'
|
||||||
|
import { Color } from './color'
|
||||||
|
|
||||||
import { lastOf } from './utils'
|
import { lastOf } from './utils'
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ interface FlamechartDataSource {
|
|||||||
closeFrame: (value: number) => void
|
closeFrame: (value: number) => void
|
||||||
): void
|
): void
|
||||||
|
|
||||||
forEachFrame(fn: (frame: Frame) => void): void
|
getColorForFrame(f: Frame): Color
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Flamechart {
|
export class Flamechart {
|
||||||
@ -31,88 +32,12 @@ export class Flamechart {
|
|||||||
private totalWeight: number = 0
|
private totalWeight: number = 0
|
||||||
private minFrameWidth: number = 1
|
private minFrameWidth: number = 1
|
||||||
|
|
||||||
private frameColors = new Map<Frame, [number, number, number]>()
|
|
||||||
|
|
||||||
getTotalWeight() { return this.totalWeight }
|
getTotalWeight() { return this.totalWeight }
|
||||||
getLayers() { return this.layers }
|
getLayers() { return this.layers }
|
||||||
getFrameColors() { return this.frameColors }
|
getColorForFrame(f: Frame) { return this.source.getColorForFrame(f) }
|
||||||
getMinFrameWidth() { return this.minFrameWidth }
|
getMinFrameWidth() { return this.minFrameWidth }
|
||||||
formatValue(v: number) { return this.source.formatValue(v) }
|
formatValue(v: number) { return this.source.formatValue(v) }
|
||||||
|
|
||||||
// TODO(jlfwong): Move this a color generation file
|
|
||||||
private selectFrameColors(source: FlamechartDataSource) {
|
|
||||||
const frames: Frame[] = []
|
|
||||||
|
|
||||||
function parts(f: Frame) {
|
|
||||||
return (f.file || '').split('/').concat(f.name.split(/\W/))
|
|
||||||
}
|
|
||||||
|
|
||||||
function compare(a: Frame, b: Frame) {
|
|
||||||
const aParts = parts(a)
|
|
||||||
const bParts = parts(b)
|
|
||||||
|
|
||||||
const minLength = Math.min(aParts.length, bParts.length)
|
|
||||||
|
|
||||||
let prefixMatchLength = 0
|
|
||||||
for (let i = 0; i < minLength; i++) {
|
|
||||||
if (aParts[i] === bParts[i]) prefixMatchLength++
|
|
||||||
else break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Weight matches at the beginning of the string more heavily
|
|
||||||
const score = Math.pow(0.90, prefixMatchLength)
|
|
||||||
|
|
||||||
return aParts.join() > bParts.join() ? score : -score
|
|
||||||
}
|
|
||||||
|
|
||||||
this.source.forEachFrame(f => frames.push(f))
|
|
||||||
frames.sort(compare)
|
|
||||||
|
|
||||||
const cumulativeScores: number[] = []
|
|
||||||
let lastScore = 0
|
|
||||||
for (let i = 0; i < frames.length; i++) {
|
|
||||||
const score = lastScore + Math.abs(compare(frames[i], frames[(i + 1)%frames.length]))
|
|
||||||
cumulativeScores.push(score)
|
|
||||||
lastScore = score
|
|
||||||
}
|
|
||||||
|
|
||||||
// We now have a sorted list of frames s.t. frames with similar
|
|
||||||
// file paths and method names are clustered together.
|
|
||||||
//
|
|
||||||
// Now, to assign them colors, we map normalized cumulative
|
|
||||||
// score values onto the full range of hue values.
|
|
||||||
const hues: number[] = []
|
|
||||||
const totalScore = cumulativeScores[cumulativeScores.length - 1] || 1
|
|
||||||
for (let i = 0; i < cumulativeScores.length; i++) {
|
|
||||||
hues.push(360 * cumulativeScores[i] / totalScore)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < hues.length; i++) {
|
|
||||||
const H = hues[i]
|
|
||||||
|
|
||||||
const delta = 0.20 * Math.random() - 0.1
|
|
||||||
const C = 0.20 + delta
|
|
||||||
const Y = 0.85 - delta
|
|
||||||
|
|
||||||
// TODO(jlfwong): Move this into color routines in a different file
|
|
||||||
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_luma/chroma/hue
|
|
||||||
|
|
||||||
const hPrime = H / 60
|
|
||||||
const X = C * (1 - Math.abs(hPrime % 2 - 1))
|
|
||||||
const [R1, G1, B1] = (
|
|
||||||
hPrime < 1 ? [C, X, 0] :
|
|
||||||
hPrime < 2 ? [X, C, 0] :
|
|
||||||
hPrime < 3 ? [0, C, X] :
|
|
||||||
hPrime < 4 ? [0, X, C] :
|
|
||||||
hPrime < 5 ? [X, 0, C] :
|
|
||||||
[C, 0, X]
|
|
||||||
)
|
|
||||||
|
|
||||||
const m = Y - (0.30 * R1 + 0.59 * G1 + 0.11 * B1)
|
|
||||||
this.frameColors.set(frames[i], [R1 + m, G1 + m, B1 + m])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(private source: FlamechartDataSource) {
|
constructor(private source: FlamechartDataSource) {
|
||||||
const stack: FlamechartFrame[] = []
|
const stack: FlamechartFrame[] = []
|
||||||
const openFrame = (node: CallTreeNode, value: number) => {
|
const openFrame = (node: CallTreeNode, value: number) => {
|
||||||
@ -144,6 +69,5 @@ export class Flamechart {
|
|||||||
|
|
||||||
this.totalWeight = source.getTotalWeight()
|
this.totalWeight = source.getTotalWeight()
|
||||||
source.forEachCall(openFrame, closeFrame)
|
source.forEachCall(openFrame, closeFrame)
|
||||||
this.selectFrameColors(source)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -94,12 +94,18 @@ export function importFromChrome(events: TimelineEvent[]) {
|
|||||||
for (let node = nodeById.get(nodeId); node; node = node.parent) {
|
for (let node = nodeById.get(nodeId); node; node = node.parent) {
|
||||||
if (node.callFrame.functionName === '(root)') continue
|
if (node.callFrame.functionName === '(root)') continue
|
||||||
if (node.callFrame.functionName === '(idle)') continue
|
if (node.callFrame.functionName === '(idle)') continue
|
||||||
|
|
||||||
|
const name = node.callFrame.functionName || "(anonymous)"
|
||||||
|
const file = node.callFrame.url
|
||||||
|
const line = node.callFrame.lineNumber
|
||||||
|
const col = node.callFrame.columnNumber
|
||||||
|
|
||||||
stack.push({
|
stack.push({
|
||||||
key: node.id,
|
key: `${name}:${file}:${line}:${col}`,
|
||||||
name: node.callFrame.functionName || "(anonymous)",
|
name,
|
||||||
file: node.callFrame.url,
|
file,
|
||||||
line: node.callFrame.lineNumber,
|
line,
|
||||||
col: node.callFrame.columnNumber
|
col
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
stack.reverse()
|
stack.reverse()
|
||||||
|
Loading…
Reference in New Issue
Block a user