diff --git a/contributor_docs/performance_testing.md b/contributor_docs/performance_testing.md new file mode 100644 index 0000000000..33d355d5f3 --- /dev/null +++ b/contributor_docs/performance_testing.md @@ -0,0 +1,34 @@ + + +# Performance Testing + +Performance testing is an essential part of measuring how fast isolated code paths take to execute under a known range of conditions. + +Implementation notes: + +- All pre-existing benchmarks included the full p5 instance setup which adds ~50ms to each test. The typography performance tests show how to isolate performance testing away from setup overhead. +- The `textToPoints() single word, with render` test draws the calculated points, others just run `textToPoints()`. + +## Typography Tests + +To isolate typography tests, run the following: + +```shell +npx vitest bench test/bench/typography.bench.js --reporter=verbose +``` + +### Manual tests on v1.x + +These manual tests are useful to loosely check performance against v2.x as a baseline. The test harness is compatible with the one in `test/bench/typography.bench.js`. + +To manually run typography tests against p5.js v1.x, open the following files in a browser: + +- `textToPoints()`: `test/bench/v1_manual/text_to_points.html` + +Note: you will need to use a [local server](https://github.com/processing/p5.js/wiki/Local-server) to avoid CORS issues when loading fonts. For example, from the root of the repo: + +```shell +npx http-server -c-1 +``` + +Then tests can be accessed at `http://localhost:8080/test/bench/v1_manual/text_to_points.html`. diff --git a/test/bench/typography.bench.js b/test/bench/typography.bench.js new file mode 100644 index 0000000000..0b7b611ac3 --- /dev/null +++ b/test/bench/typography.bench.js @@ -0,0 +1,125 @@ +import { bench, describe } from "vitest"; +import p5 from "../../src/app"; +import { WEBGL, WEBGPU } from "../../src/core/constants"; + +const fontFile = "../../test/manual-test-examples/type/font/LiberationSans-Bold.ttf"; + +const strs = { + single: "Performance", + ten: "Performance testing 10 words at a time is exhaustive! Right?", + paragraph: Array.from({ length: 10 }, (_, i) => + `${i === 0 ? "\t": ""}Performance is vital in all aspects of text rendering, even 10 lines at a time. This is line ${i + 1} of 10.` + ).join("\n") // This will hit around 15fps, 21275 points +}; + +// Parameterizing test cases by function ensures consistency in test parameters across all renderers. +// Future tests should follow a similar format (e.g. TO_CONTOURS_CASES, etc) +const TO_POINTS_CASES = [ + {label: "textToPoints() single word", str: strs.single, textSize: 20, sampleFactor: 0.5, points: 317, variance: 5, render: false}, + {label: "textToPoints() single word, 150pt", str: strs.single, textSize: 150, sampleFactor: 0.5, points: 2336, variance: 50, render: false}, + {label: "textToPoints() single word, with render", str: strs.single, textSize: 20, sampleFactor: 0.5, points: 317, variance: 5, render: true}, + {label: "textToPoints() 10 words", str: strs.ten, textSize: 20, sampleFactor: 0.5, points: 1453, variance: 5, render: false}, + {label: "textToPoints() paragraph", str: strs.paragraph, textSize: 20, sampleFactor: 0.5, points: 21275, variance: 50, render: false}, +]; + +async function bootstrap(w = 400, h = 400, renderer = undefined) { + var myp5; + var font; + new p5(function (p) { + p.setup = async function () { + myp5 = p; + font = await p.loadFont(fontFile); + p.createCanvas(w, h, renderer); + }; + }); + await vi.waitFor(() => { + if (myp5 === undefined) throw new Error("not ready"); + }); + return { myp5, font }; +} + +function drawPoints(myp5, points) { + for (let point of points) { + myp5.point(point.x, point.y); + } +} + +const options = { iterations: 20, time: 500 }; + +describe("Typography: bench 2D", function() { + var myp5, font; + + for (let testCase of TO_POINTS_CASES) { + bench( + testCase.label, + async () => { + const points = font.textToPoints(testCase.str, 10, 20, { sampleFactor: testCase.sampleFactor }); + assert.closeTo(points.length, testCase.points, testCase.variance); + if (testCase.render) { + drawPoints(myp5, points); + } + }, + { + ...options, + setup: async () => { + ({myp5, font} = await bootstrap()); + myp5.textSize(testCase.textSize); + }, + teardown: () => myp5.remove() + } + ); + } + +}); + +describe("Typography: bench WebGL", function() { + var myp5, font; + + for (let testCase of TO_POINTS_CASES) { + bench( + testCase.label, + async () => { + const points = font.textToPoints(testCase.str, 10, 20, { sampleFactor: testCase.sampleFactor }); + assert.closeTo(points.length, testCase.points, testCase.variance); + if (testCase.render) { + drawPoints(myp5, points); + } + }, + { + ...options, + setup: async () => { + ({myp5, font} = await bootstrap(400, 400, WEBGL)); + myp5.textSize(testCase.textSize); + }, + teardown: () => myp5.remove() + } + ); + } + +}); + +describe("Typography: bench WebGPU", function() { + var myp5, font; + + for (let testCase of TO_POINTS_CASES) { + bench( + testCase.label, + async () => { + const points = font.textToPoints(testCase.str, 10, 20, { sampleFactor: testCase.sampleFactor }); + assert.closeTo(points.length, testCase.points, testCase.variance); + if (testCase.render) { + drawPoints(myp5, points); + } + }, + { + ...options, + setup: async () => { + ({myp5, font} = await bootstrap(400, 400, WEBGPU)); + myp5.textSize(testCase.textSize); + }, + teardown: () => myp5.remove() + } + ); + } + +}); \ No newline at end of file diff --git a/test/bench/v1_manual/text_to_points.html b/test/bench/v1_manual/text_to_points.html new file mode 100644 index 0000000000..254e3caf8c --- /dev/null +++ b/test/bench/v1_manual/text_to_points.html @@ -0,0 +1,39 @@ + + + + P5.js v1.x Typography Performance Tests + + + + + + +
+ + + + + \ No newline at end of file diff --git a/test/bench/v1_manual/text_to_points_v1.js b/test/bench/v1_manual/text_to_points_v1.js new file mode 100644 index 0000000000..3e0e872591 --- /dev/null +++ b/test/bench/v1_manual/text_to_points_v1.js @@ -0,0 +1,101 @@ +// Ensure these tests do not drift too far from ../typography.bench.js + +const fontFile = "../../../test/manual-test-examples/type/font/LiberationSans-Bold.ttf"; + +const strs = { + single: "Performance", + ten: "Performance testing 10 words at a time is exhaustive! Right?", + paragraph: Array.from({ length: 10 }, (_, i) => + `${i === 0 ? "\t": ""}Performance is vital in all aspects of text rendering, even 10 lines at a time. This is line ${i + 1} of 10.` + ).join("\n") // This will hit around 15fps, 21275 points +}; + +// Parameterizing test cases by function ensures consistency in test parameters across all renderers. +const TO_POINTS_CASES = [ + {label: "textToPoints() single word", str: strs.single, textSize: 20, sampleFactor: 0.5, points: 317, variance: 5, render: false}, + {label: "textToPoints() single word, 150pt", str: strs.single, textSize: 150, sampleFactor: 0.5, points: 2336, variance: 50, render: false}, + {label: "textToPoints() single word, with render", str: strs.single, textSize: 20, sampleFactor: 0.5, points: 317, variance: 5, render: true}, + {label: "textToPoints() 10 words", str: strs.ten, textSize: 20, sampleFactor: 0.5, points: 1453, variance: 5, render: false}, + {label: "textToPoints() paragraph", str: strs.paragraph, textSize: 20, sampleFactor: 0.5, points: 21275, variance: 50, render: false}, +]; + +function bootstrap(w = 400, h = 400, renderer = "p2d") { + return new Promise((resolve) => { + let myp5, font; + new p5(function (p) { + p.preload = function() { + font = p.loadFont(fontFile); + } + p.setup = function () { + myp5 = p; + p.createCanvas(w, h, renderer); + resolve({ myp5, font }); + }; + }); + }); +} + +function drawPoints(myp5, points) { + for (let point of points) { + myp5.point(point.x, point.y); + } +} + +(async function run() { + let suiteName, results; + + suiteName = "Typography v1.x: 2D"; + results = []; + for (let testCase of TO_POINTS_CASES) { + let {myp5, font} = await bootstrap(); + myp5.textSize(testCase.textSize); + + let points; + let duration = runTest(() => { + points = font.textToPoints(testCase.str, 10, 20, testCase.textSize, { sampleFactor: testCase.sampleFactor}); + if (testCase.render) { + drawPoints(myp5, points); + } + }); + results.push({ + label: testCase.label, + duration: duration.toFixed(1), + points: points.length, + expectedPoints: testCase.points, + variance: testCase.variance + }); + myp5.remove(); + } + writeResults(suiteName, results); + +})(); + +function runTest(fn) { + let startTime = performance.now(); + fn(); + return performance.now() - startTime; +} + +function writeResults(suiteName, results) { + const container = document.getElementById("results"); + const table = document.createElement("table"); + + const headRow = table.createTHead().insertRow(); + [suiteName, "Case", "Time (ms)", "Points", "Expected Points"].forEach((cellText) => { + const newCell = document.createElement("th"); + newCell.textContent = cellText; + headRow.append(newCell); + }); + + const body = table.createTBody(); + for (let res of results) { + const row = body.insertRow(); + row.insertCell(); // Intentionally blank, table left-padding + row.insertCell().textContent = res.label; + row.insertCell().textContent = res.duration; + row.insertCell().textContent = res.points; + row.insertCell().textContent = `${res.expectedPoints} +/- ${res.variance}`; + } + + container.append(table); +}