// 火焰图主类实现
class FlameGraph extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
let sr = this.shadowRoot;
// 使用全局template变量
const template = window.flameGraphTemplate;
if (template) {
sr.appendChild(template.content.cloneNode(true));
} else {
console.error('FlameGraph template not found. Please include flame-graph-component.js first.');
}
this.$canvas = sr.getElementById('flame-graph-canvas');
this.$context = this.$canvas.getContext('2d');
this.$context.save();
this.$frameHeight = 24;
this.$fgVGap = 0;
this.$fgVEndGap = 5;
this.$xGap = 0.2;
this.$xGapThreashold = 0.01;
this.$yGap = 0.5;
this.$textGap = 6;
this.$showTextWidthThreshold = 30;
this.$fontFamily = 'Menlo,NotoSans,"Lucida Grande","Lucida Sans Unicode",sans-serif';
this.$font = '400 12px ' + this.$fontFamily;
this.$font_600 = '600 12px ' + this.$fontFamily;
this.$rootFont = '600 14px ' + this.$fontFamily;
this.$moreText = '...';
this.$defaultColorScheme = {
colorForZero: ['#c5c8d3', '#000000'],
colors: [
['#761d96', '#ffffff'],
['#c12561', '#ffffff'],
['#fec91b', '#000000'],
['#3f7350', '#ffffff'],
['#408118', '#ffffff'],
['#3ea9da', '#000000'],
['#9fb036', '#ffffff'],
['#b671c1', '#ffffff'],
['#faa938', '#000000']
]
};
this.$maxY = 0;
this.$flameGraph = sr.getElementById('flame-graph');
this.$flameGraphInner = sr.getElementById('flame-graph-inner');
this.$flameGraphInnerWrapper = sr.getElementById('flame-graph-inner-wrapper');
this.$pinnedFrameMask = sr.getElementById('pinned-frame-mask');
this.$frameMask = sr.getElementById('frame-mask');
this.$flameGraphHelp = sr.getElementById('flame-graph-help');
this.$closeFlameGraphHelp = sr.getElementById('close-flame-graph-help');
this.$helpButton = sr.getElementById('help-button');
this.$helpButton.addEventListener('click', () => {
if (this.$flameGraphHelp.style.visibility !== 'visible') {
this.$flameGraphHelp.style.visibility = 'visible';
} else {
this.$flameGraphHelp.style.visibility = 'hidden';
}
});
this.$flameGraphInner.addEventListener('click', () => {
if (this.$flameGraphHelp.style.visibility === 'visible') {
this.$flameGraphHelp.style.visibility = 'hidden';
}
});
this.$closeFlameGraphHelp.addEventListener('click', () => {
this.$flameGraphHelp.style.visibility = 'hidden';
});
this.$commandMode = false;
this.$frameMask.addEventListener('keydown', (e) => {
if ((e.key === 'c' || e.key === 'C') && (e.metaKey || e.ctrlKey)) {
this.copy(false);
this.$commandMode = false;
} else {
if (this.$commandMode) {
if (e.key === 'f' || e.key === 'F') {
this.copy(false);
} else if (e.key === 's' || e.key === 'S') {
this.copy(true);
}
this.$commandMode = false;
} else if (e.key === 'f' || e.key === 'F') {
this.$commandMode = true;
}
}
});
this.$frameMaskText = sr.getElementById('frame-mask-text');
this.$framePostcard = sr.getElementById('frame-postcard');
this.$framePostcardShadow = sr.getElementById('frame-postcard-shadow');
this.$framePostcardConnectingLine = sr.getElementById('frame-postcard-connecting-line');
this.$framePostcardContent = sr.getElementById('frame-postcard-content');
this.$framePostcardContentMain = sr.getElementById('frame-postcard-content-main');
this.$framePostcardContentMainLine = sr.getElementById('frame-postcard-content-main-line');
this.$framePostcardContentMainTitle = sr.getElementById('frame-postcard-content-main-title');
this.$framePostcardContentFoot = sr.getElementById('frame-postcard-content-foot');
this.$frameMask.style.font = this.$font_600;
this.$root = null;
this.$currentFrame = null;
this.$pinned = false;
this.$pinnedFrame = null;
this.$pinnedFrameLeft = null;
this.$pinnedFrameRight = null;
this.$drawingChildrenOfPinnedFrame = false;
this.$frameMask.addEventListener('mousemove', (e) => {
this.handleFrameMaskMouseMoveEvent(e);
});
this.$frameMask.addEventListener('click', (e) => {
this.handleFrameMaskClickEvent(e);
});
this.$frameMask.addEventListener('dblclick', (e) => {
if (window.getSelection) {
window.getSelection().removeAllRanges();
}
this.handleFrameMaskClickEvent(e);
});
this.$scrollEventListener = () => {
this.handleScrollEvent();
};
this.$flameGraphInner.addEventListener('scroll', this.$scrollEventListener);
window.addEventListener('scroll', this.$scrollEventListener);
this.$downwardBunnton = sr.getElementById('downward-button');
this.$downwardBunnton.addEventListener('click', () => (this.downward = !this.downward));
this.$root = new Frame(this, null, 0, true);
this.$touchedFrame = null;
this.$canvas.addEventListener('mousemove', (e) => {
this.handleCanvasMouseMoveEvent(e);
});
this.$flameGraph.addEventListener('mouseleave', () => {
if (this.$touchedFrame) {
let tf = this.$touchedFrame;
this.$touchedFrame = null;
tf.leave();
}
});
this.$totalWeight = 0;
let o = this;
new ResizeObserver(function () {
o.render(true, false);
}).observe(this.$flameGraph);
this.$colorBarDiv = sr.getElementById('color-bar-div');
this.$colorArrow = sr.getElementById('color-arrow');
}
handleScrollEvent() {
this.$currentFrame = null;
this.$touchedFrame = null;
this.$frameMask.style.cursor = 'default';
this.$frameMask.style.visibility = 'hidden';
this.$framePostcard.style.visibility = 'hidden';
if (this.$stackTraceMaxDrawnDepth < this.$stackTraceMaxDepth) {
if (this.downward) {
if (this.$flameGraphInner.scrollTop > this.$currentScrollTopLimit) {
this.$flameGraphInner.scrollTop = this.$currentScrollTopLimit;
}
} else {
if (this.$flameGraphInner.scrollTop < this.$currentScrollTopLimit) {
this.$flameGraphInner.scrollTop = this.$currentScrollTopLimit;
}
}
}
}
handleFrameMaskMouseMoveEvent(e) {
this.$framePostcardShadow.style.left = this.$frameMask.offsetLeft + e.offsetX + 'px';
this.decideFramePostcardLayout();
e.stopPropagation();
}
handleFrameMaskClickEvent(e) {
if (window.getSelection().type === 'Range') {
e.stopPropagation();
return;
}
if (this.$currentFrame === this.$root) {
return;
}
if (!this.$pinned) {
this.$pinned = true;
this.$pinnedFrame = this.$currentFrame;
this.$pinnedFrame.setPinned();
this.$pinnedFrame.findSide();
} else {
if (this.$pinnedFrame === this.$currentFrame) {
this.$pinnedFrame.setUnpinned();
this.$pinnedFrame.clearFindSide();
this.$pinnedFrame = null;
this.$pinnedFrameMask.style.visibility = 'hidden';
this.$pinned = false;
} else {
this.$pinnedFrame.setUnpinned();
this.$pinnedFrame.clearFindSide();
this.$pinnedFrame = this.$currentFrame;
this.$pinnedFrame.setPinned();
this.$pinnedFrame.findSide();
}
}
this.render(false, false);
if (this.$pinned && this.$pinnedFrame) {
this.$pinnedFrameMask.style.left = this.$pinnedFrame.x + 'px';
this.$pinnedFrameMask.style.top = this.$pinnedFrame.y + 'px';
this.$pinnedFrameMask.style.width = this.$pinnedFrame.width + 'px';
this.$pinnedFrameMask.style.height = this.$pinnedFrame.height + 'px';
this.$pinnedFrameMask.style.visibility = 'visible';
}
let ne = new Event('mousemove');
ne.offsetX = this.$frameMask.offsetLeft + e.offsetX;
ne.offsetY = this.$frameMask.offsetTop + e.offsetY;
this.$framePostcard.style.visibility = 'hidden';
this.$frameMask.style.cursor = 'default';
this.$frameMask.style.visibility = 'hidden';
this.$canvas.dispatchEvent(ne);
e.stopPropagation();
}
render(reInitRenderContext, reGenFrames) {
if (this.$dataSource) {
this.$$isLineFormat = this.$dataSource.format.toLowerCase() === 'line';
this.$$diff = !!this.$dataSource.diff;
this.$$dataExtractor = this.dataExtractor;
this.$$stackTracesCounter = this.stackTracesCounter;
this.$$stackTraceExtractor = this.stackTraceExtractor;
this.$$framesCounter = this.framesCounter;
this.$$frameExtractor = this.frameExtractor;
this.$$framesIndexer = this.framesIndexer;
this.$$stackTraceFilter = this.stackTraceFilter;
this.$$frameEquator = this.frameEquator;
this.$$reverse = this.reverse;
this.$$rootFramesCounter = this.rootFramesCounter;
this.$$rootFrameExtractor = this.rootFrameExtractor;
this.$$childFramesCounter = this.childFramesCounter;
this.$$childFrameExtractor = this.childFrameExtractor;
this.$$frameStepper = this.frameStepper;
this.$$childFramesIndexer = this.childFramesIndexer;
this.$$weightsExtractor = this.weightsExtractor;
this.$$rootTextGenerator = this.rootTextGenerator;
this.$$textGenerator = this.textGenerator;
this.$$titleGenerator = this.titleGenerator;
this.$$detailsGenerator = this.detailsGenerator;
this.$$footTextGenerator = this.footTextGenerator;
this.$$rootColorSelector = this.rootColorSelector;
this.$$colorSelector = this.colorSelector;
this.$$footColorSelector = this.footColorSelector;
this.$$hashCodeGenerator = this.hashCodeGenerator;
this.$$showHelpButton = this.showHelpButton;
}
if (reInitRenderContext) {
this.clearState();
}
this.clearCanvas();
if (reGenFrames) {
this.genFrames();
}
if (reInitRenderContext) {
this.initRenderContext();
}
if (this.$totalWeight === 0) {
this.$helpButton.style.visibility = 'hidden';
this.$sibling = null;
return;
}
if (this.$$diff) {
this.$colorBarDiv.style.display = 'flex';
}
if (this.$$showHelpButton) {
this.$helpButton.style.visibility = 'visible';
this.$fgHGap = 15;
} else {
this.$helpButton.style.visibility = 'hidden';
this.$fgHGap = 0;
}
let rect = this.$canvas.getBoundingClientRect();
this.$stackTraceMaxDrawnDepth = 0;
this.$sibling = Array(this.$stackTraceMaxDepth + 1);
for (let i = 0; i < this.$stackTraceMaxDepth + 1; i++) {
this.$sibling[i] = [];
}
if (this.downward) {
this.$root.draw(
this.$fgHGap,
this.$fgVGap,
rect.width - this.$fgHGap * 2,
this.$frameHeight
);
} else {
this.$root.draw(
this.$fgHGap,
rect.height - this.$fgVGap - this.$frameHeight,
rect.width - this.$fgHGap * 2,
this.$frameHeight
);
}
if (this.$stackTraceMaxDrawnDepth < this.$stackTraceMaxDepth) {
let height = (this.$stackTraceMaxDrawnDepth + 1) * this.$frameHeight;
if (this.$stackTraceMaxDrawnDepth > 0) {
height += this.$stackTraceMaxDrawnDepth * this.$yGap;
}
height += this.$fgVGap + this.$fgVEndGap;
if (this.downward) {
this.$currentScrollTopLimit = Math.max(
height - this.$flameGraphInner.getBoundingClientRect().height,
this.$flameGraphInner.scrollTop
);
} else {
this.$currentScrollTopLimit = Math.min(
this.$flameGraphHeight - height,
this.$flameGraphInner.scrollTop
);
}
}
}
clearState() {
this.$pinnedFrame = null;
this.$currentFrame = null;
this.$touchedFrame = null;
this.$pinnedFrameMask.style.visibility = 'hidden';
this.$frameMask.style.cursor = 'default';
this.$frameMask.style.visibility = 'hidden';
this.$framePostcard.style.visibility = 'hidden';
this.$pinned = false;
}
clearCanvas() {
this.$context.clearRect(0, 0, this.$canvas.width, this.$canvas.height);
}
genFramesFromLineData() {
let dataSource = this.$dataSource;
for (let i = 0; i < this.$$stackTracesCounter(dataSource); i++) {
const stackTrace = this.$$stackTraceExtractor(dataSource, i);
if (!this.$$stackTraceFilter(dataSource, stackTrace)) {
continue;
}
const frameCount = this.$$framesCounter(dataSource, stackTrace);
if (frameCount === 0) {
return;
}
this.$stackTraceMaxDepth = Math.max(this.$stackTraceMaxDepth, frameCount);
let weights = this.$$weightsExtractor(dataSource, stackTrace);
let weight, weightOfBaseline1, weightOfBaseline2;
if (this.$$diff) {
[weightOfBaseline1, weightOfBaseline2] = weights;
weight = weightOfBaseline1 + weightOfBaseline2;
this.$totalWeightOfBaseline1 += weightOfBaseline1;
this.$totalWeightOfBaseline2 += weightOfBaseline2;
} else {
weight = weights;
}
this.$totalWeight += weight;
this.$root.addWeight(weight);
let current = this.$root;
let j = this.$$reverse ? frameCount - 1 : 0;
let end = this.$$reverse ? -1 : frameCount;
let step = this.$$reverse ? -1 : 1;
for (; j !== end; j += step) {
const frame = this.$$frameExtractor(dataSource, stackTrace, j);
const child = current.findOrAddChild(frame);
child.addWeight(weight);
if (this.$$diff) {
child.addWeightOfBaselines(weightOfBaseline1, weightOfBaseline2);
}
current = child;
}
current.addSelfWeight(weight);
if (this.$$diff) {
current.addSelfWeightOfBaselines(weightOfBaseline1, weightOfBaseline2);
}
}
}
genFramesFromTreeData() {
const queue = [];
let dataSource = this.$dataSource;
const process = (parent, frame) => {
let child = parent.addChild(frame);
let weights = this.$$weightsExtractor(dataSource, frame);
let selfWeight, weight, selfWeightOfBaseline1, weightOfBaseline1, selfWeightOfBaseline2, weightOfBaseline2;
if (this.$$diff) {
[selfWeightOfBaseline1, weightOfBaseline1, selfWeightOfBaseline2, weightOfBaseline2] = weights;
child.addSelfWeightOfBaselines(selfWeightOfBaseline1, selfWeightOfBaseline2);
child.addWeightOfBaselines(weightOfBaseline1, weightOfBaseline2);
selfWeight = selfWeightOfBaseline1 + selfWeightOfBaseline2;
weight = weightOfBaseline1 + weightOfBaseline2;
} else {
[selfWeight, weight] = weights;
}
child.addSelfWeight(selfWeight);
child.addWeight(weight);
this.$stackTraceMaxDepth = Math.max(this.$stackTraceMaxDepth, child.depth);
if (this.$$childFramesCounter(dataSource, frame) > 0) {
queue.push(child);
}
return child;
};
const rootFramesCount = this.$$rootFramesCounter(dataSource);
for (let i = 0; i < rootFramesCount; i++) {
const rootFrame = process(this.$root, this.$$rootFrameExtractor(dataSource, i));
this.$totalWeight += rootFrame.weight;
if (this.$$diff) {
this.$totalWeightOfBaseline1 += rootFrame.weightOfBaseline1;
this.$totalWeightOfBaseline2 += rootFrame.weightOfBaseline2;
}
}
this.$root.addWeight(this.$totalWeight);
while (queue.length > 0) {
const frame = queue.shift();
const childrenCount = this.$$childFramesCounter(dataSource, frame.raw);
for (let i = 0; i < childrenCount; i++) {
process(frame, this.$$childFrameExtractor(dataSource, frame.raw, i));
}
}
}
genFrames() {
this.$root.clear();
this.$stackTraceMaxDepth = 0;
this.$totalWeight = 0;
this.$totalWeightOfBaseline1 = 0;
this.$totalWeightOfBaseline2 = 0;
if (this.$dataSource) {
let format = this.$dataSource.format;
if (format === 'line') {
this.genFramesFromLineData();
} else if (format === 'tree') {
if (this.$$reverse) {
console.warn("Tree format data doesn't support reverse");
}
this.genFramesFromTreeData();
} else {
throw new Error(`Unsupported dataSource format ${format}`);
}
}
this.$root.sort();
this.$information = this.$$diff
? {
totalWeight: this.$totalWeight,
totalWeightOfBaseline1: this.$totalWeightOfBaseline1,
totalWeightOfBaseline2: this.$totalWeightOfBaseline2
}
: {
totalWeight: this.$totalWeight
};
this.$root.text = this.$$rootTextGenerator(this.$dataSource, this.$information);
}
initRenderContext() {
let w = this.width;
if (w) {
if (w.endsWith('%')) {
this.$flameGraph.style.width = w;
} else {
this.$flameGraph.style.width = w + 'px';
}
}
let h = this.height;
if (h) {
if (h.endsWith('%')) {
this.$flameGraph.style.height = h;
} else {
this.$flameGraph.style.height = h + 'px';
}
}
this.$context.restore();
this.$context.save();
let height = (this.$stackTraceMaxDepth + 1) * this.$frameHeight;
if (this.$stackTraceMaxDepth > 0) {
height += this.$stackTraceMaxDepth * this.$yGap;
}
height += this.$fgVGap + this.$fgVEndGap;
this.$flameGraphInnerWrapper.style.height = height + 'px';
this.$flameGraphHeight = height;
let innerHeight = this.$flameGraphInner.getBoundingClientRect().height;
this.$flameGraphInner.style.overflowY = null;
if (innerHeight < height) {
this.$flameGraphInner.style.overflowY = 'auto';
} else if (!this.downward) {
this.$flameGraphInnerWrapper.style.height = innerHeight + 'px';
}
if (!this.downward) {
this.$flameGraphInner.scrollTop = this.$flameGraphInner.scrollHeight;
this.$downwardBunnton.style.background = 'grey';
this.$downwardBunnton.style.flexDirection = 'row';
} else {
this.$flameGraphInner.scrollTop = 0;
this.$downwardBunnton.style.background = 'rgb(24, 144, 255)';
this.$downwardBunnton.style.flexDirection = 'row-reverse';
}
const dpr = window.devicePixelRatio || 1;
const rect = this.$canvas.getBoundingClientRect();
this.$canvas.width = rect.width * dpr;
this.$canvas.height = rect.height * dpr;
this.$context.scale(dpr, dpr);
if (this.$dataSource) {
this.$root.color = this.$$rootColorSelector(this.$dataSource, this.$information);
}
this.$colorBarDiv.style.display = 'none';
}
connectedCallback() {
this.addEventListener('re-render', () => {
this.render(true, true);
});
}
disconnectedCallback() {
window.removeEventListener('scroll', this.$scrollEventListener);
}
get width() {
return this.getAttribute('width');
}
set width(w) {
this.setAttribute('width', w);
}
get height() {
return this.getAttribute('height');
}
set height(h) {
this.setAttribute('height', h);
}
get downward() {
return this.hasAttribute('downward');
}
set downward(downward) {
this.toggleAttribute('downward', !!downward);
}
static get observedAttributes() {
return ['width', 'height', 'downward'];
}
attributeChangedCallback(name, oldVal, newVal) {
if (!this.$dataSource || oldVal === newVal) {
return;
}
this.render(true, false);
}
set dataSource(dataSource) {
if (!dataSource.format) {
throw new Error("Should specify the format of dataSource: 'line' or 'tree'");
}
if (typeof dataSource.format !== 'string') {
throw new Error('Illegal dataSource format type, must be string');
}
let format = dataSource.format.toLowerCase();
if ('line' !== format && 'tree' !== format) {
throw new Error("Illegal dataSource format, must be 'line' or 'tree'");
}
this.$dataSource = dataSource;
this.render(true, true);
}
get dataSource() {
return this.$dataSource;
}
set configuration(configuration) {
if (typeof configuration !== 'object') {
throw new Error('Configuration should be an object');
}
this.$configuration = configuration;
}
get configuration() {
return this.$configuration;
}
getConfigItemOrDefault(name, def) {
if (this.$configuration && this.$configuration[name]) {
return this.$configuration[name];
}
return def;
}
_(name, def) {
return this.getConfigItemOrDefault(name, def);
}
// 配置方法
get dataExtractor() {
return this._('dataExtractor', (dataSource) => dataSource.data);
}
get stackTracesCounter() {
return this._('stackTracesCounter', (dataSource) => this.$$dataExtractor(dataSource).length);
}
get stackTraceExtractor() {
return this._(
'stackTraceExtractor',
(dataSource, index) => this.$$dataExtractor(dataSource)[index]
);
}
get framesCounter() {
return this._('framesCounter', (dataSource, stackTrace) => {
return stackTrace[this.$$framesIndexer(dataSource, stackTrace)].length;
});
}
get frameExtractor() {
return this._('frameExtractor', (dataSource, stackTrace, index) => {
return stackTrace[this.$$framesIndexer(dataSource, stackTrace)][index];
});
}
get framesIndexer() {
return this._('framesIndexer', (dataSource, stackTrace) => 0);
}
get stackTraceFilter() {
return this._('stackTraceFilter', (dataSource, stackTrace) => true);
}
get frameEquator() {
return this._('frameEquator', (dataSource, left, right) => {
return left === right;
});
}
get reverse() {
return !!this._('reverse', false);
}
get rootFramesCounter() {
return this._(
'rootFramesCounter',
(dataSource) =>
this.$$dataExtractor(dataSource).length / this.$$frameStepper(dataSource, null)
);
}
get rootFrameExtractor() {
return this._('rootFrameExtractor', (dataSource, index) => {
let steps = this.$$frameStepper(dataSource, null);
const start = index * steps;
return this.$$dataExtractor(dataSource).slice(start, start + steps);
});
}
get childFramesCounter() {
return this._('childFramesCounter', (dataSource, frame) => {
return (
frame[this.$$childFramesIndexer(dataSource, frame)].length /
this.$$frameStepper(dataSource, frame)
);
});
}
get childFrameExtractor() {
return this._('childFrameExtractor', (dataSource, frame, index) => {
let steps = this.$$frameStepper(dataSource, frame);
const start = index * steps;
return frame[this.$$childFramesIndexer(dataSource, frame)].slice(start, start + steps);
});
}
get frameStepper() {
return this._(
'frameStepper',
this.$$diff ? (dataSource, frame) => 6 : (dataSource, frame) => 4
);
}
get childFramesIndexer() {
return this._(
'childFramesIndexer',
this.$$diff ? (dataSource, frame) => 5 : (dataSource, frame) => 3
);
}
get weightsExtractor() {
return this._(
'weightsExtractor',
this.$$isLineFormat
? this.$$diff
? (dataSource, input) => [input[1], input[2]]
: (dataSource, input) => input[1]
: this.$$diff
? (dataSource, input) => [input[1], input[2], input[3], input[4]]
: (dataSource, input) => [input[1], input[2]]
);
}
get rootTextGenerator() {
return this._('rootTextGenerator', (dataSource, information) => {
let totalWeight = information.totalWeight.toLocaleString();
if (this.$$diff) {
let totalWeightOfBaseline1 = information.totalWeightOfBaseline1.toLocaleString();
let totalWeightOfBaseline2 = information.totalWeightOfBaseline2.toLocaleString();
return `Total: ${totalWeight} (Baseline1: ${totalWeightOfBaseline1}, Baseline2: ${totalWeightOfBaseline2})`;
}
return `Total: ${totalWeight}`;
});
}
get textGenerator() {
return this._(
'textGenerator',
this.$$isLineFormat
? (dataSource, frame, information) => frame
: (dataSource, frame, information) => frame[0]
);
}
get titleGenerator() {
return this._('titleGenerator', (dataSource, frame, information) => information.text);
}
get detailsGenerator() {
return this._('detailsGenerator', (dataSource, frame, information) => null);
}
get footTextGenerator() {
return this._('footTextGenerator', (dataSource, frame, information) => {
let selfWeight = information.selfWeight;
let weight = information.weight;
let totalWeight = information.totalWeight;
let value = Math.round((weight / totalWeight) * 100 * 100) / 100;
return `${value.toLocaleString()}% - (${selfWeight.toLocaleString()}, ${weight.toLocaleString()}, ${totalWeight.toLocaleString()})`;
});
}
get rootColorSelector() {
return this._('rootColorSelector', (dataSource, information) => ['#537e8b', '#ffffff']);
}
get colorSelector() {
return this._('colorSelector', (dataSource, frame, information) => {
if (this.$$diff) {
return [this.diffColor(information.diffPercent), '#ffffff'];
}
let hashCode = this.$$hashCodeGenerator(dataSource, frame, information);
if (hashCode === 0) {
return this.$defaultColorScheme.colorForZero;
}
let colorIndex = Math.abs(hashCode) % this.$defaultColorScheme.colors.length;
if (!colorIndex && colorIndex !== 0) {
colorIndex = 0;
}
return this.$defaultColorScheme.colors[colorIndex];
});
}
get footColorSelector() {
return this._('footColorSelector', (dataSource, frame, information) => {
return ['#537e8bff', '#373b46e6', '#ffffff'];
});
}
get hashCodeGenerator() {
return this._('hashCodeGenerator', (dataSource, frame, information) => {
let text = information.text;
let hash = 0;
for (let i = 0; i < text.length; i++) {
hash = 31 * hash + (text.charCodeAt(i) & 0xff);
hash &= 0xffffffff;
}
return hash;
});
}
get showHelpButton() {
return !!this._('showHelpButton', false);
}
hexColorToFloatColor(hex) {
return [
parseInt(hex.substring(1, 3), 16) / 255,
parseInt(hex.substring(3, 5), 16) / 255,
parseInt(hex.substring(5, 7), 16) / 255
];
}
floatToHex(f) {
let v = Math.round(f);
let r = v.toString(16);
if (r.length === 1) {
return '0' + r;
}
return r;
}
floatColorToHexColor(float) {
return (
'#' +
this.floatToHex(float[0] * 255) +
this.floatToHex(float[1] * 255) +
this.floatToHex(float[2] * 255)
);
}
linearColor(from, to, pct) {
return [
from[0] + (to[0] - from[0]) * pct,
from[1] + (to[1] - from[1]) * pct,
from[2] + (to[2] - from[2]) * pct
];
}
diffColor(diffPercent) {
let from = '#808080';
let to = '#FF0000';
if (diffPercent < 0) {
to = '#008000';
if (diffPercent < -1) {
diffPercent = -1;
}
} else if (diffPercent > 1) {
diffPercent = 1;
}
from = this.hexColorToFloatColor(from);
to = this.hexColorToFloatColor(to);
return this.floatColorToHexColor(this.linearColor(from, to, Math.abs(diffPercent)));
}
findFrame(x, y) {
if (!this.$sibling) {
return null;
}
let index;
if (this.downward) {
if (y <= this.$root.y) {
return null;
}
index = Math.floor((y - this.$root.y) / (this.$frameHeight + this.$yGap));
} else {
if (y >= this.$root.y + this.$frameHeight) {
return null;
}
index = Math.floor(
(this.$root.y + this.$frameHeight - y) / (this.$frameHeight + this.$yGap)
);
}
if (index >= this.$sibling.length || this.$sibling[index].length === 0) {
return null;
}
let frame = this.$sibling[index][0];
if (y <= frame.y || y >= frame.y + frame.height) {
return null;
}
let start = 0;
let end = this.$sibling[index].length - 1;
while (start <= end) {
const mid = (start + end) >>> 1;
frame = this.$sibling[index][mid];
if (x <= frame.x) {
end = mid - 1;
} else if (x >= frame.x + frame.width) {
start = mid + 1;
} else {
return frame;
}
}
return null;
}
handleCanvasMouseMoveEvent(e) {
let lastTouchedFrame = this.$touchedFrame;
if (lastTouchedFrame) {
if (lastTouchedFrame.contain(e.offsetX, e.offsetY)) {
lastTouchedFrame.touch(e.offsetX, e.offsetY);
return;
}
}
this.$touchedFrame = this.findFrame(e.offsetX, e.offsetY);
if (lastTouchedFrame !== null && lastTouchedFrame !== this.$touchedFrame) {
lastTouchedFrame.leave();
}
if (this.$touchedFrame) {
this.$touchedFrame.touch(e.offsetX, e.offsetY);
}
e.stopPropagation();
}
decideFramePostcardLayout() {
let rect = this.$framePostcardShadow.getBoundingClientRect();
this.$framePostcard.style.left = rect.left + 'px';
this.$framePostcard.style.top = rect.top + 'px';
let height = this.$framePostcardContent.getBoundingClientRect().height + 26;
let showAtTop = rect.top - height < 0;
if (showAtTop) {
this.$framePostcardContent.style.top = '26px';
this.$framePostcardContent.style.bottom = null;
} else {
this.$framePostcardContent.style.top = null;
this.$framePostcardContent.style.bottom = '26px';
}
let showAtLeft =
rect.left + 392 > (window.innerWidth || document.documentElement.clientWidth);
if (showAtLeft) {
this.$framePostcardContentMain.style.marginLeft =
366 - this.$framePostcardContentMain.clientWidth + 'px';
this.$framePostcardContent.style.left = '-392px';
if (showAtTop) {
this.$framePostcardConnectingLine.style.transform =
'rotate(135deg) translate3d(0px, -.5px, 0)';
} else {
this.$framePostcardConnectingLine.style.transform =
'rotate(-135deg) translate3d(0px, -.5px, 0)';
}
} else {
this.$framePostcardContentMain.style.marginLeft = '0px';
this.$framePostcardContent.style.left = '26px';
if (showAtTop) {
this.$framePostcardConnectingLine.style.transform =
'rotate(45deg) translate3d(0px, -.5px, 0)';
} else {
this.$framePostcardConnectingLine.style.transform =
'rotate(-45deg) translate3d(0px, -.5px, 0)';
}
}
}
copy(stackTrace) {
let text = this.$touchedFrame.text;
if (stackTrace) {
let f = this.$touchedFrame.parent;
while (f && f !== this.$root) {
text += '\n' + f.text;
f = f.parent;
}
}
if (navigator.clipboard && window.isSecureContext && false) {
navigator.clipboard.writeText(text).then(() => {
this.dispatchEvent(
new CustomEvent('copied', {
detail: {
text: text
}
})
);
});
} else {
let textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
let success = document.execCommand('copy');
textArea.remove();
this.$frameMask.focus();
if (success) {
this.dispatchEvent(
new CustomEvent('copied', {
detail: {
text: text
}
})
);
}
}
}
}
// 导出FlameGraph类
if (typeof module !== 'undefined' && module.exports) {
module.exports = FlameGraph;
} else if (typeof window !== 'undefined') {
window.FlameGraph = FlameGraph;
}