mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-04 07:27:20 +00:00
fix(ci): rebalance native tests and stabilize flaky gates (#3719)
* fix(ci): stabilize timing gates and artifact uploads * fix(ci): rebalance undercharged native test shards --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
This commit is contained in:
parent
be32540b6b
commit
ee7eff97f7
7 changed files with 252 additions and 95 deletions
66
.github/actions/upload-test-artifact/action.yml
vendored
Normal file
66
.github/actions/upload-test-artifact/action.yml
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
name: Upload test artifact
|
||||
description: Upload required CI evidence with bounded transport retries
|
||||
|
||||
inputs:
|
||||
name:
|
||||
description: Unique artifact name for this job
|
||||
required: true
|
||||
path:
|
||||
description: Files to upload
|
||||
required: true
|
||||
retention-days:
|
||||
description: Artifact retention in days
|
||||
default: '5'
|
||||
include-hidden-files:
|
||||
description: Include explicitly selected hidden report directories
|
||||
default: 'false'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Upload
|
||||
id: first
|
||||
if: ${{ always() }}
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: ${{ inputs.name }}
|
||||
path: ${{ inputs.path }}
|
||||
retention-days: ${{ inputs.retention-days }}
|
||||
include-hidden-files: ${{ inputs.include-hidden-files }}
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Wait before retry
|
||||
if: ${{ !cancelled() && steps.first.outcome == 'failure' }}
|
||||
shell: bash
|
||||
run: sleep 10
|
||||
|
||||
- name: Retry upload
|
||||
id: second
|
||||
if: ${{ !cancelled() && steps.first.outcome == 'failure' }}
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: ${{ inputs.name }}
|
||||
path: ${{ inputs.path }}
|
||||
retention-days: ${{ inputs.retention-days }}
|
||||
include-hidden-files: ${{ inputs.include-hidden-files }}
|
||||
if-no-files-found: error
|
||||
# A failed request may already have created an incomplete artifact.
|
||||
overwrite: true
|
||||
|
||||
- name: Wait before final attempt
|
||||
if: ${{ !cancelled() && steps.second.outcome == 'failure' }}
|
||||
shell: bash
|
||||
run: sleep 30
|
||||
|
||||
- name: Final upload attempt
|
||||
if: ${{ !cancelled() && steps.second.outcome == 'failure' }}
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: ${{ inputs.name }}
|
||||
path: ${{ inputs.path }}
|
||||
retention-days: ${{ inputs.retention-days }}
|
||||
include-hidden-files: ${{ inputs.include-hidden-files }}
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
37
.github/workflows/ci-tests.yml
vendored
37
.github/workflows/ci-tests.yml
vendored
|
|
@ -29,13 +29,12 @@ jobs:
|
|||
fi
|
||||
- name: Upload release accuracy evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: ./.github/actions/upload-test-artifact
|
||||
with:
|
||||
name: release-tool-accuracy
|
||||
path: |
|
||||
${{ runner.temp }}/tool-accuracy/accuracy.json
|
||||
${{ runner.temp }}/tool-accuracy/accuracy.md
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
# Ubuntu full-suite coverage, sharded. Each shard writes a vitest blob report
|
||||
|
|
@ -108,7 +107,7 @@ jobs:
|
|||
working-directory: gitnexus
|
||||
- name: Upload coverage blob
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: ./.github/actions/upload-test-artifact
|
||||
with:
|
||||
name: coverage-blob-${{ matrix.shard }}
|
||||
path: gitnexus/.vitest/blob/
|
||||
|
|
@ -171,7 +170,7 @@ jobs:
|
|||
run: node --test docker-server.test.mjs
|
||||
- name: Upload test reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: ./.github/actions/upload-test-artifact
|
||||
with:
|
||||
name: coverage-reports
|
||||
path: |
|
||||
|
|
@ -195,9 +194,9 @@ jobs:
|
|||
steps:
|
||||
- id: gen
|
||||
run: |
|
||||
# Refreshed Windows timings leave eight shards too close to the
|
||||
# 20-minute watchdog. Ten preserve headroom for runner variation.
|
||||
TOTAL=10 # cross-platform (windows/macOS) shards per OS
|
||||
# Keep the recorded native workload below 17 minutes, leaving
|
||||
# headroom for runner variation under the 20-minute watchdog.
|
||||
TOTAL=11 # cross-platform (windows/macOS) shards per OS
|
||||
# Four coverage shards exceeded the 25-minute job limit while tests
|
||||
# were still passing. Split the full suite further; keep the timeout
|
||||
# and merged coverage thresholds unchanged.
|
||||
|
|
@ -297,11 +296,10 @@ jobs:
|
|||
working-directory: gitnexus
|
||||
- name: Upload platform execution report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: ./.github/actions/upload-test-artifact
|
||||
with:
|
||||
name: execution-${{ matrix.os }}-${{ matrix.shard }}
|
||||
path: gitnexus/${{ matrix.os }}-${{ matrix.shard }}.json
|
||||
if-no-files-found: error
|
||||
retention-days: 5
|
||||
|
||||
# Branch protection still requires these six names from the three-shard
|
||||
|
|
@ -1103,11 +1101,10 @@ jobs:
|
|||
working-directory: gitnexus
|
||||
- name: Upload benchmark execution report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: ./.github/actions/upload-test-artifact
|
||||
with:
|
||||
name: execution-benchmarks
|
||||
path: gitnexus/benchmarks.json
|
||||
if-no-files-found: error
|
||||
retention-days: 5
|
||||
|
||||
# Fresh-process measurements have their own runner and time budget so they
|
||||
|
|
@ -1130,11 +1127,10 @@ jobs:
|
|||
working-directory: gitnexus
|
||||
- name: Upload scope-resolution measurements
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: ./.github/actions/upload-test-artifact
|
||||
with:
|
||||
name: scope-resolution-benchmark
|
||||
path: gitnexus/scope-resolution-benchmark.json
|
||||
if-no-files-found: error
|
||||
retention-days: 5
|
||||
|
||||
# Locked eval suite. setup-uv and uv itself are immutable so CI exercises
|
||||
|
|
@ -1158,11 +1154,10 @@ jobs:
|
|||
working-directory: eval
|
||||
- name: Upload pytest execution report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: ./.github/actions/upload-test-artifact
|
||||
with:
|
||||
name: execution-pytest-locked
|
||||
path: eval/pytest-locked.xml
|
||||
if-no-files-found: error
|
||||
retention-days: 5
|
||||
- uses: ./.github/actions/setup-gitnexus
|
||||
with:
|
||||
|
|
@ -1175,11 +1170,10 @@ jobs:
|
|||
working-directory: gitnexus
|
||||
- name: Upload preflight execution report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: ./.github/actions/upload-test-artifact
|
||||
with:
|
||||
name: execution-preflight
|
||||
path: gitnexus/preflight.json
|
||||
if-no-files-found: error
|
||||
retention-days: 5
|
||||
|
||||
test-completeness:
|
||||
|
|
@ -1225,7 +1219,7 @@ jobs:
|
|||
run: python3 eval/check_test_execution.py execution-reports eval/pytest-results.json
|
||||
- name: Upload complete test reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: ./.github/actions/upload-test-artifact
|
||||
with:
|
||||
name: test-reports
|
||||
path: |
|
||||
|
|
@ -1234,7 +1228,6 @@ jobs:
|
|||
gitnexus/test-results.json
|
||||
gitnexus-web/web-test-results.json
|
||||
eval/pytest-results.json
|
||||
if-no-files-found: error
|
||||
retention-days: 5
|
||||
|
||||
# Native Linux ownership and Bubblewrap boundary. The environment flag makes
|
||||
|
|
@ -1336,11 +1329,10 @@ jobs:
|
|||
working-directory: eval
|
||||
- name: Upload pytest execution report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: ./.github/actions/upload-test-artifact
|
||||
with:
|
||||
name: execution-pytest-ubuntu
|
||||
path: eval/pytest-ubuntu.xml
|
||||
if-no-files-found: error
|
||||
retention-days: 5
|
||||
|
||||
# Native Windows Job Object canary. POSIX-only tests skip by platform, while
|
||||
|
|
@ -1367,9 +1359,8 @@ jobs:
|
|||
working-directory: eval
|
||||
- name: Upload pytest execution report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
uses: ./.github/actions/upload-test-artifact
|
||||
with:
|
||||
name: execution-pytest-windows
|
||||
path: eval/pytest-windows.xml
|
||||
if-no-files-found: error
|
||||
retention-days: 5
|
||||
|
|
|
|||
|
|
@ -352,6 +352,62 @@ def test_platform_ci_preserves_required_check_names_with_complete_execution_gate
|
|||
}
|
||||
|
||||
|
||||
def test_test_artifact_retries_cannot_hide_missing_evidence(ci_test_jobs):
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
action_path = ".github/actions/upload-test-artifact/action.yml"
|
||||
action = yaml.safe_load((repo_root / action_path).read_text())
|
||||
uploads = [step for step in action["runs"]["steps"] if "uses" in step]
|
||||
assert len(uploads) == 3
|
||||
first, second, final = uploads
|
||||
assert first["if"] == "${{ always() }}"
|
||||
assert second["if"] == "${{ !cancelled() && steps.first.outcome == 'failure' }}"
|
||||
assert final["if"] == "${{ !cancelled() && steps.second.outcome == 'failure' }}"
|
||||
assert first["id"] == "first" and second["id"] == "second"
|
||||
assert first["continue-on-error"] is True
|
||||
assert second["continue-on-error"] is True
|
||||
assert not final.get("continue-on-error", False)
|
||||
assert not first["with"].get("overwrite", False)
|
||||
assert second["with"]["overwrite"] is True
|
||||
assert final["with"]["overwrite"] is True
|
||||
for step in uploads:
|
||||
assert re.fullmatch(r"actions/upload-artifact@[a-f0-9]{40}", step["uses"])
|
||||
assert step["with"]["if-no-files-found"] == "error"
|
||||
for key in ("name", "path", "retention-days", "include-hidden-files"):
|
||||
assert step["with"][key] == "${{ inputs." + key + " }}"
|
||||
|
||||
evidence_jobs = {
|
||||
"tests", "coverage-merge", "cross-platform", "benchmarks",
|
||||
"scope-resolution-benchmark", "eval-tests", "test-completeness",
|
||||
"eval-containment-windows", "eval-containment-linux", "tool-accuracy",
|
||||
}
|
||||
for name in evidence_jobs:
|
||||
job = ci_test_jobs[name]
|
||||
reports = [
|
||||
step for step in job["steps"]
|
||||
if step.get("uses") == "./.github/actions/upload-test-artifact"
|
||||
]
|
||||
assert reports, f"{name} must publish execution evidence"
|
||||
assert not job.get("continue-on-error", False)
|
||||
for step in reports:
|
||||
assert not step.get("continue-on-error", False)
|
||||
assert step["with"]["name"] and step["with"]["path"]
|
||||
|
||||
artifact_names = set()
|
||||
for job in ci_test_jobs.values():
|
||||
for step in job.get("steps", []):
|
||||
assert not step.get("uses", "").startswith("actions/upload-artifact@")
|
||||
if step.get("uses") != "./.github/actions/upload-test-artifact":
|
||||
continue
|
||||
name = step["with"]["name"]
|
||||
assert name not in artifact_names, f"Retry could overwrite another job: {name}"
|
||||
artifact_names.add(name)
|
||||
for axis in job.get("strategy", {}).get("matrix", {}):
|
||||
if axis not in {"include", "exclude"}:
|
||||
assert "${{ matrix." + axis + " }}" in name, (
|
||||
f"Artifact {name} must distinguish matrix axis {axis}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("native_result", ["success", "failure", "cancelled", "skipped", ""])
|
||||
@pytest.mark.parametrize("execution_result", ["success", "failure", "cancelled", "skipped", ""])
|
||||
@pytest.mark.parametrize("scope_bench_result", ["success", "failure", "cancelled", "skipped", ""])
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
* same-named callable in the file.
|
||||
*
|
||||
* For a synthetic corpus at two scales it reports:
|
||||
* - elapsed_ms_small / elapsed_ms_large (fastest of REPS, see `fastest`) + a scaling ratio
|
||||
* - elapsed_ms_small / elapsed_ms_large (median of window minima) + a scaling ratio
|
||||
* `(t_large/t_small)/(LARGE/SMALL)`: ~1.0 linear, ~3.x quadratic;
|
||||
* - `callable_only_ms_large`, the same corpus with the PRE-#2693 def
|
||||
* population, so the cost the widening actually added stays visible as
|
||||
|
|
@ -48,8 +48,9 @@ const BASELINE_PATH = path.resolve(__dirname, 'baselines.json');
|
|||
|
||||
const SMALL = 250;
|
||||
const LARGE = 800;
|
||||
const REPS = 15;
|
||||
const WARMUP = 5;
|
||||
const REPS = 7;
|
||||
const WARMUP = 2;
|
||||
const WINDOW_MS = 1000;
|
||||
|
||||
/**
|
||||
* Deterministic synthetic corpus — no randomness, so the fingerprint is stable.
|
||||
|
|
@ -144,34 +145,23 @@ function callableOnlyScopes(scopes) {
|
|||
return { defs: { byId } };
|
||||
}
|
||||
|
||||
/**
|
||||
* MIN, not median. Both scales are timed in one process, and every source of
|
||||
* error here is additive — scheduler preemption, GC, a noisy neighbour on a
|
||||
* shared CI runner. The fastest observed run is the closest estimate of the
|
||||
* uncontended cost, so the derived ratios stay comparable across machines
|
||||
* instead of tracking whatever else the box was doing. (Measured directly: the
|
||||
* same build reported an overhead of 1.65 idle and 2.03 while a test shard was
|
||||
* running — a median-based gate would have to be loosened until it could no
|
||||
* longer detect the regression it exists to catch.)
|
||||
*/
|
||||
function fastest(values) {
|
||||
return Math.min(...values);
|
||||
function median(values) {
|
||||
return [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)];
|
||||
}
|
||||
|
||||
function timeIndex(scopes, nodeLookup, graph) {
|
||||
// Warm up before timing: the first calls carry JIT compilation of the whole
|
||||
// resolve chain, and the widened and callable-only runs would otherwise be
|
||||
// measured at different optimisation tiers — which alone moved the reported
|
||||
// overhead by ~30%.
|
||||
for (let w = 0; w < WARMUP; w++) buildGraphTargetIndex(scopes, nodeLookup, undefined, graph);
|
||||
const samples = [];
|
||||
// A short handful of calls can all overlap JIT, GC or scheduler pauses.
|
||||
// Keep the uncontended-cost estimator, but give each variant a full window
|
||||
// to reach it. Retaining only the minimum also bounds sampling memory.
|
||||
const start = performance.now();
|
||||
let best = Infinity;
|
||||
let last;
|
||||
for (let r = 0; r < REPS; r++) {
|
||||
do {
|
||||
const t0 = performance.now();
|
||||
last = buildGraphTargetIndex(scopes, nodeLookup, undefined, graph);
|
||||
samples.push(performance.now() - t0);
|
||||
}
|
||||
return { ms: fastest(samples), result: last };
|
||||
best = Math.min(best, performance.now() - t0);
|
||||
} while (performance.now() - start < WINDOW_MS);
|
||||
return { ms: best, result: last };
|
||||
}
|
||||
|
||||
function fingerprint(targets) {
|
||||
|
|
@ -179,29 +169,46 @@ function fingerprint(targets) {
|
|||
return crypto.createHash('sha256').update(lines.join('\n')).digest('hex');
|
||||
}
|
||||
|
||||
const scales = {};
|
||||
for (const [name, fileCount] of [
|
||||
const cases = [
|
||||
['small', SMALL],
|
||||
['large', LARGE],
|
||||
]) {
|
||||
const { graph, scopes, nodeLookup } = buildCorpus(fileCount);
|
||||
const widened = timeIndex(scopes, nodeLookup, graph);
|
||||
const callableOnly = timeIndex(callableOnlyScopes(scopes), nodeLookup, graph);
|
||||
scales[name] = {
|
||||
files: fileCount,
|
||||
defs: scopes.defs.byId.size,
|
||||
ms: widened.ms,
|
||||
callable_only_ms: callableOnly.ms,
|
||||
targets: widened.result.size,
|
||||
callable_only_targets: callableOnly.result.size,
|
||||
fingerprint: fingerprint(widened.result),
|
||||
].map(([name, files]) => ({ name, files, ...buildCorpus(files) }));
|
||||
const variants = cases.flatMap((corpus) => [
|
||||
corpus,
|
||||
{ ...corpus, scopes: callableOnlyScopes(corpus.scopes) },
|
||||
]);
|
||||
const samples = variants.map(() => []);
|
||||
const results = [];
|
||||
// Measure all four variants in each round, reversing their order to avoid
|
||||
// consistently favouring one tier or scale. Discard complete warmup rounds.
|
||||
// Pair ratios within rounds so a change in runner load does not compare a
|
||||
// quiet control period with a busy widened period (or small with large).
|
||||
for (let round = 0; round < WARMUP + REPS; round++) {
|
||||
for (const i of round % 2 === 0 ? [0, 1, 2, 3] : [3, 2, 1, 0]) {
|
||||
const { scopes, nodeLookup, graph } = variants[i];
|
||||
const timed = timeIndex(scopes, nodeLookup, graph);
|
||||
results[i] = timed.result;
|
||||
if (round >= WARMUP) samples[i].push(timed.ms);
|
||||
}
|
||||
}
|
||||
|
||||
const scales = {};
|
||||
for (const [i, corpus] of cases.entries()) {
|
||||
scales[corpus.name] = {
|
||||
files: corpus.files,
|
||||
defs: corpus.scopes.defs.byId.size,
|
||||
ms: median(samples[i * 2]),
|
||||
callable_only_ms: median(samples[i * 2 + 1]),
|
||||
targets: results[i * 2].size,
|
||||
callable_only_targets: results[i * 2 + 1].size,
|
||||
fingerprint: fingerprint(results[i * 2]),
|
||||
};
|
||||
}
|
||||
|
||||
const scalingRatio = scales.large.ms / scales.small.ms / (LARGE / SMALL);
|
||||
const scalingRatio = median(samples[2].map((ms, i) => ms / samples[0][i] / (LARGE / SMALL)));
|
||||
// How much slower the widened gate is than the pre-#2693 one on the same
|
||||
// corpus. 1.0 = free; 2.0 = the widening doubled the index build.
|
||||
const wideningOverhead = scales.large.ms / scales.large.callable_only_ms;
|
||||
const wideningOverhead = median(samples[2].map((ms, i) => ms / samples[3][i]));
|
||||
|
||||
const report = {
|
||||
small: scales.small,
|
||||
|
|
|
|||
|
|
@ -59,13 +59,13 @@ export const WINDOWS_WEIGHTS_SEC: Readonly<Record<string, number>> = {
|
|||
'test/integration/rename-provider-c.test.ts': 202,
|
||||
'test/integration/rename-provider-cobol.test.ts': 75,
|
||||
'test/integration/rename-provider-cpp.test.ts': 250,
|
||||
'test/integration/rename-provider-csharp.test.ts': 200,
|
||||
'test/integration/rename-provider-csharp.test.ts': 266,
|
||||
'test/integration/rename-provider-dart.test.ts': 425,
|
||||
'test/integration/rename-provider-go.test.ts': 221,
|
||||
'test/integration/rename-provider-java.test.ts': 300,
|
||||
'test/integration/rename-provider-javascript.test.ts': 225,
|
||||
'test/integration/rename-provider-kotlin.test.ts': 300,
|
||||
'test/integration/rename-provider-objective-c.test.ts': 285,
|
||||
'test/integration/rename-provider-objective-c.test.ts': 386,
|
||||
'test/integration/rename-provider-php.test.ts': 100,
|
||||
'test/integration/rename-provider-python.test.ts': 375,
|
||||
'test/integration/rename-provider-ruby.test.ts': 100,
|
||||
|
|
@ -78,7 +78,12 @@ export const WINDOWS_WEIGHTS_SEC: Readonly<Record<string, number>> = {
|
|||
// Windows persisted-fixture profile above. Replace with observed CI timing.
|
||||
'test/integration/rename-preview-plan.test.ts': 175,
|
||||
'test/integration/mcp-index-generation.test.ts': 201,
|
||||
'test/unit/incremental-dirty-recovery.test.ts': 76,
|
||||
// Windows run 39934695864 timed out after undercharged native suites;
|
||||
// 40172490240 supplies the query-symbol and dirty-recovery measurements.
|
||||
'test/integration/incremental-method-integrity.test.ts': 145,
|
||||
'test/integration/fts-repair-warm-session.test.ts': 62,
|
||||
'test/integration/query-symbol-integrity.test.ts': 34,
|
||||
'test/unit/incremental-dirty-recovery.test.ts': 128,
|
||||
// Re-measured after the analyze --watch e2e landed in #3072. The previous
|
||||
// 361 s entry undercharged this suite and left shard 1 close to the watchdog.
|
||||
'test/integration/cli-e2e.test.ts': 621,
|
||||
|
|
|
|||
|
|
@ -74,38 +74,50 @@ describe('segmentCjkSpans', () => {
|
|||
expect(outputBytes).toBeLessThan(expectedBytes * 1.05);
|
||||
});
|
||||
|
||||
it('scales linearly on realistic interleaved CJK/non-CJK content, not quadratically', () => {
|
||||
// Regression guard: an earlier implementation indexed into the growing
|
||||
// accumulated output string once per run boundary, which forces V8 to
|
||||
// flatten its internal rope representation on every access — O(n^2) on
|
||||
// content that alternates CJK and non-CJK runs (ordinary source code
|
||||
// with inline CJK comments, the feature's actual target). A single-run
|
||||
// input (like the growth-factor test above) never exercises this path,
|
||||
// since there is only one run boundary regardless of size.
|
||||
const unit = '采购订单自动审批流程 // approve the request after manual review\n';
|
||||
const build = (totalBytes: number) => unit.repeat(Math.ceil(totalBytes / unit.length));
|
||||
it.skipIf(!process.env.GITNEXUS_BENCH)(
|
||||
'scales linearly on realistic interleaved CJK/non-CJK content, not quadratically',
|
||||
() => {
|
||||
// Regression guard: an earlier implementation indexed into the growing
|
||||
// accumulated output string once per run boundary, which forces V8 to
|
||||
// flatten its internal rope representation on every access — O(n^2) on
|
||||
// content that alternates CJK and non-CJK runs (ordinary source code
|
||||
// with inline CJK comments, the feature's actual target). A single-run
|
||||
// input (like the growth-factor test above) never exercises this path,
|
||||
// since there is only one run boundary regardless of size.
|
||||
const unit = '采购订单自动审批流程 // approve the request after manual review\n';
|
||||
const build = (totalBytes: number) => unit.repeat(Math.ceil(totalBytes / unit.length));
|
||||
|
||||
const small = build(64 * 1024);
|
||||
const large = build(512 * 1024); // 8x the input size
|
||||
const small = build(64 * 1024);
|
||||
const large = build(512 * 1024); // 8x the input size
|
||||
|
||||
const timeOf = (input: string) => {
|
||||
const start = performance.now();
|
||||
segmentCjkSpans(input);
|
||||
return performance.now() - start;
|
||||
};
|
||||
const timeOf = (input: string) => {
|
||||
const start = performance.now();
|
||||
segmentCjkSpans(input);
|
||||
return performance.now() - start;
|
||||
};
|
||||
|
||||
// Warm up the JIT before measuring either size.
|
||||
timeOf(small);
|
||||
timeOf(large);
|
||||
// run-benchmarks.ts discovers this opt-in test and runs it serially.
|
||||
// Sample both sizes after warmup: one scheduler pause must not look like
|
||||
// the quadratic regression this guard is intended to catch.
|
||||
const smallSamples: number[] = [];
|
||||
const largeSamples: number[] = [];
|
||||
for (let sample = 0; sample < 10; sample++) {
|
||||
const smallMs = timeOf(small);
|
||||
const largeMs = timeOf(large);
|
||||
if (sample >= 3) {
|
||||
smallSamples.push(smallMs);
|
||||
largeSamples.push(largeMs);
|
||||
}
|
||||
}
|
||||
const smallMs = Math.min(...smallSamples);
|
||||
const largeMs = Math.min(...largeSamples);
|
||||
|
||||
const smallMs = timeOf(small);
|
||||
const largeMs = timeOf(large);
|
||||
|
||||
// Linear scaling means ~8x input takes roughly ~8x time, with headroom
|
||||
// for noise; quadratic scaling would mean ~64x time. 20x catches the
|
||||
// regression while tolerating CI timing variance.
|
||||
expect(largeMs).toBeLessThan(Math.max(smallMs, 1) * 20);
|
||||
});
|
||||
// Linear scaling means ~8x input takes roughly ~8x time, with headroom
|
||||
// for noise; quadratic scaling would mean ~64x time. 20x catches the
|
||||
// regression while tolerating CI timing variance.
|
||||
expect(largeMs).toBeLessThan(Math.max(smallMs, 1) * 20);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('containsCjkIdeograph', () => {
|
||||
|
|
|
|||
|
|
@ -134,6 +134,26 @@ describe('cross-platform shard partition', () => {
|
|||
expect(Math.max(...loads)).toBeLessThanOrEqual(17 * 60);
|
||||
});
|
||||
|
||||
it('leaves watchdog headroom for the October 23 native-suite timeout', () => {
|
||||
// Windows run 39934695864: shard 10 timed out with Kotlin still pending.
|
||||
// Use its completed suite times, plus the October 25 green run 40172490240
|
||||
// for query-symbol-integrity and incremental-dirty-recovery. These measured
|
||||
// costs must remain independent of the scheduling hints being tested.
|
||||
const observed: Readonly<Record<string, number>> = {
|
||||
'test/integration/rename-provider-objective-c.test.ts': 385.174,
|
||||
'test/integration/rename-provider-csharp.test.ts': 265.99,
|
||||
'test/integration/incremental-method-integrity.test.ts': 144.974,
|
||||
'test/integration/fts-repair-warm-session.test.ts': 61.015,
|
||||
'test/integration/query-symbol-integrity.test.ts': 33.892,
|
||||
'test/unit/incremental-dirty-recovery.test.ts': 127.8,
|
||||
};
|
||||
const floor = weightOf('test/unmeasured.test.ts');
|
||||
const loads = allShards(ALL_CROSS_PLATFORM, SHARD_TOTAL).map((files) =>
|
||||
files.reduce((sum, file) => sum + Math.max(weightOf(file), (observed[file] ?? 0) + floor), 0),
|
||||
);
|
||||
expect(Math.max(...loads)).toBeLessThanOrEqual(17 * 60);
|
||||
});
|
||||
|
||||
it('keeps each shard within a shard of the ideal weight', () => {
|
||||
const shards = allShards(ALL_CROSS_PLATFORM, SHARD_TOTAL);
|
||||
const weights = shards.map(shardWeight);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue