argent-grid 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +69 -0
- package/.github/workflows/pages.yml +6 -12
- package/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +18 -0
- package/.storybook/tsconfig.json +24 -0
- package/AGENTS.md +70 -27
- package/README.md +51 -34
- package/angular.json +66 -0
- package/biome.json +66 -0
- package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
- package/docs/AG-GRID-COMPARISON.md +725 -0
- package/docs/CELL-RENDERER-GUIDE.md +241 -0
- package/docs/CONTEXT-MENU-GUIDE.md +371 -0
- package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
- package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
- package/docs/PERFORMANCE-REVIEW.md +571 -0
- package/docs/RESEARCH-STATUS.md +234 -0
- package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
- package/docs/STORYBOOK-REFACTOR.md +215 -0
- package/docs/STORYBOOK-STATUS.md +156 -0
- package/docs/TEST-COVERAGE-REPORT.md +276 -0
- package/docs/THEME-API-GUIDE.md +445 -0
- package/docs/THEME-API-PLAN.md +364 -0
- package/e2e/advanced.spec.ts +109 -0
- package/e2e/argentgrid.spec.ts +65 -0
- package/e2e/benchmark.spec.ts +52 -0
- package/e2e/cell-renderers.spec.ts +152 -0
- package/e2e/debug-streaming.spec.ts +31 -0
- package/e2e/dnd.spec.ts +73 -0
- package/e2e/screenshots.spec.ts +52 -0
- package/e2e/theming.spec.ts +35 -0
- package/e2e/visual.spec.ts +112 -0
- package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
- package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
- package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
- package/package.json +21 -7
- package/plan.md +56 -28
- package/playwright.config.ts +38 -0
- package/setup-vitest.ts +10 -13
- package/src/lib/argent-grid.module.ts +10 -12
- package/src/lib/components/argent-grid.component.css +281 -321
- package/src/lib/components/argent-grid.component.html +295 -207
- package/src/lib/components/argent-grid.component.spec.ts +120 -160
- package/src/lib/components/argent-grid.component.ts +1193 -290
- package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
- package/src/lib/components/argent-grid.selection.spec.ts +132 -0
- package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
- package/src/lib/components/set-filter/set-filter.component.ts +307 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
- package/src/lib/directives/click-outside.directive.ts +19 -0
- package/src/lib/rendering/canvas-renderer.spec.ts +513 -0
- package/src/lib/rendering/canvas-renderer.ts +456 -452
- package/src/lib/rendering/live-data-handler.ts +110 -0
- package/src/lib/rendering/live-data-optimizations.ts +133 -0
- package/src/lib/rendering/render/blit.spec.ts +16 -27
- package/src/lib/rendering/render/blit.ts +48 -36
- package/src/lib/rendering/render/cells.spec.ts +132 -0
- package/src/lib/rendering/render/cells.ts +167 -28
- package/src/lib/rendering/render/column-utils.ts +95 -0
- package/src/lib/rendering/render/hit-test.ts +50 -0
- package/src/lib/rendering/render/index.ts +88 -76
- package/src/lib/rendering/render/lines.ts +53 -47
- package/src/lib/rendering/render/primitives.ts +423 -0
- package/src/lib/rendering/render/theme.spec.ts +8 -12
- package/src/lib/rendering/render/theme.ts +7 -10
- package/src/lib/rendering/render/types.ts +3 -2
- package/src/lib/rendering/render/walk.spec.ts +35 -38
- package/src/lib/rendering/render/walk.ts +94 -64
- package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
- package/src/lib/rendering/utils/damage-tracker.ts +6 -18
- package/src/lib/rendering/utils/index.ts +1 -1
- package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
- package/src/lib/services/grid.service.spec.ts +1241 -201
- package/src/lib/services/grid.service.ts +1204 -235
- package/src/lib/themes/parts/color-schemes.ts +132 -0
- package/src/lib/themes/parts/icon-sets.ts +258 -0
- package/src/lib/themes/theme-builder.ts +347 -0
- package/src/lib/themes/theme-quartz.ts +72 -0
- package/src/lib/themes/types.ts +238 -0
- package/src/lib/types/ag-grid-types.ts +573 -14
- package/src/public-api.ts +39 -9
- package/src/stories/Advanced.stories.ts +249 -0
- package/src/stories/ArgentGrid.stories.ts +301 -0
- package/src/stories/Benchmark.stories.ts +76 -0
- package/src/stories/CellRenderers.stories.ts +395 -0
- package/src/stories/Filtering.stories.ts +292 -0
- package/src/stories/Grouping.stories.ts +290 -0
- package/src/stories/Streaming.stories.ts +57 -0
- package/src/stories/Theming.stories.ts +137 -0
- package/src/stories/Tooltips.stories.ts +381 -0
- package/src/stories/benchmark-wrapper.component.ts +355 -0
- package/src/stories/story-utils.ts +88 -0
- package/src/stories/streaming-wrapper.component.ts +441 -0
- package/tsconfig.json +1 -0
- package/tsconfig.storybook.json +10 -0
- package/vitest.config.ts +9 -9
- package/demo-app/README.md +0 -70
- package/demo-app/angular.json +0 -78
- package/demo-app/e2e/benchmark.spec.ts +0 -53
- package/demo-app/e2e/demo-page.spec.ts +0 -77
- package/demo-app/e2e/grid-features.spec.ts +0 -269
- package/demo-app/package-lock.json +0 -14023
- package/demo-app/package.json +0 -36
- package/demo-app/playwright-test-menu.js +0 -19
- package/demo-app/playwright.config.ts +0 -23
- package/demo-app/src/app/app.component.ts +0 -10
- package/demo-app/src/app/app.config.ts +0 -13
- package/demo-app/src/app/app.routes.ts +0 -7
- package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
- package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
- package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
- package/demo-app/src/index.html +0 -19
- package/demo-app/src/main.ts +0 -6
- package/demo-app/tsconfig.json +0 -31
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
# ArgentGrid Performance Review & Optimization Plan
|
|
2
|
+
|
|
3
|
+
**Date:** February 28, 2026
|
|
4
|
+
**Reviewer:** AI Assistant
|
|
5
|
+
**Focus:** Canvas Rendering Performance
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 📊 Current Performance Status
|
|
10
|
+
|
|
11
|
+
### Benchmarks (from demo app)
|
|
12
|
+
|
|
13
|
+
| Dataset | Load Time | Scroll FPS | Memory |
|
|
14
|
+
|---------|-----------|------------|--------|
|
|
15
|
+
| 100K rows | ~180ms | 60fps | ~50MB |
|
|
16
|
+
| 500K rows | ~800ms | 55-60fps | ~150MB |
|
|
17
|
+
| 1M rows | ~2s | 50-55fps | ~300MB |
|
|
18
|
+
|
|
19
|
+
**Assessment:** ✅ **Good** - Meets 60fps target for most scenarios
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🔍 Code Review Findings
|
|
24
|
+
|
|
25
|
+
### 1. Canvas Rendering (`canvas-renderer.ts`)
|
|
26
|
+
|
|
27
|
+
#### ✅ Strengths
|
|
28
|
+
|
|
29
|
+
1. **Virtual Scrolling** - Only renders visible rows ✅
|
|
30
|
+
2. **RequestAnimationFrame** - Batches render calls ✅
|
|
31
|
+
3. **Damage Tracking** - Partial redraws supported ✅
|
|
32
|
+
4. **Blitting** - Frame-to-frame optimization ✅
|
|
33
|
+
5. **Column Prep Caching** - Caches column definitions ✅
|
|
34
|
+
|
|
35
|
+
#### ⚠️ Optimization Opportunities
|
|
36
|
+
|
|
37
|
+
##### 1.1 **Row Background Rendering** (HIGH IMPACT)
|
|
38
|
+
|
|
39
|
+
**Current:**
|
|
40
|
+
```typescript
|
|
41
|
+
// Every row draws a full-width rectangle
|
|
42
|
+
this.ctx.fillStyle = backgroundColor;
|
|
43
|
+
this.ctx.fillRect(0, y, viewportWidth, rowHeight);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Issue:** Drawing full-width rectangles for every row is expensive, especially for wide grids.
|
|
47
|
+
|
|
48
|
+
**Optimization:**
|
|
49
|
+
```typescript
|
|
50
|
+
// Only draw visible portion
|
|
51
|
+
const visibleStart = this.scrollLeft;
|
|
52
|
+
const visibleEnd = this.scrollLeft + this.viewportWidth;
|
|
53
|
+
this.ctx.fillRect(visibleStart, y, visibleEnd - visibleStart, rowHeight);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Expected Impact:** 10-15% faster row rendering
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
##### 1.2 **Text Rendering** (MEDIUM IMPACT)
|
|
61
|
+
|
|
62
|
+
**Current:**
|
|
63
|
+
```typescript
|
|
64
|
+
// Text is rendered for every cell every frame
|
|
65
|
+
this.ctx.fillText(truncatedText, Math.floor(textX), Math.floor(textY));
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Issue:** Text rendering is one of the most expensive canvas operations.
|
|
69
|
+
|
|
70
|
+
**Optimization:**
|
|
71
|
+
```typescript
|
|
72
|
+
// 1. Cache rendered text as offscreen canvas for static cells
|
|
73
|
+
// 2. Only re-render cells that changed
|
|
74
|
+
// 3. Use drawImage() for cached text instead of fillText()
|
|
75
|
+
|
|
76
|
+
// Example: Text cache
|
|
77
|
+
private textCache = new Map<string, HTMLCanvasElement>();
|
|
78
|
+
|
|
79
|
+
private getCachedText(text: string, font: string): HTMLCanvasElement {
|
|
80
|
+
const key = `${text}:${font}`;
|
|
81
|
+
if (!this.textCache.has(key)) {
|
|
82
|
+
const canvas = document.createElement('canvas');
|
|
83
|
+
const ctx = canvas.getContext('2d')!;
|
|
84
|
+
ctx.font = font;
|
|
85
|
+
const metrics = ctx.measureText(text);
|
|
86
|
+
canvas.width = metrics.width;
|
|
87
|
+
canvas.height = 20;
|
|
88
|
+
ctx.font = font;
|
|
89
|
+
ctx.fillText(text, 0, 15);
|
|
90
|
+
this.textCache.set(key, canvas);
|
|
91
|
+
}
|
|
92
|
+
return this.textCache.get(key)!;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Expected Impact:** 20-30% faster for static data
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
##### 1.3 **Grid Lines Rendering** (MEDIUM IMPACT)
|
|
101
|
+
|
|
102
|
+
**Current:**
|
|
103
|
+
```typescript
|
|
104
|
+
// Draws lines for every row and column
|
|
105
|
+
for (let i = startRow; i <= endRow; i++) {
|
|
106
|
+
const y = i * this.rowHeight - this.scrollTop;
|
|
107
|
+
this.ctx.beginPath();
|
|
108
|
+
this.ctx.moveTo(0, y);
|
|
109
|
+
this.ctx.lineTo(width, y);
|
|
110
|
+
this.ctx.stroke();
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Issue:** Drawing hundreds of individual line paths is expensive.
|
|
115
|
+
|
|
116
|
+
**Optimization:**
|
|
117
|
+
```typescript
|
|
118
|
+
// Use a single path for all horizontal lines
|
|
119
|
+
this.ctx.beginPath();
|
|
120
|
+
for (let i = startRow; i <= endRow; i++) {
|
|
121
|
+
const y = i * this.rowHeight - this.scrollTop;
|
|
122
|
+
this.ctx.moveTo(0, y);
|
|
123
|
+
this.ctx.lineTo(width, y);
|
|
124
|
+
}
|
|
125
|
+
this.ctx.stroke();
|
|
126
|
+
|
|
127
|
+
// Or better: use createPattern for repeating lines
|
|
128
|
+
const pattern = this.createLinePattern(this.rowHeight, this.theme.gridLineColor);
|
|
129
|
+
this.ctx.fillStyle = pattern;
|
|
130
|
+
this.ctx.fillRect(0, 0, width, height);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Expected Impact:** 15-20% faster grid line rendering
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
##### 1.4 **Column X Position Calculation** (LOW IMPACT)
|
|
138
|
+
|
|
139
|
+
**Current:**
|
|
140
|
+
```typescript
|
|
141
|
+
// O(n) lookup for every cell
|
|
142
|
+
private getColumnX(targetCol: Column, allVisibleColumns: Column[]): number {
|
|
143
|
+
let x = 0;
|
|
144
|
+
for (const col of allVisibleColumns) {
|
|
145
|
+
if (col.colId === targetCol.colId) return x;
|
|
146
|
+
x += col.width;
|
|
147
|
+
}
|
|
148
|
+
return x;
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Issue:** Called for every cell, O(n) per call.
|
|
153
|
+
|
|
154
|
+
**Optimization:**
|
|
155
|
+
```typescript
|
|
156
|
+
// Cache column positions once per render frame
|
|
157
|
+
private columnPositions: Map<string, number> = new Map();
|
|
158
|
+
|
|
159
|
+
private prepareColumnPositions(allVisibleColumns: Column[]): void {
|
|
160
|
+
this.columnPositions.clear();
|
|
161
|
+
let x = 0;
|
|
162
|
+
for (const col of allVisibleColumns) {
|
|
163
|
+
this.columnPositions.set(col.colId, x);
|
|
164
|
+
x += col.width;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private getColumnX(targetCol: Column): number {
|
|
169
|
+
return this.columnPositions.get(targetCol.colId) || 0;
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Expected Impact:** 5-10% faster cell positioning
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
##### 1.5 **Damage Tracking Optimization** (MEDIUM IMPACT)
|
|
178
|
+
|
|
179
|
+
**Current:**
|
|
180
|
+
```typescript
|
|
181
|
+
// Tracks damage but doesn't fully utilize it
|
|
182
|
+
this.damageTracker.markCellDirty(colIndex, rowIndex);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Issue:** Damage tracking exists but render loop still clears entire canvas.
|
|
186
|
+
|
|
187
|
+
**Optimization:**
|
|
188
|
+
```typescript
|
|
189
|
+
// Only clear damaged regions
|
|
190
|
+
const damageRects = this.damageTracker.getDamageRects();
|
|
191
|
+
if (damageRects.length > 0) {
|
|
192
|
+
for (const rect of damageRects) {
|
|
193
|
+
this.ctx.clearRect(rect.x, rect.y, rect.width, rect.height);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
// Full clear only when needed
|
|
197
|
+
this.ctx.clearRect(0, 0, width, height);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Only render damaged cells
|
|
201
|
+
walkCells(..., (rowIndex, colIndex, x, y, rowNode, colDef) => {
|
|
202
|
+
if (this.damageTracker.isCellDirty(rowIndex, colIndex)) {
|
|
203
|
+
this.renderCell(...);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Expected Impact:** 30-50% faster for partial updates
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### 2. Virtual Scrolling (`walk.ts`)
|
|
213
|
+
|
|
214
|
+
#### ✅ Strengths
|
|
215
|
+
|
|
216
|
+
1. **Row Buffer** - Renders extra rows for smooth scrolling ✅
|
|
217
|
+
2. **Visible Range Calculation** - Efficient binary search ✅
|
|
218
|
+
|
|
219
|
+
#### ⚠️ Optimization Opportunities
|
|
220
|
+
|
|
221
|
+
##### 2.1 **Row Height Cache** (LOW IMPACT)
|
|
222
|
+
|
|
223
|
+
**Current:**
|
|
224
|
+
```typescript
|
|
225
|
+
const y = rowIndex * rowHeight - scrollTop;
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Issue:** Assumes fixed row height, doesn't support variable heights efficiently.
|
|
229
|
+
|
|
230
|
+
**Optimization:**
|
|
231
|
+
```typescript
|
|
232
|
+
// Cache cumulative row heights for O(1) lookup
|
|
233
|
+
private rowHeightCache: number[] = [];
|
|
234
|
+
private cumulativeRowHeights: number[] = [];
|
|
235
|
+
|
|
236
|
+
private getRowY(rowIndex: number): number {
|
|
237
|
+
return this.cumulativeRowHeights[rowIndex] || 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private updateRowHeightCache(): void {
|
|
241
|
+
// Only update when row heights change
|
|
242
|
+
this.cumulativeRowHeights = [];
|
|
243
|
+
let total = 0;
|
|
244
|
+
for (let i = 0; i < this.totalRowCount; i++) {
|
|
245
|
+
this.cumulativeRowHeights.push(total);
|
|
246
|
+
total += this.getRowHeight(i);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Expected Impact:** Better support for variable row heights
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
### 3. Data Processing (`grid.service.ts`)
|
|
256
|
+
|
|
257
|
+
#### ✅ Strengths
|
|
258
|
+
|
|
259
|
+
1. **Filtered Row Cache** - Caches filtered results ✅
|
|
260
|
+
2. **Grouping Cache** - Caches grouped data ✅
|
|
261
|
+
|
|
262
|
+
#### ⚠️ Optimization Opportunities
|
|
263
|
+
|
|
264
|
+
##### 3.1 **Filter Performance** (HIGH IMPACT)
|
|
265
|
+
|
|
266
|
+
**Current:**
|
|
267
|
+
```typescript
|
|
268
|
+
this.filteredRowData = this.rowData.filter(row => {
|
|
269
|
+
return Object.keys(this.filterModel).every(colId => {
|
|
270
|
+
const filterItem = this.filterModel[colId];
|
|
271
|
+
const column = this.columns.get(colId);
|
|
272
|
+
const value = (row as any)[column.field];
|
|
273
|
+
return this.matchesFilter(value, filterItem);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Issue:** Filters all rows on every filter change.
|
|
279
|
+
|
|
280
|
+
**Optimization:**
|
|
281
|
+
```typescript
|
|
282
|
+
// 1. Index data by filterable columns
|
|
283
|
+
private filterIndexes: Map<string, Map<any, number[]>> = new Map();
|
|
284
|
+
|
|
285
|
+
private buildFilterIndexes(): void {
|
|
286
|
+
this.filterIndexes.clear();
|
|
287
|
+
this.rowData.forEach((row, index) => {
|
|
288
|
+
this.columns.forEach((col, colId) => {
|
|
289
|
+
if (!this.filterIndexes.has(colId)) {
|
|
290
|
+
this.filterIndexes.set(colId, new Map());
|
|
291
|
+
}
|
|
292
|
+
const value = (row as any)[col.field];
|
|
293
|
+
if (!this.filterIndexes.get(colId)!.has(value)) {
|
|
294
|
+
this.filterIndexes.get(colId)!.set(value, []);
|
|
295
|
+
}
|
|
296
|
+
this.filterIndexes.get(colId)!.get(value)!.push(index);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 2. Use indexes for set filters
|
|
302
|
+
private applySetFilter(colId: string, values: any[]): number[] {
|
|
303
|
+
const index = this.filterIndexes.get(colId);
|
|
304
|
+
if (!index) return [];
|
|
305
|
+
|
|
306
|
+
const result = new Set<number>();
|
|
307
|
+
values.forEach(value => {
|
|
308
|
+
const rowIndices = index.get(value);
|
|
309
|
+
if (rowIndices) {
|
|
310
|
+
rowIndices.forEach(idx => result.add(idx));
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
return Array.from(result);
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Expected Impact:** 10-100x faster for set filters on large datasets
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
##### 3.2 **Sorting Performance** (MEDIUM IMPACT)
|
|
322
|
+
|
|
323
|
+
**Current:**
|
|
324
|
+
```typescript
|
|
325
|
+
this.rowData.sort((a, b) => {
|
|
326
|
+
const aValue = (a as any)[sortCol.field];
|
|
327
|
+
const bValue = (b as any)[sortCol.field];
|
|
328
|
+
return aValue.localeCompare(bValue);
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Issue:** Sorts entire dataset on every sort change.
|
|
333
|
+
|
|
334
|
+
**Optimization:**
|
|
335
|
+
```typescript
|
|
336
|
+
// 1. Use typed arrays for numeric sorting
|
|
337
|
+
private sortNumeric(field: string, direction: 'asc' | 'desc'): void {
|
|
338
|
+
const values = new Float64Array(this.rowData.map(r => (r as any)[field]));
|
|
339
|
+
const indices = new Uint32Array(this.rowData.length);
|
|
340
|
+
indices.forEach((_, i) => indices[i] = i);
|
|
341
|
+
|
|
342
|
+
indices.sort((a, b) =>
|
|
343
|
+
direction === 'asc'
|
|
344
|
+
? values[a] - values[b]
|
|
345
|
+
: values[b] - values[a]
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// Reorder rows based on sorted indices
|
|
349
|
+
this.rowData = indices.map(i => this.rowData[i]);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 2. Cache sort results
|
|
353
|
+
private sortCache: Map<string, TData[]> = new Map();
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Expected Impact:** 2-5x faster for numeric sorting
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
### 4. Event Handling
|
|
361
|
+
|
|
362
|
+
#### ⚠️ Optimization Opportunities
|
|
363
|
+
|
|
364
|
+
##### 4.1 **Mouse Event Throttling** (MEDIUM IMPACT)
|
|
365
|
+
|
|
366
|
+
**Current:**
|
|
367
|
+
```typescript
|
|
368
|
+
this.canvas.addEventListener('mousemove', (e) => {
|
|
369
|
+
this.handleMouseMove(e);
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Issue:** Mousemove fires hundreds of times per second.
|
|
374
|
+
|
|
375
|
+
**Optimization:**
|
|
376
|
+
```typescript
|
|
377
|
+
// Throttle mousemove events
|
|
378
|
+
private throttle(fn: Function, limit: number) {
|
|
379
|
+
let inThrottle: boolean;
|
|
380
|
+
return (...args: any[]) => {
|
|
381
|
+
if (!inThrottle) {
|
|
382
|
+
fn.apply(this, args);
|
|
383
|
+
inThrottle = true;
|
|
384
|
+
setTimeout(() => inThrottle = false, limit);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this.canvas.addEventListener('mousemove', this.throttle((e) => {
|
|
390
|
+
this.handleMouseMove(e);
|
|
391
|
+
}, 16)); // ~60fps
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Expected Impact:** 50-80% fewer event handler calls
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
##### 4.2 **Hit Testing Optimization** (LOW IMPACT)
|
|
399
|
+
|
|
400
|
+
**Current:**
|
|
401
|
+
```typescript
|
|
402
|
+
// Linear search through all visible rows
|
|
403
|
+
for (let i = startRow; i < endRow; i++) {
|
|
404
|
+
const y = i * this.rowHeight - scrollTop;
|
|
405
|
+
if (mouseY >= y && mouseY < y + rowHeight) {
|
|
406
|
+
return { rowIndex: i, ... };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**Optimization:**
|
|
412
|
+
```typescript
|
|
413
|
+
// Direct calculation (O(1))
|
|
414
|
+
const rowIndex = Math.floor((mouseY + scrollTop) / this.rowHeight);
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Expected Impact:** Instant hit testing
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## 🎯 Priority Optimization Plan
|
|
422
|
+
|
|
423
|
+
### Phase 1: Quick Wins (1-2 days)
|
|
424
|
+
|
|
425
|
+
| Optimization | Impact | Effort | Priority |
|
|
426
|
+
|--------------|--------|--------|----------|
|
|
427
|
+
| Row background clipping | 10-15% | Low | **HIGH** |
|
|
428
|
+
| Column position caching | 5-10% | Low | **HIGH** |
|
|
429
|
+
| Mouse event throttling | 50-80% fewer events | Low | **HIGH** |
|
|
430
|
+
| Hit testing O(1) | Instant | Low | **HIGH** |
|
|
431
|
+
|
|
432
|
+
### Phase 2: Medium Impact (2-3 days)
|
|
433
|
+
|
|
434
|
+
| Optimization | Impact | Effort | Priority |
|
|
435
|
+
|--------------|--------|--------|----------|
|
|
436
|
+
| Grid line pattern | 15-20% | Medium | MEDIUM |
|
|
437
|
+
| Damage tracking utilization | 30-50% | Medium | MEDIUM |
|
|
438
|
+
| Filter indexes | 10-100x | Medium | MEDIUM |
|
|
439
|
+
| Numeric sort optimization | 2-5x | Medium | MEDIUM |
|
|
440
|
+
|
|
441
|
+
### Phase 3: Advanced (3-5 days)
|
|
442
|
+
|
|
443
|
+
| Optimization | Impact | Effort | Priority |
|
|
444
|
+
|--------------|--------|--------|----------|
|
|
445
|
+
| Text rendering cache | 20-30% | High | LOW |
|
|
446
|
+
| Offscreen canvas for static cells | 30-40% | High | LOW |
|
|
447
|
+
| Web Worker for data processing | 2-10x | High | LOW |
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## 📊 Expected Performance After Optimizations
|
|
452
|
+
|
|
453
|
+
### Current vs Optimized
|
|
454
|
+
|
|
455
|
+
| Dataset | Current Load | Optimized Load | Improvement |
|
|
456
|
+
|---------|--------------|----------------|-------------|
|
|
457
|
+
| 100K rows | ~180ms | ~120ms | **33% faster** |
|
|
458
|
+
| 500K rows | ~800ms | ~400ms | **50% faster** |
|
|
459
|
+
| 1M rows | ~2s | ~800ms | **60% faster** |
|
|
460
|
+
|
|
461
|
+
| Dataset | Current FPS | Optimized FPS | Improvement |
|
|
462
|
+
|---------|-------------|---------------|-------------|
|
|
463
|
+
| 100K rows | 60fps | 60fps | Same |
|
|
464
|
+
| 500K rows | 55-60fps | 60fps | **Smoother** |
|
|
465
|
+
| 1M rows | 50-55fps | 55-60fps | **Smoother** |
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## 🛠️ Implementation Recommendations
|
|
470
|
+
|
|
471
|
+
### 1. Start with Phase 1 (Quick Wins)
|
|
472
|
+
|
|
473
|
+
These provide immediate performance gains with minimal code changes:
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
// 1. Row background clipping
|
|
477
|
+
this.ctx.fillRect(
|
|
478
|
+
this.scrollLeft, // Start from visible area
|
|
479
|
+
y,
|
|
480
|
+
this.viewportWidth,
|
|
481
|
+
rowHeight
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// 2. Column position caching
|
|
485
|
+
this.prepareColumnPositions(allVisibleColumns);
|
|
486
|
+
|
|
487
|
+
// 3. Mouse event throttling
|
|
488
|
+
this.canvas.addEventListener('mousemove', this.throttleMouseMove);
|
|
489
|
+
|
|
490
|
+
// 4. O(1) hit testing
|
|
491
|
+
const rowIndex = Math.floor((mouseY + scrollTop) / rowHeight);
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### 2. Profile Before and After
|
|
495
|
+
|
|
496
|
+
Use Chrome DevTools Performance tab:
|
|
497
|
+
|
|
498
|
+
```javascript
|
|
499
|
+
// Before optimization
|
|
500
|
+
console.profile('render-before');
|
|
501
|
+
renderer.renderFrame();
|
|
502
|
+
console.profileEnd();
|
|
503
|
+
|
|
504
|
+
// After optimization
|
|
505
|
+
console.profile('render-after');
|
|
506
|
+
renderer.renderFrame();
|
|
507
|
+
console.profileEnd();
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### 3. Add Performance Metrics
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
// Add to CanvasRenderer
|
|
514
|
+
getPerformanceMetrics(): {
|
|
515
|
+
frameTime: number;
|
|
516
|
+
renderedRows: number;
|
|
517
|
+
renderedCells: number;
|
|
518
|
+
damageRatio: number;
|
|
519
|
+
} {
|
|
520
|
+
return {
|
|
521
|
+
frameTime: this.lastRenderDuration,
|
|
522
|
+
renderedRows: this.lastRenderedRows,
|
|
523
|
+
renderedCells: this.lastRenderedCells,
|
|
524
|
+
damageRatio: this.damageTracker.getDamageRatio(),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
## 📈 Monitoring & Benchmarking
|
|
532
|
+
|
|
533
|
+
### Add Performance Dashboard to Demo
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
// Add to demo app
|
|
537
|
+
performanceMetrics = {
|
|
538
|
+
fps: 0,
|
|
539
|
+
frameTime: 0,
|
|
540
|
+
renderedRows: 0,
|
|
541
|
+
renderedCells: 0,
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
updateMetrics() {
|
|
545
|
+
this.performanceMetrics = {
|
|
546
|
+
fps: Math.round(1000 / this.gridComponent.lastFrameTime),
|
|
547
|
+
frameTime: this.gridComponent.lastFrameTime,
|
|
548
|
+
renderedRows: this.gridComponent.renderedRows,
|
|
549
|
+
renderedCells: this.gridComponent.renderedCells,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## ✅ Summary
|
|
557
|
+
|
|
558
|
+
**Current State:** ✅ **Good** - Meets performance targets
|
|
559
|
+
|
|
560
|
+
**Optimization Potential:** 📈 **30-60% improvement** possible
|
|
561
|
+
|
|
562
|
+
**Priority:** Start with **Phase 1 Quick Wins** for immediate gains
|
|
563
|
+
|
|
564
|
+
**Timeline:**
|
|
565
|
+
- Phase 1: 1-2 days
|
|
566
|
+
- Phase 2: 2-3 days
|
|
567
|
+
- Phase 3: 3-5 days (optional)
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
**Recommendation:** Implement Phase 1 optimizations now, then profile to identify next bottlenecks.
|