mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-09-10 19:01:58 +00:00
* chore: format the 35 files CI had been silently rewriting
`scripts/lint.js --prettier` and the release workflow's `Format Project`
step both run `prettier --write`, which reformats in place and exits 0.
Neither is followed by a dirty-tree check, so the rewrites are discarded
when the job ends and these files have stayed unformatted on `main`.
This is the output of `prettier --write` over exactly those files, so the
next commit can turn the check into a real gate without failing on a
backlog. No behaviour change: every hunk is whitespace, quoting or
wrapping.
Verified with prettier 3.6.1, the version pinned in package-lock.json.
`prettier --check .` over the whole repo reports these 35 and nothing
else.
Refs #11109
* fix(ci): make the Prettier lane a real gate
`runPrettier()` ran `prettier --write .`. Prettier exits 0 after
reformatting, and no caller checks the tree afterwards, so the lane
passed on unformatted code and CI threw the rewrites away — the 35 files
in the previous commit are what it had been silently fixing and
discarding.
ESLint does not cover the gap: eslint.config.js loads
eslint-config-prettier, which disables formatting rules rather than
enforcing them. With this change the only enforcement point outside the
husky pre-commit hook actually enforces something.
`npm run lint:all` now reports unformatted files instead of fixing them;
`npm run format` is the fix. Prettier's own failure message says so.
Refs #11109
* fix(release): drop Format Project, it writes and discards
The step ran `npm run format` (`prettier --write .`) and nothing in
release.yml inspected the tree afterwards — no `git diff --exit-code`, no
`git status --porcelain` anywhere in the file. The rewrites died with the
job, so the only failure it could produce was Prettier crashing on an
unparsable file, which the Run Lint step reports anyway.
It cost 13m58s on a quiet host and 20m45s on a contended one, against a
30-minute cap that `quality_static` hit twice today (runs 33957952281 and
33963757913, both cancelled mid-lint with nothing failing). The remaining
steps are `npm ci` (1m44s) and `lint:ci` (~8m20s), so the job now has real
headroom instead of finishing 24 seconds over the wall.
Formatting is now gated where it belongs: the previous commit makes the
PR-CI Prettier lane `--check`.
Refs #11109
* fix(ci): keep prettier --check out of .qwen/ and the pre-paint theme script
Two things the `--write` → `--check` flip broke that `--write` had hidden.
`.qwen/` is gitignored as `.qwen/*` and holds machine-written state —
pending-skills/, auto-skill-*/, learned-skill-*/,
computer-use/installed.json. Prettier does not read .gitignore, so it was
walking in and silently reformatting whatever a contributor had there;
under `--check` that turns into a failed `npm run lint:all` on files git
does not even track. CI never saw it (fresh checkout, tracked files only),
which is exactly why it went unnoticed. Adding `.qwen/` to
.prettierignore also drops the one tracked file under it —
skills/find-simplifications/SKILL.md, force-added past .gitignore — from
the previous formatting commit, since a directory the gate should not
police should not be reformatted for it either.
The inline theme script in web-shell's index.html is restored to its
original single line behind `<!-- prettier-ignore -->`. It is a
render-blocking script in <head> that applies the stored theme before
first paint; the expanded form ships ~600 more bytes on the critical path
and buys no readability the comment above it does not already give. A
change about CI hygiene should not be reshaping that.
Refs #11109
* fix(ci): keep prettier --check out of vendored packages/mobile-mcp
eslint.config.js has ignored `packages/mobile-mcp/**` since it landed —
"vendored mobile-next/mobile-mcp; has own eslint config" — and
.prettierignore already carries its sibling `packages/cua-driver/`, listed
one line above with the same rationale. mobile-mcp was simply missed.
`--write` hid that: it reformatted the vendored package's own eslint
config to our house style every run and threw the result away. The
formatting commit in this PR would have made that permanent, widening the
diff against upstream on the next sync for a package this repo has already
decided not to lint. That file is reverted here and the directory is
ignored.
Same class as the .qwen/ fix in the previous commit: the gate reaching
somewhere the repo had already decided it should not.
Refs #11109
* fix(ci): mirror eslint's remaining global ignores in .prettierignore
`--check` makes .prettierignore load-bearing in a way `--write` never was:
under `--write` a file prettier should not touch was reformatted and the
result discarded, so nobody noticed; under `--check` it fails the lane.
These three are the rest of eslint.config.js's global ignores, added for
the reasons it already states. `scripts/codemod/fixtures/` is the one that
matters: those files are "intentionally non-idiomatic ink input/output",
so the obvious response to a `--check` failure there — reformat to house
style — would corrupt the fixtures the codemod tests assert against. The
trap is not live today (all three currently satisfy prettier), which is
precisely why it is worth closing before someone meets it.
Refs #11109
* chore: format converter.test.ts, which drifted onto main since this branch
This branch merged main 32 commits after its base and `prettier --check .`
found one new offender. It is the same gate failing the same way: nothing
enforces formatting, so `--write` reformatted this file on every CI run
since it landed and threw the result away.
Worth noting as evidence rather than noise — the backlog this PR fixes was
not a one-off from before someone got careless; it accrues continuously,
and it accrued again while the PR was open.
Refs #11109
* Revert "fix(release): drop Format Project, it writes and discards"
This reverts the release.yml half of this PR. #11195 removes that step on
its own, in one file and four lines, and two PRs racing on the same
deletion helps nobody.
The reasoning and the evidence are unchanged and now live on #11195: the
dry-run release dispatched from this branch with the step removed
(34022919438) went 18/18 green in 65m43s, quality_static finished in
7m00s against main's 8m57s, and Run Lint was 4m52s vs 4m43s — nothing
downstream ever read what the step produced.
Dropping release.yml from this PR also widens who can review it. It was
the only file here owned by @pomelo-nwu and @wenshao alone; what remains
under CODEOWNERS is /packages/core/, whose owner list is six people. The
Prettier gate no longer waits on the two maintainers who must sign off on
release workflow changes.
Refs #11109, #11195
* fix(ci): make the gate resolve the file set npm run format resolves
Two review findings, one root cause: the gate was checking a different set
of files than the tree it is supposed to police.
**The `.qwen/` ignore rested on a false premise (R1-1, @chiga0).** Prettier
3.6's `--ignore-path` already defaults to `[.gitignore, .prettierignore]`,
so every machine-written path that entry's comment named —
`pending-skills/`, `auto-skill-*/`, `learned-skill-*/` — was outside the
gate before it via `.gitignore:32`. What the entry actually excluded is the
corpus `.gitignore` deliberately re-includes: `.qwen/commands/**`,
`skills/**`, `agents/**`, `team-memory/**`, `review-context.json` — 57
tracked, hand-authored files. The gate then passed because the surface had
been narrowed, not because the tree was formatted, and contributors editing
those docs could ship them unformatted with both enforcement points silent.
The entry is dropped and the first commit's formatting of
`skills/find-simplifications/SKILL.md`, reverted alongside it, is re-landed.
**The gate and the remedy used different CLIs (R1-2).** `runPrettier()` ran
the classic `prettier --check .` while `npm run format` runs
`prettier --experimental-cli --write .`. The classic CLI reads only the
repo-root `.gitignore`; the experimental one also honours nested ones, and
this repo has 23 of those. Verified on a scratch tree: given
`pkg/.gitignore` containing `lib/`, classic reports `pkg/lib/nested.js` and
experimental skips it. So the lane could go red on a locally built package's
output that `npm run format` provably cannot clear. The gate now uses the
same CLI as the remedy it names.
Refs #11109
* fix(ci): let the pre-commit hook cover what the gate now checks, and pin it
**The hook was narrower than the gate (R1-5).** `lint-staged` formatted only
`*.{js,jsx,ts,tsx}` and `*.{json,md}`, but the gate checks every extension
Prettier has a parser for. Of the 35 files the first commit formatted, 29
(83%) are in classes the hook never touched — 13 `.yml`, 12 `.mjs`, 3
`.css`, 1 `.html`. Left alone, the backlog this PR just cleared would
re-accumulate, and now as a red CI step on someone else's PR rather than as
silent drift. `.mjs`, `.cjs`, `.yml`, `.yaml`, `.css` and `.html` join the
Prettier-only group; ESLint's globs are unchanged, since the gap is about
formatting coverage, not lint coverage.
**Nothing pinned the flag (R1-4).** `scripts/tests/lint.test.js` exercised
only the linter-directory helpers, and `ci-platform-lanes.test.js` asserts
that the `Run Prettier` step exists and is profile-gated, not what it runs.
A refactor, or a well-meant "make lint auto-fix again", could restore the
silent pass with every suite green. Two assertions now pin it: that
`runPrettier()` checks rather than writes, and that it uses the same
Prettier CLI as `npm run format`, so the two cannot drift apart again the
way R1-2 found them.
Refs #11109
* docs: correct the instructions and comments the --check flip falsified
**Three tracked instructions to automation agents now say the opposite of
what the code does (R1-9), and one is fed to a CI lane today** —
`.github/workflows/qwen-triage.yml` re-pins `verify-pr` from the base commit
and hands it to the verifying agent.
- `verify-pr/SKILL.md` told agents never to run `scripts/lint.js` with no
arguments because it would rewrite the working tree under their A/B
harness. That is no longer why. The prohibition is kept — it is still
right — but re-grounded in what the no-arg form actually does now
(`setupLinters()` wipes the linter temp dir and re-downloads three pinned
binaries), and `--prettier` joins the enumerated non-mutating checks it
was the only flag missing from.
- `find-simplifications/SKILL.md` and its `references/land.md` told the
simplification agent that a formatting diff "carries zero signal" because
CI runs `--write`. An agent believing that would skip `npm run format`
after a deletion changed line shape — on the very extensions the hook does
not cover — and go red on formatting it was told could not matter.
- `update-ecs-runner-qwen-workflow.test.js`'s comment justified parsing two
matrix layouts by "CI runs `prettier --write .` before this suite". CI no
longer rewrites anything; the two layouts are still worth parsing, so the
comment is re-grounded in the checked-in form versus older branches.
**The index.html rationale overstated its own number (R1-8, @chiga0).** It
claimed the expanded script ships "~600 more bytes"; measured against this
repo's Prettier 3.6.1 and its `tabWidth: 2`, it is +258 source and +8
gzipped here, +287/+20 by the reviewer's method — either way roughly half
what was claimed, and "~600" is what the same expansion costs at
`--tab-width 4`. Since the prose ships verbatim inside the built
`index.html`, the fix is to drop the contested figure and keep the comment
short. The `<!-- prettier-ignore -->` guard itself stays: it is load-bearing.
Refs #11109
* chore: format classify-profile.test.mjs, drifted again after commit 1
Third instance in this PR's lifetime, and the sharpest: commit `b1b94a8762`
formatted this exact file as one of the 35, and `main` reintroduced
unformatted content in it within a day. The other two were
`converter.test.ts` at the first merge and this file at the second.
The gate is the fix; until it lands, every merge from `main` re-opens the
same hole.
Refs #11109
176 lines
5.1 KiB
JavaScript
176 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* API Preconnect Latency Benchmark
|
||
*
|
||
* Measures the real TCP+TLS connection reuse benefit of preconnect by using
|
||
* undici (the same library as apiPreconnect.ts) within a single process.
|
||
*
|
||
* Unlike the previous curl-based approach, this correctly measures connection
|
||
* pool reuse: the same dispatcher instance is shared between the preconnect
|
||
* HEAD request and the subsequent measured request, just like in production.
|
||
*
|
||
* Usage:
|
||
* node scripts/benchmark-api-latency.mjs
|
||
*
|
||
* Environment variables:
|
||
* ITERATIONS=3 Number of cold/warm pairs per endpoint (default: 3)
|
||
* REQUEST_TIMEOUT_MS=5000 Per-request timeout in ms (default: 5000)
|
||
* BENCHMARK_URLS Space-separated extra URLs to benchmark
|
||
*/
|
||
|
||
import { createRequire } from 'module';
|
||
import { performance } from 'perf_hooks';
|
||
|
||
// Resolve undici from the core package (same version used by preconnect)
|
||
const require = createRequire(import.meta.url);
|
||
const { Agent } = require('../packages/core/node_modules/undici/index.js');
|
||
|
||
const ITERATIONS = parseInt(process.env['ITERATIONS'] ?? '3', 10);
|
||
const REQUEST_TIMEOUT_MS = parseInt(
|
||
process.env['REQUEST_TIMEOUT_MS'] ?? '5000',
|
||
10,
|
||
);
|
||
|
||
const DEFAULT_ENDPOINTS = [
|
||
{ url: 'https://api.openai.com', label: 'OpenAI' },
|
||
{ url: 'https://api.anthropic.com', label: 'Anthropic' },
|
||
{
|
||
url: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||
label: 'DashScope (openai-compatible)',
|
||
},
|
||
];
|
||
|
||
const extraUrls = process.env['BENCHMARK_URLS']
|
||
? process.env['BENCHMARK_URLS']
|
||
.split(' ')
|
||
.filter(Boolean)
|
||
.map((url) => ({ url, label: url }))
|
||
: [];
|
||
|
||
const ENDPOINTS = [...DEFAULT_ENDPOINTS, ...extraUrls];
|
||
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function newDispatcher() {
|
||
return new Agent({
|
||
headersTimeout: 0,
|
||
bodyTimeout: 0,
|
||
keepAliveTimeout: 60_000,
|
||
});
|
||
}
|
||
|
||
async function fetchOnce(url, dispatcher, method = 'HEAD') {
|
||
const start = performance.now();
|
||
try {
|
||
await fetch(url, {
|
||
method,
|
||
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||
headers: { 'User-Agent': 'QwenCode-Benchmark/1.0' },
|
||
dispatcher,
|
||
});
|
||
} catch (err) {
|
||
// Timeouts and non-2xx are fine — we only care about connection timing
|
||
if (err?.name === 'TimeoutError') {
|
||
return performance.now() - start; // still records the time spent
|
||
}
|
||
}
|
||
return performance.now() - start;
|
||
}
|
||
|
||
/**
|
||
* Cold measurement: brand-new dispatcher, no preconnect.
|
||
* Returns elapsed ms of the measured request.
|
||
*/
|
||
async function measureCold(url) {
|
||
const dispatcher = newDispatcher();
|
||
return fetchOnce(url, dispatcher, 'HEAD');
|
||
}
|
||
|
||
/**
|
||
* Warm measurement: same dispatcher for preconnect HEAD + measured request.
|
||
* Returns elapsed ms of the measured request only (not the preconnect time).
|
||
*/
|
||
async function measureWarm(url) {
|
||
const dispatcher = newDispatcher();
|
||
// Preconnect — mirrors apiPreconnect.ts behaviour
|
||
await fetchOnce(url, dispatcher, 'HEAD').catch(() => {});
|
||
// Measured request reuses the warmed connection from the same pool
|
||
return fetchOnce(url, dispatcher, 'HEAD');
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function fmt(ms) {
|
||
return `${ms.toFixed(1)}ms`;
|
||
}
|
||
|
||
function avg(arr) {
|
||
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
||
}
|
||
|
||
async function benchmarkEndpoint({ url, label }) {
|
||
console.log(`\n ${label}`);
|
||
console.log(` ${url}`);
|
||
|
||
const coldTimes = [];
|
||
const warmTimes = [];
|
||
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
const cold = await measureCold(url);
|
||
coldTimes.push(cold);
|
||
|
||
// Brief pause so the OS can release the cold connection
|
||
await new Promise((r) => setTimeout(r, 500));
|
||
|
||
const warm = await measureWarm(url);
|
||
warmTimes.push(warm);
|
||
|
||
console.log(` run ${i + 1}: cold=${fmt(cold)} warm=${fmt(warm)}`);
|
||
|
||
await new Promise((r) => setTimeout(r, 500));
|
||
}
|
||
|
||
const avgCold = avg(coldTimes);
|
||
const avgWarm = avg(warmTimes);
|
||
const saved = avgCold - avgWarm;
|
||
const pct = avgCold > 0 ? (saved / avgCold) * 100 : 0;
|
||
|
||
return { label, url, avgCold, avgWarm, saved, pct };
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
|
||
console.log('=== Qwen Code API Preconnect Latency Benchmark ===');
|
||
console.log(`Iterations per endpoint : ${ITERATIONS}`);
|
||
console.log(`Request timeout : ${REQUEST_TIMEOUT_MS}ms`);
|
||
console.log('\nRunning...');
|
||
|
||
const results = [];
|
||
for (const endpoint of ENDPOINTS) {
|
||
const result = await benchmarkEndpoint(endpoint);
|
||
results.push(result);
|
||
}
|
||
|
||
// Summary table
|
||
console.log('\n\n=== Results ===\n');
|
||
console.log(
|
||
'Endpoint'.padEnd(36) +
|
||
'Cold (avg)'.padStart(12) +
|
||
'Warm (avg)'.padStart(12) +
|
||
'Saved'.padStart(10) +
|
||
'Improvement'.padStart(13),
|
||
);
|
||
console.log('─'.repeat(83));
|
||
|
||
for (const r of results) {
|
||
const status = r.pct >= 30 ? '✓' : r.pct >= 10 ? '~' : '✗';
|
||
console.log(
|
||
r.label.slice(0, 35).padEnd(36) +
|
||
fmt(r.avgCold).padStart(12) +
|
||
fmt(r.avgWarm).padStart(12) +
|
||
fmt(r.saved).padStart(10) +
|
||
`${r.pct.toFixed(1)}% ${status}`.padStart(13),
|
||
);
|
||
}
|
||
|
||
console.log('\nLegend: ✓ ≥30% improvement ~ 10–30% ✗ <10%');
|