diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f870dbc..8082a4e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -53,6 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Marker Navigation**: Click markers to select; arrow Left/Right to navigate between markers.
- **Clear Selection**: Press `Escape` to deselect the current frame or marker.
- **β±οΈ Time Axis Auto-Spacing**: Markers intelligently and naturally auto-space as you zoom.
+ - **π Wall-Clock Time**: Toggle between elapsed time and wall-clock time (HH:MM:SS.mmm) on the time axis. Click the clock button in the timeline toolbar to switch modes. Tooltips also show the wall-clock start β end time for each event. ([#685])
- **π Search + Highlight**: Dims non-matches for fast scanning.
- **Timeline Categories**: Redesigned timeline categories for clearer, more meaningful event grouping. ([#98])
- Apex (Apex code), Automation (Workflow, NBA), Callout, Code Unit, DML, SOQL, System (System, Visualforce), Validation
@@ -475,6 +476,7 @@ Skipped due to adopting odd numbering for pre releases and even number for relea
+[#685]: https://github.com/certinia/debug-log-analyzer/issues/685
[#98]: https://github.com/certinia/debug-log-analyzer/issues/98
[#204]: https://github.com/certinia/debug-log-analyzer/issues/204
[#714]: https://github.com/certinia/debug-log-analyzer/issues/714
diff --git a/README.md b/README.md
index 45f42d21..35478c9e 100644
--- a/README.md
+++ b/README.md
@@ -87,18 +87,12 @@ Use `Log: Retrieve Apex Log And Show Analysis` from the Command Palette.
The Timeline view shows a live visualization of your Salesforce Apex log execution β including methods, SOQL queries, DML operations, workflows, flows, and more.
- **β‘ Fast** β Blazing-fast zoom, pan, and rendering even on massive logs (500k+ lines).
-- **π― Frame Selection & Navigation** β Click to select frames, use arrow keys to navigate the call stack, double-click or press `Enter` to zoom and focus.
-- **Zoom & Pan** β Navigate your logs down to 0.001 ms with precision zoom. `W`/`S` keys or scroll wheel for zoom; `A`/`D` keys or drag for pan.
-- **Dynamic Frame Labels** β Instantly see method names on timeline frames for faster scanning.
-- **πΊοΈ Minimap** β Bird's-eye view with skyline overview showing call stack depth, viewport lens for navigation, and instant teleport to any position.
-- **π Governor Limits Strip** β At-a-glance limit usage with traffic light coloring (safe/warning/critical/breach). Expand for detailed step chart.
-- **π Measure Range** β `Shift+Drag` to measure the duration between any two points. Resize edges, double-click to zoom.
-- **π Area Zoom** β `Alt/Option+Drag` to select a region and instantly zoom to fit.
-- **Tooltips** β Hover for duration, event name, SOQL/DML/Exception counts, SOQL/DML rows, and more.
-- **Cotext Menu Actions** β Right-click for context actions; `Cmd/Ctrl+Click` to jump directly to the Call Tree; `Cmd/Ctrl+C` to copy frame names.
-- **19 Curated Themes** β Choose from beautiful, optimized color themes or create your own via Settings.
-- **Adaptive Frame Detail** β Level-of-detail bucketing reveals richer detail as you zoom while keeping performance snappy.
-- **Stacked by Time** β See how execution time is distributed across nested method calls and system events.
+- **πΊοΈ Minimap** β Bird's-eye view with skyline density overview, viewport lens, and instant teleport.
+- **π Governor Limits Strip** β At-a-glance limit usage with traffic light coloring. Expand for detailed step chart.
+- **π Measure & Zoom** β `Shift+Drag` to measure durations, `Alt/Option+Drag` to area-zoom, precision keyboard controls.
+- **π Wall-Clock Time** β Toggle between elapsed and real-time (HH:MM:SS.mmm) on the time axis via the toolbar clock button.
+
+Also: Frame Selection & Navigation, Dynamic Frame Labels, Adaptive Frame Detail, Tooltips, Context Menu, Search & Highlight, 19 Curated Themes.

diff --git a/apex-log-parser/__tests__/ApexLogParser.test.ts b/apex-log-parser/__tests__/ApexLogParser.test.ts
index e244752d..7ec51f90 100644
--- a/apex-log-parser/__tests__/ApexLogParser.test.ts
+++ b/apex-log-parser/__tests__/ApexLogParser.test.ts
@@ -1480,3 +1480,51 @@ describe('Aggregating Totals', () => {
);
});
});
+
+describe('ApexLog.startTime tests', () => {
+ it('should parse startTime from first child log line', () => {
+ const logData =
+ '10:29:24.6 (6329577)|EXECUTION_STARTED\n' +
+ '10:29:24.6 (6400000)|METHOD_ENTRY|[1]|01p000000000000|MyClass.myMethod()\n' +
+ '10:29:24.7 (7000000)|METHOD_EXIT|[1]|01p000000000000|MyClass.myMethod()\n' +
+ '10:29:24.7 (7100000)|EXECUTION_FINISHED';
+
+ const log = parse(logData);
+ expect(log.startTime).toBe(37764600);
+ });
+
+ it('should set startTime to null when no children', () => {
+ const log = parse('');
+ expect(log.startTime).toBeNull();
+ });
+
+ it('should parse midnight time (00:00:00.0) as 0', () => {
+ const logData = '00:00:00.0 (100)|EXECUTION_STARTED\n' + '00:00:00.0 (200)|EXECUTION_FINISHED';
+
+ const log = parse(logData);
+ expect(log.startTime).toBe(0);
+ });
+
+ it('should parse end-of-day time (23:59:59.9)', () => {
+ const logData = '23:59:59.9 (100)|EXECUTION_STARTED\n' + '23:59:59.9 (200)|EXECUTION_FINISHED';
+
+ const log = parse(logData);
+ expect(log.startTime).toBe(86399900);
+ });
+
+ it('should parse multi-digit fraction (.12) as 120ms', () => {
+ const logData =
+ '14:30:05.12 (100)|EXECUTION_STARTED\n' + '14:30:05.12 (200)|EXECUTION_FINISHED';
+
+ const log = parse(logData);
+ expect(log.startTime).toBe(52205120);
+ });
+
+ it('should parse 3-digit fraction (.123) as 123ms', () => {
+ const logData =
+ '14:30:05.123 (100)|EXECUTION_STARTED\n' + '14:30:05.123 (200)|EXECUTION_FINISHED';
+
+ const log = parse(logData);
+ expect(log.startTime).toBe(52205123);
+ });
+});
diff --git a/apex-log-parser/src/LogEvents.ts b/apex-log-parser/src/LogEvents.ts
index a973dae6..2ded1810 100644
--- a/apex-log-parser/src/LogEvents.ts
+++ b/apex-log-parser/src/LogEvents.ts
@@ -350,6 +350,13 @@ export class ApexLog extends LogEvent {
snapshots: [],
};
+ /**
+ * The wall-clock time of the first event, in milliseconds since midnight.
+ * Parsed from the `HH:MM:SS.f` portion of the first log line.
+ * Null if no wall-clock time could be parsed.
+ */
+ startTime: number | null = null;
+
/**
* The endtime with nodes of 0 duration excluded
*/
@@ -360,10 +367,16 @@ export class ApexLog extends LogEvent {
}
setTimes() {
- this.timestamp =
- this.children.find((child) => {
- return child.timestamp;
- })?.timestamp || 0;
+ const firstChild = this.children.find((child) => {
+ return child.timestamp;
+ });
+ this.timestamp = firstChild?.timestamp || 0;
+
+ // Parse wall-clock time from the first child's log line (HH:MM:SS.f before the '(')
+ if (firstChild?.logLine) {
+ this.startTime = parseWallClockTime(firstChild.logLine);
+ }
+
// We do not just want to use the very last exitStamp because it could be CUMULATIVE_USAGE which is not really part of the code execution time but does have a later time.
let endTime;
const reverseLen = this.children.length - 1;
@@ -412,6 +425,25 @@ export function parseVfNamespace(text: string): string {
return text.substring(secondSlash + 1, sep);
}
+/**
+ * Parses the wall-clock time from a log line's timestamp portion.
+ * Log lines start with `HH:MM:SS.f (nanoseconds)|...`
+ * Returns milliseconds since midnight, or null if parsing fails.
+ */
+function parseWallClockTime(logLine: string): number | null {
+ const match = /^(\d{1,2}):(\d{2}):(\d{2})\.(\d+)\s/.exec(logLine);
+ if (!match) {
+ return null;
+ }
+
+ const hours = Number(match[1]);
+ const minutes = Number(match[2]);
+ const seconds = Number(match[3]);
+ const fraction = Number(match[4]!.padEnd(3, '0'));
+
+ return (hours * 3600 + minutes * 60 + seconds) * 1000 + fraction;
+}
+
export function parseRows(text: string | null | undefined): number {
if (!text) {
return 0;
diff --git a/lana-docs-site/docs/docs/features/timeline.md b/lana-docs-site/docs/docs/features/timeline.md
index 43b0784d..8caeb752 100644
--- a/lana-docs-site/docs/docs/features/timeline.md
+++ b/lana-docs-site/docs/docs/features/timeline.md
@@ -258,6 +258,15 @@ Use `Alt/Option+Drag` to select a time range and instantly zoom to fit it. This
Release the mouse button to zoom the viewport to fit the selected area exactly.
+## Wall-Clock Time
+
+The timeline supports switching between two time display modes:
+
+- **Elapsed Time** (default): Time relative to the start of the log (e.g., `0 ms`, `1500 ms`, `2.5 s`)
+- **Wall-Clock Time**: Actual time of day from the log (e.g., `14:30:05.122`)
+
+Click the clock icon button in the top-right corner of the timeline toolbar to toggle between modes.
+
## Tooltip
(callBack: (...args: T) => unknown) {
let requestId: number = 0;
diff --git a/log-viewer/src/core/utility/__tests__/Util.test.ts b/log-viewer/src/core/utility/__tests__/Util.test.ts
new file mode 100644
index 00000000..0e195f97
--- /dev/null
+++ b/log-viewer/src/core/utility/__tests__/Util.test.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2020 Certinia Inc. All rights reserved.
+ */
+
+import { computeWallClockMs, formatWallClockTime } from '../Util.js';
+
+describe('formatWallClockTime', () => {
+ it('should format midnight as 00:00:00.000', () => {
+ expect(formatWallClockTime(0)).toBe('00:00:00.000');
+ });
+
+ it('should format a mid-day time', () => {
+ // 14:30:05.122 = (14*3600 + 30*60 + 5) * 1000 + 122 = 52205122
+ expect(formatWallClockTime(52205122)).toBe('14:30:05.122');
+ });
+
+ it('should format end-of-day time', () => {
+ // 23:59:59.999
+ expect(formatWallClockTime(86399999)).toBe('23:59:59.999');
+ });
+
+ it('should pad single-digit hours, minutes, seconds', () => {
+ // 01:02:03.004
+ expect(formatWallClockTime(3723004)).toBe('01:02:03.004');
+ });
+
+ it('should handle exact seconds (no fractional ms)', () => {
+ // 10:00:00.000
+ expect(formatWallClockTime(36000000)).toBe('10:00:00.000');
+ });
+
+ it('should handle sub-millisecond precision by rounding', () => {
+ // 1000.5 ms β rounds to 1001 ms fraction β 00:00:01.001
+ expect(formatWallClockTime(1000.5)).toBe('00:00:01.001');
+ });
+});
+
+describe('computeWallClockMs', () => {
+ it('should return startTime when event is the first event', () => {
+ const result = computeWallClockMs(37764600, 6329577, 6329577);
+ expect(result).toBe(37764600);
+ });
+
+ it('should compute wall-clock for a later event', () => {
+ // Event is 1ms (1,000,000 ns) after first event
+ const result = computeWallClockMs(37764600, 6329577, 7329577);
+ expect(result).toBe(37764601);
+ });
+
+ it('should compute wall-clock for an event 1 second later', () => {
+ // 1 second = 1,000,000,000 ns
+ const result = computeWallClockMs(37764600, 6329577, 1006329577);
+ expect(result).toBe(37765600);
+ });
+
+ it('should handle fractional nanosecond differences', () => {
+ // 500,000 ns = 0.5 ms
+ const result = computeWallClockMs(0, 0, 500000);
+ expect(result).toBe(0.5);
+ });
+});
diff --git a/log-viewer/src/features/timeline/__tests__/tooltip.test.ts b/log-viewer/src/features/timeline/__tests__/tooltip.test.ts
index d5d8c926..51427ba6 100644
--- a/log-viewer/src/features/timeline/__tests__/tooltip.test.ts
+++ b/log-viewer/src/features/timeline/__tests__/tooltip.test.ts
@@ -316,6 +316,72 @@ describe('TimelineTooltipManager', () => {
tooltipManager.hide();
});
+
+ it('should display wall-clock time row when apexLog has startTime', () => {
+ tooltipManager.destroy();
+ const mockApexLog = {
+ startTime: 37764600, // 10:29:24.600
+ timestamp: 6329577, // first event nanosecond offset
+ governorLimits: {
+ dmlStatements: { limit: 150 },
+ dmlRows: { limit: 10000 },
+ soqlQueries: { limit: 100 },
+ queryRows: { limit: 50000 },
+ soslQueries: { limit: 20 },
+ },
+ };
+
+ tooltipManager = new TimelineTooltipManager(container, {
+ categoryColors: {},
+ cursorOffset: 10,
+ enableFlip: true,
+ apexLog: mockApexLog as never,
+ });
+
+ // Event at timestamp 6329577ns with duration 1,000,000ns
+ const event = createEvent(6329577, 1_000_000);
+
+ tooltipManager.show(event, 100, 100);
+
+ const tooltip = container.querySelector('#timeline-tooltip') as HTMLElement;
+ expect(tooltip.textContent).toContain('time:');
+ expect(tooltip.textContent).toContain('10:29:24.600');
+ // End time should be ~1ms later
+ expect(tooltip.textContent).toContain('10:29:24.601');
+ expect(tooltip.textContent).toContain('β');
+
+ tooltipManager.hide();
+ });
+
+ it('should not display wall-clock time row when apexLog has no startTime', () => {
+ tooltipManager.destroy();
+ tooltipManager = new TimelineTooltipManager(container, {
+ categoryColors: {},
+ cursorOffset: 10,
+ enableFlip: true,
+ apexLog: { startTime: null, timestamp: 0 } as never,
+ });
+
+ const event = createEvent(0, 1_000_000);
+
+ tooltipManager.show(event, 100, 100);
+
+ const tooltip = container.querySelector('#timeline-tooltip') as HTMLElement;
+ expect(tooltip.textContent).not.toContain('time:');
+
+ tooltipManager.hide();
+ });
+
+ it('should not display wall-clock time row when no apexLog', () => {
+ const event = createEvent(0, 1_000_000);
+
+ tooltipManager.show(event, 100, 100);
+
+ const tooltip = container.querySelector('#timeline-tooltip') as HTMLElement;
+ expect(tooltip.textContent).not.toContain('time:');
+
+ tooltipManager.hide();
+ });
});
describe('positioning - basic', () => {
diff --git a/log-viewer/src/features/timeline/components/TimelineFlameChart.ts b/log-viewer/src/features/timeline/components/TimelineFlameChart.ts
index c8a3a2fe..33cbea7f 100644
--- a/log-viewer/src/features/timeline/components/TimelineFlameChart.ts
+++ b/log-viewer/src/features/timeline/components/TimelineFlameChart.ts
@@ -159,6 +159,13 @@ export class TimelineFlameChart extends LitElement {
}
}
+ /**
+ * Set the time display mode on the axis (called by parent TimelineView).
+ */
+ public setTimeDisplayMode(mode: 'elapsed' | 'wallClock'): void {
+ this.apexLogTimeline?.setTimeDisplayMode(mode);
+ }
+
// ============================================================================
// CLEANUP
// ============================================================================
diff --git a/log-viewer/src/features/timeline/components/TimelineView.ts b/log-viewer/src/features/timeline/components/TimelineView.ts
index 9f3c8c5b..3878e650 100644
--- a/log-viewer/src/features/timeline/components/TimelineView.ts
+++ b/log-viewer/src/features/timeline/components/TimelineView.ts
@@ -1,8 +1,8 @@
/*
* Copyright (c) 2023 Certinia Inc. All rights reserved.
*/
-import { LitElement, css, html } from 'lit';
-import { customElement, property, state } from 'lit/decorators.js';
+import { LitElement, css, html, unsafeCSS } from 'lit';
+import { customElement, property, query, state } from 'lit/decorators.js';
import type { ApexLog } from 'apex-log-parser';
import { VSCodeExtensionMessenger } from '../../../core/messaging/VSCodeExtensionMessenger.js';
@@ -12,7 +12,11 @@ import { type TimelineGroup, keyMap, setColors } from '../services/Timeline.js';
import { DEFAULT_THEME_NAME, type TimelineColors } from '../themes/Themes.js';
import { addCustomThemes, getTheme } from '../themes/ThemeSelector.js';
+import type { TimeDisplayMode } from '../types/flamechart.types.js';
+import type { TimelineFlameChart } from './TimelineFlameChart.js';
+
// styles
+import codiconStyles from '../../../styles/codicon.css';
import { globalStyles } from '../../../styles/global.styles.js';
// web components
@@ -53,14 +57,23 @@ export class TimelineView extends LitElement {
@state()
private useLegacyTimeline: boolean | null = null;
+ @state()
+ private timeDisplayMode: TimeDisplayMode = 'elapsed';
+
+ @query('timeline-flame-chart')
+ private flameChartRef!: TimelineFlameChart;
+
constructor() {
super();
}
static styles = [
globalStyles,
+ unsafeCSS(codiconStyles),
css`
:host {
+ --button-icon-hover-background: var(--vscode-toolbar-hoverBackground);
+
display: flex;
flex-direction: column;
flex: 1;
@@ -68,6 +81,19 @@ export class TimelineView extends LitElement {
width: 100%;
height: 90%;
}
+
+ .timeline-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 4px;
+ flex: 0 0 auto;
+ }
+
+ vscode-button {
+ height: 22px;
+ width: 22px;
+ }
`,
];
@@ -102,7 +128,24 @@ export class TimelineView extends LitElement {
}
if (!this.useLegacyTimeline) {
- return html`
+ this.toggleTimeDisplay()}
+ >
+
+
+ `
+ : ''}
+ `;
}
+ private toggleTimeDisplay(): void {
+ this.timeDisplayMode = this.timeDisplayMode === 'elapsed' ? 'wallClock' : 'elapsed';
+ this.flameChartRef?.setTimeDisplayMode(this.timeDisplayMode);
+ }
+
private setTheme(themeName: string) {
this.activeTheme = themeName ?? DEFAULT_THEME_NAME;
this.timelineKeys = this.toTimelineKeys(getTheme(themeName));
diff --git a/log-viewer/src/features/timeline/optimised/ApexLogTimeline.ts b/log-viewer/src/features/timeline/optimised/ApexLogTimeline.ts
index e476b063..11efd946 100644
--- a/log-viewer/src/features/timeline/optimised/ApexLogTimeline.ts
+++ b/log-viewer/src/features/timeline/optimised/ApexLogTimeline.ts
@@ -262,6 +262,20 @@ export class ApexLogTimeline {
this.flamechart.requestRender();
}
+ /**
+ * Set time display mode (elapsed vs wall-clock) for axis labels.
+ * Only has effect when apexLog has a valid startTime.
+ */
+ public setTimeDisplayMode(mode: 'elapsed' | 'wallClock'): void {
+ if (!this.apexLog) {
+ return;
+ }
+
+ const startTime = this.apexLog.startTime ?? 0;
+ const firstTimestamp = this.apexLog.timestamp;
+ this.flamechart.setTimeDisplayMode(mode, startTime, firstTimestamp);
+ }
+
/**
* Clean up resources.
*/
diff --git a/log-viewer/src/features/timeline/optimised/FlameChart.ts b/log-viewer/src/features/timeline/optimised/FlameChart.ts
index d120e866..584679f0 100644
--- a/log-viewer/src/features/timeline/optimised/FlameChart.ts
+++ b/log-viewer/src/features/timeline/optimised/FlameChart.ts
@@ -813,6 +813,27 @@ export class FlameChart {
this.updateMetricStripVisibility();
}
+ /**
+ * Set the time display mode for the axis (elapsed vs wall-clock).
+ * In wall-clock mode, axis labels show HH:MM:SS.mmm instead of elapsed ms.
+ */
+ public setTimeDisplayMode(
+ mode: 'elapsed' | 'wallClock',
+ startTimeMs: number,
+ firstTimestampNs: number,
+ ): void {
+ if (this.axisRenderer instanceof MeshAxisRenderer) {
+ this.axisRenderer.setTimeDisplayMode(mode, startTimeMs, firstTimestampNs);
+
+ if (!this.state) {
+ return;
+ }
+ this.state.needsRender = true;
+ this.state.renderDirty.background = true;
+ this.scheduleRender();
+ }
+ }
+
/**
* Update metric strip visibility based on whether there's data to display.
* Hides the metric strip container and gap if no governor limit data exists.
diff --git a/log-viewer/src/features/timeline/optimised/TimelineTooltipManager.ts b/log-viewer/src/features/timeline/optimised/TimelineTooltipManager.ts
index ba68e175..ca29456d 100644
--- a/log-viewer/src/features/timeline/optimised/TimelineTooltipManager.ts
+++ b/log-viewer/src/features/timeline/optimised/TimelineTooltipManager.ts
@@ -10,7 +10,11 @@
*/
import type { ApexLog, LogEvent } from 'apex-log-parser';
-import { formatDuration } from '../../../core/utility/Util.js';
+import {
+ computeWallClockMs,
+ formatDuration,
+ formatWallClockTime,
+} from '../../../core/utility/Util.js';
import type { TimelineMarker } from '../types/flamechart.types.js';
/**
@@ -276,6 +280,26 @@ export class TimelineTooltipManager {
rows.push({ label: 'total:', value: val });
}
+ // Wall-clock time row (only if startTime is available)
+ const apexLog = this.options.apexLog;
+ if (apexLog?.startTime !== null && apexLog?.timestamp !== undefined) {
+ const startWallClock = computeWallClockMs(
+ apexLog.startTime,
+ apexLog.timestamp,
+ event.timestamp,
+ );
+ let timeVal = formatWallClockTime(startWallClock);
+ if (event.exitStamp) {
+ const endWallClock = computeWallClockMs(
+ apexLog.startTime,
+ apexLog.timestamp,
+ event.exitStamp,
+ );
+ timeVal += ` β ${formatWallClockTime(endWallClock)}`;
+ }
+ rows.push({ label: 'time:', value: timeVal });
+ }
+
const govLimits = this.options.apexLog?.governorLimits;
if (event.dmlCount.total) {
rows.push({
diff --git a/log-viewer/src/features/timeline/optimised/time-axis/MeshAxisRenderer.ts b/log-viewer/src/features/timeline/optimised/time-axis/MeshAxisRenderer.ts
index a563ac51..d9fa2b79 100644
--- a/log-viewer/src/features/timeline/optimised/time-axis/MeshAxisRenderer.ts
+++ b/log-viewer/src/features/timeline/optimised/time-axis/MeshAxisRenderer.ts
@@ -22,6 +22,7 @@
*/
import { Container, Geometry, Mesh, Shader, Text } from 'pixi.js';
+import { formatWallClockTime } from '../../../../core/utility/Util.js';
import type { ViewportState } from '../../types/flamechart.types.js';
import { RectangleGeometry, type ViewportTransform } from '../RectangleGeometry.js';
import { createRectangleShader } from '../RectangleShader.js';
@@ -73,6 +74,13 @@ export class MeshAxisRenderer {
/** Grid line color */
private gridLineColor: number;
+ /** Time display mode: 'elapsed' (default) or 'wallClock' */
+ private displayMode: 'elapsed' | 'wallClock' = 'elapsed';
+ /** Wall-clock time of the first event in ms since midnight (for wallClock mode) */
+ private startTimeMs = 0;
+ /** Nanosecond timestamp of the first event (for wallClock mode) */
+ private firstTimestampNs = 0;
+
constructor(container: Container, config?: Partial) {
this.parentContainer = container;
@@ -177,6 +185,20 @@ export class MeshAxisRenderer {
}
}
+ /**
+ * Set the time display mode for axis labels.
+ * In 'wallClock' mode, labels show wall-clock time (HH:MM:SS.mmm) instead of elapsed time.
+ */
+ public setTimeDisplayMode(
+ mode: 'elapsed' | 'wallClock',
+ startTimeMs: number,
+ firstTimestampNs: number,
+ ): void {
+ this.displayMode = mode;
+ this.startTimeMs = startTimeMs;
+ this.firstTimestampNs = firstTimestampNs;
+ }
+
/**
* Apply alpha to a color by pre-multiplying into ABGR format for the shader.
* The shader expects colors in ABGR format with alpha in the high byte.
@@ -382,6 +404,9 @@ export class MeshAxisRenderer {
let rectIndex = 0;
+ const isWallClockDisplay = this.displayMode === 'wallClock';
+ const isIntervalMsOrMore = tickInterval.interval >= NS_PER_MS;
+
// Render all ticks in range
for (let i = firstTickIndex; i <= lastTickIndex; i++) {
const time = i * tickInterval.interval;
@@ -415,7 +440,10 @@ export class MeshAxisRenderer {
// Add label at top if requested (only when showLabels is enabled)
if (showLabels && shouldShowLabel && this.screenSpaceContainer) {
const timeMs = time / NS_PER_MS;
- const labelText = this.formatMilliseconds(timeMs);
+ const useWallClock = isWallClockDisplay && (isIntervalMsOrMore || time % NS_PER_MS === 0);
+ const labelText = useWallClock
+ ? formatWallClockTime(this.startTimeMs + (time - this.firstTimestampNs) / NS_PER_MS)
+ : this.formatMilliseconds(timeMs);
// Only show label if not empty (skip zero)
if (labelText) {
diff --git a/log-viewer/src/features/timeline/types/flamechart.types.ts b/log-viewer/src/features/timeline/types/flamechart.types.ts
index 61a9a0fe..cfce2f74 100644
--- a/log-viewer/src/features/timeline/types/flamechart.types.ts
+++ b/log-viewer/src/features/timeline/types/flamechart.types.ts
@@ -532,6 +532,11 @@ export class TimelineError extends Error {
*/
export type MarkerType = 'error' | 'skip' | 'unexpected';
+/**
+ * Controls whether the timeline axis shows elapsed time or wall-clock time.
+ */
+export type TimeDisplayMode = 'elapsed' | 'wallClock';
+
/**
* Represents a time range in the log where Marker occurred.
* Extracted from ApexLog.logIssues during timeline initialization.
diff --git a/log-viewer/src/styles/codicon.css b/log-viewer/src/styles/codicon.css
index b36799a7..960679d0 100644
--- a/log-viewer/src/styles/codicon.css
+++ b/log-viewer/src/styles/codicon.css
@@ -112,15 +112,6 @@
.codicon-person-filled:before {
content: '\ea67';
}
-.codicon-git-branch:before {
- content: '\ea68';
-}
-.codicon-git-branch-create:before {
- content: '\ea68';
-}
-.codicon-git-branch-delete:before {
- content: '\ea68';
-}
.codicon-source-control:before {
content: '\ea68';
}
@@ -265,9 +256,6 @@
.codicon-file:before {
content: '\ea7b';
}
-.codicon-file-text:before {
- content: '\ea7b';
-}
.codicon-more:before {
content: '\ea7c';
}
@@ -811,6 +799,9 @@
.codicon-json:before {
content: '\eb0f';
}
+.codicon-bracket:before {
+ content: '\eb0f';
+}
.codicon-kebab-vertical:before {
content: '\eb10';
}
@@ -1489,9 +1480,6 @@
.codicon-pie-chart:before {
content: '\ebe4';
}
-.codicon-bracket:before {
- content: '\eb0f';
-}
.codicon-bracket-dot:before {
content: '\ebe5';
}
@@ -1753,6 +1741,225 @@
.codicon-sort-percentage:before {
content: '\ec33';
}
+.codicon-attach:before {
+ content: '\ec34';
+}
+.codicon-go-to-editing-session:before {
+ content: '\ec35';
+}
+.codicon-edit-session:before {
+ content: '\ec36';
+}
+.codicon-code-review:before {
+ content: '\ec37';
+}
+.codicon-copilot-warning:before {
+ content: '\ec38';
+}
+.codicon-python:before {
+ content: '\ec39';
+}
+.codicon-copilot-large:before {
+ content: '\ec3a';
+}
+.codicon-copilot-warning-large:before {
+ content: '\ec3b';
+}
+.codicon-keyboard-tab:before {
+ content: '\ec3c';
+}
+.codicon-copilot-blocked:before {
+ content: '\ec3d';
+}
+.codicon-copilot-not-connected:before {
+ content: '\ec3e';
+}
+.codicon-flag:before {
+ content: '\ec3f';
+}
+.codicon-lightbulb-empty:before {
+ content: '\ec40';
+}
+.codicon-symbol-method-arrow:before {
+ content: '\ec41';
+}
+.codicon-copilot-unavailable:before {
+ content: '\ec42';
+}
+.codicon-repo-pinned:before {
+ content: '\ec43';
+}
+.codicon-keyboard-tab-above:before {
+ content: '\ec44';
+}
+.codicon-keyboard-tab-below:before {
+ content: '\ec45';
+}
+.codicon-git-pull-request-done:before {
+ content: '\ec46';
+}
+.codicon-mcp:before {
+ content: '\ec47';
+}
+.codicon-extensions-large:before {
+ content: '\ec48';
+}
+.codicon-layout-panel-dock:before {
+ content: '\ec49';
+}
+.codicon-layout-sidebar-left-dock:before {
+ content: '\ec4a';
+}
+.codicon-layout-sidebar-right-dock:before {
+ content: '\ec4b';
+}
+.codicon-copilot-in-progress:before {
+ content: '\ec4c';
+}
+.codicon-copilot-error:before {
+ content: '\ec4d';
+}
+.codicon-copilot-success:before {
+ content: '\ec4e';
+}
+.codicon-chat-sparkle:before {
+ content: '\ec4f';
+}
+.codicon-search-sparkle:before {
+ content: '\ec50';
+}
+.codicon-edit-sparkle:before {
+ content: '\ec51';
+}
+.codicon-copilot-snooze:before {
+ content: '\ec52';
+}
+.codicon-send-to-remote-agent:before {
+ content: '\ec53';
+}
+.codicon-comment-discussion-sparkle:before {
+ content: '\ec54';
+}
+.codicon-chat-sparkle-warning:before {
+ content: '\ec55';
+}
+.codicon-chat-sparkle-error:before {
+ content: '\ec56';
+}
+.codicon-collection:before {
+ content: '\ec57';
+}
+.codicon-new-collection:before {
+ content: '\ec58';
+}
+.codicon-thinking:before {
+ content: '\ec59';
+}
+.codicon-build:before {
+ content: '\ec5a';
+}
+.codicon-comment-discussion-quote:before {
+ content: '\ec5b';
+}
+.codicon-cursor:before {
+ content: '\ec5c';
+}
+.codicon-eraser:before {
+ content: '\ec5d';
+}
+.codicon-file-text:before {
+ content: '\ec5e';
+}
+.codicon-quotes:before {
+ content: '\ec60';
+}
+.codicon-rename:before {
+ content: '\ec61';
+}
+.codicon-run-with-deps:before {
+ content: '\ec62';
+}
+.codicon-debug-connected:before {
+ content: '\ec63';
+}
+.codicon-strikethrough:before {
+ content: '\ec64';
+}
+.codicon-open-in-product:before {
+ content: '\ec65';
+}
+.codicon-index-zero:before {
+ content: '\ec66';
+}
+.codicon-agent:before {
+ content: '\ec67';
+}
+.codicon-edit-code:before {
+ content: '\ec68';
+}
+.codicon-repo-selected:before {
+ content: '\ec69';
+}
+.codicon-skip:before {
+ content: '\ec6a';
+}
+.codicon-merge-into:before {
+ content: '\ec6b';
+}
+.codicon-git-branch-changes:before {
+ content: '\ec6c';
+}
+.codicon-git-branch-staged-changes:before {
+ content: '\ec6d';
+}
+.codicon-git-branch-conflicts:before {
+ content: '\ec6e';
+}
+.codicon-git-branch:before {
+ content: '\ec6f';
+}
+.codicon-git-branch-create:before {
+ content: '\ec6f';
+}
+.codicon-git-branch-delete:before {
+ content: '\ec6f';
+}
+.codicon-search-large:before {
+ content: '\ec70';
+}
+.codicon-terminal-git-bash:before {
+ content: '\ec71';
+}
+.codicon-window-active:before {
+ content: '\ec72';
+}
+.codicon-forward:before {
+ content: '\ec73';
+}
+.codicon-download:before {
+ content: '\ec74';
+}
+.codicon-clockface:before {
+ content: '\ec75';
+}
+.codicon-unarchive:before {
+ content: '\ec76';
+}
+.codicon-session-in-progress:before {
+ content: '\ec77';
+}
+.codicon-collection-small:before {
+ content: '\ec78';
+}
+.codicon-vm-small:before {
+ content: '\ec79';
+}
+.codicon-cloud-small:before {
+ content: '\ec7a';
+}
.codicon-git-fetch:before {
content: '\f101';
}
+.codicon-vm-pending:before {
+ content: '\f102';
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 676b4661..74ef1221 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -179,8 +179,8 @@ importers:
specifier: ^4.4.0
version: 4.4.1
'@vscode/codicons':
- specifier: ^0.0.36
- version: 0.0.36
+ specifier: ^0.0.44
+ version: 0.0.44
'@vscode/webview-ui-toolkit':
specifier: ^1.4.0
version: 1.4.0(react@19.1.1)
@@ -2919,8 +2919,8 @@ packages:
cpu: [x64]
os: [win32]
- '@vscode/codicons@0.0.36':
- resolution: {integrity: sha512-wsNOvNMMJ2BY8rC2N2MNBG7yOowV3ov8KlvUE/AiVUlHKTfWsw3OgAOQduX7h0Un6GssKD3aoTVH+TF3DSQwKQ==}
+ '@vscode/codicons@0.0.44':
+ resolution: {integrity: sha512-F7qPRumUK3EHjNdopfICLGRf3iNPoZQt+McTHAn4AlOWPB3W2kL4H0S7uqEqbyZ6rCxaeDjpAn3MCUnwTu/VJQ==}
'@vscode/webview-ui-toolkit@1.4.0':
resolution: {integrity: sha512-modXVHQkZLsxgmd5yoP3ptRC/G8NBDD+ob+ngPiWNQdlrH6H1xR/qgOBD85bfU3BhOB5sZzFWBwwhp9/SfoHww==}
@@ -11613,7 +11613,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
- '@vscode/codicons@0.0.36': {}
+ '@vscode/codicons@0.0.44': {}
'@vscode/webview-ui-toolkit@1.4.0(react@19.1.1)':
dependencies: