From 107cf0dc04d7fd5c5eee0fc30b1dbab518baff17 Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Mon, 30 Oct 2023 10:43:14 +0000 Subject: [PATCH] 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 --- .betterer.results | 7 - .../features/explore/TraceView/TraceView.tsx | 8 + .../components/CriticalPath/index.test.ts | 35 ++++ .../components/CriticalPath/index.tsx | 115 ++++++++++++ .../CriticalPath/testCases/test1.ts | 126 +++++++++++++ .../CriticalPath/testCases/test2.ts | 116 ++++++++++++ .../CriticalPath/testCases/test3.ts | 113 ++++++++++++ .../CriticalPath/testCases/test4.ts | 106 +++++++++++ .../CriticalPath/testCases/test5.ts | 105 +++++++++++ .../CriticalPath/testCases/test6.ts | 113 ++++++++++++ .../CriticalPath/testCases/test7.ts | 113 ++++++++++++ .../CriticalPath/testCases/test8.ts | 82 +++++++++ .../CriticalPath/testCases/test9.ts | 84 +++++++++ .../utils/findLastFinishingChildSpan.test.ts | 48 +++++ .../utils/findLastFinishingChildSpan.tsx | 42 +++++ .../utils/getChildOfSpans.test.ts | 46 +++++ .../CriticalPath/utils/getChildOfSpans.tsx | 54 ++++++ .../utils/sanitizeOverFlowingChildren.test.ts | 53 ++++++ .../utils/sanitizeOverFlowingChildren.tsx | 115 ++++++++++++ .../SearchBar/TracePageSearchBar.test.tsx | 2 + .../SearchBar/TracePageSearchBar.tsx | 19 ++ .../SpanFilters/SpanFilters.test.tsx | 3 + .../SpanFilters/SpanFilters.tsx | 6 + .../TracePageHeader/TracePageHeader.test.tsx | 5 + .../TracePageHeader/TracePageHeader.tsx | 6 + .../TraceTimelineViewer/SpanBar.tsx | 169 +++++++++++------- .../TraceTimelineViewer/SpanBarRow.tsx | 5 +- .../VirtualizedTraceView.tsx | 69 ++++++- .../components/TraceTimelineViewer/index.tsx | 4 +- .../components/model/transform-trace-data.tsx | 16 +- .../components/selectors/trace.test.ts | 2 +- .../TraceView/components/selectors/trace.ts | 22 +-- .../TraceView/components/types/index.tsx | 10 +- .../TraceView/components/types/trace.ts | 9 + .../components/utils/TreeNode.test.ts | 16 +- .../TraceView/components/utils/TreeNode.ts | 92 ++++++---- 36 files changed, 1793 insertions(+), 143 deletions(-) create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/index.test.ts create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/index.tsx create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/testCases/test1.ts create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/testCases/test2.ts create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/testCases/test3.ts create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/testCases/test4.ts create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/testCases/test5.ts create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/testCases/test6.ts create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/testCases/test7.ts create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/testCases/test8.ts create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/testCases/test9.ts create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/utils/findLastFinishingChildSpan.test.ts create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/utils/findLastFinishingChildSpan.tsx create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/utils/getChildOfSpans.test.ts create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/utils/getChildOfSpans.tsx create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.test.ts create mode 100644 public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.tsx diff --git a/.betterer.results b/.betterer.results index 28408dbcdcb..fcfcd4abf07 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"], diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index 1b5e8d29414..b759fa36df1 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -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} /> ) : ( diff --git a/public/app/features/explore/TraceView/components/CriticalPath/index.test.ts b/public/app/features/explore/TraceView/components/CriticalPath/index.test.ts new file mode 100644 index 00000000000..bdca1b8c5f9 --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/index.test.ts @@ -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); + }); + } +); diff --git a/public/app/features/explore/TraceView/components/CriticalPath/index.tsx b/public/app/features/explore/TraceView/components/CriticalPath/index.tsx new file mode 100644 index 00000000000..7d8a8d3dc99 --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/index.tsx @@ -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, + 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()); + 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; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test1.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test1.ts new file mode 100644 index 00000000000..d2f0cf757fd --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test1.ts @@ -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; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test2.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test2.ts new file mode 100644 index 00000000000..1f445a9339e --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test2.ts @@ -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; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test3.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test3.ts new file mode 100644 index 00000000000..ab13f8d12fb --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test3.ts @@ -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; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test4.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test4.ts new file mode 100644 index 00000000000..dfab4611889 --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test4.ts @@ -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; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test5.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test5.ts new file mode 100644 index 00000000000..0f057fe60e1 --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test5.ts @@ -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; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test6.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test6.ts new file mode 100644 index 00000000000..3d35d416f85 --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test6.ts @@ -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; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test7.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test7.ts new file mode 100644 index 00000000000..ef67a541817 --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test7.ts @@ -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; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test8.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test8.ts new file mode 100644 index 00000000000..4be57aa68db --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test8.ts @@ -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; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test9.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test9.ts new file mode 100644 index 00000000000..b1e52d18f22 --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test9.ts @@ -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; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/utils/findLastFinishingChildSpan.test.ts b/public/app/features/explore/TraceView/components/CriticalPath/utils/findLastFinishingChildSpan.test.ts new file mode 100644 index 00000000000..27ed01b0f78 --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/utils/findLastFinishingChildSpan.test.ts @@ -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(); + }); +}); diff --git a/public/app/features/explore/TraceView/components/CriticalPath/utils/findLastFinishingChildSpan.tsx b/public/app/features/explore/TraceView/components/CriticalPath/utils/findLastFinishingChildSpan.tsx new file mode 100644 index 00000000000..21bdc7edd22 --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/utils/findLastFinishingChildSpan.tsx @@ -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, + 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; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/utils/getChildOfSpans.test.ts b/public/app/features/explore/TraceView/components/CriticalPath/utils/getChildOfSpans.test.ts new file mode 100644 index 00000000000..f479114a4ae --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/utils/getChildOfSpans.test.ts @@ -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); + }); +}); diff --git a/public/app/features/explore/TraceView/components/CriticalPath/utils/getChildOfSpans.tsx b/public/app/features/explore/TraceView/components/CriticalPath/utils/getChildOfSpans.tsx new file mode 100644 index 00000000000..850c59c04c6 --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/utils/getChildOfSpans.tsx @@ -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): Map => { + 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; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.test.ts b/public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.test.ts new file mode 100644 index 00000000000..c27a6eab732 --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.test.ts @@ -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); + }); +}); diff --git a/public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.tsx b/public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.tsx new file mode 100644 index 00000000000..2b0b93eef8e --- /dev/null +++ b/public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.tsx @@ -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): Map => { + 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; diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.test.tsx index e343e54db44..691a85826e0 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.test.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.test.tsx @@ -31,10 +31,12 @@ describe('', () => { setFocusedSpanIdForSearch: jest.fn(), focusedSpanIndexForSearch: -1, setFocusedSpanIndexForSearch: jest.fn(), + setShowCriticalPathSpansOnly: jest.fn(), datasourceType: '', clear: jest.fn(), totalSpans: 100, showSpanFilters: true, + showCriticalPathSpansOnly: false, }; return ; diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.tsx index 86f574891bc..ecdcd93f51e 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.tsx @@ -31,6 +31,8 @@ export type TracePageSearchBarProps = { spanFilterMatches: Set | undefined; showSpanFilterMatchesOnly: boolean; setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void; + showCriticalPathSpansOnly: boolean; + setShowCriticalPathSpansOnly: (showCriticalPath: boolean) => void; focusedSpanIndexForSearch: number; setFocusedSpanIndexForSearch: Dispatch>; setFocusedSpanIdForSearch: Dispatch>; @@ -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 +
+ setShowCriticalPathSpansOnly(value.currentTarget.checked ?? false)} + label="Show critical path only switch" + /> + +
{ const SpanFiltersWithProps = ({ showFilters = true, matches }: { showFilters?: boolean; matches?: Set }) => { 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, diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx index d3db03719c0..f29caaca8b6 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx @@ -37,6 +37,8 @@ export type SpanFilterProps = { showSpanFilterMatchesOnly: boolean; setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void; setFocusedSpanIdForSearch: React.Dispatch>; + showCriticalPathSpansOnly: boolean; + setShowCriticalPathSpansOnly: (showCriticalPathSpansOnly: boolean) => void; spanFilterMatches: Set | 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} diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx index 65d72983099..42804129129 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx @@ -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', diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx index 1935ac0a0ea..dc310cc4bca 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx @@ -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>; spanFilterMatches: Set | 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} diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx index c00a5d9be8d..a04f95c3006 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx @@ -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({ >
} > -
+
))}
{rpc && (
)} + {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 ( + + A segment on the critical path of the overall trace/request/workflow. +
+ } + > +
+ + ); + })}
); } diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx index f74e8258d98..cd4e3cea429 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx @@ -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 { datasourceType, showServiceName, visibleSpanIds, + criticalPath, } = this.props; const { duration, @@ -530,6 +532,7 @@ export class UnthemedSpanBarRow extends React.PureComponent { > LinkModel; topOfViewRef?: RefObject; topOfViewRefType?: TopOfViewRefType; datasourceType: string; headerHeight: number; + criticalPath: CriticalPathSection[]; }; export type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TTraceTimeline; @@ -129,7 +131,9 @@ function generateRowStates( childrenHiddenIDs: Set, detailStates: Map, findMatchesIDs: Set | 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, detailStates: Map, findMatchesIDs: Set | 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 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 (
@@ -452,6 +506,7 @@ export class UnthemedVirtualizedTraceView extends React.Component
); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx index fa9e36875d9..38453863cd6 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx @@ -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; topOfViewRefType?: TopOfViewRefType; headerHeight: number; + criticalPath: CriticalPathSection[]; }; type State = { diff --git a/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx b/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx index ccd1905bd77..e4d294c8eb2 100644 --- a/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx +++ b/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx @@ -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 = {}; - // 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, 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); diff --git a/public/app/features/explore/TraceView/components/selectors/trace.test.ts b/public/app/features/explore/TraceView/components/selectors/trace.test.ts index 13346e359ce..e29aa0f0e9a 100644 --- a/public/app/features/explore/TraceView/components/selectors/trace.test.ts +++ b/public/app/features/explore/TraceView/components/selectors/trace.test.ts @@ -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) => { const expectedParentValue = value === traceSelectors.TREE_ROOT_ID ? null : value; node.children.forEach((childNode) => { expect(getSpanParentId(spanMap.get(childNode.value))).toBe(expectedParentValue); diff --git a/public/app/features/explore/TraceView/components/selectors/trace.ts b/public/app/features/explore/TraceView/components/selectors/trace.ts index ca944ba67ba..7a4f40d9f71 100644 --- a/public/app/features/explore/TraceView/components/selectors/trace.ts +++ b/public/app/features/explore/TraceView/components/selectors/trace.ts @@ -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 | 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, nodeB: TreeNode) => { 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; - } -); diff --git a/public/app/features/explore/TraceView/components/types/index.tsx b/public/app/features/explore/TraceView/components/types/index.tsx index a491bb9df8b..2c1ad3dca5d 100644 --- a/public/app/features/explore/TraceView/components/types/index.tsx +++ b/public/app/features/explore/TraceView/components/types/index.tsx @@ -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'; diff --git a/public/app/features/explore/TraceView/components/types/trace.ts b/public/app/features/explore/TraceView/components/types/trace.ts index 178c8d864e6..0507287b987 100644 --- a/public/app/features/explore/TraceView/components/types/trace.ts +++ b/public/app/features/explore/TraceView/components/types/trace.ts @@ -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; references: NonNullable; warnings: NonNullable; + childSpanIds: NonNullable; 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; +}; diff --git a/public/app/features/explore/TraceView/components/utils/TreeNode.test.ts b/public/app/features/explore/TraceView/components/utils/TreeNode.test.ts index 5136cdccb2b..2585e361858 100644 --- a/public/app/features/explore/TraceView/components/utils/TreeNode.test.ts +++ b/public/app/features/explore/TraceView/components/utils/TreeNode.test.ts @@ -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(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) => value === exteriorNode)).toEqual(null); }); it('walk() should iterate over every item once in the right order', () => { diff --git a/public/app/features/explore/TraceView/components/utils/TreeNode.ts b/public/app/features/explore/TraceView/components/utils/TreeNode.ts index 168e1eec803..f6b123f2d50 100644 --- a/public/app/features/explore/TraceView/components/utils/TreeNode.ts +++ b/public/app/features/explore/TraceView/components/utils/TreeNode.ts @@ -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 { + value: TValue; + children: Array>; -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( + fn: ((value: TValue, node: TreeNode, depth: number) => TreeNode | null) | Function, + depth = 0 + ) { + return (node: TreeNode) => fn(node.value, node, depth); } - static searchFunction(search: TreeNode | number | SearchFn | string) { + static searchFunction(search: Function | TreeNode) { 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) => (search instanceof TreeNode ? node === search : value === search); } - constructor(value?: string | number, children: TreeNode[] = []) { + constructor(value: TValue, children: Array> = []) { 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) { + this.children.push(child instanceof TreeNode ? child : new TreeNode(child)); return this; } - find(search: TreeNode | number | SearchFn | string): TreeNode | null { + find(search: Function | TreeNode): TreeNode | 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) { const searchFn = TreeNode.iterFunction(TreeNode.searchFunction(search)); - const findPath = (currentNode: TreeNode, currentPath: TreeNode[]): TreeNode[] | null => { + const findPath = ( + currentNode: TreeNode, + currentPath: Array> + ): Array> | 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, depth: number) => void, startDepth = 0) { + type StackEntry = { + node: TreeNode; + 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; + 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; + } }