[APM] Additional clock skew fixes (#25097) (#25277)

* Refactor service colors

* Calculate duration from full waterfall

* Ensure timeline label is not truncated

* Adjust child if it starts after parent has ended

* Add mark for traceRootDuration instead of xMax

* Fix tests
This commit is contained in:
Søren Louv-Jansen 2018-11-07 13:32:34 +01:00 committed by GitHub
parent 9dbc08b32f
commit 9b04e556be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 285 additions and 152 deletions

View file

@ -10,6 +10,7 @@ import styled from 'styled-components';
import { px, unit } from '../../../../../style/variables';
// @ts-ignore
import Legend from '../../../../shared/charts/Legend';
import { IServiceColors } from './Waterfall/waterfall_helpers/waterfall_helpers';
const Legends = styled.div`
display: flex;
@ -23,9 +24,7 @@ const Legends = styled.div`
`;
interface Props {
serviceColors: {
[key: string]: string;
};
serviceColors: IServiceColors;
}
export function ServiceLegends({ serviceColors }: Props) {

View file

@ -44,7 +44,7 @@ const SpanLabel = styled<{ left: number }, any>('div')`
white-space: nowrap;
position: relative;
left: ${props => `${props.left}%`};
width: ${props => `${100 - props.left}%`};
width: ${props => `${Math.max(100 - props.left, 0)}%`};
direction: rtl;
text-align: left;
margin: ${px(units.quarter)} 0 0;

View file

@ -19,6 +19,7 @@ import { AgentMark } from '../get_agent_marks';
import { SpanFlyout } from './SpanFlyout';
import { TransactionFlyout } from './TransactionFlyout';
import {
IServiceColors,
IWaterfall,
IWaterfallItem
} from './waterfall_helpers/waterfall_helpers';
@ -42,9 +43,7 @@ interface Props {
urlParams: IUrlParams;
waterfall: IWaterfall;
location: any;
serviceColors: {
[key: string]: string;
};
serviceColors: IServiceColors;
}
export class Waterfall extends Component<Props> {
@ -129,6 +128,7 @@ export class Waterfall extends Component<Props> {
<Timeline
agentMarks={this.props.agentMarks}
duration={waterfall.duration}
traceRootDuration={waterfall.traceRootDuration}
height={waterfallHeight}
margins={TIMELINE_MARGINS}
/>
@ -157,8 +157,3 @@ export class Waterfall extends Component<Props> {
});
}
}
// TODO: the agent marks and note about dropped spans were removed. Need to be re-added
// agentMarks: PropTypes.array,
// agentName: PropTypes.string.isRequired,
// droppedSpans: PropTypes.number.isRequired,

View file

@ -8,6 +8,7 @@ import { groupBy, indexBy } from 'lodash';
import { Span } from 'x-pack/plugins/apm/typings/Span';
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
import {
getClockSkew,
getWaterfallItems,
IWaterfallIndex,
IWaterfallItem
@ -101,4 +102,79 @@ describe('waterfall_helpers', () => {
).toMatchSnapshot();
});
});
describe('getClockSkew', () => {
it('should adjust when child starts before parent', () => {
const item = {
docType: 'transaction',
timestamp: 0,
duration: 50
} as IWaterfallItem;
const parentItem = {
timestamp: 100,
skew: 5,
duration: 100
} as IWaterfallItem;
const parentTransactionSkew = 1337;
expect(getClockSkew(item, parentItem, parentTransactionSkew)).toBe(130);
});
it('should adjust when child starts after parent has ended', () => {
const item = {
docType: 'transaction',
timestamp: 250,
duration: 50
} as IWaterfallItem;
const parentItem = {
timestamp: 100,
skew: 5,
duration: 100
} as IWaterfallItem;
const parentTransactionSkew = 1337;
expect(getClockSkew(item, parentItem, parentTransactionSkew)).toBe(-120);
});
it('should not adjust when child starts within parent duration', () => {
const item = {
docType: 'transaction',
timestamp: 150,
duration: 50
} as IWaterfallItem;
const parentItem = {
timestamp: 100,
skew: 5,
duration: 100
} as IWaterfallItem;
const parentTransactionSkew = 1337;
expect(getClockSkew(item, parentItem, parentTransactionSkew)).toBe(0);
});
it('should return parentTransactionSkew for spans', () => {
const item = {
docType: 'span'
} as IWaterfallItem;
const parentItem = {} as IWaterfallItem;
const parentTransactionSkew = 1337;
expect(getClockSkew(item, parentItem, parentTransactionSkew)).toBe(1337);
});
it('should handle missing parentItem', () => {
const item = {
docType: 'transaction'
} as IWaterfallItem;
const parentItem = undefined;
const parentTransactionSkew = 1337;
expect(getClockSkew(item, parentItem, parentTransactionSkew)).toBe(0);
});
});
});

View file

@ -11,8 +11,10 @@ import {
indexBy,
isEmpty,
sortBy,
uniq
uniq,
zipObject
} from 'lodash';
import { colors } from 'x-pack/plugins/apm/public/style/variables';
import { Span } from '../../../../../../../../typings/Span';
import { Transaction } from '../../../../../../../../typings/Transaction';
@ -26,12 +28,17 @@ export interface IWaterfallGroup {
export interface IWaterfall {
traceRoot?: Transaction;
traceRootDuration?: number;
duration?: number;
traceRootDuration: number;
/**
* Duration in us
*/
duration: number;
services: string[];
items: IWaterfallItem[];
itemsById: IWaterfallIndex;
getTransactionById: (id?: IWaterfallItem['id']) => Transaction | undefined;
serviceColors: IServiceColors;
}
interface IWaterfallItemBase {
@ -39,9 +46,25 @@ interface IWaterfallItemBase {
parentId?: string;
serviceName: string;
name: string;
/**
* Duration in us
*/
duration: number;
/**
* start timestamp in us
*/
timestamp: number;
/**
* offset from first item in us
*/
offset: number;
/**
* skew from timestamp in us
*/
skew: number;
childIds?: Array<IWaterfallItemBase['id']>;
}
@ -120,38 +143,39 @@ function getSpanItem(span: Span): IWaterfallItemSpan {
};
}
function getClockSkew(
export function getClockSkew(
item: IWaterfallItem,
itemsById: IWaterfallIndex,
parentItem: IWaterfallItem | undefined,
parentTransactionSkew: number
) {
switch (item.docType) {
case 'span':
return parentTransactionSkew;
case 'transaction': {
if (!item.parentId) {
return 0;
}
const parentItem = itemsById[item.parentId];
// For some reason the parent span and related transactions might be missing.
if (!parentItem) {
return 0;
}
// determine if child starts before the parent, and in that case how much
const diff = parentItem.timestamp + parentItem.skew - item.timestamp;
const parentStart = parentItem.timestamp + parentItem.skew;
const parentEnd = parentStart + parentItem.duration;
// If child transaction starts after parent span there is no clock skew
if (diff < 0) {
// determine if child starts before the parent
const offsetStart = parentStart - item.timestamp;
// determine if child starts after the parent has ended
const offsetEnd = item.timestamp - parentEnd;
// child transaction starts before parent OR
// child transaction starts after parent has ended
if (offsetStart > 0 || offsetEnd > 0) {
const latency = Math.max(parentItem.duration - item.duration, 0) / 2;
return offsetStart + latency;
// child transaction starts withing parent duration and no adjustment is needed
} else {
return 0;
}
// latency can only be calculated if parent duration is larger than child duration
const latency = Math.max(parentItem.duration - item.duration, 0);
const skew = diff + latency / 2;
return skew;
}
}
}
@ -165,7 +189,8 @@ export function getWaterfallItems(
item: IWaterfallItem,
parentTransactionSkew: number
): IWaterfallItem[] {
const skew = getClockSkew(item, itemsById, parentTransactionSkew);
const parentItem = item.parentId ? itemsById[item.parentId] : undefined;
const skew = getClockSkew(item, parentItem, parentTransactionSkew);
const children = sortBy(childrenByParentId[item.id] || [], 'timestamp');
item.childIds = children.map(child => child.id);
@ -193,6 +218,45 @@ function getServices(items: IWaterfallItem[]) {
return uniq(serviceNames);
}
export interface IServiceColors {
[key: string]: string;
}
function getServiceColors(services: string[]) {
const assignedColors = [
colors.apmBlue,
colors.apmGreen,
colors.apmPurple,
colors.apmRed2,
colors.apmTan,
colors.apmOrange,
colors.apmYellow
];
return zipObject(services, assignedColors) as IServiceColors;
}
function getDuration(items: IWaterfallItem[]) {
const timestampStart = items[0].timestamp;
const timestampEnd = Math.max(
...items.map(item => item.timestamp + item.duration + item.skew)
);
return timestampEnd - timestampStart;
}
function createGetTransactionById(itemsById: IWaterfallIndex) {
return (id?: IWaterfallItem['id']) => {
if (!id) {
return;
}
const item = itemsById[id];
if (item.docType === 'transaction') {
return item.transaction;
}
};
}
export function getWaterfall(
hits: Array<Span | Transaction>,
entryTransaction: Transaction
@ -200,9 +264,12 @@ export function getWaterfall(
if (isEmpty(hits)) {
return {
services: [],
duration: 0,
traceRootDuration: 0,
items: [],
itemsById: {},
getTransactionById: () => undefined
getTransactionById: () => undefined,
serviceColors: {}
};
}
@ -235,25 +302,22 @@ export function getWaterfall(
entryTransactionItem
);
const traceRoot = getTraceRoot(childrenByParentId);
const getTransactionById = (id?: IWaterfallItem['id']) => {
if (!id) {
return;
}
const item = itemsById[id];
if (item.docType === 'transaction') {
return item.transaction;
}
};
const duration = getDuration(items);
const traceRootDuration = traceRoot
? traceRoot.transaction.duration.us
: duration;
const services = getServices(items);
const getTransactionById = createGetTransactionById(itemsById);
const serviceColors = getServiceColors(services);
return {
traceRoot,
traceRootDuration: traceRoot && traceRoot.transaction.duration.us,
duration: entryTransaction.transaction.duration.us,
services: getServices(items),
traceRootDuration,
duration,
services,
items,
itemsById,
getTransactionById
getTransactionById,
serviceColors
};
}

View file

@ -1,26 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { zipObject } from 'lodash';
import { colors } from '../../../../../style/variables';
interface IServiceColors {
[key: string]: string;
}
export function getServiceColors(services: string[]): IServiceColors {
const assignedColors = [
colors.apmBlue,
colors.apmGreen,
colors.apmPurple,
colors.apmRed2,
colors.apmTan,
colors.apmOrange,
colors.apmYellow
];
return zipObject(services, assignedColors);
}

View file

@ -17,7 +17,6 @@ import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
import { getAgentMarks } from './get_agent_marks';
import { getServiceColors } from './getServiceColors';
import { ServiceLegends } from './ServiceLegends';
import { Waterfall } from './Waterfall';
import { IWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers';
@ -39,13 +38,12 @@ export function WaterfallContainer({
if (!waterfall) {
return null;
}
const serviceColors = getServiceColors(waterfall.services);
return (
<div>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<ServiceLegends serviceColors={serviceColors} />
<ServiceLegends serviceColors={waterfall.serviceColors} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
@ -59,7 +57,7 @@ export function WaterfallContainer({
<Waterfall
agentMarks={agentMarks}
location={location}
serviceColors={serviceColors}
serviceColors={waterfall.serviceColors}
urlParams={urlParams}
waterfall={waterfall}
/>

View file

@ -6,7 +6,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { inRange } from 'lodash';
import { Sticky } from 'react-sticky';
import { XYPlot, XAxis } from 'react-vis';
import LastTickValue from './LastTickValue';
@ -14,14 +14,26 @@ import AgentMarker from './AgentMarker';
import { colors, px } from '../../../../style/variables';
import { getTimeFormatter } from '../../../../utils/formatters';
// Remove last tick if it's too close to xMax
const getXAxisTickValues = (tickValues, xMax) =>
_.last(tickValues) * 1.05 > xMax ? tickValues.slice(0, -1) : tickValues;
// Remove any tick that is too close to traceRootDuration
const getXAxisTickValues = (tickValues, traceRootDuration) => {
if (!tickValues) {
return [];
}
function TimelineAxis({ plotValues, agentMarks }) {
const padding = (tickValues[1] - tickValues[0]) / 2;
const lowerBound = traceRootDuration - padding;
const upperBound = traceRootDuration + padding;
return tickValues.filter(value => {
const isInRange = inRange(value, lowerBound, upperBound);
return !isInRange && value !== traceRootDuration;
});
};
function TimelineAxis({ plotValues, agentMarks, traceRootDuration }) {
const { margins, tickValues, width, xDomain, xMax, xScale } = plotValues;
const tickFormat = getTimeFormatter(xMax);
const xAxisTickValues = getXAxisTickValues(tickValues, xMax);
const xAxisTickValues = getXAxisTickValues(tickValues, traceRootDuration);
return (
<Sticky disableCompensation>
@ -62,8 +74,8 @@ function TimelineAxis({ plotValues, agentMarks }) {
/>
<LastTickValue
x={xScale(xMax)}
value={tickFormat(xMax)}
x={xScale(traceRootDuration)}
value={tickFormat(traceRootDuration)}
marginTop={28}
/>

View file

@ -11,13 +11,13 @@ import { colors } from '../../../../style/variables';
class VerticalLines extends PureComponent {
render() {
const { traceRootDuration } = this.props;
const {
width,
height,
margins,
xDomain,
tickValues,
xMax
tickValues
} = this.props.plotValues;
const agentMarkTimes = this.props.agentMarks.map(({ us }) => us);
@ -43,7 +43,7 @@ class VerticalLines extends PureComponent {
/>
<VerticalGridLines
tickValues={[...agentMarkTimes, xMax]}
tickValues={[...agentMarkTimes, traceRootDuration]}
style={{ stroke: colors.gray3 }}
/>
</XYPlot>

View file

@ -9,7 +9,6 @@ import { mount } from 'enzyme';
import { StickyContainer } from 'react-sticky';
import Timeline from '../index';
import props from './props.json';
import { mockMoment, toJson } from '../../../../../utils/testHelpers';
describe('Timeline', () => {
@ -18,9 +17,28 @@ describe('Timeline', () => {
});
it('should render with data', () => {
const props = {
traceRootDuration: 200000,
width: 1000,
duration: 200000,
height: 116,
margins: {
top: 100,
left: 50,
right: 50,
bottom: 0
},
animation: null,
agentMarks: [
{ name: 'timeToFirstByte', us: 100000 },
{ name: 'domInteractive', us: 110000 },
{ name: 'domComplete', us: 190000 }
]
};
const wrapper = mount(
<StickyContainer>
<Timeline header={<div>Hello - i am a header</div>} {...props} />
<Timeline {...props} />
</StickyContainer>
);

View file

@ -144,7 +144,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(87.9408646541237, 0)"
transform="translate(90, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -186,7 +186,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(175.8817293082474, 0)"
transform="translate(180, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -228,7 +228,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(263.82259396237106, 0)"
transform="translate(270, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -270,7 +270,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(351.7634586164948, 0)"
transform="translate(360, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -312,7 +312,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(439.7043232706185, 0)"
transform="translate(450, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -354,7 +354,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(527.6451879247421, 0)"
transform="translate(540, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -396,7 +396,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(615.5860525788659, 0)"
transform="translate(630, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -438,7 +438,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(703.5269172329896, 0)"
transform="translate(720, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -480,7 +480,7 @@ exports[`Timeline should render with data 1`] = `
},
}
}
transform="translate(791.4677818871132, 0)"
transform="translate(810, 0)"
>
<line
className="rv-xy-plot__axis__tick__line"
@ -523,7 +523,7 @@ exports[`Timeline should render with data 1`] = `
textAnchor="middle"
transform="translate(0, -8)"
>
205 ms
200 ms
</text>
</g>
</svg>
@ -531,7 +531,7 @@ exports[`Timeline should render with data 1`] = `
style={
Object {
"bottom": 0,
"left": "484.2043232706185px",
"left": "494.5px",
"position": "absolute",
}
}
@ -561,7 +561,7 @@ exports[`Timeline should render with data 1`] = `
style={
Object {
"bottom": 0,
"left": "528.1747555976804px",
"left": "539.5px",
"position": "absolute",
}
}
@ -591,7 +591,7 @@ exports[`Timeline should render with data 1`] = `
style={
Object {
"bottom": 0,
"left": "879.9382142141751px",
"left": "899.5px",
"position": "absolute",
}
}
@ -673,8 +673,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={87.9408646541237}
x2={87.9408646541237}
x1={90}
x2={90}
y1={0}
y2={116}
/>
@ -685,8 +685,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={175.8817293082474}
x2={175.8817293082474}
x1={180}
x2={180}
y1={0}
y2={116}
/>
@ -697,8 +697,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={263.82259396237106}
x2={263.82259396237106}
x1={270}
x2={270}
y1={0}
y2={116}
/>
@ -709,8 +709,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={351.7634586164948}
x2={351.7634586164948}
x1={360}
x2={360}
y1={0}
y2={116}
/>
@ -721,8 +721,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={439.7043232706185}
x2={439.7043232706185}
x1={450}
x2={450}
y1={0}
y2={116}
/>
@ -733,8 +733,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={527.6451879247421}
x2={527.6451879247421}
x1={540}
x2={540}
y1={0}
y2={116}
/>
@ -745,8 +745,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={615.5860525788659}
x2={615.5860525788659}
x1={630}
x2={630}
y1={0}
y2={116}
/>
@ -757,8 +757,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={703.5269172329896}
x2={703.5269172329896}
x1={720}
x2={720}
y1={0}
y2={116}
/>
@ -769,8 +769,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={791.4677818871132}
x2={791.4677818871132}
x1={810}
x2={810}
y1={0}
y2={116}
/>
@ -781,8 +781,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#f5f5f5",
}
}
x1={879.408646541237}
x2={879.408646541237}
x1={900}
x2={900}
y1={0}
y2={116}
/>
@ -798,8 +798,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#999999",
}
}
x1={439.7043232706185}
x2={439.7043232706185}
x1={450}
x2={450}
y1={0}
y2={116}
/>
@ -810,8 +810,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#999999",
}
}
x1={483.6747555976803}
x2={483.6747555976803}
x1={495.00000000000006}
x2={495.00000000000006}
y1={0}
y2={116}
/>
@ -822,8 +822,8 @@ exports[`Timeline should render with data 1`] = `
"stroke": "#999999",
}
}
x1={835.4382142141751}
x2={835.4382142141751}
x1={855}
x2={855}
y1={0}
y2={116}
/>

View file

@ -1,17 +0,0 @@
{
"width": 1000,
"duration": 204683,
"height": 116,
"margins": {
"top": 100,
"left": 50,
"right": 50,
"bottom": 0
},
"animation": null,
"agentMarks": [
{ "name": "timeToFirstByte", "us": 100000 },
{ "name": "domInteractive", "us": 110000 },
{ "name": "domComplete", "us": 190000 }
]
}

View file

@ -14,17 +14,31 @@ import VerticalLines from './VerticalLines';
class Timeline extends PureComponent {
render() {
const { width, duration, agentMarks, height, margins } = this.props;
const {
width,
duration,
agentMarks,
traceRootDuration,
height,
margins
} = this.props;
if (duration == null || !width) {
return null;
}
const plotValues = getPlotValues({ width, duration, height, margins });
return (
<div>
<TimelineAxis plotValues={plotValues} agentMarks={agentMarks} />
<VerticalLines plotValues={plotValues} agentMarks={agentMarks} />
<TimelineAxis
plotValues={plotValues}
agentMarks={agentMarks}
traceRootDuration={traceRootDuration}
/>
<VerticalLines
plotValues={plotValues}
agentMarks={agentMarks}
traceRootDuration={traceRootDuration}
/>
</div>
);
}