构建、测试快照与性能基准
本页说明仓库中用于构建本地 npm registry 元数据、执行依赖解析测试快照,以及比较 npm 解析性能的基准工具。当前实现的核心是 scripts/benchmark-npm-resolution.ts 与 scripts/benchmark-next-package-dependency.ts;仓库中的运行入口说明位于 BENCHMARK.md。
Purpose and Scope
本页覆盖以下闭环:
- 从 workspace、已安装的 pnpm virtual store 或指定 Git ref 收集 package manifest;
- 将 workspace 协议范围转换为发布到 registry 后的 semver 范围;
- 构建仅包含元数据的本地 registry index;
- 通过 npm 生成依赖解析结果和 package-lock 布局,作为“测试快照”式的可检查结果;
- 测量解析耗时、registry 请求、archive 请求和未知包;
- 使用粗筛选、候选包注入和 finalist 重复测量来寻找最能减少 peer resolution 的 Host package。
本页不覆盖 Python SDK 的安装细节、jsonrpc-agent 的业务行为,也不把一般 package dependency policy 检查扩展为本页主题。BENCHMARK.md 只给出了运行前置条件;更完整的 SDK 使用方式应留在对应的 SDK 文档页。
Overview
该能力把“真实发布包的依赖元数据”与“npm 的解析算法”隔离开来:工具不直接下载待测包 archive,而是启动本地 HTTP registry,提供从 workspace manifest 和已安装依赖中构造出的 package metadata。解析完成后,工具读取 npm 输出的 package-lock,并将 registry 请求、archive 请求、未知包和耗时作为观测结果返回。
基准脚本进一步在内存中复制 registry index,对候选 Host package 应用经过 source-derived policy 修复的 manifest,然后重复执行 npm resolution。这样可以比较“加入某个 Host package 后,解析是否更快”,而不修改工作区文件或真实 registry 数据。
Architecture
图中的 buildRegistryIndex、applyFactsToRegistry、benchmarkNpmResolution 和 NpmPackageLockResolution 都是源码中的真实符号。RegistryIndex 先成为解析服务的元数据来源,再被 npm 解析过程消费;package-lock 既是 npm 选择出的安装布局,也是基准结果中可进一步检查的快照对象。
Source: benchmark-npm-resolution.ts Source: benchmark-next-package-dependency.ts
元数据索引与发布语义
RegistryIndex 的数据边界
scripts/benchmark-npm-resolution.ts 将 registry index 定义为 Map<packageName, Map<version, RegistryVersion>> 的只读视图。每个 RegistryVersion 至少包含 name 与 version,并可携带 dependencies、optionalDependencies、peerDependencies、peerDependenciesMeta、engines、平台限制和 bin 信息。构建时只保留 PUBLISHED_FIELDS 中的字段,因此基准关注的是 npm 解析真正需要的已发布元数据,而不是完整工作区文件。
工作区依赖不能直接把 workspace:*、workspace:^ 和 workspace:~ 交给 registry。publishWorkspaceRange 按当前 workspace 版本转换它们:* 变成精确版本,^ 和 ~ 分别变成对应的 semver 范围;其他 workspace: 前缀则去掉前缀。这一步模拟了 pnpm pack 后发布 manifest 的依赖表达,避免基准因使用了非 registry 语法而失真。
1export function publishWorkspaceRange(range: string, targetVersion: string): string {
2 if (range === 'workspace:*') return targetVersion
3 if (range === 'workspace:^') return `^${targetVersion}`
4 if (range === 'workspace:~') return `~${targetVersion}`
5 if (range.startsWith('workspace:')) return range.slice('workspace:'.length)
6 return range
7}Source: benchmark-npm-resolution.ts
buildRegistryIndex(root, ref?) 有两种输入路径:没有 ref 时,从工作树 glob 找 workspace manifest;有 ref 时,通过 git ls-tree 找指定 ref 中的 manifest,再用 git cat-file --batch 批量读取内容。外部依赖则从 pnpm virtual store 的 manifest glob 中收集。由此,基准可以测当前工作树,也可以复现历史 Git ref,而不需要将历史版本检出到工作区。
Source-derived policy 的应用
next-package benchmark 先读取依赖状态。如果存在 policyViolations,主流程立即抛出错误,不继续测量。否则,它克隆基础 index,并对每条 PackageDependencyFacts 调用 applyFactsToRegistry。该函数深拷贝 manifest,运行 repairPackageDependencyManifest,按照 workspace 当前版本重写依赖范围,然后覆盖 registry 中同名同版本的 dependencies、optionalDependencies、peerDependencies 和 peer metadata。若目标版本不存在,函数抛出明确错误,防止基准悄悄使用不完整的 registry。
1export function applyFactsToRegistry(
2 index: Map<string, Map<string, MutableRegistryManifest>>,
3 facts: PackageDependencyFacts,
4 workspaceVersions: ReadonlyMap<string, string>,
5): void {
6 const source = structuredClone(facts.manifest)
7 repairPackageDependencyManifest({ ...facts, manifest: source })
8 const version = workspaceVersions.get(source.name ?? '')
9 const target = version === undefined ? undefined : index.get(source.name ?? '')?.get(version)
10 if (target === undefined) throw new Error(`local registry has no ${source.name ?? 'unnamed package'}@${version ?? 'unknown'}`)
11 for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies'] as const) {
12 const values = publishedSection(source[field], workspaceVersions)
13 if (values !== undefined) target[field] = values
14 else if (field === 'dependencies') delete target.dependencies
15 else if (field === 'optionalDependencies') delete target.optionalDependencies
16 else delete target.peerDependencies
17 }
18 if (source.peerDependenciesMeta === undefined) delete target.peerDependenciesMeta
19 else target.peerDependenciesMeta = structuredClone(source.peerDependenciesMeta) as Record<string, { optional?: boolean }>
20}这种“先 clone、再修复、最后覆盖”的顺序很重要:每个候选项都应基于相同基础 registry 独立比较,不能把前一个候选的修改泄漏给后一个候选。
核心流程:解析快照与基准测量
Source: benchmark-npm-resolution.ts
一次解析的结果类型 BenchmarkRun 包含四类观测:durationMs、registryRequests、archiveRequests 和 unknownPackages。扩展类型 NpmPackageLockResolution 再附加 packageLock,其 packages 字段记录 npm 最终选择的安装路径布局。这个设计把“性能数字”和“解析是否得到预期布局”放在同一个结果中,便于测试脚本同时检查速度与正确性。
next-package benchmark 的 measure 函数按指定次数串行执行 benchmarkNpmResolution,每次明确拒绝 archiveRequests > 0 的结果,并把毫秒转换为保留两位小数的秒数。串行重复让同一候选的测量次数可解释;候选之间则通过 mapConcurrent 使用有限 worker 并发,提高整体搜索速度。
1async function measure(
2 index: RegistryIndex,
3 targetVersion: string,
4 runs: number,
5 timeoutMs: number,
6): Promise<number[]> {
7 const seconds: number[] = []
8 for (let run = 0; run < runs; run += 1) {
9 const result = await benchmarkNpmResolution(index, targetVersion, timeoutMs)
10 if (result.archiveRequests > 0) throw new Error('metadata-only benchmark requested package archives')
11 seconds.push(Number((result.durationMs / 1000).toFixed(2)))
12 }
13 return seconds
14}候选发现与两阶段搜索
discoverBenchmarkCandidates 从 @deepseek-ai/dsh 开始做队列遍历。对每个可达包,它合并普通依赖、可选依赖以及非 optional peer dependency,并把尚未访问的依赖加入队列。遍历结束后,只保留:不是 policy package、存在 release manifest、且当前 manifest 至少含有一个非 @deepseek-ai/cordis peer dependency 的包。这使搜索空间限定在实际可达、可发布并与目标 peer-resolution 问题相关的 Host package。
next-package 的选项默认值来自实际解析代码:粗筛 coarseRuns 为 1,finalist 测量为 3,保留 finalist 数为 5,jobs 默认是 min(8, availableParallelism()),超时默认 120000 ms。若传入 --candidates,每个候选必须属于自动发现集合,否则直接报错。
1return {
2 ...(values.candidates === undefined
3 ? {}
4 : { candidates: values.candidates.split(',').filter(Boolean) }),
5 coarseRuns: parsePositiveIntegerOption(values.runs, 1, '--runs'),
6 finalistRuns: parsePositiveIntegerOption(values['finalist-runs'], 3, '--finalist-runs'),
7 finalists: parsePositiveIntegerOption(values.finalists, 5, '--finalists'),
8 jobs: parsePositiveIntegerOption(values.jobs, Math.min(8, availableParallelism()), '--jobs'),
9 timeoutMs: parsePositiveIntegerOption(values['timeout-ms'], 120_000, '--timeout-ms'),
10}配置与 CLI 参数
| 工具 | 选项 | 类型 | 默认值 | 语义 |
|---|---|---|---|---|
benchmark-npm-resolution | --ref | string | 未设置 | 从指定 Git ref 读取 workspace manifests |
benchmark-npm-resolution | --runs | 正整数 | 1 | 单次基准重复次数 |
benchmark-npm-resolution | --timeout-ms | 正整数 | 300000 | 单次 npm resolution 超时 |
benchmark-npm-resolution | --max-ms | 正整数,可选 | 未设置 | 可选的最大耗时限制 |
benchmark-next-package-dependency | --candidates | 逗号分隔字符串 | 自动发现 | 指定待测 Host packages |
benchmark-next-package-dependency | --runs | 正整数 | 1 | 候选粗筛重复次数 |
benchmark-next-package-dependency | --finalist-runs | 正整数 | 3 | finalist 重复次数 |
benchmark-next-package-dependency | --finalists | 正整数 | 5 | 保留 finalist 数量 |
benchmark-next-package-dependency | --jobs | 正整数 | min(8, availableParallelism()) | 候选间并发 worker 数 |
benchmark-next-package-dependency | --timeout-ms | 正整数 | 120000 | 候选测量超时 |
所有正整数选项都经过 parsePositiveIntegerOption 校验:缺省时使用 fallback;显式值必须是 safe integer、至少为 1,且字符串不能包含额外格式,否则抛出带选项名的错误。--max-ms 在底层脚本中是可选限制;它与 --timeout-ms 不应混为同一概念。
API Reference
parseBenchmarkOptions(args: readonly string[]): BenchmarkOptions
解析底层 npm resolution benchmark 的 --ref、--runs、--timeout-ms 和 --max-ms。返回经验证的 BenchmarkOptions;非法正整数会抛出 Error。
buildRegistryIndex(root: string, ref?: string): RegistryIndex
从工作树或指定 Git ref 构建 registry 元数据索引。root 必须是包含 workspace 与 pnpm virtual store 的仓库根目录;指定 ref 时,读取的是 Git 对象中的 manifest,而非工作树版本。
benchmarkNpmResolution(index: RegistryIndex, targetVersion: string, timeoutMs?: number): Promise<BenchmarkRun>
使用给定的 registry metadata 对目标版本执行 npm 解析,并返回耗时、请求统计、未知包及解析结果相关观测。底层实现的具体 HTTP server 生命周期和 npm 子进程细节在本次读取范围之外;这些细节不在本页臆测。
parseNextPackageBenchmarkOptions(args: readonly string[]): Options
解析候选搜索的重复次数、候选数、并发数和超时。它还将逗号分隔的 --candidates 转换为去除空值的数组。
失败模式、边界条件与并发
输入和一致性错误
parsePositiveIntegerOption对非正整数、超出 safe integer 的值、带前导或尾随格式差异的字符串抛出错误;因此 CLI 不会把模糊参数静默解释成其他次数。buildRegistryIndex在读取 Git ref 时会检查git cat-file --batch的 header、对象大小、missing 标记和结尾换行;读取缺失或截断对象会失败,而不是生成不完整索引。applyFactsToRegistry找不到 workspace package 对应的 registry name/version 时立即抛错。这避免把 policy 修复应用到错误版本。benchmark-next-package-dependency在发现 policy violation 时停止;显式传入不可达或未配置的 candidate 也会停止。- metadata-only 基准一旦观测到 archive 请求就失败。该约束是核心正确性检查:如果发生 archive 请求,测量的就不再是纯依赖元数据解析。
并发边界
mapConcurrent 创建不超过 min(jobs, values.length) 个 worker,共享递增的 next 游标,每个 worker 处理一个候选后继续领取下一个。结果按原候选索引写回,因此并发不会改变输出排序。源码没有显示跨进程共享状态或持久化缓存;可以确认的并发范围仅是同一进程内候选 benchmark 的 worker 并发。
性能与运营注意事项
- 底层 benchmark 默认单次超时 300 秒;候选 benchmark 默认 120 秒。运营上应根据 CI 预算显式设置 timeout,而不是假设两个脚本相同。
- 粗筛与 finalist 分离,目的在于先低成本比较所有候选,再对少数候选重复测量并计算 median。源码中
Measurement持有原始秒数和medianSeconds,因此最终排序应优先使用中位数而不是单个样本。 structuredClone用于复制 manifest 和 registry index,换取候选之间的隔离;代价是内存复制,但避免了基准污染。availableParallelism()只用于设置候选搜索的默认 worker 上限,并额外限制为 8。它不是 npm 解析过程的并发配置。BENCHMARK.md要求使用独立 workspace 和 session ID 执行独立 benchmark task;该操作约束是当前仓库运行说明中明确给出的隔离策略。
测试快照的解释边界
本实现没有在已读取源码中发现一个名为 snapshot 的持久化文件格式或独立 snapshot writer。可以确认的“快照”对象是 NpmPackageLockResolution.packageLock:它记录 npm 在本次解析中选出的 package path 与 manifest 关系,并与请求计数和耗时一起返回。若需要断言依赖树,建议围绕该 packageLock.packages 结构进行测试;具体测试断言和 fixture 不在本次源代码摘录中,因此不在此虚构测试命令或预期内容。
扩展点
- 增加发布字段:如果解析场景需要新的 package.json 发布字段,应在
PUBLISHED_FIELDS增加字段,并确认PackageManifest、RegistryVersion和复制逻辑都能保留它。 - 改变 workspace 转换策略:修改
publishWorkspaceRange时必须同时考虑 workspace manifest 的发布语义与 registry 解析兼容性;这会直接影响所有 benchmark 结果。 - 增加候选筛选策略:候选发现当前以从
@deepseek-ai/dsh可达、具有 release manifest 和非 Cordis peer 为边界。新增筛选条件应保持可解释,并避免把不可达 package 纳入比较。 - 扩展观测结果:
BenchmarkRun已将耗时、registry/archive 请求与 unknown packages 分开表达;可在此基础上增加观测字段,但应保持 metadata-only 检查清晰可见。
运行入口与相关链接
BENCHMARK.md 的现有说明要求先按照 Python SDK 指南安装 SDK 并运行 jsonrpc-agent minimal variant,然后为独立 benchmark 使用独立 workspace 和 session ID。底层 benchmark 的参数与实现请以 benchmark-npm-resolution.ts 为准;候选搜索和 policy 注入请以 benchmark-next-package-dependency.ts 为准。