Skip to content

Commit 1f231a4

Browse files
Merge pull request certinia#722 from lukecotter/chore-ms-time-display
feat: ms time display
2 parents 5cbfda3 + 9898738 commit 1f231a4

5 files changed

Lines changed: 89 additions & 71 deletions

File tree

log-viewer/src/__tests__/Util.test.ts

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@
44
import { formatDuration } from '../core/utility/Util.js';
55

66
describe('Format duration tests', () => {
7-
it('Shows µs for very small values', () => {
8-
expect(formatDuration(5)).toBe('0.01 µs');
9-
expect(formatDuration(50)).toBe('0.05 µs');
10-
expect(formatDuration(500)).toBe('0.5 µs');
11-
expect(formatDuration(1000)).toBe('1 µs');
12-
expect(formatDuration(5000)).toBe('5 µs');
13-
expect(formatDuration(9999)).toBe('10 µs');
14-
expect(formatDuration(10000)).toBe('10 µs');
7+
it('Shows ms with decimals for very small values (sub-millisecond)', () => {
8+
expect(formatDuration(5)).toBe('0 ms'); // 0.000005 ms rounds to 0
9+
expect(formatDuration(50)).toBe('0 ms'); // 0.00005 ms rounds to 0
10+
expect(formatDuration(500)).toBe('0.001 ms');
11+
expect(formatDuration(1000)).toBe('0.001 ms');
12+
expect(formatDuration(5000)).toBe('0.005 ms');
13+
expect(formatDuration(9999)).toBe('0.01 ms');
14+
expect(formatDuration(10000)).toBe('0.01 ms');
15+
expect(formatDuration(50000)).toBe('0.05 ms');
16+
expect(formatDuration(99999)).toBe('0.1 ms');
1517
});
1618

1719
it('handles ms duration', () => {
1820
expect(formatDuration(100_000)).toBe('0.1 ms');
21+
expect(formatDuration(500_000)).toBe('0.5 ms');
1922
expect(formatDuration(1_000_000)).toBe('1 ms');
2023
expect(formatDuration(1_234_567)).toBe('1.23 ms');
2124
expect(formatDuration(9_999_999)).toBe('10 ms');
@@ -41,22 +44,22 @@ describe('Format duration tests', () => {
4144
});
4245

4346
it('handles remove trailing 0 for all units types', () => {
44-
expect(formatDuration(5000)).toBe('5 µs');
47+
expect(formatDuration(5000)).toBe('0.005 ms');
4548
expect(formatDuration(100_000)).toBe('0.1 ms');
4649
expect(formatDuration(5_000_000_000)).toBe('5 s');
4750
expect(formatDuration(60_000_000_000)).toBe('1m');
4851
});
4952

50-
it('handles rounding to 2dp for µs, ms, s', () => {
51-
// microseconds
52-
expect(formatDuration(1234)).toBe('1.23 µs');
53-
expect(formatDuration(9876)).toBe('9.88 µs');
53+
it('handles rounding to appropriate precision', () => {
54+
// sub-milliseconds (up to 3 decimal places)
55+
expect(formatDuration(1234)).toBe('0.001 ms');
56+
expect(formatDuration(9876)).toBe('0.01 ms');
5457

55-
// milliseconds
58+
// milliseconds (up to 2 decimal places)
5659
expect(formatDuration(1_234_567)).toBe('1.23 ms');
5760
expect(formatDuration(9_876_543)).toBe('9.88 ms');
5861

59-
// seconds
62+
// seconds (up to 2 decimal places)
6063
expect(formatDuration(1_234_567_890)).toBe('1.23 s');
6164
expect(formatDuration(9_876_543_210)).toBe('9.88 s');
6265
});
@@ -65,4 +68,25 @@ describe('Format duration tests', () => {
6568
// minutes with fractional seconds
6669
expect(formatDuration(125_670_000_000)).toBe('2m 5.7s');
6770
});
71+
72+
describe('compact option', () => {
73+
it('omits spaces for milliseconds', () => {
74+
expect(formatDuration(0, { compact: true })).toBe('0ms');
75+
expect(formatDuration(50000, { compact: true })).toBe('0.05ms');
76+
expect(formatDuration(1_000_000, { compact: true })).toBe('1ms');
77+
expect(formatDuration(1_234_567, { compact: true })).toBe('1.23ms');
78+
expect(formatDuration(100_000_000, { compact: true })).toBe('100ms');
79+
});
80+
81+
it('omits spaces for seconds', () => {
82+
expect(formatDuration(5_000_000_000, { compact: true })).toBe('5s');
83+
expect(formatDuration(59_500_000_000, { compact: true })).toBe('59.5s');
84+
});
85+
86+
it('omits spaces for minutes', () => {
87+
expect(formatDuration(60_000_000_000, { compact: true })).toBe('1m');
88+
expect(formatDuration(125_000_000_000, { compact: true })).toBe('2m5s');
89+
expect(formatDuration(125_500_000_000, { compact: true })).toBe('2m5.5s');
90+
});
91+
});
6892
});

log-viewer/src/core/utility/Util.ts

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,58 @@
22
* Copyright (c) 2020 Certinia Inc. All rights reserved.
33
*/
44

5+
/**
6+
* Options for formatting durations.
7+
*/
8+
export interface FormatDurationOptions {
9+
/**
10+
* If true, omits spaces between values and units (e.g., "5ms" instead of "5 ms").
11+
* Useful for compact displays like the minimap axis.
12+
*/
13+
compact?: boolean;
14+
}
15+
516
/**
617
* Formats a duration in nanoseconds into a human-readable string.
718
*
8-
* Automatically selects the most appropriate unit (microseconds, milliseconds, seconds, or minutes)
19+
* Automatically selects the most appropriate unit (milliseconds, seconds, or minutes)
920
* based on the magnitude of the duration. Applies appropriate precision for each unit.
1021
*
1122
* @param ns - The duration in nanoseconds to format
23+
* @param options - Optional formatting options
1224
* @returns A formatted string representing the duration with appropriate units:
13-
* - Microseconds (µs) for durations < 0.1ms
14-
* - Milliseconds (ms) for durations < 1000ms
25+
* - Milliseconds (ms) for durations < 1000ms (with up to 3 decimal places for sub-ms)
1526
* - Seconds (s) for durations < 60s
1627
* - Minutes and seconds (e.g., "2m 30s") for durations ≥ 60s
1728
*
1829
* @example
1930
* ```typescript
20-
* formatDuration(5000); // "5 µs"
21-
* formatDuration(1500000); // "1.5 ms"
22-
* formatDuration(2500000000); // "2.5 s"
23-
* formatDuration(90000000000); // "1m 30s"
31+
* formatDuration(50000); // "0.05 ms"
32+
* formatDuration(1500000); // "1.5 ms"
33+
* formatDuration(2500000000); // "2.5 s"
34+
* formatDuration(90000000000); // "1m 30s"
35+
* formatDuration(50000, { compact: true }); // "0.05ms"
2436
* ```
2537
*/
26-
export function formatDuration(ns: number) {
38+
export function formatDuration(ns: number, options?: FormatDurationOptions): string {
39+
const space = options?.compact ? '' : ' ';
40+
2741
if (!ns) {
28-
return '0 ms';
42+
return `0${space}ms`;
2943
}
3044

3145
const ms = ns / 1e6;
3246

33-
// microseconds (< 0.01 ms)
34-
if (ms < 0.1) {
35-
const us = ns / 1e3;
36-
const precision = us < 10 ? 100 : us < 100 ? 10 : 1;
37-
return `${round(us, precision)} µs`;
38-
}
39-
4047
if (ms < 1000) {
41-
const precision = ms < 10 ? 100 : ms < 100 ? 10 : 1;
42-
return `${round(ms, precision)} ms`;
48+
// Precision: 3 decimals for sub-ms, 2 for <10ms, 1 for <100ms, 0 for >=100ms
49+
const precision = ms < 1 ? 1000 : ms < 10 ? 100 : ms < 100 ? 10 : 1;
50+
return `${round(ms, precision)}${space}ms`;
4351
}
4452

4553
const s = ms / 1000;
4654
if (s < 60) {
4755
const precision = s < 10 ? 100 : s < 100 ? 10 : 1;
48-
return `${round(s, precision)} s`;
56+
return `${round(s, precision)}${space}s`;
4957
}
5058

5159
const m = Math.floor(s / 60);
@@ -56,7 +64,7 @@ export function formatDuration(ns: number) {
5664
}
5765

5866
const secStr = sec === Math.floor(sec) ? `${sec}s` : `${round(sec, 10)}s`;
59-
return `${m}m ${secStr}`;
67+
return `${m}m${space}${secStr}`;
6068
}
6169

6270
function round(value: number, precision: number): number {

log-viewer/src/features/timeline/__tests__/tooltip.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ describe('TimelineTooltipManager', () => {
272272
const tooltip = container.querySelector('#timeline-tooltip') as HTMLElement;
273273
expect(tooltip.textContent).toContain('total');
274274
// Check for some duration value (format may vary)
275-
expect(tooltip.textContent).toContain('µs');
275+
expect(tooltip.textContent).toContain('ms');
276276

277277
tooltipManager.hide();
278278
});

log-viewer/src/features/timeline/optimised/minimap/MinimapAxisRenderer.ts

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,20 @@
2222
*/
2323

2424
import { BitmapText, Container, Graphics } from 'pixi.js';
25-
import { TEXT_LABEL_CONSTANTS } from '../../types/flamechart.types.js';
25+
26+
import { formatDuration, TEXT_LABEL_CONSTANTS } from '../../types/flamechart.types.js';
2627
import type { MinimapManager } from './MinimapManager.js';
2728

2829
/**
2930
* Nanoseconds per millisecond conversion constant.
3031
*/
3132
const NS_PER_MS = 1_000_000;
3233

34+
/**
35+
* Pre-allocated options for compact duration formatting (avoids allocation in render loop).
36+
*/
37+
const COMPACT_FORMAT = { compact: true } as const;
38+
3339
/**
3440
* Axis configuration.
3541
*/
@@ -67,6 +73,9 @@ export class MinimapAxisRenderer {
6773
private labelPool: BitmapText[] = [];
6874
private activeLabelCount = 0;
6975

76+
/** Pre-allocated stroke options (avoids allocation in render loop) */
77+
private strokeOptions: { color: number; width: number };
78+
7079
/**
7180
* Creates the axis renderer without adding to any parent container.
7281
* Caller is responsible for adding getTickGraphics() and getLabelsContainer()
@@ -82,6 +91,9 @@ export class MinimapAxisRenderer {
8291

8392
// Create container for labels
8493
this.labelsContainer = new Container();
94+
95+
// Initialize stroke options (updated in refreshColors)
96+
this.strokeOptions = { color: this.config.tickColor, width: 1 };
8597
}
8698

8799
/**
@@ -158,12 +170,12 @@ export class MinimapAxisRenderer {
158170
// Draw tick line full height of minimap
159171
this.tickGraphics.moveTo(x, axisY);
160172
this.tickGraphics.lineTo(x, minimapHeight);
161-
this.tickGraphics.stroke({ color: this.config.tickColor, width: 1 });
173+
this.tickGraphics.stroke(this.strokeOptions);
162174

163175
// Add label if needed (positioned to the left of the line)
164176
if (shouldShowLabel && timeNs > 0) {
165177
const label = this.getOrCreateLabel();
166-
label.text = this.formatTime(timeNs);
178+
label.text = formatDuration(timeNs, COMPACT_FORMAT);
167179
label.x = x - 3; // 3px to the left of line
168180
label.y = axisY + 2; // Near top
169181
label.anchor.set(1, 0); // Right-align to line
@@ -194,6 +206,7 @@ export class MinimapAxisRenderer {
194206
const tickColorStr =
195207
computedStyle.getPropertyValue('--vscode-editorLineNumber-foreground').trim() || '#808080';
196208
this.config.tickColor = this.parseColorToHex(tickColorStr);
209+
this.strokeOptions.color = this.config.tickColor;
197210

198211
// Update label tint color
199212
this.config.labelTint = this.parseColorToHex(tickColorStr);
@@ -370,36 +383,4 @@ export class MinimapAxisRenderer {
370383
label.visible = false;
371384
}
372385
}
373-
374-
// ============================================================================
375-
// PRIVATE: FORMATTING
376-
// ============================================================================
377-
378-
/**
379-
* Format time with appropriate units.
380-
* Similar to main axis but more compact for minimap.
381-
*/
382-
private formatTime(timeNs: number): string {
383-
const timeMs = timeNs / NS_PER_MS;
384-
385-
// Convert to seconds if >= 1000ms and whole seconds
386-
if (timeMs >= 1000 && timeMs % 1000 === 0) {
387-
const seconds = timeMs / 1000;
388-
return `${seconds}s`;
389-
}
390-
391-
// Format as milliseconds
392-
if (timeMs >= 1) {
393-
// Whole milliseconds: no decimals
394-
if (timeMs === Math.floor(timeMs)) {
395-
return `${Math.floor(timeMs)}ms`;
396-
}
397-
// Fractional: up to 2 decimal places
398-
return `${timeMs.toFixed(2).replace(/\.?0+$/, '')}ms`;
399-
}
400-
401-
// Sub-millisecond: show as microseconds
402-
const timeUs = timeMs * 1000;
403-
return `${Math.round(timeUs)}µs`;
404-
}
405386
}

log-viewer/src/features/timeline/types/flamechart.types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@
1212
//TODO: Remove deps outside timeline
1313

1414
import type { LogEvent, LogSubCategory } from 'apex-log-parser';
15+
import { formatDuration } from '../../../core/utility/Util.js';
1516
import type { PrecomputedRect } from '../optimised/RectangleManager.js';
1617

1718
// Re-export LogEvent for internal use within timeline/ folder
1819
// Note: FlameChart's PUBLIC API (callbacks) should use EventNode, not LogEvent
1920
// LogEvent is only used internally by data structures like RectangleManager, HitTestManager
2021
export type { LogEvent };
2122

23+
// Re-export formatDuration for use within timeline/optimised folder
24+
// This keeps the Util.ts dependency at the boundary (types file)
25+
export { formatDuration };
26+
2227
// ============================================================================
2328
// VIEWPORT STATE
2429
// ============================================================================

0 commit comments

Comments
 (0)