Skip to content
Merged
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
84 changes: 47 additions & 37 deletions benchmarkFixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,30 +222,37 @@ async function writeRegistryConfig (pm, cwd, opts) {
* installs from one (see `tarRegistryUrl` in `pnprServer.js`).
*/
export function pnpmWorkspaceYaml (opts = {}) {
let yaml = "packages:\n - '.'\ntrustLockfile: true\n"
if (opts.pnprServer) {
yaml += `pnprServer: ${opts.pnprServer}\n`
}
if (opts.tarRegistries?.length) {
yaml += 'registries:\n'
opts.tarRegistries.forEach((url, i) => {
yaml += ` ${url}:\n`
// The first is the one the project installs from, so the default
// scope routes to it; any other is the same registry under another
// address, declared so the resolver knows what it serves too.
if (i === 0) yaml += " scopes: ['@']\n"
// A tar registry's metadata names no tarball URL: the client rebuilds
// it from pnpr's layout.
yaml += ' serverType: pnpr\n'
// pnpr's abbreviated metadata carries `time`, so the release-age
// check reads it rather than every package's full metadata.
yaml += ' supportsTimeField: true\n'
})
}
for (const [key, value] of Object.entries(opts.settings ?? {})) {
yaml += `${key}: ${value}\n`
}
return yaml
return yamlText({
packages: ['.'],
trustLockfile: true,
...(opts.pnprServer ? { pnprServer: opts.pnprServer } : {}),
...(opts.tarRegistries?.length
? {
registries: Object.fromEntries(opts.tarRegistries.map((url, i) => [url, {
// The first is the one the project installs from, so the default
// scope routes to it; any other is the same registry under another
// address, declared so the resolver knows what it serves too.
...(i === 0 ? { scopes: ['@'] } : {}),
// A tar registry's metadata names no tarball URL: the client rebuilds
// it from pnpr's layout.
serverType: 'pnpr',
// pnpr's abbreviated metadata carries `time`, so the release-age
// check reads it rather than every package's full metadata.
supportsTimeField: true,
}])),
}
: {}),
...opts.settings,
})
}

/**
* Serializes a config file that is read as YAML. JSON is valid YAML, and
* `JSON.stringify` quotes and escapes every value correctly, which
* hand-written YAML does only for values that happen not to need it.
*/
export function yamlText (value) {
return `${JSON.stringify(value, null, 2)}\n`
}

/**
Expand Down Expand Up @@ -298,26 +305,29 @@ export async function benchmark (pm, fixture, opts) {
// stopped being enough once Yarn started caching globally by default:
// without `enableGlobalCache`, packages land in `globalFolder` instead and
// survive every scenario, so no run ever measures a cold cache.
let yarnRc =
'enableImmutableInstalls: false\n'
+ 'enableGlobalCache: false\n'
+ 'enableMirror: false\n'
+ `cacheFolder: ${path.join(cwd, 'cache')}\n`
+ `globalFolder: ${path.join(cwd, 'cache', 'global')}\n`
+ 'enableScripts: false\n'
// Yarn reads none of `.npmrc`, so its registry has to be set here.
+ (opts.registry ? `npmRegistryServer: ${opts.registry}\nunsafeHttpWhitelist:\n - 127.0.0.1\n` : '')
const yarnRc = {
enableImmutableInstalls: false,
enableGlobalCache: false,
enableMirror: false,
cacheFolder: path.join(cwd, 'cache'),
globalFolder: path.join(cwd, 'cache', 'global'),
enableScripts: false,
// Yarn reads none of `.npmrc`, so its registry has to be set here.
...(opts.registry ? { npmRegistryServer: opts.registry, unsafeHttpWhitelist: ['127.0.0.1'] } : {}),
}
/**
* @see https://yarnpkg.com/configuration/yarnrc#nodeLinker
*/
switch (pm.scenario) {
case 'yarn':
yarnRc += 'nodeLinker: pnpm\n'
+ 'nmMode: hardlinks-local\n'
+ 'compressionLevel: 0\n'
Object.assign(yarnRc, {
nodeLinker: 'pnpm',
nmMode: 'hardlinks-local',
compressionLevel: 0,
})
break
}
await fs.writeFile(path.join(cwd, '.yarnrc.yml'), yarnRc)
await fs.writeFile(path.join(cwd, '.yarnrc.yml'), yamlText(yarnRc))
}

// Installs that nothing is measured from, to keep whatever only the very
Expand Down
11 changes: 7 additions & 4 deletions benchmarkLockfileConflicts.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'

import spawn from 'cross-spawn'

import { yamlText } from './benchmarkFixture.js'
import { lockfileConflictScenarios } from './lockfileConflictScenarios.js'

const DIRNAME = path.dirname(fileURLToPath(import.meta.url))
Expand Down Expand Up @@ -115,11 +116,13 @@ export function writeProjectState (dir, state, { clean = true } = {}) {
* Writes `pnpm-workspace.yaml` with workspace globs and optional `lockfile.format: v13`.
*/
function writePnpmWorkspaceYaml (dir, state, isV13) {
const lines = state.workspaces ? ['packages:', ' - packages/*'] : []
if (isV13) lines.push('lockfile:', ' format: v13')
const workspace = {
...(state.workspaces ? { packages: ['packages/*'] } : {}),
...(isV13 ? { lockfile: { format: 'v13' } } : {}),
}
const wsYamlPath = path.join(dir, 'pnpm-workspace.yaml')
if (lines.length > 0) {
fs.writeFileSync(wsYamlPath, `${lines.join('\n')}\n`)
if (Object.keys(workspace).length > 0) {
fs.writeFileSync(wsYamlPath, yamlText(workspace))
} else {
fs.rmSync(wsYamlPath, { force: true })
}
Expand Down
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url'
import rimraf from 'rimraf'
import tempy from 'tempy'

import { yamlText } from './benchmarkFixture.js'
import {
benchmarkNodeVersions,
cloneNvm,
Expand Down Expand Up @@ -209,7 +210,7 @@ async function measure () {
// The benchmark exists to measure the latest of everything, so the hold
// is turned off where the managers are installed. The yaml is what the
// Rust engine reads reliably — its `.npmrc`/`--config` parsing differs.
fs.writeFileSync(path.join(dir, 'pnpm-workspace.yaml'), "packages:\n - '.'\nminimumReleaseAge: 0\n", 'utf8')
fs.writeFileSync(path.join(dir, 'pnpm-workspace.yaml'), yamlText({ packages: ['.'], minimumReleaseAge: 0 }), 'utf8')
}
// pnpm 12 installs the other package managers natively, so one bootstrapped
// installer provisions everything the benchmark measures.
Expand Down
142 changes: 72 additions & 70 deletions pnprServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import path from 'node:path'
import spawn from 'cross-spawn'
import { loadYamlFileSync } from 'load-yaml-file'

import { applyPnprServerRegistry, createEnv, pnpmWorkspaceYaml, UPDATED_DEPENDENCIES } from './benchmarkFixture.js'
import { applyPnprServerRegistry, createEnv, pnpmWorkspaceYaml, UPDATED_DEPENDENCIES, yamlText } from './benchmarkFixture.js'

/**
* Takes a port the operating system says is free.
Expand Down Expand Up @@ -119,43 +119,42 @@ export function pnprVersion (managersDir) {
export async function startPnpr ({ managersDir, dir, port, publicUrl, logLevel = 'error' }) {
fs.mkdirSync(dir, { recursive: true })
const configPath = path.join(dir, 'pnpr.yaml')
fs.writeFileSync(configPath, [
`listen: ${PNPR_HOST}:${port}`,
fs.writeFileSync(configPath, yamlText({
listen: `${PNPR_HOST}:${port}`,
// pnpr rewrites the tarball URLs it serves to this address. It has to be
// the address clients actually use, or every tarball would be fetched on a
// link the benchmark isn't emulating — and npm refuses outright to fetch a
// tarball from a host other than the registry it was told about. Set here
// rather than on the command line because the registries import below
// checks its routes against it too.
...(publicUrl ? [`publicUrl: ${publicUrl}`] : []),
...(publicUrl ? { publicUrl } : {}),
// Accounts and the registries live in the database, which pnpr keeps
// inside the storage directory and binds the storage to. A storage
// `populatePnprCache.js` filled therefore arrives with the populate run's
// account and registries: the account is signed in to again (see
// `mintToken`) and the registries are replaced below.
`storage: ${path.join(dir, 'storage')}`,
storage: path.join(dir, 'storage'),
// Upstream content is cached under keys derived from this, and pnpr
// refuses to reuse a cache directory without one. The benchmark hands
// every run the storage `populatePnprCache.js` filled, so it has to be
// the same in all of them. Nothing here is secret: the server only ever
// listens on loopback.
'secret: pnpm-benchmarks-pnpr-cache-secret',
'auth:',
' users:',
// The benchmark registers its own resolver user on startup.
' maxUsers: 1',
'resolver:',
' enabled: true',
'routes:',
// The tar registry the accelerated scenario resolves against is this
// server's own, at its un-proxied address (see `applyPnprServerRegistry`),
// and the resolver refuses loopback addresses that are not listed here.
' allowedPrivateNetworks: [127.0.0.0/8]',
'log:',
' format: pretty',
` level: ${logLevel}`,
'',
].join('\n'))
secret: 'pnpm-benchmarks-pnpr-cache-secret',
auth: {
users: {
// The benchmark registers its own resolver user on startup.
maxUsers: 1,
},
},
resolver: { enabled: true },
routes: {
// The tar registry the accelerated scenario resolves against is this
// server's own, at its un-proxied address (see `applyPnprServerRegistry`),
// and the resolver refuses loopback addresses that are not listed here.
allowedPrivateNetworks: ['127.0.0.0/8'],
},
log: { format: 'pretty', level: logLevel },
}))
const url = `http://${PNPR_HOST}:${port}`
importRegistries({ managersDir, dir, configPath, url })

Expand Down Expand Up @@ -225,54 +224,57 @@ export async function startPnpr ({ managersDir, dir, port, publicUrl, logLevel =
*/
function importRegistries ({ managersDir, dir, configPath, url }) {
const registriesPath = path.join(dir, 'registries.yaml')
fs.writeFileSync(registriesPath, [
'registries:',
// The public npm registry. No `packages:` map, so it claims every
// name — the catch-all, and therefore last in the router's sources.
' npmjs:',
' type: upstream',
' url: https://registry.npmjs.org/',
' public: true',
// Bit publishes to a registry of its own, and `alotta-packages`
// depends on it. Declaring the scopes it serves is all the routing
// that is needed: every manager keeps installing from the one
// registry URL the benchmark hands it, and the names below are the
// only ones fetched from bit. Tarball URLs come back rewritten to
// pnpr's own address, so bit's packages cross the emulated link
// like every other package.
' bit:',
' type: upstream',
` url: ${BIT_REGISTRY}`,
' public: true',
' packages:',
...BIT_SCOPES.map((scope) => ` '${scope}/*':`),
// One URL in front of both: a router resolves a name to the first
// source whose declared packages claim it. `defaultRegistry` has to
// name *this* rather than a concrete registry — a concrete one
// resolves only its own namespace and answers `Unclaimed` for
// everything else, with no fall-through to a sibling.
' main:',
' type: router',
' sources: [bit, npmjs]',
// The same sources again, served as tars rather than `.tgz` files (see
// `tarRegistryUrl`). One storage behind both: a package pulled through
// either is cached once.
` ${TAR_REGISTRY}:`,
' type: router',
' sources: [bit, npmjs]',
' tarballs: tar',
'defaultRegistry: main',
// The accelerated scenario asks the server to resolve against this
// address — its own un-proxied tar registry — so the resolver's metadata fetches
// stay on loopback instead of crossing the emulated link and coming back
// in through the front door (see `applyPnprServerRegistry`). The server
// resolves against whatever registry the client sends but only if that
// registry is declared, so the address has to be listed as a route here.
'routes:',
' public:',
` - registry: ${tarRegistryUrl(url)}`,
'',
].join('\n'))
fs.writeFileSync(registriesPath, yamlText({
registries: {
// The public npm registry. No `packages:` map, so it claims every
// name — the catch-all, and therefore last in the router's sources.
npmjs: {
type: 'upstream',
url: 'https://registry.npmjs.org/',
public: true,
},
// Bit publishes to a registry of its own, and `alotta-packages`
// depends on it. Declaring the scopes it serves is all the routing
// that is needed: every manager keeps installing from the one
// registry URL the benchmark hands it, and the names below are the
// only ones fetched from bit. Tarball URLs come back rewritten to
// pnpr's own address, so bit's packages cross the emulated link
// like every other package.
bit: {
type: 'upstream',
url: BIT_REGISTRY,
public: true,
packages: Object.fromEntries(BIT_SCOPES.map((scope) => [`${scope}/*`, null])),
},
// One URL in front of both: a router resolves a name to the first
// source whose declared packages claim it. `defaultRegistry` has to
// name *this* rather than a concrete registry — a concrete one
// resolves only its own namespace and answers `Unclaimed` for
// everything else, with no fall-through to a sibling.
main: {
type: 'router',
sources: ['bit', 'npmjs'],
},
// The same sources again, served as tars rather than `.tgz` files (see
// `tarRegistryUrl`). One storage behind both: a package pulled through
// either is cached once.
[TAR_REGISTRY]: {
type: 'router',
sources: ['bit', 'npmjs'],
tarballs: 'tar',
},
},
defaultRegistry: 'main',
routes: {
// The accelerated scenario asks the server to resolve against this
// address — its own un-proxied tar registry — so the resolver's metadata fetches
// stay on loopback instead of crossing the emulated link and coming back
// in through the front door (see `applyPnprServerRegistry`). The server
// resolves against whatever registry the client sends but only if that
// registry is declared, so the address has to be listed as a route here.
public: [{ registry: tarRegistryUrl(url) }],
},
}))
const result = spawn.sync('pnpr', ['-c', configPath, 'registries', 'import', registriesPath], {
cwd: managersDir,
env: createEnv(managersDir),
Expand Down
3 changes: 2 additions & 1 deletion populatePnprCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'

import tempy from 'tempy'

import { yamlText } from './benchmarkFixture.js'
import { cmdsMap } from './commandsMap.js'
import { fixtureNames } from './fixtures.js'
import { installPnpr, npmRegistryUrl, PNPR_HOST, populateCache, reservePort, startPnpr, tarRegistryUrl } from './pnprServer.js'
Expand Down Expand Up @@ -42,7 +43,7 @@ async function run () {
for (const dir of Object.values(managersDirs)) {
fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(path.join(dir, 'package.json'), '{}', 'utf8')
fs.writeFileSync(path.join(dir, 'pnpm-workspace.yaml'), "packages:\n - '.'\nminimumReleaseAge: 0\n", 'utf8')
fs.writeFileSync(path.join(dir, 'pnpm-workspace.yaml'), yamlText({ packages: ['.'], minimumReleaseAge: 0 }), 'utf8')
}
// Only the client the populate pass installs with, not the whole set the
// benchmark measures: nothing here is timed, so the other managers would
Expand Down
4 changes: 3 additions & 1 deletion setupPackageManagers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import path from 'node:path'
import spawn from 'cross-spawn'
import pathKey from 'path-key'

import { yamlText } from './benchmarkFixture.js'

/**
* pnpm 12 installs the other package managers natively: `pnpm add --global`
* resolves npm, Yarn, and Bun through their trusted release channels and
Expand Down Expand Up @@ -37,7 +39,7 @@ export function bootstrapInstaller (setupDir) {
// benchmark exists to measure the newest release, not the newest mature one.
fs.writeFileSync(
path.join(setupDir, 'pnpm-workspace.yaml'),
'packages:\n - "."\nminimumReleaseAge: 0\n',
yamlText({ packages: ['.'], minimumReleaseAge: 0 }),
'utf8'
)
// pnpm 12 ships its Rust binary via an install script, so the build must be
Expand Down
Loading