vite-plugin-tsl-operator
Version:
A Vite plugin to let you use standard JS operators (+, -, *, /, %, >, <, ==, &&, ||, !) with TSL Nodes in your Three.js project.
168 lines (135 loc) • 5.72 kB
JavaScript
import { createRequire } from 'module'
import { performance } from 'perf_hooks'
import TSLOperatorPlugin from '../src/index.js'
const require = createRequire(import.meta.url)
const { parse } = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
// --- Test input generators ---
const makeExpressions = count => {
const ops = ['+', '-', '*', '/', '%']
const vars = ['a', 'b', 'c', 'd', 'uv', 'normal', 'position', 'color']
const lines = []
for (let i = 0; i < count; i++) {
const v1 = vars[i % vars.length]
const v2 = vars[(i + 1) % vars.length]
const op = ops[i % ops.length]
if (i % 5 === 0) {
lines.push(` const r${i} = ${v1} ${op} ${v2} ${op} float(${i + 1})`)
} else if (i % 5 === 1) {
lines.push(` ${v1} += ${v2}`)
} else if (i % 5 === 2) {
lines.push(` const r${i} = (${v1} ${op} 2) ${ops[(i + 2) % ops.length]} (${v2} ${op} 3)`)
} else if (i % 5 === 3) {
lines.push(` const r${i} = vec3(${v1} ${op} ${v2}, ${v2}, ${v1})`)
} else {
lines.push(` const r${i} = ${v1} ${op} ${v2}`)
}
}
return lines.join('\n')
}
const makeFnBlock = (exprCount, idx) => {
const params = ['a', 'b', 'c', 'd'].slice(0, Math.min(4, Math.max(2, idx % 4 + 1)))
return `const shader${idx} = Fn((${params.join(', ')}) => {\n${makeExpressions(exprCount)}\n return a\n})`
}
const makeInput = (fnCount, exprsPerFn) => {
const imports = `import { Fn, float, vec3, vec4 } from 'three/tsl'\n`
const blocks = []
for (let i = 0; i < fnCount; i++) {
blocks.push(makeFnBlock(exprsPerFn, i))
}
return imports + blocks.join('\n\n')
}
const parserPlugins = ['jsx', 'typescript', 'classProperties', 'decorators-legacy', 'importMeta', 'topLevelAwait']
// --- Profiling ---
const profilePhases = (code, iterations) => {
const timings = { parse: 0, directives: 0, traverse: 0, generate: 0, total: 0 }
// Warmup
const plugin = TSLOperatorPlugin({ logs: false })
for (let i = 0; i < 5; i++) plugin.transform(code, 'warmup.js')
for (let i = 0; i < iterations; i++) {
const t0 = performance.now()
// Phase 1: Parse
const p0 = performance.now()
const ast = parse(code, { sourceType: 'module', plugins: parserPlugins })
const p1 = performance.now()
timings.parse += p1 - p0
// Phase 2: Directives (inline — just scan for @tsl/@js)
const d0 = performance.now()
const lower = code.toLowerCase()
lower.includes('@tsl') || lower.includes('@js')
const d1 = performance.now()
timings.directives += d1 - d0
// Phase 3: Traverse (use the plugin's full transform for this, minus parse/generate)
const tr0 = performance.now()
traverse(ast, {
CallExpression(path) {
// Minimal check to match the plugin's behavior
if (!path.node.callee?.name || path.node.callee.name !== 'Fn') return
}
})
const tr1 = performance.now()
timings.traverse += tr1 - tr0
// Phase 4: Generate
const g0 = performance.now()
generate(ast, { retainLines: true }, code)
const g1 = performance.now()
timings.generate += g1 - g0
timings.total += performance.now() - t0
}
return timings
}
const profileFullTransform = (code, iterations) => {
const plugin = TSLOperatorPlugin({ logs: false })
// Warmup
for (let i = 0; i < 5; i++) plugin.transform(code, 'warmup.js')
let total = 0
for (let i = 0; i < iterations; i++) {
const t0 = performance.now()
plugin.transform(code, 'bench.js')
total += performance.now() - t0
}
return total
}
const formatMs = (ms) => ms.toFixed(3).padStart(8)
const formatPct = (pct) => (pct.toFixed(1) + '%').padStart(7)
const printResults = (label, timings, iterations) => {
const phases = ['parse', 'directives', 'traverse', 'generate']
const total = timings.total / iterations
console.log(`\n=== ${label} — avg over ${iterations} runs ===`)
console.log(`${'Phase'.padEnd(24)} ${'Avg (ms)'.padStart(10)} ${'% of total'.padStart(10)}`)
console.log('-'.repeat(46))
for (const phase of phases) {
const avg = timings[phase] / iterations
const pct = (avg / total) * 100
console.log(`${phase.padEnd(24)} ${formatMs(avg)} ${formatPct(pct)}`)
}
console.log('-'.repeat(46))
console.log(`${'TOTAL (phases)'.padEnd(24)} ${formatMs(total)} ${formatPct(100)}`)
}
// --- Run ---
const scenarios = [
{ label: 'Small (1 Fn, 5 expr)', fnCount: 1, exprs: 5, iterations: 500 },
{ label: 'Medium (5 Fn, 10 expr)', fnCount: 5, exprs: 10, iterations: 200 },
{ label: 'Large (20 Fn, 15 expr)', fnCount: 20, exprs: 15, iterations: 100 },
]
console.log('TSL Operator Plugin — Performance Profile')
console.log('==========================================')
for (const { label, fnCount, exprs, iterations } of scenarios) {
const code = makeInput(fnCount, exprs)
console.log(`\nInput: ${code.split('\n').length} lines, ${code.length} chars`)
// Phase breakdown (individual Babel calls)
const phaseTimings = profilePhases(code, iterations)
printResults(`${label} — Phase Breakdown`, phaseTimings, iterations)
// Full plugin transform (end-to-end including actual transformations)
const fullTime = profileFullTransform(code, iterations)
const avgFull = fullTime / iterations
console.log(`\n Full plugin transform: ${formatMs(avgFull)} ms/call`)
// Delta = time spent in actual TSL transformation logic
const phaseTotal = phaseTimings.total / iterations
const delta = avgFull - phaseTotal
if (delta > 0) {
console.log(` TSL transform overhead: ${formatMs(delta)} ms/call (${formatPct((delta / avgFull) * 100)} of full)`)
}
}
console.log('\n')