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
45 changes: 19 additions & 26 deletions log-viewer/src/features/timeline/__tests__/markers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class MockSprite {
public width = 0;
public height = 0;
public tint = 0xffffff;
public alpha = 1;
public visible = true;
public parent: unknown = null;
public _zIndex = 0;
Expand Down Expand Up @@ -67,22 +68,11 @@ jest.mock('pixi.js', () => {
});

import * as PIXI from 'pixi.js';
import { blendWithBackground } from '../optimised/BucketColorResolver.js';
import { TimelineMarkerRenderer } from '../optimised/markers/TimelineMarkerRenderer.js';
import { TimelineViewport } from '../optimised/TimelineViewport.js';
import type { TimelineMarker } from '../types/flamechart.types.js';
import { MARKER_ALPHA, MARKER_COLORS } from '../types/flamechart.types.js';

/**
* Pre-blended marker colors for testing (matches TimelineMarkerRenderer).
* These are computed once to match the expected render output.
*/
const MARKER_COLORS_BLENDED = {
error: blendWithBackground(MARKER_COLORS.error, MARKER_ALPHA),
skip: blendWithBackground(MARKER_COLORS.skip, MARKER_ALPHA),
unexpected: blendWithBackground(MARKER_COLORS.unexpected, MARKER_ALPHA),
};

// Mock PIXI.Container
class MockContainer {
private children: unknown[] = [];
Expand Down Expand Up @@ -125,7 +115,7 @@ describe('TimelineMarkerRenderer', () => {
});

describe('T013: Color Accuracy Verification', () => {
it('should render error markers with pre-blended color via sprite tint', () => {
it('should render error markers with raw color and alpha via sprite tint', () => {
const markers: TimelineMarker[] = [
{ id: 'marker-error', type: 'error', startTime: 100_000, summary: 'Test error' },
];
Expand All @@ -137,14 +127,15 @@ describe('TimelineMarkerRenderer', () => {
);
renderer.render();

// Find the sprite with error color
// Find the sprite with error color and alpha
const errorSprites = createdMockSprites.filter(
(s) => s.visible && s.tint === MARKER_COLORS_BLENDED.error,
(s) => s.visible && s.tint === MARKER_COLORS.error,
);
expect(errorSprites.length).toBeGreaterThanOrEqual(1);
expect(errorSprites[0]!.alpha).toBe(MARKER_ALPHA);
});

it('should render skip markers with pre-blended color via sprite tint', () => {
it('should render skip markers with raw color and alpha via sprite tint', () => {
const markers: TimelineMarker[] = [
{ id: 'marker-skip', type: 'skip', startTime: 100_000, summary: 'Test skip' },
];
Expand All @@ -156,14 +147,15 @@ describe('TimelineMarkerRenderer', () => {
);
renderer.render();

// Find the sprite with skip color
// Find the sprite with skip color and alpha
const skipSprites = createdMockSprites.filter(
(s) => s.visible && s.tint === MARKER_COLORS_BLENDED.skip,
(s) => s.visible && s.tint === MARKER_COLORS.skip,
);
expect(skipSprites.length).toBeGreaterThanOrEqual(1);
expect(skipSprites[0]!.alpha).toBe(MARKER_ALPHA);
});

it('should render unexpected markers with pre-blended color via sprite tint', () => {
it('should render unexpected markers with raw color and alpha via sprite tint', () => {
const markers: TimelineMarker[] = [
{
id: 'marker-unexpected',
Expand All @@ -180,11 +172,12 @@ describe('TimelineMarkerRenderer', () => {
);
renderer.render();

// Find the sprite with unexpected color
// Find the sprite with unexpected color and alpha
const unexpectedSprites = createdMockSprites.filter(
(s) => s.visible && s.tint === MARKER_COLORS_BLENDED.unexpected,
(s) => s.visible && s.tint === MARKER_COLORS.unexpected,
);
expect(unexpectedSprites.length).toBeGreaterThanOrEqual(1);
expect(unexpectedSprites[0]!.alpha).toBe(MARKER_ALPHA);
});
});

Expand Down Expand Up @@ -300,7 +293,7 @@ describe('TimelineMarkerRenderer', () => {
// First marker should be culled (ends at 200_000 < viewport start 250_000)
// Second marker should be visible (200_000 to 1_000_000 overlaps 250_000)
expect(visibleSprites.length).toBe(1);
expect(visibleSprites[0]!.tint).toBe(MARKER_COLORS_BLENDED.skip);
expect(visibleSprites[0]!.tint).toBe(MARKER_COLORS.skip);
});

it('should cull markers entirely after viewport', () => {
Expand Down Expand Up @@ -412,11 +405,11 @@ describe('TimelineMarkerRenderer', () => {
expect(visibleSprites.length).toBe(3);

// First sprite (skip at 100_000) should be leftmost
expect(visibleSprites[0]!.tint).toBe(MARKER_COLORS_BLENDED.skip);
expect(visibleSprites[0]!.tint).toBe(MARKER_COLORS.skip);
// Second sprite (unexpected at 200_000)
expect(visibleSprites[1]!.tint).toBe(MARKER_COLORS_BLENDED.unexpected);
expect(visibleSprites[1]!.tint).toBe(MARKER_COLORS.unexpected);
// Third sprite (error at 300_000)
expect(visibleSprites[2]!.tint).toBe(MARKER_COLORS_BLENDED.error);
expect(visibleSprites[2]!.tint).toBe(MARKER_COLORS.error);
});
});

Expand Down Expand Up @@ -580,8 +573,8 @@ describe('TimelineMarkerRenderer', () => {
expect(visibleSprites.length).toBe(2);

// Verify the new markers are rendered with correct tints
const skipSprites = visibleSprites.filter((s) => s.tint === MARKER_COLORS_BLENDED.skip);
const errorSprites = visibleSprites.filter((s) => s.tint === MARKER_COLORS_BLENDED.error);
const skipSprites = visibleSprites.filter((s) => s.tint === MARKER_COLORS.skip);
const errorSprites = visibleSprites.filter((s) => s.tint === MARKER_COLORS.error);
expect(skipSprites.length).toBe(1);
expect(errorSprites.length).toBe(1);
});
Expand Down
10 changes: 5 additions & 5 deletions log-viewer/src/features/timeline/optimised/RectangleShader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ void main() {
/**
* WebGL fragment shader (GLSL 300 ES)
*
* Simply outputs the interpolated vertex color.
* Colors are pre-blended with background, so no alpha blending needed.
* Outputs premultiplied alpha color for correct blending with PixiJS 8's
* default blend mode (GL_ONE, GL_ONE_MINUS_SRC_ALPHA).
*/
const glslFragment = /* glsl */ `#version 300 es
precision highp float;
Expand All @@ -51,7 +51,7 @@ in vec4 vColor;
out vec4 fragColor;

void main() {
fragColor = vColor;
fragColor = vec4(vColor.rgb * vColor.a, vColor.a);
}
`;

Expand Down Expand Up @@ -83,7 +83,7 @@ fn main(input: VertexInput) -> VertexOutput {
/**
* WebGPU fragment shader (WGSL)
*
* Same functionality as GLSL version for WebGPU renderer.
* Outputs premultiplied alpha color for correct blending.
*/
const wgslFragment = /* wgsl */ `
struct FragmentInput {
Expand All @@ -92,7 +92,7 @@ struct FragmentInput {

@fragment
fn main(input: FragmentInput) -> @location(0) vec4f {
return input.vColor;
return vec4f(input.vColor.rgb * input.vColor.a, input.vColor.a);
}
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,8 @@
* used by both MeshMarkerRenderer and TimelineMarkerRenderer.
*/

import type { MarkerType, TimelineMarker } from '../../types/flamechart.types.js';
import { MARKER_ALPHA, MARKER_COLORS, SEVERITY_RANK } from '../../types/flamechart.types.js';
import { blendWithBackground } from '../BucketColorResolver.js';

/**
* Pre-blended opaque marker colors (MARKER_COLORS blended at MARKER_ALPHA opacity).
* Computed once at module load time for performance.
*
* Using pre-blended colors avoids runtime alpha compositing on the GPU,
* which is more efficient for static opacity values.
*/
export const MARKER_COLORS_BLENDED: Record<MarkerType, number> = {
error: blendWithBackground(MARKER_COLORS.error, MARKER_ALPHA),
skip: blendWithBackground(MARKER_COLORS.skip, MARKER_ALPHA),
unexpected: blendWithBackground(MARKER_COLORS.unexpected, MARKER_ALPHA),
};
import type { TimelineMarker } from '../../types/flamechart.types.js';
import { SEVERITY_RANK } from '../../types/flamechart.types.js';

/**
* Sort markers by startTime, then by severity (higher severity first for stacking).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@
* - Single Mesh draw call for all markers
* - Direct buffer updates (no scene graph overhead)
* - Clip-space coordinates (no uniform binding overhead)
* - Pre-blended opaque colors (no alpha blending)
* - True alpha transparency for theme-adaptive colors
*/

import { Container, Geometry, Mesh, Shader } from 'pixi.js';
import type { TimelineMarker } from '../../types/flamechart.types.js';
import { MARKER_ALPHA, MARKER_COLORS } from '../../types/flamechart.types.js';
import { RectangleGeometry, type ViewportTransform } from '../RectangleGeometry.js';
import { createRectangleShader } from '../RectangleShader.js';
import type { TimelineViewport } from '../TimelineViewport.js';
import { hitTestMarkers, type MarkerIndicator } from './MarkerHitTest.js';
import { MARKER_COLORS_BLENDED, sortMarkersByTimeAndSeverity } from './MarkerProcessor.js';
import { sortMarkersByTimeAndSeverity } from './MarkerProcessor.js';

/**
* Renders marker indicators as semi-transparent vertical bands using Mesh.
Expand Down Expand Up @@ -136,14 +137,13 @@ export class MeshMarkerRenderer {
continue;
}

// Create indicator record with pre-blended opaque color
const indicator: MarkerIndicator = {
marker,
resolvedEndTime,
screenStartX: worldStartX,
screenEndX: worldEndX,
screenWidth: worldWidth,
color: MARKER_COLORS_BLENDED[marker.type],
color: MARKER_COLORS[marker.type],
isVisible: true,
};

Expand Down Expand Up @@ -193,6 +193,7 @@ export class MeshMarkerRenderer {
viewportState.displayHeight,
indicator.color,
viewportTransform,
MARKER_ALPHA,
);
rectIndex++;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,16 @@
* Performance optimizations:
* - Uses SpritePool with shared 1x1 white texture for automatic GPU batching
* - Sprites are pooled and reused (no GC overhead after warmup)
* - Color applied via sprite.tint (pre-blended opaque colors)
* - Color applied via sprite.tint with true alpha transparency
*/

import type { Container } from 'pixi.js';
import type { MarkerType, TimelineMarker } from '../../types/flamechart.types.js';
import type { TimelineMarker } from '../../types/flamechart.types.js';
import { MARKER_ALPHA, MARKER_COLORS, SEVERITY_RANK } from '../../types/flamechart.types.js';
import { blendWithBackground } from '../BucketColorResolver.js';
import { SpritePool } from '../SpritePool.js';
import type { TimelineViewport } from '../TimelineViewport.js';
import { hitTestMarkers, type MarkerIndicator } from './MarkerHitTest.js';

/**
* Pre-blended opaque marker colors (MARKER_COLORS blended at MARKER_ALPHA opacity).
* Computed once at module load time for performance.
*/
const MARKER_COLORS_BLENDED: Record<MarkerType, number> = {
error: blendWithBackground(MARKER_COLORS.error, MARKER_ALPHA),
skip: blendWithBackground(MARKER_COLORS.skip, MARKER_ALPHA),
unexpected: blendWithBackground(MARKER_COLORS.unexpected, MARKER_ALPHA),
};

/**
* Renders marker indicators as semi-transparent vertical bands using sprites.
*
Expand Down Expand Up @@ -130,14 +119,13 @@ export class TimelineMarkerRenderer {
continue;
}

// Create indicator record with pre-blended opaque color
const indicator: MarkerIndicator = {
marker,
resolvedEndTime,
screenStartX: worldStartX,
screenEndX: worldEndX,
screenWidth: worldWidth,
color: MARKER_COLORS_BLENDED[marker.type],
color: MARKER_COLORS[marker.type],
isVisible: true,
};

Expand All @@ -161,6 +149,7 @@ export class TimelineMarkerRenderer {
sprite.width = gappedWidth;
sprite.height = viewportState.displayHeight;
sprite.tint = indicator.color;
sprite.alpha = MARKER_ALPHA;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,14 @@ import type {
TimelineMarker,
ViewportState,
} from '../../types/flamechart.types.js';
import { MARKER_ALPHA, MARKER_COLORS } from '../../types/flamechart.types.js';
import {
BREACH_AREA_OPACITY,
DANGER_ZONE_OPACITY,
getMetricStripColors,
getTrafficLightColor,
METRIC_STRIP_HEIGHT,
METRIC_STRIP_LINE_WIDTHS,
METRIC_STRIP_MARKER_COLORS_BLENDED,
METRIC_STRIP_MARKER_OPACITY,
METRIC_STRIP_THRESHOLDS,
METRIC_STRIP_TOGGLE_WIDTH,
METRIC_STRIP_Y_MAX_PERCENT,
Expand Down Expand Up @@ -402,7 +401,7 @@ export class MetricStripRenderer {
continue;
}

const color = METRIC_STRIP_MARKER_COLORS_BLENDED[marker.type];
const color = MARKER_COLORS[marker.type];
if (color === undefined) {
continue;
}
Expand All @@ -413,7 +412,7 @@ export class MetricStripRenderer {

if (gappedWidth > 0) {
g.rect(gappedStartX, 0, gappedWidth, this.height);
g.fill({ color, alpha: METRIC_STRIP_MARKER_OPACITY });
g.fill({ color, alpha: MARKER_ALPHA });
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
* Colors are designed to be distinguishable on both light and dark backgrounds.
*/

import type { MarkerType } from '../../types/flamechart.types.js';

/**
* MetricStrip color palette.
*/
Expand Down Expand Up @@ -264,22 +262,6 @@ export const METRIC_STRIP_TIME_GRID_COLOR = 0x808080;
*/
export const METRIC_STRIP_TIME_GRID_OPACITY = 0.3;

/**
* Marker colors pre-blended with background for metric strip background bands.
* These match the minimap marker colors for visual consistency.
*/
export const METRIC_STRIP_MARKER_COLORS_BLENDED: Record<MarkerType, number> = {
error: 0xff8080, // Light red
skip: 0x1e80ff, // Light blue
unexpected: 0x8080ff, // Light purple
};

/**
* Marker band opacity in metric strip.
* Matches MARKER_ALPHA (0.2) from main timeline for visual consistency.
*/
export const METRIC_STRIP_MARKER_OPACITY = 0.2;

/**
* Width of the expand/collapse toggle area on the left side.
*/
Expand Down
Loading
Loading