Skip to content

Commit 43186cb

Browse files
Merge pull request certinia#612 from lukecotter/perf-apex-log-parser
perf: improve apex log parser speed
2 parents 5775460 + 30660bc commit 43186cb

3 files changed

Lines changed: 106 additions & 119 deletions

File tree

log-viewer/modules/components/LogViewer.ts

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -97,38 +97,41 @@ export class LogViewer extends LitElement {
9797
this.notifications = localNotifications;
9898

9999
this.parserIssues = this.parserIssuesToMessages(apexLog);
100-
101100
this.logStatus = 'Ready';
102101
}
103102

104103
async _readLog(logUri: string): Promise<string> {
104+
let msg = '';
105105
if (logUri) {
106-
return fetch(logUri)
107-
.then((response) => {
108-
if (response.ok) {
109-
return response.text();
110-
} else {
111-
throw Error(response.statusText || `Error reading log file: ${response.status}`);
106+
try {
107+
const response = await fetch(logUri);
108+
if (!response.ok || !response.body) {
109+
throw new Error(response.statusText || `Error reading log file: ${response.status}`);
110+
}
111+
112+
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
113+
const chunks: string[] = [];
114+
while (true) {
115+
const { done, value } = await reader.read();
116+
if (done) {
117+
break;
112118
}
113-
})
114-
.catch((err: unknown) => {
115-
const msg = err instanceof Error ? err.message : String(err);
116-
117-
const logMessage = new Notification();
118-
logMessage.summary = 'Could not read log';
119-
logMessage.message = msg || '';
120-
logMessage.severity = 'Error';
121-
this.notifications.push(logMessage);
122-
return Promise.resolve('');
123-
});
119+
chunks.push(value);
120+
}
121+
return chunks.join('');
122+
} catch (err: unknown) {
123+
msg = (err instanceof Error ? err.message : String(err)) ?? '';
124+
}
124125
} else {
125-
const logMessage = new Notification();
126-
logMessage.summary = 'Could not read log';
127-
logMessage.message = 'Invalid Log Path';
128-
logMessage.severity = 'Error';
129-
this.notifications.push(logMessage);
130-
return Promise.resolve('');
126+
msg = 'Invalid Log Path';
131127
}
128+
129+
const logMessage = new Notification();
130+
logMessage.summary = 'Could not read log';
131+
logMessage.message = msg;
132+
logMessage.severity = 'Error';
133+
this.notifications.push(logMessage);
134+
return '';
132135
}
133136

134137
severity = new Map<string, NotificationSeverity>([

log-viewer/modules/parsers/ApexLogParser.ts

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ export class ApexLogParser {
4242
cpuUsed = 0;
4343
lastTimestamp = 0;
4444
discontinuity = false;
45-
namespaces: string[] = [];
46-
namespacesUniq = new Set<string>();
45+
namespaces = new Set<string>();
4746

4847
/**
4948
* Takes string input of a log and returns the ApexLog class, which represents a log tree
@@ -58,7 +57,7 @@ export class ApexLogParser {
5857
apexLog.logIssues = this.logIssues;
5958
apexLog.parsingErrors = this.parsingErrors;
6059
apexLog.cpuTime = this.cpuUsed;
61-
apexLog.namespaces = this.namespaces;
60+
apexLog.namespaces = Array.from(this.namespaces);
6261

6362
return apexLog;
6463
}
@@ -67,19 +66,19 @@ export class ApexLogParser {
6766
const parts = line.split('|');
6867

6968
const type = parts[1] ?? '';
69+
7070
const metaCtor = getLogEventClass(type as LogEventType);
7171
if (metaCtor) {
7272
const entry = new metaCtor(this, parts);
7373
entry.logLine = line;
7474
lastEntry?.onAfter?.(this, entry);
75-
if (entry.namespace && !this.namespacesUniq.has(entry.namespace)) {
76-
this.namespaces.push(entry.namespace);
77-
this.namespacesUniq.add(entry.namespace);
75+
if (entry.namespace) {
76+
this.namespaces.add(entry.namespace);
7877
}
7978
return entry;
8079
}
8180

82-
const hasType = type && typePattern.test(type);
81+
const hasType = !!(type && typePattern.test(type));
8382
if (!hasType && lastEntry?.acceptsText) {
8483
// wrapped text from the previous entry?
8584
lastEntry.text += '\n' + line;
@@ -169,11 +168,10 @@ export class ApexLogParser {
169168
line.parent = rootMethod;
170169
rootMethod.addChild(line);
171170
}
172-
rootMethod.setTimes();
173171

172+
rootMethod.setTimes();
174173
this.insertPackageWrappers(rootMethod);
175174
this.aggregateTotals([rootMethod]);
176-
177175
return rootMethod;
178176
}
179177

@@ -220,40 +218,39 @@ export class ApexLogParser {
220218
break;
221219
}
222220

223-
nextLine.namespace ||= currentLine.namespace || 'default';
224221
lineIter.fetch(); // it's a child - consume the line
225222
this.lastTimestamp = nextLine.timestamp;
223+
nextLine.namespace ||= currentLine.namespace || 'default';
224+
nextLine.parent = currentLine;
225+
currentLine.children.push(nextLine);
226226

227227
if (nextLine instanceof Method) {
228228
this.parseTree(nextLine, lineIter, stack);
229229
}
230-
231-
nextLine.parent = currentLine;
232-
currentLine.children.push(nextLine);
233230
}
234231

235232
// End of line error handling. We have finished processing this log line and either got to the end
236233
// of the log without finding an exit line or the current line was truncated)
237234
if (!nextLine || currentLine.isTruncated) {
238235
// truncated method - terminate at the end of the log
239-
currentLine.exitStamp = this.lastTimestamp;
236+
currentLine.exitStamp = this.lastTimestamp ?? currentLine.timestamp;
240237

241238
// we found an entry event on its own e.g a `METHOD_ENTRY` without a `METHOD_EXIT` and got to the end of the log
242239
this.addLogIssue(
243-
this.lastTimestamp,
240+
currentLine.exitStamp,
244241
'Unexpected-End',
245242
'An entry event was found without a corresponding exit event e.g a `METHOD_ENTRY` event without a `METHOD_EXIT`',
246243
'unexpected',
247244
);
248245

249246
if (currentLine.isTruncated) {
250247
this.updateLogIssue(
251-
this.lastTimestamp,
248+
currentLine.exitStamp,
252249
'Max-Size-reached',
253250
'The maximum log size has been reached. Part of the log has been truncated.',
254251
'skip',
255252
);
256-
this.maxSizeTimestamp = this.lastTimestamp;
253+
this.maxSizeTimestamp = currentLine.exitStamp;
257254
}
258255
currentLine.isTruncated = true;
259256
}
@@ -264,7 +261,7 @@ export class ApexLogParser {
264261
}
265262

266263
private isMatchingEnd(startMethod: Method, endLine: LogLine) {
267-
return (
264+
return !!(
268265
endLine.type &&
269266
startMethod.exitTypes.includes(endLine.type) &&
270267
(endLine.lineNumber === startMethod.lineNumber ||
@@ -305,26 +302,26 @@ export class ApexLogParser {
305302

306303
private flattenByDepth(nodes: LogLine[]) {
307304
const result = new Map<number, LogLine[]>();
308-
result.set(0, nodes);
309-
310-
let currentDepth = 1;
311305

306+
let currentDepth = 0;
312307
let currentNodes = nodes;
313308
let len = currentNodes.length;
314309
while (len) {
315-
result.set(currentDepth, []);
310+
result.set(currentDepth, currentNodes);
311+
312+
const children: LogLine[] = [];
316313
while (len--) {
317314
const node = currentNodes[len];
318315
if (node?.children) {
319-
const children = result.get(currentDepth)!;
320316
node.children.forEach((c) => {
321317
if (c.children.length) {
322318
children.push(c);
323319
}
324320
});
325321
}
326322
}
327-
currentNodes = result.get(currentDepth++) ?? [];
323+
currentDepth++;
324+
currentNodes = children;
328325
len = currentNodes.length;
329326
}
330327

@@ -677,7 +674,7 @@ export abstract class LogLine {
677674
if (parts) {
678675
const [timeData, type] = parts;
679676
this.text = this.type = type as LogEventType;
680-
this.timestamp = this.parseTimestamp(timeData || '');
677+
this.timestamp = timeData ? this.parseTimestamp(timeData) : 0;
681678
} else {
682679
this.timestamp = 0;
683680
this.text = '';
@@ -965,14 +962,10 @@ class ConstructorEntryLine extends Method {
965962

966963
_parseConstructorNamespace(className: string): string {
967964
let possibleNs = className.slice(0, className.indexOf('.'));
968-
possibleNs =
969-
this.logParser.namespaces.find((ns) => {
970-
return ns === possibleNs;
971-
}) || '';
972-
973-
if (possibleNs) {
965+
if (this.logParser.namespaces.has(possibleNs)) {
974966
return possibleNs;
975967
}
968+
976969
const constructorParts = (className ?? '').split('.');
977970
possibleNs = constructorParts[0] || '';
978971
// inmner class with a namespace
@@ -1007,12 +1000,12 @@ export class MethodEntryLine extends Method {
10071000
constructor(parser: ApexLogParser, parts: string[]) {
10081001
super(parser, parts, ['METHOD_EXIT'], 'Method', 'method');
10091002
this.lineNumber = this.parseLineNumber(parts[2]);
1010-
this.text = parts[4] || this.type || '';
1003+
this.text = parts[4] || this.type || this.text;
10111004
if (this.text.indexOf('System.Type.forName(') !== -1) {
10121005
// assume we are not charged for class loading (or at least not lengthy remote-loading / compiling)
10131006
this.cpuType = 'loading';
10141007
} else {
1015-
const possibleNs = this._parseMethodNamespace(parts[4] || '');
1008+
const possibleNs = this._parseMethodNamespace(parts[4]);
10161009
if (possibleNs) {
10171010
this.namespace = possibleNs;
10181011
}
@@ -1025,23 +1018,27 @@ export class MethodEntryLine extends Method {
10251018
}
10261019
}
10271020

1028-
_parseMethodNamespace(methodName: string): string {
1021+
_parseMethodNamespace(methodName: string | undefined): string {
1022+
if (!methodName) {
1023+
return '';
1024+
}
1025+
10291026
const methodBracketIndex = methodName.indexOf('(');
10301027
if (methodBracketIndex === -1) {
10311028
return '';
10321029
}
10331030

1034-
let possibleNs = methodName.slice(0, methodName.indexOf('.'));
1035-
possibleNs =
1036-
this.logParser.namespaces.find((ns) => {
1037-
return ns === possibleNs;
1038-
}) || '';
1031+
const nsSeparator = methodName.indexOf('.');
1032+
if (nsSeparator === -1) {
1033+
return '';
1034+
}
10391035

1040-
if (possibleNs) {
1036+
const possibleNs = methodName.slice(0, nsSeparator);
1037+
if (this.logParser.namespaces.has(possibleNs)) {
10411038
return possibleNs;
10421039
}
10431040

1044-
const methodNameParts = methodName ? methodName.slice(0, methodBracketIndex)?.split('.') : '';
1041+
const methodNameParts = methodName.slice(0, methodBracketIndex)?.split('.');
10451042
if (methodNameParts.length === 4) {
10461043
return methodNameParts[0] ?? '';
10471044
} else if (methodNameParts.length === 2) {
@@ -1057,9 +1054,12 @@ class MethodExitLine extends LogLine {
10571054
constructor(parser: ApexLogParser, parts: string[]) {
10581055
super(parser, parts);
10591056
this.lineNumber = this.parseLineNumber(parts[2]);
1060-
this.text = parts[4] ?? parts[3] ?? '';
1057+
this.text = parts[4] ?? parts[3] ?? this.text;
10611058

1059+
/*A method will end with ')'. Without that this it represents the first reference to a class, outer or inner. One of the few reliable ways to determine valid namespaces. The first reference to a class (outer or inner) will always have an METHOD_EXIT containing the Outer class name with namespace if present. Other events will follow, CONSTRUCTOR_ENTRY etc. But this case will only ever have 2 parts ns.Outer even if the first reference was actually an inner class e.g new ns.Outer.Inner();*/
1060+
// If does not end in ) then we have a reference to the class, either via outer or inner.
10621061
if (!this.text.endsWith(')')) {
1062+
// if there is a . the we have a namespace e.g ns.Outer
10631063
const index = this.text.indexOf('.');
10641064
if (index !== -1) {
10651065
this.namespace = this.text.slice(0, index);

0 commit comments

Comments
 (0)