Skip to content

Commit 285291d

Browse files
authored
feat(benchmarks): add canonical benchmark page (#125)
* feat(benchmarks): add canonical benchmark page * docs(benchmarks): add comparison context * docs(benchmarks): add external comparison table * style(theme): improve accent text contrast
1 parent 6e51f7f commit 285291d

31 files changed

Lines changed: 1781 additions & 123 deletions
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
name: Update Benchmarks
2+
3+
on:
4+
repository_dispatch:
5+
types: [benchmarks-update]
6+
workflow_dispatch:
7+
inputs:
8+
source_sha:
9+
description: "automem commit SHA, branch, or tag to sync"
10+
required: false
11+
reason:
12+
description: "Why benchmark data needs updating"
13+
required: false
14+
15+
jobs:
16+
update-benchmarks:
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: write
20+
pull-requests: write
21+
steps:
22+
- uses: actions/checkout@v6
23+
24+
- name: Resolve source ref
25+
id: resolve
26+
env:
27+
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
28+
run: |
29+
SOURCE_REPO="verygoodplugins/automem"
30+
SOURCE_REF="${{ github.event.client_payload.source_sha || github.event.inputs.source_sha || 'main' }}"
31+
REASON="${{ github.event.client_payload.reason || github.event.inputs.reason || 'Benchmark update' }}"
32+
FULL_SHA=$(gh api "repos/$SOURCE_REPO/commits/$SOURCE_REF" -q '.sha')
33+
SHORT_SHA="${FULL_SHA:0:7}"
34+
35+
echo "source_repo=$SOURCE_REPO" >> "$GITHUB_OUTPUT"
36+
echo "source_sha=$FULL_SHA" >> "$GITHUB_OUTPUT"
37+
echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
38+
echo "reason=$REASON" >> "$GITHUB_OUTPUT"
39+
40+
- name: Checkout automem source
41+
uses: actions/checkout@v6
42+
with:
43+
repository: ${{ steps.resolve.outputs.source_repo }}
44+
ref: ${{ steps.resolve.outputs.source_sha }}
45+
path: .source-repo
46+
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
47+
48+
- name: Setup Node.js
49+
uses: actions/setup-node@v6
50+
with:
51+
node-version: 24
52+
53+
- name: Install dependencies
54+
run: npm ci
55+
56+
- name: Sync benchmark data
57+
run: npm run sync-benchmarks -- --source .source-repo
58+
59+
- name: Build
60+
run: npm run build
61+
env:
62+
NODE_OPTIONS: "--max-old-space-size=8192"
63+
64+
- name: Detect changes
65+
id: diff
66+
run: |
67+
if git diff --quiet -- src/data/benchmarks.json; then
68+
echo "changed=false" >> "$GITHUB_OUTPUT"
69+
else
70+
echo "changed=true" >> "$GITHUB_OUTPUT"
71+
fi
72+
73+
- name: Create benchmark update PR
74+
if: steps.diff.outputs.changed == 'true'
75+
env:
76+
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
77+
SHORT_SHA: ${{ steps.resolve.outputs.short_sha }}
78+
FULL_SHA: ${{ steps.resolve.outputs.source_sha }}
79+
REASON: ${{ steps.resolve.outputs.reason }}
80+
run: |
81+
BRANCH="docs/benchmarks-$SHORT_SHA"
82+
git config user.name "github-actions[bot]"
83+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
84+
git checkout -b "$BRANCH"
85+
git add src/data/benchmarks.json
86+
git commit -m "docs(benchmarks): update data from automem@$SHORT_SHA"
87+
git push --set-upstream origin "$BRANCH"
88+
gh pr create \
89+
--title "docs(benchmarks): update benchmark data from automem@$SHORT_SHA" \
90+
--body "## Summary
91+
92+
- Syncs benchmark data from verygoodplugins/automem@$FULL_SHA
93+
- Source trigger: $REASON
94+
- Verified with npm run build
95+
96+
## Breaking changes
97+
98+
None." \
99+
--base main \
100+
--head "$BRANCH"

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ img/*.jpeg
3636
.dev.vars
3737

3838
# EmDash local data
39-
data/
39+
/data/
4040
uploads/
4141

4242
# TypeScript

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
"start": "astro dev",
99
"build": "rm -rf dist .astro node_modules/.astro .wrangler/deploy && node scripts/build-pages.mjs",
1010
"preview": "astro preview",
11+
"sync-benchmarks": "node scripts/sync-benchmarks.mjs",
12+
"test": "node --test tests/*.test.mjs",
13+
"test:benchmarks": "node --test tests/benchmarks-lib.test.mjs tests/sync-benchmarks.test.mjs",
1114
"astro": "astro",
1215
"check-links": "node scripts/check-links.js",
1316
"postinstall": "patch-package"

scripts/check-links.js

Lines changed: 101 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,41 +9,96 @@
99
import { LinkChecker } from 'linkinator';
1010
import { fileURLToPath } from 'url';
1111
import { dirname, join } from 'path';
12+
import http from 'http';
1213
import net from 'net';
1314
import fs from 'fs';
14-
import { spawn } from 'child_process';
15+
import { spawn, spawnSync } from 'child_process';
1516

1617
const __dirname = dirname(fileURLToPath(import.meta.url));
17-
const distDir = join(__dirname, '../dist');
1818

19-
// Check if dist exists
20-
if (!fs.existsSync(distDir)) {
21-
console.error('❌ dist/ directory not found. Run `npm run build` first.');
22-
process.exit(1);
19+
export function getServeDir(projectRoot = join(__dirname, '..')) {
20+
const distDir = join(projectRoot, 'dist');
21+
const cloudflareClientDir = join(distDir, 'client');
22+
23+
if (
24+
fs.existsSync(cloudflareClientDir) &&
25+
fs.existsSync(join(cloudflareClientDir, 'index.html'))
26+
) {
27+
return cloudflareClientDir;
28+
}
29+
30+
return distDir;
31+
}
32+
33+
export function getServeCommand(serveDir, port) {
34+
if (fs.existsSync(join(serveDir, '_worker.js'))) {
35+
return {
36+
label: 'Cloudflare Pages worker',
37+
command: 'npx',
38+
args: ['wrangler', 'pages', 'dev', serveDir, '--port', String(port)],
39+
};
40+
}
41+
42+
return {
43+
label: 'static files',
44+
command: 'npx',
45+
args: ['serve', serveDir, '-l', String(port)],
46+
};
2347
}
2448

2549
// Find an available port
2650
function findPort(startPort = 3456) {
2751
return new Promise((resolve) => {
2852
const server = net.createServer();
29-
server.listen(startPort, () => {
53+
server.listen(startPort, '127.0.0.1', () => {
3054
server.close(() => resolve(startPort));
3155
});
3256
server.on('error', () => resolve(findPort(startPort + 1)));
3357
});
3458
}
3559

60+
function killProcessesOnPort(port) {
61+
const result = spawnSync('lsof', [`-tiTCP:${port}`, '-sTCP:LISTEN'], {
62+
encoding: 'utf8',
63+
});
64+
65+
if (result.error || result.status !== 0 || !result.stdout?.trim()) {
66+
return;
67+
}
68+
69+
for (const pid of result.stdout.trim().split(/\s+/)) {
70+
try {
71+
process.kill(Number(pid), 'SIGKILL');
72+
} catch {
73+
// Ignore cleanup errors; the primary process may have exited already.
74+
}
75+
}
76+
}
77+
3678
// Wait for server to be ready
37-
async function waitForServer(port, maxAttempts = 20) {
79+
async function waitForServer(port, maxAttempts = 60) {
3880
for (let i = 0; i < maxAttempts; i++) {
3981
try {
4082
await new Promise((resolve, reject) => {
41-
const socket = new net.Socket();
42-
socket.setTimeout(500);
43-
socket.on('connect', () => { socket.destroy(); resolve(); });
44-
socket.on('error', reject);
45-
socket.on('timeout', () => { socket.destroy(); reject(); });
46-
socket.connect(port, 'localhost');
83+
const request = http.get({
84+
hostname: 'localhost',
85+
port,
86+
path: '/',
87+
timeout: 1000,
88+
}, (response) => {
89+
response.resume();
90+
if (response.statusCode && response.statusCode < 500) {
91+
resolve();
92+
} else {
93+
reject(new Error(`HTTP ${response.statusCode}`));
94+
}
95+
});
96+
97+
request.on('error', reject);
98+
request.on('timeout', () => {
99+
request.destroy();
100+
reject(new Error('HTTP readiness timed out'));
101+
});
47102
});
48103
return true;
49104
} catch {
@@ -54,14 +109,26 @@ async function waitForServer(port, maxAttempts = 20) {
54109
}
55110

56111
async function main() {
112+
const projectRoot = join(__dirname, '..');
113+
const serveDir = getServeDir(projectRoot);
114+
115+
// Check if dist exists
116+
if (!fs.existsSync(serveDir)) {
117+
console.error('❌ dist/ directory not found. Run `npm run build` first.');
118+
process.exit(1);
119+
}
120+
57121
const port = await findPort();
122+
const serveCommand = getServeCommand(serveDir, port);
58123
console.log(`🚀 Starting server on port ${port}...`);
124+
console.log(`📁 Serving ${serveDir} via ${serveCommand.label}`);
59125

60-
// Start serve in the background (no -s flag so 404s are real 404s)
61-
const server = spawn('npx', ['serve', 'dist', '-l', String(port)], {
62-
cwd: join(__dirname, '..'),
126+
// Start the built site in the background. Cloudflare adapter builds need the
127+
// Pages worker for dynamic routes; static builds can use serve directly.
128+
const server = spawn(serveCommand.command, serveCommand.args, {
129+
cwd: projectRoot,
63130
stdio: ['ignore', 'pipe', 'pipe'],
64-
detached: true
131+
detached: false
65132
});
66133

67134
let serverPid = server.pid;
@@ -72,6 +139,12 @@ async function main() {
72139
spawnError = err;
73140
console.error('❌ Failed to spawn server:', err);
74141
});
142+
server.stdout?.on('data', (chunk) => {
143+
if (process.env.CHECK_LINKS_DEBUG) process.stdout.write(chunk);
144+
});
145+
server.stderr?.on('data', (chunk) => {
146+
if (process.env.CHECK_LINKS_DEBUG) process.stderr.write(chunk);
147+
});
75148

76149
try {
77150
// Check if spawn failed before waiting
@@ -103,6 +176,8 @@ async function main() {
103176
'railway.com',
104177
'api.pirsch.io',
105178
'echodash.com', // Bot protection returns 403
179+
'github.com', // GitHub edit/source links often rate-limit CI
180+
'automem.ai', // Canonical/OG URLs point at production before deploy
106181
'localhost:4321', // Dev server references
107182
'localhost:4322',
108183
'localhost:4323',
@@ -128,15 +203,18 @@ async function main() {
128203
// Only kill if we have a valid PID
129204
if (serverPid && typeof serverPid === 'number') {
130205
try {
131-
process.kill(-serverPid);
206+
server.kill('SIGTERM');
132207
} catch (killErr) {
133208
// Ignore kill errors in cleanup
134209
}
135210
}
211+
killProcessesOnPort(port);
136212
}
137213
}
138214

139-
main().catch((err) => {
140-
console.error('Error:', err);
141-
process.exit(1);
142-
});
215+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
216+
main().catch((err) => {
217+
console.error('Error:', err);
218+
process.exit(1);
219+
});
220+
}

scripts/file-doc-map.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@
7575
"railway.json": ["deployment/railway"],
7676
"railway.toml": ["deployment/railway"],
7777
"README.md": ["overview", "getting-started/introduction"],
78+
"benchmarks/EXPERIMENT_LOG.md": ["site:/benchmarks", "overview", "core-concepts/hybrid-search", "research"],
79+
"benchmarks/publication/**": ["site:/benchmarks", "overview", "core-concepts/hybrid-search", "research"],
80+
"docs/BENCHMARK_JUDGE_POLICY.md": ["site:/benchmarks", "research", "development/testing"],
7881

7982
"scripts/health_monitor.py": ["operations/health"],
8083
"scripts/backup_automem.py": ["operations/backup"],

0 commit comments

Comments
 (0)