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. ![Flame Chart](https://raw.githubusercontent.com/certinia/debug-log-analyzer/main/lana/assets/v1.18/lana-timeline.png) 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: