[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:
Joseph Crail 2023-02-07 04:22:59 -08:00 committed by GitHub
parent 56d53675d1
commit b42bb18119
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 194 additions and 43 deletions

View file

@ -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 {

View file

@ -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: {

View file

@ -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);
}

View file

@ -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);
});
});

View file

@ -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);