Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -475,6 +476,7 @@ Skipped due to adopting odd numbering for pre releases and even number for relea

<!-- Unreleased -->

[#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
Expand Down
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

![Flame Chart](https://raw.githubusercontent.com/certinia/debug-log-analyzer/main/lana/assets/v1.18/lana-timeline.png)

Expand Down
48 changes: 48 additions & 0 deletions apex-log-parser/__tests__/ApexLogParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
40 changes: 36 additions & 4 deletions apex-log-parser/src/LogEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions lana-docs-site/docs/docs/features/timeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<img
Expand All @@ -275,6 +284,7 @@ The tooltip provides the following information:
- **Event Name** - e.g., `METHOD_ENTRY`, `EXECUTION_STARTED`, `SOQL_EXECUTION_BEGIN`
- **Event Description** - Additional information about the event such as method name or SOQL query executed
- **Timestamp** - The start and end timestamp for the given event which can be cross-referenced in the log file
- **Time** - Wall-clock time of day for the event (e.g., `14:30:05.122 β†’ 14:30:06.625`). Only shown when wall-clock data is available in the log.
- **Duration** - Made up of **Total Time** (time spent in that event and its children) and **Self Time** (time directly spent in that event)
- **Rows** - Shows **Total Rows** (rows from that event and its children) and **Self Rows** (rows directly from that event)

Expand Down
2 changes: 1 addition & 1 deletion log-viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"version": "0.1.0",
"dependencies": {
"@apexdevtools/apex-parser": "^4.4.0",
"@vscode/codicons": "^0.0.36",
"@vscode/codicons": "^0.0.44",
"@vscode/webview-ui-toolkit": "^1.4.0",
"lit": "^3.3.1",
"pixi.js": "^8.5.2",
Expand Down
43 changes: 43 additions & 0 deletions log-viewer/src/core/utility/Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,49 @@ export function formatTimeRange(startTimeNs: number, endTimeNs: number): string
return `${formatDuration(startTimeNs)} β†’ ${formatDuration(endTimeNs)}`;
}

/**
* Formats milliseconds-since-midnight to `HH:MM:SS.mmm` wall-clock time string.
*
* @param ms - Milliseconds since midnight (0–86,400,000)
* @returns Formatted time string like "14:30:05.122"
*/
export function formatWallClockTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const millis = Math.round(ms % 1000);
const seconds = totalSeconds % 60;
const minutes = Math.floor(totalSeconds / 60) % 60;
const hours = Math.floor(totalSeconds / 3600) % 24;

return (
String(hours).padStart(2, '0') +
':' +
String(minutes).padStart(2, '0') +
':' +
String(seconds).padStart(2, '0') +
'.' +
String(millis).padStart(3, '0')
);
}

/**
* Computes the wall-clock time (ms since midnight) for any event given:
* - The wall-clock start time of the log (from ApexLog.startTime)
* - The nanosecond timestamp of the first event (ApexLog.timestamp)
* - The nanosecond timestamp of the target event
*
* @param startTimeMs - Wall-clock time of the first event, in ms since midnight
* @param firstTimestampNs - Nanosecond timestamp of the first event
* @param eventTimestampNs - Nanosecond timestamp of the event to compute
* @returns Wall-clock time in milliseconds since midnight
*/
export function computeWallClockMs(
startTimeMs: number,
firstTimestampNs: number,
eventTimestampNs: number,
): number {
return startTimeMs + (eventTimestampNs - firstTimestampNs) / 1_000_000;
}

export function debounce<T extends unknown[]>(callBack: (...args: T) => unknown) {
let requestId: number = 0;

Expand Down
61 changes: 61 additions & 0 deletions log-viewer/src/core/utility/__tests__/Util.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
66 changes: 66 additions & 0 deletions log-viewer/src/features/timeline/__tests__/tooltip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ============================================================================
Expand Down
Loading
Loading