Skip to content

Commit

Permalink
Merge pull request elastic#5 from AlexanderWert/cpa-onweek
Browse files Browse the repository at this point in the history
Implemented critical path calculation in Kibana frontend
  • Loading branch information
dgieselaar authored Jun 28, 2022
2 parents 0d283e1 + b548530 commit 216bcab
Show file tree
Hide file tree
Showing 3 changed files with 324 additions and 30 deletions.
191 changes: 191 additions & 0 deletions x-pack/plugins/apm/public/components/app/trace_explorer/cpa_helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { Transaction } from "../../../../typings/es_schemas/ui/transaction";
import { Span } from "../../../../typings/es_schemas/ui/span";
import { groupBy, sortBy } from 'lodash';
import hash from 'object-hash'

const ROOT_ID = 'root';

interface ITraceItem {
name: string;
id: string;
parentId?: string;
start: number;
end: number;
span?: Span;
transaction?: Transaction;
}

interface ITrace {
root: ITraceItem;
childrenByParentId: Record<string, ITraceItem[]>;
}

interface TraceSegment {
item: ITraceItem;
intervalStart: number;
intervalEnd: number;
parentHash: string;
}

export interface ICriticalPathItem {
hash: string;
name: string;
selfDuration: number;
duration: number;
parentHash: string;
isRoot: boolean;
}

export interface ICriticalPath {
elemetnsByHash: Record<string, ICriticalPathItem>;
roots: ICriticalPathItem[];
}

export const calculateCriticalPath = (criticalPathData: Array<Transaction | Span>) : ICriticalPath => {
const tracesMap = groupBy(criticalPathData, (item) => item.trace.id);
const criticalPaths = Object.entries(tracesMap).map((entry) => getTrace(entry[1])).filter(t => t !== undefined)
.map(trace => calculateCriticalPathForTrace(trace))
const numOfcriticalPaths = criticalPaths.length;
const criticalPath: Record<string, ICriticalPathItem> = {};

criticalPaths.forEach(cp => {
cp?.forEach(cpi => {
var obj = criticalPath[cpi.hash];
if(!obj){
criticalPath[cpi.hash] = cpi;
obj = cpi;
obj.selfDuration = obj.selfDuration / numOfcriticalPaths
obj.duration = obj.duration / numOfcriticalPaths
} else {
obj.selfDuration += cpi.selfDuration / numOfcriticalPaths;
obj.duration += cpi.duration / numOfcriticalPaths;
}
});
});

return {
elemetnsByHash: criticalPath,
roots: Object.entries(criticalPath).map((entry) => entry[1]).filter(cpi => cpi.isRoot)
};
};


const getTrace = (criticalPathData: Array<Transaction | Span>) : ITrace | undefined => {
const traceItems = criticalPathData.map(item => {
const docType: 'span' | 'transaction' = item.processor.event;
switch (docType) {
case 'span': {
const span = item as Span;
return {
name: span.span.name,
span: span,
transaction: undefined,
id: span.span.id,
parentId: span.parent?.id,
start: span.timestamp.us,
end: span.timestamp.us + span.span.duration.us
};
}
case 'transaction':
const transaction = item as Transaction;
return {
name: transaction.transaction.name,
span: undefined,
transaction: transaction,
id: transaction.transaction.id,
parentId: transaction.parent?.id,
start: transaction.timestamp.us,
end: transaction.timestamp.us + transaction.transaction.duration.us
};
}
});


const itemsByParent = groupBy(traceItems, (item) => (item.parentId ? item.parentId : ROOT_ID));
const rootItem = itemsByParent[ROOT_ID];
if(rootItem){
return {
root: rootItem[0],
childrenByParentId: itemsByParent
};
} else {
return undefined;
}



}

const calculateCriticalPathForTrace = (trace: ITrace | undefined) => {
if(trace){
const calculateCriticalPathForChildren : Array<TraceSegment> = [{
item: trace.root,
intervalStart: trace.root.start,
intervalEnd: trace.root.end,
parentHash: ROOT_ID
}];

const criticalPathSegments : ICriticalPathItem[] = [];

while( calculateCriticalPathForChildren.length > 0){
const nextSegment = calculateCriticalPathForChildren.pop();
if(nextSegment){
const result = criticalPathForItem(trace, nextSegment)
calculateCriticalPathForChildren.push(...result.childrenOnCriticalPath);
criticalPathSegments.push(result.criticalPathItem);
}
}

return criticalPathSegments;
}
}


const criticalPathForItem = ( trace: ITrace, segment: TraceSegment ) => {
var criticalPathDurationSum = 0;
const item = segment.item;
const directChildren = trace.childrenByParentId[item.id];

var childrenOnCriticalPath : TraceSegment[] = [];
const thisHash = hash({'name': item.name, 'parent': segment.parentHash});
if(directChildren && directChildren.length > 0){
const orderedChildren = [...directChildren].sort((a,b) => (b.end - a.end));
var scanTimestamp = segment.intervalEnd;
orderedChildren.forEach(child => {
const childStart = Math.max(child.start, segment.intervalStart);
const childEnd = Math.min(child.end, scanTimestamp);
if(childStart >= scanTimestamp) {
// ignore this child as it is not on the critical path
} else {
if(childEnd < scanTimestamp){
criticalPathDurationSum += scanTimestamp - childEnd;
}
childrenOnCriticalPath.push({
item: child,
intervalStart: childStart,
intervalEnd: childEnd,
parentHash: thisHash
});
scanTimestamp = childStart;
}
});
if(scanTimestamp > segment.intervalStart){
criticalPathDurationSum += scanTimestamp - segment.intervalStart;
}
} else {
criticalPathDurationSum += segment.intervalEnd - segment.intervalStart;
}

return {
criticalPathItem: {
hash: thisHash,
name: item.name,
selfDuration: criticalPathDurationSum,
duration: segment.intervalEnd - segment.intervalStart,
parentHash: segment.parentHash,
isRoot: segment.parentHash === ROOT_ID
},
childrenOnCriticalPath: childrenOnCriticalPath
};
};

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@

import React from 'react';
import {
Chart,
Datum,
Partition,
PartitionLayout,
PrimitiveValue,
Settings,
TooltipInfo,
PartialTheme,
} from '@elastic/charts';

import { useChartTheme } from '@kbn/observability-plugin/public';
import { useTheme } from '../../../../hooks/use_theme';
import { ICriticalPath } from '../cpa_helper';

export const CriticalPathFlamegraph = ({criticalPath}:{
criticalPath: ICriticalPath;
}) => {

const theme = useTheme();

const chartSize = {
//height: layers.length * 20,
width: '100%',
};

const chartTheme = useChartTheme();
const themeOverrides: PartialTheme = {
chartMargins: { top: 0, bottom: 0, left: 0, right: 0 },
partition: {
fillLabel: {
fontFamily: theme.eui.euiCodeFontFamily,
clipText: true,
},
fontFamily: theme.eui.euiCodeFontFamily,
minFontSize: 9,
maxFontSize: 9,
},
};

return (
<></>
/*
<Chart size={chartSize}>
<Settings
theme={[themeOverrides, ...chartTheme]}
tooltip={{
customTooltip: (info) => (
<CustomTooltip
{...info}
valueUnit={valueUnit}
nodes={data?.nodes ?? {}}
/>
),
}}
/>
<Partition
id="profile_graph"
data={points}
layers={layers}
drilldown
maxRowCount={1}
layout={PartitionLayout.icicle}
valueAccessor={(d: Datum) => d.value as number}
valueFormatter={() => ''}
/>
</Chart>
*/
);
}
Loading

0 comments on commit 216bcab

Please sign in to comment.