benchmark-next-package-dependency.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. /** Benchmark which additional Host package most reduces npm peer resolution. */
  2. import { availableParallelism } from 'node:os'
  3. import { resolve } from 'node:path'
  4. import { parseArgs } from 'node:util'
  5. import {
  6. benchmarkNpmResolution,
  7. buildRegistryIndex,
  8. parsePositiveIntegerOption,
  9. publishWorkspaceRange,
  10. type RegistryIndex,
  11. } from './benchmark-npm-resolution.ts'
  12. import {
  13. readPackageDependencyFacts,
  14. readPackageDependencyState,
  15. readWorkspacePackageManifests,
  16. repairPackageDependencyManifest,
  17. type PackageDependencyFacts,
  18. type WorkspacePackageManifest,
  19. } from './verify-package-dependencies.ts'
  20. const TARGET_PACKAGE = '@deepseek-ai/dsh'
  21. const CORDIS = '@deepseek-ai/cordis'
  22. interface Options {
  23. readonly candidates?: readonly string[]
  24. readonly coarseRuns: number
  25. readonly finalistRuns: number
  26. readonly finalists: number
  27. readonly jobs: number
  28. readonly timeoutMs: number
  29. }
  30. export interface MutableRegistryManifest {
  31. name: string
  32. version: string
  33. dependencies?: Record<string, string>
  34. optionalDependencies?: Record<string, string>
  35. peerDependencies?: Record<string, string>
  36. peerDependenciesMeta?: Record<string, { optional?: boolean }>
  37. }
  38. interface Measurement {
  39. readonly package: string
  40. readonly seconds: readonly number[]
  41. readonly medianSeconds: number
  42. }
  43. /** Parse benchmark selection and repetition options. */
  44. export function parseNextPackageBenchmarkOptions(args: readonly string[]): Options {
  45. const normalized = args[0] === '--' ? args.slice(1) : args
  46. const { values } = parseArgs({
  47. args: [...normalized],
  48. options: {
  49. candidates: { type: 'string' },
  50. runs: { type: 'string' },
  51. 'finalist-runs': { type: 'string' },
  52. finalists: { type: 'string' },
  53. jobs: { type: 'string' },
  54. 'timeout-ms': { type: 'string' },
  55. },
  56. allowPositionals: false,
  57. })
  58. return {
  59. ...(values.candidates === undefined
  60. ? {}
  61. : { candidates: values.candidates.split(',').filter(Boolean) }),
  62. coarseRuns: parsePositiveIntegerOption(values.runs, 1, '--runs'),
  63. finalistRuns: parsePositiveIntegerOption(values['finalist-runs'], 3, '--finalist-runs'),
  64. finalists: parsePositiveIntegerOption(values.finalists, 5, '--finalists'),
  65. jobs: parsePositiveIntegerOption(values.jobs, Math.min(8, availableParallelism()), '--jobs'),
  66. timeoutMs: parsePositiveIntegerOption(values['timeout-ms'], 120_000, '--timeout-ms'),
  67. }
  68. }
  69. function median(values: readonly number[]): number {
  70. const sorted = [...values].sort((left, right) => left - right)
  71. const middle = Math.floor(sorted.length / 2)
  72. return sorted.length % 2 === 0
  73. ? ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2
  74. : sorted[middle] ?? 0
  75. }
  76. function cloneIndex(index: RegistryIndex): Map<string, Map<string, MutableRegistryManifest>> {
  77. return new Map([...index].map(([name, versions]) => [
  78. name,
  79. new Map([...versions].map(([version, manifest]) => [
  80. version,
  81. structuredClone(manifest) as MutableRegistryManifest,
  82. ])),
  83. ]))
  84. }
  85. function publishedSection(
  86. values: Readonly<Record<string, string>> | undefined,
  87. workspaceVersions: ReadonlyMap<string, string>,
  88. ): Record<string, string> | undefined {
  89. if (values === undefined) return undefined
  90. return Object.fromEntries(Object.entries(values).map(([name, range]) => {
  91. const version = workspaceVersions.get(name)
  92. return [name, version === undefined ? range : publishWorkspaceRange(range, version)]
  93. }))
  94. }
  95. /** Apply one source-derived policy result to an in-memory registry manifest. */
  96. export function applyFactsToRegistry(
  97. index: Map<string, Map<string, MutableRegistryManifest>>,
  98. facts: PackageDependencyFacts,
  99. workspaceVersions: ReadonlyMap<string, string>,
  100. ): void {
  101. const source = structuredClone(facts.manifest)
  102. repairPackageDependencyManifest({ ...facts, manifest: source })
  103. const version = workspaceVersions.get(source.name ?? '')
  104. const target = version === undefined ? undefined : index.get(source.name ?? '')?.get(version)
  105. if (target === undefined) throw new Error(`local registry has no ${source.name ?? 'unnamed package'}@${version ?? 'unknown'}`)
  106. for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies'] as const) {
  107. const values = publishedSection(source[field], workspaceVersions)
  108. if (values !== undefined) target[field] = values
  109. else if (field === 'dependencies') delete target.dependencies
  110. else if (field === 'optionalDependencies') delete target.optionalDependencies
  111. else delete target.peerDependencies
  112. }
  113. if (source.peerDependenciesMeta === undefined) delete target.peerDependenciesMeta
  114. else target.peerDependenciesMeta = structuredClone(source.peerDependenciesMeta) as Record<string, { optional?: boolean }>
  115. }
  116. function currentVersion(pkg: WorkspacePackageManifest): string {
  117. const version = pkg.manifest.version
  118. if (typeof version !== 'string') throw new Error(`${pkg.manifestPath}: missing package version`)
  119. return version
  120. }
  121. /** Find reachable Host candidates whose published manifests still carry non-Cordis peers. */
  122. export function discoverBenchmarkCandidates(
  123. index: RegistryIndex,
  124. workspaceVersions: ReadonlyMap<string, string>,
  125. releasePackages: ReadonlyMap<string, WorkspacePackageManifest>,
  126. policyPackages: ReadonlySet<string>,
  127. ): string[] {
  128. const reached = new Set<string>()
  129. const queue = [TARGET_PACKAGE]
  130. for (let cursor = 0; cursor < queue.length; cursor += 1) {
  131. const name = queue[cursor]
  132. if (name === undefined || reached.has(name)) continue
  133. const version = workspaceVersions.get(name)
  134. const manifest = version === undefined ? undefined : index.get(name)?.get(version)
  135. if (manifest === undefined) continue
  136. reached.add(name)
  137. const installed = {
  138. ...manifest.dependencies,
  139. ...manifest.optionalDependencies,
  140. ...Object.fromEntries(Object.entries(manifest.peerDependencies ?? {})
  141. .filter(([peer]) => (manifest.peerDependenciesMeta?.[peer] as { optional?: boolean } | undefined)?.optional !== true)),
  142. }
  143. for (const dependency of Object.keys(installed).sort()) {
  144. if (!reached.has(dependency)) queue.push(dependency)
  145. }
  146. }
  147. return [...reached].filter((name) => {
  148. if (policyPackages.has(name) || !releasePackages.has(name)) return false
  149. const version = workspaceVersions.get(name)
  150. const manifest = version === undefined ? undefined : index.get(name)?.get(version)
  151. return Object.keys(manifest?.peerDependencies ?? {}).some(peer => peer !== CORDIS)
  152. }).sort()
  153. }
  154. async function measure(
  155. index: RegistryIndex,
  156. targetVersion: string,
  157. runs: number,
  158. timeoutMs: number,
  159. ): Promise<number[]> {
  160. const seconds: number[] = []
  161. for (let run = 0; run < runs; run += 1) {
  162. const result = await benchmarkNpmResolution(index, targetVersion, timeoutMs)
  163. if (result.archiveRequests > 0) throw new Error('metadata-only benchmark requested package archives')
  164. seconds.push(Number((result.durationMs / 1000).toFixed(2)))
  165. }
  166. return seconds
  167. }
  168. async function mapConcurrent<T, R>(
  169. values: readonly T[],
  170. jobs: number,
  171. operation: (value: T) => Promise<R>,
  172. ): Promise<R[]> {
  173. const results: R[] = []
  174. let next = 0
  175. await Promise.all(Array.from({ length: Math.min(jobs, values.length) }, async () => {
  176. while (next < values.length) {
  177. const index = next
  178. next += 1
  179. const value = values[index]
  180. if (value === undefined) return
  181. results[index] = await operation(value)
  182. }
  183. }))
  184. return results
  185. }
  186. async function main(): Promise<void> {
  187. const options = parseNextPackageBenchmarkOptions(process.argv.slice(2))
  188. const root = resolve(import.meta.dirname, '..')
  189. const packages = readWorkspacePackageManifests(root)
  190. const workspaceVersions = new Map(packages.all.map(pkg => [pkg.name, currentVersion(pkg)]))
  191. const releaseByName = new Map(packages.release.map(pkg => [pkg.name, pkg]))
  192. const state = readPackageDependencyState(root)
  193. if (state.policyViolations.length > 0) throw new Error(state.policyViolations.join('\n'))
  194. const base = cloneIndex(buildRegistryIndex(root))
  195. for (const facts of state.facts) applyFactsToRegistry(base, facts, workspaceVersions)
  196. const targetVersion = workspaceVersions.get(TARGET_PACKAGE)
  197. if (targetVersion === undefined) throw new Error(`workspace has no ${TARGET_PACKAGE}`)
  198. const policyNames = new Set(state.facts.map(facts => facts.manifest.name).filter(name => name !== undefined))
  199. const discovered = discoverBenchmarkCandidates(base, workspaceVersions, releaseByName, policyNames)
  200. const candidates = options.candidates ?? discovered
  201. for (const name of candidates) {
  202. if (!discovered.includes(name)) throw new Error(`${name} is not a reachable unconfigured Host candidate`)
  203. }
  204. const candidateFacts = new Map(candidates.map((name) => {
  205. const pkg = releaseByName.get(name)
  206. if (pkg === undefined) throw new Error(`release set has no ${name}`)
  207. return [name, readPackageDependencyFacts(root, pkg, 'configured-host', state.workspaceNames)]
  208. }))
  209. const baselineSeconds = await measure(base, targetVersion, options.finalistRuns, options.timeoutMs)
  210. const baseline = median(baselineSeconds)
  211. console.log(JSON.stringify({ type: 'baseline', seconds: baselineSeconds, medianSeconds: baseline }))
  212. const coarse = await mapConcurrent(candidates, options.jobs, async (name): Promise<Measurement> => {
  213. const index = cloneIndex(base)
  214. const facts = candidateFacts.get(name)
  215. if (facts === undefined) throw new Error(`missing source facts for ${name}`)
  216. applyFactsToRegistry(index, facts, workspaceVersions)
  217. const seconds = await measure(index, targetVersion, options.coarseRuns, options.timeoutMs)
  218. const result = { package: name, seconds, medianSeconds: median(seconds) }
  219. console.log(JSON.stringify({ type: 'coarse', ...result }))
  220. return result
  221. })
  222. const finalists = coarse.sort((left, right) => left.medianSeconds - right.medianSeconds)
  223. .slice(0, options.finalists)
  224. const measured: Measurement[] = []
  225. for (const finalist of finalists) {
  226. const index = cloneIndex(base)
  227. const facts = candidateFacts.get(finalist.package)
  228. if (facts === undefined) throw new Error(`missing source facts for ${finalist.package}`)
  229. applyFactsToRegistry(index, facts, workspaceVersions)
  230. const seconds = await measure(index, targetVersion, options.finalistRuns, options.timeoutMs)
  231. measured.push({ package: finalist.package, seconds, medianSeconds: median(seconds) })
  232. }
  233. const ranking = measured.sort((left, right) => left.medianSeconds - right.medianSeconds)
  234. .map(result => ({
  235. ...result,
  236. gainSeconds: Number((baseline - result.medianSeconds).toFixed(2)),
  237. }))
  238. console.log(JSON.stringify({
  239. type: 'result',
  240. baselineSeconds,
  241. baselineMedianSeconds: baseline,
  242. candidateCount: candidates.length,
  243. ranking,
  244. }, null, 2))
  245. }
  246. if (import.meta.main) {
  247. try {
  248. await main()
  249. } catch (error) {
  250. console.error(`benchmark-next-package-dependency: ${error instanceof Error ? error.message : String(error)}`)
  251. process.exitCode = 1
  252. }
  253. }