From 3aa4eaf01fbc3ee9b18cfb9ea38eb9980339110b Mon Sep 17 00:00:00 2001 From: Joseph Crail Date: Thu, 24 Mar 2022 17:46:31 -0700 Subject: [PATCH] Render Pixi flamegraph using Elasticsearch as data source (#44) * Add comments * Add stricter typing * Collate stack frame metadata * Split grouping frames and creating a hash defaultGroupBy previously did both and now we have separate methods. * Decode file ID from base64 URL * Rename creation methods We use creation methods to construct instances of certain types. Sometimes these instances allow for no arguments, thus, the created instance is an instance with sensible defaults. To make the intent clearer for readers and also to adhere to the conventions used throughout the Kibana codebase, I renamed the creation methods to use 'create*' instead of 'build*'. --- .../profiling/common/callercallee.test.ts | 54 +++++++ src/plugins/profiling/common/callercallee.ts | 147 +++++++++++------- src/plugins/profiling/common/flamegraph.ts | 54 +++++-- src/plugins/profiling/common/index.ts | 7 - .../profiling/common/profiling.test.ts | 29 ++-- src/plugins/profiling/common/profiling.ts | 131 +++++++++------- 6 files changed, 279 insertions(+), 143 deletions(-) create mode 100644 src/plugins/profiling/common/callercallee.test.ts diff --git a/src/plugins/profiling/common/callercallee.test.ts b/src/plugins/profiling/common/callercallee.test.ts new file mode 100644 index 00000000000000..310ec4210a5a00 --- /dev/null +++ b/src/plugins/profiling/common/callercallee.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + createCallerCalleeIntermediateNode, + fromCallerCalleeIntermediateNode, +} from './callercallee'; +import { createStackFrameMetadata, hashFrameGroup } from './profiling'; + +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', + Index: 1, + }); + const parent = createCallerCalleeIntermediateNode(parentFrame, 10); + + const childFrame = createStackFrameMetadata({ + FileID: '8d8696a4fd51fa88da70d3fde138247d', + FrameType: 3, + AddressOrLine: 67000, + FunctionName: 'epoll_poll', + SourceID: 'f0a7901dcefed6cc8992a324b9df733c', + SourceLine: 150, + ExeFileName: 'auditd', + Index: 0, + }); + const child = createCallerCalleeIntermediateNode(childFrame, 10); + + const root = createCallerCalleeIntermediateNode(createStackFrameMetadata(), 10); + root.callees.set(hashFrameGroup(child.frameGroup), child); + root.callees.set(hashFrameGroup(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); + }); +}); diff --git a/src/plugins/profiling/common/callercallee.ts b/src/plugins/profiling/common/callercallee.ts index 7cfca4197c74c6..5f9358140df033 100644 --- a/src/plugins/profiling/common/callercallee.ts +++ b/src/plugins/profiling/common/callercallee.ts @@ -6,44 +6,43 @@ * Side Public License, v 1. */ -import { override } from '.'; +import { clone } from 'lodash'; + import { - buildFrameGroup, compareFrameGroup, defaultGroupBy, FrameGroup, FrameGroupID, + hashFrameGroup, StackFrameMetadata, StackTraceID, } from './profiling'; -export interface CallerCalleeIntermediateNode { - nodeID: FrameGroup; +export type CallerCalleeIntermediateNode = { + frameGroup: FrameGroup; callers: Map; callees: Map; frameMetadata: Set; samples: number; -} +}; -export function buildCallerCalleeIntermediateNode( - frame: StackFrameMetadata, +export function createCallerCalleeIntermediateNode( + frameMetadata: StackFrameMetadata, samples: number ): CallerCalleeIntermediateNode { - let node: CallerCalleeIntermediateNode = { - nodeID: buildFrameGroup(), + return { + frameGroup: defaultGroupBy(frameMetadata), callers: new Map(), callees: new Map(), - frameMetadata: new Set(), + frameMetadata: new Set([frameMetadata]), samples: samples, }; - node.frameMetadata.add(frame); - return node; } -interface relevantTrace { +type relevantTrace = { frames: StackFrameMetadata[]; index: 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 @@ -59,7 +58,7 @@ function selectRelevantTraces( frames: Map ): Map { const result = new Map(); - const rootString = defaultGroupBy(rootFrame); + const rootString = hashFrameGroup(defaultGroupBy(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 @@ -74,7 +73,7 @@ function selectRelevantTraces( // 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 === defaultGroupBy(frameMetadata[i])) { + if (rootString === hashFrameGroup(defaultGroupBy(frameMetadata[i]))) { result.set(stackTraceID, { frames: frameMetadata, index: i, @@ -87,7 +86,7 @@ function selectRelevantTraces( } function sortRelevantTraces(relevantTraces: Map): StackTraceID[] { - const sortedRelevantTraces: StackTraceID[] = new Array(relevantTraces.size); + const sortedRelevantTraces = new Array(); for (const trace of relevantTraces.keys()) { sortedRelevantTraces.push(trace); } @@ -98,30 +97,58 @@ function sortRelevantTraces(relevantTraces: Map): S }); } -export function buildCallerCalleeIntermediateRoot( +// 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. +// +// 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, frames: Map ): CallerCalleeIntermediateNode { - const root = buildCallerCalleeIntermediateNode(rootFrame, 0); + // Create a node for the centered frame + const root = createCallerCalleeIntermediateNode(rootFrame, 0); + + // 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); + + // 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. + // + // We currently assume there are no callers. const callees = trace.frames; const samples = traces.get(traceHash)!; // Go through the callees, reverse iteration - let currentNode = root; + let currentNode = clone(root); + root.samples += samples; for (let i = callees.length - 1; i >= 0; i--) { const callee = callees[i]; - const calleeName = defaultGroupBy(callee); + const calleeName = hashFrameGroup(defaultGroupBy(callee)); let node = currentNode.callees.get(calleeName); if (node === undefined) { - node = buildCallerCalleeIntermediateNode(callee, samples); + node = createCallerCalleeIntermediateNode(callee, samples); currentNode.callees.set(calleeName, node); } else { - node.samples = samples; + node.samples += samples; } currentNode = node; } @@ -129,10 +156,9 @@ export function buildCallerCalleeIntermediateRoot( return root; } -export interface CallerCalleeNode { +export type CallerCalleeNode = { Callers: CallerCalleeNode[]; Callees: CallerCalleeNode[]; - FileID: string; FrameType: number; ExeFileName: string; @@ -140,52 +166,63 @@ export interface CallerCalleeNode { FunctionName: string; AddressOrLine: number; FunctionSourceLine: number; - - // symbolization fields - currently unused FunctionSourceID: string; FunctionSourceURL: string; SourceFilename: string; SourceLine: number; - Samples: number; -} - -const defaultCallerCalleeNode: CallerCalleeNode = { - Callers: [], - Callees: [], - FileID: '', - FrameType: 0, - ExeFileName: '', - FunctionID: '', - FunctionName: '', - AddressOrLine: 0, - FunctionSourceLine: 0, - FunctionSourceID: '', - FunctionSourceURL: '', - SourceFilename: '', - SourceLine: 0, - Samples: 0, }; -export function buildCallerCalleeNode(node: Partial = {}): CallerCalleeNode { - return override(defaultCallerCalleeNode, node); +export function createCallerCalleeNode(options: Partial = {}): CallerCalleeNode { + const node = {} as CallerCalleeNode; + + 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; + + 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, 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; + + // 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.FunctionSourceLine = metadata.FunctionLine; - node.SourceLine = metadata.SourceLine; - node.FrameType = metadata.FrameType; node.SourceFilename = metadata.SourceFilename; - node.FileID = metadata.FileID; - node.AddressOrLine = metadata.AddressOrLine; + node.SourceLine = metadata.SourceLine; break; } } @@ -193,12 +230,12 @@ function selectCallerCalleeData(frameMetadata: Set, node: Ca function sortNodes( nodes: Map ): CallerCalleeIntermediateNode[] { - const sortedNodes: CallerCalleeIntermediateNode[] = new Array(nodes.size); + const sortedNodes = new Array(); for (const node of nodes.values()) { sortedNodes.push(node); } return sortedNodes.sort((n1, n2) => { - return compareFrameGroup(n1.nodeID, n2.nodeID); + return compareFrameGroup(n1.frameGroup, n2.frameGroup); }); } @@ -208,7 +245,7 @@ function sortNodes( export function fromCallerCalleeIntermediateNode( root: CallerCalleeIntermediateNode ): CallerCalleeNode { - const node = buildCallerCalleeNode({ Samples: root.samples }); + const node = createCallerCalleeNode({ Samples: root.samples }); // Populate the other fields with data from the root node. Selectors are not supposed // to be able to fail. diff --git a/src/plugins/profiling/common/flamegraph.ts b/src/plugins/profiling/common/flamegraph.ts index 4055acb7257fae..1e92d1495c0ae3 100644 --- a/src/plugins/profiling/common/flamegraph.ts +++ b/src/plugins/profiling/common/flamegraph.ts @@ -7,8 +7,8 @@ */ import { Logger } from 'kibana/server'; import { - buildCallerCalleeIntermediateRoot, CallerCalleeNode, + createCallerCalleeIntermediateRoot, fromCallerCalleeIntermediateNode, } from './callercallee'; import { @@ -18,7 +18,7 @@ import { StackTrace, StackFrame, Executable, - buildStackFrameMetadata, + createStackFrameMetadata, StackFrameMetadata, } from './profiling'; @@ -31,7 +31,7 @@ function checkIfStringHasParentheses(s: string) { return /\(|\)/.test(s); } -function getFunctionName(frame: any) { +function getFunctionName(frame: StackFrame) { return frame.FunctionName !== '' && !checkIfStringHasParentheses(frame.FunctionName) ? `${frame.FunctionName}()` : frame.FunctionName; @@ -75,6 +75,36 @@ export class FlameGraph { this.logger = logger; } + // getFrameMetadataForTraces 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 + private getFrameMetadataForTraces(): Map { + const frameMetadataForTraces = new Map(); + for (const [stackTraceID, trace] of this.stacktraces) { + const frameMetadata = new Array(); + for (let i = 0; i < trace.FrameID.length; i++) { + const frame = this.stackframes.get(trace.FrameID[i])!; + const executable = this.executables.get(trace.FileID[i])!; + + const metadata = createStackFrameMetadata({ + FileID: Buffer.from(trace.FileID[i], 'base64url').toString('hex'), + FrameType: trace.Type[i], + AddressOrLine: frame.LineNumber, + FunctionName: frame.FunctionName, + FunctionOffset: frame.FunctionOffset, + SourceLine: frame.LineNumber, + ExeFileName: executable.FileName, + Index: i, + }); + + frameMetadata.push(metadata); + } + frameMetadataForTraces.set(stackTraceID, frameMetadata); + } + return frameMetadataForTraces; + } + private getExeFileName(exe: any, type: number) { if (exe?.FileName === undefined) { this.logger.warn('missing executable FileName'); @@ -110,7 +140,7 @@ export class FlameGraph { // Generates the label for a flamegraph node // // This is slightly modified from the original code in elastic/prodfiler_ui - private getLabel(frame: any, executable: any, type: number) { + private getLabel(frame: StackFrame, executable: Executable, type: number) { if (frame.FunctionName !== '') { return `${this.getExeFileName(executable, type)}: ${getFunctionName(frame)} in #${ frame.LineNumber @@ -127,9 +157,9 @@ export class FlameGraph { const path = ['root']; for (let i = 0; i < trace.FrameID.length; i++) { const label = this.getLabel( - this.stackframes.get(trace.FrameID[i]), - this.executables.get(trace.FileID[i]), - parseInt(trace.Type[i], 10) + this.stackframes.get(trace.FrameID[i])!, + this.executables.get(trace.FileID[i])!, + trace.Type[i] ); if (label.length === 0) { @@ -157,9 +187,13 @@ export class FlameGraph { } toPixi(): PixiFlameGraph { - const rootFrame = buildStackFrameMetadata(); - const metadataForTraces = new Map(); - const diagram = buildCallerCalleeIntermediateRoot(rootFrame, this.events, metadataForTraces); + const rootFrame = createStackFrameMetadata(); + const frameMetadataForTraces = this.getFrameMetadataForTraces(); + const diagram = createCallerCalleeIntermediateRoot( + rootFrame, + this.events, + frameMetadataForTraces + ); return { ...fromCallerCalleeIntermediateNode(diagram), TotalTraces: this.totalCount, diff --git a/src/plugins/profiling/common/index.ts b/src/plugins/profiling/common/index.ts index 2afdefd6d87684..df5f8f527c57fc 100644 --- a/src/plugins/profiling/common/index.ts +++ b/src/plugins/profiling/common/index.ts @@ -40,13 +40,6 @@ export function getRemoteRoutePaths() { }; } -// override combines the template object and the overrides to produce a new -// object. Any missing properties from the partial object will be set by the -// template object. -export function override(template: T, overrides: Partial): T { - return { ...template, ...overrides }; -} - function toMilliseconds(seconds: string): number { return parseInt(seconds, 10) * 1000; } diff --git a/src/plugins/profiling/common/profiling.test.ts b/src/plugins/profiling/common/profiling.test.ts index 6cc2b915fc6103..2e667c5ea0d032 100644 --- a/src/plugins/profiling/common/profiling.test.ts +++ b/src/plugins/profiling/common/profiling.test.ts @@ -7,58 +7,59 @@ */ import { - buildFrameGroup, - buildStackFrameMetadata, + createFrameGroup, + createStackFrameMetadata, compareFrameGroup, defaultGroupBy, + hashFrameGroup, } from './profiling'; describe('Frame group operations', () => { test('check if a frame group is less than another', () => { - const a = buildFrameGroup({ ExeFileName: 'chrome' }); - const b = buildFrameGroup({ ExeFileName: 'dockerd' }); + const a = createFrameGroup({ ExeFileName: 'chrome' }); + const b = createFrameGroup({ ExeFileName: 'dockerd' }); expect(compareFrameGroup(a, b)).toEqual(-1); }); test('check if a frame group is greater than another', () => { - const a = buildFrameGroup({ ExeFileName: 'oom_reaper' }); - const b = buildFrameGroup({ ExeFileName: 'dockerd' }); + const a = createFrameGroup({ ExeFileName: 'oom_reaper' }); + const b = createFrameGroup({ ExeFileName: 'dockerd' }); expect(compareFrameGroup(a, b)).toEqual(1); }); test('check if frame groups are equal', () => { - const a = buildFrameGroup({ AddressOrLine: 1234 }); - const b = buildFrameGroup({ AddressOrLine: 1234 }); + const a = createFrameGroup({ AddressOrLine: 1234 }); + const b = createFrameGroup({ AddressOrLine: 1234 }); expect(compareFrameGroup(a, b)).toEqual(0); }); test('check serialized non-symbolized frame', () => { - const metadata = buildStackFrameMetadata({ + const metadata = createStackFrameMetadata({ FileID: '0x0123456789ABCDEF', AddressOrLine: 102938, }); - expect(defaultGroupBy(metadata)).toEqual( + expect(hashFrameGroup(defaultGroupBy(metadata))).toEqual( '{"FileID":"0x0123456789ABCDEF","ExeFileName":"","FunctionName":"","AddressOrLine":102938,"SourceFilename":""}' ); }); test('check serialized non-symbolized ELF frame', () => { - const metadata = buildStackFrameMetadata({ + const metadata = createStackFrameMetadata({ FunctionName: 'strlen()', FileID: '0x0123456789ABCDEF', }); - expect(defaultGroupBy(metadata)).toEqual( + expect(hashFrameGroup(defaultGroupBy(metadata))).toEqual( '{"FileID":"0x0123456789ABCDEF","ExeFileName":"","FunctionName":"strlen()","AddressOrLine":0,"SourceFilename":""}' ); }); test('check serialized symbolized frame', () => { - const metadata = buildStackFrameMetadata({ + const metadata = createStackFrameMetadata({ ExeFileName: 'chrome', SourceFilename: 'strlen()', FunctionName: 'strlen()', }); - expect(defaultGroupBy(metadata)).toEqual( + expect(hashFrameGroup(defaultGroupBy(metadata))).toEqual( '{"FileID":"","ExeFileName":"chrome","FunctionName":"strlen()","AddressOrLine":0,"SourceFilename":"strlen()"}' ); }); diff --git a/src/plugins/profiling/common/profiling.ts b/src/plugins/profiling/common/profiling.ts index e8a7c7ceace536..f7d05683d5af05 100644 --- a/src/plugins/profiling/common/profiling.ts +++ b/src/plugins/profiling/common/profiling.ts @@ -6,22 +6,20 @@ * Side Public License, v 1. */ -import { override } from '.'; - export type StackTraceID = string; export type StackFrameID = string; export type FileID = string; -export interface StackTraceEvent { +export type StackTraceEvent = { StackTraceID: StackTraceID; Count: number; -} +}; -export interface StackTrace { +export type StackTrace = { FileID: string[]; FrameID: string[]; - Type: string[]; -} + Type: number[]; +}; export type StackFrame = { FileName: string; @@ -31,56 +29,71 @@ export type StackFrame = { SourceType: number; }; +export type Executable = { + FileName: string; +}; + export type StackFrameMetadata = { + // StackTrace.FileID FileID: FileID; - AddressOrLine: number; + // StackTrace.Type + FrameType: number; + // stringified FrameType -- FrameType.String() + FrameTypeString: string; + // StackFrame.LineNumber? + AddressOrLine: number; + // StackFrame.FunctionName FunctionName: string; + // StackFrame.FunctionOffset + FunctionOffset: number; + // should this be StackFrame.SourceID? SourceID: FileID; + // StackFrame.LineNumber SourceLine: number; - FunctionOffset: number; + // Executable.FileName + ExeFileName: string; + + // unused atm due to lack of symbolization metadata CommitHash: string; + // unused atm due to lack of symbolization metadata SourceCodeURL: string; - SourcePackageHash: string; - FrameTypeString: string; + // unused atm due to lack of symbolization metadata SourceFilename: string; - ExeFileName: string; + // unused atm due to lack of symbolization metadata + SourcePackageHash: string; + // unused atm due to lack of symbolization metadata SourcePackageURL: string; - FrameType: number; - FunctionLine: number; + // unused atm due to lack of symbolization metadata SourceType: number; -}; -const defaultStackFrameMetadata: StackFrameMetadata = { - FileID: '', - AddressOrLine: 0, - - FunctionName: '', - SourceID: '', - SourceLine: 0, - FunctionOffset: 0, - - CommitHash: '', - SourceCodeURL: '', - SourcePackageHash: '', - FrameTypeString: '', - SourceFilename: '', - ExeFileName: '', - SourcePackageURL: '', - FrameType: 0, - FunctionLine: 0, - SourceType: 0, + Index: number; }; -export function buildStackFrameMetadata( - metadata: Partial = {} +export function createStackFrameMetadata( + options: Partial = {} ): StackFrameMetadata { - return override(defaultStackFrameMetadata, metadata); -} - -export interface Executable { - FileName: string; + const metadata = {} as StackFrameMetadata; + + metadata.FileID = options.FileID ?? ''; + metadata.FrameType = options.FrameType ?? 0; + metadata.FrameTypeString = options.FrameTypeString ?? ''; + metadata.AddressOrLine = options.AddressOrLine ?? 0; + metadata.FunctionName = options.FunctionName ?? ''; + metadata.FunctionOffset = options.FunctionOffset ?? 0; + metadata.SourceID = options.SourceID ?? ''; + metadata.SourceLine = options.SourceLine ?? 0; + metadata.ExeFileName = options.ExeFileName ?? ''; + metadata.CommitHash = options.CommitHash ?? ''; + metadata.SourceCodeURL = options.SourceCodeURL ?? ''; + metadata.SourceFilename = options.SourceFilename ?? ''; + metadata.SourcePackageHash = options.SourcePackageHash ?? ''; + metadata.SourcePackageURL = options.SourcePackageURL ?? ''; + metadata.SourceType = options.SourceType ?? 0; + metadata.Index = options.Index ?? 0; + + return metadata; } export type FrameGroup = Pick< @@ -88,18 +101,18 @@ export type FrameGroup = Pick< 'FileID' | 'ExeFileName' | 'FunctionName' | 'AddressOrLine' | 'SourceFilename' >; -const defaultFrameGroup: FrameGroup = { - FileID: '', - ExeFileName: '', - FunctionName: '', - AddressOrLine: 0, - SourceFilename: '', -}; - -// This is a convenience function to build a FrameGroup value with +// This is a convenience function to create a FrameGroup value with // defaults for missing fields -export function buildFrameGroup(frameGroup: Partial = {}): FrameGroup { - return override(defaultFrameGroup, frameGroup); +export function createFrameGroup(options: Partial = {}): FrameGroup { + const frameGroup = {} as FrameGroup; + + frameGroup.FileID = options.FileID ?? ''; + frameGroup.ExeFileName = options.ExeFileName ?? ''; + frameGroup.FunctionName = options.FunctionName ?? ''; + frameGroup.AddressOrLine = options.AddressOrLine ?? 0; + frameGroup.SourceFilename = options.SourceFilename ?? ''; + + return frameGroup; } export function compareFrameGroup(a: FrameGroup, b: FrameGroup): number { @@ -116,16 +129,14 @@ export function compareFrameGroup(a: FrameGroup, b: FrameGroup): number { return 0; } -export type FrameGroupID = string; - // defaultGroupBy is the "standard" way of grouping frames, by commonly // shared group identifiers. // // For ELF-symbolized frames, group by FunctionName and FileID. // For non-symbolized frames, group by FileID and AddressOrLine. // Otherwise group by ExeFileName, SourceFilename and FunctionName. -export function defaultGroupBy(frame: StackFrameMetadata): FrameGroupID { - const frameGroup = buildFrameGroup(); +export function defaultGroupBy(frame: StackFrameMetadata): FrameGroup { + const frameGroup = createFrameGroup(); if (frame.FunctionName === '') { // Non-symbolized frame where we only have FileID and AddressOrLine @@ -142,6 +153,12 @@ export function defaultGroupBy(frame: StackFrameMetadata): FrameGroupID { frameGroup.FunctionName = frame.FunctionName; } - // We serialize to JSON string to use FrameGroup as a key + return frameGroup; +} + +export type FrameGroupID = string; + +export function hashFrameGroup(frameGroup: FrameGroup): FrameGroupID { + // We use serialized JSON as the unique value of a frame group for now return JSON.stringify(frameGroup); }