mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Profiling] Reduce CPU usage for flamegraph and TopN function (#141017)
* Add instrumentation for flamegraph I inlined the construction of the flamegraph into the respective route so that we could add fine-grained instrumentation. We now use APM and console logging to understand how long flamegraph construction takes. * Remove unnecessary Set usage * Remove superfluous clone This was likely added when we needed to avoid infinite recursion when serializing to JSON. This no longer has a useful function. * Pass in pre-calculated frame group info I noticed that we were creating frame group info multiple times so I added it as a parameter for the intermediate node. * Sort callees in one place Callees should be sorted first by samples decreasing and then by frame groups. Combining the two sorts makes the post-processing clearer to future readers and/or maintainers. * Capitalize fields in preparation of merging * Align both node data structures * Pass metadata instead of copying fields * Refactor frame label method * Use pre-calculated array length * Use pre-allocated array * Refactor intermediate node * Remove intermediate node structure * Move if statement out of for loop * Fix comments * Sort sibling nodes by frame group ID * Calculate graph size during creation * Add missing groupStackFrameMetadataByStackTrace * Fix formatting * Fix generated callee source * Fix creation of frame group * Fix test * Remove filter for relevant traces * Stop passing frame group * Create root node inside createCallerCalleeGraph * Fix timestamps * Remove frame group comparator * Add instrumentation for topN functions * Allow for missing stacktraces * Use Date.now instead
This commit is contained in:
parent
0fcfaec2dd
commit
d8901857aa
14 changed files with 413 additions and 809 deletions
|
@ -6,60 +6,18 @@
|
|||
*/
|
||||
|
||||
import { sum } from 'lodash';
|
||||
import {
|
||||
createCallerCalleeDiagram,
|
||||
createCallerCalleeIntermediateNode,
|
||||
fromCallerCalleeIntermediateNode,
|
||||
} from './callercallee';
|
||||
import { createFrameGroupID } from './frame_group';
|
||||
import { createStackFrameMetadata } from './profiling';
|
||||
import { createCallerCalleeGraph } from './callercallee';
|
||||
|
||||
import { events, stackTraces, stackFrames, executables } from './__fixtures__/stacktraces';
|
||||
|
||||
describe('Caller-callee operations', () => {
|
||||
test('1', () => {
|
||||
const parentFrame = createStackFrameMetadata({
|
||||
FileID: '6bc50d345244d5956f93a1b88f41874d',
|
||||
FrameType: 3,
|
||||
AddressOrLine: 971740,
|
||||
FunctionName: 'epoll_wait',
|
||||
SourceID: 'd670b496cafcaea431a23710fb5e4f58',
|
||||
SourceLine: 30,
|
||||
ExeFileName: 'libc-2.26.so',
|
||||
});
|
||||
const parent = createCallerCalleeIntermediateNode(parentFrame, 10, 'parent');
|
||||
|
||||
const childFrame = createStackFrameMetadata({
|
||||
FileID: '8d8696a4fd51fa88da70d3fde138247d',
|
||||
FrameType: 3,
|
||||
AddressOrLine: 67000,
|
||||
FunctionName: 'epoll_poll',
|
||||
SourceID: 'f0a7901dcefed6cc8992a324b9df733c',
|
||||
SourceLine: 150,
|
||||
ExeFileName: 'auditd',
|
||||
});
|
||||
const child = createCallerCalleeIntermediateNode(childFrame, 10, 'child');
|
||||
|
||||
const root = createCallerCalleeIntermediateNode(createStackFrameMetadata(), 10, 'root');
|
||||
root.callees.set(createFrameGroupID(child.frameGroup), child);
|
||||
root.callees.set(createFrameGroupID(parent.frameGroup), parent);
|
||||
|
||||
const graph = fromCallerCalleeIntermediateNode(root);
|
||||
|
||||
// Modify original frames to verify graph does not contain references
|
||||
parent.samples = 30;
|
||||
child.samples = 20;
|
||||
|
||||
expect(graph.Callees[0].Samples).toEqual(10);
|
||||
expect(graph.Callees[1].Samples).toEqual(10);
|
||||
});
|
||||
|
||||
test('2', () => {
|
||||
const totalSamples = sum([...events.values()]);
|
||||
|
||||
const root = createCallerCalleeDiagram(events, stackTraces, stackFrames, executables);
|
||||
expect(root.Samples).toEqual(totalSamples);
|
||||
expect(root.CountInclusive).toEqual(totalSamples);
|
||||
expect(root.CountExclusive).toEqual(0);
|
||||
const graph = createCallerCalleeGraph(events, stackTraces, stackFrames, executables);
|
||||
|
||||
expect(graph.root.Samples).toEqual(totalSamples);
|
||||
expect(graph.root.CountInclusive).toEqual(totalSamples);
|
||||
expect(graph.root.CountExclusive).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,19 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { clone } from 'lodash';
|
||||
import {
|
||||
compareFrameGroup,
|
||||
createFrameGroup,
|
||||
createFrameGroupID,
|
||||
FrameGroup,
|
||||
FrameGroupID,
|
||||
} from './frame_group';
|
||||
import { createFrameGroup, createFrameGroupID, FrameGroupID } from './frame_group';
|
||||
import {
|
||||
createStackFrameMetadata,
|
||||
emptyStackTrace,
|
||||
Executable,
|
||||
FileID,
|
||||
groupStackFrameMetadataByStackTrace,
|
||||
StackFrame,
|
||||
StackFrameID,
|
||||
StackFrameMetadata,
|
||||
|
@ -25,298 +18,160 @@ import {
|
|||
StackTraceID,
|
||||
} from './profiling';
|
||||
|
||||
export interface CallerCalleeIntermediateNode {
|
||||
frameGroup: FrameGroup;
|
||||
frameGroupID: string;
|
||||
callers: Map<FrameGroupID, CallerCalleeIntermediateNode>;
|
||||
callees: Map<FrameGroupID, CallerCalleeIntermediateNode>;
|
||||
frameMetadata: Set<StackFrameMetadata>;
|
||||
samples: number;
|
||||
countInclusive: number;
|
||||
countExclusive: number;
|
||||
export interface CallerCalleeNode {
|
||||
Callers: Map<FrameGroupID, CallerCalleeNode>;
|
||||
Callees: Map<FrameGroupID, CallerCalleeNode>;
|
||||
FrameMetadata: StackFrameMetadata;
|
||||
FrameGroupID: FrameGroupID;
|
||||
Samples: number;
|
||||
CountInclusive: number;
|
||||
CountExclusive: number;
|
||||
}
|
||||
|
||||
export function createCallerCalleeIntermediateNode(
|
||||
export function createCallerCalleeNode(
|
||||
frameMetadata: StackFrameMetadata,
|
||||
samples: number,
|
||||
frameGroupID: string
|
||||
): CallerCalleeIntermediateNode {
|
||||
frameGroupID: FrameGroupID,
|
||||
samples: number
|
||||
): CallerCalleeNode {
|
||||
return {
|
||||
frameGroup: createFrameGroup(frameMetadata),
|
||||
callers: new Map<FrameGroupID, CallerCalleeIntermediateNode>(),
|
||||
callees: new Map<FrameGroupID, CallerCalleeIntermediateNode>(),
|
||||
frameMetadata: new Set<StackFrameMetadata>([frameMetadata]),
|
||||
samples,
|
||||
countInclusive: 0,
|
||||
countExclusive: 0,
|
||||
frameGroupID,
|
||||
Callers: new Map<FrameGroupID, CallerCalleeNode>(),
|
||||
Callees: new Map<FrameGroupID, CallerCalleeNode>(),
|
||||
FrameMetadata: frameMetadata,
|
||||
FrameGroupID: frameGroupID,
|
||||
Samples: samples,
|
||||
CountInclusive: 0,
|
||||
CountExclusive: 0,
|
||||
};
|
||||
}
|
||||
|
||||
interface RelevantTrace {
|
||||
frames: StackFrameMetadata[];
|
||||
index: number;
|
||||
export interface CallerCalleeGraph {
|
||||
root: CallerCalleeNode;
|
||||
size: number;
|
||||
}
|
||||
|
||||
// selectRelevantTraces searches through a map that maps trace hashes to their
|
||||
// frames and only returns those traces that have a frame that are equivalent
|
||||
// to the rootFrame provided. It also sets the "index" in the sequence of
|
||||
// traces at which the rootFrame is found.
|
||||
//
|
||||
// If the rootFrame is "empty" (e.g. fileID is empty and line number is 0), all
|
||||
// traces in the given time frame are deemed relevant, and the "index" is set
|
||||
// to the length of the trace -- since there is no root frame, the frame should
|
||||
// be considered "calls-to" only going.
|
||||
function selectRelevantTraces(
|
||||
rootFrame: StackFrameMetadata,
|
||||
frames: Map<StackTraceID, StackFrameMetadata[]>
|
||||
): Map<StackTraceID, RelevantTrace> {
|
||||
const result = new Map<StackTraceID, RelevantTrace>();
|
||||
const rootString = createFrameGroupID(createFrameGroup(rootFrame));
|
||||
for (const [stackTraceID, frameMetadata] of frames) {
|
||||
if (rootFrame.FileID === '' && rootFrame.AddressOrLine === 0) {
|
||||
// If the root frame is empty, every trace is relevant, and all elements
|
||||
// of the trace are relevant. This means that the index is set to the
|
||||
// length of the frameMetadata, implying that in the absence of a root
|
||||
// frame the "topmost" frame is the root frame.
|
||||
result.set(stackTraceID, {
|
||||
frames: frameMetadata,
|
||||
index: frameMetadata.length,
|
||||
} as RelevantTrace);
|
||||
} else {
|
||||
// Search for the right index of the root frame in the frameMetadata, and
|
||||
// set it in the result.
|
||||
for (let i = 0; i < frameMetadata.length; i++) {
|
||||
if (rootString === createFrameGroupID(createFrameGroup(frameMetadata[i]))) {
|
||||
result.set(stackTraceID, {
|
||||
frames: frameMetadata,
|
||||
index: i,
|
||||
} as RelevantTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function sortRelevantTraces(relevantTraces: Map<StackTraceID, RelevantTrace>): StackTraceID[] {
|
||||
const sortedRelevantTraces = new Array<StackTraceID>();
|
||||
for (const trace of relevantTraces.keys()) {
|
||||
sortedRelevantTraces.push(trace);
|
||||
}
|
||||
return sortedRelevantTraces.sort((t1, t2) => {
|
||||
if (t1 < t2) return -1;
|
||||
if (t1 > t2) return 1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
// createCallerCalleeIntermediateRoot creates a graph in the internal
|
||||
// representation from a StackFrameMetadata that identifies the "centered"
|
||||
// function and the trace results that provide traces and the number of times
|
||||
// that the trace has been seen.
|
||||
// createCallerCalleeGraph creates a graph in the internal representation
|
||||
// from a StackFrameMetadata that identifies the "centered" function and
|
||||
// the trace results that provide traces and the number of times that the
|
||||
// trace has been seen.
|
||||
//
|
||||
// The resulting data structure contains all of the data, but is not yet in the
|
||||
// form most easily digestible by others.
|
||||
export function createCallerCalleeIntermediateRoot(
|
||||
rootFrame: StackFrameMetadata,
|
||||
traces: Map<StackTraceID, number>,
|
||||
frames: Map<StackTraceID, StackFrameMetadata[]>
|
||||
): CallerCalleeIntermediateNode {
|
||||
// Create a node for the centered frame
|
||||
const root = createCallerCalleeIntermediateNode(rootFrame, 0, 'root');
|
||||
export function createCallerCalleeGraph(
|
||||
events: Map<StackTraceID, number>,
|
||||
stackTraces: Map<StackTraceID, StackTrace>,
|
||||
stackFrames: Map<StackFrameID, StackFrame>,
|
||||
executables: Map<FileID, Executable>
|
||||
): CallerCalleeGraph {
|
||||
// Create a root node for the graph
|
||||
const rootFrame = createStackFrameMetadata();
|
||||
const rootFrameGroup = createFrameGroup(
|
||||
rootFrame.FileID,
|
||||
rootFrame.AddressOrLine,
|
||||
rootFrame.ExeFileName,
|
||||
rootFrame.SourceFilename,
|
||||
rootFrame.FunctionName
|
||||
);
|
||||
const rootFrameGroupID = createFrameGroupID(rootFrameGroup);
|
||||
const root = createCallerCalleeNode(rootFrame, rootFrameGroupID, 0);
|
||||
const graph: CallerCalleeGraph = { root, size: 1 };
|
||||
|
||||
// Obtain only the relevant frames (e.g. frames that contain the root frame
|
||||
// somewhere). If the root frame is "empty" (e.g. fileID is zero and line
|
||||
// number is zero), all frames are deemed relevant.
|
||||
const relevantTraces = selectRelevantTraces(rootFrame, frames);
|
||||
|
||||
// For a deterministic result we have to walk the traces in a deterministic
|
||||
// order. A deterministic result allows for deterministic UI views, something
|
||||
// that users expect.
|
||||
const relevantTracesSorted = sortRelevantTraces(relevantTraces);
|
||||
const sortedStackTraceIDs = new Array<StackTraceID>();
|
||||
for (const trace of stackTraces.keys()) {
|
||||
sortedStackTraceIDs.push(trace);
|
||||
}
|
||||
sortedStackTraceIDs.sort((t1, t2) => {
|
||||
return t1.localeCompare(t2);
|
||||
});
|
||||
|
||||
// Walk through all traces that contain the root. Increment the count of the
|
||||
// root by the count of that trace. Walk "up" the trace (through the callers)
|
||||
// and add the count of the trace to each caller. Then walk "down" the trace
|
||||
// (through the callees) and add the count of the trace to each callee.
|
||||
|
||||
for (const traceHash of relevantTracesSorted) {
|
||||
const trace = relevantTraces.get(traceHash)!;
|
||||
|
||||
// The slice of frames is ordered so that the leaf function is at index 0.
|
||||
// This means that the "second part" of the slice are the callers, and the
|
||||
// "first part" are the callees.
|
||||
for (const stackTraceID of sortedStackTraceIDs) {
|
||||
// The slice of frames is ordered so that the leaf function is at the
|
||||
// highest index. This means that the "first part" of the slice are the
|
||||
// callers, and the "second part" are the callees.
|
||||
//
|
||||
// We currently assume there are no callers.
|
||||
const callees = trace.frames;
|
||||
const samples = traces.get(traceHash)!;
|
||||
|
||||
// Go through the callees, reverse iteration
|
||||
let currentNode = clone(root);
|
||||
root.samples += samples;
|
||||
// It is possible that we do not have a stacktrace for an event,
|
||||
// e.g. when stopping the host agent or on network errors.
|
||||
const stackTrace = stackTraces.get(stackTraceID) ?? emptyStackTrace;
|
||||
const lenStackTrace = stackTrace.FrameIDs.length;
|
||||
const samples = events.get(stackTraceID)!;
|
||||
|
||||
let currentNode = root;
|
||||
root.Samples += samples;
|
||||
|
||||
for (let i = 0; i < lenStackTrace; i++) {
|
||||
const frameID = stackTrace.FrameIDs[i];
|
||||
const fileID = stackTrace.FileIDs[i];
|
||||
const addressOrLine = stackTrace.AddressOrLines[i];
|
||||
const frame = stackFrames.get(frameID)!;
|
||||
const executable = executables.get(fileID)!;
|
||||
|
||||
const frameGroup = createFrameGroup(
|
||||
fileID,
|
||||
addressOrLine,
|
||||
executable.FileName,
|
||||
frame.FileName,
|
||||
frame.FunctionName
|
||||
);
|
||||
const frameGroupID = createFrameGroupID(frameGroup);
|
||||
|
||||
let node = currentNode.Callees.get(frameGroupID);
|
||||
|
||||
for (let i = 0; i < callees.length; i++) {
|
||||
const callee = callees[i];
|
||||
const calleeName = createFrameGroupID(createFrameGroup(callee));
|
||||
let node = currentNode.callees.get(calleeName);
|
||||
if (node === undefined) {
|
||||
node = createCallerCalleeIntermediateNode(callee, samples, calleeName);
|
||||
currentNode.callees.set(calleeName, node);
|
||||
const callee = createStackFrameMetadata({
|
||||
FrameID: frameID,
|
||||
FileID: fileID,
|
||||
AddressOrLine: addressOrLine,
|
||||
FrameType: stackTrace.Types[i],
|
||||
FunctionName: frame.FunctionName,
|
||||
FunctionOffset: frame.FunctionOffset,
|
||||
SourceLine: frame.LineNumber,
|
||||
SourceFilename: frame.FileName,
|
||||
ExeFileName: executable.FileName,
|
||||
});
|
||||
|
||||
node = createCallerCalleeNode(callee, frameGroupID, samples);
|
||||
currentNode.Callees.set(frameGroupID, node);
|
||||
graph.size++;
|
||||
} else {
|
||||
node.samples += samples;
|
||||
node.Samples += samples;
|
||||
}
|
||||
|
||||
node.countInclusive += samples;
|
||||
node.CountInclusive += samples;
|
||||
|
||||
if (i === callees.length - 1) {
|
||||
if (i === lenStackTrace - 1) {
|
||||
// Leaf frame: sum up counts for exclusive CPU.
|
||||
node.countExclusive += samples;
|
||||
node.CountExclusive += samples;
|
||||
}
|
||||
currentNode = node;
|
||||
}
|
||||
}
|
||||
|
||||
root.countExclusive = 0;
|
||||
root.countInclusive = root.samples;
|
||||
root.CountExclusive = 0;
|
||||
root.CountInclusive = root.Samples;
|
||||
|
||||
return root;
|
||||
return graph;
|
||||
}
|
||||
|
||||
export interface CallerCalleeNode {
|
||||
FrameID: string;
|
||||
FrameGroupID: string;
|
||||
Callers: CallerCalleeNode[];
|
||||
Callees: CallerCalleeNode[];
|
||||
FileID: string;
|
||||
FrameType: number;
|
||||
ExeFileName: string;
|
||||
FunctionID: string;
|
||||
FunctionName: string;
|
||||
AddressOrLine: number;
|
||||
FunctionSourceLine: number;
|
||||
FunctionSourceID: string;
|
||||
FunctionSourceURL: string;
|
||||
SourceFilename: string;
|
||||
SourceLine: number;
|
||||
Samples: number;
|
||||
CountInclusive: number;
|
||||
CountExclusive: number;
|
||||
}
|
||||
|
||||
export function createCallerCalleeNode(options: Partial<CallerCalleeNode> = {}): CallerCalleeNode {
|
||||
const node = {} as CallerCalleeNode;
|
||||
|
||||
node.FrameID = options.FrameID ?? '';
|
||||
node.FrameGroupID = options.FrameGroupID ?? '';
|
||||
node.Callers = clone(options.Callers ?? []);
|
||||
node.Callees = clone(options.Callees ?? []);
|
||||
node.FileID = options.FileID ?? '';
|
||||
node.FrameType = options.FrameType ?? 0;
|
||||
node.ExeFileName = options.ExeFileName ?? '';
|
||||
node.FunctionID = options.FunctionID ?? '';
|
||||
node.FunctionName = options.FunctionName ?? '';
|
||||
node.AddressOrLine = options.AddressOrLine ?? 0;
|
||||
node.FunctionSourceLine = options.FunctionSourceLine ?? 0;
|
||||
node.FunctionSourceID = options.FunctionSourceID ?? '';
|
||||
node.FunctionSourceURL = options.FunctionSourceURL ?? '';
|
||||
node.SourceFilename = options.SourceFilename ?? '';
|
||||
node.SourceLine = options.SourceLine ?? 0;
|
||||
node.Samples = options.Samples ?? 0;
|
||||
node.CountInclusive = options.CountInclusive ?? 0;
|
||||
node.CountExclusive = options.CountExclusive ?? 0;
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
// selectCallerCalleeData is the "standard" way of merging multiple frames into
|
||||
// one node. It simply takes the data from the first frame.
|
||||
function selectCallerCalleeData(frameMetadata: Set<StackFrameMetadata>, node: CallerCalleeNode) {
|
||||
for (const metadata of frameMetadata) {
|
||||
node.FileID = metadata.FileID;
|
||||
node.FrameType = metadata.FrameType;
|
||||
node.ExeFileName = metadata.ExeFileName;
|
||||
node.FunctionID = metadata.FunctionName;
|
||||
node.FunctionName = metadata.FunctionName;
|
||||
node.AddressOrLine = metadata.AddressOrLine;
|
||||
node.FrameID = metadata.FrameID;
|
||||
|
||||
// Unknown/invalid offsets are currently set to 0.
|
||||
//
|
||||
// In this case we leave FunctionSourceLine=0 as a flag for the UI that the
|
||||
// FunctionSourceLine should not be displayed.
|
||||
//
|
||||
// As FunctionOffset=0 could also be a legit value, this work-around needs
|
||||
// a real fix. The idea for after GA is to change FunctionOffset=-1 to
|
||||
// indicate unknown/invalid.
|
||||
if (metadata.FunctionOffset > 0) {
|
||||
node.FunctionSourceLine = metadata.SourceLine - metadata.FunctionOffset;
|
||||
} else {
|
||||
node.FunctionSourceLine = 0;
|
||||
}
|
||||
|
||||
node.FunctionSourceID = metadata.SourceID;
|
||||
node.FunctionSourceURL = metadata.SourceCodeURL;
|
||||
node.SourceFilename = metadata.SourceFilename;
|
||||
node.SourceLine = metadata.SourceLine;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function sortNodes(
|
||||
nodes: Map<FrameGroupID, CallerCalleeIntermediateNode>
|
||||
): CallerCalleeIntermediateNode[] {
|
||||
const sortedNodes = new Array<CallerCalleeIntermediateNode>();
|
||||
for (const node of nodes.values()) {
|
||||
export function sortCallerCalleeNodes(
|
||||
nodes: Map<FrameGroupID, CallerCalleeNode>
|
||||
): CallerCalleeNode[] {
|
||||
const sortedNodes = new Array<CallerCalleeNode>();
|
||||
for (const [_, node] of nodes) {
|
||||
sortedNodes.push(node);
|
||||
}
|
||||
return sortedNodes.sort((n1, n2) => {
|
||||
return compareFrameGroup(n1.frameGroup, n2.frameGroup);
|
||||
if (n1.Samples > n2.Samples) {
|
||||
return -1;
|
||||
}
|
||||
if (n1.Samples < n2.Samples) {
|
||||
return 1;
|
||||
}
|
||||
return n1.FrameGroupID.localeCompare(n2.FrameGroupID);
|
||||
});
|
||||
}
|
||||
|
||||
// fromCallerCalleeIntermediateNode is used to convert the intermediate representation
|
||||
// of the diagram into the format that is easily JSONified and more easily consumed by
|
||||
// others.
|
||||
export function fromCallerCalleeIntermediateNode(
|
||||
root: CallerCalleeIntermediateNode
|
||||
): CallerCalleeNode {
|
||||
const node = createCallerCalleeNode({
|
||||
FrameGroupID: root.frameGroupID,
|
||||
Samples: root.samples,
|
||||
CountInclusive: root.countInclusive,
|
||||
CountExclusive: root.countExclusive,
|
||||
});
|
||||
|
||||
// Populate the other fields with data from the root node. Selectors are not supposed
|
||||
// to be able to fail.
|
||||
selectCallerCalleeData(root.frameMetadata, node);
|
||||
|
||||
// Now fill the caller and callee arrays.
|
||||
// For a deterministic result we have to walk the callers / callees in a deterministic
|
||||
// order. A deterministic result allows deterministic UI views, something that users expect.
|
||||
for (const caller of sortNodes(root.callers)) {
|
||||
node.Callers.push(fromCallerCalleeIntermediateNode(caller));
|
||||
}
|
||||
for (const callee of sortNodes(root.callees)) {
|
||||
node.Callees.push(fromCallerCalleeIntermediateNode(callee));
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
export function createCallerCalleeDiagram(
|
||||
events: Map<StackTraceID, number>,
|
||||
stackTraces: Map<StackTraceID, StackTrace>,
|
||||
stackFrames: Map<StackFrameID, StackFrame>,
|
||||
executables: Map<FileID, Executable>
|
||||
): CallerCalleeNode {
|
||||
const rootFrame = createStackFrameMetadata();
|
||||
const frameMetadataForTraces = groupStackFrameMetadataByStackTrace(
|
||||
stackTraces,
|
||||
stackFrames,
|
||||
executables
|
||||
);
|
||||
const root = createCallerCalleeIntermediateRoot(rootFrame, events, frameMetadataForTraces);
|
||||
return fromCallerCalleeIntermediateNode(root);
|
||||
}
|
||||
|
|
|
@ -6,16 +6,8 @@
|
|||
*/
|
||||
|
||||
import fnv from 'fnv-plus';
|
||||
import { CallerCalleeNode, createCallerCalleeDiagram } from './callercallee';
|
||||
import {
|
||||
describeFrameType,
|
||||
Executable,
|
||||
FileID,
|
||||
StackFrame,
|
||||
StackFrameID,
|
||||
StackTrace,
|
||||
StackTraceID,
|
||||
} from './profiling';
|
||||
import { CallerCalleeGraph, sortCallerCalleeNodes } from './callercallee';
|
||||
import { getCalleeLabel } from './profiling';
|
||||
|
||||
interface ColumnarCallerCallee {
|
||||
Label: string[];
|
||||
|
@ -30,7 +22,7 @@ interface ColumnarCallerCallee {
|
|||
ExecutableID: string[];
|
||||
}
|
||||
|
||||
export interface ElasticFlameGraph {
|
||||
export interface FlameGraph {
|
||||
Label: string[];
|
||||
Value: number[];
|
||||
Position: number[];
|
||||
|
@ -41,6 +33,9 @@ export interface ElasticFlameGraph {
|
|||
ID: string[];
|
||||
FrameID: string[];
|
||||
ExecutableID: string[];
|
||||
}
|
||||
|
||||
export interface ElasticFlameGraph extends FlameGraph {
|
||||
TotalSeconds: number;
|
||||
TotalTraces: number;
|
||||
SampledTraces: number;
|
||||
|
@ -99,205 +94,110 @@ function normalize(n: number, lower: number, upper: number): number {
|
|||
return (n - lower) / (upper - lower);
|
||||
}
|
||||
|
||||
function checkIfStringHasParentheses(s: string) {
|
||||
return /\(|\)/.test(s);
|
||||
}
|
||||
// createColumnarCallerCallee flattens the intermediate representation of the diagram
|
||||
// into a columnar format that is more compact than JSON. This representation will later
|
||||
// need to be normalized into the response ultimately consumed by the flamegraph.
|
||||
export function createColumnarCallerCallee(graph: CallerCalleeGraph): ColumnarCallerCallee {
|
||||
const numCallees = graph.size;
|
||||
const columnar: ColumnarCallerCallee = {
|
||||
Label: new Array<string>(numCallees),
|
||||
Value: new Array<number>(numCallees),
|
||||
X: new Array<number>(numCallees),
|
||||
Y: new Array<number>(numCallees),
|
||||
Color: new Array<number>(numCallees * 4),
|
||||
CountInclusive: new Array<number>(numCallees),
|
||||
CountExclusive: new Array<number>(numCallees),
|
||||
ID: new Array<string>(numCallees),
|
||||
FrameID: new Array<string>(numCallees),
|
||||
ExecutableID: new Array<string>(numCallees),
|
||||
};
|
||||
|
||||
function getFunctionName(node: CallerCalleeNode) {
|
||||
return node.FunctionName !== '' && !checkIfStringHasParentheses(node.FunctionName)
|
||||
? `${node.FunctionName}()`
|
||||
: node.FunctionName;
|
||||
}
|
||||
const queue = [{ x: 0, depth: 1, node: graph.root, parentID: 'root' }];
|
||||
|
||||
function getExeFileName(node: CallerCalleeNode) {
|
||||
if (node?.ExeFileName === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (node.ExeFileName !== '') {
|
||||
return node.ExeFileName;
|
||||
}
|
||||
return describeFrameType(node.FrameType);
|
||||
}
|
||||
let idx = 0;
|
||||
while (queue.length > 0) {
|
||||
const { x, depth, node, parentID } = queue.pop()!;
|
||||
|
||||
function getLabel(node: CallerCalleeNode) {
|
||||
if (node.FunctionName !== '') {
|
||||
const sourceFilename = node.SourceFilename;
|
||||
const sourceURL = sourceFilename ? sourceFilename.split('/').pop() : '';
|
||||
return `${getExeFileName(node)}: ${getFunctionName(node)} in ${sourceURL} #${node.SourceLine}`;
|
||||
}
|
||||
return getExeFileName(node);
|
||||
}
|
||||
|
||||
export class FlameGraph {
|
||||
// sampleRate is 1/5^N, with N being the downsampled index the events were fetched from.
|
||||
// N=0: full events table (sampleRate is 1)
|
||||
// N=1: downsampled by 5 (sampleRate is 0.2)
|
||||
// ...
|
||||
sampleRate: number;
|
||||
|
||||
// totalCount is the sum(Count) of all events in the filter range in the
|
||||
// downsampled index we were looking at.
|
||||
// To estimate how many events we have in the full events index: totalCount / sampleRate.
|
||||
// Do the same for single entries in the events array.
|
||||
totalCount: number;
|
||||
|
||||
totalSeconds: number;
|
||||
|
||||
events: Map<StackTraceID, number>;
|
||||
stacktraces: Map<StackTraceID, StackTrace>;
|
||||
stackframes: Map<StackFrameID, StackFrame>;
|
||||
executables: Map<FileID, Executable>;
|
||||
|
||||
constructor({
|
||||
sampleRate,
|
||||
totalCount,
|
||||
events,
|
||||
stackTraces,
|
||||
stackFrames,
|
||||
executables,
|
||||
totalSeconds,
|
||||
}: {
|
||||
sampleRate: number;
|
||||
totalCount: number;
|
||||
events: Map<StackTraceID, number>;
|
||||
stackTraces: Map<StackTraceID, StackTrace>;
|
||||
stackFrames: Map<StackFrameID, StackFrame>;
|
||||
executables: Map<FileID, Executable>;
|
||||
totalSeconds: number;
|
||||
}) {
|
||||
this.sampleRate = sampleRate;
|
||||
this.totalCount = totalCount;
|
||||
this.events = events;
|
||||
this.stacktraces = stackTraces;
|
||||
this.stackframes = stackFrames;
|
||||
this.executables = executables;
|
||||
this.totalSeconds = totalSeconds;
|
||||
}
|
||||
|
||||
private countCallees(root: CallerCalleeNode): number {
|
||||
let numCallees = 1;
|
||||
for (const callee of root.Callees) {
|
||||
numCallees += this.countCallees(callee);
|
||||
if (x === 0 && depth === 1) {
|
||||
columnar.Label[idx] = 'root: Represents 100% of CPU time.';
|
||||
} else {
|
||||
columnar.Label[idx] = getCalleeLabel(node.FrameMetadata);
|
||||
}
|
||||
return numCallees;
|
||||
}
|
||||
columnar.Value[idx] = node.Samples;
|
||||
columnar.X[idx] = x;
|
||||
columnar.Y[idx] = depth;
|
||||
|
||||
// createColumnarCallerCallee flattens the intermediate representation of the diagram
|
||||
// into a columnar format that is more compact than JSON. This representation will later
|
||||
// need to be normalized into the response ultimately consumed by the flamegraph.
|
||||
private createColumnarCallerCallee(root: CallerCalleeNode): ColumnarCallerCallee {
|
||||
const numCallees = this.countCallees(root);
|
||||
const columnar: ColumnarCallerCallee = {
|
||||
Label: new Array<string>(numCallees),
|
||||
Value: new Array<number>(numCallees),
|
||||
X: new Array<number>(numCallees),
|
||||
Y: new Array<number>(numCallees),
|
||||
Color: new Array<number>(numCallees * 4),
|
||||
CountInclusive: new Array<number>(numCallees),
|
||||
CountExclusive: new Array<number>(numCallees),
|
||||
ID: new Array<string>(numCallees),
|
||||
FrameID: new Array<string>(numCallees),
|
||||
ExecutableID: new Array<string>(numCallees),
|
||||
};
|
||||
const [red, green, blue, alpha] = rgbToRGBA(frameTypeToRGB(node.FrameMetadata.FrameType, x));
|
||||
const j = 4 * idx;
|
||||
columnar.Color[j] = red;
|
||||
columnar.Color[j + 1] = green;
|
||||
columnar.Color[j + 2] = blue;
|
||||
columnar.Color[j + 3] = alpha;
|
||||
|
||||
const queue = [{ x: 0, depth: 1, node: root, parentID: 'root' }];
|
||||
columnar.CountInclusive[idx] = node.CountInclusive;
|
||||
columnar.CountExclusive[idx] = node.CountExclusive;
|
||||
|
||||
let idx = 0;
|
||||
while (queue.length > 0) {
|
||||
const { x, depth, node, parentID } = queue.pop()!;
|
||||
const id = fnv.fast1a64utf(`${parentID}${node.FrameGroupID}`).toString();
|
||||
|
||||
if (x === 0 && depth === 1) {
|
||||
columnar.Label[idx] = 'root: Represents 100% of CPU time.';
|
||||
} else {
|
||||
columnar.Label[idx] = getLabel(node);
|
||||
}
|
||||
columnar.Value[idx] = node.Samples;
|
||||
columnar.X[idx] = x;
|
||||
columnar.Y[idx] = depth;
|
||||
columnar.ID[idx] = id;
|
||||
columnar.FrameID[idx] = node.FrameMetadata.FrameID;
|
||||
columnar.ExecutableID[idx] = node.FrameMetadata.FileID;
|
||||
|
||||
const [red, green, blue, alpha] = rgbToRGBA(frameTypeToRGB(node.FrameType, x));
|
||||
const j = 4 * idx;
|
||||
columnar.Color[j] = red;
|
||||
columnar.Color[j + 1] = green;
|
||||
columnar.Color[j + 2] = blue;
|
||||
columnar.Color[j + 3] = alpha;
|
||||
// For a deterministic result we have to walk the callers / callees in a deterministic
|
||||
// order. A deterministic result allows deterministic UI views, something that users expect.
|
||||
const callees = sortCallerCalleeNodes(node.Callees);
|
||||
|
||||
columnar.CountInclusive[idx] = node.CountInclusive;
|
||||
columnar.CountExclusive[idx] = node.CountExclusive;
|
||||
|
||||
const id = fnv.fast1a64utf(`${parentID}${node.FrameGroupID}`).toString();
|
||||
|
||||
columnar.ID[idx] = id;
|
||||
columnar.FrameID[idx] = node.FrameID;
|
||||
columnar.ExecutableID[idx] = node.FileID;
|
||||
|
||||
node.Callees.sort((a: CallerCalleeNode, b: CallerCalleeNode) => b.Samples - a.Samples);
|
||||
|
||||
let delta = 0;
|
||||
for (const callee of node.Callees) {
|
||||
delta += callee.Samples;
|
||||
}
|
||||
|
||||
for (let i = node.Callees.length - 1; i >= 0; i--) {
|
||||
delta -= node.Callees[i].Samples;
|
||||
queue.push({ x: x + delta, depth: depth + 1, node: node.Callees[i], parentID: id });
|
||||
}
|
||||
|
||||
idx++;
|
||||
let delta = 0;
|
||||
for (const callee of callees) {
|
||||
delta += callee.Samples;
|
||||
}
|
||||
|
||||
return columnar;
|
||||
}
|
||||
|
||||
// createElasticFlameGraph normalizes the intermediate columnar representation into the
|
||||
// response ultimately consumed by the flamegraph.
|
||||
private createElasticFlameGraph(columnar: ColumnarCallerCallee): ElasticFlameGraph {
|
||||
const graph: ElasticFlameGraph = {
|
||||
Label: [],
|
||||
Value: [],
|
||||
Position: [],
|
||||
Size: [],
|
||||
Color: [],
|
||||
CountInclusive: [],
|
||||
CountExclusive: [],
|
||||
ID: [],
|
||||
FrameID: [],
|
||||
ExecutableID: [],
|
||||
TotalSeconds: this.totalSeconds,
|
||||
TotalTraces: Math.floor(this.totalCount / this.sampleRate),
|
||||
SampledTraces: this.totalCount,
|
||||
};
|
||||
|
||||
graph.Label = columnar.Label;
|
||||
graph.Value = columnar.Value;
|
||||
graph.Color = columnar.Color;
|
||||
graph.CountInclusive = columnar.CountInclusive;
|
||||
graph.CountExclusive = columnar.CountExclusive;
|
||||
graph.ID = columnar.ID;
|
||||
graph.FrameID = columnar.FrameID;
|
||||
graph.ExecutableID = columnar.ExecutableID;
|
||||
|
||||
const maxX = columnar.Value[0];
|
||||
const maxY = columnar.Y.reduce((max, n) => (n > max ? n : max), 0);
|
||||
|
||||
for (let i = 0; i < columnar.X.length; i++) {
|
||||
const x = normalize(columnar.X[i], 0, maxX);
|
||||
const y = normalize(maxY - columnar.Y[i], 0, maxY);
|
||||
graph.Position.push(x, y);
|
||||
for (let i = callees.length - 1; i >= 0; i--) {
|
||||
delta -= callees[i].Samples;
|
||||
queue.push({ x: x + delta, depth: depth + 1, node: callees[i], parentID: id });
|
||||
}
|
||||
|
||||
graph.Size = graph.Value.map((n) => normalize(n, 0, maxX));
|
||||
|
||||
return graph;
|
||||
idx++;
|
||||
}
|
||||
|
||||
toElastic(): ElasticFlameGraph {
|
||||
const root = createCallerCalleeDiagram(
|
||||
this.events,
|
||||
this.stacktraces,
|
||||
this.stackframes,
|
||||
this.executables
|
||||
);
|
||||
return this.createElasticFlameGraph(this.createColumnarCallerCallee(root));
|
||||
}
|
||||
return columnar;
|
||||
}
|
||||
|
||||
// createFlameGraph normalizes the intermediate columnar representation into the
|
||||
// response ultimately consumed by the flamegraph in the UI.
|
||||
export function createFlameGraph(columnar: ColumnarCallerCallee): FlameGraph {
|
||||
const graph: FlameGraph = {
|
||||
Label: [],
|
||||
Value: [],
|
||||
Position: [],
|
||||
Size: [],
|
||||
Color: [],
|
||||
CountInclusive: [],
|
||||
CountExclusive: [],
|
||||
ID: [],
|
||||
FrameID: [],
|
||||
ExecutableID: [],
|
||||
};
|
||||
|
||||
graph.Label = columnar.Label;
|
||||
graph.Value = columnar.Value;
|
||||
graph.Color = columnar.Color;
|
||||
graph.CountInclusive = columnar.CountInclusive;
|
||||
graph.CountExclusive = columnar.CountExclusive;
|
||||
graph.ID = columnar.ID;
|
||||
graph.FrameID = columnar.FrameID;
|
||||
graph.ExecutableID = columnar.ExecutableID;
|
||||
|
||||
const maxX = columnar.Value[0];
|
||||
const maxY = columnar.Y.reduce((max, n) => (n > max ? n : max), 0);
|
||||
|
||||
for (let i = 0; i < columnar.X.length; i++) {
|
||||
const x = normalize(columnar.X[i], 0, maxX);
|
||||
const y = normalize(maxY - columnar.Y[i], 0, maxY);
|
||||
graph.Position.push(x, y);
|
||||
}
|
||||
|
||||
graph.Size = graph.Value.map((n) => normalize(n, 0, maxX));
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
|
|
@ -5,161 +5,27 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { compareFrameGroup, createFrameGroup, createFrameGroupID } from './frame_group';
|
||||
import { createStackFrameMetadata } from './profiling';
|
||||
import { createFrameGroup, createFrameGroupID } from './frame_group';
|
||||
|
||||
const nonSymbolizedFrameGroups = [
|
||||
createFrameGroup(
|
||||
createStackFrameMetadata({
|
||||
FileID: '0x0123456789ABCDEF',
|
||||
AddressOrLine: 102938,
|
||||
})
|
||||
),
|
||||
createFrameGroup(
|
||||
createStackFrameMetadata({
|
||||
FileID: '0x0123456789ABCDEF',
|
||||
AddressOrLine: 1234,
|
||||
})
|
||||
),
|
||||
createFrameGroup(
|
||||
createStackFrameMetadata({
|
||||
FileID: '0x0102030405060708',
|
||||
AddressOrLine: 1234,
|
||||
})
|
||||
),
|
||||
createFrameGroup('0x0123456789ABCDEF', 102938, '', '', ''),
|
||||
createFrameGroup('0x0123456789ABCDEF', 1234, '', '', ''),
|
||||
createFrameGroup('0x0102030405060708', 1234, '', '', ''),
|
||||
];
|
||||
|
||||
const elfSymbolizedFrameGroups = [
|
||||
createFrameGroup(
|
||||
createStackFrameMetadata({
|
||||
FileID: '0x0123456789ABCDEF',
|
||||
ExeFileName: 'libc',
|
||||
FunctionName: 'strlen()',
|
||||
})
|
||||
),
|
||||
createFrameGroup(
|
||||
createStackFrameMetadata({
|
||||
FileID: '0xFEDCBA9876543210',
|
||||
ExeFileName: 'libc',
|
||||
FunctionName: 'strtok()',
|
||||
})
|
||||
),
|
||||
createFrameGroup(
|
||||
createStackFrameMetadata({
|
||||
FileID: '0xFEDCBA9876543210',
|
||||
ExeFileName: 'myapp',
|
||||
FunctionName: 'main()',
|
||||
})
|
||||
),
|
||||
createFrameGroup('0x0123456789ABCDEF', 0, 'libc', '', 'strlen()'),
|
||||
createFrameGroup('0xFEDCBA9876543210', 0, 'libc', '', 'strtok()'),
|
||||
createFrameGroup('0xFEDCBA9876543210', 0, 'myapp', '', 'main()'),
|
||||
];
|
||||
|
||||
const symbolizedFrameGroups = [
|
||||
createFrameGroup(
|
||||
createStackFrameMetadata({
|
||||
ExeFileName: 'chrome',
|
||||
SourceFilename: 'strlen()',
|
||||
FunctionName: 'strlen()',
|
||||
})
|
||||
),
|
||||
createFrameGroup(
|
||||
createStackFrameMetadata({
|
||||
ExeFileName: 'dockerd',
|
||||
SourceFilename: 'main()',
|
||||
FunctionName: 'createTask()',
|
||||
})
|
||||
),
|
||||
createFrameGroup(
|
||||
createStackFrameMetadata({
|
||||
ExeFileName: 'oom_reaper',
|
||||
SourceFilename: 'main()',
|
||||
FunctionName: 'crash()',
|
||||
})
|
||||
),
|
||||
createFrameGroup('', 0, 'chrome', 'strlen()', 'strlen()'),
|
||||
createFrameGroup('', 0, 'dockerd', 'main()', 'createTask()'),
|
||||
createFrameGroup('', 0, 'oom_reaper', 'main()', 'crash()'),
|
||||
];
|
||||
|
||||
describe('Frame group operations', () => {
|
||||
describe('check if a non-symbolized frame group is', () => {
|
||||
test('less than another non-symbolized frame group', () => {
|
||||
expect(compareFrameGroup(nonSymbolizedFrameGroups[1], nonSymbolizedFrameGroups[0])).toEqual(
|
||||
-1
|
||||
);
|
||||
});
|
||||
|
||||
test('equal to another non-symbolized frame group', () => {
|
||||
expect(compareFrameGroup(nonSymbolizedFrameGroups[0], nonSymbolizedFrameGroups[0])).toEqual(
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
test('greater than another non-symbolized frame group', () => {
|
||||
expect(compareFrameGroup(nonSymbolizedFrameGroups[1], nonSymbolizedFrameGroups[2])).toEqual(
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
test('less than an ELF-symbolized frame group', () => {
|
||||
expect(compareFrameGroup(nonSymbolizedFrameGroups[1], elfSymbolizedFrameGroups[0])).toEqual(
|
||||
-1
|
||||
);
|
||||
});
|
||||
|
||||
test('less than a symbolized frame group', () => {
|
||||
expect(compareFrameGroup(nonSymbolizedFrameGroups[1], symbolizedFrameGroups[0])).toEqual(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('check if an ELF-symbolized frame group is', () => {
|
||||
test('less than another ELF-symbolized frame group', () => {
|
||||
expect(compareFrameGroup(elfSymbolizedFrameGroups[0], elfSymbolizedFrameGroups[1])).toEqual(
|
||||
-1
|
||||
);
|
||||
});
|
||||
|
||||
test('equal to another ELF-symbolized frame group', () => {
|
||||
expect(compareFrameGroup(elfSymbolizedFrameGroups[0], elfSymbolizedFrameGroups[0])).toEqual(
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
test('greater than another ELF-symbolized frame group', () => {
|
||||
expect(compareFrameGroup(elfSymbolizedFrameGroups[1], elfSymbolizedFrameGroups[0])).toEqual(
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
test('greater than a non-symbolized frame group', () => {
|
||||
expect(compareFrameGroup(elfSymbolizedFrameGroups[0], nonSymbolizedFrameGroups[0])).toEqual(
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
test('less than a symbolized frame group', () => {
|
||||
expect(compareFrameGroup(elfSymbolizedFrameGroups[2], symbolizedFrameGroups[0])).toEqual(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('check if a symbolized frame group is', () => {
|
||||
test('less than another symbolized frame group', () => {
|
||||
expect(compareFrameGroup(symbolizedFrameGroups[0], symbolizedFrameGroups[1])).toEqual(-1);
|
||||
});
|
||||
|
||||
test('equal to another symbolized frame group', () => {
|
||||
expect(compareFrameGroup(symbolizedFrameGroups[0], symbolizedFrameGroups[0])).toEqual(0);
|
||||
});
|
||||
|
||||
test('greater than another symbolized frame group', () => {
|
||||
expect(compareFrameGroup(symbolizedFrameGroups[1], symbolizedFrameGroups[0])).toEqual(1);
|
||||
});
|
||||
|
||||
test('greater than a non-symbolized frame group', () => {
|
||||
expect(compareFrameGroup(symbolizedFrameGroups[0], nonSymbolizedFrameGroups[0])).toEqual(1);
|
||||
});
|
||||
|
||||
test('greater than an ELF-symbolized frame group', () => {
|
||||
expect(compareFrameGroup(symbolizedFrameGroups[0], elfSymbolizedFrameGroups[2])).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('check serialization for', () => {
|
||||
test('non-symbolized frame', () => {
|
||||
expect(createFrameGroupID(nonSymbolizedFrameGroups[0])).toEqual(
|
||||
|
|
|
@ -47,113 +47,38 @@ export type FrameGroup = EmptyFrameGroup | ElfFrameGroup | FullFrameGroup;
|
|||
// For ELF-symbolized frames, group by FunctionName, ExeFileName and FileID.
|
||||
// For non-symbolized frames, group by FileID and AddressOrLine.
|
||||
// otherwise group by ExeFileName, SourceFilename and FunctionName.
|
||||
export function createFrameGroup(frame: StackFrameMetadata): FrameGroup {
|
||||
if (frame.FunctionName === '') {
|
||||
export function createFrameGroup(
|
||||
fileID: StackFrameMetadata['FileID'],
|
||||
addressOrLine: StackFrameMetadata['AddressOrLine'],
|
||||
exeFilename: StackFrameMetadata['ExeFileName'],
|
||||
sourceFilename: StackFrameMetadata['SourceFilename'],
|
||||
functionName: StackFrameMetadata['FunctionName']
|
||||
): FrameGroup {
|
||||
if (functionName === '') {
|
||||
return {
|
||||
name: FrameGroupName.EMPTY,
|
||||
fileID: frame.FileID,
|
||||
addressOrLine: frame.AddressOrLine,
|
||||
fileID,
|
||||
addressOrLine,
|
||||
} as EmptyFrameGroup;
|
||||
}
|
||||
|
||||
if (frame.SourceFilename === '') {
|
||||
if (sourceFilename === '') {
|
||||
return {
|
||||
name: FrameGroupName.ELF,
|
||||
fileID: frame.FileID,
|
||||
exeFilename: frame.ExeFileName,
|
||||
functionName: frame.FunctionName,
|
||||
fileID,
|
||||
exeFilename,
|
||||
functionName,
|
||||
} as ElfFrameGroup;
|
||||
}
|
||||
|
||||
return {
|
||||
name: FrameGroupName.FULL,
|
||||
exeFilename: frame.ExeFileName,
|
||||
functionName: frame.FunctionName,
|
||||
sourceFilename: frame.SourceFilename,
|
||||
exeFilename,
|
||||
functionName,
|
||||
sourceFilename,
|
||||
} as FullFrameGroup;
|
||||
}
|
||||
|
||||
// compareFrameGroup compares any two frame groups
|
||||
//
|
||||
// In general, frame groups are ordered using the following steps:
|
||||
//
|
||||
// * If frame groups are the same type, then we compare using their same
|
||||
// properties
|
||||
// * If frame groups have different types, then we compare using overlapping
|
||||
// properties
|
||||
// * If frame groups do not share properties, then we compare using the frame
|
||||
// group type
|
||||
//
|
||||
// The union of the properties across all frame group types are ordered below
|
||||
// from highest to lowest. For instance, given any two frame groups, shared
|
||||
// properties are compared in the given order:
|
||||
//
|
||||
// * exeFilename
|
||||
// * sourceFilename
|
||||
// * functionName
|
||||
// * fileID
|
||||
// * addressOrLine
|
||||
//
|
||||
// Frame group types are ordered according to how much symbolization metadata
|
||||
// is available, starting from most to least:
|
||||
//
|
||||
// * Symbolized frame group
|
||||
// * ELF-symbolized frame group
|
||||
// * Unsymbolized frame group
|
||||
export function compareFrameGroup(a: FrameGroup, b: FrameGroup): number {
|
||||
if (a.name === FrameGroupName.EMPTY) {
|
||||
if (b.name === FrameGroupName.EMPTY) {
|
||||
if (a.fileID < b.fileID) return -1;
|
||||
if (a.fileID > b.fileID) return 1;
|
||||
if (a.addressOrLine < b.addressOrLine) return -1;
|
||||
if (a.addressOrLine > b.addressOrLine) return 1;
|
||||
return 0;
|
||||
}
|
||||
if (b.name === FrameGroupName.ELF) {
|
||||
if (a.fileID < b.fileID) return -1;
|
||||
if (a.fileID > b.fileID) return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.name === FrameGroupName.ELF) {
|
||||
if (b.name === FrameGroupName.EMPTY) {
|
||||
if (a.fileID < b.fileID) return -1;
|
||||
if (a.fileID > b.fileID) return 1;
|
||||
return 1;
|
||||
}
|
||||
if (b.name === FrameGroupName.ELF) {
|
||||
if (a.functionName < b.functionName) return -1;
|
||||
if (a.functionName > b.functionName) return 1;
|
||||
if (a.exeFilename < b.exeFilename) return -1;
|
||||
if (a.exeFilename > b.exeFilename) return 1;
|
||||
return 0;
|
||||
}
|
||||
if (a.functionName < b.functionName) return -1;
|
||||
if (a.functionName > b.functionName) return 1;
|
||||
if (a.exeFilename < b.exeFilename) return -1;
|
||||
if (a.exeFilename > b.exeFilename) return 1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b.name === FrameGroupName.FULL) {
|
||||
if (a.exeFilename < b.exeFilename) return -1;
|
||||
if (a.exeFilename > b.exeFilename) return 1;
|
||||
if (a.sourceFilename < b.sourceFilename) return -1;
|
||||
if (a.sourceFilename > b.sourceFilename) return 1;
|
||||
if (a.functionName < b.functionName) return -1;
|
||||
if (a.functionName > b.functionName) return 1;
|
||||
return 0;
|
||||
}
|
||||
if (b.name === FrameGroupName.ELF) {
|
||||
if (a.functionName < b.functionName) return -1;
|
||||
if (a.functionName > b.functionName) return 1;
|
||||
if (a.exeFilename < b.exeFilename) return -1;
|
||||
if (a.exeFilename > b.exeFilename) return 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
export function createFrameGroupID(frameGroup: FrameGroup): FrameGroupID {
|
||||
switch (frameGroup.name) {
|
||||
case FrameGroupName.EMPTY:
|
||||
|
|
|
@ -5,17 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { createFrameGroup, createFrameGroupID, FrameGroupID } from './frame_group';
|
||||
import {
|
||||
compareFrameGroup,
|
||||
createFrameGroup,
|
||||
createFrameGroupID,
|
||||
FrameGroup,
|
||||
FrameGroupID,
|
||||
} from './frame_group';
|
||||
import {
|
||||
createStackFrameMetadata,
|
||||
emptyStackTrace,
|
||||
Executable,
|
||||
FileID,
|
||||
groupStackFrameMetadataByStackTrace,
|
||||
StackFrame,
|
||||
StackFrameID,
|
||||
StackFrameMetadata,
|
||||
|
@ -25,7 +20,7 @@ import {
|
|||
|
||||
interface TopNFunctionAndFrameGroup {
|
||||
Frame: StackFrameMetadata;
|
||||
FrameGroup: FrameGroup;
|
||||
FrameGroupID: FrameGroupID;
|
||||
CountExclusive: number;
|
||||
CountInclusive: number;
|
||||
}
|
||||
|
@ -48,8 +43,6 @@ export function createTopNFunctions(
|
|||
startIndex: number,
|
||||
endIndex: number
|
||||
): TopNFunctions {
|
||||
const metadata = groupStackFrameMetadataByStackTrace(stackTraces, stackFrames, executables);
|
||||
|
||||
// The `count` associated with a frame provides the total number of
|
||||
// traces in which that node has appeared at least once. However, a
|
||||
// frame may appear multiple times in a trace, and thus to avoid
|
||||
|
@ -59,35 +52,63 @@ export function createTopNFunctions(
|
|||
const topNFunctions = new Map<FrameGroupID, TopNFunctionAndFrameGroup>();
|
||||
|
||||
// Collect metadata and inclusive + exclusive counts for each distinct frame.
|
||||
for (const [traceHash, count] of events) {
|
||||
for (const [stackTraceID, count] of events) {
|
||||
const uniqueFrameGroupsPerEvent = new Set<FrameGroupID>();
|
||||
|
||||
totalCount += count;
|
||||
|
||||
// It is possible that we do not have a stacktrace for an event,
|
||||
// e.g. when stopping the host agent or on network errors.
|
||||
const frames = metadata.get(traceHash) ?? [];
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
const frameGroup = createFrameGroup(frames[i]);
|
||||
const stackTrace = stackTraces.get(stackTraceID) ?? emptyStackTrace;
|
||||
const lenStackTrace = stackTrace.FrameIDs.length;
|
||||
|
||||
for (let i = 0; i < lenStackTrace; i++) {
|
||||
const frameID = stackTrace.FrameIDs[i];
|
||||
const fileID = stackTrace.FileIDs[i];
|
||||
const addressOrLine = stackTrace.AddressOrLines[i];
|
||||
const frame = stackFrames.get(frameID)!;
|
||||
const executable = executables.get(fileID)!;
|
||||
|
||||
const frameGroup = createFrameGroup(
|
||||
fileID,
|
||||
addressOrLine,
|
||||
executable.FileName,
|
||||
frame.FileName,
|
||||
frame.FunctionName
|
||||
);
|
||||
const frameGroupID = createFrameGroupID(frameGroup);
|
||||
|
||||
if (!topNFunctions.has(frameGroupID)) {
|
||||
topNFunctions.set(frameGroupID, {
|
||||
Frame: frames[i],
|
||||
FrameGroup: frameGroup,
|
||||
let topNFunction = topNFunctions.get(frameGroupID);
|
||||
|
||||
if (topNFunction === undefined) {
|
||||
const metadata = createStackFrameMetadata({
|
||||
FrameID: frameID,
|
||||
FileID: fileID,
|
||||
AddressOrLine: addressOrLine,
|
||||
FrameType: stackTrace.Types[i],
|
||||
FunctionName: frame.FunctionName,
|
||||
FunctionOffset: frame.FunctionOffset,
|
||||
SourceLine: frame.LineNumber,
|
||||
SourceFilename: frame.FileName,
|
||||
ExeFileName: executable.FileName,
|
||||
});
|
||||
|
||||
topNFunction = {
|
||||
Frame: metadata,
|
||||
FrameGroupID: frameGroupID,
|
||||
CountExclusive: 0,
|
||||
CountInclusive: 0,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const topNFunction = topNFunctions.get(frameGroupID)!;
|
||||
topNFunctions.set(frameGroupID, topNFunction);
|
||||
}
|
||||
|
||||
if (!uniqueFrameGroupsPerEvent.has(frameGroupID)) {
|
||||
uniqueFrameGroupsPerEvent.add(frameGroupID);
|
||||
topNFunction.CountInclusive += count;
|
||||
}
|
||||
|
||||
if (i === frames.length - 1) {
|
||||
if (i === lenStackTrace - 1) {
|
||||
// Leaf frame: sum up counts for exclusive CPU.
|
||||
topNFunction.CountExclusive += count;
|
||||
}
|
||||
|
@ -105,7 +126,7 @@ export function createTopNFunctions(
|
|||
if (a.CountExclusive < b.CountExclusive) {
|
||||
return -1;
|
||||
}
|
||||
return compareFrameGroup(a.FrameGroup, b.FrameGroup);
|
||||
return a.FrameGroupID.localeCompare(b.FrameGroupID);
|
||||
})
|
||||
.reverse();
|
||||
|
||||
|
@ -121,7 +142,7 @@ export function createTopNFunctions(
|
|||
Frame: frameAndCount.Frame,
|
||||
CountExclusive: frameAndCount.CountExclusive,
|
||||
CountInclusive: frameAndCount.CountInclusive,
|
||||
Id: createFrameGroupID(frameAndCount.FrameGroup),
|
||||
Id: frameAndCount.FrameGroupID,
|
||||
}));
|
||||
|
||||
return {
|
||||
|
|
|
@ -50,7 +50,7 @@ describe('Stack frame metadata operations', () => {
|
|||
SourceFilename: 'dockerd',
|
||||
FunctionOffset: 0x183a5b0,
|
||||
});
|
||||
expect(getCalleeSource(metadata)).toEqual('dockerd+0x0');
|
||||
expect(getCalleeSource(metadata)).toEqual('dockerd');
|
||||
});
|
||||
|
||||
test('metadata has source name and function offset', () => {
|
||||
|
|
|
@ -54,6 +54,13 @@ export interface StackTrace {
|
|||
Types: number[];
|
||||
}
|
||||
|
||||
export const emptyStackTrace: StackTrace = {
|
||||
FrameIDs: [],
|
||||
FileIDs: [],
|
||||
AddressOrLines: [],
|
||||
Types: [],
|
||||
};
|
||||
|
||||
export interface StackFrame {
|
||||
FileName: string;
|
||||
FunctionName: string;
|
||||
|
@ -86,6 +93,8 @@ export interface StackFrameMetadata {
|
|||
SourceFilename: string;
|
||||
// StackFrame.LineNumber
|
||||
SourceLine: number;
|
||||
// auto-generated - see createStackFrameMetadata
|
||||
FunctionSourceLine: number;
|
||||
|
||||
// Executable.FileName
|
||||
ExeFileName: string;
|
||||
|
@ -123,9 +132,54 @@ export function createStackFrameMetadata(
|
|||
metadata.SourcePackageURL = options.SourcePackageURL ?? '';
|
||||
metadata.SourceType = options.SourceType ?? 0;
|
||||
|
||||
// Unknown/invalid offsets are currently set to 0.
|
||||
//
|
||||
// In this case we leave FunctionSourceLine=0 as a flag for the UI that the
|
||||
// FunctionSourceLine should not be displayed.
|
||||
//
|
||||
// As FunctionOffset=0 could also be a legit value, this work-around needs
|
||||
// a real fix. The idea for after GA is to change FunctionOffset=-1 to
|
||||
// indicate unknown/invalid.
|
||||
if (metadata.FunctionOffset > 0) {
|
||||
metadata.FunctionSourceLine = metadata.SourceLine - metadata.FunctionOffset;
|
||||
} else {
|
||||
metadata.FunctionSourceLine = 0;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
function checkIfStringHasParentheses(s: string) {
|
||||
return /\(|\)/.test(s);
|
||||
}
|
||||
|
||||
function getFunctionName(metadata: StackFrameMetadata) {
|
||||
return metadata.FunctionName !== '' && !checkIfStringHasParentheses(metadata.FunctionName)
|
||||
? `${metadata.FunctionName}()`
|
||||
: metadata.FunctionName;
|
||||
}
|
||||
|
||||
function getExeFileName(metadata: StackFrameMetadata) {
|
||||
if (metadata?.ExeFileName === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (metadata.ExeFileName !== '') {
|
||||
return metadata.ExeFileName;
|
||||
}
|
||||
return describeFrameType(metadata.FrameType);
|
||||
}
|
||||
|
||||
export function getCalleeLabel(metadata: StackFrameMetadata) {
|
||||
if (metadata.FunctionName !== '') {
|
||||
const sourceFilename = metadata.SourceFilename;
|
||||
const sourceURL = sourceFilename ? sourceFilename.split('/').pop() : '';
|
||||
return `${getExeFileName(metadata)}: ${getFunctionName(metadata)} in ${sourceURL} #${
|
||||
metadata.SourceLine
|
||||
}`;
|
||||
}
|
||||
return getExeFileName(metadata);
|
||||
}
|
||||
|
||||
export function getCalleeFunction(frame: StackFrameMetadata): string {
|
||||
// In the best case scenario, we have the file names, source lines,
|
||||
// and function names. However we need to deal with missing function or
|
||||
|
@ -137,7 +191,7 @@ export function getCalleeFunction(frame: StackFrameMetadata): string {
|
|||
}
|
||||
|
||||
export function getCalleeSource(frame: StackFrameMetadata): string {
|
||||
if (frame.FunctionName === '' && frame.SourceLine === 0) {
|
||||
if (frame.SourceFilename === '' && frame.SourceLine === 0) {
|
||||
if (frame.ExeFileName) {
|
||||
// If no source line or filename available, display the executable offset
|
||||
return frame.ExeFileName + '+0x' + frame.AddressOrLine.toString(16);
|
||||
|
@ -154,26 +208,23 @@ export function getCalleeSource(frame: StackFrameMetadata): string {
|
|||
return frame.SourceFilename + (frame.SourceLine !== 0 ? `#${frame.SourceLine}` : '');
|
||||
}
|
||||
|
||||
// groupStackFrameMetadataByStackTrace collects all of the per-stack-frame
|
||||
// metadata for a given set of trace IDs and their respective stack frames.
|
||||
//
|
||||
// This is similar to GetTraceMetaData in pf-storage-backend/storagebackend/storagebackendv1/reads_webservice.go
|
||||
export function groupStackFrameMetadataByStackTrace(
|
||||
stackTraces: Map<StackTraceID, StackTrace>,
|
||||
stackFrames: Map<StackFrameID, StackFrame>,
|
||||
executables: Map<FileID, Executable>
|
||||
): Map<StackTraceID, StackFrameMetadata[]> {
|
||||
const frameMetadataForTraces = new Map<StackTraceID, StackFrameMetadata[]>();
|
||||
): Record<string, StackFrameMetadata[]> {
|
||||
const stackTraceMap: Record<string, StackFrameMetadata[]> = {};
|
||||
for (const [stackTraceID, trace] of stackTraces) {
|
||||
const frameMetadata = new Array<StackFrameMetadata>();
|
||||
for (let i = 0; i < trace.FrameIDs.length; i++) {
|
||||
const numFramesPerTrace = trace.FrameIDs.length;
|
||||
const frameMetadata = new Array<StackFrameMetadata>(numFramesPerTrace);
|
||||
for (let i = 0; i < numFramesPerTrace; i++) {
|
||||
const frameID = trace.FrameIDs[i];
|
||||
const fileID = trace.FileIDs[i];
|
||||
const addressOrLine = trace.AddressOrLines[i];
|
||||
const frame = stackFrames.get(frameID)!;
|
||||
const executable = executables.get(fileID)!;
|
||||
|
||||
const metadata = createStackFrameMetadata({
|
||||
frameMetadata[i] = createStackFrameMetadata({
|
||||
FrameID: frameID,
|
||||
FileID: fileID,
|
||||
AddressOrLine: addressOrLine,
|
||||
|
@ -184,10 +235,8 @@ export function groupStackFrameMetadataByStackTrace(
|
|||
SourceFilename: frame.FileName,
|
||||
ExeFileName: executable.FileName,
|
||||
});
|
||||
|
||||
frameMetadata.push(metadata);
|
||||
}
|
||||
frameMetadataForTraces.set(stackTraceID, frameMetadata);
|
||||
stackTraceMap[stackTraceID] = frameMetadata;
|
||||
}
|
||||
return frameMetadataForTraces;
|
||||
return stackTraceMap;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,12 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { RouteRegisterParameters } from '.';
|
||||
import { getRoutePaths } from '../../common';
|
||||
import { FlameGraph } from '../../common/flamegraph';
|
||||
import { createCallerCalleeGraph } from '../../common/callercallee';
|
||||
import {
|
||||
createColumnarCallerCallee,
|
||||
createFlameGraph,
|
||||
ElasticFlameGraph,
|
||||
} from '../../common/flamegraph';
|
||||
import { createProfilingEsClient } from '../utils/create_profiling_es_client';
|
||||
import { withProfilingSpan } from '../utils/with_profiling_span';
|
||||
import { getClient } from './compat';
|
||||
|
@ -39,6 +44,7 @@ export function registerFlameChartSearchRoute({ router, logger }: RouteRegisterP
|
|||
timeTo,
|
||||
kuery,
|
||||
});
|
||||
const totalSeconds = timeTo - timeFrom;
|
||||
|
||||
const { stackTraces, executables, stackFrames, eventsIndex, totalCount, stackTraceEvents } =
|
||||
await getExecutablesAndStackTraces({
|
||||
|
@ -48,23 +54,47 @@ export function registerFlameChartSearchRoute({ router, logger }: RouteRegisterP
|
|||
sampleSize: targetSampleSize,
|
||||
});
|
||||
|
||||
const flamegraph = await withProfilingSpan('collect_flamegraph', async () => {
|
||||
return new FlameGraph({
|
||||
sampleRate: eventsIndex.sampleRate,
|
||||
totalCount,
|
||||
events: stackTraceEvents,
|
||||
const flamegraph = await withProfilingSpan('create_flamegraph', async () => {
|
||||
const t0 = Date.now();
|
||||
const graph = createCallerCalleeGraph(
|
||||
stackTraceEvents,
|
||||
stackTraces,
|
||||
stackFrames,
|
||||
executables,
|
||||
totalSeconds: timeTo - timeFrom,
|
||||
}).toElastic();
|
||||
executables
|
||||
);
|
||||
logger.info(`creating caller-callee graph took ${Date.now() - t0} ms`);
|
||||
|
||||
const t1 = Date.now();
|
||||
const columnar = createColumnarCallerCallee(graph);
|
||||
logger.info(`creating columnar caller-callee graph took ${Date.now() - t1} ms`);
|
||||
|
||||
const t2 = Date.now();
|
||||
const fg = createFlameGraph(columnar);
|
||||
logger.info(`creating flamegraph took ${Date.now() - t2} ms`);
|
||||
|
||||
return fg;
|
||||
});
|
||||
|
||||
// sampleRate is 1/5^N, with N being the downsampled index the events were fetched from.
|
||||
// N=0: full events table (sampleRate is 1)
|
||||
// N=1: downsampled by 5 (sampleRate is 0.2)
|
||||
// ...
|
||||
|
||||
// totalCount is the sum(Count) of all events in the filter range in the
|
||||
// downsampled index we were looking at.
|
||||
// To estimate how many events we have in the full events index: totalCount / sampleRate.
|
||||
// Do the same for single entries in the events array.
|
||||
|
||||
const body: ElasticFlameGraph = {
|
||||
...flamegraph,
|
||||
TotalSeconds: totalSeconds,
|
||||
TotalTraces: Math.floor(totalCount / eventsIndex.sampleRate),
|
||||
SampledTraces: totalCount,
|
||||
};
|
||||
|
||||
logger.info('returning payload response to client');
|
||||
|
||||
return response.ok({
|
||||
body: flamegraph,
|
||||
});
|
||||
return response.ok({ body });
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return response.customError({
|
||||
|
|
|
@ -54,7 +54,8 @@ export function registerTopNFunctionsSearchRoute({ router, logger }: RouteRegist
|
|||
sampleSize: targetSampleSize,
|
||||
});
|
||||
|
||||
const topNFunctions = await withProfilingSpan('collect_topn_functions', async () => {
|
||||
const t0 = Date.now();
|
||||
const topNFunctions = await withProfilingSpan('create_topn_functions', async () => {
|
||||
return createTopNFunctions(
|
||||
stackTraceEvents,
|
||||
stackTraces,
|
||||
|
@ -64,6 +65,7 @@ export function registerTopNFunctionsSearchRoute({ router, logger }: RouteRegist
|
|||
endIndex
|
||||
);
|
||||
});
|
||||
logger.info(`creating topN functions took ${Date.now() - t0} ms`);
|
||||
|
||||
logger.info('returning payload response to client');
|
||||
|
||||
|
|
|
@ -50,13 +50,13 @@ export async function getExecutablesAndStackTraces({
|
|||
if (totalCount > sampleSize * 1.1) {
|
||||
p = sampleSize / totalCount;
|
||||
logger.info('downsampling events with p=' + p);
|
||||
const t0 = new Date().getTime();
|
||||
const t0 = Date.now();
|
||||
const downsampledTotalCount = downsampleEventsRandomly(
|
||||
stackTraceEvents,
|
||||
p,
|
||||
filter.toString()
|
||||
);
|
||||
logger.info(`downsampling events took ${new Date().getTime() - t0} ms`);
|
||||
logger.info(`downsampling events took ${Date.now() - t0} ms`);
|
||||
logger.info('downsampled total count: ' + downsampledTotalCount);
|
||||
logger.info('unique downsampled stacktraces: ' + stackTraceEvents.size);
|
||||
}
|
||||
|
|
|
@ -12,9 +12,9 @@ export async function logExecutionLatency<T>(
|
|||
activity: string,
|
||||
func: () => Promise<T>
|
||||
): Promise<T> {
|
||||
const start = new Date().getTime();
|
||||
const start = Date.now();
|
||||
return await func().then((res) => {
|
||||
logger.info(activity + ' took ' + (new Date().getTime() - start) + 'ms');
|
||||
logger.info(activity + ' took ' + (Date.now() - start) + 'ms');
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -269,7 +269,7 @@ export async function mgetStackTraces({
|
|||
const stackFrameDocIDs = new Set<string>();
|
||||
const executableDocIDs = new Set<string>();
|
||||
|
||||
const t0 = new Date().getTime();
|
||||
const t0 = Date.now();
|
||||
// flatMap() is significantly slower than an explicit for loop
|
||||
for (const res of stackResponses) {
|
||||
for (const trace of res.docs) {
|
||||
|
@ -298,7 +298,7 @@ export async function mgetStackTraces({
|
|||
}
|
||||
}
|
||||
}
|
||||
logger.info(`processing data took ${new Date().getTime() - t0} ms`);
|
||||
logger.info(`processing data took ${Date.now() - t0} ms`);
|
||||
|
||||
if (stackTraces.size !== 0) {
|
||||
logger.info('Average size of stacktrace: ' + totalFrames / stackTraces.size);
|
||||
|
@ -336,7 +336,7 @@ export async function mgetStackFrames({
|
|||
|
||||
// Create a lookup map StackFrameID -> StackFrame.
|
||||
let framesFound = 0;
|
||||
const t0 = new Date().getTime();
|
||||
const t0 = Date.now();
|
||||
const docs = resStackFrames.docs;
|
||||
for (const frame of docs) {
|
||||
if ('error' in frame) {
|
||||
|
@ -361,7 +361,7 @@ export async function mgetStackFrames({
|
|||
});
|
||||
}
|
||||
}
|
||||
logger.info(`processing data took ${new Date().getTime() - t0} ms`);
|
||||
logger.info(`processing data took ${Date.now() - t0} ms`);
|
||||
|
||||
logger.info('found ' + framesFound + ' / ' + stackFrameIDs.size + ' frames');
|
||||
|
||||
|
@ -391,7 +391,7 @@ export async function mgetExecutables({
|
|||
|
||||
// Create a lookup map StackFrameID -> StackFrame.
|
||||
let exeFound = 0;
|
||||
const t0 = new Date().getTime();
|
||||
const t0 = Date.now();
|
||||
const docs = resExecutables.docs;
|
||||
for (const exe of docs) {
|
||||
if ('error' in exe) {
|
||||
|
@ -408,7 +408,7 @@ export async function mgetExecutables({
|
|||
});
|
||||
}
|
||||
}
|
||||
logger.info(`processing data took ${new Date().getTime() - t0} ms`);
|
||||
logger.info(`processing data took ${Date.now() - t0} ms`);
|
||||
|
||||
logger.info('found ' + exeFound + ' / ' + executableIDs.size + ' executables');
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import type { IRouter, Logger } from '@kbn/core/server';
|
||||
import { RouteRegisterParameters } from '.';
|
||||
import { fromMapToRecord, getRoutePaths, INDEX_EVENTS } from '../../common';
|
||||
import { getRoutePaths, INDEX_EVENTS } from '../../common';
|
||||
import { ProfilingESField } from '../../common/elasticsearch';
|
||||
import { computeBucketWidthFromTimeRangeAndBucketCount } from '../../common/histogram';
|
||||
import { groupStackFrameMetadataByStackTrace, StackTraceID } from '../../common/profiling';
|
||||
|
@ -142,9 +142,7 @@ export async function topNElasticSearchQuery({
|
|||
);
|
||||
|
||||
const metadata = await withProfilingSpan('collect_stackframe_metadata', async () => {
|
||||
return fromMapToRecord(
|
||||
groupStackFrameMetadataByStackTrace(stackTraces, stackFrames, executables)
|
||||
);
|
||||
return groupStackFrameMetadataByStackTrace(stackTraces, stackFrames, executables);
|
||||
});
|
||||
|
||||
logger.info('returning payload response to client');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue