Skip to content

Commit

Permalink
PP-3302: Add more units to duration formatting in the UI
Browse files Browse the repository at this point in the history
Summary: We want to use the duration type to visualize things like pod age, and data retention period. However, the current units for a human readable duration only go up to seconds. This makes it very difficult when reading a duration in days. In this diff, (as well as D10997 for the CLI), days, minutes, and hours are added as an option. The common case of latency of 60 seconds or less will remain the same.

Test Plan: ran the UI , unit tests, tried existing

Reviewers: nlanam, michelle, jamesbartlett, zasgar

Reviewed By: nlanam

JIRA Issues: PP-3302

Signed-off-by: Natalie Serrino <nserrino@pixielabs.ai>

Differential Revision: https://phab.corp.pixielabs.ai/D11006

GitOrigin-RevId: ed955a9
  • Loading branch information
Natalie Serrino authored and copybaranaut committed Mar 19, 2022
1 parent ed77a68 commit 55931b4
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 39 deletions.
81 changes: 66 additions & 15 deletions src/ui/src/containers/format-data/format-data-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,38 +26,89 @@ import { DARK_THEME } from 'app/components';
import {
AlertData,
BasicDurationRenderer,
dataWithUnitsToString,
LatencyDurationRenderer,
formatBytes,
formatDuration,
} from './format-data';

/* eslint-disable react-memo/require-usememo */
describe('formatters Test', () => {
it('should handle 0 Bytes correctly', () => {
expect(formatBytes(0)).toEqual({ units: '\u00a0B', val: '0' });
});

describe('formatDuration test', () => {
it('should handle 0 duration correctly', () => {
expect(formatDuration(0)).toEqual({ units: 'ns', val: '0' });
});
it('should handle |x| < 1 Bytes correctly', () => {
expect(formatBytes(0.1)).toEqual({ units: '\u00a0B', val: '0.1' });
expect(formatDuration(0)).toEqual({ units: ['ns'], val: ['0'] });
});

it('should handle |x| < 1 duration correctly', () => {
expect(formatDuration(0.1)).toEqual({ units: 'ns', val: '0.1' });
expect(formatDuration(0.1)).toEqual({ units: ['ns'], val: ['0.1'] });
});
it('should handle x < 0 duration correctly', () => {
expect(formatDuration(-2)).toEqual({ units: 'ns', val: '-2' });
expect(formatDuration(-2)).toEqual({ units: ['ns'], val: ['-2'] });
});
it('should handle large negative durations correctly', () => {
expect(formatDuration(-12 * 60 * 60 * 1000 * 1000 * 1000 - 1335144)).toEqual(
{ units: ['hours', 'min'], val: ['-12', '0'] });
});
it('should handle nanoseconds correctly', () => {
expect(formatDuration(144)).toEqual({ units: ['ns'], val: ['144'] });
});
it('should handle microseconds correctly', () => {
expect(formatDuration(5144)).toEqual({ units: ['\u00b5s'], val: ['5.1'] });
});
it('should handle milliseconds correctly', () => {
expect(formatDuration(5 * 1000 * 1000)).toEqual({ units: ['ms'], val: ['5'] });
});
it('should handle seconds correctly', () => {
expect(formatDuration(13 * 1000 * 1000 * 1000 + 1242)).toEqual({ units: ['\u00a0s'], val: ['13'] });
});
it('should handle minutes correctly', () => {
expect(formatDuration(5 * 60 * 1000 * 1000 * 1000 + 1334)).toEqual(
{ units: ['min', 's'], val: ['5', '0'] });
});
it('should handle hours correctly', () => {
expect(formatDuration(12 * 60 * 60 * 1000 * 1000 * 1000 + 1335144)).toEqual(
{ units: ['hours', 'min'], val: ['12', '0'] });
});
it('should handle days correctly', () => {
expect(formatDuration(25 * 24 * 60 * 60 * 1000 * 1000 * 1000 + 133514124)).toEqual(
{ units: ['days', 'hours'], val: ['25', '0'] });
});
});

describe('formatBytes Test', () => {
it('should handle 0 Bytes correctly', () => {
expect(formatBytes(0)).toEqual({ units: ['\u00a0B'], val: ['0'] });
});
it('should handle |x| < 1 Bytes correctly', () => {
expect(formatBytes(0.1)).toEqual({ units: ['\u00a0B'], val: ['0.1'] });
});
it('should handle x < 0 bytes correctly', () => {
expect(formatBytes(-2048)).toEqual({ units: 'KB', val: '-2' });
expect(formatBytes(-2048)).toEqual({ units: ['KB'], val: ['-2'] });
});
it('should handle large bytes correctly', () => {
expect(formatBytes(1024 ** 9)).toEqual({ units: 'YB', val: '1024' });
expect(formatBytes(1024 ** 9)).toEqual({ units: ['YB'], val: ['1024'] });
});
});

describe('dataWithUnitsToString test', () => {
it('should handle 0 duration correctly', () => {
expect(dataWithUnitsToString({ units: ['ns'], val: ['0'] })).toEqual('0 ns');
});
it('should handle |x| < 1 duration correctly', () => {
expect(dataWithUnitsToString({ units: ['ns'], val: ['0.1'] })).toEqual(
'0.1 ns');
});
it('should handle large durations correctly', () => {
expect(formatDuration(1000 ** 4)).toEqual({ units: '\u00A0s', val: '1000' });
expect(dataWithUnitsToString({ units: ['min', 's'], val: ['10', '35'] })).toEqual(
'10 min 35 s');
});
it('should handle large negative durations correctly', () => {
expect(dataWithUnitsToString({ units: ['min', 's'], val: ['-10', '35'] })).toEqual(
'-10 min 35 s');
});
it('should handle x < 0 bytes correctly', () => {
expect(dataWithUnitsToString({ units: ['KB'], val: ['-2'] })).toEqual('-2 KB');
});
it('should handle bytes correctly', () => {
expect(dataWithUnitsToString({ units: ['YB'], val: ['1024'] })).toEqual('1024 YB');
});
});

Expand Down
78 changes: 62 additions & 16 deletions src/ui/src/containers/format-data/format-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,14 @@ export const PortRenderer: React.FC<{ data: any }> = React.memo(
PortRenderer.displayName = 'PortRenderer';

export interface DataWithUnits {
val: string;
units: string;
val: string[];
units: string[];
}

export const dataWithUnitsToString = (dataWithUnits: DataWithUnits): string => (
dataWithUnits?.val.map((v, i) => `${v} ${dataWithUnits.units[i]}`).join(' ')
);

export const formatScaled = (data: number, scale: number, suffixes: string[], decimals = 2): DataWithUnits => {
const dm = decimals < 0 ? 0 : decimals;
let i = Math.floor(Math.log(Math.abs(data)) / Math.log(scale));
Expand All @@ -104,35 +108,69 @@ export const formatScaled = (data: number, scale: number, suffixes: string[], de
const units = suffixes[i];

return {
val,
units,
val: [ val ],
units: [ units ],
};
};

export const formatBytes = (data: number): DataWithUnits => (
formatScaled(data,
1024,
// \u00a0 is a space, written in order to prevent it from being stripped by React.
['\u00a0B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
1)
);

export const formatDuration = (data: number): DataWithUnits => (
formatScaled(data,
const nanosPerS = 1000 * 1000 * 1000;
const nanosPerMin = 60 * nanosPerS;
const nanosPerH = 60 * nanosPerMin;
const nanosPerD = 24 * nanosPerH;

export const formatDuration = (data: number): DataWithUnits => {
const absRounded = Math.abs(Math.round(data));
const days = Math.floor(absRounded / nanosPerD);
const hours = Math.floor((absRounded % nanosPerD) / nanosPerH);
const min = Math.floor((absRounded % nanosPerH) / nanosPerMin);
const seconds = Math.floor((absRounded % nanosPerMin) / nanosPerS);

if (days > 0) {
return {
val: [`${days * Math.sign(data)}`, `${hours}`],
units: ['days', 'hours'],
};
}
if (hours > 0) {
return {
val: [`${hours * Math.sign(data)}`, `${min}`],
units: ['hours', 'min'],
};
}
if (min > 0) {
return {
val: [`${min * Math.sign(data)}`, `${seconds}`],
units: ['min', 's'],
};
}
return formatScaled(data,
1000,
// \u00a0 is a space, which can sometimes be stripped by React if written as ' '.
// \u00b5 is μ.
['ns', '\u00b5s', 'ms', '\u00a0s'],
1)
);
1);
};

export const formatThroughput = (data: number): DataWithUnits => (
formatScaled(data * 1E9,
1000,
// \u00a0 is a space, which can sometimes be stripped by React if written as ' '.
['\u00a0/s', 'K/s', 'M/s', 'B/s'],
1)
);

export const formatThroughputBytes = (data: number): DataWithUnits => (
formatScaled(data * 1E9,
1024,
// \u00a0 is a space, which can sometimes be stripped by React if written as ' '.
['\u00a0B/s', 'KB/s', 'MB/s', 'GB/s'],
1)
);
Expand All @@ -152,10 +190,18 @@ const useRenderValueWithUnitsStyles = makeStyles(({ typography }: Theme) => crea
const RenderValueWithUnits: React.FC<{ data: DataWithUnits }> = React.memo(({ data }) => {
const classes = useRenderValueWithUnitsStyles();
return (
<>
<span className={classes.value}>{`${data.val}\u00A0`}</span>
<span className={classes.units}>{data.units}</span>
</>
<React.Fragment>
{
// \u00a0 is a space, written in order to prevent it from being stripped by React.
data.val.map((val, i) => (
<React.Fragment key={i}>
{i > 0 && <span key={'space' + i} className={classes.value}>{'\u00a0'}</span>}
<span key={'val' + i} className={classes.value}>{`${val}\u00A0`}</span>
<span key={'unit' + i} className={classes.units}>{data.units[i]}</span>
</React.Fragment>
))
}
</React.Fragment>
);
});
RenderValueWithUnits.displayName = 'RenderValueWithUnits';
Expand Down Expand Up @@ -230,8 +276,8 @@ export const formatPercent = (data: number): DataWithUnits => {
const units = '%';

return {
val,
units,
val: [val],
units: [units],
};
};

Expand Down Expand Up @@ -286,8 +332,8 @@ export const formatBySemType = (semType: SemanticType, val: any): DataWithUnits
return formatFnMd.formatFn(val);
}
return {
val,
units: '',
val: [ val ],
units: [''],
};
};

Expand Down
7 changes: 1 addition & 6 deletions src/ui/src/containers/live-data-table/renderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
AlertData,
BytesRenderer,
CPUData,
dataWithUnitsToString,
DataWithUnits,
BasicDurationRenderer,
formatDuration,
Expand Down Expand Up @@ -60,12 +61,6 @@ import { ColumnDisplayInfo, QuantilesDisplayState } from './column-display-info'

interface Quant { p50: number; p90: number; p99: number; }

// Helper to durationQuantilesRenderer since it takes in a string, rather than a span
// for p50Display et al.
function dataWithUnitsToString(dataWithUnits: DataWithUnits): string {
return `${dataWithUnits.val} ${dataWithUnits.units}`;
}

interface LiveCellProps {
data: any;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import { data as visData } from 'vis-network/standalone';

import { formatBySemType } from 'app/containers/format-data/format-data';
import { dataWithUnitsToString, formatBySemType } from 'app/containers/format-data/format-data';
import { deepLinkURLFromSemanticType, EmbedState } from 'app/containers/live-widgets/utils/live-view-params';
import { WidgetDisplay } from 'app/containers/live/vis';
import { Relation, SemanticType } from 'app/types/generated/vizierapi_pb';
Expand Down Expand Up @@ -129,7 +129,7 @@ const humanReadableMetric = (value: any, semType: SemanticType, defaultUnits: st
return `${formatFloat64Data(value)}${defaultUnits}`;
}
const valWithUnits = formatBySemType(semType, value);
return `${valWithUnits.val} ${valWithUnits.units}`;
return dataWithUnitsToString(valWithUnits);
};

const getEdgeText = (edge: EdgeStats, display: RequestGraphDisplay,
Expand Down

0 comments on commit 55931b4

Please sign in to comment.