Skip to content

Commit

Permalink
feat(webapp): Issue when comparison / diff timelines are out of range (
Browse files Browse the repository at this point in the history
…#1615)

* feat: sync timeline
  • Loading branch information
pavelpashkovsky authored Nov 1, 2022
1 parent 6edb97b commit 211ccca
Show file tree
Hide file tree
Showing 14 changed files with 456 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import SyncTimelines from './index';
import { getTitle, getSelectionBoundaries } from './useSync';
import { Selection } from '../markings';

const from = 1666790156;
const to = 1666791905;

const propsWhenActive = {
timeline: {
color: 'rgb(208, 102, 212)',
data: {
startTime: 1666790760,
samples: [
16629, 50854, 14454, 3819, 40720, 23172, 22483, 7854, 33186, 81804,
46942, 40631, 14135, 12824, 27514, 14366, 39691, 45412, 18631, 10371,
31606, 53775, 42399, 40527, 20599, 27836, 23624, 80152, 9149, 45283,
58361, 48738, 30363, 13834, 30849, 81892,
],
durationDelta: 10,
},
},
leftSelection: {
from: String(from),
to: '1666790783',
},
rightSelection: {
from: '1666791459',
to: String(to),
},
};

const propsWhenHidden = {
timeline: {
data: {
startTime: 1666779070,
samples: [
1601, 30312, 22044, 53925, 44264, 26014, 15645, 14376, 21880, 8555,
15995, 5849, 14138, 18929, 41842, 59101, 18931, 65541, 47674, 35886,
55583, 19283, 19745, 9314, 1531,
],
durationDelta: 10,
},
},
leftSelection: {
from: '1666779093',
to: '1666779239',
},
rightSelection: {
from: '1666779140',
to: '1666779296',
},
};

const { getByRole, queryByText } = screen;

describe('SyncTimelines', () => {
it('renders sync and ignore buttons when active', async () => {
render(<SyncTimelines onSync={() => {}} {...propsWhenActive} />);

expect(getByRole('button', { name: 'Ignore' })).toBeInTheDocument();
expect(getByRole('button', { name: 'Sync Timelines' })).toBeInTheDocument();
});

it('hidden when selections are in range', async () => {
render(<SyncTimelines onSync={() => {}} {...propsWhenHidden} />);

expect(queryByText('Sync')).not.toBeInTheDocument();
});

it('onSync returns correct from/to', async () => {
let result = { from: '', to: '' };
render(
<SyncTimelines
{...propsWhenActive}
onSync={(from, to) => {
result = { from, to };
}}
/>
);

fireEvent.click(getByRole('button', { name: 'Sync Timelines' }));

// new main timeline FROM = from - 1ms, TO = to + 1ms
expect(Number(result.from) - from * 1000).toEqual(-1);
expect(Number(result.to) - to * 1000).toEqual(1);
});

it('Hide button works', async () => {
render(<SyncTimelines onSync={() => {}} {...propsWhenActive} />);

fireEvent.click(getByRole('button', { name: 'Ignore' }));

expect(queryByText('Sync')).not.toBeInTheDocument();
});
});

describe('getTitle', () => {
it('both selections are out of range', () => {
expect(getTitle(false, false)).toEqual(
'Warning: Baseline and Comparison timeline selections are out of range'
);
});
it('baseline timeline selection is out of range', () => {
expect(getTitle(false, true)).toEqual(
'Warning: Baseline timeline selection is out of range'
);
});
it('comparison timeline selection is out of range', () => {
expect(getTitle(true, false)).toEqual(
'Warning: Comparison timeline selection is out of range'
);
});
});

describe('getSelectionBoundaries', () => {
const boundariesFromRelativeTime = getSelectionBoundaries({
from: 'now-1h',
to: 'now',
} as Selection);
const boundariesFromUnixTime = getSelectionBoundaries({
from: '1667204605',
to: '1667204867',
} as Selection);

const res = [
boundariesFromRelativeTime.from,
boundariesFromRelativeTime.to,
boundariesFromUnixTime.from,
boundariesFromUnixTime.to,
];

it('returns correct data type', () => {
expect(res.every((i) => typeof i === 'number')).toBe(true);
});

it('returns ms format (13 digits)', () => {
expect(res.every((i) => String(i).length === 13)).toBe(true);
});

it('TO greater than FROM', () => {
expect(
boundariesFromRelativeTime.to > boundariesFromRelativeTime.from &&
boundariesFromUnixTime.to > boundariesFromUnixTime.from
).toBe(true);
});
});
64 changes: 64 additions & 0 deletions webapp/javascript/components/TimelineChart/SyncTimelines/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import Button from '@webapp/ui/Button';
import { TimelineData } from '@webapp/components/TimelineChart/TimelineChartWrapper';
import StatusMessage from '@webapp/ui/StatusMessage';
import { useSync } from './useSync';
import styles from './styles.module.scss';

interface SyncTimelinesProps {
timeline: TimelineData;
leftSelection: {
from: string;
to: string;
};
rightSelection: {
from: string;
to: string;
};
onSync: (from: string, until: string) => void;
}

function SyncTimelines({
timeline,
leftSelection,
rightSelection,
onSync,
}: SyncTimelinesProps) {
const { isWarningHidden, onIgnore, title, onSyncClick } = useSync({
timeline,
leftSelection,
rightSelection,
onSync,
});

if (isWarningHidden) {
return null;
}

return (
<StatusMessage
type="warning"
message={title}
action={
<div className={styles.buttons}>
<Button
kind="outline"
onClick={onIgnore}
className={styles.ignoreButton}
>
Ignore
</Button>
<Button
kind="outline"
onClick={onSyncClick}
className={styles.syncButton}
>
Sync Timelines
</Button>
</div>
}
/>
);
}

export default SyncTimelines;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.buttons {
display: flex;
align-items: center;
flex-direction: row;
}

.syncButton {
margin-left: 0.5em;
font-weight: normal;
}

.ignoreButton {
@extend .syncButton;
}
119 changes: 119 additions & 0 deletions webapp/javascript/components/TimelineChart/SyncTimelines/useSync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { useEffect, useState } from 'react';
import { centerTimelineData } from '@webapp/components/TimelineChart/centerTimelineData';
import { TimelineData } from '@webapp/components/TimelineChart/TimelineChartWrapper';
import { formatAsOBject } from '@webapp/util/formatDate';
import { Selection } from '../markings';

interface UseSyncParams {
timeline: TimelineData;
leftSelection: {
from: string;
to: string;
};
rightSelection: {
from: string;
to: string;
};
onSync: (from: string, until: string) => void;
}

const timeOffset = 5 * 60 * 1000;
const selectionOffset = 5000;

export const getTitle = (leftInRange: boolean, rightInRange: boolean) => {
if (!leftInRange && !rightInRange) {
return 'Warning: Baseline and Comparison timeline selections are out of range';
}
if (!rightInRange) {
return 'Warning: Comparison timeline selection is out of range';
}
return 'Warning: Baseline timeline selection is out of range';
};

const isInRange = (
from: number,
to: number,
selectionFrom: number,
selectionTo: number
) => {
return selectionFrom + timeOffset >= from && selectionTo - timeOffset <= to;
};

export const getSelectionBoundaries = (selection: Selection) => {
if (selection.from.startsWith('now') || selection.to.startsWith('now')) {
return {
from: new Date(formatAsOBject(selection.from)).getTime(),
to: new Date(formatAsOBject(selection.to)).getTime(),
};
}

return {
from: Number(selection.from) * 1000,
to: Number(selection.to) * 1000,
};
};

export function useSync({
timeline,
leftSelection,
rightSelection,
onSync,
}: UseSyncParams) {
const [isIgnoring, setIgnoring] = useState(false);

useEffect(() => {
if (isIgnoring) {
setIgnoring(false);
}
}, [leftSelection, rightSelection, timeline]);

const { from: leftFrom, to: leftTo } = getSelectionBoundaries(
leftSelection as Selection
);

const { from: rightFrom, to: rightTo } = getSelectionBoundaries(
rightSelection as Selection
);

const centeredData = centerTimelineData(timeline);

const timelineFrom = centeredData?.[0]?.[0];
const timelineTo = centeredData?.[centeredData?.length - 1]?.[0];

const isLeftInRange = isInRange(timelineFrom, timelineTo, leftFrom, leftTo);
const isRightInRange = isInRange(
timelineFrom,
timelineTo,
rightFrom,
rightTo
);

const onSyncClick = () => {
const selectionsLimits = [leftFrom, leftTo, rightFrom, rightTo];
const selectionMin = Math.min(...selectionsLimits);
const selectionMax = Math.max(...selectionsLimits);
// when some of selection is in relative time (now, now-1h etc.), we have to extend detecting time buffer
// 1) to prevent falsy detections
// 2) to workaraund pecularity that when we change selection we don't refetch main timeline
const offset = [
leftSelection.from,
rightSelection.from,
leftSelection.to,
rightSelection.to,
].some((p) => String(p).startsWith('now'))
? selectionOffset
: 1;

onSync(String(selectionMin - offset), String(selectionMax + offset));
};

return {
isWarningHidden:
!timeline.data?.samples.length ||
(isLeftInRange && isRightInRange) ||
isIgnoring,
title: getTitle(isLeftInRange, isRightInRange),
onIgnore: () => setIgnoring(true),
onSyncClick,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import TimelineChart from './TimelineChart';
import styles from './TimelineChartWrapper.module.css';
import { markingsFromSelection, ANNOTATION_COLOR } from './markings';
import { ContextMenuProps } from './ContextMenu.plugin';
import { centerTimelineData } from './centerTimelineData';

export interface TimelineGroupData {
data: Group;
tagName: string;
color?: Color;
}

interface TimelineData {
export interface TimelineData {
data?: Timeline;
color?: string;
}
Expand Down Expand Up @@ -495,28 +496,4 @@ function areTimelinesTheSame(
return smallest.samples.every((a) => map.has(a));
}

// Since profiling data is chuked by 10 seconds slices
// it's more user friendly to point a `center` of a data chunk
// as a bar rather than starting point, so we add 5 seconds to each chunk to 'center' it
function centerTimelineData(timelineData: TimelineData) {
return timelineData.data
? decodeTimelineData(timelineData.data).map((x) => [
x[0] + 5000,
x[1] === 0 ? 0 : x[1] - 1,
])
: [[]];
}

function decodeTimelineData(timeline: Timeline) {
if (!timeline) {
return [];
}
let time = timeline.startTime;
return timeline.samples.map((x) => {
const res = [time * 1000, x];
time += timeline.durationDelta;
return res;
});
}

export default TimelineChartWrapper;
Loading

0 comments on commit 211ccca

Please sign in to comment.