From 21a52912357c53fed4c7fa95ed7e5e557efc9d35 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 3 Mar 2026 19:26:25 +0100 Subject: [PATCH 1/4] initial commit --- .github/workflows/resolution-time.yml | 168 ++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 .github/workflows/resolution-time.yml diff --git a/.github/workflows/resolution-time.yml b/.github/workflows/resolution-time.yml new file mode 100644 index 000000000..84ca8ae1f --- /dev/null +++ b/.github/workflows/resolution-time.yml @@ -0,0 +1,168 @@ +name: Resolution time benchmark + +on: + pull_request: + workflow_dispatch: + inputs: + target_branch: + description: "Target branch to compare against" + required: true + default: "main" + +jobs: + resolution-time: + permissions: + contents: read + issues: write + 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.event_name == 'workflow_dispatch' && inputs.target_branch || github.base_ref }} + + - name: Checkout release branch + uses: actions/checkout@v4 + with: + path: release-branch + ref: release + + - name: Copy resolution-time app into target and release branches + run: | + cp -r pr-branch/apps/resolution-time target-branch/apps/resolution-time + cp -r pr-branch/apps/resolution-time release-branch/apps/resolution-time + + - name: Install dependencies (PR branch) + working-directory: pr-branch + run: pnpm install + + - name: Install dependencies (target branch) + working-directory: target-branch + run: pnpm install + + - name: Install dependencies (release branch) + working-directory: release-branch + run: pnpm install + + - 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: Run benchmark on PR branch + working-directory: pr-branch + run: pnpm --filter resolution-time resolution-time + + - name: Run benchmark on target branch + working-directory: target-branch + run: pnpm --filter resolution-time resolution-time + continue-on-error: true + + - name: Run benchmark on release branch + working-directory: release-branch + run: pnpm --filter resolution-time resolution-time + continue-on-error: true + + - name: Generate charts + run: | + SCRIPT=pr-branch/apps/resolution-time/generateChart.ts + + DEPTH_ARGS="--input PR:pr-branch/apps/resolution-time/results-max-depth.json" + LINEAR_ARGS="--input PR:pr-branch/apps/resolution-time/results-linear-recursion.json" + + if [ -f target-branch/apps/resolution-time/results-max-depth.json ]; then + DEPTH_ARGS="$DEPTH_ARGS --input target:target-branch/apps/resolution-time/results-max-depth.json" + LINEAR_ARGS="$LINEAR_ARGS --input target:target-branch/apps/resolution-time/results-linear-recursion.json" + fi + + if [ -f release-branch/apps/resolution-time/results-max-depth.json ]; then + DEPTH_ARGS="$DEPTH_ARGS --input release:release-branch/apps/resolution-time/results-max-depth.json" + LINEAR_ARGS="$LINEAR_ARGS --input release:release-branch/apps/resolution-time/results-linear-recursion.json" + fi + + node $SCRIPT $DEPTH_ARGS --title "Resolution Time vs Max Depth" --xAxisTitle "max depth" --yAxisTitle "time (ms)" --output max-depth.md + node $SCRIPT $LINEAR_ARGS --title "Resolution Time vs Linear Recursion" --xAxisTitle "max depth" --yAxisTitle "time (ms)" --output linear-recursion.md + + - name: Build comment + run: | + { + echo '' + echo '## Resolution Time Benchmark' + echo '' + echo '### Max Depth (branching=2)' + cat max-depth.md + echo '' + echo '### Linear Recursion (branching=1)' + cat linear-recursion.md + } > comment.md + + - name: Comment PR with results + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const comment = fs.readFileSync('comment.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((c) => + c.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: comment, + }); + } else { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: issueNumber, + body: comment, + }); + } + } + + 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(comment) + .write(); From f949f6dcfb0f39570db738666f7d0897b829123b Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 3 Mar 2026 20:20:37 +0100 Subject: [PATCH 2/4] oxfmt --- .github/workflows/resolution-time.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/resolution-time.yml b/.github/workflows/resolution-time.yml index 84ca8ae1f..24d1026d3 100644 --- a/.github/workflows/resolution-time.yml +++ b/.github/workflows/resolution-time.yml @@ -5,9 +5,9 @@ on: workflow_dispatch: inputs: target_branch: - description: "Target branch to compare against" + description: 'Target branch to compare against' required: true - default: "main" + default: 'main' jobs: resolution-time: @@ -61,7 +61,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: 24.x - cache: "pnpm" + cache: 'pnpm' cache-dependency-path: | pr-branch/pnpm-lock.yaml target-branch/pnpm-lock.yaml From e5954d579c2e5315a5209ee7e9bb4a0443f8b27c Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Tue, 3 Mar 2026 22:34:29 +0100 Subject: [PATCH 3/4] readme update --- apps/resolution-time/README.md | 15 +++------------ apps/resolution-time/procedural.ts | 8 ++++---- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/apps/resolution-time/README.md b/apps/resolution-time/README.md index 0058b1d2d..3a1ba58c2 100644 --- a/apps/resolution-time/README.md +++ b/apps/resolution-time/README.md @@ -1,4 +1,4 @@ -# Procedural TypeGPU function generator for benchmarking WGSL resolution time +# Procedural TypeGPU function generator for benchmarking resolution time ## Adding a new instruction @@ -33,15 +33,6 @@ const myRecursiveFn = tgpu.comptime(() => { }); ``` -### Registering - -Add your function to the `instructions.push(...)` call. **Leaves must come first**, followed by recursive functions. Update `LEAF_COUNT` if you add a new leaf. - -### Rules - -- Every instruction **must** call `popDepth()` exactly once, as the last statement -- No direct function calls inside instructions — branching happens only via the `tgpu.unroll` + `choice()` pattern - ## Generating Mermaid charts `generateChart.ts` reads benchmark result JSON files and outputs a Mermaid xychart comparing up to 3 datasets. @@ -50,8 +41,8 @@ Add your function to the `instructions.push(...)` call. **Leaves must come first ```sh node generateChart.ts \ - --input "PR:results-max-depth.json" \ - --input "main:../other/results-max-depth.json" \ + --input "PR:pr-branch/results-max-depth.json" \ + --input "main:main-branch/results-max-depth.json" \ --title "Resolution Time vs Max Depth" \ --xAxisTitle "max depth" \ --yAxisTitle "time (ms)" \ diff --git a/apps/resolution-time/procedural.ts b/apps/resolution-time/procedural.ts index 85aa373bb..7a77c091f 100644 --- a/apps/resolution-time/procedural.ts +++ b/apps/resolution-time/procedural.ts @@ -47,8 +47,8 @@ const instructions: TgpuComptime<() => TgpuGenericFn<() => void>>[] = []; const LEAF_COUNT = 3; // TODO: replace it with number, when unroll supports that -const arrayForUnroll = tgpu.comptime((n: number) => Array.from({ length: n })); -let branchingUnrollArray = arrayForUnroll(config.branching); +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) { @@ -209,7 +209,7 @@ instructions.push(baseFn, blendFn, thresholdFn, waveFn, accFn, rotateFn, spiralF const main = () => { 'use gpu'; - for (const _i of tgpu.unroll(arrayForUnroll(config.mainBranching))) { + for (const _i of tgpu.unroll(getArrayForUnroll(config.mainBranching))) { // @ts-expect-error trust me instructions[choice()]()(); } @@ -232,7 +232,7 @@ const outDir = resolve(import.meta.dirname ?? '.', '.'); function runBenchmark(input: ProcGenConfig, output: BenchmarkResult[]) { Object.assign(config, { samples: input.samples ?? SAMPLES }, input); - branchingUnrollArray = arrayForUnroll(config.branching); + branchingUnrollArray = getArrayForUnroll(config.branching); for (let i = 0; i < config.samples; i++) { rand = splitmix32(config.maxDepth * 2 ** 32); From 42bfa9f7f94551fa4271774202f1262655e421fd Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 4 Mar 2026 11:26:20 +0100 Subject: [PATCH 4/4] heavy leaf --- apps/resolution-time/generateChart.ts | 2 +- apps/resolution-time/procedural.ts | 32 +++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/resolution-time/generateChart.ts b/apps/resolution-time/generateChart.ts index 7a90e0f50..eb69fa7f6 100644 --- a/apps/resolution-time/generateChart.ts +++ b/apps/resolution-time/generateChart.ts @@ -64,7 +64,7 @@ if (title === '') { function median(values: number[]): number { // oxlint-disable-next-line eslint-plugin-unicorn(no-array-sort) - const sorted = [...values].sort((a, b) => a - b); + const sorted = values.sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); // oxlint-disable-next-line typescript/no-non-null-assertion return sorted.length % 2 ? sorted[mid]! : (sorted[mid - 1]! + sorted[mid]!) / 2; diff --git a/apps/resolution-time/procedural.ts b/apps/resolution-time/procedural.ts index 7a77c091f..ad7f8e695 100644 --- a/apps/resolution-time/procedural.ts +++ b/apps/resolution-time/procedural.ts @@ -44,7 +44,6 @@ const state = tgpu.lazy(() => ({ })); const instructions: TgpuComptime<() => TgpuGenericFn<() => void>>[] = []; -const LEAF_COUNT = 3; // TODO: replace it with number, when unroll supports that const getArrayForUnroll = tgpu.comptime((n: number) => Array.from({ length: n })); @@ -75,6 +74,14 @@ const dataLayout = tgpu.bindGroupLayout({ scale: { uniform: d.f32 }, }); +const Cell = d.struct({ + col: d.vec3f, +}); + +const cellsLayout = tgpu.bindGroupLayout({ + grid: { storage: d.arrayOf(d.arrayOf(Cell, 7), 7) }, +}); + const baseFn = tgpu.comptime(() => { return tgpu .fn(() => { @@ -120,6 +127,26 @@ const thresholdFn = tgpu.comptime(() => { .$name('thresholdFn'); }); +const filterFn = tgpu.comptime(() => { + return tgpu + .fn(() => { + 'use gpu'; + + const xy = d.vec2u(7, 8); + + let _result = d.vec3f(); + for (const dy of tgpu.unroll([-1, 0, 1])) { + for (const dx of tgpu.unroll([-1, 0, 1])) { + // oxlint-disable-next-line typescript-eslint(no-non-null-assertion + _result += cellsLayout.$.grid[xy.x + dx]![xy.y + dy]!.col; + } + } + + popDepth(); + }) + .$name('filterFn'); +}); + const waveFn = tgpu.comptime(() => { return tgpu .fn(() => { @@ -204,7 +231,8 @@ const spiralFn = tgpu.comptime(() => { }); // leaves first, then recursive -instructions.push(baseFn, blendFn, thresholdFn, waveFn, accFn, rotateFn, spiralFn); +instructions.push(baseFn, blendFn, thresholdFn, filterFn, waveFn, accFn, rotateFn, spiralFn); +const LEAF_COUNT = 4; const main = () => { 'use gpu';