diff --git a/.github/workflows/resolution-time.yml b/.github/workflows/resolution-time.yml new file mode 100644 index 0000000000..a6fd9ecdab --- /dev/null +++ b/.github/workflows/resolution-time.yml @@ -0,0 +1,176 @@ +name: Resolution time benchmark + +on: + pull_request: + +jobs: + resolution-time: + if: github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.27.0 + run_install: false + + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + path: pr-branch + + - name: Checkout target branch + uses: actions/checkout@v4 + with: + path: target-branch + ref: ${{ github.base_ref }} + + - name: Checkout latest release + uses: actions/checkout@v4 + with: + path: release-branch + ref: release + + - name: Copy resolution-time app to release checkout + run: | + if [ ! -d release-branch/apps/resolution-time ]; then + cp -r target-branch/apps/resolution-time release-branch/apps/resolution-time + echo "Copied resolution-time app from target branch to release checkout" + else + echo "resolution-time app already exists in release checkout" + fi + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: 'pnpm' + cache-dependency-path: | + pr-branch/pnpm-lock.yaml + target-branch/pnpm-lock.yaml + release-branch/pnpm-lock.yaml + + - name: Install dependencies (PR branch) + working-directory: pr-branch + run: pnpm install --frozen-lockfile && cd apps/resolution-time/node_modules/bun && node install.js + + - name: Install dependencies (target branch) + working-directory: target-branch + run: pnpm install --frozen-lockfile && cd apps/resolution-time/node_modules/bun && node install.js + + - name: Install dependencies (release branch) + working-directory: release-branch + run: pnpm install --no-frozen-lockfile && cd apps/resolution-time/node_modules/bun && node install.js + + - name: Run benchmark (PR branch) + working-directory: pr-branch + run: pnpm --filter resolution-time resolution-time + + - name: Run benchmark (target branch) + working-directory: target-branch + run: pnpm --filter resolution-time resolution-time + + - name: Run benchmark (release branch) + working-directory: release-branch + run: pnpm --filter resolution-time resolution-time + + - name: Generate chart (random) + run: | + node pr-branch/apps/resolution-time/generateChart.ts \ + --input "PR:pr-branch/apps/resolution-time/results-random.json" \ + --input "main:target-branch/apps/resolution-time/results-random.json" \ + --input "release:release-branch/apps/resolution-time/results-random.json" \ + --title "Random Branching" \ + --xAxisTitle "max depth" \ + --yAxisTitle "time (ms)" \ + --output chart-random.md + + - name: Generate chart (linear) + run: | + node pr-branch/apps/resolution-time/generateChart.ts \ + --input "PR:pr-branch/apps/resolution-time/results-linear-recursion.json" \ + --input "main:target-branch/apps/resolution-time/results-linear-recursion.json" \ + --input "release:release-branch/apps/resolution-time/results-linear-recursion.json" \ + --title "Linear Recursion" \ + --xAxisTitle "max depth" \ + --yAxisTitle "time (ms)" \ + --output chart-linear.md + + - name: Generate chart (max depth) + run: | + node pr-branch/apps/resolution-time/generateChart.ts \ + --input "PR:pr-branch/apps/resolution-time/results-max-depth.json" \ + --input "main:target-branch/apps/resolution-time/results-max-depth.json" \ + --input "release:release-branch/apps/resolution-time/results-max-depth.json" \ + --title "Full Tree" \ + --xAxisTitle "max depth" \ + --yAxisTitle "time (ms)" \ + --output chart-max-depth.md + + - name: Prepare comment + run: | + { + echo "## Resolution Time Benchmark" + echo "" + cat chart-random.md + echo "" + cat chart-linear.md + echo "" + cat chart-max-depth.md + } > comparison.md + + - name: Comment PR with results + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const comparison = fs.readFileSync('comparison.md', 'utf8'); + + const botCommentIdentifier = ''; + + async function findBotComment(issueNumber) { + if (!issueNumber) return null; + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + }); + return comments.data.find((comment) => + comment.body.includes(botCommentIdentifier) + ); + } + + async function createOrUpdateComment(issueNumber) { + if (!issueNumber) { + console.log('No issue number provided. Cannot post or update comment.'); + return; + } + + const existingComment = await findBotComment(issueNumber); + if (existingComment) { + await github.rest.issues.updateComment({ + ...context.repo, + comment_id: existingComment.id, + body: botCommentIdentifier + '\n' + comparison, + }); + } else { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: issueNumber, + body: botCommentIdentifier + '\n' + comparison, + }); + } + } + + const issueNumber = context.issue.number; + if (!issueNumber) { + console.log('No issue number found in context. Skipping comment.'); + } else { + await createOrUpdateComment(issueNumber); + } + await core.summary + .addRaw(comparison) + .write(); diff --git a/apps/resolution-time/README.md b/apps/resolution-time/README.md index 3a1ba58c28..38d498a991 100644 --- a/apps/resolution-time/README.md +++ b/apps/resolution-time/README.md @@ -18,14 +18,14 @@ const myLeafFn = tgpu.comptime(() => { ### Recursive instruction -Use `tgpu.unroll` over `arrayForUnroll(BRANCHING)` and call `instructions[choice()]()()` to branch into other instructions. The `choice()` function handles depth tracking and picks a leaf when at max depth. +Use `tgpu.unroll` over `std.range(n)` and call `instructions[choice()]()()` to branch into other instructions. The `choice()` function handles depth tracking and picks a leaf when at max depth. ```ts const myRecursiveFn = tgpu.comptime(() => { return tgpu.fn(() => { 'use gpu'; // ... - for (const _i of tgpu.unroll(arrayForUnroll(BRANCHING))) { + for (const _i of tgpu.unroll(std.range(n))) { instructions[choice()]()(); } popDepth(); // REQUIRED — always call at the end, after the unroll diff --git a/apps/resolution-time/procedural.ts b/apps/resolution-time/procedural.ts index e933b0f622..3046015a6f 100644 --- a/apps/resolution-time/procedural.ts +++ b/apps/resolution-time/procedural.ts @@ -12,7 +12,7 @@ interface ProcGenConfig { } // default config -const SAMPLES = 10; +const SAMPLES = 50; const config: ProcGenConfig & { samples: number } = { mainBranching: 2, branching: 2, @@ -45,10 +45,6 @@ const state = tgpu.lazy(() => ({ const instructions: TgpuComptime<() => () => void>[] = []; const LEAF_COUNT = 4; -// TODO: replace it with number, when unroll supports that -const getArrayForUnroll = tgpu.comptime((n: number) => Array.from({ length: n })); -let branchingUnrollArray = getArrayForUnroll(config.branching); - const choice = tgpu.comptime((): number => { if (state.$.stackDepth == config.maxDepth - 1 || rand() > config.recurseProb) { state.$.stackDepth++; @@ -156,7 +152,7 @@ const waveFn = tgpu.comptime(() => { v = d.vec2f(std.sin(v.x * Math.PI), std.cos(v.y * Math.PI)); const _energy = std.dot(v, v); - for (const _i of tgpu.unroll(branchingUnrollArray)) { + for (const _i of tgpu.unroll(std.range(config.branching))) { // @ts-expect-error trust me instructions[choice()]()(); } @@ -176,7 +172,7 @@ const accFn = tgpu.comptime(() => { let acc = d.vec2f(); acc = d.vec2f(acc.x + offset.x * scale, acc.y + offset.y * scale); - for (const _i of tgpu.unroll(branchingUnrollArray)) { + for (const _i of tgpu.unroll(std.range(config.branching))) { // @ts-expect-error trust me instructions[choice()]()(); } @@ -198,7 +194,7 @@ const rotateFn = tgpu.comptime(() => { const s = std.sin(angle); v = d.vec2f(v.x * c - v.y * s, v.x * s + v.y * c); - for (const _i of tgpu.unroll(branchingUnrollArray)) { + for (const _i of tgpu.unroll(std.range(config.branching))) { // @ts-expect-error trust me instructions[choice()]()(); } @@ -220,7 +216,7 @@ const spiralFn = tgpu.comptime(() => { const pos = d.vec2f(center.x + radius * std.cos(angle), center.y + radius * std.sin(angle)); const _dist = std.length(pos); - for (const _i of tgpu.unroll(branchingUnrollArray)) { + for (const _i of tgpu.unroll(std.range(config.branching))) { // @ts-expect-error trust me instructions[choice()]()(); } @@ -236,7 +232,7 @@ instructions.push(baseFn, blendFn, thresholdFn, filterFn, waveFn, accFn, rotateF const main = () => { 'use gpu'; - for (const _i of tgpu.unroll(getArrayForUnroll(config.mainBranching))) { + for (const _i of tgpu.unroll(std.range(config.mainBranching))) { // @ts-expect-error trust me instructions[choice()]()(); } @@ -259,10 +255,15 @@ const outDir = resolve(import.meta.dirname ?? '.', '.'); function runBenchmark(input: ProcGenConfig, output: BenchmarkResult[]) { Object.assign(config, { samples: input.samples ?? SAMPLES }, input); - branchingUnrollArray = getArrayForUnroll(config.branching); + // warmup for (let i = 0; i < config.samples; i++) { rand = splitmix32(config.seed); + benchmarkResolve(); + } + + for (let i = 0; i < config.samples; i++) { + rand = splitmix32((config.seed << i) | (config.seed >> (32 - i))); const result = benchmarkResolve(); output.push(result); console.log( @@ -273,21 +274,6 @@ function runBenchmark(input: ProcGenConfig, output: BenchmarkResult[]) { } } -function warmupJIT() { - runBenchmark( - { - mainBranching: 1, - branching: 1, - maxDepth: 1, - recurseProb: 0, - seed: 0.1882 * 2 ** 32, - }, - [], - ); -} - -warmupJIT(); - const results: BenchmarkResult[] = []; const DEPTHS = Array.from({ length: 8 }, (_, i) => i + 1);