@grafana/flamegraph
Version:
Grafana flamegraph visualization component
275 lines (272 loc) • 9.14 kB
JavaScript
import { FieldType, getDisplayProcessor, createTheme } from '@grafana/data';
import { SampleUnit } from '../types.mjs';
import { mergeParentSubtrees, mergeSubtrees } from './treeTransforms.mjs';
function nestedSetToLevels(container, options) {
const levels = [];
let offset = 0;
let parent = void 0;
const uniqueLabels = {};
for (let i = 0; i < container.data.length; i++) {
const currentLevel = container.getLevel(i);
const prevLevel = i > 0 ? container.getLevel(i - 1) : void 0;
levels[currentLevel] = levels[currentLevel] || [];
if (prevLevel && prevLevel >= currentLevel) {
const lastSibling = levels[currentLevel][levels[currentLevel].length - 1];
offset = lastSibling.start + container.getValue(lastSibling.itemIndexes[0]) + container.getValueRight(lastSibling.itemIndexes[0]);
parent = lastSibling.parents[0];
}
const newItem = {
itemIndexes: [i],
value: container.getValue(i) + container.getValueRight(i),
valueRight: container.isDiffFlamegraph() ? container.getValueRight(i) : void 0,
start: offset,
parents: parent && [parent],
children: [],
level: currentLevel
};
if (uniqueLabels[container.getLabel(i)]) {
uniqueLabels[container.getLabel(i)].push(newItem);
} else {
uniqueLabels[container.getLabel(i)] = [newItem];
}
if (parent) {
parent.children.push(newItem);
}
parent = newItem;
levels[currentLevel].push(newItem);
}
const collapsedMapContainer = new CollapsedMapBuilder(options == null ? void 0 : options.collapsingThreshold);
if (options == null ? void 0 : options.collapsing) {
collapsedMapContainer.addTree(levels[0][0]);
}
return [levels, uniqueLabels, collapsedMapContainer.getCollapsedMap()];
}
class CollapsedMap {
constructor(map) {
// The levelItem used as a key is the item that will always be rendered in the flame graph. The config.items are all
// the items that are in the group and if the config.collapsed is true they will be hidden.
this.map = /* @__PURE__ */ new Map();
this.map = map || /* @__PURE__ */ new Map();
}
get(item) {
return this.map.get(item);
}
keys() {
return this.map.keys();
}
values() {
return this.map.values();
}
size() {
return this.map.size;
}
setCollapsedStatus(item, collapsed) {
const newMap = new Map(this.map);
const collapsedConfig = this.map.get(item);
const newConfig = { ...collapsedConfig, collapsed };
for (const item2 of collapsedConfig.items) {
newMap.set(item2, newConfig);
}
return new CollapsedMap(newMap);
}
setAllCollapsedStatus(collapsed) {
const newMap = new Map(this.map);
for (const item of this.map.keys()) {
const collapsedConfig = this.map.get(item);
const newConfig = { ...collapsedConfig, collapsed };
newMap.set(item, newConfig);
}
return new CollapsedMap(newMap);
}
}
class CollapsedMapBuilder {
constructor(threshold) {
this.map = /* @__PURE__ */ new Map();
this.threshold = 0.99;
if (threshold !== void 0) {
this.threshold = threshold;
}
}
addTree(root) {
var _a;
const stack = [root];
while (stack.length) {
const current = stack.shift();
if ((_a = current.parents) == null ? void 0 : _a.length) {
this.addItem(current, current.parents[0]);
}
if (current.children.length) {
stack.unshift(...current.children);
}
}
}
// The heuristics here is pretty simple right now. Just check if it's single child and if we are within threshold.
// We assume items with small self just aren't too important while we cannot really collapse items with siblings
// as it's not clear what to do with said sibling.
addItem(item, parent) {
if (parent && item.value > parent.value * this.threshold && parent.children.length === 1) {
if (this.map.has(parent)) {
const config = this.map.get(parent);
this.map.set(item, config);
config.items.push(item);
} else {
const config = { items: [parent, item], collapsed: true };
this.map.set(parent, config);
this.map.set(item, config);
}
}
}
getCollapsedMap() {
return new CollapsedMap(this.map);
}
}
function getMessageCheckFieldsResult(wrongFields) {
if (wrongFields.missingFields.length) {
return `Data is missing fields: ${wrongFields.missingFields.join(", ")}`;
}
if (wrongFields.wrongTypeFields.length) {
return `Data has fields of wrong type: ${wrongFields.wrongTypeFields.map((f) => `${f.name} has type ${f.type} but should be ${f.expectedTypes.join(" or ")}`).join(", ")}`;
}
return "";
}
function checkFields(data) {
const fields = [
["label", [FieldType.string, FieldType.enum]],
["level", [FieldType.number]],
["value", [FieldType.number]],
["self", [FieldType.number]]
];
const missingFields = [];
const wrongTypeFields = [];
for (const field of fields) {
const [name, types] = field;
const frameField = data == null ? void 0 : data.fields.find((f) => f.name === name);
if (!frameField) {
missingFields.push(name);
continue;
}
if (!types.includes(frameField.type)) {
wrongTypeFields.push({ name, expectedTypes: types, type: frameField.type });
}
}
if (missingFields.length > 0 || wrongTypeFields.length > 0) {
return {
wrongTypeFields,
missingFields
};
}
return void 0;
}
class FlameGraphDataContainer {
constructor(data, options, theme = createTheme()) {
var _a, _b, _c;
this.data = data;
this.options = options;
const wrongFields = checkFields(data);
if (wrongFields) {
throw new Error(getMessageCheckFieldsResult(wrongFields));
}
this.labelField = data.fields.find((f) => f.name === "label");
this.levelField = data.fields.find((f) => f.name === "level");
this.valueField = data.fields.find((f) => f.name === "value");
this.selfField = data.fields.find((f) => f.name === "self");
this.valueRightField = data.fields.find((f) => f.name === "valueRight");
this.selfRightField = data.fields.find((f) => f.name === "selfRight");
if ((this.valueField || this.selfField) && !(this.valueField && this.selfField)) {
throw new Error(
"Malformed dataFrame: both valueRight and selfRight has to be present if one of them is present."
);
}
const enumConfig = (_c = (_b = (_a = this.labelField) == null ? void 0 : _a.config) == null ? void 0 : _b.type) == null ? void 0 : _c.enum;
if (enumConfig) {
this.labelDisplayProcessor = getDisplayProcessor({ field: this.labelField, theme });
this.uniqueLabels = enumConfig.text || [];
} else {
this.labelDisplayProcessor = (value) => ({
text: value + "",
numeric: 0
});
this.uniqueLabels = [...new Set(this.labelField.values)];
}
this.valueDisplayProcessor = getDisplayProcessor({
field: this.valueField,
theme
});
}
isDiffFlamegraph() {
return Boolean(this.valueRightField && this.selfRightField);
}
getLabel(index) {
return this.labelDisplayProcessor(this.labelField.values[index]).text;
}
getLevel(index) {
return this.levelField.values[index];
}
getValue(index) {
return fieldAccessor(this.valueField, index);
}
getValueRight(index) {
return fieldAccessor(this.valueRightField, index);
}
getSelf(index) {
return fieldAccessor(this.selfField, index);
}
getSelfRight(index) {
return fieldAccessor(this.selfRightField, index);
}
getSelfDisplay(index) {
return this.valueDisplayProcessor(this.getSelf(index));
}
getUniqueLabels() {
return this.uniqueLabels;
}
getUnitTitle() {
switch (this.valueField.config.unit) {
case SampleUnit.Bytes:
return "RAM";
case SampleUnit.Nanoseconds:
return "Time";
}
return "Count";
}
getLevels() {
this.initLevels();
return this.levels;
}
getSandwichLevels(label) {
const nodes = this.getNodesWithLabel(label);
if (!(nodes == null ? void 0 : nodes.length)) {
return [[], []];
}
const callers = mergeParentSubtrees(nodes, this);
const callees = mergeSubtrees(nodes, this);
return [callers, callees];
}
getNodesWithLabel(label) {
this.initLevels();
return this.uniqueLabelsMap[label];
}
getCollapsedMap() {
this.initLevels();
return this.collapsedMap;
}
initLevels() {
if (!this.levels) {
const [levels, uniqueLabelsMap, collapsedMap] = nestedSetToLevels(this, this.options);
this.levels = levels;
this.uniqueLabelsMap = uniqueLabelsMap;
this.collapsedMap = collapsedMap;
}
}
}
function fieldAccessor(field, index) {
if (!field) {
return 0;
}
let indexArray = typeof index === "number" ? [index] : index;
return indexArray.reduce((acc, index2) => {
return acc + field.values[index2];
}, 0);
}
export { CollapsedMap, CollapsedMapBuilder, FlameGraphDataContainer, checkFields, getMessageCheckFieldsResult, nestedSetToLevels };
//# sourceMappingURL=dataTransform.mjs.map