mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
4ed36cbc1d
commit
107cf0dc04
@ -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"],
|
||||
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
);
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
@ -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} />;
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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',
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 = {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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', () => {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user