From 8e3997d81f795ed4dc37fd413a2a967e2ac32048 Mon Sep 17 00:00:00 2001 From: Jamie Wong Date: Sun, 7 Jan 2018 18:36:23 -0800 Subject: [PATCH] Speed up color genration --- application.tsx | 21 ++++++++-- color.ts | 73 +++++++++++++++++++++++++++++++++ flamechart-minimap-view.tsx | 5 +-- flamechart-view.tsx | 4 +- flamechart.ts | 82 ++----------------------------------- import/chrome.ts | 16 +++++--- 6 files changed, 108 insertions(+), 93 deletions(-) create mode 100644 color.ts 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()