Skip to content

Commit b6d7755

Browse files
Copilotyurishkuro
andcommitted
Keep childSpanIds in start-time order and add nodesBySpanId map
Based on feedback: 1. Keep childSpanIds sorted by start time (as they come from the tree) 2. Add nodesBySpanId: Map<string, TreeNode> to Trace type for direct tree navigation 3. Update getTraceSpanIdsAsTree to return {root, nodesBySpanId} 4. Update critical path to find last-finishing child from start-time sorted children 5. Update statistics self-time calculation to use childSpanIds instead of rebuilding parent-child map This eliminates the parentChildOfMap rebuilding and allows components to leverage the tree structure more efficiently. Co-authored-by: yurishkuro <3523016+yurishkuro@users.noreply.github.com>
1 parent d6cbd82 commit b6d7755

File tree

7 files changed

+70
-59
lines changed

7 files changed

+70
-59
lines changed

packages/jaeger-ui/src/components/TracePage/CriticalPath/utils/findLastFinishingChildSpan.tsx

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,32 @@ const findLastFinishingChildSpan = (
1313
currentSpan: Span,
1414
returningChildStartTime?: number
1515
): Span | undefined => {
16-
let lastFinishingChildSpanId: string | undefined;
17-
if (returningChildStartTime) {
18-
lastFinishingChildSpanId = currentSpan.childSpanIds.find(
19-
each =>
20-
// Look up the span using the map
21-
spanMap.has(each) &&
22-
spanMap.get(each)!.startTime + spanMap.get(each)!.duration < returningChildStartTime
23-
);
24-
} else {
25-
// If `returningChildStartTime` is not provided, select the first child span.
26-
// As they are sorted based on endTime
27-
lastFinishingChildSpanId = currentSpan.childSpanIds[0];
16+
let lastFinishingChildSpan: Span | undefined;
17+
let latestEndTime = -1;
18+
19+
// Iterate through children (now sorted by start time) to find the one that finishes last
20+
for (const childId of currentSpan.childSpanIds) {
21+
const childSpan = spanMap.get(childId);
22+
if (!childSpan) continue;
23+
24+
const childEndTime = childSpan.startTime + childSpan.duration;
25+
26+
if (returningChildStartTime) {
27+
// Find child that finishes just before returningChildStartTime
28+
if (childEndTime < returningChildStartTime && childEndTime > latestEndTime) {
29+
latestEndTime = childEndTime;
30+
lastFinishingChildSpan = childSpan;
31+
}
32+
} else {
33+
// Find child that finishes last overall
34+
if (childEndTime > latestEndTime) {
35+
latestEndTime = childEndTime;
36+
lastFinishingChildSpan = childSpan;
37+
}
38+
}
2839
}
29-
return lastFinishingChildSpanId ? spanMap.get(lastFinishingChildSpanId) : undefined;
40+
41+
return lastFinishingChildSpan;
3042
};
3143

3244
export default findLastFinishingChildSpan;

packages/jaeger-ui/src/components/TracePage/TraceStatistics/tableValues.tsx

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,23 @@
11
// Copyright (c) 2020 The Jaeger Authors.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import memoizeOne from 'memoize-one';
54
import { Trace, Span } from '../../../types/trace';
65
import { ITableSpan } from './types';
76
import colorGenerator from '../../../utils/color-generator';
87

98
const serviceName = 'Service Name';
109
const operationName = 'Operation Name';
1110

12-
function parentChildOfMap(allSpans: Span[]): Record<string, Span[]> {
13-
const parentChildOfMap: Record<string, Span[]> = {};
14-
allSpans.forEach(s => {
15-
if (s.references) {
16-
// Filter for CHILD_OF we don't want to calculate FOLLOWS_FROM (prod-cons)
17-
const parentIDs = s.references.filter(r => r.refType === 'CHILD_OF').map(r => r.spanID);
18-
parentIDs.forEach((pID: string) => {
19-
parentChildOfMap[pID] = parentChildOfMap[pID] || [];
20-
parentChildOfMap[pID].push(s);
21-
});
22-
}
23-
});
24-
return parentChildOfMap;
25-
}
26-
27-
const memoizedParentChildOfMap = memoizeOne(parentChildOfMap);
28-
29-
function getChildOfSpans(parentID: string, allSpans: Span[]): Span[] {
30-
return memoizedParentChildOfMap(allSpans)[parentID] || [];
31-
}
32-
33-
function computeSelfTime(parentSpan: Span, allSpans: Span[]): number {
11+
function computeSelfTime(parentSpan: Span, allSpans: Span[], spanMap: Map<string, Span>): number {
3412
if (!parentSpan.hasChildren) return parentSpan.duration;
3513

3614
let parentSpanSelfTime = parentSpan.duration;
3715
let previousChildEndTime = parentSpan.startTime;
3816

39-
const children = getChildOfSpans(parentSpan.spanID, allSpans).sort((a, b) => a.startTime - b.startTime);
17+
// Use childSpanIds from the span (already sorted by startTime from the tree)
18+
const children = parentSpan.childSpanIds
19+
.map(id => spanMap.get(id))
20+
.filter((child): child is Span => child !== undefined);
4021

4122
const parentSpanEndTime = parentSpan.startTime + parentSpan.duration;
4223

@@ -79,7 +60,13 @@ function computeSelfTime(parentSpan: Span, allSpans: Span[]): number {
7960
return parentSpanSelfTime;
8061
}
8162

82-
function computeColumnValues(trace: Trace, span: Span, allSpans: Span[], resultValue: StatsPerTag) {
63+
function computeColumnValues(
64+
trace: Trace,
65+
span: Span,
66+
allSpans: Span[],
67+
spanMap: Map<string, Span>,
68+
resultValue: StatsPerTag
69+
) {
8370
const resultValueChange = resultValue;
8471
resultValueChange.count += 1;
8572
resultValueChange.total += span.duration;
@@ -90,7 +77,7 @@ function computeColumnValues(trace: Trace, span: Span, allSpans: Span[], resultV
9077
resultValueChange.max = span.duration;
9178
}
9279

93-
const tempSelf = computeSelfTime(span, allSpans);
80+
const tempSelf = computeSelfTime(span, allSpans, spanMap);
9481
if (resultValueChange.selfMin > tempSelf) {
9582
resultValueChange.selfMin = tempSelf;
9683
}
@@ -178,6 +165,8 @@ function getTagValueFromSpan(tagKey: string, span: Span) {
178165
*/
179166
function valueFirstDropdown(selectedTagKey: string, trace: Trace) {
180167
const allSpans = trace.spans;
168+
// Use the pre-built spanMap or create one if not available (e.g., in tests)
169+
const spanMap = trace.spanMap || new Map(allSpans.map(s => [s.spanID, s]));
181170

182171
// used to build the table
183172
const allTableValues = [];
@@ -199,6 +188,7 @@ function valueFirstDropdown(selectedTagKey: string, trace: Trace) {
199188
trace,
200189
allSpans[i],
201190
allSpans,
191+
spanMap,
202192
statsPerTagValue[tagValue] ?? getDefaultStatsValue(trace)
203193
);
204194

@@ -251,7 +241,7 @@ function valueFirstDropdown(selectedTagKey: string, trace: Trace) {
251241
// Others is calculated
252242
let resultValue = getDefaultStatsValue(trace);
253243
for (let i = 0; i < spanWithNoSelectedTag.length; i++) {
254-
resultValue = computeColumnValues(trace, spanWithNoSelectedTag[i], allSpans, resultValue);
244+
resultValue = computeColumnValues(trace, spanWithNoSelectedTag[i], allSpans, spanMap, resultValue);
255245
}
256246
if (resultValue.count !== 0) {
257247
// Others is build
@@ -290,6 +280,7 @@ function valueFirstDropdown(selectedTagKey: string, trace: Trace) {
290280
function buildDetail(
291281
tempArray: Span[],
292282
allSpans: Span[],
283+
spanMap: Map<string, Span>,
293284
selectedTagKeySecond: string,
294285
parentName: string,
295286
trace: Trace
@@ -310,6 +301,7 @@ function buildDetail(
310301
trace,
311302
tempArray[i],
312303
allSpans,
304+
spanMap,
313305
statsPerTagValue[tagValue] ?? getDefaultStatsValue(trace)
314306
);
315307

@@ -355,7 +347,12 @@ function buildDetail(
355347
/**
356348
* Used to generate detail rest.
357349
*/
358-
function generateDetailRest(allColumnValues: ITableSpan[], selectedTagKeySecond: string, trace: Trace) {
350+
function generateDetailRest(
351+
allColumnValues: ITableSpan[],
352+
selectedTagKeySecond: string,
353+
trace: Trace,
354+
spanMap: Map<string, Span>
355+
) {
359356
const allSpans = trace.spans;
360357
const newTable = [];
361358
for (let i = 0; i < allColumnValues.length; i++) {
@@ -386,7 +383,7 @@ function generateDetailRest(allColumnValues: ITableSpan[], selectedTagKeySecond:
386383
}
387384
}
388385
if (rest) {
389-
resultValue = computeColumnValues(trace, allSpans[j], allSpans, resultValue);
386+
resultValue = computeColumnValues(trace, allSpans[j], allSpans, spanMap, resultValue);
390387
}
391388
}
392389
}
@@ -432,6 +429,8 @@ function valueSecondDropdown(
432429
) {
433430
const allSpans = trace.spans;
434431
const allTableValues = [];
432+
// Use the pre-built spanMap or create one if not available (e.g., in tests)
433+
const spanMap = trace.spanMap || new Map(allSpans.map(s => [s.spanID, s]));
435434

436435
const isSecondDropdownTag = selectedTagKeySecond !== serviceName && selectedTagKeySecond !== operationName;
437436

@@ -468,6 +467,7 @@ function valueSecondDropdown(
468467
const newColumnValues = buildDetail(
469468
spansWithSecondTag,
470469
allSpans,
470+
spanMap,
471471
selectedTagKeySecond,
472472
actualTableValues[i].name,
473473
trace
@@ -484,7 +484,7 @@ function valueSecondDropdown(
484484

485485
// if second dropdown is a tag a rest must be created
486486
if (isSecondDropdownTag) {
487-
return generateDetailRest(allTableValues, selectedTagKeySecond, trace);
487+
return generateDetailRest(allTableValues, selectedTagKeySecond, trace, spanMap);
488488
// if no tag is selected the values can be returned
489489
}
490490
return allTableValues;

packages/jaeger-ui/src/model/ddg/transformTracesToPaths.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function transformTracesToPaths(
2222
// Use the pre-built spanMap and tree from the trace object if available,
2323
// otherwise build them on demand (e.g., in tests)
2424
const spanMap = data.spanMap || null;
25-
const tree = data.tree || getTraceSpanIdsAsTree(data, spanMap);
25+
const tree = data.tree || getTraceSpanIdsAsTree(data, spanMap).root;
2626
const { traceID } = data;
2727
// Ensure we have a spanMap for looking up spans
2828
const effectiveSpanMap = spanMap || new Map(data.spans.map(span => [span.spanID, span]));

packages/jaeger-ui/src/model/transform-trace-data.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export default function transformTraceData(data: TraceData & { spans: SpanData[]
105105
}
106106
// tree is necessary to sort the spans, so children follow parents, and
107107
// siblings are sorted by start time
108-
const tree = getTraceSpanIdsAsTree(data, spanMap);
108+
const { root: tree, nodesBySpanId } = getTraceSpanIdsAsTree(data, spanMap);
109109
const spans: Span[] = [];
110110
const svcCounts: Record<string, number> = {};
111111

@@ -122,16 +122,8 @@ export default function transformTraceData(data: TraceData & { spans: SpanData[]
122122
span.relativeStartTime = span.startTime - traceStartTime;
123123
span.depth = depth - 1;
124124
span.hasChildren = node.children.length > 0;
125-
// Get the childSpanIds sorted based on endTime without changing tree structure
126-
// TODO move this enrichment into Critical Path computation
127-
span.childSpanIds = node.children
128-
.slice()
129-
.sort((a, b) => {
130-
const spanA = spanMap.get(a.value)!;
131-
const spanB = spanMap.get(b.value)!;
132-
return spanB.startTime + spanB.duration - (spanA.startTime + spanA.duration);
133-
})
134-
.map(each => each.value);
125+
// Get the childSpanIds sorted by startTime (as they are in the tree)
126+
span.childSpanIds = node.children.map(each => each.value);
135127
span.warnings = span.warnings || [];
136128
span.tags = span.tags || [];
137129
span.references = span.references || [];
@@ -182,6 +174,7 @@ export default function transformTraceData(data: TraceData & { spans: SpanData[]
182174
// Optimized data structures - created once during trace transformation
183175
spanMap,
184176
tree,
177+
nodesBySpanId,
185178
// Can't use spread operator for intersection types
186179
// repl: https://goo.gl/4Z23MJ
187180
// issue: https://github.com/facebook/flow/issues/1511

packages/jaeger-ui/src/selectors/trace.test.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('getTraceSpanIdsAsTree()', () => {
1111
const getTraceSpansAsMap = trace =>
1212
trace.spans.reduce((map, span) => map.set(span.spanID, span), new Map());
1313
it('builds the tree properly', () => {
14-
const tree = traceSelectors.getTraceSpanIdsAsTree(generatedTrace);
14+
const { root: tree, nodesBySpanId } = traceSelectors.getTraceSpanIdsAsTree(generatedTrace);
1515
const spanMap = getTraceSpansAsMap(generatedTrace);
1616

1717
tree.walk((value, node) => {
@@ -22,6 +22,12 @@ describe('getTraceSpanIdsAsTree()', () => {
2222
expect(parentId).toBe(expectedParentValue);
2323
});
2424
});
25+
26+
// Verify nodesBySpanId contains all spans
27+
expect(nodesBySpanId.size).toBe(generatedTrace.spans.length);
28+
generatedTrace.spans.forEach(span => {
29+
expect(nodesBySpanId.has(span.spanID)).toBe(true);
30+
});
2531
});
2632

2733
it('#115 - handles FOLLOW_FROM refs', () => {

packages/jaeger-ui/src/selectors/trace.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ export const TREE_ROOT_ID = '__root__';
1818
*
1919
* @param {Trace} trace The trace to build the tree of spanIDs.
2020
* @param {Map<string, Span>} spanMap map from span IDs to Spans
21-
* @return {TreeNode} A tree of spanIDs derived from the relationships
22-
* between spans in the trace.
21+
* @return {{ root: TreeNode, nodesBySpanId: Map }} The tree root and a map of spanID to TreeNode
2322
*/
2423
export function getTraceSpanIdsAsTree(
2524
trace: { spans: SpanData[] },
@@ -60,5 +59,5 @@ export function getTraceSpanIdsAsTree(
6059
}
6160
});
6261
root.children.sort(comparator);
63-
return root;
62+
return { root, nodesBySpanId: nodesById };
6463
}

packages/jaeger-ui/src/types/trace.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export type Trace = TraceData & {
8383
// Optional to support test scenarios where traces may be mocked
8484
spanMap?: Map<string, Span>;
8585
tree?: TreeNode<string>;
86+
nodesBySpanId?: Map<string, TreeNode<string>>;
8687

8788
// OTEL facade - lazy-initialized and memoized
8889
_otelFacade?: IOtelTrace;

0 commit comments

Comments
 (0)