forked from certinia/debug-log-analyzer
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRectangleCache.ts
More file actions
319 lines (280 loc) · 10.5 KB
/
RectangleCache.ts
File metadata and controls
319 lines (280 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
/*
* Copyright (c) 2025 Certinia Inc. All rights reserved.
*/
/**
* RectangleCache
*
* Single source of truth for rectangle computation and viewport culling.
* Separates "what to draw" from "how to draw it" to eliminate coupling between renderers.
*
* Responsibilities:
* - Pre-compute all rectangles from event tree (once at initialization)
* - Maintain spatial index (rectsByCategory) for efficient access
* - Maintain event → rect mapping (rectMap) for search functionality
* - Perform viewport culling on demand
* - Provide culled rectangles to any consumer (renderers)
*
* Does NOT:
* - Perform any rendering
* - Contain any PixiJS graphics logic
* - Implement any search logic
*/
import type { LogEvent } from 'apex-log-parser';
import type {
CulledRenderData,
RenderRectangle,
ViewportState,
} from '../types/flamechart.types.js';
import { TIMELINE_CONSTANTS } from '../types/flamechart.types.js';
import type { BatchColorInfo } from './BucketColorResolver.js';
import { TemporalSegmentTree } from './TemporalSegmentTree.js';
/**
* Pre-computed event rectangle with fixed and dynamic properties.
* Stored once at construction to avoid recalculating every frame.
*/
export interface PrecomputedRect extends RenderRectangle {
/** Unique ID for this rectangle (timestamp-depth-childIndex) */
id: string;
/** Timestamp in nanoseconds (fixed) */
timeStart: number;
/** Exit timestamp in nanoseconds (fixed) */
timeEnd: number;
/** Call stack depth (fixed) */
depth: number;
/** Total duration in nanoseconds including children (fixed) */
duration: number;
/** Self duration in nanoseconds excluding children (fixed, for category resolution) */
selfDuration: number;
/** Event category for color batching (fixed) */
category: string;
/** Reference to original event (fixed) */
eventRef: LogEvent;
/** Screen X position (updated during culling) */
x: number;
/** Screen Y position (fixed) */
y: number;
/** Screen width (updated during culling) */
width: number;
/** Screen height (fixed) */
height: number;
}
/**
* Precomputed data from unified tree conversion (single-pass optimization).
* When provided, RectangleCache skips its own flattenEvents traversal.
*/
export interface PrecomputedRectData {
rectsByCategory: Map<string, PrecomputedRect[]>;
rectMap: Map<LogEvent, PrecomputedRect>;
/** Pre-grouped by depth for TemporalSegmentTree (optional - computed if not provided) */
rectsByDepth?: Map<number, PrecomputedRect[]>;
/** Whether rectsByCategory arrays are pre-sorted by timeStart (skips sorting) */
preSorted?: boolean;
}
/**
* RectangleCache
*
* Manages rectangle pre-computation, spatial indexing, and viewport culling.
* Provides a clean API for renderers to consume rectangle data without coupling.
*
* Uses TemporalSegmentTree for O(log n) viewport culling.
* For O(n) legacy culling, use LegacyViewportCuller directly.
*/
export class RectangleCache {
/** Spatial index: all rectangles grouped by category */
private rectsByCategory: Map<string, PrecomputedRect[]> = new Map();
/** Map from LogEvent to RenderRectangle for search functionality */
private rectMap: Map<LogEvent, PrecomputedRect> = new Map();
/** Cached map from rect ID to PrecomputedRect (lazy-built on first access) */
private rectMapById: Map<string, PrecomputedRect> | null = null;
/** Segment tree for O(log n) viewport culling */
private segmentTree: TemporalSegmentTree;
/**
* Create RectangleCache with either raw events or precomputed data.
*
* @param events - Event tree to pre-compute rectangles from
* @param categories - Set of valid categories for spatial indexing
* @param precomputed - Optional precomputed rectangle data from unified conversion
*/
constructor(events: LogEvent[], categories: Set<string>, precomputed?: PrecomputedRectData) {
if (precomputed) {
// Use precomputed data from unified conversion (skips flattenEvents traversal)
this.rectsByCategory = precomputed.rectsByCategory;
this.rectMap = precomputed.rectMap;
// PERF: Only sort if not already pre-sorted (~15-20ms saved)
if (!precomputed.preSorted) {
for (const rects of this.rectsByCategory.values()) {
rects.sort((a, b) => a.timeStart - b.timeStart);
}
}
} else {
// Legacy path: compute rectangles from events
this.precomputeRectangles(events, categories);
}
// Pass pre-grouped rectsByDepth if available (saves ~12ms grouping iteration)
this.segmentTree = new TemporalSegmentTree(this.rectsByCategory, precomputed?.rectsByDepth);
}
/**
* Get culled rectangles and buckets for current viewport.
* Uses TemporalSegmentTree for O(log n) performance.
*
* Events > MIN_RECT_SIZE (2px) are returned as visible rectangles.
* Events <= MIN_RECT_SIZE are aggregated into time-aligned buckets.
*
* @param viewport - Current viewport state
* @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>,
): CulledRenderData {
return this.segmentTree.query(viewport, batchColors);
}
/**
* Get map from LogEvent to PrecomputedRect for search functionality.
* Returns live references that update during each culling pass.
*
* @returns Map from LogEvent to PrecomputedRect (live references)
*/
public getRectMap(): Map<LogEvent, PrecomputedRect> {
return this.rectMap;
}
/**
* Get map from rect ID to PrecomputedRect.
* Lazy-built on first access to avoid O(n) iteration at init time.
* Used by SearchOrchestrator for O(1) rect lookup by ID.
*
* PERF: Saves ~18ms by avoiding redundant map rebuild in SearchOrchestrator.init()
*
* @returns Map from rect ID string to PrecomputedRect
*/
public getRectMapById(): Map<string, PrecomputedRect> {
if (!this.rectMapById) {
this.rectMapById = new Map();
for (const rect of this.rectMap.values()) {
this.rectMapById.set(rect.id, rect);
}
}
return this.rectMapById;
}
/**
* Get spatial index of rectangles by category.
* Used for search functionality and segment tree construction.
*
* @returns Map of category to rectangles
*/
public getRectsByCategory(): Map<string, PrecomputedRect[]> {
return this.rectsByCategory;
}
/**
* Query events within a specific time and depth region.
* Delegates to TemporalSegmentTree for O(log n + k) performance.
* Used for hit testing when bucket eventRefs are empty.
*
* @param timeStart - Start time in nanoseconds
* @param timeEnd - End time in nanoseconds
* @param depthStart - Minimum depth (inclusive)
* @param depthEnd - Maximum depth (inclusive)
* @returns Array of LogEvent references in the region
*/
public queryEventsInRegion(
timeStart: number,
timeEnd: number,
depthStart: number,
depthEnd: number,
): LogEvent[] {
return this.segmentTree.queryEventsInRegion(timeStart, timeEnd, depthStart, depthEnd);
}
/**
* Get the underlying segment tree for direct queries.
* Used by MinimapDensityQuery for O(B×log N) density computation.
*
* @returns The TemporalSegmentTree instance
*/
public getSegmentTree(): TemporalSegmentTree {
return this.segmentTree;
}
// ============================================================================
// PRIVATE METHODS
// ============================================================================
/**
* Pre-compute all rectangles from event tree.
* Creates a flat spatial index for fast culling during render.
*
* @param events - Root events to process
* @param categories - Valid categories for indexing
*/
private precomputeRectangles(events: LogEvent[], categories: Set<string>): void {
// Initialize category arrays
for (const category of categories) {
this.rectsByCategory.set(category, []);
}
// Flatten event tree into rectangles
this.flattenEvents(events, 0);
// Sort rectangles by timeStart for early exit during culling
// This enables breaking out of the loop when we've passed the viewport
for (const rects of this.rectsByCategory.values()) {
rects.sort((a, b) => a.timeStart - b.timeStart);
}
}
/**
* Iteratively flatten event tree into pre-computed rectangles.
* Uses parallel stacks to avoid recursion for better performance.
*
* @param events - Events at root level
* @param startDepth - Starting depth (0-indexed)
*/
private flattenEvents(events: LogEvent[], startDepth: number): void {
const eventHeight = TIMELINE_CONSTANTS.EVENT_HEIGHT;
// Parallel stacks for iterative traversal
const stackEvents: LogEvent[][] = [events];
const stackDepths: number[] = [startDepth];
let stackSize = 1;
while (stackSize > 0) {
// Pop from parallel stacks
stackSize--;
const currentEvents = stackEvents[stackSize]!;
const depth = stackDepths[stackSize]!;
const depthY = depth * eventHeight;
// Process all events at current depth
const len = currentEvents.length;
for (let i = 0; i < len; i++) {
const event = currentEvents[i]!;
const { duration, category, timestamp, exitStamp, children } = event;
// Check if this event should be rendered
if (duration.total && category) {
const rects = this.rectsByCategory.get(category);
if (rects) {
// Create persistent rect object (updated during culling)
// ID format: timestamp-depth-childIndex
const rect: PrecomputedRect = {
id: `${timestamp}-${depth}-${i}`,
timeStart: timestamp,
timeEnd: exitStamp ?? timestamp,
depth,
duration: duration.total,
selfDuration: duration.self,
category,
eventRef: event,
x: 0,
y: depthY,
width: 0,
height: eventHeight,
};
rects.push(rect);
// Store live reference for search (will be updated during culling)
this.rectMap.set(event, rect);
}
}
// Push children onto parallel stacks for processing at depth + 1
if (children?.length) {
stackEvents[stackSize] = children;
stackDepths[stackSize] = depth + 1;
stackSize++;
}
}
}
}
}