Skip to content
Open
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
143 changes: 143 additions & 0 deletions packages/modeling/bench/splitPolygon.bench.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Direct benchmark for splitPolygonByPlane - the hot path in boolean operations
*
* Run with: node --expose-gc bench/splitPolygon.bench.js
*/

const { sphere, torus } = require('../src/primitives')
const splitPolygonByPlane = require('../src/operations/booleans/trees/splitPolygonByPlane')
const poly3 = require('../src/geometries/poly3')
const plane = require('../src/maths/plane')

// Benchmark helper
const benchmark = (name, setup, fn, iterations = 1000) => {
const data = setup()

// Force GC if available
if (global.gc) global.gc()

// Warmup
for (let i = 0; i < 100; i++) fn(data)

if (global.gc) global.gc()
const heapBefore = process.memoryUsage().heapUsed

const start = process.hrtime.bigint()
for (let i = 0; i < iterations; i++) {
fn(data)
}
const end = process.hrtime.bigint()

if (global.gc) global.gc()
const heapAfter = process.memoryUsage().heapUsed

const totalNs = Number(end - start)
const avgNs = totalNs / iterations
const avgUs = avgNs / 1000
const heapDelta = (heapAfter - heapBefore) / 1024

console.log(`${name.padEnd(50)} ${avgUs.toFixed(2).padStart(8)} µs/op heap: ${heapDelta > 0 ? '+' : ''}${heapDelta.toFixed(0)} KB`)
return avgUs
}

console.log('='.repeat(80))
console.log('splitPolygonByPlane Direct Benchmark')
console.log('='.repeat(80))
console.log()

// Get polygons from a sphere
const testSphere = sphere({ radius: 5, segments: 32 })
const polygons = testSphere.polygons

console.log(`Test geometry: sphere(32) with ${polygons.length} polygons`)
console.log(`Average vertices per polygon: ${(polygons.reduce((sum, p) => sum + p.vertices.length, 0) / polygons.length).toFixed(1)}`)
console.log()

// Test 1: Coplanar case (fast path - type 0 or 1)
console.log('--- Coplanar Cases (fast path) ---')
benchmark('coplanar-front (type 0)', () => {
const polygon = polygons[0]
const pplane = poly3.plane(polygon)
return { polygon, splane: pplane }
}, (data) => splitPolygonByPlane(data.splane, data.polygon), 10000)

// Test 2: Entirely front (type 2)
console.log()
console.log('--- One-side Cases (no split needed) ---')
benchmark('entirely front (type 2)', () => {
const polygon = polygons[0]
// Create a plane far behind the polygon
const splane = [0, 0, 1, -100]
return { polygon, splane }
}, (data) => splitPolygonByPlane(data.splane, data.polygon), 10000)

benchmark('entirely back (type 3)', () => {
const polygon = polygons[0]
// Create a plane far in front of the polygon
const splane = [0, 0, 1, 100]
return { polygon, splane }
}, (data) => splitPolygonByPlane(data.splane, data.polygon), 10000)

// Test 3: Spanning case (type 4) - this is where allocations hurt
console.log()
console.log('--- Spanning Cases (allocations happen here) ---')
benchmark('spanning split (type 4) - triangle', () => {
// A triangle that will be split
const polygon = poly3.create([[0, 0, 0], [10, 0, 0], [5, 10, 0]])
const splane = [1, 0, 0, 5] // Split down the middle
return { polygon, splane }
}, (data) => splitPolygonByPlane(data.splane, data.polygon), 10000)

benchmark('spanning split (type 4) - quad', () => {
const polygon = poly3.create([[0, 0, 0], [10, 0, 0], [10, 10, 0], [0, 10, 0]])
const splane = [1, 0, 0, 5]
return { polygon, splane }
}, (data) => splitPolygonByPlane(data.splane, data.polygon), 10000)

benchmark('spanning split (type 4) - hexagon', () => {
const polygon = poly3.create([
[0, 0, 0], [5, 0, 0], [7.5, 4, 0],
[5, 8, 0], [0, 8, 0], [-2.5, 4, 0]
])
const splane = [1, 0, 0, 2.5]
return { polygon, splane }
}, (data) => splitPolygonByPlane(data.splane, data.polygon), 10000)

// Test 4: Realistic mix from actual boolean operation
console.log()
console.log('--- Realistic Mix (simulating boolean op) ---')
benchmark('mixed types from sphere polygons', () => {
// Use multiple polygons with a plane that hits various cases
const splane = [0.577, 0.577, 0.577, 0] // Diagonal plane through origin
return { polygons: polygons.slice(0, 50), splane }
}, (data) => {
let spanning = 0
for (const polygon of data.polygons) {
const result = splitPolygonByPlane(data.splane, polygon)
if (result.type === 4) spanning++
}
return spanning
}, 1000)

// Measure allocation overhead specifically
console.log()
console.log('--- Allocation Stress Test ---')
benchmark('10k spanning splits (allocation heavy)', () => {
const polygon = poly3.create([[0, 0, 0], [10, 0, 0], [10, 10, 0], [0, 10, 0]])
const splane = [1, 0, 0, 5]
return { polygon, splane }
}, (data) => {
for (let i = 0; i < 100; i++) {
splitPolygonByPlane(data.splane, data.polygon)
}
}, 100)

console.log()
console.log('='.repeat(80))
console.log('Benchmark complete')
console.log()
console.log('Note: "spanning split" cases allocate the most:')
console.log(' - result object { type, front, back }')
console.log(' - vertexIsBack[] array')
console.log(' - frontvertices[] and backvertices[] arrays')
console.log(' - Two new poly3 objects (front and back)')
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,26 @@ const splitLineSegmentByPlane = require('./splitLineSegmentByPlane')

const EPS_SQUARED = EPS * EPS

// Object pool for reducing allocations in hot path.
// The result object is safe to reuse because callers extract front/back immediately.
// The temporary arrays are only used within splitPolygonByPlane.
const pool = {
result: { type: null, front: null, back: null },
vertexIsBack: new Array(64), // Pre-allocated, grows if needed
frontvertices: new Array(32),
backvertices: new Array(32)
}

// Remove consecutive duplicate vertices from a polygon vertex list.
// Compares last vertex to first to handle wraparound.
// Returns a new array (does not modify input).
// IMPORTANT: Caller must ensure vertices.length >= 3 before calling.
const removeConsecutiveDuplicates = (vertices) => {
// Returns a new array (polygon vertices must be fresh arrays since they're stored in geometry).
// Accepts optional count parameter for use with pre-allocated pooled arrays.
const removeConsecutiveDuplicates = (vertices, count) => {
const vertexCount = count !== undefined ? count : vertices.length
if (vertexCount < 3) return vertices.slice(0, vertexCount)
const result = []
let prevvertex = vertices[vertices.length - 1]
for (let i = 0; i < vertices.length; i++) {
let prevvertex = vertices[vertexCount - 1]
for (let i = 0; i < vertexCount; i++) {
const vertex = vertices[i]
if (vec3.squaredDistance(vertex, prevvertex) >= EPS_SQUARED) {
result.push(vertex)
Expand All @@ -36,12 +48,15 @@ const removeConsecutiveDuplicates = (vertices) => {
// In case the polygon is spanning, returns:
// .front: a Polygon3 of the front part
// .back: a Polygon3 of the back part
//
// IMPORTANT: The returned object is reused between calls to reduce allocations.
// Callers must extract .front and .back before the next call to splitPolygonByPlane.
const splitPolygonByPlane = (splane, polygon) => {
const result = {
type: null,
front: null,
back: null
}
const result = pool.result
result.type = null
result.front = null
result.back = null

// cache in local lets (speedup):
const vertices = polygon.vertices
const numvertices = vertices.length
Expand All @@ -51,12 +66,16 @@ const splitPolygonByPlane = (splane, polygon) => {
} else {
let hasfront = false
let hasback = false
const vertexIsBack = []
// Use pooled array, grow if needed
let vertexIsBack = pool.vertexIsBack
if (vertexIsBack.length < numvertices) {
vertexIsBack = pool.vertexIsBack = new Array(numvertices * 2)
}
const MINEPS = -EPS
for (let i = 0; i < numvertices; i++) {
const t = vec3.dot(splane, vertices[i]) - splane[3]
const isback = (t < MINEPS)
vertexIsBack.push(isback)
vertexIsBack[i] = isback
if (t > EPS) hasfront = true
if (t < MINEPS) hasback = true
}
Expand All @@ -71,8 +90,19 @@ const splitPolygonByPlane = (splane, polygon) => {
} else {
// spanning
result.type = 4
const frontvertices = []
const backvertices = []
// Use pooled arrays, grow if needed (max 2 vertices added per original vertex)
const maxVerts = numvertices * 2
let frontvertices = pool.frontvertices
let backvertices = pool.backvertices
if (frontvertices.length < maxVerts) {
frontvertices = pool.frontvertices = new Array(maxVerts)
}
if (backvertices.length < maxVerts) {
backvertices = pool.backvertices = new Array(maxVerts)
}
let frontCount = 0
let backCount = 0

let isback = vertexIsBack[0]
for (let vertexindex = 0; vertexindex < numvertices; vertexindex++) {
const vertex = vertices[vertexindex]
Expand All @@ -82,35 +112,36 @@ const splitPolygonByPlane = (splane, polygon) => {
if (isback === nextisback) {
// line segment is on one side of the plane:
if (isback) {
backvertices.push(vertex)
backvertices[backCount++] = vertex
} else {
frontvertices.push(vertex)
frontvertices[frontCount++] = vertex
}
} else {
// line segment intersects plane:
const nextpoint = vertices[nextvertexindex]
const intersectionpoint = splitLineSegmentByPlane(splane, vertex, nextpoint)
if (isback) {
backvertices.push(vertex)
backvertices.push(intersectionpoint)
frontvertices.push(intersectionpoint)
backvertices[backCount++] = vertex
backvertices[backCount++] = intersectionpoint
frontvertices[frontCount++] = intersectionpoint
} else {
frontvertices.push(vertex)
frontvertices.push(intersectionpoint)
backvertices.push(intersectionpoint)
frontvertices[frontCount++] = vertex
frontvertices[frontCount++] = intersectionpoint
backvertices[backCount++] = intersectionpoint
}
}
isback = nextisback
} // for vertexindex
// remove consecutive duplicate vertices (check length before calling to avoid function overhead)
if (frontvertices.length >= 3) {
const frontFiltered = removeConsecutiveDuplicates(frontvertices)
// remove consecutive duplicate vertices and create final polygons
// We need fresh arrays for the polygon vertices since they become part of the geometry
if (frontCount >= 3) {
const frontFiltered = removeConsecutiveDuplicates(frontvertices, frontCount)
if (frontFiltered.length >= 3) {
result.front = poly3.fromPointsAndPlane(frontFiltered, pplane)
}
}
if (backvertices.length >= 3) {
const backFiltered = removeConsecutiveDuplicates(backvertices)
if (backCount >= 3) {
const backFiltered = removeConsecutiveDuplicates(backvertices, backCount)
if (backFiltered.length >= 3) {
result.back = poly3.fromPointsAndPlane(backFiltered, pplane)
}
Expand Down