@flamedeck/flamechart-mcp
Version:
MCP server for debugging and analyzing flamegraphs using Model Context Protocol
1,560 lines (1,541 loc) • 187 kB
JavaScript
#!/usr/bin/env node
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/index.ts
import { FastMCP } from "fastmcp";
// src/tools/top-functions.ts
import { z } from "zod";
import { UserError } from "fastmcp";
// src/utils/profile-loader.ts
import { readFile } from "node:fs/promises";
import { existsSync as existsSync2 } from "node:fs";
// ../speedscope-core/src/lib-utils.ts
function lastOf(ts) {
return ts[ts.length - 1] || null;
}
__name(lastOf, "lastOf");
function sortBy(ts, key) {
function comparator(a, b) {
const keyA = key(a);
const keyB = key(b);
return keyA < keyB ? -1 : keyA > keyB ? 1 : 0;
}
__name(comparator, "comparator");
ts.sort(comparator);
}
__name(sortBy, "sortBy");
function getOrInsert(map, k, fallback) {
if (!map.has(k)) map.set(k, fallback(k));
return map.get(k);
}
__name(getOrInsert, "getOrInsert");
function getOrElse(map, k, fallback) {
if (!map.has(k)) return fallback(k);
return map.get(k);
}
__name(getOrElse, "getOrElse");
var _KeyedSet = class _KeyedSet {
constructor() {
__publicField(this, "map", /* @__PURE__ */ new Map());
}
getOrInsert(t) {
const key = t.key;
const existing = this.map.get(key);
if (existing) return existing;
this.map.set(key, t);
return t;
}
forEach(fn) {
this.map.forEach(fn);
}
[Symbol.iterator]() {
return this.map.values();
}
};
__name(_KeyedSet, "KeyedSet");
var KeyedSet = _KeyedSet;
function* itMap(it, f) {
for (const t of it) {
yield f(t);
}
}
__name(itMap, "itMap");
function itForEach(it, f) {
for (const t of it) {
f(t);
}
}
__name(itForEach, "itForEach");
function zeroPad(s, width) {
return new Array(Math.max(width - s.length, 0) + 1).join("0") + s;
}
__name(zeroPad, "zeroPad");
function formatPercent(percent) {
let formattedPercent = `${percent.toFixed(0)}%`;
if (percent === 100) formattedPercent = "100%";
else if (percent > 99) formattedPercent = ">99%";
else if (percent < 0.01) formattedPercent = "<0.01%";
else if (percent < 1) formattedPercent = `${percent.toFixed(2)}%`;
else if (percent < 10) formattedPercent = `${percent.toFixed(1)}%`;
return formattedPercent;
}
__name(formatPercent, "formatPercent");
function fract(x) {
return x - Math.floor(x);
}
__name(fract, "fract");
function triangle(x) {
return 2 * Math.abs(fract(x) - 0.5) - 1;
}
__name(triangle, "triangle");
function lazyStatic(cb) {
let last = null;
return () => {
if (last == null) {
last = {
result: cb()
};
}
return last.result;
};
}
__name(lazyStatic, "lazyStatic");
var base64lookupTable = lazyStatic(() => {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const ret = /* @__PURE__ */ new Map();
for (let i = 0; i < alphabet.length; i++) {
ret.set(alphabet.charAt(i), i);
}
ret.set("=", -1);
return ret;
});
// ../speedscope-core/src/value-formatters.ts
var _RawValueFormatter = class _RawValueFormatter {
constructor() {
__publicField(this, "unit", "none");
}
format(v) {
return v.toLocaleString();
}
};
__name(_RawValueFormatter, "RawValueFormatter");
var RawValueFormatter = _RawValueFormatter;
var _TimeFormatter = class _TimeFormatter {
constructor(unit) {
__publicField(this, "unit");
__publicField(this, "multiplier");
this.unit = unit;
if (unit === "nanoseconds") this.multiplier = 1e-9;
else if (unit === "microseconds") this.multiplier = 1e-6;
else if (unit === "milliseconds") this.multiplier = 1e-3;
else this.multiplier = 1;
}
formatUnsigned(v) {
const s = v * this.multiplier;
if (s / 60 >= 1) {
const minutes = Math.floor(s / 60);
const seconds = Math.floor(s - minutes * 60).toString();
return `${minutes}:${zeroPad(seconds, 2)}`;
}
if (s / 1 >= 1) return `${s.toFixed(2)}s`;
if (s / 1e-3 >= 1) return `${(s / 1e-3).toFixed(2)}ms`;
if (s / 1e-6 >= 1) return `${(s / 1e-6).toFixed(2)}\xB5s`;
else return `${(s / 1e-9).toFixed(2)}ns`;
}
format(v) {
return `${v < 0 ? "-" : ""}${this.formatUnsigned(Math.abs(v))}`;
}
};
__name(_TimeFormatter, "TimeFormatter");
var TimeFormatter = _TimeFormatter;
var _ByteFormatter = class _ByteFormatter {
constructor() {
__publicField(this, "unit", "bytes");
}
format(v) {
if (v < 1024) return `${v.toFixed(0)} B`;
v /= 1024;
if (v < 1024) return `${v.toFixed(2)} KB`;
v /= 1024;
if (v < 1024) return `${v.toFixed(2)} MB`;
v /= 1024;
return `${v.toFixed(2)} GB`;
}
};
__name(_ByteFormatter, "ByteFormatter");
var ByteFormatter = _ByteFormatter;
// ../speedscope-core/src/profile.ts
var _HasWeights = class _HasWeights {
constructor() {
__publicField(this, "selfWeight", 0);
__publicField(this, "totalWeight", 0);
}
getSelfWeight() {
return this.selfWeight;
}
getTotalWeight() {
return this.totalWeight;
}
addToTotalWeight(delta) {
this.totalWeight += delta;
}
addToSelfWeight(delta) {
this.selfWeight += delta;
}
overwriteWeightWith(other) {
this.selfWeight = other.selfWeight;
this.totalWeight = other.totalWeight;
}
};
__name(_HasWeights, "HasWeights");
var HasWeights = _HasWeights;
var _Frame = class _Frame extends HasWeights {
constructor(info) {
super();
__publicField(this, "key");
// Name of the frame. May be a method name, e.g.
// "ActiveRecord##to_hash"
__publicField(this, "name");
// File path of the code corresponding to this
// call stack frame.
__publicField(this, "file");
// Line in the given file where this frame occurs
__publicField(this, "line");
// Column in the file
__publicField(this, "col");
this.key = info.key;
this.name = info.name;
this.file = info.file;
this.line = info.line;
this.col = info.col;
}
static getOrInsert(set, info) {
return set.getOrInsert(new _Frame(info));
}
};
__name(_Frame, "Frame");
__publicField(_Frame, "root", new _Frame({
key: "(speedscope root)",
name: "(speedscope root)"
}));
var Frame = _Frame;
var _CallTreeNode = class _CallTreeNode extends HasWeights {
constructor(frame, parent) {
super();
__publicField(this, "frame");
__publicField(this, "parent");
__publicField(this, "children");
// If a node is "frozen", it means it should no longer be mutated.
__publicField(this, "frozen");
this.frame = frame, this.parent = parent, this.children = [], this.frozen = false;
}
isRoot() {
return this.frame === Frame.root;
}
isFrozen() {
return this.frozen;
}
freeze() {
this.frozen = true;
}
};
__name(_CallTreeNode, "CallTreeNode");
var CallTreeNode = _CallTreeNode;
var _Profile = class _Profile {
constructor(totalWeight = 0) {
__publicField(this, "name", "");
__publicField(this, "totalWeight");
__publicField(this, "frames", new KeyedSet());
// Profiles store two call-trees.
//
// The "append order" call tree is the one in which nodes are ordered in
// whatever order they were appended to their parent.
//
// The "grouped" call tree is one in which each node has at most one child per
// frame. Nodes are ordered in decreasing order of weight
__publicField(this, "appendOrderCalltreeRoot", new CallTreeNode(Frame.root, null));
__publicField(this, "groupedCalltreeRoot", new CallTreeNode(Frame.root, null));
// List of references to CallTreeNodes at the top of the
// stack at the time of the sample.
__publicField(this, "samples", []);
__publicField(this, "weights", []);
__publicField(this, "valueFormatter", new RawValueFormatter());
__publicField(this, "totalNonIdleWeight", null);
this.totalWeight = totalWeight;
}
getAppendOrderCalltreeRoot() {
return this.appendOrderCalltreeRoot;
}
getGroupedCalltreeRoot() {
return this.groupedCalltreeRoot;
}
shallowClone() {
const profile = new _Profile(this.totalWeight);
Object.assign(profile, this);
return profile;
}
formatValue(v) {
return this.valueFormatter.format(v);
}
setValueFormatter(f) {
this.valueFormatter = f;
}
getWeightUnit() {
return this.valueFormatter.unit;
}
getName() {
return this.name;
}
setName(name) {
this.name = name;
}
getTotalWeight() {
return this.totalWeight;
}
getTotalNonIdleWeight() {
if (this.totalNonIdleWeight === null) {
this.totalNonIdleWeight = this.groupedCalltreeRoot.children.reduce((n, c) => n + c.getTotalWeight(), 0);
}
return this.totalNonIdleWeight;
}
// This is private because it should only be called in the ProfileBuilder
// classes. Once a Profile instance has been constructed, it should be treated
// as immutable.
sortGroupedCallTree() {
function visit(node) {
node.children.sort((a, b) => -(a.getTotalWeight() - b.getTotalWeight()));
node.children.forEach(visit);
}
__name(visit, "visit");
visit(this.groupedCalltreeRoot);
}
forEachCallGrouped(openFrame, closeFrame) {
function visit(node, start) {
if (node.frame !== Frame.root) {
openFrame(node, start);
}
let childTime = 0;
node.children.forEach(function(child) {
visit(child, start + childTime);
childTime += child.getTotalWeight();
});
if (node.frame !== Frame.root) {
closeFrame(node, start + node.getTotalWeight());
}
}
__name(visit, "visit");
visit(this.groupedCalltreeRoot, 0);
}
forEachCall(openFrame, closeFrame) {
let prevStack = [];
let value = 0;
let sampleIndex = 0;
for (const stackTop of this.samples) {
let lca = null;
for (lca = stackTop; lca && lca.frame != Frame.root && prevStack.indexOf(lca) === -1; lca = lca.parent) {
}
while (prevStack.length > 0 && lastOf(prevStack) != lca) {
const node = prevStack.pop();
closeFrame(node, value);
}
const toOpen = [];
for (let node = stackTop; node && node.frame != Frame.root && node != lca; node = node.parent) {
toOpen.push(node);
}
toOpen.reverse();
for (const node of toOpen) {
openFrame(node, value);
}
prevStack = prevStack.concat(toOpen);
value += this.weights[sampleIndex++];
}
for (let i = prevStack.length - 1; i >= 0; i--) {
closeFrame(prevStack[i], value);
}
}
forEachFrame(fn) {
this.frames.forEach(fn);
}
getProfileWithRecursionFlattened() {
const builder = new CallTreeProfileBuilder();
const stack = [];
const framesInStack = /* @__PURE__ */ new Set();
function openFrame(node, value) {
if (framesInStack.has(node.frame)) {
stack.push(null);
} else {
framesInStack.add(node.frame);
stack.push(node);
builder.enterFrame(node.frame, value);
}
}
__name(openFrame, "openFrame");
function closeFrame(node, value) {
const stackTop = stack.pop();
if (stackTop) {
framesInStack.delete(stackTop.frame);
builder.leaveFrame(stackTop.frame, value);
}
}
__name(closeFrame, "closeFrame");
this.forEachCall(openFrame, closeFrame);
const flattenedProfile = builder.build();
flattenedProfile.name = this.name;
flattenedProfile.valueFormatter = this.valueFormatter;
this.forEachFrame((f) => {
flattenedProfile.frames.getOrInsert(f).overwriteWeightWith(f);
});
return flattenedProfile;
}
getInvertedProfileForCallersOf(focalFrameInfo) {
const focalFrame = Frame.getOrInsert(this.frames, focalFrameInfo);
const builder = new StackListProfileBuilder();
const nodes = [];
function visit(node) {
if (node.frame === focalFrame) {
nodes.push(node);
} else {
for (const child of node.children) {
visit(child);
}
}
}
__name(visit, "visit");
visit(this.appendOrderCalltreeRoot);
for (const node of nodes) {
const stack = [];
for (let n = node; n != null && n.frame !== Frame.root; n = n.parent) {
stack.push(n.frame);
}
builder.appendSampleWithWeight(stack, node.getTotalWeight());
}
const ret = builder.build();
ret.name = this.name;
ret.valueFormatter = this.valueFormatter;
return ret;
}
getProfileForCalleesOf(focalFrameInfo) {
const focalFrame = Frame.getOrInsert(this.frames, focalFrameInfo);
const builder = new StackListProfileBuilder();
function recordSubtree(focalFrameNode) {
const stack = [];
function visit(node) {
stack.push(node.frame);
builder.appendSampleWithWeight(stack, node.getSelfWeight());
for (const child of node.children) {
visit(child);
}
stack.pop();
}
__name(visit, "visit");
visit(focalFrameNode);
}
__name(recordSubtree, "recordSubtree");
function findCalls(node) {
if (node.frame === focalFrame) {
recordSubtree(node);
} else {
for (const child of node.children) {
findCalls(child);
}
}
}
__name(findCalls, "findCalls");
findCalls(this.appendOrderCalltreeRoot);
const ret = builder.build();
ret.name = this.name;
ret.valueFormatter = this.valueFormatter;
return ret;
}
// Demangle symbols for readability
async demangle() {
return Promise.resolve();
}
remapSymbols(callback) {
for (const frame of this.frames) {
const remapped = callback(frame);
if (remapped == null) {
continue;
}
const { name, file, line, col } = remapped;
if (name != null) {
frame.name = name;
}
if (file != null) {
frame.file = file;
}
if (line != null) {
frame.line = line;
}
if (col != null) {
frame.col = col;
}
}
}
};
__name(_Profile, "Profile");
var Profile = _Profile;
var _StackListProfileBuilder = class _StackListProfileBuilder extends Profile {
constructor() {
super(...arguments);
__publicField(this, "pendingSample", null);
}
_appendSample(stack, weight, useAppendOrder) {
if (isNaN(weight)) throw new Error("invalid weight");
let node = useAppendOrder ? this.appendOrderCalltreeRoot : this.groupedCalltreeRoot;
const framesInStack = /* @__PURE__ */ new Set();
for (const frame of stack) {
const last = useAppendOrder ? lastOf(node.children) : node.children.find((c) => c.frame === frame);
if (last && !last.isFrozen() && last.frame == frame) {
node = last;
} else {
const parent = node;
node = new CallTreeNode(frame, node);
parent.children.push(node);
}
node.addToTotalWeight(weight);
framesInStack.add(node.frame);
}
node.addToSelfWeight(weight);
if (useAppendOrder) {
for (const child of node.children) {
child.freeze();
}
}
if (useAppendOrder) {
node.frame.addToSelfWeight(weight);
for (const frame of framesInStack) {
frame.addToTotalWeight(weight);
}
if (node === lastOf(this.samples)) {
this.weights[this.weights.length - 1] += weight;
} else {
this.samples.push(node);
this.weights.push(weight);
}
}
}
appendSampleWithWeight(stack, weight) {
if (weight === 0) {
return;
}
if (weight < 0) {
throw new Error("Samples must have positive weights");
}
const frames = stack.map((fr) => Frame.getOrInsert(this.frames, fr));
this._appendSample(frames, weight, true);
this._appendSample(frames, weight, false);
}
appendSampleWithTimestamp(stack, timestamp) {
if (this.pendingSample) {
if (timestamp < this.pendingSample.centralTimestamp) {
throw new Error("Timestamps received out of order");
}
const endTimestamp = (timestamp + this.pendingSample.centralTimestamp) / 2;
this.appendSampleWithWeight(this.pendingSample.stack, endTimestamp - this.pendingSample.startTimestamp);
this.pendingSample = {
stack,
startTimestamp: endTimestamp,
centralTimestamp: timestamp
};
} else {
this.pendingSample = {
stack,
startTimestamp: timestamp,
centralTimestamp: timestamp
};
}
}
build() {
if (this.pendingSample) {
if (this.samples.length > 0) {
this.appendSampleWithWeight(this.pendingSample.stack, this.pendingSample.centralTimestamp - this.pendingSample.startTimestamp);
} else {
this.appendSampleWithWeight(this.pendingSample.stack, 1);
this.setValueFormatter(new RawValueFormatter());
}
}
this.totalWeight = Math.max(this.totalWeight, this.weights.reduce((a, b) => a + b, 0));
this.sortGroupedCallTree();
return this;
}
};
__name(_StackListProfileBuilder, "StackListProfileBuilder");
var StackListProfileBuilder = _StackListProfileBuilder;
var _CallTreeProfileBuilder = class _CallTreeProfileBuilder extends Profile {
constructor() {
super(...arguments);
__publicField(this, "appendOrderStack", [
this.appendOrderCalltreeRoot
]);
__publicField(this, "groupedOrderStack", [
this.groupedCalltreeRoot
]);
__publicField(this, "framesInStack", /* @__PURE__ */ new Map());
__publicField(this, "stack", []);
__publicField(this, "lastValue", 0);
}
addWeightsToFrames(value) {
const delta = value - this.lastValue;
for (const frame of this.framesInStack.keys()) {
frame.addToTotalWeight(delta);
}
const stackTop = lastOf(this.stack);
if (stackTop) {
stackTop.addToSelfWeight(delta);
}
}
addWeightsToNodes(value, stack) {
const delta = value - this.lastValue;
for (const node of stack) {
node.addToTotalWeight(delta);
}
const stackTop = lastOf(stack);
if (stackTop) {
stackTop.addToSelfWeight(delta);
}
}
_enterFrame(frame, value, useAppendOrder) {
const stack = useAppendOrder ? this.appendOrderStack : this.groupedOrderStack;
this.addWeightsToNodes(value, stack);
const prevTop = lastOf(stack);
if (prevTop) {
if (useAppendOrder) {
const delta = value - this.lastValue;
if (delta > 0) {
this.samples.push(prevTop);
this.weights.push(value - this.lastValue);
} else if (delta < 0) {
throw new Error(`Samples must be provided in increasing order of cumulative value. Last sample was ${this.lastValue}, this sample was ${value}`);
}
}
const last = useAppendOrder ? lastOf(prevTop.children) : prevTop.children.find((c) => c.frame === frame);
let node;
if (last && !last.isFrozen() && last.frame == frame) {
node = last;
} else {
node = new CallTreeNode(frame, prevTop);
prevTop.children.push(node);
}
stack.push(node);
}
}
enterFrame(frameInfo, value) {
const frame = Frame.getOrInsert(this.frames, frameInfo);
this.addWeightsToFrames(value);
this._enterFrame(frame, value, true);
this._enterFrame(frame, value, false);
this.stack.push(frame);
const frameCount = this.framesInStack.get(frame) || 0;
this.framesInStack.set(frame, frameCount + 1);
this.lastValue = value;
this.totalWeight = Math.max(this.totalWeight, this.lastValue);
}
_leaveFrame(frame, value, useAppendOrder) {
const stack = useAppendOrder ? this.appendOrderStack : this.groupedOrderStack;
this.addWeightsToNodes(value, stack);
if (useAppendOrder) {
const leavingStackTop = this.appendOrderStack.pop();
if (leavingStackTop == null) {
throw new Error(`Trying to leave ${frame.key} when stack is empty`);
}
if (this.lastValue == null) {
throw new Error(`Trying to leave a ${frame.key} before any have been entered`);
}
leavingStackTop.freeze();
if (leavingStackTop.frame.key !== frame.key) {
throw new Error(`Tried to leave frame "${frame.name}" while frame "${leavingStackTop.frame.name}" was at the top at ${value}`);
}
const delta = value - this.lastValue;
if (delta > 0) {
this.samples.push(leavingStackTop);
this.weights.push(value - this.lastValue);
} else if (delta < 0) {
throw new Error(`Samples must be provided in increasing order of cumulative value. Last sample was ${this.lastValue}, this sample was ${value}`);
}
} else {
this.groupedOrderStack.pop();
}
}
leaveFrame(frameInfo, value) {
const frame = Frame.getOrInsert(this.frames, frameInfo);
this.addWeightsToFrames(value);
this._leaveFrame(frame, value, true);
this._leaveFrame(frame, value, false);
this.stack.pop();
const frameCount = this.framesInStack.get(frame);
if (frameCount == null) return;
if (frameCount === 1) {
this.framesInStack.delete(frame);
} else {
this.framesInStack.set(frame, frameCount - 1);
}
this.lastValue = value;
this.totalWeight = Math.max(this.totalWeight, this.lastValue);
}
build() {
if (this.appendOrderStack.length > 1 || this.groupedOrderStack.length > 1) {
throw new Error("Tried to complete profile construction with a non-empty stack");
}
this.sortGroupedCallTree();
return this;
}
};
__name(_CallTreeProfileBuilder, "CallTreeProfileBuilder");
var CallTreeProfileBuilder = _CallTreeProfileBuilder;
// ../speedscope-import/src/speedscope-import/v8cpuFormatter.ts
function treeToArray(root) {
const nodes = [];
function visit(node) {
nodes.push({
id: node.id,
callFrame: {
columnNumber: 0,
functionName: node.functionName,
lineNumber: node.lineNumber,
scriptId: node.scriptId,
url: node.url
},
hitCount: node.hitCount,
children: node.children.map((child) => child.id)
});
node.children.forEach(visit);
}
__name(visit, "visit");
visit(root);
return nodes;
}
__name(treeToArray, "treeToArray");
function timestampsToDeltas(timestamps, startTime) {
return timestamps.map((timestamp, index) => {
const lastTimestamp = index === 0 ? startTime * 1e6 : timestamps[index - 1];
return timestamp - lastTimestamp;
});
}
__name(timestampsToDeltas, "timestampsToDeltas");
function chromeTreeToNodes(content) {
return {
samples: content.samples,
startTime: content.startTime * 1e6,
endTime: content.endTime * 1e6,
nodes: treeToArray(content.head),
timeDeltas: timestampsToDeltas(content.timestamps, content.startTime)
};
}
__name(chromeTreeToNodes, "chromeTreeToNodes");
// ../speedscope-import/src/speedscope-import/chrome.ts
function isChromeTimeline(rawProfile) {
if (!Array.isArray(rawProfile)) return false;
if (rawProfile.length < 1) return false;
const first = rawProfile[0];
if (!("pid" in first && "tid" in first && "ph" in first && "cat" in first)) return false;
if (!rawProfile.find((e) => e.name === "CpuProfile" || e.name === "Profile" || e.name === "ProfileChunk")) return false;
return true;
}
__name(isChromeTimeline, "isChromeTimeline");
function isChromeTimelineObject(rawProfile) {
if (!("traceEvents" in rawProfile)) return false;
return isChromeTimeline(rawProfile.traceEvents);
}
__name(isChromeTimelineObject, "isChromeTimelineObject");
function importFromChromeTimeline(events, fileName) {
const cpuProfileByID = /* @__PURE__ */ new Map();
const pidTidById = /* @__PURE__ */ new Map();
const threadNameByPidTid = /* @__PURE__ */ new Map();
sortBy(events, (e) => e.ts);
for (const event of events) {
if (event.name === "CpuProfile") {
const pidTid = `${event.pid}:${event.tid}`;
const id = event.id || pidTid;
cpuProfileByID.set(id, event.args.data.cpuProfile);
pidTidById.set(id, pidTid);
}
if (event.name === "Profile") {
const pidTid = `${event.pid}:${event.tid}`;
cpuProfileByID.set(event.id || pidTid, {
startTime: 0,
endTime: 0,
nodes: [],
samples: [],
timeDeltas: [],
...event.args.data
});
if (event.id) {
pidTidById.set(event.id, `${event.pid}:${event.tid}`);
}
}
if (event.name === "thread_name") {
threadNameByPidTid.set(`${event.pid}:${event.tid}`, event.args.name);
}
if (event.name === "ProfileChunk") {
const pidTid = `${event.pid}:${event.tid}`;
const cpuProfile = cpuProfileByID.get(event.id || pidTid);
if (cpuProfile) {
const chunk = event.args.data;
if (chunk.cpuProfile) {
if (chunk.cpuProfile.nodes) {
cpuProfile.nodes = cpuProfile.nodes.concat(chunk.cpuProfile.nodes);
}
if (chunk.cpuProfile.samples) {
cpuProfile.samples = cpuProfile.samples.concat(chunk.cpuProfile.samples);
}
}
if (chunk.timeDeltas) {
cpuProfile.timeDeltas = cpuProfile.timeDeltas.concat(chunk.timeDeltas);
}
if (chunk.startTime != null) {
cpuProfile.startTime = chunk.startTime;
}
if (chunk.endTime != null) {
cpuProfile.endTime = chunk.endTime;
}
} else {
console.warn(`Ignoring ProfileChunk for undeclared Profile with id ${event.id || pidTid}`);
}
}
}
if (cpuProfileByID.size > 0) {
const profiles = [];
let indexToView = 0;
itForEach(cpuProfileByID.keys(), (profileId) => {
let threadName = null;
const pidTid = pidTidById.get(profileId);
if (pidTid) {
threadName = threadNameByPidTid.get(pidTid) || null;
if (threadName) {
}
}
const profile = importFromChromeCPUProfile(cpuProfileByID.get(profileId));
if (threadName && cpuProfileByID.size > 1) {
profile.setName(`${fileName} - ${threadName}`);
if (threadName === "CrRendererMain") {
indexToView = profiles.length;
}
} else {
profile.setName(`${fileName}`);
}
profiles.push(profile);
});
return {
name: fileName,
indexToView,
profiles
};
} else {
throw new Error("Could not find CPU profile in Timeline");
}
}
__name(importFromChromeTimeline, "importFromChromeTimeline");
var callFrameToFrameInfo = /* @__PURE__ */ new Map();
function frameInfoForCallFrame(callFrame) {
return getOrInsert(callFrameToFrameInfo, callFrame, (callFrame2) => {
const file = callFrame2.url;
let line = callFrame2.lineNumber;
if (line != null) line++;
let col = callFrame2.columnNumber;
if (col != null) col++;
const name = callFrame2.functionName || (file ? `(anonymous ${file.split("/").pop()}:${line})` : "(anonymous)");
return {
key: `${name}:${file}:${line}:${col}`,
name,
file,
line,
col
};
});
}
__name(frameInfoForCallFrame, "frameInfoForCallFrame");
function shouldIgnoreFunction(callFrame) {
const { functionName, url } = callFrame;
if (url === "native dummy.js") {
return true;
}
return functionName === "(root)" || functionName === "(idle)";
}
__name(shouldIgnoreFunction, "shouldIgnoreFunction");
function shouldPlaceOnTopOfPreviousStack(functionName) {
return functionName === "(garbage collector)" || functionName === "(program)";
}
__name(shouldPlaceOnTopOfPreviousStack, "shouldPlaceOnTopOfPreviousStack");
function importFromChromeCPUProfile(chromeProfile) {
const profile = new CallTreeProfileBuilder(chromeProfile.endTime - chromeProfile.startTime);
const nodeById = /* @__PURE__ */ new Map();
for (const node of chromeProfile.nodes) {
nodeById.set(node.id, node);
}
for (const node of chromeProfile.nodes) {
if (typeof node.parent === "number") {
node.parent = nodeById.get(node.parent);
}
if (!node.children) continue;
for (const childId of node.children) {
const child = nodeById.get(childId);
if (!child) continue;
child.parent = node;
}
}
const samples = [];
const sampleTimes = [];
let elapsed = chromeProfile.timeDeltas[0];
let lastValidElapsed = elapsed;
let lastNodeId = NaN;
for (let i = 0; i < chromeProfile.samples.length; i++) {
const nodeId = chromeProfile.samples[i];
if (nodeId != lastNodeId) {
samples.push(nodeId);
if (elapsed < lastValidElapsed) {
sampleTimes.push(lastValidElapsed);
} else {
sampleTimes.push(elapsed);
lastValidElapsed = elapsed;
}
}
if (i === chromeProfile.samples.length - 1) {
if (!isNaN(lastNodeId)) {
samples.push(lastNodeId);
if (elapsed < lastValidElapsed) {
sampleTimes.push(lastValidElapsed);
} else {
sampleTimes.push(elapsed);
lastValidElapsed = elapsed;
}
}
} else {
const timeDelta = chromeProfile.timeDeltas[i + 1];
elapsed += timeDelta;
lastNodeId = nodeId;
}
}
let prevStack = [];
for (let i = 0; i < samples.length; i++) {
const value = sampleTimes[i];
const nodeId = samples[i];
const stackTop = nodeById.get(nodeId);
if (!stackTop) continue;
let lca = null;
for (lca = stackTop; lca && prevStack.indexOf(lca) === -1; lca = shouldPlaceOnTopOfPreviousStack(lca.callFrame.functionName) ? lastOf(prevStack) : lca.parent || null) {
}
while (prevStack.length > 0 && lastOf(prevStack) != lca) {
const closingNode = prevStack.pop();
const frame = frameInfoForCallFrame(closingNode.callFrame);
profile.leaveFrame(frame, value);
}
const toOpen = [];
for (
let node = stackTop;
node && node != lca && !shouldIgnoreFunction(node.callFrame);
// Place Chrome internal functions on top of the previous call stack
node = shouldPlaceOnTopOfPreviousStack(node.callFrame.functionName) ? lastOf(prevStack) : node.parent || null
) {
toOpen.push(node);
}
toOpen.reverse();
for (const node of toOpen) {
profile.enterFrame(frameInfoForCallFrame(node.callFrame), value);
}
prevStack = prevStack.concat(toOpen);
}
for (let i = prevStack.length - 1; i >= 0; i--) {
profile.leaveFrame(frameInfoForCallFrame(prevStack[i].callFrame), lastOf(sampleTimes));
}
profile.setValueFormatter(new TimeFormatter("microseconds"));
return profile.build();
}
__name(importFromChromeCPUProfile, "importFromChromeCPUProfile");
function importFromOldV8CPUProfile(content) {
return importFromChromeCPUProfile(chromeTreeToNodes(content));
}
__name(importFromOldV8CPUProfile, "importFromOldV8CPUProfile");
// ../speedscope-import/src/speedscope-import/stackprof.ts
function importFromStackprof(stackprofProfile) {
const { frames, mode, raw, raw_timestamp_deltas, interval } = stackprofProfile;
const profile = new StackListProfileBuilder();
profile.setValueFormatter(new TimeFormatter("microseconds"));
let sampleIndex = 0;
let prevStack = [];
for (let i = 0; i < raw.length; ) {
const stackHeight = raw[i++];
let stack = [];
for (let j = 0; j < stackHeight; j++) {
const id = raw[i++];
let frameName = frames[id].name;
if (frameName == null) {
frameName = "(unknown)";
}
const frame = {
key: id,
...frames[id],
name: frameName
};
stack.push(frame);
}
if (stack.length === 1 && stack[0].name === "(garbage collection)") {
stack = prevStack.concat(stack);
}
const nSamples = raw[i++];
switch (mode) {
case "object":
profile.appendSampleWithWeight(stack, nSamples);
profile.setValueFormatter(new RawValueFormatter());
break;
case "cpu":
profile.appendSampleWithWeight(stack, nSamples * interval);
break;
default:
let sampleDuration = 0;
for (let j = 0; j < nSamples; j++) {
sampleDuration += raw_timestamp_deltas[sampleIndex++];
}
profile.appendSampleWithWeight(stack, sampleDuration);
}
prevStack = stack;
}
return profile.build();
}
__name(importFromStackprof, "importFromStackprof");
// ../speedscope-import/src/speedscope-import/importer-utils.ts
var TEXT_FILE_CHUNK_SIZE = 1 << 27;
function permissivelyParseJSONString(content) {
content = content.trim();
if (content[0] === "[") {
content = content.replace(/,\s*$/, "");
if (content[content.length - 1] !== "]") {
content += "]";
}
}
return JSON.parse(content);
}
__name(permissivelyParseJSONString, "permissivelyParseJSONString");
var _StringBackedTextFileContent = class _StringBackedTextFileContent {
constructor(s) {
__publicField(this, "s");
this.s = s;
}
splitLines() {
return this.s.split("\n");
}
firstChunk() {
return this.s;
}
parseAsJSON(deps) {
return permissivelyParseJSONString(this.s);
}
};
__name(_StringBackedTextFileContent, "StringBackedTextFileContent");
var StringBackedTextFileContent = _StringBackedTextFileContent;
var _TextProfileDataSource = class _TextProfileDataSource {
constructor(fileName, contents) {
__publicField(this, "fileName");
__publicField(this, "contents");
this.fileName = fileName;
this.contents = contents;
}
async name() {
return this.fileName;
}
async readAsArrayBuffer() {
return new ArrayBuffer(0);
}
async readAsText(deps) {
return new StringBackedTextFileContent(this.contents);
}
};
__name(_TextProfileDataSource, "TextProfileDataSource");
var TextProfileDataSource = _TextProfileDataSource;
// ../speedscope-import/src/speedscope-import/instruments.ts
function parseTSV(contents) {
const lines = [
...contents.splitLines()
].map((l) => l.split(" "));
const headerLine = lines.shift();
if (!headerLine) return [];
const indexToField = /* @__PURE__ */ new Map();
for (let i = 0; i < headerLine.length; i++) {
indexToField.set(i, headerLine[i]);
}
const ret = [];
for (const line of lines) {
const row = {};
for (let i = 0; i < line.length; i++) {
row[indexToField.get(i)] = line[i];
}
ret.push(row);
}
return ret;
}
__name(parseTSV, "parseTSV");
function getWeight(deepCopyRow) {
if ("Bytes Used" in deepCopyRow) {
const bytesUsedString = deepCopyRow["Bytes Used"];
const parts = /\s*(\d+(?:[.]\d+)?) (\w+)\s+(?:\d+(?:[.]\d+))%/.exec(bytesUsedString);
if (!parts) return 0;
const value = parseInt(parts[1], 10);
const units = parts[2];
switch (units) {
case "Bytes":
return value;
case "KB":
return 1024 * value;
case "MB":
return 1024 * 1024 * value;
case "GB":
return 1024 * 1024 * 1024 * value;
}
throw new Error(`Unrecognized units ${units}`);
}
if ("Weight" in deepCopyRow || "Running Time" in deepCopyRow) {
const weightString = deepCopyRow["Weight"] || deepCopyRow["Running Time"];
const parts = /\s*(\d+(?:[.]\d+)?) ?(\w+)\s+(?:\d+(?:[.]\d+))%/.exec(weightString);
if (!parts) return 0;
const value = parseInt(parts[1], 10);
const units = parts[2];
switch (units) {
case "ms":
return value;
case "s":
return 1e3 * value;
case "min":
return 60 * 1e3 * value;
case "cycles":
return value;
case "Kc":
return 1e3 * value;
case "Mc":
return 1e3 * 1e3 * value;
case "Gc":
return 1e3 * 1e3 * 1e3 * value;
}
throw new Error(`Unrecognized units ${units}`);
}
return -1;
}
__name(getWeight, "getWeight");
function importFromInstrumentsDeepCopy(contents) {
const profile = new CallTreeProfileBuilder();
const rows = parseTSV(contents);
const stack = [];
let cumulativeValue = 0;
for (const row of rows) {
const symbolName = row["Symbol Name"];
if (!symbolName) continue;
const trimmedSymbolName = symbolName.trim();
const stackDepth = symbolName.length - trimmedSymbolName.length;
if (stack.length - stackDepth < 0) {
throw new Error("Invalid format");
}
const framesToLeave = [];
while (stackDepth < stack.length) {
const stackTop = stack.pop();
framesToLeave.push(stackTop);
}
for (const frameToLeave of framesToLeave) {
cumulativeValue = Math.max(cumulativeValue, frameToLeave.endValue);
profile.leaveFrame(frameToLeave, cumulativeValue);
}
const newFrameInfo = {
key: `${row["Source Path"] || ""}:${trimmedSymbolName}`,
name: trimmedSymbolName,
file: row["Source Path"],
endValue: cumulativeValue + getWeight(row)
};
profile.enterFrame(newFrameInfo, cumulativeValue);
stack.push(newFrameInfo);
}
while (stack.length > 0) {
const frameToLeave = stack.pop();
cumulativeValue = Math.max(cumulativeValue, frameToLeave.endValue);
profile.leaveFrame(frameToLeave, cumulativeValue);
}
if ("Bytes Used" in rows[0]) {
profile.setValueFormatter(new ByteFormatter());
} else if ("Weight" in rows[0] || "Running Time" in rows[0]) {
profile.setValueFormatter(new TimeFormatter("milliseconds"));
}
return profile.build();
}
__name(importFromInstrumentsDeepCopy, "importFromInstrumentsDeepCopy");
// ../speedscope-import/src/speedscope-import/bg-flamegraph.ts
function parseBGFoldedStacks(contents) {
const samples = [];
for (const line of contents.splitLines()) {
const match = /^(.*) (\d+)$/gm.exec(line);
if (!match) continue;
const stack = match[1];
const n = match[2];
samples.push({
stack: stack.split(";").map((name) => ({
key: name,
name
})),
duration: parseInt(n, 10)
});
}
return samples;
}
__name(parseBGFoldedStacks, "parseBGFoldedStacks");
function importFromBGFlameGraph(contents) {
const parsed = parseBGFoldedStacks(contents);
const duration = parsed.reduce((prev, cur) => prev + cur.duration, 0);
const profile = new StackListProfileBuilder(duration);
if (parsed.length === 0) {
return null;
}
for (const sample of parsed) {
profile.appendSampleWithWeight(sample.stack, sample.duration);
}
return profile.build();
}
__name(importFromBGFlameGraph, "importFromBGFlameGraph");
// ../speedscope-import/src/speedscope-import/firefox.ts
function importFromFirefox(firefoxProfile) {
const cpuProfile = firefoxProfile.profile;
const thread = cpuProfile.threads.length === 1 ? cpuProfile.threads[0] : cpuProfile.threads.filter((t) => t.name === "GeckoMain")[0];
const frameKeyToFrameInfo = /* @__PURE__ */ new Map();
function extractStack(sample) {
let stackFrameId = sample[0];
const ret = [];
while (stackFrameId != null) {
const nextStackFrame = thread.stackTable.data[stackFrameId];
const [nextStackId, frameId] = nextStackFrame;
ret.push(frameId);
stackFrameId = nextStackId;
}
ret.reverse();
return ret.map((f) => {
const frameData = thread.frameTable.data[f];
const location = thread.stringTable[frameData[0]];
const match = /(.*)\s+\((.*?)(?::(\d+))?(?::(\d+))?\)$/.exec(location);
if (!match) return null;
if (match[2].startsWith("resource:") || match[2] === "self-hosted" || match[2].startsWith("self-hosted:")) {
return null;
}
return getOrInsert(frameKeyToFrameInfo, location, () => ({
key: location,
name: match[1],
file: match[2],
// In Firefox profiles, line numbers are 1-based, but columns are
// 0-based. Let's normalize both to be 1-based.
line: match[3] ? parseInt(match[3]) : void 0,
col: match[4] ? parseInt(match[4]) + 1 : void 0
}));
}).filter((f) => f != null);
}
__name(extractStack, "extractStack");
const profile = new CallTreeProfileBuilder(firefoxProfile.duration);
let prevStack = [];
for (const sample of thread.samples.data) {
const stack = extractStack(sample);
const value = sample[1];
let lcaIndex = -1;
for (let i = 0; i < Math.min(stack.length, prevStack.length); i++) {
if (prevStack[i] !== stack[i]) {
break;
}
lcaIndex = i;
}
for (let i = prevStack.length - 1; i > lcaIndex; i--) {
profile.leaveFrame(prevStack[i], value);
}
for (let i = lcaIndex + 1; i < stack.length; i++) {
profile.enterFrame(stack[i], value);
}
prevStack = stack;
}
profile.setValueFormatter(new TimeFormatter("milliseconds"));
return profile.build();
}
__name(importFromFirefox, "importFromFirefox");
// ../speedscope-core/src/file-format-spec.ts
(function(FileFormat2) {
(function(ProfileType) {
ProfileType["EVENTED"] = "evented";
ProfileType["SAMPLED"] = "sampled";
})(FileFormat2.ProfileType || (FileFormat2.ProfileType = {}));
(function(EventType) {
EventType["OPEN_FRAME"] = "O";
EventType["CLOSE_FRAME"] = "C";
})(FileFormat2.EventType || (FileFormat2.EventType = {}));
})(FileFormat || (FileFormat = {}));
var FileFormat;
// ../speedscope-import/src/speedscope-import/file-format.ts
function importSpeedscopeProfile(serialized, frames) {
function setCommonProperties(p) {
const { name, unit } = serialized;
switch (unit) {
case "nanoseconds":
case "microseconds":
case "milliseconds":
case "seconds":
p.setValueFormatter(new TimeFormatter(unit));
break;
case "bytes":
p.setValueFormatter(new ByteFormatter());
break;
case "none":
p.setValueFormatter(new RawValueFormatter());
break;
}
p.setName(name);
}
__name(setCommonProperties, "setCommonProperties");
function importEventedProfile(evented) {
const { startValue, endValue, events } = evented;
const profile = new CallTreeProfileBuilder(endValue - startValue);
setCommonProperties(profile);
const frameInfos = frames.map((frame, i) => ({
key: i,
...frame
}));
for (const ev of events) {
switch (ev.type) {
case FileFormat.EventType.OPEN_FRAME: {
profile.enterFrame(frameInfos[ev.frame], ev.at - startValue);
break;
}
case FileFormat.EventType.CLOSE_FRAME: {
profile.leaveFrame(frameInfos[ev.frame], ev.at - startValue);
break;
}
}
}
return profile.build();
}
__name(importEventedProfile, "importEventedProfile");
function importSampledProfile(sampled) {
const { startValue, endValue, samples, weights } = sampled;
const profile = new StackListProfileBuilder(endValue - startValue);
setCommonProperties(profile);
const frameInfos = frames.map((frame, i) => ({
key: i,
...frame
}));
if (samples.length !== weights.length) {
throw new Error(`Expected samples.length (${samples.length}) to equal weights.length (${weights.length})`);
}
for (let i = 0; i < samples.length; i++) {
const stack = samples[i];
const weight = weights[i];
profile.appendSampleWithWeight(stack.map((n) => frameInfos[n]), weight);
}
return profile.build();
}
__name(importSampledProfile, "importSampledProfile");
switch (serialized.type) {
case FileFormat.ProfileType.EVENTED:
return importEventedProfile(serialized);
case FileFormat.ProfileType.SAMPLED:
return importSampledProfile(serialized);
}
}
__name(importSpeedscopeProfile, "importSpeedscopeProfile");
function importSpeedscopeProfiles(serialized) {
return {
name: serialized.name || serialized.profiles[0].name || "profile",
indexToView: serialized.activeProfileIndex || 0,
profiles: serialized.profiles.map((p) => importSpeedscopeProfile(p, serialized.shared.frames))
};
}
__name(importSpeedscopeProfiles, "importSpeedscopeProfiles");
// ../speedscope-import/src/speedscope-import/v8proflog.ts
function codeToFrameInfo(code, v8log) {
if (!code || !code.type) {
return {
key: "(unknown type)",
name: "(unknown type)"
};
}
let name = code.name;
switch (code.type) {
case "CPP": {
const matches = name.match(/[tT] ([^(<]*)/);
if (matches) name = `(c++) ${matches[1]}`;
break;
}
case "SHARED_LIB":
name = "(LIB) " + name;
break;
case "JS": {
const matches = name.match(/([a-zA-Z0-9\._\-$]*) ([a-zA-Z0-9\.\-_\/$]*):(\d+):(\d+)/);
if (matches) {
const file = matches[2];
const line = parseInt(matches[3], 10);
const col = parseInt(matches[4], 10);
const functionName = matches[1].length > 0 ? matches[1] : file ? `(anonymous ${file.split("/").pop()}:${line})` : "(anonymous)";
return {
key: name,
name: functionName,
file: file.length > 0 ? file : "(unknown file)",
line,
col
};
}
break;
}
case "CODE": {
switch (code.kind) {
case "LoadIC":
case "StoreIC":
case "KeyedStoreIC":
case "KeyedLoadIC":
case "LoadGlobalIC":
case "Handler":
name = "(IC) " + name;
break;
case "BytecodeHandler":
name = "(bytecode) ~" + name;
break;
case "Stub":
name = "(stub) " + name;
break;
case "Builtin":
name = "(builtin) " + name;
break;
case "RegExp":
name = "(regexp) " + name;
break;
}
break;
}
default: {
name = `(${code.type}) ${name}`;
break;
}
}
return {
key: name,
name
};
}
__name(codeToFrameInfo, "codeToFrameInfo");
function importFromV8ProfLog(v8log) {
const profile = new StackListProfileBuilder();
const sToFrameInfo = /* @__PURE__ */ new Map();
function getFrameInfo(t) {
return getOrInsert(sToFrameInfo, t, (t2) => {
const code = v8log.code[t2];
return codeToFrameInfo(code, v8log);
});
}
__name(getFrameInfo, "getFrameInfo");
let lastTm = 0;
sortBy(v8log.ticks, (tick) => tick.tm);
for (const tick of v8log.ticks) {
const stack = [];
for (let i = tick.s.length - 2; i >= 0; i -= 2) {
const id = tick.s[i];
if (id === -1) continue;
if (id > v8log.code.length) {
stack.push({
key: id,
name: `0x${id.toString(16)}`
});
continue;
}
stack.push(getFrameInfo(id));
}
profile.appendSampleWithWeight(stack, tick.tm - lastTm);
lastTm = tick.tm;
}
profile.setValueFormatter(new TimeFormatter("microseconds"));
return profile.build();
}
__name(importFromV8ProfLog, "importFromV8ProfLog");
// ../speedscope-import/src/speedscope-import/linux-tools-perf.ts
function* parseEvents(contents) {
let buffer = [];
for (const line of contents.splitLines()) {
if (line === "") {
yield parseEvent(buffer);
buffer = [];
} else buffer.push(line);
}
if (buffer.length > 0) yield parseEvent(buffer);
}
__name(parseEvents, "parseEvents");
function parseEvent(rawEvent) {
const lines = rawEvent.filter((l) => !/^\s*#/.exec(l));
const event = {
command: null,
processID: null,
threadID: null,
time: null,
eventType: "",
stack: []
};
const firstLine = lines.shift();
if (!firstLine) return null;
const eventStartMatch = /^(\S.+?)\s+(\d+)(?:\/?(\d+))?\s+/.exec(firstLine);
if (!eventStartMatch) return null;
event.command = eventStartMatch[1];
if (eventStartMatch[3]) {
event.processID = parseInt(eventStartMatch[2], 10);
event.threadID = parseInt(eventStartMatch[3], 10);
} else {
event.threadID = parseInt(eventStartMatch[2], 10);
}
const timeMatch = /\s+(\d+\.\d+):\s+/.exec(firstLine);
if (timeMatch) {
event.time = parseFloat(timeMatch[1]);
}
const evName = /(\S+):\s*$/.exec(firstLine);
if (evName) {
event.eventType = evName[1];
}
for (const line of lines) {
const lineMatch = /^\s*(\w+)\s*(.+) \((\S*)\)/.exec(line);
if (!lineMatch) continue;
let [, address, symbolName, file] = lineMatch;
symbolName = symbolName.replace(/\+0x[\da-f]+$/, "");
event.stack.push({
address: `0x${address}`,
symbolName,
file
});
}
event.stack.reverse();
return event;
}
__name(parseEvent, "parseEvent");
function importFromLinuxPerf(contents) {
const profiles = /* @__PURE__ */ new Map();
let eventType = null;
for (const event of parseEvents(contents)) {
if (event == null) continue;
if (eventType != null && eventType != event.eventType) continue;
if (event.time == null) continue;
eventType = event.eventType;
const profileNameParts = [];
if (event.command) profileNameParts.push(event.command);
if (event.processID) profileNameParts.push(`pid: ${event.processID}`);
if (event.threadID) profileNameParts.push(`tid: ${event.threadID}`);
const profileName = profileNameParts.join(" ");
const builderState = getOrInsert(profiles, profileName, () => {
const builder2 = new StackListProfileBuilder();
builder2.setName(profileName);
builder2.setValueFormatter(new TimeFormatter("seconds"));
return builder2;
});
const builder = builderState;
builder.appendSampleWithTimestamp(event.stack.map(({ symbolName, file }) => {
return {
key: `${symbolName} (${file})`,
name: symbolName === "[unknown]" ? `??? (${file})` : symbolName,
file
};
}), event.time);
}
if (profiles.size === 0) {
return null;
}
return {
name: profiles.size === 1 ? Array.from(profiles.keys())[0] : "",
indexToView: 0,
profiles: Array.from(itMap(profiles.values(), (builder) => builder.build()))
};
}
__name(importFromLinuxPerf, "importFromLinuxPerf");
// ../speedscope-import/src/speedscope-import/haskell.ts
function addToProfile(tree, startVal, profile, infos, attribute) {
if (tree.ticks === 0 && tree.entries