From 260be615677b9f043ff96a18a27a84aa474140d8 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Mon, 6 Oct 2025 13:45:29 -0400 Subject: [PATCH 1/2] add cpu intensive benchmarks --- src/workerd/tests/BUILD.bazel | 10 + src/workerd/tests/bench-vanilla.c++ | 478 ++++++++++++++++++++++++++++ 2 files changed, 488 insertions(+) create mode 100644 src/workerd/tests/bench-vanilla.c++ diff --git a/src/workerd/tests/BUILD.bazel b/src/workerd/tests/BUILD.bazel index cfda85ccb32..a89558fffc4 100644 --- a/src/workerd/tests/BUILD.bazel +++ b/src/workerd/tests/BUILD.bazel @@ -90,6 +90,15 @@ wd_cc_benchmark( ], ) +wd_cc_benchmark( + name = "bench-vanilla", + srcs = ["bench-vanilla.c++"], + deps = [ + ":test-fixture", + "//src/workerd/jsg", + ], +) + wd_test( src = "unknown-import-assertions-test.wd-test", args = ["--experimental"], @@ -120,6 +129,7 @@ filegroup( ":bench-mimetype", ":bench-regex", ":bench-util", + ":bench-vanilla", ], visibility = ["//visibility:public"], ) diff --git a/src/workerd/tests/bench-vanilla.c++ b/src/workerd/tests/bench-vanilla.c++ new file mode 100644 index 00000000000..8f00483abd5 --- /dev/null +++ b/src/workerd/tests/bench-vanilla.c++ @@ -0,0 +1,478 @@ +// Copyright (c) 2023 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +// +// MIT License +// Copyright (c) 2025 Theo Browne +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#include +#include + +// A benchmark for GlobalScope functionality. + +namespace workerd { +namespace { + +// Benchmark is taken from +// https://github.com/t3dotgg/cf-vs-vercel-bench/blob/98d815ca53a587d9bade2208c17e4f5411c0b96c/vanilla-bench/cf-edition/worker.js +struct VanillaJs: public benchmark::Fixture { + virtual ~VanillaJs() noexcept(true) {} + + void SetUp(benchmark::State& state) noexcept(true) override { + TestFixture::SetupParams params = {.mainModuleSource = R"( + function isPrime(num) { + if (num <= 1) return false; + if (num <= 3) return true; + if (num % 2 === 0 || num % 3 === 0) return false; + for (let i = 5; i * i <= num; i += 6) { + if (num % i === 0 || num % (i + 2) === 0) return false; + } + return true; + } + + function calculatePrimes(limit) { + const primes = []; + for (let i = 2; i <= limit; i++) { + if (isPrime(i)) { + primes.push(i); + } + } + return primes; + } + + function fibonacci(n) { + if (n <= 1) return n; + let a = 0, b = 1; + for (let i = 2; i <= n; i++) { + const temp = a + b; + a = b; + b = temp; + } + return b; + } + + function generateComplexData() { + // Calculate first 10,000 primes + const primes = calculatePrimes(100000); + + // Calculate fibonacci numbers + const fibs = Array.from({ length: 100 }, (_, i) => fibonacci(i)); + + // Generate complex nested data + const complexData = Array.from({ length: 50 }, (_, i) => ({ + id: i, + title: `Section ${i + 1}`, + primes: primes.slice(i * 100, (i + 1) * 100), + fibonacci: fibs, + items: Array.from({ length: 20 }, (_, j) => ({ + id: j, + value: Math.sqrt(i * 1000 + j), + description: `Item ${j} in section ${i}`, + metadata: { + timestamp: Date.now(), + hash: (i * j * 12345).toString(36), + complexity: Math.sin(i) * Math.cos(j), + }, + })), + })); + + return complexData; + } + + function generateSlowerComplexData() { + // Calculate way more primes (5x more) + const primes = calculatePrimes(500000); + + // Calculate more fibonacci numbers + const fibs = Array.from({ length: 200 }, (_, i) => fibonacci(i)); + + // Generate much more complex nested data (3x sections, 3x items) + const complexData = Array.from({ length: 150 }, (_, i) => ({ + id: i, + title: `Section ${i + 1}`, + primes: primes.slice(i * 200, (i + 1) * 200), + fibonacci: fibs, + items: Array.from({ length: 60 }, (_, j) => ({ + id: j, + value: Math.sqrt(i * 1000 + j), + description: `Item ${j} in section ${i}`, + metadata: { + timestamp: Date.now(), + hash: (i * j * 12345).toString(36), + complexity: Math.sin(i) * Math.cos(j), + extraComputation: Math.pow(i + j, 2) * Math.log(i + j + 1), + }, + })), + })); + + return complexData; + } + + function renderComplexComponent(data) { + const totalPrimes = data.reduce((sum, section) => sum + section.primes.length, 0); + const averageFib = data[0].fibonacci.reduce((a, b) => a + b, 0) / data[0].fibonacci.length; + + const sectionsHtml = data.map(section => { + const primesHtml = section.primes.map(prime => + `
${prime}
` + ).join(''); + + const fibsHtml = section.fibonacci.map(fib => + `
${fib}
` + ).join(''); + + const itemsHtml = section.items.map(item => ` +
+

Item ${item.id}

+

${item.description}

+

Value: ${item.value.toFixed(4)}

+
+

Hash: ${item.metadata.hash}

+

Complexity: ${item.metadata.complexity.toFixed(6)}

+ ${item.metadata.extraComputation ? `

Extra: ${item.metadata.extraComputation.toFixed(6)}

` : ''} +

Timestamp: ${item.metadata.timestamp}

+
+
+ `).join(''); + + return ` +
+

${section.title}

+ +
+

Prime Numbers (100 samples)

+
${primesHtml}
+
+ +
+

Fibonacci Sequence

+
${fibsHtml}
+
+ +
+

Items (${section.items.length})

+
${itemsHtml}
+
+
+ `; + }).join(''); + + const additionalComputations = Array.from({ length: 300 }, (_, i) => { + const n = i + 1; + const factorial = Array.from({ length: Math.min(n, 20) }, (_, j) => j + 1) + .reduce((acc, val) => acc * val, 1); + return ` +
+

n=${n}, f=${factorial.toExponential(2)}

+
+ `; + }).join(''); + + return ` +
+

Complex Server-Rendered Component (Vanilla)

+ +
+

Statistics

+

Total Prime Numbers: ${totalPrimes}

+

Average Fibonacci Value: ${averageFib.toFixed(2)}

+

Total Sections: ${data.length}

+
+ + ${sectionsHtml} + +
+

Additional Computations

+
${additionalComputations}
+
+
+ `; + } + + export default { + async fetch(request, env, ctx) { + const url = new URL(request.url); + + if (url.pathname === '/bench') { + console.log("rendering", Date.now()); + + // Expensive server-side computation + const data = generateComplexData(); + const currentTime = new Date().toLocaleString(); + + const componentHtml = renderComplexComponent(data); + + const html = ` + + + + + + Vanilla SSR Benchmark + + + +
+

Last rendered at:

+

${currentTime}

+ ${componentHtml} +
+ + + `; + + return new Response(html, { + headers: { + 'Content-Type': 'text/html; charset=utf-8', + }, + }); + } + + if (url.pathname === '/slower-bench') { + console.log("rendering slower bench", Date.now()); + + // Much more expensive server-side computation + const data = generateSlowerComplexData(); + const currentTime = new Date().toLocaleString(); + + const componentHtml = renderComplexComponent(data); + + const html = ` + + + + + + Vanilla SSR Slower Benchmark + + + +
+

Slower Bench - Last rendered at:

+

${currentTime}

+ ${componentHtml} +
+ + + `; + + return new Response(html, { + headers: { + 'Content-Type': 'text/html; charset=utf-8', + }, + }); + } + + if (url.pathname === '/sine-bench') { + console.log("rendering sine bench", Date.now()); + + // CPU-bound floating-point math benchmark + let sum = 0; + for (let i = 0; i < 100_000_000; i++) { + const result = Math.sin(Math.PI * i) * Math.cos(Math.PI * i); + sum += result; + } + + const currentTime = new Date().toLocaleString(); + + const html = ` + + + + + + Sine Benchmark + + + +
+

Sine Bench - Last rendered at:

+

${currentTime}

+
+

CPU-Bound Floating-Point Math Test

+

Computed 100 million sine/cosine operations

+
+

Result Sum: ${sum.toFixed(6)}

+
+
+
+ + + `; + + return new Response(html, { + headers: { + 'Content-Type': 'text/html; charset=utf-8', + }, + }); + } + + if (url.pathname === '/realistic-math-bench') { + console.log("rendering realistic math bench", Date.now()); + + // CPU-bound integer and string operations benchmark + let result = 0; + + // Integer arithmetic and bitwise operations + for (let i = 0; i < 10_000_000; i++) { + result += ((i * 31) ^ (i << 3)) & 0xFFFFFFFF; + result = (result * 1103515245 + 12345) & 0x7FFFFFFF; // LCG + } + + // Array sorting and manipulation + const arrays = []; + for (let i = 0; i < 10; i++) { + const arr = Array.from({ length: 10000 }, (_, idx) => (idx * 7919) % 10007); + arr.sort((a, b) => a - b); + arrays.push(arr.reduce((acc, val) => acc + val, 0)); + } + + // String operations and hashing + let stringHash = 0; + const baseStr = "benchmark-test-string-"; + for (let i = 0; i < 1_000_000; i++) { + const str = baseStr + i; + for (let j = 0; j < str.length; j++) { + stringHash = ((stringHash << 5) - stringHash + str.charCodeAt(j)) | 0; + } + } + + // Prime counting with optimized trial division + let primeCount = 0; + for (let n = 2; n < 100000; n++) { + let isPrime = n > 1; + if (n > 2 && n % 2 === 0) isPrime = false; + else { + for (let i = 3; i * i <= n; i += 2) { + if (n % i === 0) { + isPrime = false; + break; + } + } + } + if (isPrime) primeCount++; + } + + const currentTime = new Date().toLocaleString(); + + const html = ` + + + + + + Realistic Math Benchmark + + + +
+

Realistic Math Bench - Last rendered at:

+

${currentTime}

+
+

CPU-Bound Integer & String Operations Test

+

Mixed workload: integer arithmetic, array sorting, string hashing, prime counting

+
+

Integer result: ${result}

+

Array ops: ${arrays.length} sorts completed

+

String hash: ${stringHash}

+

Primes found: ${primeCount}

+
+
+
+ + + `; + + return new Response(html, { + headers: { + 'Content-Type': 'text/html; charset=utf-8', + }, + }); + } + + return new Response('Not Found', { status: 404 }); + }, + }; + )"_kj}; + fixture = kj::heap(kj::mv(params)); + } + + void TearDown(benchmark::State& state) noexcept(true) override { + fixture = nullptr; + } + + kj::Own fixture; +}; + +BENCHMARK_F(VanillaJs, bench)(benchmark::State& state) { + for (auto _: state) { + benchmark::DoNotOptimize( + fixture->runRequest(kj::HttpMethod::GET, "http://www.example.com/bench"_kj, "TEST"_kj)); + } +} + +BENCHMARK_F(VanillaJs, slowerBench)(benchmark::State& state) { + for (auto _: state) { + benchmark::DoNotOptimize(fixture->runRequest( + kj::HttpMethod::GET, "http://www.example.com/slower-bench"_kj, "TEST"_kj)); + } +} + +BENCHMARK_F(VanillaJs, sineBench)(benchmark::State& state) { + for (auto _: state) { + benchmark::DoNotOptimize(fixture->runRequest( + kj::HttpMethod::GET, "http://www.example.com/sine-bench"_kj, "TEST"_kj)); + } +} + +BENCHMARK_F(VanillaJs, realisticMathBench)(benchmark::State& state) { + for (auto _: state) { + benchmark::DoNotOptimize(fixture->runRequest( + kj::HttpMethod::GET, "http://www.example.com/realistic-math-bench"_kj, "TEST"_kj)); + } +} + +} // namespace +} // namespace workerd From 62a01bfb46ce0797b49294934be94cc9c7c7be8a Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Mon, 6 Oct 2025 16:21:00 -0400 Subject: [PATCH 2/2] fix profile command --- justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/justfile b/justfile index d591ebc5435..4707a8acc5e 100644 --- a/justfile +++ b/justfile @@ -134,7 +134,7 @@ coverage path="//...": profile path: bazel run //src/workerd/tests:bench-{{path}} --config=benchmark --run_under="perf record -F max --call-graph lbr" - PERF_FILE=$(fdfind perf.data bazel-bin | grep "{{path}}" | head -1); \ + PERF_FILE=$(fdfind --hidden perf.data bazel-bin | grep "{{path}}" | head -1); \ if [ -n "$PERF_FILE" ] && [ -s "$PERF_FILE" ]; then \ cp $PERF_FILE perf.data; \ perf report --input=$PERF_FILE; \