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
11 changes: 9 additions & 2 deletions log-viewer/src/features/timeline/__tests__/batching.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@

import type { LogCategory, LogEvent } from 'apex-log-parser';
import * as PIXI from 'pixi.js';
import type { BatchColorInfo } from '../optimised/BucketColorResolver.js';
import { EventBatchRenderer } from '../optimised/EventBatchRenderer.js';
import { RectangleCache } from '../optimised/RectangleCache.js';
import type { RenderBatch, ViewportState } from '../types/flamechart.types.js';
import { TIMELINE_CONSTANTS } from '../types/flamechart.types.js';

/** Empty batch colors β€” tests that don't assert color values use this. */
const EMPTY_BATCH_COLORS: Map<string, BatchColorInfo> = new Map();

describe('EventBatchRenderer', () => {
let container: PIXI.Container;
let renderer: EventBatchRenderer;
Expand Down Expand Up @@ -75,7 +79,10 @@ describe('EventBatchRenderer', () => {
rectangleManager = new RectangleCache(events, categories);
renderer = new EventBatchRenderer(container, batches);

const { visibleRects, buckets } = rectangleManager.getCulledRectangles(viewport);
const { visibleRects, buckets } = rectangleManager.getCulledRectangles(
viewport,
EMPTY_BATCH_COLORS,
);
renderer.render(visibleRects, buckets);
}

Expand Down Expand Up @@ -455,7 +462,7 @@ describe('EventBatchRenderer', () => {
// Second render with different viewport (should recalculate)
const viewport2 = createViewport(1, 150, 0); // Pan to cull first event
const { visibleRects: visibleRects2, buckets: buckets2 } =
rectangleManager.getCulledRectangles(viewport2);
rectangleManager.getCulledRectangles(viewport2, EMPTY_BATCH_COLORS);
renderer.render(visibleRects2, buckets2);
expect(batches.get('Apex')?.rectangles).toHaveLength(1);
});
Expand Down
135 changes: 3 additions & 132 deletions log-viewer/src/features/timeline/optimised/BucketColorResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,6 @@
import type { CategoryStats } from '../types/flamechart.types.js';
import { BUCKET_CONSTANTS } from '../types/flamechart.types.js';

/**
* Map category names to their hex colors (numeric format).
* Colors match TIMELINE_CONSTANTS.DEFAULT_COLORS but in numeric format.
*
* This is the single source of truth for category colors in numeric format.
* All timeline code should use this via import or batchColors from theme.
*/
export const CATEGORY_COLORS: Record<string, number> = {
Apex: 0x2b8f81, // #2B8F81
'Code Unit': 0x88ae58, // #88AE58
System: 0x8d6e63, // #8D6E63
Automation: 0x51a16e, // #51A16E
DML: 0xb06868, // #B06868
SOQL: 0x6d4c7d, // #6D4C7D
Callout: 0xcca033, // #CCA033
Validation: 0x5c8fa6, // #5C8FA6
};

/**
* Default gray color for unknown categories.
*/
Expand Down Expand Up @@ -72,12 +54,12 @@ export interface BatchColorInfo {
* 4. Tie-break by event count (higher count wins)
*
* @param categoryStats - Statistics for all categories in the bucket
* @param batchColors - Optional colors from RenderBatch (for theme support)
* @param batchColors - Colors from RenderBatch (theme-aware category colors)
* @returns Color and dominant category
*/
export function resolveColor(
categoryStats: CategoryStats,
batchColors?: Map<string, BatchColorInfo>,
batchColors: Map<string, BatchColorInfo>,
): ColorResolutionResult {
const { byCategory } = categoryStats;

Expand Down Expand Up @@ -119,121 +101,10 @@ export function resolveColor(
}
}

// Get color for winning category (prefer batch colors for theme support)
const color =
batchColors?.get(winningCategory)?.color ??
CATEGORY_COLORS[winningCategory] ??
UNKNOWN_CATEGORY_COLOR;
const color = batchColors.get(winningCategory)?.color ?? UNKNOWN_CATEGORY_COLOR;

return {
color,
dominantCategory: winningCategory,
};
}

// ============================================================================
// COLOR BLENDING UTILITIES
// ============================================================================

/**
* Default dark theme background color for alpha blending.
* This is the standard VS Code dark theme background.
*/
const DEFAULT_BACKGROUND_COLOR = 0x1e1e1e;

/**
* Blend a color with a background color based on opacity.
* Returns an opaque color that simulates the visual appearance of
* the original color at the given opacity over the background.
*
* Formula: result = foreground * alpha + background * (1 - alpha)
*
* @param foregroundColor - The foreground color (0xRRGGBB)
* @param opacity - Opacity value (0 to 1)
* @param backgroundColor - Background color to blend against (default: dark theme background)
* @returns Opaque blended color (0xRRGGBB)
*/
export function blendWithBackground(
foregroundColor: number,
opacity: number,
backgroundColor: number = DEFAULT_BACKGROUND_COLOR,
): number {
// Extract RGB components from foreground
const fgR = (foregroundColor >> 16) & 0xff;
const fgG = (foregroundColor >> 8) & 0xff;
const fgB = foregroundColor & 0xff;

// Extract RGB components from background
const bgR = (backgroundColor >> 16) & 0xff;
const bgG = (backgroundColor >> 8) & 0xff;
const bgB = backgroundColor & 0xff;

// Blend each channel: result = fg * alpha + bg * (1 - alpha)
const invAlpha = 1 - opacity;
const resultR = Math.round(fgR * opacity + bgR * invAlpha);
const resultG = Math.round(fgG * opacity + bgG * invAlpha);
const resultB = Math.round(fgB * opacity + bgB * invAlpha);

// Combine back to a single color value
return (resultR << 16) | (resultG << 8) | resultB;
}

/**
* Parse CSS color string to PixiJS numeric color (opaque).
* If the color has alpha < 1, it will be pre-blended with the background
* to produce an opaque result for better GPU performance.
*
* Supported formats:
* - #RGB (3 hex digits)
* - #RGBA (4 hex digits)
* - #RRGGBB (6 hex digits)
* - #RRGGBBAA (8 hex digits)
* - rgb(r, g, b)
* - rgba(r, g, b, a)
*
* @param cssColor - CSS color string
* @returns Opaque PixiJS numeric color (0xRRGGBB)
*/
export function cssColorToPixi(cssColor: string): number {
let color = 0x000000;
let alpha = 1;

if (cssColor.startsWith('#')) {
const hex = cssColor.slice(1);
if (hex.length === 8) {
const rgb = hex.slice(0, 6);
alpha = parseInt(hex.slice(6, 8), 16) / 255;
color = parseInt(rgb, 16);
} else if (hex.length === 6) {
color = parseInt(hex, 16);
} else if (hex.length === 4) {
const r = hex[0]!;
const g = hex[1]!;
const b = hex[2]!;
const a = hex[3]!;
color = parseInt(r + r + g + g + b + b, 16);
alpha = parseInt(a + a, 16) / 255;
} else if (hex.length === 3) {
const r = hex[0]!;
const g = hex[1]!;
const b = hex[2]!;
color = parseInt(r + r + g + g + b + b, 16);
}
} else {
const rgbMatch = cssColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d*(?:\.\d+)?))?\)/);
if (rgbMatch) {
const r = parseInt(rgbMatch[1] ?? '0', 10);
const g = parseInt(rgbMatch[2] ?? '0', 10);
const b = parseInt(rgbMatch[3] ?? '0', 10);
alpha = rgbMatch[4] ? parseFloat(rgbMatch[4]) : 1;
color = (r << 16) | (g << 8) | b;
}
}

// Pre-blend with background if color has alpha < 1
if (alpha < 1) {
return blendWithBackground(color, alpha);
}

return color;
}
2 changes: 1 addition & 1 deletion log-viewer/src/features/timeline/optimised/FlameChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import { MeshAxisRenderer } from './time-axis/MeshAxisRenderer.js';

import { TextLabelRenderer } from './TextLabelRenderer.js';

import { cssColorToPixi } from './BucketColorResolver.js';
import { HitDetector } from './interaction/HitDetector.js';
import {
KEYBOARD_CONSTANTS,
Expand All @@ -44,6 +43,7 @@ import { TimelineInteractionHandler } from './interaction/TimelineInteractionHan
import { TimelineResizeHandler } from './interaction/TimelineResizeHandler.js';
import type { MeasurementSnapshot } from './measurement/MeasurementState.js';
import { RectangleCache, type PrecomputedRect } from './RectangleCache.js';
import { cssColorToPixi } from './rendering/ColorUtils.js';
import { CursorLineRenderer } from './rendering/CursorLineRenderer.js';
import { TimelineEventIndex } from './TimelineEventIndex.js';
import { TimelineViewport } from './TimelineViewport.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ import { calculateViewportBounds } from './ViewportUtils.js';
*
* @param rectsByCategory - Spatial index of rectangles by category
* @param viewport - Current viewport state
* @param batchColors - Optional colors from RenderBatch (for theme support)
* @param batchColors - Theme-aware category colors
* @returns CulledRenderData with visible rectangles, buckets, and stats
*/
export function legacyCullRectangles(
rectsByCategory: Map<string, PrecomputedRect[]>,
viewport: ViewportState,
batchColors?: Map<string, BatchColorInfo>,
batchColors: Map<string, BatchColorInfo>,
): CulledRenderData {
const bounds = calculateViewportBounds(viewport);
const visibleRects = new Map<string, PrecomputedRect[]>();
Expand Down
21 changes: 3 additions & 18 deletions log-viewer/src/features/timeline/optimised/RectangleCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,7 @@ export class RectangleCache {
}

// Pass pre-grouped rectsByDepth if available (saves ~12ms grouping iteration)
this.segmentTree = new TemporalSegmentTree(
this.rectsByCategory,
undefined, // batchColors
precomputed?.rectsByDepth,
);
this.segmentTree = new TemporalSegmentTree(this.rectsByCategory, precomputed?.rectsByDepth);
}

/**
Expand All @@ -148,14 +144,14 @@ export class RectangleCache {
* Events <= MIN_RECT_SIZE are aggregated into time-aligned buckets.
*
* @param viewport - Current viewport state
* @param batchColors - Optional colors from RenderBatch (for theme support)
* @param batchColors - Theme-aware category colors for bucket color resolution
* @returns CulledRenderData with visible rectangles, buckets, and stats
*
* Performance target: <5ms for 50,000 events
*/
public getCulledRectangles(
viewport: ViewportState,
batchColors?: Map<string, BatchColorInfo>,
batchColors: Map<string, BatchColorInfo>,
): CulledRenderData {
return this.segmentTree.query(viewport, batchColors);
}
Expand Down Expand Up @@ -189,17 +185,6 @@ export class RectangleCache {
return this.rectMapById;
}

/**
* Update batch colors for segment tree (for theme changes).
*
* @param batchColors - New batch colors from theme
*/
public setBatchColors(batchColors: Map<string, BatchColorInfo>): void {
if (this.segmentTree) {
this.segmentTree.setBatchColors(batchColors);
}
}

/**
* Get spatial index of rectangles by category.
* Used for search functionality and segment tree construction.
Expand Down
30 changes: 4 additions & 26 deletions log-viewer/src/features/timeline/optimised/TemporalSegmentTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,7 @@ import {
SEGMENT_TREE_CONSTANTS,
TIMELINE_CONSTANTS,
} from '../types/flamechart.types.js';
import {
CATEGORY_COLORS,
UNKNOWN_CATEGORY_COLOR,
type BatchColorInfo,
} from './BucketColorResolver.js';
import { UNKNOWN_CATEGORY_COLOR, type BatchColorInfo } from './BucketColorResolver.js';
import type { PrecomputedRect } from './RectangleCache.js';
import { calculateViewportBounds } from './ViewportUtils.js';

Expand Down Expand Up @@ -94,9 +90,6 @@ export class TemporalSegmentTree {
/** Maximum depth in the tree */
private maxDepth = 0;

/** Cached batch colors for theme support */
private batchColors?: Map<string, BatchColorInfo>;

/**
* Unsorted frames collected during tree construction.
* Sorting is deferred to first getAllFramesSorted() call.
Expand All @@ -113,38 +106,26 @@ export class TemporalSegmentTree {
* Build segment trees from pre-computed rectangles.
*
* @param rectsByCategory - Rectangles grouped by category (from RectangleCache)
* @param batchColors - Optional colors for theme support
* @param rectsByDepth - Optional pre-grouped by depth (from unified conversion, saves ~12ms)
*/
constructor(
rectsByCategory: Map<string, PrecomputedRect[]>,
batchColors?: Map<string, BatchColorInfo>,
rectsByDepth?: Map<number, PrecomputedRect[]>,
) {
this.batchColors = batchColors;
this.buildTrees(rectsByCategory, rectsByDepth);
}

/**
* Update batch colors (for theme changes).
*/
public setBatchColors(batchColors: Map<string, BatchColorInfo>): void {
this.batchColors = batchColors;
// Note: We don't rebuild trees - colors are resolved at query time
}

/**
* Query the segment tree for nodes to render at current viewport.
*
* @param viewport - Current viewport state
* @param batchColors - Optional colors from RenderBatch (for theme support)
* @param batchColors - Theme-aware category colors for bucket color resolution
* @returns CulledRenderData compatible with existing rendering pipeline
*/
public query(
viewport: ViewportState,
batchColors?: Map<string, BatchColorInfo>,
batchColors: Map<string, BatchColorInfo>,
): CulledRenderData {
const effectiveBatchColors = batchColors ?? this.batchColors;
const bounds = calculateViewportBounds(viewport);
// T = 2px / zoom (ns) - used for both threshold check and bucket width
const bucketTimeWidth = BUCKET_CONSTANTS.BUCKET_WIDTH / viewport.zoom;
Expand All @@ -162,10 +143,7 @@ export class TemporalSegmentTree {
visibleRects.set(category, []);
bucketsByCategory.set(category, []);
// Pre-cache base color for each known category
const baseColor =
effectiveBatchColors?.get(category)?.color ??
CATEGORY_COLORS[category] ??
UNKNOWN_CATEGORY_COLOR;
const baseColor = batchColors.get(category)?.color ?? UNKNOWN_CATEGORY_COLOR;
categoryBaseColors.set(category, baseColor);
}

Expand Down
Loading
Loading