Trace View: Critical path highlighting (#76857)

* Added critical path computation code. Refactor some trace view code

* Refactor js to ts

* First implementation of critical path working

* Simplified code

* Added filter to show only critical path spans

* Lint and stuff

* Fixes and moving styling to object

* Betterer
This commit is contained in:
Andre Pereira 2023-10-30 10:43:14 +00:00 committed by GitHub
parent 4ed36cbc1d
commit 107cf0dc04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1793 additions and 143 deletions

View File

@ -3920,13 +3920,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "11"],
[0, 0, 0, "Styles should be written using objects.", "12"]
],
"public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"],
[0, 0, 0, "Styles should be written using objects.", "4"]
],
"public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],

View File

@ -35,6 +35,7 @@ import {
TraceTimelineViewer,
TTraceTimeline,
} from './components';
import memoizedTraceCriticalPath from './components/CriticalPath';
import SpanGraph from './components/TracePageHeader/SpanGraph';
import { TopOfViewRefType } from './components/TraceTimelineViewer/VirtualizedTraceView';
import { createSpanLinkFactory } from './createSpanLink';
@ -99,6 +100,7 @@ export function TraceView(props: Props) {
const [focusedSpanIdForSearch, setFocusedSpanIdForSearch] = useState('');
const [showSpanFilters, setShowSpanFilters] = useToggle(false);
const [showSpanFilterMatchesOnly, setShowSpanFilterMatchesOnly] = useState(false);
const [showCriticalPathSpansOnly, setShowCriticalPathSpansOnly] = useState(false);
const [headerHeight, setHeaderHeight] = useState(100);
const styles = useStyles2(getStyles);
@ -159,6 +161,8 @@ export function TraceView(props: Props) {
? props.scrollElement
: document.getElementsByClassName(props.scrollElementClass ?? '')[0];
const criticalPath = memoizedTraceCriticalPath(traceProp);
return (
<>
{props.dataFrames?.length && traceProp ? (
@ -173,6 +177,8 @@ export function TraceView(props: Props) {
setShowSpanFilters={setShowSpanFilters}
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}
setShowSpanFilterMatchesOnly={setShowSpanFilterMatchesOnly}
showCriticalPathSpansOnly={showCriticalPathSpansOnly}
setShowCriticalPathSpansOnly={setShowCriticalPathSpansOnly}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
spanFilterMatches={spanFilterMatches}
datasourceType={datasourceType}
@ -218,10 +224,12 @@ export function TraceView(props: Props) {
focusedSpanId={focusedSpanId}
focusedSpanIdForSearch={focusedSpanIdForSearch}
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}
showCriticalPathSpansOnly={showCriticalPathSpansOnly}
createFocusSpanLink={createFocusSpanLink}
topOfViewRef={topOfViewRef}
topOfViewRefType={topOfViewRefType}
headerHeight={headerHeight}
criticalPath={criticalPath}
/>
</>
) : (

View File

@ -0,0 +1,35 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import test1 from './testCases/test1';
import test2 from './testCases/test2';
import test3 from './testCases/test3';
import test4 from './testCases/test4';
import test5 from './testCases/test5';
import test6 from './testCases/test6';
import test7 from './testCases/test7';
import test8 from './testCases/test8';
import test9 from './testCases/test9';
import TraceCriticalPath from './index';
describe.each([[test1], [test2], [test3], [test4], [test5], [test6], [test7], [test8], [test9]])(
'Happy Path',
(testProps) => {
it('should find criticalPathSections correctly', () => {
const criticalPath = TraceCriticalPath(testProps.trace);
expect(criticalPath).toStrictEqual(testProps.criticalPathSections);
});
}
);

View File

@ -0,0 +1,115 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import memoizeOne from 'memoize-one';
import { CriticalPathSection, Trace, TraceSpan } from '../types';
import findLastFinishingChildSpan from './utils/findLastFinishingChildSpan';
import getChildOfSpans from './utils/getChildOfSpans';
import sanitizeOverFlowingChildren from './utils/sanitizeOverFlowingChildren';
/**
* Computes the critical path sections of a Jaeger trace.
* The algorithm begins with the top-level span and iterates through the last finishing children (LFCs).
* It recursively computes the critical path for each LFC span.
* Upon return from recursion, the algorithm walks backward and picks another child that
* finished just before the LFC's start.
* @param spanMap - A map associating span IDs with spans.
* @param spanId - The ID of the current span.
* @param criticalPath - An array of critical path sections.
* @param returningChildStartTime - Optional parameter representing the span's start time.
* It is provided only during the recursive return phase.
* @returns - An array of critical path sections for the trace.
* @example -
* |-------------spanA--------------|
* |--spanB--| |--spanC--|
* The LFC of spanA is spanC, as it finishes last among its child spans.
* After invoking CP recursively on LFC, for spanC there is no LFC, so the algorithm walks backward.
* At this point, it uses returningChildStartTime (startTime of spanC) to select another child that finished
* immediately before the LFC's start.
*/
const computeCriticalPath = (
spanMap: Map<string, TraceSpan>,
spanId: string,
criticalPath: CriticalPathSection[],
returningChildStartTime?: number
): CriticalPathSection[] => {
const currentSpan = spanMap.get(spanId);
if (!currentSpan) {
return criticalPath;
}
const lastFinishingChildSpan = findLastFinishingChildSpan(spanMap, currentSpan, returningChildStartTime);
let spanCriticalSection: CriticalPathSection;
if (lastFinishingChildSpan) {
spanCriticalSection = {
spanId: currentSpan.spanID,
section_start: lastFinishingChildSpan.startTime + lastFinishingChildSpan.duration,
section_end: returningChildStartTime || currentSpan.startTime + currentSpan.duration,
};
if (spanCriticalSection.section_start !== spanCriticalSection.section_end) {
criticalPath.push(spanCriticalSection);
}
// Now focus shifts to the lastFinishingChildSpan of cuurent span
computeCriticalPath(spanMap, lastFinishingChildSpan.spanID, criticalPath);
} else {
// If there is no last finishing child then total section upto startTime of span is on critical path
spanCriticalSection = {
spanId: currentSpan.spanID,
section_start: currentSpan.startTime,
section_end: returningChildStartTime || currentSpan.startTime + currentSpan.duration,
};
if (spanCriticalSection.section_start !== spanCriticalSection.section_end) {
criticalPath.push(spanCriticalSection);
}
// Now as there are no lfc's focus shifts to parent span from startTime of span
// return from recursion and walk backwards to one level depth to parent span
// provide span's startTime as returningChildStartTime
if (currentSpan.references.length) {
const parentSpanId: string = currentSpan.references.filter((reference) => reference.refType === 'CHILD_OF')[0]
.spanID;
computeCriticalPath(spanMap, parentSpanId, criticalPath, currentSpan.startTime);
}
}
return criticalPath;
};
function criticalPathForTrace(trace: Trace) {
let criticalPath: CriticalPathSection[] = [];
// As spans are already sorted based on startTime first span is always rootSpan
const rootSpanId = trace?.spans[0].spanID;
// If there is root span then algorithm implements
if (rootSpanId) {
const spanMap = trace.spans.reduce((map, span) => {
map.set(span.spanID, span);
return map;
}, new Map<string, TraceSpan>());
try {
const refinedSpanMap = getChildOfSpans(spanMap);
const sanitizedSpanMap = sanitizeOverFlowingChildren(refinedSpanMap);
criticalPath = computeCriticalPath(sanitizedSpanMap, rootSpanId, criticalPath);
} catch (error) {
/* eslint-disable no-console */
console.log('error while computing critical path for a trace', error);
}
}
return criticalPath;
}
const memoizedTraceCriticalPath = memoizeOne(criticalPathForTrace);
export default memoizedTraceCriticalPath;

View File

@ -0,0 +1,126 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
|
Span C |
| span C
+++ +++++++++ ++++ | / \
| / \
| span D span E
Span D Span E |
| (parent-child tree)
+++++++++++ ++++++++++++ |
Here +++++ are critical path sections
*/
import { Trace, TraceResponse, transformTraceData } from '../../index';
const testTrace: TraceResponse = {
traceID: 'test1-trace',
spans: [
{
traceID: 'test1-trace',
spanID: 'span-E',
operationName: 'operation E',
references: [
{
refType: 'CHILD_OF',
spanID: 'span-C',
traceID: 'test1-trace',
},
],
startTime: 50,
duration: 10,
processID: 'p1',
logs: [],
flags: 0,
},
{
traceID: 'test1-trace',
spanID: 'span-C',
operationName: 'operation C',
references: [],
startTime: 1,
duration: 100,
processID: 'p1',
logs: [],
flags: 0,
},
{
traceID: 'test1-trace',
spanID: 'span-D',
operationName: 'operation D',
references: [
{
refType: 'CHILD_OF',
spanID: 'span-C',
traceID: 'test1-trace',
},
],
startTime: 20,
duration: 20,
processID: 'p1',
logs: [],
flags: 0,
},
],
processes: {
p1: {
serviceName: 'customers-service',
tags: [],
},
},
};
const transformedTrace: Trace = transformTraceData(testTrace)!;
const criticalPathSections = [
{
spanId: 'span-C',
section_start: 60,
section_end: 101,
},
{
spanId: 'span-E',
section_start: 50,
section_end: 60,
},
{
spanId: 'span-C',
section_start: 40,
section_end: 50,
},
{
spanId: 'span-D',
section_start: 20,
section_end: 40,
},
{
spanId: 'span-C',
section_start: 1,
section_end: 20,
},
];
const test1 = {
criticalPathSections,
trace: transformedTrace,
};
export default test1;

View File

@ -0,0 +1,116 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/* |
|
Span X |
|
++++++++++++++ |++++++++++ | span X
| | / \
Span A | | / \
| | span A span C
| |
| |
| (parent-child tree)
Span C |
|
++++++++++++++++++++++++++ |
|
Here ++++++ is critical path |
*/
import { TraceResponse, transformTraceData } from '../../index';
const happyTrace: TraceResponse = {
traceID: 'trace-123',
spans: [
{
traceID: 'trace-123',
spanID: 'span-X',
operationName: 'op1',
startTime: 1,
duration: 100,
references: [],
processID: 'p1',
logs: [],
flags: 0,
},
{
traceID: 'trace-123',
spanID: 'span-A',
operationName: 'op2',
startTime: 10,
duration: 40,
references: [
{
refType: 'CHILD_OF',
spanID: 'span-X',
traceID: 'trace-123',
},
],
processID: 'p1',
logs: [],
flags: 0,
},
{
traceID: 'trace-123',
spanID: 'span-C',
operationName: 'op3',
startTime: 20,
duration: 40,
references: [
{
refType: 'CHILD_OF',
spanID: 'span-X',
traceID: 'trace-123',
},
],
processID: 'p1',
logs: [],
flags: 0,
},
],
processes: {
p1: {
serviceName: 'service1',
tags: [],
},
},
};
const transformedTrace = transformTraceData(happyTrace)!;
const criticalPathSections = [
{
spanId: 'span-X',
section_start: 60,
section_end: 101,
},
{
spanId: 'span-C',
section_start: 20,
section_end: 60,
},
{
spanId: 'span-X',
section_start: 1,
section_end: 20,
},
];
const test2 = {
criticalPathSections,
trace: transformedTrace,
};
export default test2;

View File

@ -0,0 +1,113 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
|
|
Span A | span A
| /
++++++++++++ | /
Span B | span B
|
| (parent-child tree)
|
Span B will be dropped. |
span A is on critical path(+++++) |
*/
import { TraceResponse, transformTraceData } from '../../index';
const trace: TraceResponse = {
traceID: '006c3cf93508f205',
spans: [
{
traceID: '006c3cf93508f205',
spanID: '006c3cf93508f205',
flags: 1,
operationName: 'send',
references: [],
startTime: 1679437737490189,
duration: 36,
tags: [
{
key: 'span.kind',
type: 'string',
value: 'producer',
},
],
logs: [],
processID: 'p1',
warnings: null,
},
{
traceID: '006c3cf93508f205',
spanID: '2dc4b796e2127e32',
flags: 1,
operationName: 'async task 1',
references: [
{
refType: 'CHILD_OF',
traceID: '006c3cf93508f205',
spanID: '006c3cf93508f205',
},
],
startTime: 1679437737491529,
duration: 79182,
tags: [
{
key: 'span.kind',
type: 'string',
value: 'client',
},
{
key: 'http.method',
type: 'string',
value: 'POST',
},
],
logs: [],
processID: 'p2',
warnings: null,
},
],
processes: {
p1: {
serviceName: 'service-one',
tags: [],
},
p2: {
serviceName: 'service-two',
tags: [],
},
},
warnings: null,
};
const transformedTrace = transformTraceData(trace)!;
const traceStart = 1679437737490189;
const criticalPathSections = [
{
spanId: '006c3cf93508f205',
section_start: traceStart,
section_end: traceStart + 36,
},
];
const test3 = {
criticalPathSections,
trace: transformedTrace,
};
export default test3;

View File

@ -0,0 +1,106 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
|
Span A |
|
++++++++++++ | span A
Span B | /
| /
| span B
| /
| /
Span C | span C
|
| (parent-child tree)
Both spanB and spanC will be dropped. |
span A is on critical path(+++++) |
*/
import { TraceResponse, transformTraceData } from '../../index';
const trace: TraceResponse = {
traceID: 'trace-abc',
spans: [
{
traceID: 'trace-abc',
spanID: 'span-A',
operationName: 'op-A',
references: [],
startTime: 1,
duration: 30,
processID: 'p1',
logs: [],
flags: 0,
},
{
traceID: 'trace-abc',
spanID: 'span-B',
operationName: 'op-B',
references: [
{
refType: 'CHILD_OF',
spanID: 'span-A',
traceID: 'trace-abc',
},
],
startTime: 40,
duration: 40,
processID: 'p1',
logs: [],
flags: 0,
},
{
traceID: 'trace-abc',
spanID: 'span-c',
operationName: 'op-C',
references: [
{
refType: 'CHILD_OF',
spanID: 'span-B',
traceID: 'trace-abc',
},
],
startTime: 50,
duration: 10,
processID: 'p1',
logs: [],
flags: 0,
},
],
processes: {
p1: {
serviceName: 'service-one',
tags: [],
},
},
};
const transformedTrace = transformTraceData(trace)!;
const criticalPathSections = [
{
spanId: 'span-A',
section_start: 1,
section_end: 31,
},
];
const test4 = {
criticalPathSections,
trace: transformedTrace,
};
export default test4;

View File

@ -0,0 +1,105 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
|
Span A | span A
| /
| /
| span B(FOLLOW_FROM)
Span B | /
| /
| span C(CHILD_OF)
|
Span C |
| (parent-child tree)
|
Here span B is ref-type is 'FOLLOWS_FROM' |
*/
import { TraceResponse, transformTraceData } from '../../index';
const trace: TraceResponse = {
traceID: 'trace-abc',
spans: [
{
traceID: 'trace-abc',
spanID: 'span-A',
operationName: 'op-A',
references: [],
startTime: 1,
duration: 30,
processID: 'p1',
logs: [],
flags: 0,
},
{
traceID: 'trace-abc',
spanID: 'span-B',
operationName: 'op-B',
references: [
{
refType: 'FOLLOWS_FROM',
spanID: 'span-A',
traceID: 'trace-abc',
},
],
startTime: 10,
duration: 10,
processID: 'p1',
logs: [],
flags: 0,
},
{
traceID: 'trace-abc',
spanID: 'span-C',
operationName: 'op-C',
references: [
{
refType: 'CHILD_OF',
spanID: 'span-B',
traceID: 'trace-abc',
},
],
startTime: 12,
duration: 2,
processID: 'p1',
logs: [],
flags: 0,
},
],
processes: {
p1: {
serviceName: 'service-one',
tags: [],
},
},
};
const transformedTrace = transformTraceData(trace)!;
const criticalPathSections = [
{
spanId: 'span-A',
section_start: 1,
section_end: 31,
},
];
const test5 = {
criticalPathSections,
trace: transformedTrace,
};
export default test5;

View File

@ -0,0 +1,113 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
|
Span A |
| span A
| /
| /
Span B | span B
| /
| /
| span C
Span C |
| (parent-child tree)
*/
import { TraceResponse, transformTraceData } from '../../index';
const trace: TraceResponse = {
traceID: 'trace-abc',
spans: [
{
traceID: 'trace-abc',
spanID: 'span-A',
operationName: 'op-A',
references: [],
startTime: 1,
duration: 29,
processID: 'p1',
logs: [],
flags: 0,
},
{
traceID: 'trace-abc',
spanID: 'span-B',
operationName: 'op-B',
references: [
{
refType: 'CHILD_OF',
spanID: 'span-A',
traceID: 'trace-abc',
},
],
startTime: 15,
duration: 20,
processID: 'p1',
logs: [],
flags: 0,
},
{
traceID: 'trace-abc',
spanID: 'span-C',
operationName: 'op-C',
references: [
{
refType: 'CHILD_OF',
spanID: 'span-B',
traceID: 'trace-abc',
},
],
startTime: 10,
duration: 15,
processID: 'p1',
logs: [],
flags: 0,
},
],
processes: {
p1: {
serviceName: 'service-one',
tags: [],
},
},
};
const transformedTrace = transformTraceData(trace)!;
const criticalPathSections = [
{
spanId: 'span-B',
section_start: 25,
section_end: 30,
},
{
spanId: 'span-C',
section_start: 15,
section_end: 25,
},
{
spanId: 'span-A',
section_start: 1,
section_end: 15,
},
];
const test6 = {
criticalPathSections,
trace: transformedTrace,
};
export default test6;

View File

@ -0,0 +1,113 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
|
Span A | spanA
| /
| /
| spanB
Span B | /
| /
| spanC
|
Span C | ((parent-child tree))
|
*/
import { TraceResponse, transformTraceData } from '../../index';
const trace: TraceResponse = {
traceID: 'trace-abc',
spans: [
{
traceID: 'trace-abc',
spanID: 'span-A',
operationName: 'op-A',
references: [],
startTime: 1,
duration: 29,
processID: 'p1',
logs: [],
flags: 0,
},
{
traceID: 'trace-abc',
spanID: 'span-B',
operationName: 'op-B',
references: [
{
refType: 'CHILD_OF',
spanID: 'span-A',
traceID: 'trace-abc',
},
],
startTime: 15,
duration: 20,
processID: 'p1',
logs: [],
flags: 0,
},
{
traceID: 'trace-abc',
spanID: 'span-C',
operationName: 'op-C',
references: [
{
refType: 'CHILD_OF',
spanID: 'span-B',
traceID: 'trace-abc',
},
],
startTime: 20,
duration: 20,
processID: 'p1',
logs: [],
flags: 0,
},
],
processes: {
p1: {
serviceName: 'service-one',
tags: [],
},
},
};
const transformedTrace = transformTraceData(trace)!;
const criticalPathSections = [
{
spanId: 'span-C',
section_start: 20,
section_end: 30,
},
{
spanId: 'span-B',
section_start: 15,
section_end: 20,
},
{
spanId: 'span-A',
section_start: 1,
section_end: 15,
},
];
const test7 = {
criticalPathSections,
trace: transformedTrace,
};
export default test7;

View File

@ -0,0 +1,82 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceResponse, transformTraceData } from '../../index';
/*
|
Span A | spanA
| /
| /
| spanB (CHILD_OF)
Span B |
| ((parent-child tree))
*/
const trace: TraceResponse = {
traceID: 'trace-abc',
spans: [
{
traceID: 'trace-abc',
spanID: 'span-A',
operationName: 'op-A',
references: [],
startTime: 10,
duration: 20,
processID: 'p1',
logs: [],
flags: 0,
},
{
traceID: 'trace-abc',
spanID: 'span-B',
operationName: 'op-B',
references: [
{
refType: 'CHILD_OF',
spanID: 'span-A',
traceID: 'trace-abc',
},
],
startTime: 5,
duration: 30,
processID: 'p1',
logs: [],
flags: 0,
},
],
processes: {
p1: {
serviceName: 'service-one',
tags: [],
},
},
};
const transformedTrace = transformTraceData(trace)!;
const criticalPathSections = [
{
spanId: 'span-B',
section_start: 10,
section_end: 30,
},
];
const test8 = {
criticalPathSections,
trace: transformedTrace,
};
export default test8;

View File

@ -0,0 +1,84 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceResponse, transformTraceData } from '../../index';
/*
|
Span A | span A
| /
++++++++++++ | /
| span B
Span B |
| (parent-child tree)
spanB will be dropped. |
span A is on critical path(+++++) |
*/
const trace: TraceResponse = {
traceID: 'trace-abc',
spans: [
{
traceID: 'trace-abc',
spanID: 'span-A',
operationName: 'op-A',
references: [],
startTime: 10,
duration: 20,
processID: 'p1',
logs: [],
flags: 0,
},
{
traceID: 'trace-abc',
spanID: 'span-B',
operationName: 'op-B',
references: [
{
refType: 'CHILD_OF',
spanID: 'span-A',
traceID: 'trace-abc',
},
],
startTime: 1,
duration: 4,
processID: 'p1',
logs: [],
flags: 0,
},
],
processes: {
p1: {
serviceName: 'service-one',
tags: [],
},
},
};
const transformedTrace = transformTraceData(trace)!;
const criticalPathSections = [
{
spanId: 'span-A',
section_start: 10,
section_end: 30,
},
];
const test9 = {
criticalPathSections,
trace: transformedTrace,
};
export default test9;

View File

@ -0,0 +1,48 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import test1 from '../testCases/test1';
import test2 from '../testCases/test2';
import findLastFinishingChildSpanId from './findLastFinishingChildSpan';
import getChildOfSpans from './getChildOfSpans';
import sanitizeOverFlowingChildren from './sanitizeOverFlowingChildren';
describe('findLastFinishingChildSpanId', () => {
it('Should find lfc of a span correctly', () => {
const refinedSpanData = getChildOfSpans(new Map(test1.trace.spans.map((span) => [span.spanID, span])));
const sanitizedSpanMap = sanitizeOverFlowingChildren(refinedSpanData);
const currentSpan = sanitizedSpanMap.get('span-C')!;
let lastFinishingChildSpan = findLastFinishingChildSpanId(sanitizedSpanMap, currentSpan);
expect(lastFinishingChildSpan).toStrictEqual(sanitizedSpanMap.get('span-E'));
// Second Case to check if it works with spawn time or not
lastFinishingChildSpan = findLastFinishingChildSpanId(sanitizedSpanMap, currentSpan, 50);
expect(lastFinishingChildSpan).toStrictEqual(sanitizedSpanMap.get('span-D'));
});
it('Should find lfc of a span correctly', () => {
const refinedSpanData = getChildOfSpans(new Map(test2.trace.spans.map((span) => [span.spanID, span])));
const sanitizedSpanMap = sanitizeOverFlowingChildren(refinedSpanData);
const currentSpan = sanitizedSpanMap.get('span-X')!;
let lastFinishingChildSpanId = findLastFinishingChildSpanId(sanitizedSpanMap, currentSpan);
expect(lastFinishingChildSpanId).toStrictEqual(sanitizedSpanMap.get('span-C'));
// Second Case to check if it works with spawn time or not
lastFinishingChildSpanId = findLastFinishingChildSpanId(sanitizedSpanMap, currentSpan, 20);
expect(lastFinishingChildSpanId).toBeUndefined();
});
});

View File

@ -0,0 +1,42 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceSpan } from '../../types';
/**
* @returns - Returns the span that finished last among the remaining child spans.
* If a `returningChildStartTime` is provided as a parameter, it returns the child span that finishes
* just before the specified `returningChildStartTime`.
*/
const findLastFinishingChildSpan = (
spanMap: Map<string, TraceSpan>,
currentSpan: TraceSpan,
returningChildStartTime?: number
): TraceSpan | undefined => {
let lastFinishingChildSpanId: string | undefined;
if (returningChildStartTime) {
lastFinishingChildSpanId = currentSpan?.childSpanIds.find(
(each) =>
// Look up the span using the map
spanMap.has(each) && spanMap.get(each)!.startTime + spanMap.get(each)!.duration < returningChildStartTime
);
} else {
// If `returningChildStartTime` is not provided, select the first child span.
// As they are sorted based on endTime
lastFinishingChildSpanId = currentSpan.childSpanIds[0];
}
return lastFinishingChildSpanId ? spanMap.get(lastFinishingChildSpanId) : undefined;
};
export default findLastFinishingChildSpan;

View File

@ -0,0 +1,46 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import test2 from '../testCases/test2';
import test5 from '../testCases/test5';
import getChildOfSpans from './getChildOfSpans';
describe('getChildOfSpans', () => {
it('Should not remove CHILD_OF child spans if there are any', () => {
const spanMap = test2.trace.spans.reduce((map, span) => {
map.set(span.spanID, span);
return map;
}, new Map());
const refinedSpanMap = getChildOfSpans(spanMap);
const expectedRefinedSpanMap = spanMap;
expect(refinedSpanMap.size).toBe(3);
expect(refinedSpanMap).toStrictEqual(expectedRefinedSpanMap);
});
it('Should remove FOLLOWS_FROM child spans if there are any', () => {
const spanMap = test5.trace.spans.reduce((map, span) => {
map.set(span.spanID, span);
return map;
}, new Map());
const refinedSpanMap = getChildOfSpans(spanMap);
const expectedRefinedSpanMap = new Map().set(test5.trace.spans[0].spanID, {
...test5.trace.spans[0],
childSpanIds: [],
});
expect(refinedSpanMap.size).toBe(1);
expect(refinedSpanMap).toStrictEqual(expectedRefinedSpanMap);
});
});

View File

@ -0,0 +1,54 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceSpan } from '../../types';
/**
* Removes child spans whose refType is FOLLOWS_FROM and their descendants.
* @param spanMap - The map containing spans.
* @returns - A map with spans whose refType is CHILD_OF.
*/
const getChildOfSpans = (spanMap: Map<string, TraceSpan>): Map<string, TraceSpan> => {
const followFromSpanIds: string[] = [];
const followFromSpansDescendantIds: string[] = [];
// First find all FOLLOWS_FROM refType spans
spanMap.forEach((each) => {
if (each.references[0]?.refType === 'FOLLOWS_FROM') {
followFromSpanIds.push(each.spanID);
// Remove the spanId from childSpanIds array of its parentSpan
const parentSpan = spanMap.get(each.references[0].spanID)!;
parentSpan.childSpanIds = parentSpan.childSpanIds.filter((a) => a !== each.spanID);
spanMap.set(parentSpan.spanID, { ...parentSpan });
}
});
// Recursively find all Descendants of FOLLOWS_FROM spans
const findDescendantSpans = (spanIds: string[]) => {
spanIds.forEach((spanId) => {
const span = spanMap.get(spanId)!;
if (span.hasChildren) {
followFromSpansDescendantIds.push(...span.childSpanIds);
findDescendantSpans(span.childSpanIds);
}
});
};
findDescendantSpans(followFromSpanIds);
// Delete all FOLLOWS_FROM spans and its descendants
const idsToBeDeleted = [...followFromSpanIds, ...followFromSpansDescendantIds];
idsToBeDeleted.forEach((id) => spanMap.delete(id));
return spanMap;
};
export default getChildOfSpans;

View File

@ -0,0 +1,53 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceSpan } from '../../types';
import test3 from '../testCases/test3';
import test4 from '../testCases/test4';
import test6 from '../testCases/test6';
import test7 from '../testCases/test7';
import test8 from '../testCases/test8';
import test9 from '../testCases/test9';
import getChildOfSpans from './getChildOfSpans';
import sanitizeOverFlowingChildren from './sanitizeOverFlowingChildren';
// Function to make expected data for test6 and test7
function getExpectedSanitizedData(spans: TraceSpan[], test: 'test6' | 'test7' | 'test8') {
const testSanitizedData = {
test6: [spans[0], { ...spans[1], duration: 15 }, { ...spans[2], duration: 10, startTime: 15 }],
test7: [spans[0], { ...spans[1], duration: 15 }, { ...spans[2], duration: 10 }],
test8: [spans[0], { ...spans[1], startTime: 10, duration: 20 }],
};
const spanMap = testSanitizedData[test].reduce((map, span) => {
map.set(span.spanID, span);
return map;
}, new Map());
return spanMap;
}
describe.each([
[test3, new Map().set(test3.trace.spans[0].spanID, { ...test3.trace.spans[0], childSpanIds: [] })],
[test4, new Map().set(test4.trace.spans[0].spanID, { ...test4.trace.spans[0], childSpanIds: [] })],
[test6, getExpectedSanitizedData(test6.trace.spans, 'test6')],
[test7, getExpectedSanitizedData(test7.trace.spans, 'test7')],
[test8, getExpectedSanitizedData(test8.trace.spans, 'test8')],
[test9, new Map().set(test9.trace.spans[0].spanID, { ...test9.trace.spans[0], childSpanIds: [] })],
])('sanitizeOverFlowingChildren', (testProps, expectedSanitizedData) => {
it('Should sanitize the data(overflowing spans) correctly', () => {
const refinedSpanData = getChildOfSpans(new Map(testProps.trace.spans.map((span) => [span.spanID, span])));
const sanitizedSpanMap = sanitizeOverFlowingChildren(refinedSpanData);
expect(sanitizedSpanMap).toStrictEqual(expectedSanitizedData);
});
});

View File

@ -0,0 +1,115 @@
// Copyright (c) 2023 The Jaeger Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceSpan } from '../../types';
/**
* This function resolves overflowing child spans for each span.
* An overflowing child span is one whose time range falls outside its parent span's time range.
* The function adjusts the start time and duration of overflowing child spans
* to ensure they fit within the time range of their parent span.
* @param spanMap - A Map where span IDs are keys and the corresponding spans are values.
* @returns - A sanitized span Map.
*/
const sanitizeOverFlowingChildren = (spanMap: Map<string, TraceSpan>): Map<string, TraceSpan> => {
let spanIds: string[] = [...spanMap.keys()];
spanIds.forEach((spanId) => {
const span = spanMap.get(spanId)!;
if (!(span && span.references.length && span.depth)) {
return;
}
// parentSpan will be undefined when its parentSpan is dropped previously
const parentSpan = spanMap.get(span.references[0].spanID);
if (!parentSpan) {
// Drop the child spans of dropped parent span
spanMap.delete(span.spanID);
return;
}
const childEndTime = span.startTime + span.duration;
const parentEndTime = parentSpan.startTime + parentSpan.duration;
if (span.startTime >= parentSpan.startTime) {
if (span.startTime >= parentEndTime) {
// child outside of parent range => drop the child span
// |----parent----|
// |----child--|
// Remove the childSpan from spanMap
spanMap.delete(span.spanID);
// Remove the childSpanId from its parent span
parentSpan.childSpanIds = parentSpan.childSpanIds.filter((id) => id !== span.spanID);
return;
}
if (childEndTime > parentEndTime) {
// child end after parent, truncate is needed
// |----parent----|
// |----child--|
spanMap.set(span.spanID, {
...span,
duration: parentEndTime - span.startTime,
});
return;
}
// everything looks good
// |----parent----|
// |----child--|
return;
}
if (childEndTime <= parentSpan.startTime) {
// child outside of parent range => drop the child span
// |----parent----|
// |----child--|
// Remove the childSpan from spanMap
spanMap.delete(span.spanID);
// Remove the childSpanId from its parent span
parentSpan.childSpanIds = parentSpan.childSpanIds.filter((id) => id !== span.spanID);
} else if (childEndTime <= parentEndTime) {
// child start before parent, truncate is needed
// |----parent----|
// |----child--|
spanMap.set(span.spanID, {
...span,
startTime: parentSpan.startTime,
duration: childEndTime - parentSpan.startTime,
});
} else {
// child start before parent and end after parent, truncate is needed
// |----parent----|
// |---------child---------|
spanMap.set(span.spanID, {
...span,
startTime: parentSpan.startTime,
duration: parentEndTime - parentSpan.startTime,
});
}
});
// Updated spanIds to ensure to not include dropped spans
spanIds = [...spanMap.keys()];
// Update Child Span References with updated parent span
spanIds.forEach((spanId) => {
const span = spanMap.get(spanId)!;
if (span.references.length) {
const parentSpan = spanMap.get(span.references[0].spanID);
span.references[0].span = parentSpan;
spanMap.set(spanId, { ...span });
}
});
return spanMap;
};
export default sanitizeOverFlowingChildren;

View File

@ -31,10 +31,12 @@ describe('<TracePageSearchBar>', () => {
setFocusedSpanIdForSearch: jest.fn(),
focusedSpanIndexForSearch: -1,
setFocusedSpanIndexForSearch: jest.fn(),
setShowCriticalPathSpansOnly: jest.fn(),
datasourceType: '',
clear: jest.fn(),
totalSpans: 100,
showSpanFilters: true,
showCriticalPathSpansOnly: false,
};
return <TracePageSearchBar {...searchBarProps} />;

View File

@ -31,6 +31,8 @@ export type TracePageSearchBarProps = {
spanFilterMatches: Set<string> | undefined;
showSpanFilterMatchesOnly: boolean;
setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void;
showCriticalPathSpansOnly: boolean;
setShowCriticalPathSpansOnly: (showCriticalPath: boolean) => void;
focusedSpanIndexForSearch: number;
setFocusedSpanIndexForSearch: Dispatch<SetStateAction<number>>;
setFocusedSpanIdForSearch: Dispatch<SetStateAction<string>>;
@ -46,6 +48,8 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps)
spanFilterMatches,
showSpanFilterMatchesOnly,
setShowSpanFilterMatchesOnly,
showCriticalPathSpansOnly,
setShowCriticalPathSpansOnly,
focusedSpanIndexForSearch,
setFocusedSpanIndexForSearch,
setFocusedSpanIdForSearch,
@ -101,6 +105,21 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps)
Show matches only
</Button>
</div>
<div className={styles.matchesOnly}>
<Switch
value={showCriticalPathSpansOnly}
onChange={(value) => setShowCriticalPathSpansOnly(value.currentTarget.checked ?? false)}
label="Show critical path only switch"
/>
<Button
onClick={() => setShowCriticalPathSpansOnly(!showCriticalPathSpansOnly)}
className={styles.clearMatchesButton}
variant="secondary"
fill="text"
>
Show critical path only
</Button>
</div>
</div>
<div className={styles.nextPrevResult}>
<NextPrevResult

View File

@ -52,12 +52,15 @@ describe('SpanFilters', () => {
const SpanFiltersWithProps = ({ showFilters = true, matches }: { showFilters?: boolean; matches?: Set<string> }) => {
const [search, setSearch] = useState(defaultFilters);
const [showSpanFilterMatchesOnly, setShowSpanFilterMatchesOnly] = useState(false);
const [showCriticalPathSpansOnly, setShowCriticalPathSpansOnly] = useState(false);
const props = {
trace: trace,
showSpanFilters: showFilters,
setShowSpanFilters: jest.fn(),
showSpanFilterMatchesOnly,
setShowSpanFilterMatchesOnly,
showCriticalPathSpansOnly,
setShowCriticalPathSpansOnly,
search,
setSearch,
spanFilterMatches: matches,

View File

@ -37,6 +37,8 @@ export type SpanFilterProps = {
showSpanFilterMatchesOnly: boolean;
setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void;
setFocusedSpanIdForSearch: React.Dispatch<React.SetStateAction<string>>;
showCriticalPathSpansOnly: boolean;
setShowCriticalPathSpansOnly: (showCriticalPathSpansOnly: boolean) => void;
spanFilterMatches: Set<string> | undefined;
datasourceType: string;
};
@ -50,6 +52,8 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
setShowSpanFilters,
showSpanFilterMatchesOnly,
setShowSpanFilterMatchesOnly,
showCriticalPathSpansOnly,
setShowCriticalPathSpansOnly,
setFocusedSpanIdForSearch,
spanFilterMatches,
datasourceType,
@ -456,6 +460,8 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
spanFilterMatches={spanFilterMatches}
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}
setShowSpanFilterMatchesOnly={setShowSpanFilterMatchesOnly}
showCriticalPathSpansOnly={showCriticalPathSpansOnly}
setShowCriticalPathSpansOnly={setShowCriticalPathSpansOnly}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
focusedSpanIndexForSearch={focusedSpanIndexForSearch}
setFocusedSpanIndexForSearch={setFocusedSpanIndexForSearch}

View File

@ -31,6 +31,8 @@ const setup = () => {
setShowSpanFilters: jest.fn(),
showSpanFilterMatchesOnly: false,
setShowSpanFilterMatchesOnly: jest.fn(),
showCriticalPathSpansOnly: false,
setShowCriticalPathSpansOnly: jest.fn(),
spanFilterMatches: undefined,
setFocusedSpanIdForSearch: jest.fn(),
datasourceType: 'tempo',
@ -86,6 +88,7 @@ export const trace = {
hasChildren: false,
childSpanCount: 0,
warnings: [],
childSpanIds: [],
},
{
traceID: '164afda25df92413',
@ -125,6 +128,7 @@ export const trace = {
hasChildren: false,
childSpanCount: 0,
warnings: [],
childSpanIds: [],
},
{
traceID: '164afda25df92413',
@ -164,6 +168,7 @@ export const trace = {
hasChildren: false,
childSpanCount: 0,
warnings: [],
childSpanIds: [],
},
],
traceID: '8bb35a31-eb64-512d-aaed-ddd61887bb2b',

View File

@ -42,6 +42,8 @@ export type TracePageHeaderProps = {
setShowSpanFilters: (isOpen: boolean) => void;
showSpanFilterMatchesOnly: boolean;
setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void;
showCriticalPathSpansOnly: boolean;
setShowCriticalPathSpansOnly: (showCriticalPathSpansOnly: boolean) => void;
setFocusedSpanIdForSearch: React.Dispatch<React.SetStateAction<string>>;
spanFilterMatches: Set<string> | undefined;
datasourceType: string;
@ -60,6 +62,8 @@ export const TracePageHeader = memo((props: TracePageHeaderProps) => {
setShowSpanFilters,
showSpanFilterMatchesOnly,
setShowSpanFilterMatchesOnly,
showCriticalPathSpansOnly,
setShowCriticalPathSpansOnly,
setFocusedSpanIdForSearch,
spanFilterMatches,
datasourceType,
@ -152,6 +156,8 @@ export const TracePageHeader = memo((props: TracePageHeaderProps) => {
setShowSpanFilters={setShowSpanFilters}
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}
setShowSpanFilterMatchesOnly={setShowSpanFilterMatchesOnly}
showCriticalPathSpansOnly={showCriticalPathSpansOnly}
setShowCriticalPathSpansOnly={setShowCriticalPathSpansOnly}
search={search}
setSearch={setSearch}
spanFilterMatches={spanFilterMatches}

View File

@ -19,76 +19,85 @@ import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '@grafana/ui';
import { Tooltip, useStyles2 } from '@grafana/ui';
import { autoColor } from '../Theme';
import { Popover } from '../common/Popover';
import { TraceSpan, TNil } from '../types';
import { TraceSpan, TNil, CriticalPathSection } from '../types';
import AccordianLogs from './SpanDetail/AccordianLogs';
import { ViewedBoundsFunctionType } from './utils';
const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css`
label: wrapper;
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
overflow: hidden;
z-index: 0;
`,
bar: css`
label: bar;
border-radius: 3px;
min-width: 2px;
position: absolute;
height: 36%;
top: 32%;
`,
rpc: css`
label: rpc;
position: absolute;
top: 35%;
bottom: 35%;
z-index: 1;
`,
label: css`
label: label;
color: #aaa;
font-size: 12px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1em;
white-space: nowrap;
padding: 0 0.5em;
position: absolute;
`,
logMarker: css`
label: logMarker;
background-color: ${autoColor(theme, '#2c3235')};
cursor: pointer;
height: 60%;
min-width: 1px;
position: absolute;
top: 20%;
&:hover {
background-color: ${autoColor(theme, '#464c54')};
}
&::before,
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
right: 0;
border: 1px solid transparent;
}
&::after {
left: 0;
}
`,
wrapper: css({
label: 'wrapper',
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0,
overflow: 'hidden',
zIndex: 0,
}),
bar: css({
label: 'bar',
borderRadius: theme.shape.radius.default,
minWidth: '2px',
position: 'absolute',
height: '36%',
top: '32%',
}),
rpc: css({
label: 'rpc',
position: 'absolute',
top: '35%',
bottom: '35%',
zIndex: 1,
}),
label: css({
label: 'label',
color: '#aaa',
fontSize: '12px',
fontFamily: "'Helvetica Neue', Helvetica, Arial, sans - serif",
lineHeight: '1em',
whiteSpace: 'nowrap',
padding: '0 0.5em',
position: 'absolute',
}),
logMarker: css({
label: 'logMarker',
backgroundColor: autoColor(theme, '#2c3235'),
cursor: 'pointer',
height: '60%',
minWidth: '1px',
position: 'absolute',
top: '20%',
'&:hover': {
backgroundColor: autoColor(theme, '#464c54'),
},
'&::before, &::after': {
content: "''",
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
border: '1px solid transparent',
},
'&::after': {
left: 0,
},
}),
criticalPath: css({
position: 'absolute',
top: '45%',
height: '11%',
zIndex: 2,
overflow: 'hidden',
background: autoColor(theme, '#f1f1f1'),
borderLeft: `1px solid ${autoColor(theme, '#2c3235')}`,
borderRight: `1px solid ${autoColor(theme, '#2c3235')}`,
}),
};
};
@ -111,13 +120,19 @@ export type Props = {
labelClassName?: string;
longLabel: string;
shortLabel: string;
criticalPath: CriticalPathSection[];
};
function toPercent(value: number) {
return `${(value * 100).toFixed(1)}%`;
}
function toPercentInDecimal(value: number) {
return `${value * 100}%`;
}
function SpanBar({
criticalPath,
viewEnd,
viewStart,
getViewedBounds,
@ -156,7 +171,7 @@ function SpanBar({
>
<div
aria-label={label}
className={styles.bar}
className={cx(styles.bar)}
style={{
background: color,
left: toPercent(viewStart),
@ -175,13 +190,13 @@ function SpanBar({
<AccordianLogs interactive={false} isOpen logs={logGroups[positionKey]} timestamp={traceStartTime} />
}
>
<div data-testid="SpanBar--logMarker" className={styles.logMarker} style={{ left: positionKey }} />
<div data-testid="SpanBar--logMarker" className={cx(styles.logMarker)} style={{ left: positionKey }} />
</Popover>
))}
</div>
{rpc && (
<div
className={styles.rpc}
className={cx(styles.rpc)}
style={{
background: rpc.color,
left: toPercent(rpc.viewStart),
@ -189,6 +204,32 @@ function SpanBar({
}}
/>
)}
{criticalPath?.map((each, index) => {
const critcalPathViewBounds = getViewedBounds(each.section_start, each.section_end);
const criticalPathViewStart = critcalPathViewBounds.start;
const criticalPathViewEnd = critcalPathViewBounds.end;
const key = `${each.spanId}-${index}`;
return (
<Tooltip
key={key}
placement="top"
content={
<div>
A segment on the <em>critical path</em> of the overall trace/request/workflow.
</div>
}
>
<div
data-testid="SpanBar--criticalPath"
className={styles.criticalPath}
style={{
left: toPercentInDecimal(criticalPathViewStart),
width: toPercentInDecimal(criticalPathViewEnd - criticalPathViewStart),
}}
/>
</Tooltip>
);
})}
</div>
);
}

View File

@ -21,7 +21,7 @@ import { Icon, stylesFactory, withTheme2 } from '@grafana/ui';
import { autoColor } from '../Theme';
import { DURATION, NONE, TAG } from '../settings/SpanBarSettings';
import { SpanBarOptions, SpanLinkFunc, TraceSpan, TNil } from '../types';
import { SpanBarOptions, SpanLinkFunc, TraceSpan, TNil, CriticalPathSection } from '../types';
import SpanBar from './SpanBar';
import { SpanLinksMenu } from './SpanLinks';
@ -324,6 +324,7 @@ export type SpanBarRowProps = {
createSpanLink?: SpanLinkFunc;
datasourceType: string;
visibleSpanIds: string[];
criticalPath: CriticalPathSection[];
};
/**
@ -377,6 +378,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
datasourceType,
showServiceName,
visibleSpanIds,
criticalPath,
} = this.props;
const {
duration,
@ -530,6 +532,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
>
<Ticks numTicks={numTicks} />
<SpanBar
criticalPath={criticalPath}
rpc={rpc}
viewStart={viewStart}
viewEnd={viewEnd}

View File

@ -23,7 +23,7 @@ import { config, reportInteraction } from '@grafana/runtime';
import { stylesFactory, withTheme2, ToolbarButton } from '@grafana/ui';
import { PEER_SERVICE } from '../constants/tag-keys';
import { SpanBarOptions, SpanLinkFunc, TNil } from '../types';
import { CriticalPathSection, SpanBarOptions, SpanLinkFunc, TNil } from '../types';
import TTraceTimeline from '../types/TTraceTimeline';
import { TraceLog, TraceSpan, Trace, TraceKeyValuePair, TraceLink, TraceSpanReference } from '../types/trace';
import { getColorByKey } from '../utils/color-generator';
@ -105,11 +105,13 @@ type TVirtualizedTraceViewOwnProps = {
focusedSpanId?: string;
focusedSpanIdForSearch: string;
showSpanFilterMatchesOnly: boolean;
showCriticalPathSpansOnly: boolean;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfViewRef?: RefObject<HTMLDivElement>;
topOfViewRefType?: TopOfViewRefType;
datasourceType: string;
headerHeight: number;
criticalPath: CriticalPathSection[];
};
export type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TTraceTimeline;
@ -129,7 +131,9 @@ function generateRowStates(
childrenHiddenIDs: Set<string>,
detailStates: Map<string, DetailState | TNil>,
findMatchesIDs: Set<string> | TNil,
showSpanFilterMatchesOnly: boolean
showSpanFilterMatchesOnly: boolean,
showCriticalPathSpansOnly: boolean,
criticalPath: CriticalPathSection[]
): RowState[] {
if (!spans) {
return [];
@ -138,6 +142,10 @@ function generateRowStates(
spans = spans.filter((span) => findMatchesIDs.has(span.spanID));
}
if (showCriticalPathSpansOnly && criticalPath) {
spans = spans.filter((span) => criticalPath.find((section) => section.spanId === span.spanID));
}
let collapseDepth = null;
const rowStates = [];
for (let i = 0; i < spans.length; i++) {
@ -186,10 +194,20 @@ function generateRowStatesFromTrace(
childrenHiddenIDs: Set<string>,
detailStates: Map<string, DetailState | TNil>,
findMatchesIDs: Set<string> | TNil,
showSpanFilterMatchesOnly: boolean
showSpanFilterMatchesOnly: boolean,
showCriticalPathSpansOnly: boolean,
criticalPath: CriticalPathSection[]
): RowState[] {
return trace
? generateRowStates(trace.spans, childrenHiddenIDs, detailStates, findMatchesIDs, showSpanFilterMatchesOnly)
? generateRowStates(
trace.spans,
childrenHiddenIDs,
detailStates,
findMatchesIDs,
showSpanFilterMatchesOnly,
showCriticalPathSpansOnly,
criticalPath
)
: [];
}
@ -235,8 +253,24 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
}
getRowStates(): RowState[] {
const { childrenHiddenIDs, detailStates, trace, findMatchesIDs, showSpanFilterMatchesOnly } = this.props;
return memoizedGenerateRowStates(trace, childrenHiddenIDs, detailStates, findMatchesIDs, showSpanFilterMatchesOnly);
const {
childrenHiddenIDs,
detailStates,
trace,
findMatchesIDs,
showSpanFilterMatchesOnly,
showCriticalPathSpansOnly,
criticalPath,
} = this.props;
return memoizedGenerateRowStates(
trace,
childrenHiddenIDs,
detailStates,
findMatchesIDs,
showSpanFilterMatchesOnly,
showCriticalPathSpansOnly,
criticalPath
);
}
getClipping(): { left: boolean; right: boolean } {
@ -361,7 +395,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
attrs: {},
visibleSpanIds: string[]
) {
const { spanID } = span;
const { spanID, childSpanIds } = span;
const { serviceName } = span.process;
const {
childrenHiddenIDs,
@ -381,6 +415,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
showSpanFilterMatchesOnly,
theme,
datasourceType,
criticalPath,
} = this.props;
// to avert flow error
if (!trace) {
@ -422,6 +457,25 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
const prevSpan = spanIndex > 0 ? trace.spans[spanIndex - 1] : null;
const allChildSpanIds = [spanID, ...childSpanIds];
// This function called recursively to find all descendants of a span
const findAllDescendants = (currentChildSpanIds: string[]) => {
currentChildSpanIds.forEach((eachId) => {
const currentChildSpan = trace.spans.find((a) => a.spanID === eachId)!;
if (currentChildSpan.hasChildren) {
allChildSpanIds.push(...currentChildSpan.childSpanIds);
findAllDescendants(currentChildSpan.childSpanIds);
}
});
};
findAllDescendants(childSpanIds);
const criticalPathSections = criticalPath?.filter((each) => {
if (isCollapsed) {
return allChildSpanIds.includes(each.spanId);
}
return each.spanId === spanID;
});
const styles = getStyles(this.props);
return (
<div className={styles.row} key={key} style={style} {...attrs}>
@ -452,6 +506,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
datasourceType={datasourceType}
showServiceName={prevSpan === null || prevSpan.process.serviceName !== span.process.serviceName}
visibleSpanIds={visibleSpanIds}
criticalPath={criticalPathSections}
/>
</div>
);

View File

@ -22,7 +22,7 @@ import { stylesFactory, withTheme2 } from '@grafana/ui';
import { autoColor } from '../Theme';
import { merge as mergeShortcuts } from '../keyboard-shortcuts';
import { SpanBarOptions } from '../settings/SpanBarSettings';
import { SpanLinkFunc, TNil } from '../types';
import { CriticalPathSection, SpanLinkFunc, TNil } from '../types';
import TTraceTimeline from '../types/TTraceTimeline';
import { TraceSpan, Trace, TraceLog, TraceKeyValuePair, TraceLink, TraceSpanReference } from '../types/trace';
@ -102,10 +102,12 @@ export type TProps = {
focusedSpanId?: string;
focusedSpanIdForSearch: string;
showSpanFilterMatchesOnly: boolean;
showCriticalPathSpansOnly: boolean;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfViewRef?: RefObject<HTMLDivElement>;
topOfViewRefType?: TopOfViewRefType;
headerHeight: number;
criticalPath: CriticalPathSection[];
};
type State = {

View File

@ -129,13 +129,11 @@ export default function transformTraceData(data: TraceResponse | undefined): Tra
}
// tree is necessary to sort the spans, so children follow parents, and
// siblings are sorted by start time
const tree = getTraceSpanIdsAsTree(data);
const tree = getTraceSpanIdsAsTree(data, spanMap);
const spans: TraceSpan[] = [];
const svcCounts: Record<string, number> = {};
// Eslint complains about number type not needed but then TS complains it is implicitly any.
// eslint-disable-next-line @typescript-eslint/no-inferrable-types
tree.walk((spanID: string | number | undefined, node: TreeNode, depth: number = 0) => {
tree.walk((spanID: string, node: TreeNode<string>, depth = 0) => {
if (spanID === '__root__') {
return;
}
@ -155,6 +153,16 @@ export default function transformTraceData(data: TraceResponse | undefined): Tra
span.warnings = span.warnings || [];
span.tags = span.tags || [];
span.references = span.references || [];
span.childSpanIds = node.children
.slice()
.sort((a, b) => {
const spanA = spanMap.get(a.value)!;
const spanB = spanMap.get(b.value)!;
return spanB.startTime + spanB.duration - (spanA.startTime + spanA.duration);
})
.map((each) => each.value);
const tagsInfo = deduplicateTags(span.tags);
span.tags = orderTags(tagsInfo.dedupedTags, getConfigValue('topTagPrefixes'));
span.warnings = span.warnings.concat(tagsInfo.warnings);

View File

@ -37,7 +37,7 @@ describe('getTraceSpanIdsAsTree()', () => {
const tree = traceSelectors.getTraceSpanIdsAsTree(generatedTrace);
const spanMap = traceSelectors.getTraceSpansAsMap(generatedTrace);
tree.walk((value: string | number | undefined, node: TreeNode) => {
tree.walk((value: string, node: TreeNode<string>) => {
const expectedParentValue = value === traceSelectors.TREE_ROOT_ID ? null : value;
node.children.forEach((childNode) => {
expect(getSpanParentId(spanMap.get(childNode.value))).toBe(expectedParentValue);

View File

@ -41,9 +41,9 @@ export const TREE_ROOT_ID = '__root__';
* @return {TreeNode} A tree of spanIDs derived from the relationships
* between spans in the trace.
*/
export function getTraceSpanIdsAsTree(trace: TraceResponse) {
export function getTraceSpanIdsAsTree(trace: TraceResponse, spanMap: Map<string, TraceSpanData> | null = null) {
const nodesById = new Map(trace.spans.map((span: TraceSpanData) => [span.spanID, new TreeNode(span.spanID)]));
const spansById = new Map(trace.spans.map((span: TraceSpanData) => [span.spanID, span]));
const spansById = spanMap ?? new Map(trace.spans.map((span: TraceSpanData) => [span.spanID, span]));
const root = new TreeNode(TREE_ROOT_ID);
trace.spans.forEach((span: TraceSpanData) => {
const node = nodesById.get(span.spanID)!;
@ -59,13 +59,13 @@ export function getTraceSpanIdsAsTree(trace: TraceResponse) {
root.children.push(node);
}
});
const comparator = (nodeA: TreeNode | undefined, nodeB: TreeNode | undefined) => {
const comparator = (nodeA: TreeNode<string>, nodeB: TreeNode<string>) => {
const a: TraceSpanData | undefined = nodeA?.value ? spansById.get(nodeA.value.toString()) : undefined;
const b: TraceSpanData | undefined = nodeB?.value ? spansById.get(nodeB.value.toString()) : undefined;
return +(a?.startTime! > b?.startTime!) || +(a?.startTime === b?.startTime) - 1;
};
trace.spans.forEach((span: TraceSpanData) => {
const node: TreeNode | undefined = nodesById.get(span.spanID);
const node = nodesById.get(span.spanID);
if (node!.children.length > 1) {
node?.children.sort(comparator);
}
@ -73,17 +73,3 @@ export function getTraceSpanIdsAsTree(trace: TraceResponse) {
root.children.sort(comparator);
return root;
}
export const omitCollapsedSpans = createSelector(
({ spans }: { spans: TraceSpanData[] }) => spans,
createSelector(({ trace }: { trace: TraceResponse }) => trace, getTraceSpanIdsAsTree),
({ collapsed }: { collapsed: string[] }) => collapsed,
(spans, tree, collapse) => {
const hiddenSpanIds = collapse.reduce((result, collapsedSpanId) => {
tree.find(collapsedSpanId)!.walk((id: string | number | undefined) => id !== collapsedSpanId && result.add(id));
return result;
}, new Set());
return hiddenSpanIds.size > 0 ? spans.filter((span) => !hiddenSpanIds.has(getSpanId(span))) : spans;
}
);

View File

@ -12,7 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
export { TraceSpan, TraceResponse, Trace, TraceProcess, TraceKeyValuePair, TraceLink } from './trace';
export {
TraceSpan,
TraceResponse,
Trace,
TraceProcess,
TraceKeyValuePair,
TraceLink,
CriticalPathSection,
} from './trace';
export { SpanBarOptions, SpanBarOptionsData } from '../settings/SpanBarSettings';
export { default as TTraceTimeline } from './TTraceTimeline';
export { default as TNil } from './TNil';

View File

@ -69,6 +69,7 @@ export type TraceSpanData = {
flags: number;
errorIconColor?: string;
dataFrameRowIndex?: number;
childSpanIds?: string[];
};
export type TraceSpan = TraceSpanData & {
@ -80,6 +81,7 @@ export type TraceSpan = TraceSpanData & {
tags: NonNullable<TraceSpanData['tags']>;
references: NonNullable<TraceSpanData['references']>;
warnings: NonNullable<TraceSpanData['warnings']>;
childSpanIds: NonNullable<TraceSpanData['childSpanIds']>;
subsidiarilyReferencedBy: TraceSpanReference[];
};
@ -101,3 +103,10 @@ export type Trace = TraceData & {
traceName: string;
services: Array<{ name: string; numberOfSpans: number }>;
};
// It is a section of span that lies on critical path
export type CriticalPathSection = {
spanId: string;
section_start: number;
section_end: number;
};

View File

@ -29,7 +29,7 @@ it('TreeNode constructor should return a tree node', () => {
});
it('depth should work for a single node', () => {
expect(new TreeNode().depth).toBe(1);
expect(new TreeNode<number>(0).depth).toBe(1);
});
it('depth should caluclate the depth', () => {
@ -119,8 +119,8 @@ it('find() should return the found item for a function', () => {
treeRoot.addChild(11);
treeRoot.addChild(12);
expect(treeRoot.find((value) => value === 6)).toEqual(secondChildNode);
expect(treeRoot.find(12)).toEqual(new TreeNode(12));
expect(treeRoot.find((value: number) => value === 6)).toEqual(secondChildNode);
expect(treeRoot.find((value: number) => value === 12)).toEqual(new TreeNode(12));
});
it('find() should return the found item for a value', () => {
@ -140,8 +140,8 @@ it('find() should return the found item for a value', () => {
treeRoot.addChild(11);
treeRoot.addChild(12);
expect(treeRoot.find(7)).toEqual(thirdDeepestChildNode);
expect(treeRoot.find(12)).toEqual(new TreeNode(12));
expect(treeRoot.find((value: number) => value === 7)).toEqual(thirdDeepestChildNode);
expect(treeRoot.find((value: number) => value === 12)).toEqual(new TreeNode(12));
});
it('find() should return the found item for a treenode', () => {
@ -182,8 +182,8 @@ it('find() should return null for none found', () => {
treeRoot.addChild(11);
treeRoot.addChild(12);
expect(treeRoot.find(13)).toBe(null);
expect(treeRoot.find((value) => value === 'foo')).toBe(null);
expect(treeRoot.find((value: number) => value === 13)).toBe(null);
expect(treeRoot.find((value: string) => value === 'foo')).toBe(null);
});
it('getPath() should return the path to the node', () => {
@ -225,7 +225,7 @@ it('getPath() should return null if the node is not in the tree', () => {
const exteriorNode = new TreeNode(15);
expect(treeRoot.getPath(exteriorNode)).toEqual(null);
expect(treeRoot.getPath((value: TreeNode<number>) => value === exteriorNode)).toEqual(null);
});
it('walk() should iterate over every item once in the right order', () => {

View File

@ -10,34 +10,34 @@
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// limitations under the License
type SearchFn = (value: string | number | undefined, node: TreeNode, depth?: number) => boolean;
export default class TreeNode<TValue> {
value: TValue;
children: Array<TreeNode<TValue>>;
export default class TreeNode {
value: string | number | undefined;
children: TreeNode[];
static iterFunction(fn: SearchFn, depth = 0) {
return (node: TreeNode) => fn(node.value, node, depth);
static iterFunction<TValue>(
fn: ((value: TValue, node: TreeNode<TValue>, depth: number) => TreeNode<TValue> | null) | Function,
depth = 0
) {
return (node: TreeNode<TValue>) => fn(node.value, node, depth);
}
static searchFunction(search: TreeNode | number | SearchFn | string) {
static searchFunction<TValue>(search: Function | TreeNode<TValue>) {
if (typeof search === 'function') {
return search;
}
return (value: string | number | undefined, node: TreeNode) =>
search instanceof TreeNode ? node === search : value === search;
return (value: TValue, node: TreeNode<TValue>) => (search instanceof TreeNode ? node === search : value === search);
}
constructor(value?: string | number, children: TreeNode[] = []) {
constructor(value: TValue, children: Array<TreeNode<TValue>> = []) {
this.value = value;
this.children = children;
}
get depth(): number {
return this.children?.reduce((depth: number, child: { depth: number }) => Math.max(child.depth + 1, depth), 1);
return this.children.reduce((depth, child) => Math.max(child.depth + 1, depth), 1);
}
get size() {
@ -46,12 +46,12 @@ export default class TreeNode {
return i;
}
addChild(child: string | number | TreeNode) {
this.children?.push(child instanceof TreeNode ? child : new TreeNode(child));
addChild(child: TreeNode<TValue> | TValue) {
this.children.push(child instanceof TreeNode ? child : new TreeNode(child));
return this;
}
find(search: TreeNode | number | SearchFn | string): TreeNode | null {
find(search: Function | TreeNode<TValue>): TreeNode<TValue> | null {
const searchFn = TreeNode.iterFunction(TreeNode.searchFunction(search));
if (searchFn(this)) {
return this;
@ -65,10 +65,13 @@ export default class TreeNode {
return null;
}
getPath(search: TreeNode | string) {
getPath(search: Function | TreeNode<TValue>) {
const searchFn = TreeNode.iterFunction(TreeNode.searchFunction(search));
const findPath = (currentNode: TreeNode, currentPath: TreeNode[]): TreeNode[] | null => {
const findPath = (
currentNode: TreeNode<TValue>,
currentPath: Array<TreeNode<TValue>>
): Array<TreeNode<TValue>> | null => {
// skip if we already found the result
const attempt = currentPath.concat([currentNode]);
// base case: return the array when there is a match
@ -88,22 +91,49 @@ export default class TreeNode {
return findPath(this, []);
}
walk(fn: (value: string | number | undefined, node: TreeNode, depth?: number) => void, depth = 0) {
const nodeStack: Array<{ node: TreeNode; depth?: number }> = [];
let actualDepth = depth;
walk(fn: (spanID: TValue, node: TreeNode<TValue>, depth: number) => void, startDepth = 0) {
type StackEntry = {
node: TreeNode<TValue>;
depth: number;
};
const nodeStack: StackEntry[] = [];
let actualDepth = startDepth;
nodeStack.push({ node: this, depth: actualDepth });
while (nodeStack.length) {
const popped = nodeStack.pop();
if (popped) {
const { node, depth: nodeDepth } = popped;
fn(node.value, node, nodeDepth);
actualDepth = (nodeDepth || 0) + 1;
let i = node.children.length - 1;
while (i >= 0) {
nodeStack.push({ node: node.children[i], depth: actualDepth });
i--;
}
const entry: StackEntry = nodeStack[nodeStack.length - 1];
nodeStack.pop();
const { node, depth } = entry;
fn(node.value, node, depth);
actualDepth = depth + 1;
let i = node.children.length - 1;
while (i >= 0) {
nodeStack.push({ node: node.children[i], depth: actualDepth });
i--;
}
}
}
paths(fn: (pathIds: TValue[]) => void) {
type StackEntry = {
node: TreeNode<TValue>;
childIndex: number;
};
const stack: StackEntry[] = [];
stack.push({ node: this, childIndex: 0 });
const paths: TValue[] = [];
while (stack.length) {
const { node, childIndex } = stack[stack.length - 1];
if (node.children.length >= childIndex + 1) {
stack[stack.length - 1].childIndex++;
stack.push({ node: node.children[childIndex], childIndex: 0 });
} else {
if (node.children.length === 0) {
const path = stack.map((item) => item.node.value);
fn(path);
}
stack.pop();
}
}
return paths;
}
}