mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Profiling] Prepare for inline stackframes (#150401)
This PR adds initial support for initial stackframes as described in elastic/prodfiler#2918. It also adds tests and a minor refactor to account for the removal of synthetic source from stackframes (see elastic/prodfiler#2850). For stackframes, the profiling stack is composed of multiple write paths into Elasticsearch and multiple read paths out of Elasticsearch: * there are three services that can write into Elasticsearch (`APM agent`, `pf-elastic-collector`, and `pf-elastic-symbolizer`). * there are also two ways to read from Elasticsearch (the profiling plugin in Elasticsearch, and a combination of `search` and `mget` calls). This PR was written to handle all permutations of these paths. For those reviewers that wish to try the PR, please keep this in mind. I also wrote tests to handle these permutations. Note: Future PRs will add full support for inline stackframes. At this time, we only read the first inlined stackframe since the UI does not support inline stackframes. --------- Co-authored-by: Tim Rühsen <tim.ruhsen@elastic.co>
This commit is contained in:
parent
56d53675d1
commit
b42bb18119
5 changed files with 194 additions and 43 deletions
|
@ -22,11 +22,11 @@ interface ProfilingStackTraces {
|
|||
[key: string]: ProfilingStackTrace;
|
||||
}
|
||||
|
||||
interface ProfilingStackFrame {
|
||||
['file_name']: string | undefined;
|
||||
['function_name']: string;
|
||||
['function_offset']: number | undefined;
|
||||
['line_number']: number | undefined;
|
||||
export interface ProfilingStackFrame {
|
||||
['file_name']: string[];
|
||||
['function_name']: string[];
|
||||
['function_offset']: number[];
|
||||
['line_number']: number[];
|
||||
}
|
||||
|
||||
interface ProfilingStackFrames {
|
||||
|
|
|
@ -55,10 +55,10 @@ describe('Stack trace response operations', () => {
|
|||
},
|
||||
stack_frames: {
|
||||
abc: {
|
||||
file_name: 'pthread.c',
|
||||
function_name: 'pthread_create',
|
||||
function_offset: 0,
|
||||
line_number: 0,
|
||||
file_name: ['pthread.c'],
|
||||
function_name: ['pthread_create'],
|
||||
function_offset: [0],
|
||||
line_number: [0],
|
||||
},
|
||||
},
|
||||
executables: {
|
||||
|
@ -128,10 +128,10 @@ describe('Stack trace response operations', () => {
|
|||
},
|
||||
stack_frames: {
|
||||
abc: {
|
||||
file_name: undefined,
|
||||
function_name: 'pthread_create',
|
||||
function_offset: undefined,
|
||||
line_number: undefined,
|
||||
file_name: [],
|
||||
function_name: ['pthread_create'],
|
||||
function_offset: [],
|
||||
line_number: [],
|
||||
},
|
||||
},
|
||||
executables: {
|
||||
|
|
|
@ -35,11 +35,16 @@ export function decodeStackTraceResponse(response: StackTraceResponse) {
|
|||
|
||||
const stackFrames: Map<StackFrameID, StackFrame> = new Map();
|
||||
for (const [key, value] of Object.entries(response.stack_frames ?? {})) {
|
||||
// Each field in a stackframe is represented by an array. This is
|
||||
// necessary to support inline frames.
|
||||
//
|
||||
// We only take the first available inline stackframe until the UI
|
||||
// can support all of them.
|
||||
stackFrames.set(key, {
|
||||
FileName: value.file_name ? value.file_name[0] : [],
|
||||
FunctionName: value.function_name ? value.function_name[0] : [],
|
||||
FunctionOffset: value.function_offset,
|
||||
LineNumber: value.line_number,
|
||||
FileName: value.file_name[0],
|
||||
FunctionName: value.function_name[0],
|
||||
FunctionOffset: value.function_offset[0],
|
||||
LineNumber: value.line_number[0],
|
||||
} as StackFrame);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createStackFrameID, StackTrace } from '../../common/profiling';
|
||||
import LRUCache from 'lru-cache';
|
||||
import { createStackFrameID, StackFrame, StackFrameID, StackTrace } from '../../common/profiling';
|
||||
import { runLengthEncode } from '../../common/run_length_encoding';
|
||||
import { decodeStackTrace, EncodedStackTrace } from './stacktrace';
|
||||
import { decodeStackTrace, EncodedStackTrace, updateStackFrameMap } from './stacktrace';
|
||||
|
||||
enum fileID {
|
||||
A = 'aQpJmTLWydNvOapSFZOwKg',
|
||||
|
@ -86,3 +87,109 @@ describe('Stack trace operations', () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stack frame operations', () => {
|
||||
test('updateStackFrameMap with no frames', () => {
|
||||
const stackFrameMap = new Map<StackFrameID, StackFrame>();
|
||||
const stackFrameCache = new LRUCache<StackFrameID, StackFrame>();
|
||||
|
||||
const hits = updateStackFrameMap([], stackFrameMap, stackFrameCache);
|
||||
|
||||
expect(hits).toEqual(0);
|
||||
expect(stackFrameMap.size).toEqual(0);
|
||||
expect(stackFrameCache.length).toEqual(0);
|
||||
});
|
||||
|
||||
test('updateStackFrameMap with missing frames', () => {
|
||||
const stackFrameMap = new Map<StackFrameID, StackFrame>();
|
||||
const stackFrameCache = new LRUCache<StackFrameID, StackFrame>();
|
||||
|
||||
const stackFrames = [
|
||||
{
|
||||
_index: 'profiling-stackframes',
|
||||
_id: 'stackframe-001',
|
||||
found: false,
|
||||
},
|
||||
];
|
||||
|
||||
const hits = updateStackFrameMap(stackFrames, stackFrameMap, stackFrameCache);
|
||||
|
||||
expect(hits).toEqual(0);
|
||||
expect(stackFrameMap.size).toEqual(1);
|
||||
expect(stackFrameCache.length).toEqual(1);
|
||||
});
|
||||
|
||||
test('updateStackFrameMap with one partial non-inlined frame', () => {
|
||||
const stackFrameMap = new Map<StackFrameID, StackFrame>();
|
||||
const stackFrameCache = new LRUCache<StackFrameID, StackFrame>();
|
||||
|
||||
const id = 'stackframe-001';
|
||||
const source = {
|
||||
'ecs.version': '1.0.0',
|
||||
'Stackframe.function.name': 'calloc',
|
||||
};
|
||||
const expected = {
|
||||
FileName: undefined,
|
||||
FunctionName: 'calloc',
|
||||
FunctionOffset: undefined,
|
||||
LineNumber: undefined,
|
||||
SourceType: undefined,
|
||||
};
|
||||
|
||||
const stackFrames = [
|
||||
{
|
||||
_index: 'profiling-stackframes',
|
||||
_id: id,
|
||||
_version: 1,
|
||||
_seq_no: 1,
|
||||
_primary_term: 1,
|
||||
found: true,
|
||||
_source: source,
|
||||
},
|
||||
];
|
||||
|
||||
const hits = updateStackFrameMap(stackFrames, stackFrameMap, stackFrameCache);
|
||||
|
||||
expect(hits).toEqual(1);
|
||||
expect(stackFrameMap.size).toEqual(1);
|
||||
expect(stackFrameCache.length).toEqual(1);
|
||||
expect(stackFrameMap.get(id)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('updateStackFrameMap with one partial inlined frame', () => {
|
||||
const stackFrameMap = new Map<StackFrameID, StackFrame>();
|
||||
const stackFrameCache = new LRUCache<StackFrameID, StackFrame>();
|
||||
|
||||
const id = 'stackframe-001';
|
||||
const source = {
|
||||
'ecs.version': '1.0.0',
|
||||
'Stackframe.function.name': ['calloc', 'memset'],
|
||||
};
|
||||
const expected = {
|
||||
FileName: undefined,
|
||||
FunctionName: 'calloc',
|
||||
FunctionOffset: undefined,
|
||||
LineNumber: undefined,
|
||||
SourceType: undefined,
|
||||
};
|
||||
|
||||
const stackFrames = [
|
||||
{
|
||||
_index: 'profiling-stackframes',
|
||||
_id: id,
|
||||
_version: 1,
|
||||
_seq_no: 1,
|
||||
_primary_term: 1,
|
||||
found: true,
|
||||
_source: source,
|
||||
},
|
||||
];
|
||||
|
||||
const hits = updateStackFrameMap(stackFrames, stackFrameMap, stackFrameCache);
|
||||
|
||||
expect(hits).toEqual(1);
|
||||
expect(stackFrameMap.size).toEqual(1);
|
||||
expect(stackFrameCache.length).toEqual(1);
|
||||
expect(stackFrameMap.get(id)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -285,6 +285,68 @@ export function clearStackFrameCache(): number {
|
|||
return numDeleted;
|
||||
}
|
||||
|
||||
export function updateStackFrameMap(
|
||||
stackFrames: any,
|
||||
stackFrameMap: Map<StackFrameID, StackFrame>,
|
||||
stackFrameCache: LRUCache<StackFrameID, StackFrame>
|
||||
): number {
|
||||
let found = 0;
|
||||
for (const frame of stackFrames) {
|
||||
if ('error' in frame) {
|
||||
continue;
|
||||
}
|
||||
if (frame.found) {
|
||||
found++;
|
||||
|
||||
const fileName = frame._source[ProfilingESField.StackframeFileName];
|
||||
const functionName = frame._source[ProfilingESField.StackframeFunctionName];
|
||||
const functionOffset = frame._source[ProfilingESField.StackframeFunctionOffset];
|
||||
const lineNumber = frame._source[ProfilingESField.StackframeLineNumber];
|
||||
|
||||
let stackFrame;
|
||||
if (Array.isArray(functionName)) {
|
||||
// Each field in a stackframe is represented by an array. This is
|
||||
// necessary to support inline frames.
|
||||
//
|
||||
// We only take the first available inline stackframe until the UI
|
||||
// can support all of them.
|
||||
stackFrame = {
|
||||
FileName: fileName && fileName[0],
|
||||
FunctionName: functionName && functionName[0],
|
||||
FunctionOffset: functionOffset && functionOffset[0],
|
||||
LineNumber: lineNumber && lineNumber[0],
|
||||
};
|
||||
} else {
|
||||
if (fileName || functionName) {
|
||||
stackFrame = {
|
||||
FileName: fileName,
|
||||
FunctionName: functionName,
|
||||
FunctionOffset: functionOffset,
|
||||
LineNumber: lineNumber,
|
||||
};
|
||||
} else {
|
||||
// pre 8.7 format with synthetic source
|
||||
const sf = frame._source.Stackframe;
|
||||
stackFrame = {
|
||||
FileName: sf?.file?.name,
|
||||
FunctionName: sf?.function?.name,
|
||||
FunctionOffset: sf?.function?.offset,
|
||||
LineNumber: sf?.line?.number,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
stackFrameMap.set(frame._id, stackFrame);
|
||||
stackFrameCache.set(frame._id, stackFrame);
|
||||
continue;
|
||||
}
|
||||
|
||||
stackFrameMap.set(frame._id, emptyStackFrame);
|
||||
stackFrameCache.set(frame._id, emptyStackFrame);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
export async function mgetStackFrames({
|
||||
logger,
|
||||
client,
|
||||
|
@ -319,31 +381,8 @@ export async function mgetStackFrames({
|
|||
realtime: true,
|
||||
});
|
||||
|
||||
// Create a lookup map StackFrameID -> StackFrame.
|
||||
let queryHits = 0;
|
||||
const t0 = Date.now();
|
||||
const docs = resStackFrames.docs;
|
||||
for (const frame of docs) {
|
||||
if ('error' in frame) {
|
||||
continue;
|
||||
}
|
||||
if (frame.found) {
|
||||
queryHits++;
|
||||
const stackFrame = {
|
||||
FileName: frame._source!.Stackframe.file?.name,
|
||||
FunctionName: frame._source!.Stackframe.function?.name,
|
||||
FunctionOffset: frame._source!.Stackframe.function?.offset,
|
||||
LineNumber: frame._source!.Stackframe.line?.number,
|
||||
};
|
||||
stackFrames.set(frame._id, stackFrame);
|
||||
frameLRU.set(frame._id, stackFrame);
|
||||
continue;
|
||||
}
|
||||
|
||||
stackFrames.set(frame._id, emptyStackFrame);
|
||||
frameLRU.set(frame._id, emptyStackFrame);
|
||||
}
|
||||
|
||||
const queryHits = updateStackFrameMap(resStackFrames.docs, stackFrames, frameLRU);
|
||||
logger.info(`processing data took ${Date.now() - t0} ms`);
|
||||
|
||||
summarizeCacheAndQuery(logger, 'frames', cacheHits, cacheTotal, queryHits, stackFrameIDs.size);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue