Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .github/workflows/treeshake-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Tree-shake test

on:
pull_request:

jobs:
treeshake-test:
runs-on: ubuntu-latest
steps:
- 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: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: |
pr-branch/pnpm-lock.yaml
target-branch/pnpm-lock.yaml

- 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: Run tree-shake test on PR branch
working-directory: pr-branch
run: pnpm --filter treeshake-test test

- name: Run tree-shake test on target branch
working-directory: target-branch
run: pnpm --filter treeshake-test test

- name: Compare results
run: |
node pr-branch/apps/treeshake-test/compare-results.mjs \
pr-branch/apps/treeshake-test/results.json \
target-branch/apps/treeshake-test/results.json \
> comparison.md

- name: Upload PR results
uses: actions/upload-artifact@v4
with:
name: pr-results
path: pr-branch/apps/treeshake-test/results.json

- name: Upload target results
uses: actions/upload-artifact@v4
with:
name: target-results
path: target-branch/apps/treeshake-test/results.json

- name: Upload comparison
uses: actions/upload-artifact@v4
with:
name: comparison
path: 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');

github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '## 📊 Bundle Size Comparison\n\n' + comparison
});
4 changes: 4 additions & 0 deletions apps/treeshake-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist/

results.md

251 changes: 251 additions & 0 deletions apps/treeshake-test/compare-results.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
#!/usr/bin/env node

import * as fs from 'node:fs/promises';
import { arrayOf, type } from 'arktype';

// Define schema for benchmark results
const ResultRecord = type({
exampleFilename: 'string',
exampleUrl: 'string',
bundler: 'string',
size: 'number',
});

const BenchmarkResults = arrayOf(ResultRecord);

function getSharedBundlers(
prResults: typeof BenchmarkResults.infer,
targetResults: typeof BenchmarkResults.infer,
) {
const prBundlers = new Set(prResults.map((r) => r.bundler));
const targetBundlers = new Set(targetResults.map((r) => r.bundler));
return [...prBundlers].filter((bundler) => targetBundlers.has(bundler));
}

function groupResultsByExample(results: typeof BenchmarkResults.infer) {
const grouped: Record<string, Record<string, number>> = {};
for (const result of results) {
if (!grouped[result.exampleFilename]) {
grouped[result.exampleFilename] = {};
}
grouped[result.exampleFilename]![result.bundler] = result.size;
}
return grouped;
}

function calculateTrend(
prSize: number,
targetSize: number,
): { emoji: string; percent: string; diff: number } {
if (prSize === targetSize) {
return { emoji: '➖', percent: '0.0%', diff: 0 };
}
const diff = prSize - targetSize;
const percent = ((diff / targetSize) * 100).toFixed(1);
const emoji = diff > 0 ? '▲' : '▼';
return { emoji, percent: `${diff > 0 ? '+' : ''}${percent}%`, diff };
}

async function readExampleContent(examplePath: string): Promise<string> {
try {
return await fs.readFile(examplePath, 'utf8');
} catch {
return '// Example content not found';
}
}

async function generateSingleTableReport(
prResults: typeof BenchmarkResults.infer,
targetResults: typeof BenchmarkResults.infer,
) {
const sharedBundlers = getSharedBundlers(prResults, targetResults);
const prGrouped = groupResultsByExample(prResults);
const targetGrouped = groupResultsByExample(targetResults);

// Get all unique examples from both branches
const allExamples = new Set([
...Object.keys(prGrouped),
...Object.keys(targetGrouped),
]);

let output = '# 📊 Bundle Size Comparison Report\n\n';

// Summary statistics
let totalIncrease = 0,
totalDecrease = 0,
totalUnchanged = 0;
const comparisons = [];

for (const example of allExamples) {
for (const bundler of sharedBundlers) {
const prSize = prGrouped[example]?.[bundler];
const targetSize = targetGrouped[example]?.[bundler];

if (prSize !== undefined && targetSize !== undefined) {
const trend = calculateTrend(prSize, targetSize);
if (trend.diff > 0) totalIncrease++;
else if (trend.diff < 0) totalDecrease++;
else totalUnchanged++;

comparisons.push({ example, bundler, prSize, targetSize, trend });
}
}
}

output += '## 📈 Summary\n\n';
output += `- 📈 **Increased**: ${totalIncrease} bundles\n`;
output += `- 📉 **Decreased**: ${totalDecrease} bundles\n`;
output += `- ➖ **Unchanged**: ${totalUnchanged} bundles\n\n`;

// Main comparison table
output += '## 📋 Bundle Size Comparison\n\n';

// Table header
output += '| Example';
for (const bundler of sharedBundlers) {
output += ` | ${bundler}`;
}
output += ' |\n';

// Table separator
output += '|---------';
for (const bundler of sharedBundlers) {
output += '|---------';
}
output += ' |\n';

// Table rows
for (const example of [...allExamples].sort()) {
output += `| ${example}`;

for (const bundler of sharedBundlers) {
const prSize = prGrouped[example]?.[bundler];
const targetSize = targetGrouped[example]?.[bundler];

if (prSize !== undefined && targetSize !== undefined) {
const trend = calculateTrend(prSize, targetSize);
output += ` | ${prSize}/${targetSize} ${trend.percent} ${trend.emoji}`;
} else if (prSize !== undefined) {
output += ` | ${prSize}/-`;
} else if (targetSize !== undefined) {
output += ` | -/${targetSize}`;
} else {
output += ' | -';
}
}
output += ' |\n';
}
output += '\n';

// Example code sections
output += '---\n\n';
output += '## 💻 Example Code\n\n';

for (const example of [...allExamples].sort()) {
const examplePath = `./examples/${example}`;
const exampleContent = await readExampleContent(examplePath);

output += `### ${example}\n\n`;
output += '```typescript\n';
output += exampleContent.trim();
output += '\n```\n\n';
}

return output;
}

async function generatePROnlyReport(prResults: typeof BenchmarkResults.infer) {
const grouped = groupResultsByExample(prResults);
const bundlers = [...new Set(prResults.map((r) => r.bundler))];
const examples = Object.keys(grouped).sort();

let output = '# 📊 Bundle Size Report (PR Branch Only)\n\n';

// Main results table
output += '## 📋 Bundle Sizes\n\n';

// Table header
output += '| Example';
for (const bundler of bundlers) {
output += ` | ${bundler}`;
}
output += ' |\n';

// Table separator
output += '|---------';
for (const bundler of bundlers) {
output += '|---------';
}
output += ' |\n';

// Table rows
for (const example of examples) {
output += `| ${example}`;
for (const bundler of bundlers) {
const size = grouped[example]?.[bundler];
output += size !== undefined ? ` | ${size}` : ' | -';
}
output += ' |\n';
}
output += '\n';

// Example code sections
output += '---\n\n';
output += '## 💻 Example Code\n\n';

for (const example of examples) {
const examplePath = `./examples/${example}`;
const exampleContent = await readExampleContent(examplePath);

output += `### ${example}\n\n`;
output += '```typescript\n';
output += exampleContent.trim();
output += '\n```\n\n';
}

return output;
}

async function main() {
const [prFile, targetFile] = process.argv.slice(2);

if (!prFile) {
console.error(
'Usage: compare-results.js <pr-results.json> [target-results.json]',
);
process.exit(1);
}

// Read and validate PR results
const prContent = await fs.readFile(prFile, 'utf8');
let prResults: typeof BenchmarkResults.infer;

try {
prResults = BenchmarkResults.assert(JSON.parse(prContent));
} catch (error) {
throw new Error('PR results validation failed', { cause: error });
}

// Try to read and validate target results
let targetResults: typeof BenchmarkResults.infer | null = null;
if (targetFile) {
try {
const targetContent = await fs.readFile(targetFile, 'utf8');
targetResults = BenchmarkResults.assert(JSON.parse(targetContent));
} catch (error) {
console.warn('Could not read or validate target results:', error);
}
}

// Generate appropriate report
let markdownReport;
if (targetResults && targetResults.length > 0) {
markdownReport = await generateSingleTableReport(prResults, targetResults);
} else {
markdownReport = await generatePROnlyReport(prResults);
}

console.log(markdownReport);
}

await main();
2 changes: 2 additions & 0 deletions apps/treeshake-test/examples/example1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { f32, sizeOf } from 'typegpu/data';
console.log(sizeOf(f32));
2 changes: 2 additions & 0 deletions apps/treeshake-test/examples/example2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import * as d from 'typegpu/data';
console.log(d.sizeOf(d.f32));
2 changes: 2 additions & 0 deletions apps/treeshake-test/examples/example3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import tgpu from 'typegpu';
console.log(tgpu.resolve({ externals: {} }));
Loading