Skip to content

Commit 4ad3f6c

Browse files
Merge pull request certinia#624 from lukecotter/feat-governor-limits-in-cells
2 parents 4663ad4 + 670eb24 commit 4ad3f6c

16 files changed

Lines changed: 660 additions & 163 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- To sort a group by the number of items within the group click the Group Name column until the third sort state is shown.
1515
- **🔢 New Call Tree Columns**: The Rows column is replaced with the DML Rows and SOQL Rows columns ([#93]).
1616
- **⏱️ New Timeline Tooltip values**: The Rows count in the Timeline tooltip is now split into DML Rows and SOQL Rows ([#93]).
17+
- **⏱️ New Timeline Tooltip**: The tooltip is much easier to read a glance([#308]).
1718
- **📋💾 Table Actions**: Added options to Copy to Clipboard and Export to CSV directly from the table action buttons above the Analysis and Database tables ([#589]).
1819

1920
### Changed
@@ -397,6 +398,7 @@ Skipped due to adopting odd numbering for pre releases and even number for relea
397398
[#93]: https://github.com/certinia/debug-log-analyzer/issues/93
398399
[#539]: https://github.com/certinia/debug-log-analyzer/issues/539
399400
[#619]: https://github.com/certinia/debug-log-analyzer/issues/619
401+
[#308]: https://github.com/certinia/debug-log-analyzer/issues/308
400402

401403
<!-- 1.16.1 -->
402404

lana/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "lana",
33
"displayName": "Apex Log Analyzer",
44
"version": "1.16.1",
5-
"description": "Salesforce Apex Debug Log Analyzer for VS Code - Apex Log Analyzer is a blazing-fast VS Code extension for Salesforce developers. Instantly visualize and debug Apex logs with interactive flame charts, dynamic call trees, and detailed SOQL/DML breakdowns. Identify performance bottlenecks, gain deep insight into complex transactions and optimize slow Apex methods faster than ever.",
5+
"description": "Salesforce Apex Debug Log Analyzer: Blazing-fast VS Code extension for Salesforce. Visualize and debug Apex logs with interactive flame charts, dynamic call trees, and detailed SOQL/DML breakdowns. Identify performance bottlenecks, gain deep transaction insights and optimize slow Apex.",
66
"keywords": [
77
"analysis",
88
"apex",

log-viewer/modules/Util.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
*/
44
let requestId: number = 0;
55

6-
export default function formatDuration(duration: number) {
7-
const text = `${~~(duration / 1000)}`; // convert from nano-seconds to micro-seconds
6+
export default function formatDuration(durationNs: number, totalNs = 0) {
7+
const text = `${~~(durationNs / 1000)}`; // convert from nano-seconds to micro-seconds
88
const textPadded = text.length < 4 ? '0000'.substring(text.length) + text : text; // length min = 4
99
const millis = textPadded.slice(0, -3); // everything before last 3 chars
1010
const micros = textPadded.slice(-3); // last 3 chars
11-
return `${millis}.${micros}ms`;
11+
const suffix = totalNs > 0 ? `/${Math.round(totalNs / 1_000_000)}` : '';
12+
return `${millis}.${micros}${suffix} ms`;
1213
}
1314

1415
export function debounce<T extends unknown[]>(callBack: (...args: T) => unknown) {

log-viewer/modules/__tests__/ApexLogParser.test.ts

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ describe('parseLog tests', () => {
324324
const execEvent = apexLog.children[0] as MethodEntryLine;
325325
expect(execEvent.children[0]?.namespace).toBe('appirio_core');
326326
});
327-
it('Limit Usage for NS provides cpuUsed', async () => {
327+
it('Limit Usage for NS as child of CUMULATIVE_LIMIT_USAGE', async () => {
328328
const log =
329329
'09:18:22.6 (6574780)|EXECUTION_STARTED\n' +
330330
'14:29:44.163 (40163621912)|CUMULATIVE_LIMIT_USAGE\n' +
@@ -345,7 +345,6 @@ describe('parseLog tests', () => {
345345
'09:19:13.82 (51595120059)|EXECUTION_FINISHED\n';
346346

347347
const apexLog = parse(log);
348-
expect(apexLog.cpuTime).toBe(4564000000);
349348
const execEvent = apexLog.children[0] as MethodEntryLine;
350349
expect(execEvent.children.length).toBe(1);
351350

@@ -364,8 +363,6 @@ describe('parseLog tests', () => {
364363

365364
const apexLog = parse(log);
366365

367-
expect(apexLog.cpuTime).toBe(0);
368-
369366
const execEvent = apexLog.children[0] as MethodEntryLine;
370367
expect(execEvent.children.length).toBe(1);
371368

@@ -1127,6 +1124,128 @@ describe('Recalculate durations tests', () => {
11271124
});
11281125
});
11291126

1127+
describe('Governor Limits Parsing', () => {
1128+
it('should parse LIMIT_USAGE_FOR_NS lines and populate governorLimits for multiple namespaces', () => {
1129+
const log = [
1130+
'09:18:22.6 (6574780)|EXECUTION_STARTED',
1131+
'12:43:02.105 (48105827767)|LIMIT_USAGE_FOR_NS|(default)|',
1132+
' Number of SOQL queries: 17 out of 100',
1133+
' Number of query rows: 121 out of 50000',
1134+
' Number of SOSL queries: 3 out of 20',
1135+
' Number of DML statements: 8 out of 150',
1136+
' Number of Publish Immediate DML: 5 out of 150',
1137+
' Number of DML rows: 113 out of 10000',
1138+
' Maximum CPU time: 15008 out of 10000 ******* CLOSE TO LIMIT',
1139+
' Maximum heap size: 300 out of 6000000',
1140+
' Number of callouts: 2 out of 100',
1141+
' Number of Email Invocations: 1 out of 10',
1142+
' Number of future calls: 2 out of 50',
1143+
' Number of queueable jobs added to the queue: 6 out of 50',
1144+
' Number of Mobile Apex push calls: 1 out of 10',
1145+
'12:43:02.105 (48105827768)|LIMIT_USAGE_FOR_NS|myNS|',
1146+
' Number of SOQL queries: 2 out of 100',
1147+
' Number of query rows: 10 out of 50000',
1148+
' Number of SOSL queries: 1 out of 20',
1149+
' Number of DML statements: 1 out of 150',
1150+
' Number of Publish Immediate DML: 0 out of 150',
1151+
' Number of DML rows: 5 out of 10000',
1152+
' Maximum CPU time: 2000 out of 10000',
1153+
' Maximum heap size: 100 out of 6000000',
1154+
' Number of callouts: 1 out of 100',
1155+
' Number of Email Invocations: 5 out of 10',
1156+
' Number of future calls: 2 out of 50',
1157+
' Number of queueable jobs added to the queue: 3 out of 50',
1158+
' Number of Mobile Apex push calls: 0 out of 10',
1159+
'09:19:13.82 (51595120059)|EXECUTION_FINISHED',
1160+
].join('\n');
1161+
1162+
const apexLog = parse(log);
1163+
1164+
expect(apexLog.governorLimits).toBeDefined();
1165+
expect([...apexLog.governorLimits.byNamespace.keys()]).toEqual(['default', 'myNS']);
1166+
1167+
expect(apexLog.governorLimits.byNamespace.get('default')).toMatchObject({
1168+
soqlQueries: { used: 17, limit: 100 },
1169+
queryRows: { used: 121, limit: 50000 },
1170+
soslQueries: { used: 3, limit: 20 },
1171+
dmlStatements: { used: 8, limit: 150 },
1172+
publishImmediateDml: { used: 5, limit: 150 },
1173+
dmlRows: { used: 113, limit: 10000 },
1174+
cpuTime: { used: 15008, limit: 10000 },
1175+
heapSize: { used: 300, limit: 6000000 },
1176+
callouts: { used: 2, limit: 100 },
1177+
emailInvocations: { used: 1, limit: 10 },
1178+
futureCalls: { used: 2, limit: 50 },
1179+
queueableJobsAddedToQueue: { used: 6, limit: 50 },
1180+
mobileApexPushCalls: { used: 1, limit: 10 },
1181+
});
1182+
1183+
expect(apexLog.governorLimits.byNamespace.get('myNS')).toMatchObject({
1184+
soqlQueries: { used: 2, limit: 100 },
1185+
queryRows: { used: 10, limit: 50000 },
1186+
soslQueries: { used: 1, limit: 20 },
1187+
dmlStatements: { used: 1, limit: 150 },
1188+
publishImmediateDml: { used: 0, limit: 150 },
1189+
dmlRows: { used: 5, limit: 10000 },
1190+
cpuTime: { used: 2000, limit: 10000 },
1191+
heapSize: { used: 100, limit: 6000000 },
1192+
callouts: { used: 1, limit: 100 },
1193+
emailInvocations: { used: 5, limit: 10 },
1194+
futureCalls: { used: 2, limit: 50 },
1195+
queueableJobsAddedToQueue: { used: 3, limit: 50 },
1196+
mobileApexPushCalls: { used: 0, limit: 10 },
1197+
});
1198+
1199+
expect(apexLog.governorLimits).toMatchObject({
1200+
soqlQueries: { used: 19, limit: 100 },
1201+
queryRows: { used: 131, limit: 50000 },
1202+
soslQueries: { used: 4, limit: 20 },
1203+
dmlStatements: { used: 9, limit: 150 },
1204+
publishImmediateDml: { used: 5, limit: 150 },
1205+
dmlRows: { used: 118, limit: 10000 },
1206+
cpuTime: { used: 17008, limit: 10000 },
1207+
heapSize: { used: 400, limit: 6000000 },
1208+
callouts: { used: 3, limit: 100 },
1209+
emailInvocations: { used: 6, limit: 10 },
1210+
futureCalls: { used: 4, limit: 50 },
1211+
queueableJobsAddedToQueue: { used: 9, limit: 50 },
1212+
mobileApexPushCalls: { used: 1, limit: 10 },
1213+
});
1214+
});
1215+
1216+
it('should handle missing or partial LIMIT_USAGE_FOR_NS sections gracefully', () => {
1217+
const log = [
1218+
'09:18:22.6 (6574780)|EXECUTION_STARTED',
1219+
'12:43:02.105 (48105827767)|LIMIT_USAGE_FOR_NS|(default)|',
1220+
' Number of SOQL queries: 5 out of 100',
1221+
' Number of query rows: 10 out of 50000',
1222+
// missing other lines
1223+
'09:19:13.82 (51595120059)|EXECUTION_FINISHED',
1224+
].join('\n');
1225+
1226+
const apexLog = parse(log);
1227+
1228+
expect(apexLog.governorLimits).toBeDefined();
1229+
const expected = {
1230+
soqlQueries: { used: 5, limit: 100 },
1231+
queryRows: { used: 10, limit: 50000 },
1232+
soslQueries: { used: 0, limit: 0 },
1233+
dmlStatements: { used: 0, limit: 0 },
1234+
publishImmediateDml: { used: 0, limit: 0 },
1235+
dmlRows: { used: 0, limit: 0 },
1236+
cpuTime: { used: 0, limit: 0 },
1237+
heapSize: { used: 0, limit: 0 },
1238+
callouts: { used: 0, limit: 0 },
1239+
emailInvocations: { used: 0, limit: 0 },
1240+
futureCalls: { used: 0, limit: 0 },
1241+
queueableJobsAddedToQueue: { used: 0, limit: 0 },
1242+
mobileApexPushCalls: { used: 0, limit: 0 },
1243+
};
1244+
expect(apexLog.governorLimits.byNamespace.get('default')).toMatchObject(expected);
1245+
expect(apexLog.governorLimits).toMatchObject(expected);
1246+
});
1247+
});
1248+
11301249
describe('Line Type Tests', () => {
11311250
it('Lines referenced by exitTypes should be exits', () => {
11321251
const parser = new ApexLogParser();

log-viewer/modules/__tests__/Util.test.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,35 @@ import formatDuration from '../Util.js';
66

77
describe('Format duration tests', () => {
88
it('Value converted from nanoseconds to milliseconds', () => {
9-
expect(formatDuration(1000)).toBe('0.001ms');
9+
expect(formatDuration(1000)).toBe('0.001 ms');
1010
});
1111
it('Value always has 3dp', () => {
12-
expect(formatDuration(1000000)).toBe('1.000ms');
12+
expect(formatDuration(1000000)).toBe('1.000 ms');
1313
});
1414
it('Value truncated at 3dp', () => {
15-
expect(formatDuration(1234567)).toBe('1.234ms');
15+
expect(formatDuration(1234567)).toBe('1.234 ms');
16+
});
17+
it('pads microseconds correctly for short durations', () => {
18+
expect(formatDuration(5)).toBe('0.000 ms');
19+
expect(formatDuration(50)).toBe('0.000 ms');
20+
expect(formatDuration(500)).toBe('0.000 ms');
21+
});
22+
});
23+
24+
describe('Adds out off suffix', () => {
25+
it('rounds up to 0dp', () => {
26+
expect(formatDuration(1000, 2_000_600_000)).toBe('0.001/2001 ms');
27+
});
28+
29+
it('handles zero duration and total', () => {
30+
expect(formatDuration(0, 0)).toBe('0.000 ms');
31+
});
32+
33+
it('handles zero duration with totalNs', () => {
34+
expect(formatDuration(0, 1_000_000)).toBe('0.000/1 ms');
35+
});
36+
37+
it('handles large duration and totalNs', () => {
38+
expect(formatDuration(12_345_678_900, 123_456_789_000)).toBe('12345.678/123457 ms');
1639
});
1740
});

log-viewer/modules/components/CallStack.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class CallStack extends LitElement {
2727
min-height: 1ch;
2828
max-height: 30vh;
2929
padding: 0px 5px 0px 5px;
30+
white-space: normal;
3031
}
3132
3233
:host(:hover) {

log-viewer/modules/components/analysis-view/AnalysisView.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ import { globalStyles } from '../../styles/global.styles.js';
1616

1717
// Tabulator custom modules, imports + styles
1818
import { Tabulator, type RowComponent } from 'tabulator-tables';
19-
import { isVisible } from '../../Util.js';
19+
import formatDuration, { isVisible } from '../../Util.js';
2020
import NumberAccessor from '../../datagrid/dataaccessor/Number.js';
21-
import { progressFormatter } from '../../datagrid/format/Progress.js';
21+
import { progressFormatterMS } from '../../datagrid/format/ProgressMS.js';
2222
import { GroupCalcs } from '../../datagrid/groups/GroupCalcs.js';
2323
import { GroupSort } from '../../datagrid/groups/GroupSort.js';
2424
import * as CommonModules from '../../datagrid/module/CommonModules.js';
@@ -265,14 +265,14 @@ export class AnalysisView extends LitElement {
265265
if (!this._tableWrapper) {
266266
return;
267267
}
268-
const metricList = groupMetrics(rootMethod);
269268

270269
Tabulator.registerModule(Object.values(CommonModules));
271270
Tabulator.registerModule([RowKeyboardNavigation, RowNavigation, Find, GroupCalcs, GroupSort]);
271+
272272
this.analysisTable = new Tabulator(this._tableWrapper, {
273273
rowKeyboardNavigation: true,
274274
selectableRows: 'highlight',
275-
data: metricList,
275+
data: groupMetrics(rootMethod),
276276
layout: 'fitColumns',
277277
placeholder: 'No Analysis Available',
278278
columnCalcs: 'table',
@@ -320,6 +320,7 @@ export class AnalysisView extends LitElement {
320320
headerTooltip: true,
321321
headerWordWrap: true,
322322
},
323+
tooltipDelay: 100,
323324
initialSort: [{ column: 'selfTime', dir: 'desc' }],
324325
headerSortElement: function (column, dir) {
325326
switch (dir) {
@@ -388,16 +389,18 @@ export class AnalysisView extends LitElement {
388389
width: 165,
389390
hozAlign: 'right',
390391
headerHozAlign: 'right',
391-
formatter: progressFormatter,
392+
bottomCalc: callStackSum,
393+
bottomCalcFormatter: progressFormatterMS,
394+
bottomCalcFormatterParams: { precision: 3, totalValue: rootMethod.duration.total },
395+
formatter: progressFormatterMS,
392396
formatterParams: {
393-
thousand: false,
394397
precision: 3,
395398
totalValue: rootMethod.duration.total,
396399
},
397400
accessorDownload: NumberAccessor,
398-
bottomCalcFormatter: progressFormatter,
399-
bottomCalc: callStackSum,
400-
bottomCalcFormatterParams: { precision: 3, totalValue: rootMethod.duration.total },
401+
tooltip(_event, cell, _onRender) {
402+
return formatDuration(cell.getValue(), rootMethod.duration.total);
403+
},
401404
},
402405
{
403406
title: 'Self Time (ms)',
@@ -407,15 +410,17 @@ export class AnalysisView extends LitElement {
407410
hozAlign: 'right',
408411
headerHozAlign: 'right',
409412
bottomCalc: 'sum',
413+
bottomCalcFormatter: progressFormatterMS,
410414
bottomCalcFormatterParams: { precision: 3, totalValue: rootMethod.duration.total },
411-
formatter: progressFormatter,
415+
formatter: progressFormatterMS,
412416
formatterParams: {
413-
thousand: false,
414417
precision: 3,
415418
totalValue: rootMethod.duration.total,
416419
},
417420
accessorDownload: NumberAccessor,
418-
bottomCalcFormatter: progressFormatter,
421+
tooltip(_event, cell, _onRender) {
422+
return formatDuration(cell.getValue(), rootMethod.duration.total);
423+
},
419424
},
420425
],
421426
});

log-viewer/modules/components/analysis-view/column-calcs/CallStackSum.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { LogLine } from '../../../parsers/ApexLogParser';
2-
import { type Metric } from '../AnalysisView.js';
2+
import type { Metric } from '../AnalysisView.js';
33

44
export function callStackSum(_values: number[], data: Metric[], _calcParams: unknown) {
55
const nodes: LogLine[] = [];

0 commit comments

Comments
 (0)