Files
tanzhenxin 9534a528a6 perf(cli): keep running in place when env files only hold model credentials (#12680)
* perf(cli): keep running in place when env files only hold model credentials

The env-file relaunch rule from #12602 fires for any value that `.env` files
or `settings.env` inject, including the API key that `/auth` stores and the
keys the docs recommend for `~/.qwen/.env`. Those users always paid the
second boot. Providers read credentials and endpoints per request, never
while a module loads, so `hasLoadedEnvironmentValues()` now ignores the
documented per-auth-type variables and the `envKey` of every configured
model provider. Any other injected value still forces a fresh image, and a
provider entry can never exempt a loader or boot-time key.

The DashScope proxy base URL was the one such variable read at module load;
it is now read at call time.

Reported by @wenshao in the #12622 review (F2).

Claude-Session: https://claude.ai/code/session_01163b7X4Ntw1S2pr9VrtRrs

* feat(scripts): let the startup benchmark load credentials from a file

`--credentials settings|dotenv` moves the stub's key and URL from the shell
into the settings file's `env` block or `~/.qwen/.env`, so the env-file
relaunch rule can be measured. The default stays `shell`.

Claude-Session: https://claude.ai/code/session_01163b7X4Ntw1S2pr9VrtRrs
2026-09-25 10:13:38 +00:00

130 lines
3.7 KiB
JavaScript

#!/usr/bin/env node
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
// Startup proxies for one build, which move only when the process chain or
// the module graph loaded at startup changes:
// D1 Node processes started before the prompt is typeable (and for `-p`,
// before it exits)
// D2 bytes of JavaScript those processes opened
//
// node scripts/startup-benchmark/proxies.mjs [--entry <cli-entry.js>] [--runs N] \
// [--credentials shell|settings|dotenv]
//
// `--credentials settings|dotenv` moves the model key and URL from the shell
// into the settings file or `~/.qwen/.env`. Linux only: it needs strace. See
// docs/design/2026-09-25-startup-benchmark-harness.md.
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';
import {
CREDENTIAL_SOURCES,
makeRunEnvironment,
parseStrace,
quantiles,
runHeadless,
runInteractive,
startModelServer,
} from './lib.mjs';
const here = path.dirname(fileURLToPath(import.meta.url));
const { values: options } = parseArgs({
options: {
entry: { type: 'string', default: path.join(here, '..', 'cli-entry.js') },
runs: { type: 'string', default: '3' },
credentials: { type: 'string', default: 'shell' },
},
});
if (!CREDENTIAL_SOURCES.includes(options.credentials)) {
console.error('--credentials must be shell, settings or dotenv');
process.exit(2);
}
// Containers can ship strace without the right to trace, so try it once.
try {
execFileSync(
'strace',
['-qq', '-o', os.devNull, process.execPath, '-e', '0'],
{ stdio: 'ignore', timeout: 30_000 },
);
} catch {
console.error('startup proxies need Linux with a working strace');
process.exit(1);
}
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'qwen-startup-proxies-'));
const modelServer = await startModelServer();
const entry = path.resolve(options.entry);
const results = { interactive: [], headless: [] };
try {
for (let i = 0; i < Number(options.runs); i++) {
for (const mode of ['interactive', 'headless']) {
const run = makeRunEnvironment({
root,
modelServer,
credentials: options.credentials,
});
const trace = path.join(run.dir, 'strace.txt');
const args = [
'-f',
'-qq',
'-ttt',
'-e',
'trace=execve,openat',
'-e',
'signal=none',
'-o',
trace,
process.execPath,
entry,
...(mode === 'headless' ? ['-p', 'say hi'] : []),
];
let cutoff = Infinity;
if (mode === 'interactive') {
const r = await runInteractive({
command: 'strace',
args,
cwd: run.cwd,
env: run.env,
observeMs: 0,
});
cutoff = r.ttiEpochMs;
} else {
const r = await runHeadless({
command: 'strace',
args,
cwd: run.cwd,
env: run.env,
modelServer,
runId: run.runId,
});
if (r.exitCode !== 0) {
throw new Error(`qwen -p exited with ${r.exitCode}`);
}
}
results[mode].push(parseStrace(fs.readFileSync(trace, 'utf8'), cutoff));
}
}
} finally {
await modelServer.close();
fs.rmSync(root, { recursive: true, force: true });
}
console.log(`credentials: ${options.credentials}`);
for (const mode of ['interactive', 'headless']) {
const counts = results[mode].map((r) => r.nodeProcesses);
const megabytes = (
quantiles(results[mode].map((r) => r.jsBytes)).p50 / 1e6
).toFixed(1);
console.log(
`${mode.padEnd(11)} Node processes: ${counts.join(', ')} JS loaded (median): ${megabytes} MB`,
);
}