Skip to content

Commit 6069ffc

Browse files
authored
ci: add CodSpeed benchmarks for intent CLI (#114)
* ci: add CodSpeed benchmarks for intent CLI * setup complete
1 parent e3569ff commit 6069ffc

10 files changed

Lines changed: 1031 additions & 8 deletions

File tree

.github/workflows/benchmarks.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Benchmarks
2+
3+
on:
4+
push:
5+
branches:
6+
- 'main'
7+
paths:
8+
- 'packages/**'
9+
- 'benchmarks/**'
10+
- 'pnpm-lock.yaml'
11+
- 'pnpm-workspace.yaml'
12+
pull_request:
13+
paths:
14+
- 'packages/**'
15+
- 'benchmarks/**'
16+
- 'pnpm-lock.yaml'
17+
- 'pnpm-workspace.yaml'
18+
workflow_dispatch:
19+
20+
permissions:
21+
contents: read
22+
id-token: write
23+
24+
env:
25+
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
26+
NX_NO_CLOUD: true
27+
28+
jobs:
29+
benchmarks:
30+
name: Run intent CodSpeed benchmark
31+
runs-on: ubuntu-latest
32+
steps:
33+
- name: Checkout
34+
uses: actions/checkout@v6.0.1
35+
36+
- name: Setup Tools
37+
uses: tanstack/config/.github/setup@main
38+
39+
- name: Run intent CodSpeed benchmark
40+
continue-on-error: true
41+
uses: CodSpeedHQ/action@v4
42+
with:
43+
mode: simulation
44+
run: WITH_INSTRUMENTATION=1 pnpm exec nx run @benchmarks/intent:test:perf

benchmarks/intent/helpers.ts

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
2+
import { tmpdir } from 'node:os'
3+
import { dirname, join } from 'node:path'
4+
5+
let builtCliMainPromise: Promise<
6+
(argv?: Array<string>) => Promise<number>
7+
> | null = null
8+
9+
type CliRunnerOptions = {
10+
cwd: string
11+
globalNodeModules?: string
12+
}
13+
14+
type ConsoleSnapshot = {
15+
error: typeof console.error
16+
info: typeof console.info
17+
log: typeof console.log
18+
warn: typeof console.warn
19+
}
20+
21+
export type SkillOptions = {
22+
description: string
23+
bodyLines?: number
24+
type?: 'core' | 'framework'
25+
requires?: Array<string>
26+
libraryVersion?: string
27+
sources?: Array<string>
28+
}
29+
30+
export type PackageOptions = {
31+
dependencies?: Record<string, string>
32+
peerDependencies?: Record<string, string>
33+
skills?: Array<string>
34+
requires?: Array<string>
35+
useDerivedIntent?: boolean
36+
brokenIntent?: boolean
37+
}
38+
39+
const noop = () => undefined
40+
41+
export function createBenchOptions(
42+
setup: () => void | Promise<void>,
43+
teardown: () => void | Promise<void>,
44+
) {
45+
return {
46+
warmupIterations: 100,
47+
time: 10_000,
48+
setup,
49+
teardown,
50+
}
51+
}
52+
53+
export function createConsoleSilencer() {
54+
let snapshot: ConsoleSnapshot | null = null
55+
56+
return {
57+
silence() {
58+
if (snapshot) return
59+
60+
snapshot = {
61+
log: console.log,
62+
info: console.info,
63+
warn: console.warn,
64+
error: console.error,
65+
}
66+
67+
console.log = noop as typeof console.log
68+
console.info = noop as typeof console.info
69+
console.warn = noop as typeof console.warn
70+
console.error = noop as typeof console.error
71+
},
72+
restore() {
73+
if (!snapshot) return
74+
75+
console.log = snapshot.log
76+
console.info = snapshot.info
77+
console.warn = snapshot.warn
78+
console.error = snapshot.error
79+
snapshot = null
80+
},
81+
}
82+
}
83+
84+
export function createCliRunner(options: CliRunnerOptions) {
85+
let main: ((argv?: Array<string>) => Promise<number>) | null = null
86+
let previousCwd = ''
87+
let previousGlobalNodeModules: string | undefined
88+
89+
return {
90+
async setup() {
91+
if (main) return
92+
93+
previousCwd = process.cwd()
94+
previousGlobalNodeModules = process.env.INTENT_GLOBAL_NODE_MODULES
95+
96+
process.chdir(options.cwd)
97+
if (options.globalNodeModules) {
98+
process.env.INTENT_GLOBAL_NODE_MODULES = options.globalNodeModules
99+
} else {
100+
delete process.env.INTENT_GLOBAL_NODE_MODULES
101+
}
102+
103+
main = await loadBuiltCliMain()
104+
},
105+
teardown() {
106+
if (!main) return
107+
108+
process.chdir(previousCwd)
109+
if (previousGlobalNodeModules === undefined) {
110+
delete process.env.INTENT_GLOBAL_NODE_MODULES
111+
} else {
112+
process.env.INTENT_GLOBAL_NODE_MODULES = previousGlobalNodeModules
113+
}
114+
115+
previousCwd = ''
116+
previousGlobalNodeModules = undefined
117+
main = null
118+
},
119+
async run(argv: Array<string>) {
120+
if (!main) {
121+
throw new Error('CLI runner must be set up before running benchmarks')
122+
}
123+
124+
const exitCode = await main(argv)
125+
if (exitCode !== 0) {
126+
throw new Error(
127+
`intent ${argv.join(' ')} failed with exit code ${exitCode}`,
128+
)
129+
}
130+
},
131+
}
132+
}
133+
134+
async function loadBuiltCliMain(): Promise<
135+
(argv?: Array<string>) => Promise<number>
136+
> {
137+
builtCliMainPromise ??= import('../../packages/intent/dist/cli.mjs').then(
138+
(module) => {
139+
if (typeof module.main !== 'function') {
140+
throw new TypeError(
141+
'Expected packages/intent/dist/cli.mjs to export main()',
142+
)
143+
}
144+
145+
return module.main as (argv?: Array<string>) => Promise<number>
146+
},
147+
)
148+
149+
return builtCliMainPromise
150+
}
151+
152+
export function createTempDir(name: string): string {
153+
return mkdtempSync(join(tmpdir(), `intent-bench-${name}-`))
154+
}
155+
156+
export function writeFile(filePath: string, content: string): void {
157+
mkdirSync(dirname(filePath), { recursive: true })
158+
writeFileSync(filePath, content)
159+
}
160+
161+
export function writeJson(filePath: string, value: unknown): void {
162+
writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`)
163+
}
164+
165+
export function writeSkill(
166+
root: string,
167+
skillName: string,
168+
options: SkillOptions,
169+
): void {
170+
const frontmatter = [
171+
`name: ${JSON.stringify(skillName)}`,
172+
`description: ${JSON.stringify(options.description)}`,
173+
]
174+
175+
if (options.type) {
176+
frontmatter.push(`type: ${JSON.stringify(options.type)}`)
177+
}
178+
179+
if (options.requires) {
180+
frontmatter.push('requires:')
181+
for (const requirement of options.requires) {
182+
frontmatter.push(` - ${JSON.stringify(requirement)}`)
183+
}
184+
}
185+
186+
if (options.libraryVersion) {
187+
frontmatter.push(
188+
`library_version: ${JSON.stringify(options.libraryVersion)}`,
189+
)
190+
}
191+
192+
if (options.sources) {
193+
frontmatter.push('sources:')
194+
for (const source of options.sources) {
195+
frontmatter.push(` - ${JSON.stringify(source)}`)
196+
}
197+
}
198+
199+
const bodyLines = Array.from(
200+
{ length: options.bodyLines ?? 12 },
201+
(_, index) =>
202+
`${index + 1}. Keep ${skillName} aligned with the documented workflow.`,
203+
)
204+
205+
writeFile(
206+
join(root, 'skills', ...skillName.split('/'), 'SKILL.md'),
207+
`---\n${frontmatter.join('\n')}\n---\n\n${bodyLines.join('\n')}\n`,
208+
)
209+
}
210+
211+
export function writePackage(
212+
nodeModulesDir: string,
213+
name: string,
214+
version: string,
215+
options: PackageOptions,
216+
): void {
217+
const packageRoot = join(nodeModulesDir, ...name.split('/'))
218+
const packageJson: Record<string, unknown> = {
219+
name,
220+
version,
221+
dependencies: options.dependencies,
222+
peerDependencies: options.peerDependencies,
223+
}
224+
225+
if (options.skills?.length) {
226+
if (options.brokenIntent) {
227+
packageJson.description = `Broken skill fixture for ${name}`
228+
} else if (options.useDerivedIntent) {
229+
const packageSlug = name.replace('@', '').replace('/', '-')
230+
packageJson.repository = `https://github.com/example/${packageSlug}`
231+
packageJson.homepage = `https://example.com/${packageSlug}`
232+
} else {
233+
packageJson.intent = {
234+
version: 1,
235+
repo: `example/${name.replace('@', '').replace('/', '-')}`,
236+
docs: 'docs/',
237+
requires: options.requires,
238+
}
239+
}
240+
}
241+
242+
writeJson(join(packageRoot, 'package.json'), packageJson)
243+
244+
for (const skill of options.skills ?? []) {
245+
writeSkill(packageRoot, skill, {
246+
description: `${skill} benchmark guidance`,
247+
bodyLines: 14,
248+
type: skill.includes('/') ? 'framework' : 'core',
249+
requires: skill.includes('/') ? [skill.split('/')[0]!] : undefined,
250+
})
251+
}
252+
}

0 commit comments

Comments
 (0)