diff --git a/application.tsx b/application.tsx
index 2275e41..96f783d 100644
--- a/application.tsx
+++ b/application.tsx
@@ -6,10 +6,11 @@ import {importFromBGFlameGraph} from './import/bg-flamegraph'
import {importFromStackprof} from './import/stackprof'
import {importFromChrome} from './import/chrome'
-import {Profile} from './profile'
+import {Profile, Frame} from './profile'
import {Flamechart} from './flamechart'
import { FlamechartView } from './flamechart-view'
import { FontFamily, FontSize, Colors } from './style'
+import { FrameColorGenerator } from './color'
const enum SortOrder {
CHRONO,
@@ -91,13 +92,25 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
const profile = fileName.endsWith('json') ? this.importJSON(JSON.parse(contents)) : importFromBGFlameGraph(contents)
profile.setName(fileName)
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({
getTotalWeight: profile.getTotalNonIdleWeight.bind(profile),
forEachCall: profile.forEachCallGrouped.bind(profile),
formatValue: profile.formatValue.bind(profile),
- forEachFrame: profile.forEachFrame.bind(profile),
+ getColorForFrame: colorGenerator.getColorForFrame.bind(colorGenerator)
})
+
this.setState({ profile, flamechart, sortedFlamechart }, () => {
console.timeEnd('import')
})
@@ -188,7 +201,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
}
render() {
- const {profile, flamechart, sortedFlamechart, sortOrder} = this.state
+ const {flamechart, sortedFlamechart, sortOrder} = this.state
const flamechartToView = sortOrder == SortOrder.CHRONO ? flamechart : sortedFlamechart
return
diff --git a/color.ts b/color.ts
new file mode 100644
index 0000000..4f1ebec
--- /dev/null
+++ b/color.ts
@@ -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()
+
+ 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() }
+}
\ No newline at end of file
diff --git a/flamechart-minimap-view.tsx b/flamechart-minimap-view.tsx
index 63c064d..463a276 100644
--- a/flamechart-minimap-view.tsx
+++ b/flamechart-minimap-view.tsx
@@ -308,8 +308,6 @@ export class FlamechartMinimapView extends Component void
): void
- forEachFrame(fn: (frame: Frame) => void): void
+ getColorForFrame(f: Frame): Color
}
export class Flamechart {
@@ -31,88 +32,12 @@ export class Flamechart {
private totalWeight: number = 0
private minFrameWidth: number = 1
- private frameColors = new Map()
-
getTotalWeight() { return this.totalWeight }
getLayers() { return this.layers }
- getFrameColors() { return this.frameColors }
+ getColorForFrame(f: Frame) { return this.source.getColorForFrame(f) }
getMinFrameWidth() { return this.minFrameWidth }
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) {
const stack: FlamechartFrame[] = []
const openFrame = (node: CallTreeNode, value: number) => {
@@ -144,6 +69,5 @@ export class Flamechart {
this.totalWeight = source.getTotalWeight()
source.forEachCall(openFrame, closeFrame)
- this.selectFrameColors(source)
}
}
\ No newline at end of file
diff --git a/import/chrome.ts b/import/chrome.ts
index 64599b6..2de9ef4 100644
--- a/import/chrome.ts
+++ b/import/chrome.ts
@@ -94,12 +94,18 @@ export function importFromChrome(events: TimelineEvent[]) {
for (let node = nodeById.get(nodeId); node; node = node.parent) {
if (node.callFrame.functionName === '(root)') 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({
- key: node.id,
- name: node.callFrame.functionName || "(anonymous)",
- file: node.callFrame.url,
- line: node.callFrame.lineNumber,
- col: node.callFrame.columnNumber
+ key: `${name}:${file}:${line}:${col}`,
+ name,
+ file,
+ line,
+ col
})
}
stack.reverse()