Skip to content

Commit 4d04bf6

Browse files
Merge pull request #758 from lukecotter/chore-bottom-up-comments
2 parents 44426db + e8a2e19 commit 4d04bf6

6 files changed

Lines changed: 132 additions & 10 deletions

File tree

log-viewer/src/features/call-tree/components/AggregatedTable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Certinia Inc. All rights reserved.
2+
* Copyright (c) 2026 Certinia Inc. All rights reserved.
33
*/
44
import type { ApexLog } from 'apex-log-parser';
55
import { Tabulator } from 'tabulator-tables';

log-viewer/src/features/call-tree/components/BottomUpTable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Certinia Inc. All rights reserved.
2+
* Copyright (c) 2026 Certinia Inc. All rights reserved.
33
*/
44
import type { ApexLog, LogEventType } from 'apex-log-parser';
55
import { Tabulator, type Options } from 'tabulator-tables';

log-viewer/src/features/call-tree/components/TableShared.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Certinia Inc. All rights reserved.
2+
* Copyright (c) 2025 Certinia Inc. All rights reserved.
33
*/
44
import { Tabulator } from 'tabulator-tables';
55

log-viewer/src/features/call-tree/components/TimeOrderTable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Certinia Inc. All rights reserved.
2+
* Copyright (c) 2026 Certinia Inc. All rights reserved.
33
*/
44
import type { ApexLog, LogEventType } from 'apex-log-parser';
55
import { Tabulator, type RowComponent } from 'tabulator-tables';

log-viewer/src/features/call-tree/utils/Aggregation.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Certinia Inc. All rights reserved.
2+
* Copyright (c) 2026 Certinia Inc. All rights reserved.
33
*/
44

55
import type { LogEvent, SelfTotal } from 'apex-log-parser';
@@ -233,16 +233,28 @@ function addEventToAggregatedRowWithStack(
233233
* reversed caller path. At every node, child partitions must sum to parent for
234234
* both self and total. Totals are non-overlapping and recursion-safe.
235235
*
236-
* Algorithm (see BOTTOM_UP_CALL_TREE_SPEC.md):
236+
* Input invariant expected from parser output for every metric pair M:
237+
* M.total(node) = M.self(node) + Σ M.total(children)
238+
* This converter assumes that invariant and preserves it through partitioning.
239+
*
240+
* Algorithm:
237241
* 1. Compute per-frame attributed totals. For every frame F with name R,
238242
* attr(F) = F.total - Σ T for each nearest same-name descendant T. The DFS
239243
* maintains Map<name, deepest-active-frame> and subtracts descendant totals
240244
* from their nearest same-name ancestor on entry.
241-
* 2. For every frame F with F.duration.self > 0, walk its ancestor chain and
245+
* 2. For every frame F, walk its ancestor chain and
242246
* insert F into a trie keyed by [F.name, F.parent.name, F.grandparent.name, …].
243247
* At every prefix, accumulate F.self (bucket.self) and attr(F) (bucket.total)
244248
* plus the matching metric pairs.
245249
* 3. Finalize averages and sort deterministically (totalSelfTime desc, name asc).
250+
*
251+
* Supported metric pairs (same attribution logic for each pair):
252+
* - duration.self / duration.total
253+
* - dmlCount.self / dmlCount.total
254+
* - soqlCount.self / soqlCount.total
255+
* - dmlRowCount.self / dmlRowCount.total
256+
* - soqlRowCount.self / soqlRowCount.total
257+
* - totalThrownCount (treated like a total metric for attribution)
246258
*/
247259
export function toBottomUpTree(rootChildren: LogEvent[]): BottomUpRow[] {
248260
if (rootChildren.length === 0) {

log-viewer/src/features/call-tree/utils/__tests__/Aggregation.test.ts

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ type PartitionRow = {
9595
_children: PartitionRow[] | null;
9696
};
9797

98+
function walkRows(rows: PartitionRow[], visit: (row: PartitionRow) => void): void {
99+
for (const row of rows) {
100+
visit(row);
101+
if (row._children && row._children.length > 0) {
102+
walkRows(row._children, visit);
103+
}
104+
}
105+
}
106+
98107
function assertPartitionInvariant(rows: PartitionRow[]): void {
99108
for (const row of rows) {
100109
if (!row._children || row._children.length === 0) {
@@ -140,6 +149,16 @@ function assertPartitionInvariant(rows: PartitionRow[]): void {
140149
}
141150
}
142151

152+
function assertRowsDoNotExceedGlobalTotals(rows: PartitionRow[], global: PartitionRow): void {
153+
walkRows(rows, (row) => {
154+
expect(row.totalTime).toBeLessThanOrEqual(global.totalTime);
155+
expect(row.dmlCount.total).toBeLessThanOrEqual(global.dmlCount.total);
156+
expect(row.soqlCount.total).toBeLessThanOrEqual(global.soqlCount.total);
157+
expect(row.dmlRowCount.total).toBeLessThanOrEqual(global.dmlRowCount.total);
158+
expect(row.soqlRowCount.total).toBeLessThanOrEqual(global.soqlRowCount.total);
159+
});
160+
}
161+
143162
function sumTraceSelfTime(events: LogEvent[]): number {
144163
let total = 0;
145164
for (const event of events) {
@@ -658,7 +677,7 @@ describe('toBottomUpTree', () => {
658677
expect(rootSelfBudget).toBe(traceSelfBudget);
659678
});
660679

661-
it('matches worked example A (pure recursion) from BOTTOM_UP_CALL_TREE_SPEC.md', () => {
680+
it('matches worked example A (pure recursion)', () => {
662681
const root = createEvent({ text: 'LOG_ROOT', self: 0, total: 0, type: 'EXECUTION_STARTED' });
663682
const outer = createEvent({ text: 'Outer', self: 100, total: 1000, parent: root });
664683
const r1 = createEvent({ text: 'recursive', self: 10, total: 35, parent: outer });
@@ -689,7 +708,7 @@ describe('toBottomUpTree', () => {
689708
expect(level3Outer._children).toBeNull();
690709
});
691710

692-
it('matches worked example B (other / sub other) from BOTTOM_UP_CALL_TREE_SPEC.md', () => {
711+
it('matches worked example B (other / sub other)', () => {
693712
const root = createEvent({ text: 'LOG_ROOT', self: 0, total: 0, type: 'EXECUTION_STARTED' });
694713
const outer = createEvent({ text: 'Outer', self: 0, total: 25, parent: root });
695714
const other = createEvent({ text: 'other', self: 10, total: 25, parent: outer });
@@ -713,7 +732,7 @@ describe('toBottomUpTree', () => {
713732
expect(otherOuter).toMatchObject({ totalSelfTime: 10, totalTime: 25 });
714733
});
715734

716-
it('matches worked example C (nested Search) from top-down-to-bottom-up-example.md', () => {
735+
it('matches worked example C (nested Search)', () => {
717736
const root = createEvent({ text: 'LOG_ROOT', self: 0, total: 0, type: 'EXECUTION_STARTED' });
718737
const outer = createEvent({ text: 'Outer', self: 100, total: 1000, parent: root });
719738

@@ -875,6 +894,97 @@ describe('toBottomUpTree', () => {
875894
expect(limitOnly.dmlCount.self).toBe(1);
876895
expect(limitOnly.totalThrownCount).toBe(1);
877896
});
897+
898+
it('orders roots deterministically by totalSelfTime desc, then name asc for ties', () => {
899+
const root = createEvent({ text: 'LOG_ROOT', self: 0, total: 0, type: 'EXECUTION_STARTED' });
900+
createEvent({ text: 'Zulu', self: 30, total: 30, parent: root });
901+
createEvent({ text: 'Alpha', self: 20, total: 20, parent: root });
902+
createEvent({ text: 'Beta', self: 20, total: 20, parent: root });
903+
904+
const rows = toBottomUpTree(root.children);
905+
expect(rows.map((row) => row.text)).toEqual(['Zulu', 'Alpha', 'Beta']);
906+
});
907+
908+
it('keeps every bottom-up total metric within the top-down global root totals', () => {
909+
const root = createEvent({ text: 'LOG_ROOT', self: 0, total: 0, type: 'EXECUTION_STARTED' });
910+
const outer = createEvent({
911+
text: 'Outer',
912+
self: 50,
913+
total: 500,
914+
parent: root,
915+
dmlSelf: 4,
916+
dmlTotal: 40,
917+
soqlSelf: 3,
918+
soqlTotal: 30,
919+
dmlRowSelf: 20,
920+
dmlRowTotal: 200,
921+
soqlRowSelf: 10,
922+
soqlRowTotal: 100,
923+
thrown: 7,
924+
});
925+
926+
const search = createEvent({
927+
text: 'Search',
928+
self: 25,
929+
total: 300,
930+
parent: outer,
931+
dmlSelf: 2,
932+
dmlTotal: 20,
933+
soqlSelf: 1,
934+
soqlTotal: 15,
935+
dmlRowSelf: 10,
936+
dmlRowTotal: 120,
937+
soqlRowSelf: 4,
938+
soqlRowTotal: 60,
939+
thrown: 3,
940+
});
941+
942+
createEvent({
943+
text: 'Search',
944+
self: 10,
945+
total: 150,
946+
parent: search,
947+
dmlSelf: 1,
948+
dmlTotal: 8,
949+
soqlSelf: 1,
950+
soqlTotal: 6,
951+
dmlRowSelf: 5,
952+
dmlRowTotal: 40,
953+
soqlRowSelf: 2,
954+
soqlRowTotal: 20,
955+
thrown: 1,
956+
});
957+
958+
createEvent({
959+
text: 'Worker',
960+
self: 60,
961+
total: 100,
962+
parent: outer,
963+
dmlSelf: 2,
964+
dmlTotal: 12,
965+
soqlSelf: 1,
966+
soqlTotal: 7,
967+
dmlRowSelf: 8,
968+
dmlRowTotal: 35,
969+
soqlRowSelf: 3,
970+
soqlRowTotal: 15,
971+
thrown: 1,
972+
});
973+
974+
const rows = toBottomUpTree(root.children) as PartitionRow[];
975+
const globalRoot: PartitionRow = {
976+
text: outer.text,
977+
totalTime: outer.duration.total,
978+
totalSelfTime: outer.duration.self,
979+
dmlCount: { self: outer.dmlCount.self, total: outer.dmlCount.total },
980+
soqlCount: { self: outer.soqlCount.self, total: outer.soqlCount.total },
981+
dmlRowCount: { self: outer.dmlRowCount.self, total: outer.dmlRowCount.total },
982+
soqlRowCount: { self: outer.soqlRowCount.self, total: outer.soqlRowCount.total },
983+
_children: null,
984+
};
985+
986+
assertRowsDoNotExceedGlobalTotals(rows, globalRoot);
987+
});
878988
});
879989

880990
describe('toAggregatedCallTree', () => {

0 commit comments

Comments
 (0)