Files
tools/vue/primitives/src/internal/spline/__test__/Spline.bench.ts
T
robonen 661a55719e perf(primitives): add performance audit report and vitest bench baselines
Library-wide Vue+V8 perf/leak audit (PERF_AUDIT.md) plus bench baselines for the
hot-path modules (timeline, curve-editor, spline, pointer-drag, collection, etc.).
2026-06-15 16:54:28 +07:00

320 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { bench, describe } from 'vitest';
import {
buildBezierPath,
buildPolylinePath,
buildSmoothPath,
catmullRom,
cubicBezier1D,
cubicBezierTangent,
evalCubicBezier,
linearInterpolate,
monotoneCubic,
sampleFnToPolyline,
sampleToPolyline,
solveBezierX,
toLUT,
} from '../index';
import type { Point } from '../index';
// ---------------------------------------------------------------------------
// Deterministic fixtures (NO Math.random). Values are seeded by index/formula.
// These mirror realistic usage: knot lists for tone/gamma/levels curves, the
// 4 control points of a CSS easing bezier, and pointer coordinates for a drag.
// ---------------------------------------------------------------------------
/** Build `n` knots strictly ascending in x (required by the interpolants). */
function makeKnots(n: number): Point[] {
const out: Point[] = Array.from({ length: n });
for (let i = 0; i < n; i++) {
const x = i / (n - 1); // 0..1, strictly increasing
// A wobbly-but-monotone-ish y so monotoneCubic does real tangent work.
const y = 0.5 + 0.45 * Math.sin(i * 0.7) * (1 - i / (n * 4));
out[i] = { x, y };
}
return out;
}
/** Build `n` 2D points for Catmull-Rom / smooth-path (x need not be sorted). */
function makePath(n: number): Point[] {
const out: Point[] = Array.from({ length: n });
for (let i = 0; i < n; i++) {
const a = i * 0.37;
out[i] = { x: i * 4, y: 120 + 80 * Math.sin(a) + 30 * Math.cos(a * 2.1) };
}
return out;
}
/** Evenly spaced sample parameters in [0,1] for repeated evaluation. */
function makeParams(n: number): number[] {
const out: number[] = Array.from({ length: n });
for (let i = 0; i < n; i++)
out[i] = i / (n - 1);
return out;
}
const knots100 = makeKnots(100);
const knots1000 = makeKnots(1000);
const path50 = makePath(50);
const path500 = makePath(500);
const params100 = makeParams(100);
const params1000 = makeParams(1000);
// CSS "ease" control points — the canonical solveBezierX workload.
const EASE = { x1: 0.42, y1: 0, x2: 0.58, y2: 1 } as const;
// A single cubic bezier segment (e.g. one connector / handle).
const B0: Point = { x: 0, y: 0 };
const B1: Point = { x: 0.25, y: 1 };
const B2: Point = { x: 0.75, y: 0 };
const B3: Point = { x: 1, y: 1 };
// Pre-built smooth-path control points used by the simulated pointer-move bench.
const dragPath = makePath(64);
// ---------------------------------------------------------------------------
// evalCubicBezier — point on a cubic bezier, evaluated over many parameters.
// ---------------------------------------------------------------------------
describe('evalCubicBezier — sweep t', () => {
bench('100 params', () => {
for (let i = 0; i < params100.length; i++)
evalCubicBezier(B0, B1, B2, B3, params100[i]!);
});
bench('1000 params', () => {
for (let i = 0; i < params1000.length; i++)
evalCubicBezier(B0, B1, B2, B3, params1000[i]!);
});
});
// ---------------------------------------------------------------------------
// cubicBezierTangent — derivative/handle direction, evaluated over many t.
// ---------------------------------------------------------------------------
describe('cubicBezierTangent — sweep t', () => {
bench('100 params', () => {
for (let i = 0; i < params100.length; i++)
cubicBezierTangent(B0, B1, B2, B3, params100[i]!);
});
bench('1000 params', () => {
for (let i = 0; i < params1000.length; i++)
cubicBezierTangent(B0, B1, B2, B3, params1000[i]!);
});
});
// ---------------------------------------------------------------------------
// solveBezierX — Newton-Raphson easing solver (x → eased y). The hot path for
// every animation frame that maps progress through a CSS cubic-bezier easing.
// `cubicBezier1D` benched alongside as the scalar primitive it builds on.
// ---------------------------------------------------------------------------
describe('solveBezierX — ease (x→y)', () => {
bench('100 params', () => {
for (let i = 0; i < params100.length; i++)
solveBezierX(EASE.x1, EASE.y1, EASE.x2, EASE.y2, params100[i]!);
});
bench('1000 params', () => {
for (let i = 0; i < params1000.length; i++)
solveBezierX(EASE.x1, EASE.y1, EASE.x2, EASE.y2, params1000[i]!);
});
// Identity easing (x1===y1 && x2===y2) short-circuits — establishes the floor.
bench('1000 params — identity short-circuit', () => {
for (let i = 0; i < params1000.length; i++)
solveBezierX(0, 0, 1, 1, params1000[i]!);
});
});
describe('cubicBezier1D — scalar Bernstein', () => {
bench('1000 params', () => {
for (let i = 0; i < params1000.length; i++)
cubicBezier1D(0, EASE.x1, EASE.x2, 1, params1000[i]!);
});
});
// ---------------------------------------------------------------------------
// catmullRom — spline through N knots, evaluated over many t. Two axes vary:
// knot count (segment locating cost) and sample count (per-frame evaluation).
// ---------------------------------------------------------------------------
describe('catmullRom — sweep t', () => {
bench('50 knots × 100 params', () => {
for (let i = 0; i < params100.length; i++)
catmullRom(path50, params100[i]!);
});
bench('500 knots × 100 params', () => {
for (let i = 0; i < params100.length; i++)
catmullRom(path500, params100[i]!);
});
bench('500 knots × 1000 params', () => {
for (let i = 0; i < params1000.length; i++)
catmullRom(path500, params1000[i]!);
});
bench('500 knots × 1000 params — closed', () => {
for (let i = 0; i < params1000.length; i++)
catmullRom(path500, params1000[i]!, { closed: true });
});
});
// ---------------------------------------------------------------------------
// monotoneCubic — Fritsch-Carlson build (one-time, O(n)) then repeated lookup
// (binary-search per call). This is the real tone/gamma-curve workload: build
// the interpolant once when knots change, then apply it across a LUT every
// frame. Split into "build" and "build + apply 256-LUT" benches.
// ---------------------------------------------------------------------------
describe('monotoneCubic — build', () => {
bench('100 knots', () => {
monotoneCubic(knots100);
});
bench('1000 knots', () => {
monotoneCubic(knots1000);
});
});
describe('monotoneCubic — apply (pre-built fn)', () => {
const f100 = monotoneCubic(knots100);
const f1000 = monotoneCubic(knots1000);
bench('100 knots → 256-LUT', () => {
toLUT(f100, 256);
});
bench('1000 knots → 256-LUT', () => {
toLUT(f1000, 256);
});
// 8-bit channel LUT — the full per-channel color-correction pass.
bench('1000 knots → 1024-LUT', () => {
toLUT(f1000, 1024);
});
});
describe('monotoneCubic — build + apply (knots changed)', () => {
bench('100 knots → build + 256-LUT', () => {
toLUT(monotoneCubic(knots100), 256);
});
bench('1000 knots → build + 256-LUT', () => {
toLUT(monotoneCubic(knots1000), 256);
});
});
// ---------------------------------------------------------------------------
// linearInterpolate — binary-search piecewise-linear lookup over many queries.
// ---------------------------------------------------------------------------
describe('linearInterpolate — query sweep', () => {
bench('100 knots × 1000 queries', () => {
for (let i = 0; i < params1000.length; i++)
linearInterpolate(knots100, params1000[i]!);
});
bench('1000 knots × 1000 queries', () => {
for (let i = 0; i < params1000.length; i++)
linearInterpolate(knots1000, params1000[i]!);
});
});
// ---------------------------------------------------------------------------
// Polyline sampling — turn a parametric/scalar curve into render-ready points.
// ---------------------------------------------------------------------------
describe('sampleToPolyline — bezier curve', () => {
const curve = (t: number): Point => evalCubicBezier(B0, B1, B2, B3, t);
bench('100 segments', () => {
sampleToPolyline(curve, 100);
});
bench('1000 segments', () => {
sampleToPolyline(curve, 1000);
});
});
describe('sampleFnToPolyline — monotone curve', () => {
const f = monotoneCubic(knots100);
bench('100 segments', () => {
sampleFnToPolyline(f, 0, 1, 100);
});
bench('1000 segments', () => {
sampleFnToPolyline(f, 0, 1, 1000);
});
});
// ---------------------------------------------------------------------------
// SVG path string building — runs on every reactive re-render of a curve.
// ---------------------------------------------------------------------------
describe('buildPolylinePath — string concat', () => {
const poly100 = sampleToPolyline((t: number) => evalCubicBezier(B0, B1, B2, B3, t), 100);
const poly1000 = sampleToPolyline((t: number) => evalCubicBezier(B0, B1, B2, B3, t), 1000);
bench('100 points', () => {
buildPolylinePath(poly100);
});
bench('1000 points', () => {
buildPolylinePath(poly1000);
});
});
describe('buildSmoothPath — Catmull-Rom cubics', () => {
bench('50 points', () => {
buildSmoothPath(path50);
});
bench('500 points', () => {
buildSmoothPath(path500);
});
bench('500 points — tension 0.5', () => {
buildSmoothPath(path500, 0.5);
});
});
describe('buildBezierPath — single segment', () => {
bench('1 segment', () => {
buildBezierPath(B0, B1, B2, B3);
});
});
// ---------------------------------------------------------------------------
// Simulated pointer-move: a control point is dragged each frame, the smooth
// path is rebuilt, and a LUT is re-derived from the moved knots. This is the
// end-to-end per-frame cost during an interactive curve/handle drag.
// ---------------------------------------------------------------------------
describe('pointer-move — smooth path rebuild', () => {
bench('drag mutate + buildSmoothPath (64 points)', () => {
// Move one control point along a deterministic path (no Math.random).
const i = 32;
const base = dragPath[i]!;
dragPath[i] = { x: base.x, y: base.y + 1 };
buildSmoothPath(dragPath);
// Restore so the fixture stays stable across iterations.
dragPath[i] = base;
});
});
describe('pointer-move — curve recompute', () => {
// Drag a tone-curve knot → rebuild monotone interpolant → re-apply 256-LUT.
bench('mutate knot + monotoneCubic + 256-LUT (100 knots)', () => {
const i = 50;
const base = knots100[i]!;
knots100[i] = { x: base.x, y: base.y + 0.01 };
toLUT(monotoneCubic(knots100), 256);
knots100[i] = base;
});
});