@gridspace/raster-path 1.0.3 → 1.0.4
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/README.md +3 -5
- package/build/app.js +363 -39
- package/build/index.html +39 -1
- package/build/raster-path.js +13 -13
- package/build/style.css +65 -0
- package/build/webgpu-worker.js +475 -686
- package/package.json +6 -2
- package/scripts/build-shaders.js +1 -1
- package/src/index.js +13 -13
- package/src/shaders/{radial-raster-v2.wgsl → radial-raster.wgsl} +8 -2
- package/src/test/batch-divisor-benchmark.cjs +286 -0
- package/src/test/lathe-cylinder-2-debug.cjs +334 -0
- package/src/test/lathe-cylinder-2-test.cjs +157 -0
- package/src/test/lathe-cylinder-test.cjs +198 -0
- package/src/test/work-estimation-profile.cjs +406 -0
- package/src/test/workload-calculator-demo.cjs +113 -0
- package/src/test/workload-calibration.cjs +310 -0
- package/src/web/app.js +363 -39
- package/src/web/index.html +39 -1
- package/src/web/style.css +65 -0
- package/src/web/webgpu-worker.js +470 -687
- package/src/workload-calculator.js +318 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gridspace/raster-path",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Terrain and Tool Raster Path Finder using WebGPU",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,11 @@
|
|
|
36
36
|
"test": "npm run test:planar && npm run test:radial",
|
|
37
37
|
"test:planar": "npm run build && npx electron src/test/planar-test.cjs",
|
|
38
38
|
"test:planar-tiling": "npm run build && npx electron src/test/planar-tiling-test.cjs",
|
|
39
|
-
"test:radial": "npm run build && npx electron src/test/radial-test.cjs"
|
|
39
|
+
"test:radial": "npm run build && npx electron src/test/radial-test.cjs",
|
|
40
|
+
"test:batch-divisor": "npm run build && npx electron src/test/batch-divisor-benchmark.cjs",
|
|
41
|
+
"test:work-estimation": "npm run build && npx electron src/test/work-estimation-profile.cjs",
|
|
42
|
+
"test:workload-calibration": "npm run build && npx electron src/test/workload-calibration.cjs",
|
|
43
|
+
"test:lathe-cylinder-2-debug": "npm run build && npx electron src/test/lathe-cylinder-2-debug.cjs"
|
|
40
44
|
},
|
|
41
45
|
"keywords": [
|
|
42
46
|
"cnc",
|
package/scripts/build-shaders.js
CHANGED
|
@@ -21,7 +21,7 @@ const WORKER_DEST = path.join(BUILD_DIR, 'webgpu-worker.js');
|
|
|
21
21
|
let workerCode = fs.readFileSync(WORKER_SRC, 'utf8');
|
|
22
22
|
|
|
23
23
|
// Find all shader placeholders
|
|
24
|
-
const shaderRegex =
|
|
24
|
+
const shaderRegex = /'SHADER:([a-z0-9-]+)'/g;
|
|
25
25
|
let match;
|
|
26
26
|
const replacements = [];
|
|
27
27
|
|
package/src/index.js
CHANGED
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
* @property {number} resolution - Grid step size in mm (required)
|
|
56
56
|
* @property {number} rotationStep - Radial mode only: degrees between rays (e.g., 1.0 = 360 rays)
|
|
57
57
|
* @property {number} trianglesPerTile - Target triangles per tile for radial rasterization (default: calculated)
|
|
58
|
+
* @property {number} batchDivisor - Testing parameter to artificially divide batch size (default: 1)
|
|
58
59
|
* @property {boolean} debug - Enable debug logging (default: false)
|
|
59
60
|
* @property {boolean} quiet - Suppress log output (default: false)
|
|
60
61
|
*/
|
|
@@ -103,18 +104,13 @@ export class RasterPath {
|
|
|
103
104
|
this.deviceCapabilities = null;
|
|
104
105
|
|
|
105
106
|
// Configure debug output
|
|
106
|
-
let urlOpt = [];
|
|
107
107
|
if (config.quiet) {
|
|
108
108
|
debug.log = function() {};
|
|
109
|
-
urlOpt.push('quiet');
|
|
110
|
-
}
|
|
111
|
-
if (config.debug) {
|
|
112
|
-
urlOpt.push('debug');
|
|
113
109
|
}
|
|
114
110
|
|
|
115
111
|
// Configuration with defaults
|
|
116
112
|
this.config = {
|
|
117
|
-
workerName:
|
|
113
|
+
workerName: config.workerName ?? "webgpu-worker.js",
|
|
118
114
|
maxGPUMemoryMB: config.maxGPUMemoryMB ?? 256,
|
|
119
115
|
gpuMemorySafetyMargin: config.gpuMemorySafetyMargin ?? 0.8,
|
|
120
116
|
autoTiling: config.autoTiling ?? true,
|
|
@@ -122,7 +118,12 @@ export class RasterPath {
|
|
|
122
118
|
maxConcurrentTiles: config.maxConcurrentTiles ?? 10,
|
|
123
119
|
trianglesPerTile: config.trianglesPerTile, // undefined = auto-calculate
|
|
124
120
|
radialRotationOffset: config.radialRotationOffset ?? 0, // degrees
|
|
121
|
+
batchDivisor: config.batchDivisor ?? 1, // For testing batching overhead
|
|
122
|
+
debug: config.debug,
|
|
123
|
+
quiet: config.quiet
|
|
125
124
|
};
|
|
125
|
+
|
|
126
|
+
debug.log('config', this.config);
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
/**
|
|
@@ -238,8 +239,8 @@ export class RasterPath {
|
|
|
238
239
|
const originalBounds = boundsOverride || this.#calculateBounds(triangles);
|
|
239
240
|
|
|
240
241
|
// Center model in YZ plane (required for radial rasterization)
|
|
241
|
-
// Radial mode casts rays from
|
|
242
|
-
//
|
|
242
|
+
// Radial mode casts rays from max_radius distance inward toward the X-axis,
|
|
243
|
+
// and centering ensures the geometry is symmetric around the rotation axis
|
|
243
244
|
const centerY = (originalBounds.min.y + originalBounds.max.y) / 2;
|
|
244
245
|
const centerZ = (originalBounds.min.z + originalBounds.max.z) / 2;
|
|
245
246
|
|
|
@@ -273,12 +274,10 @@ export class RasterPath {
|
|
|
273
274
|
* @param {number} params.xStep - Sample every Nth point in X direction
|
|
274
275
|
* @param {number} params.yStep - Sample every Nth point in Y direction
|
|
275
276
|
* @param {number} params.zFloor - Z floor value for out-of-bounds areas
|
|
276
|
-
* @param {number} params.radiusOffset - (Radial mode only) Distance from terrain surface to tool tip in mm.
|
|
277
|
-
* Used to calculate radial collision offset. Default: 20mm
|
|
278
277
|
* @param {function} params.onProgress - Optional progress callback (progress: number, info?: string) => void
|
|
279
278
|
* @returns {Promise<object>} Planar: {pathData, width, height} | Radial: {strips[], numStrips, totalPoints}
|
|
280
279
|
*/
|
|
281
|
-
async generateToolpaths({ xStep, yStep, zFloor,
|
|
280
|
+
async generateToolpaths({ xStep, yStep, zFloor, onProgress }) {
|
|
282
281
|
if (!this.isInitialized) {
|
|
283
282
|
throw new Error('RasterPath not initialized. Call init() first.');
|
|
284
283
|
}
|
|
@@ -287,6 +286,8 @@ export class RasterPath {
|
|
|
287
286
|
throw new Error('Tool not loaded. Call loadTool() first.');
|
|
288
287
|
}
|
|
289
288
|
|
|
289
|
+
debug.log('gen.paths', { xStep, yStep, zFloor });
|
|
290
|
+
|
|
290
291
|
if (this.mode === 'planar') {
|
|
291
292
|
if (!this.terrainData) {
|
|
292
293
|
throw new Error('Terrain not loaded. Call loadTerrain() first.');
|
|
@@ -444,8 +445,7 @@ export class RasterPath {
|
|
|
444
445
|
zFloor: zFloor,
|
|
445
446
|
bounds,
|
|
446
447
|
xStep,
|
|
447
|
-
yStep
|
|
448
|
-
gridStep: this.resolution
|
|
448
|
+
yStep
|
|
449
449
|
},
|
|
450
450
|
'radial-toolpaths-complete',
|
|
451
451
|
completionHandler
|
|
@@ -17,6 +17,7 @@ struct Uniforms {
|
|
|
17
17
|
filter_mode: u32, // 0 = max Z (terrain), 1 = min Z (tool)
|
|
18
18
|
num_buckets: u32, // Total number of X-buckets
|
|
19
19
|
start_angle: f32, // Starting angle offset in radians (for batching)
|
|
20
|
+
bucket_offset: u32, // Offset for bucket batching (bucket_idx in batch writes to bucket_offset + bucket_idx in output)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
struct BucketInfo {
|
|
@@ -125,6 +126,10 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
|
125
126
|
|
|
126
127
|
// Step 2: Rotate position (scan_x, scan_y, scan_z) around X-axis by 'angle'
|
|
127
128
|
// X stays the same, rotate YZ plane: y' = y*cos - z*sin, z' = y*sin + z*cos
|
|
129
|
+
// NOTE: This uses right-handed rotation (positive angle rotates +Y towards +Z)
|
|
130
|
+
// To reverse rotation direction (left-handed or opposite), flip signs:
|
|
131
|
+
// y' = y*cos + z*sin (flip sign on z term)
|
|
132
|
+
// z' = -y*sin + z*cos (flip sign on y term)
|
|
128
133
|
let ray_origin_x = scan_x;
|
|
129
134
|
let ray_origin_y = scan_y * cos(angle) - scan_z * sin(angle);
|
|
130
135
|
let ray_origin_z = scan_y * sin(angle) + scan_z * cos(angle);
|
|
@@ -132,6 +137,7 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
|
132
137
|
|
|
133
138
|
// Step 3: Rotate ray direction (0, 0, -1) around X-axis by 'angle'
|
|
134
139
|
// X component stays 0, rotate YZ: dy = 0*cos - (-1)*sin = sin, dz = 0*sin + (-1)*cos = -cos
|
|
140
|
+
// NOTE: For reversed rotation, use: vec3<f32>(0.0, -sin(angle), -cos(angle))
|
|
135
141
|
let ray_dir = vec3<f32>(0.0, sin(angle), -cos(angle));
|
|
136
142
|
|
|
137
143
|
// Initialize best distance (closest hit)
|
|
@@ -174,11 +180,11 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
// Write output
|
|
177
|
-
// Layout: bucket_idx * numAngles * bucketWidth * gridHeight
|
|
183
|
+
// Layout: (bucket_offset + bucket_idx) * numAngles * bucketWidth * gridHeight
|
|
178
184
|
// + angle_idx * bucketWidth * gridHeight
|
|
179
185
|
// + grid_y * bucketWidth
|
|
180
186
|
// + local_x
|
|
181
|
-
let output_idx = bucket_idx * uniforms.num_angles * uniforms.bucket_grid_width * uniforms.grid_y_height
|
|
187
|
+
let output_idx = (uniforms.bucket_offset + bucket_idx) * uniforms.num_angles * uniforms.bucket_grid_width * uniforms.grid_y_height
|
|
182
188
|
+ angle_idx * uniforms.bucket_grid_width * uniforms.grid_y_height
|
|
183
189
|
+ grid_y * uniforms.bucket_grid_width
|
|
184
190
|
+ local_x;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// batch-divisor-benchmark.cjs
|
|
2
|
+
// Benchmark test to measure batching overhead with different batch divisors
|
|
3
|
+
// Usage: node batch-divisor-benchmark.cjs [divisor1,divisor2,...]
|
|
4
|
+
// Example: node batch-divisor-benchmark.cjs 1,2,4,8,16,32
|
|
5
|
+
|
|
6
|
+
const { app, BrowserWindow } = require('electron');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
const OUTPUT_DIR = path.join(__dirname, '../../test-output');
|
|
11
|
+
const RESULTS_FILE = path.join(OUTPUT_DIR, 'batch-divisor-results.json');
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
14
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Parse batch divisors from command line args or use defaults
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const BATCH_DIVISORS = args.length > 0
|
|
20
|
+
? args[0].split(',').map(n => parseInt(n.trim()))
|
|
21
|
+
: [1, 2, 4, 8, 16, 32];
|
|
22
|
+
|
|
23
|
+
console.log('=== Batch Divisor Benchmark ===');
|
|
24
|
+
console.log('Testing with divisors:', BATCH_DIVISORS.join(', '));
|
|
25
|
+
console.log('');
|
|
26
|
+
|
|
27
|
+
let mainWindow;
|
|
28
|
+
let currentDivisorIndex = 0;
|
|
29
|
+
const results = [];
|
|
30
|
+
|
|
31
|
+
function createWindow() {
|
|
32
|
+
mainWindow = new BrowserWindow({
|
|
33
|
+
width: 1200,
|
|
34
|
+
height: 800,
|
|
35
|
+
show: false,
|
|
36
|
+
webPreferences: {
|
|
37
|
+
nodeIntegration: false,
|
|
38
|
+
contextIsolation: true,
|
|
39
|
+
enableBlinkFeatures: 'WebGPU',
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const htmlPath = path.join(__dirname, '../../build/index.html');
|
|
44
|
+
mainWindow.loadFile(htmlPath);
|
|
45
|
+
|
|
46
|
+
mainWindow.webContents.on('did-finish-load', async () => {
|
|
47
|
+
console.log('✓ Page loaded');
|
|
48
|
+
await runNextTest();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Capture console output from renderer process
|
|
52
|
+
mainWindow.webContents.on('console-message', (event, level, message) => {
|
|
53
|
+
// Filter for our timing logs
|
|
54
|
+
if (message.includes('Batch') && message.includes('timing:')) {
|
|
55
|
+
console.log('[TIMING]', message);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function runNextTest() {
|
|
61
|
+
if (currentDivisorIndex >= BATCH_DIVISORS.length) {
|
|
62
|
+
// All tests complete - analyze and report
|
|
63
|
+
await analyzeResults();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const divisor = BATCH_DIVISORS[currentDivisorIndex];
|
|
68
|
+
console.log(`\n${'='.repeat(60)}`);
|
|
69
|
+
console.log(`Testing with BATCH_DIVISOR = ${divisor}`);
|
|
70
|
+
console.log('='.repeat(60));
|
|
71
|
+
|
|
72
|
+
const testScript = `
|
|
73
|
+
(async function() {
|
|
74
|
+
const divisor = ${divisor};
|
|
75
|
+
|
|
76
|
+
if (!navigator.gpu) {
|
|
77
|
+
return { error: 'WebGPU not available' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Import RasterPath
|
|
81
|
+
const { RasterPath } = await import('./raster-path.js');
|
|
82
|
+
|
|
83
|
+
// Load STL files (same as radial-test.cjs - large enough to require batching)
|
|
84
|
+
const terrainResponse = await fetch('../benchmark/fixtures/terrain.stl');
|
|
85
|
+
const terrainBuffer = await terrainResponse.arrayBuffer();
|
|
86
|
+
|
|
87
|
+
const toolResponse = await fetch('../benchmark/fixtures/tool.stl');
|
|
88
|
+
const toolBuffer = await toolResponse.arrayBuffer();
|
|
89
|
+
|
|
90
|
+
// Parse STL files
|
|
91
|
+
function parseBinarySTL(buffer) {
|
|
92
|
+
const dataView = new DataView(buffer);
|
|
93
|
+
const numTriangles = dataView.getUint32(80, true);
|
|
94
|
+
const positions = new Float32Array(numTriangles * 9);
|
|
95
|
+
let offset = 84;
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < numTriangles; i++) {
|
|
98
|
+
offset += 12; // Skip normal
|
|
99
|
+
for (let j = 0; j < 9; j++) {
|
|
100
|
+
positions[i * 9 + j] = dataView.getFloat32(offset, true);
|
|
101
|
+
offset += 4;
|
|
102
|
+
}
|
|
103
|
+
offset += 2; // Skip attribute byte count
|
|
104
|
+
}
|
|
105
|
+
return positions;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const terrainTriangles = parseBinarySTL(terrainBuffer);
|
|
109
|
+
const toolTriangles = parseBinarySTL(toolBuffer);
|
|
110
|
+
|
|
111
|
+
// Test parameters - configured to require batching
|
|
112
|
+
const resolution = 0.05; // 0.05mm - finer resolution to force batching
|
|
113
|
+
const rotationStep = 1.0; // 1 degree between rays = 360 angles
|
|
114
|
+
const xStep = 1;
|
|
115
|
+
const yStep = 1;
|
|
116
|
+
const zFloor = 0;
|
|
117
|
+
const radiusOffset = 20;
|
|
118
|
+
|
|
119
|
+
console.log('Test parameters:');
|
|
120
|
+
console.log(' Resolution:', resolution, 'mm');
|
|
121
|
+
console.log(' Rotation step:', rotationStep, '°');
|
|
122
|
+
console.log(' Batch divisor:', divisor);
|
|
123
|
+
|
|
124
|
+
// Create RasterPath instance with specific batch divisor
|
|
125
|
+
const raster = new RasterPath({
|
|
126
|
+
mode: 'radial',
|
|
127
|
+
resolution: resolution,
|
|
128
|
+
rotationStep: rotationStep,
|
|
129
|
+
batchDivisor: divisor
|
|
130
|
+
});
|
|
131
|
+
await raster.init();
|
|
132
|
+
|
|
133
|
+
// Load tool
|
|
134
|
+
const t0 = performance.now();
|
|
135
|
+
await raster.loadTool({ triangles: toolTriangles });
|
|
136
|
+
const toolTime = performance.now() - t0;
|
|
137
|
+
|
|
138
|
+
// Load terrain
|
|
139
|
+
const t1 = performance.now();
|
|
140
|
+
await raster.loadTerrain({ triangles: terrainTriangles, zFloor: zFloor });
|
|
141
|
+
const terrainTime = performance.now() - t1;
|
|
142
|
+
|
|
143
|
+
// Generate toolpaths (this is where batching happens)
|
|
144
|
+
const t2 = performance.now();
|
|
145
|
+
const toolpathData = await raster.generateToolpaths({
|
|
146
|
+
xStep: xStep,
|
|
147
|
+
yStep: yStep,
|
|
148
|
+
zFloor: zFloor,
|
|
149
|
+
radiusOffset: radiusOffset
|
|
150
|
+
});
|
|
151
|
+
const toolpathTime = performance.now() - t2;
|
|
152
|
+
|
|
153
|
+
// Cleanup
|
|
154
|
+
raster.terminate();
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
success: true,
|
|
158
|
+
divisor: divisor,
|
|
159
|
+
timing: {
|
|
160
|
+
tool: toolTime,
|
|
161
|
+
terrain: terrainTime,
|
|
162
|
+
toolpath: toolpathTime,
|
|
163
|
+
total: toolTime + terrainTime + toolpathTime
|
|
164
|
+
},
|
|
165
|
+
result: {
|
|
166
|
+
numStrips: toolpathData.numStrips,
|
|
167
|
+
totalPoints: toolpathData.totalPoints
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
})();
|
|
171
|
+
`;
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const result = await mainWindow.webContents.executeJavaScript(testScript);
|
|
175
|
+
|
|
176
|
+
if (result.error) {
|
|
177
|
+
console.error('❌ Test failed:', result.error);
|
|
178
|
+
app.exit(1);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
results.push(result);
|
|
183
|
+
|
|
184
|
+
console.log(`\nResults for BATCH_DIVISOR = ${divisor}:`);
|
|
185
|
+
console.log(` Tool load time: ${result.timing.tool.toFixed(1)}ms`);
|
|
186
|
+
console.log(` Terrain load time: ${result.timing.terrain.toFixed(1)}ms`);
|
|
187
|
+
console.log(` Toolpath time: ${result.timing.toolpath.toFixed(1)}ms`);
|
|
188
|
+
console.log(` Total time: ${result.timing.total.toFixed(1)}ms`);
|
|
189
|
+
console.log(` Strips generated: ${result.result.numStrips}`);
|
|
190
|
+
console.log(` Total points: ${result.result.totalPoints}`);
|
|
191
|
+
|
|
192
|
+
// Move to next test
|
|
193
|
+
currentDivisorIndex++;
|
|
194
|
+
|
|
195
|
+
// Small delay before next test to ensure clean state
|
|
196
|
+
setTimeout(() => runNextTest(), 1000);
|
|
197
|
+
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('Error running test:', error);
|
|
200
|
+
app.exit(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function analyzeResults() {
|
|
205
|
+
console.log('\n' + '='.repeat(60));
|
|
206
|
+
console.log('ANALYSIS');
|
|
207
|
+
console.log('='.repeat(60));
|
|
208
|
+
|
|
209
|
+
// Save raw results
|
|
210
|
+
const resultsData = {
|
|
211
|
+
timestamp: new Date().toISOString(),
|
|
212
|
+
divisors: BATCH_DIVISORS,
|
|
213
|
+
results: results
|
|
214
|
+
};
|
|
215
|
+
fs.writeFileSync(RESULTS_FILE, JSON.stringify(resultsData, null, 2));
|
|
216
|
+
console.log(`\n✓ Raw results saved to: ${RESULTS_FILE}`);
|
|
217
|
+
|
|
218
|
+
// Analyze overhead
|
|
219
|
+
console.log('\n--- Timing Comparison ---');
|
|
220
|
+
const baseline = results[0]; // Divisor = 1
|
|
221
|
+
console.log(`\nBaseline (divisor=1): ${baseline.timing.total.toFixed(1)}ms total`);
|
|
222
|
+
console.log(` Breakdown: ${baseline.timing.tool.toFixed(1)}ms tool + ${baseline.timing.terrain.toFixed(1)}ms terrain + ${baseline.timing.toolpath.toFixed(1)}ms toolpath`);
|
|
223
|
+
|
|
224
|
+
console.log('\nOverhead Analysis:');
|
|
225
|
+
console.log('┌──────────┬───────────┬────────────┬──────────────┬──────────────┐');
|
|
226
|
+
console.log('│ Divisor │ Total (ms)│ vs Baseline│ Overhead (ms)│ Overhead (%) │');
|
|
227
|
+
console.log('├──────────┼───────────┼────────────┼──────────────┼──────────────┤');
|
|
228
|
+
|
|
229
|
+
for (const result of results) {
|
|
230
|
+
const overhead = result.timing.total - baseline.timing.total;
|
|
231
|
+
const overheadPercent = ((overhead / baseline.timing.total) * 100);
|
|
232
|
+
const comparison = result.divisor === 1 ? 'baseline' : `+${overhead.toFixed(0)}ms`;
|
|
233
|
+
|
|
234
|
+
console.log(
|
|
235
|
+
`│ ${String(result.divisor).padEnd(8)} │ ` +
|
|
236
|
+
`${result.timing.total.toFixed(1).padStart(9)} │ ` +
|
|
237
|
+
`${comparison.padStart(10)} │ ` +
|
|
238
|
+
`${overhead.toFixed(1).padStart(12)} │ ` +
|
|
239
|
+
`${overheadPercent.toFixed(1).padStart(12)}% │`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
console.log('└──────────┴───────────┴────────────┴──────────────┴──────────────┘');
|
|
243
|
+
|
|
244
|
+
// Calculate per-batch overhead
|
|
245
|
+
if (results.length > 1) {
|
|
246
|
+
console.log('\n--- Per-Batch Overhead Estimation ---');
|
|
247
|
+
// Assume divisor creates divisor times more batches
|
|
248
|
+
// So divisor=2 creates 2x batches, divisor=4 creates 4x batches, etc.
|
|
249
|
+
for (let i = 1; i < results.length; i++) {
|
|
250
|
+
const result = results[i];
|
|
251
|
+
const extraBatches = result.divisor - 1; // Assuming baseline has 1 effective batch unit
|
|
252
|
+
const overhead = result.timing.total - baseline.timing.total;
|
|
253
|
+
const perBatchOverhead = overhead / (result.divisor - 1);
|
|
254
|
+
|
|
255
|
+
console.log(`Divisor ${result.divisor}: ${overhead.toFixed(1)}ms overhead / ${extraBatches} extra batch(es) ≈ ${perBatchOverhead.toFixed(1)}ms per batch boundary`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Recommendations
|
|
260
|
+
console.log('\n--- Recommendations ---');
|
|
261
|
+
const maxResult = results[results.length - 1];
|
|
262
|
+
const maxOverheadPercent = ((maxResult.timing.total - baseline.timing.total) / baseline.timing.total) * 100;
|
|
263
|
+
|
|
264
|
+
if (maxOverheadPercent < 15) {
|
|
265
|
+
console.log('✓ LOW OVERHEAD (<15%): Batching overhead is acceptable.');
|
|
266
|
+
console.log(' Focus on other optimizations (shader efficiency, toolpath generation).');
|
|
267
|
+
} else if (maxOverheadPercent < 30) {
|
|
268
|
+
console.log('⚠ MEDIUM OVERHEAD (15-30%): Consider batch size tuning.');
|
|
269
|
+
console.log(' Investigate buffer creation/destruction costs.');
|
|
270
|
+
console.log(' Consider reusing buffers across batches.');
|
|
271
|
+
} else {
|
|
272
|
+
console.log('⚠ HIGH OVERHEAD (>30%): Priority optimization needed!');
|
|
273
|
+
console.log(' Critical to reduce batch overhead before increasing batch count.');
|
|
274
|
+
console.log(' Primary suspects:');
|
|
275
|
+
console.log(' - createReusableToolpathBuffers() per batch');
|
|
276
|
+
console.log(' - destroyReusableToolpathBuffers() per batch');
|
|
277
|
+
console.log(' - GPU context switching between batches');
|
|
278
|
+
console.log(' Recommendation: Implement buffer pooling or batch-level buffer reuse.');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log('\n✅ Benchmark complete!');
|
|
282
|
+
app.exit(0);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
app.whenReady().then(createWindow);
|
|
286
|
+
app.on('window-all-closed', () => app.quit());
|