mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-21 22:21:33 +00:00
fix(ci): speed up fast extension scheduling
This commit is contained in:
@@ -82,12 +82,16 @@ export function loadTestCatalog() {
|
||||
const reasons = [];
|
||||
const isolated =
|
||||
options.unitMemoryIsolatedFiles?.includes(normalizedFile) ||
|
||||
options.extensionTimedIsolatedFiles?.includes(normalizedFile) ||
|
||||
unitForkIsolatedFileSet.has(normalizedFile) ||
|
||||
extensionForkIsolatedFileSet.has(normalizedFile) ||
|
||||
channelIsolatedFileSet.has(normalizedFile);
|
||||
if (options.unitMemoryIsolatedFiles?.includes(normalizedFile)) {
|
||||
reasons.push("unit-memory-isolated");
|
||||
}
|
||||
if (options.extensionTimedIsolatedFiles?.includes(normalizedFile)) {
|
||||
reasons.push("extensions-timed-heavy");
|
||||
}
|
||||
if (unitForkIsolatedFileSet.has(normalizedFile)) {
|
||||
reasons.push("unit-isolated-manifest");
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
loadUnitTimingManifest,
|
||||
packFilesByDuration,
|
||||
packFilesByDurationWithBaseLoads,
|
||||
selectTimedHeavyFiles,
|
||||
selectUnitHeavyFileGroups,
|
||||
} from "../test-runner-manifest.mjs";
|
||||
import { loadTestCatalog, normalizeRepoPath } from "./catalog.mjs";
|
||||
@@ -430,6 +431,30 @@ const resolveUnitHeavyFileGroups = (context) => {
|
||||
};
|
||||
};
|
||||
|
||||
const resolveExtensionTimedHeavyFiles = (context) => {
|
||||
const { env, runtime, catalog, extensionTimingManifest } = context;
|
||||
const timedHeavyExtensionFileLimit = parseEnvNumber(
|
||||
env,
|
||||
"OPENCLAW_TEST_HEAVY_EXTENSION_FILE_LIMIT",
|
||||
runtime.isCI ? 16 : 8,
|
||||
);
|
||||
const timedHeavyExtensionMinDurationMs = parseEnvNumber(
|
||||
env,
|
||||
"OPENCLAW_TEST_HEAVY_EXTENSION_MIN_MS",
|
||||
runtime.isCI ? 9_000 : 12_000,
|
||||
);
|
||||
return selectTimedHeavyFiles({
|
||||
candidates: catalog.allKnownTestFiles.filter(
|
||||
(file) =>
|
||||
file.startsWith(BUNDLED_PLUGIN_PATH_PREFIX) &&
|
||||
!catalog.extensionForkIsolatedFileSet.has(file),
|
||||
),
|
||||
limit: timedHeavyExtensionFileLimit,
|
||||
minDurationMs: timedHeavyExtensionMinDurationMs,
|
||||
timings: extensionTimingManifest,
|
||||
});
|
||||
};
|
||||
|
||||
const buildDefaultUnits = (context, request) => {
|
||||
const {
|
||||
env,
|
||||
@@ -455,10 +480,15 @@ const buildDefaultUnits = (context, request) => {
|
||||
const unitMemoryIsolatedFiles = [...memoryHeavyUnitFiles].filter(
|
||||
(file) => !catalog.unitBehaviorOverrideSet.has(file),
|
||||
);
|
||||
const extensionTimedHeavyFiles = resolveExtensionTimedHeavyFiles(context);
|
||||
const unitSchedulingOverrideSet = new Set([
|
||||
...catalog.unitBehaviorOverrideSet,
|
||||
...memoryHeavyUnitFiles,
|
||||
]);
|
||||
const extensionSchedulingOverrideSet = new Set([
|
||||
...catalog.extensionForkIsolatedFiles,
|
||||
...extensionTimedHeavyFiles,
|
||||
]);
|
||||
const unitFastExcludedFiles = [
|
||||
...new Set([
|
||||
...unitSchedulingOverrideSet,
|
||||
@@ -483,8 +513,7 @@ const buildDefaultUnits = (context, request) => {
|
||||
catalog.unitThreadPinnedFiles.length > 0;
|
||||
const extensionSharedCandidateFiles = catalog.allKnownTestFiles.filter(
|
||||
(file) =>
|
||||
file.startsWith(BUNDLED_PLUGIN_PATH_PREFIX) &&
|
||||
!catalog.extensionForkIsolatedFileSet.has(file),
|
||||
file.startsWith(BUNDLED_PLUGIN_PATH_PREFIX) && !extensionSchedulingOverrideSet.has(file),
|
||||
);
|
||||
const channelSharedCandidateFiles = catalog.allKnownTestFiles.filter(
|
||||
(file) =>
|
||||
@@ -708,6 +737,18 @@ const buildDefaultUnits = (context, request) => {
|
||||
}),
|
||||
);
|
||||
}
|
||||
for (const file of extensionTimedHeavyFiles) {
|
||||
units.push(
|
||||
createExecutionUnit(context, {
|
||||
id: `extensions-${path.basename(file, ".test.ts")}-isolated`,
|
||||
surface: "extensions",
|
||||
isolate: true,
|
||||
estimatedDurationMs: estimateExtensionDurationMs(file),
|
||||
args: ["vitest", "run", "--config", "vitest.extensions.config.ts", "--pool=forks", file],
|
||||
reasons: ["extensions-timed-heavy"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
const extensionBatches = splitFilesByBalancedDurationBudget(
|
||||
extensionSharedCandidateFiles,
|
||||
extensionsBatchTargetMs,
|
||||
@@ -919,6 +960,7 @@ const buildTargetedUnits = (context, request) => {
|
||||
return [];
|
||||
}
|
||||
const unitMemoryIsolatedFiles = request.unitMemoryIsolatedFiles ?? [];
|
||||
const extensionTimedIsolatedFiles = request.extensionTimedIsolatedFiles ?? [];
|
||||
const estimateUnitDurationMs = (file) =>
|
||||
context.unitTimingManifest.files[file]?.durationMs ??
|
||||
context.unitTimingManifest.defaultDurationMs;
|
||||
@@ -942,6 +984,7 @@ const buildTargetedUnits = (context, request) => {
|
||||
if (matchedFiles.length === 0) {
|
||||
const classification = context.catalog.classifyTestFile(normalizeRepoPath(fileFilter), {
|
||||
unitMemoryIsolatedFiles,
|
||||
extensionTimedIsolatedFiles,
|
||||
});
|
||||
const key = `${classification.legacyBasePinned ? "base-pinned" : classification.surface}:${
|
||||
classification.isolated ? "isolated" : "default"
|
||||
@@ -954,6 +997,7 @@ const buildTargetedUnits = (context, request) => {
|
||||
for (const matchedFile of matchedFiles) {
|
||||
const classification = context.catalog.classifyTestFile(matchedFile, {
|
||||
unitMemoryIsolatedFiles,
|
||||
extensionTimedIsolatedFiles,
|
||||
});
|
||||
const key = `${classification.legacyBasePinned ? "base-pinned" : classification.surface}:${
|
||||
classification.isolated ? "isolated" : "default"
|
||||
@@ -972,6 +1016,7 @@ const buildTargetedUnits = (context, request) => {
|
||||
context,
|
||||
context.catalog.classifyTestFile(file, {
|
||||
unitMemoryIsolatedFiles,
|
||||
extensionTimedIsolatedFiles,
|
||||
}),
|
||||
[file],
|
||||
),
|
||||
@@ -1415,10 +1460,11 @@ function resolveSurfaceAwareTopLevelParallelLimit(context, units, defaultLimit)
|
||||
return defaultLimit;
|
||||
}
|
||||
|
||||
// Shared extension batches can each retain multiple GiB in CI. Limit that
|
||||
// phase to two concurrent lanes so provider-contract checks are not starved
|
||||
// behind unrelated memory-heavy extension suites.
|
||||
return Math.min(defaultLimit, 2);
|
||||
// Shared extension batches can each retain multiple GiB in CI, but capping
|
||||
// them at two lanes stretches the extensions phase into double-digit minutes.
|
||||
// Keep one slot in reserve versus the base CI budget while still allowing
|
||||
// enough overlap to amortize startup and import overhead.
|
||||
return Math.min(defaultLimit, 3);
|
||||
}
|
||||
|
||||
export function explainExecutionTarget(request, options = {}) {
|
||||
@@ -1437,8 +1483,10 @@ export function explainExecutionTarget(request, options = {}) {
|
||||
const unitMemoryIsolatedFiles = [...memoryHeavyFiles].filter(
|
||||
(file) => !context.catalog.unitBehaviorOverrideSet.has(file),
|
||||
);
|
||||
const extensionTimedIsolatedFiles = resolveExtensionTimedHeavyFiles(context);
|
||||
const classification = context.catalog.classifyTestFile(normalizedTarget, {
|
||||
unitMemoryIsolatedFiles,
|
||||
extensionTimedIsolatedFiles,
|
||||
});
|
||||
const targetedUnit = createTargetedUnit(context, classification, [normalizedTarget]);
|
||||
return {
|
||||
@@ -1522,11 +1570,13 @@ export function buildExecutionPlan(request, options = {}) {
|
||||
}
|
||||
|
||||
const defaultPlanning = buildDefaultUnits(context, { ...request, fileFilters });
|
||||
const extensionTimedIsolatedFiles = resolveExtensionTimedHeavyFiles(context);
|
||||
let units = defaultPlanning.units;
|
||||
const targetedUnits = buildTargetedUnits(context, {
|
||||
...request,
|
||||
fileFilters,
|
||||
unitMemoryIsolatedFiles: defaultPlanning.unitMemoryIsolatedFiles,
|
||||
extensionTimedIsolatedFiles,
|
||||
});
|
||||
if (context.configuredShardCount !== null && context.shardCount > 1) {
|
||||
units = expandUnitsAcrossTopLevelShards(context, units);
|
||||
|
||||
78
test/fixtures/test-timings.extensions.json
vendored
78
test/fixtures/test-timings.extensions.json
vendored
@@ -1,22 +1,66 @@
|
||||
{
|
||||
"config": "vitest.extensions.config.ts",
|
||||
"generatedAt": "2026-03-26T17:02:23.374Z",
|
||||
"generatedAt": "2026-03-31T06:24:09.128Z",
|
||||
"defaultDurationMs": 1000,
|
||||
"files": {
|
||||
"extensions/bluebubbles/src/monitor.webhook-auth.test.ts": {
|
||||
"durationMs": 30152.246337890625,
|
||||
"durationMs": 30106,
|
||||
"testCount": 19
|
||||
},
|
||||
"extensions/matrix/src/plugin-entry.runtime.test.ts": {
|
||||
"durationMs": 62700,
|
||||
"testCount": 1
|
||||
},
|
||||
"extensions/matrix/src/matrix/monitor/index.test.ts": {
|
||||
"durationMs": 23302.03173828125,
|
||||
"testCount": 8
|
||||
},
|
||||
"extensions/matrix/src/cli.test.ts": {
|
||||
"durationMs": 21762,
|
||||
"testCount": 25
|
||||
},
|
||||
"extensions/matrix/src/matrix/accounts.test.ts": {
|
||||
"durationMs": 18285,
|
||||
"testCount": 16
|
||||
},
|
||||
"extensions/matrix/src/matrix/client/storage.test.ts": {
|
||||
"durationMs": 17251,
|
||||
"testCount": 13
|
||||
},
|
||||
"extensions/matrix/src/matrix/send.test.ts": {
|
||||
"durationMs": 15471,
|
||||
"testCount": 17
|
||||
},
|
||||
"extensions/mattermost/src/dm-policy.contract.test.ts": {
|
||||
"durationMs": 15349,
|
||||
"testCount": 2
|
||||
},
|
||||
"extensions/matrix/src/matrix/thread-bindings.test.ts": {
|
||||
"durationMs": 14053,
|
||||
"testCount": 11
|
||||
},
|
||||
"extensions/openai/provider-catalog.contract.test.ts": {
|
||||
"durationMs": 13306,
|
||||
"testCount": 3
|
||||
},
|
||||
"extensions/line/src/bot-handlers.test.ts": {
|
||||
"durationMs": 13272,
|
||||
"testCount": 23
|
||||
},
|
||||
"extensions/matrix/src/matrix/draft-stream.test.ts": {
|
||||
"durationMs": 12308,
|
||||
"testCount": 13
|
||||
},
|
||||
"extensions/line/src/setup-surface.test.ts": {
|
||||
"durationMs": 12208.14111328125,
|
||||
"testCount": 8
|
||||
},
|
||||
"extensions/zai/plugin-registration.contract.test.ts": {
|
||||
"durationMs": 10292,
|
||||
"testCount": 2
|
||||
},
|
||||
"extensions/mattermost/src/mattermost/client.retry.test.ts": {
|
||||
"durationMs": 9429.540283203125,
|
||||
"durationMs": 9816,
|
||||
"testCount": 17
|
||||
},
|
||||
"extensions/feishu/src/monitor.webhook-security.test.ts": {
|
||||
@@ -39,10 +83,6 @@
|
||||
"durationMs": 4410.16259765625,
|
||||
"testCount": 10
|
||||
},
|
||||
"extensions/matrix/src/matrix/send.test.ts": {
|
||||
"durationMs": 4319.607421875,
|
||||
"testCount": 16
|
||||
},
|
||||
"extensions/matrix/src/matrix/monitor/events.test.ts": {
|
||||
"durationMs": 2489.9775390625,
|
||||
"testCount": 29
|
||||
@@ -51,10 +91,6 @@
|
||||
"durationMs": 1024.728759765625,
|
||||
"testCount": 48
|
||||
},
|
||||
"extensions/matrix/src/matrix/thread-bindings.test.ts": {
|
||||
"durationMs": 461.74658203125,
|
||||
"testCount": 11
|
||||
},
|
||||
"extensions/bluebubbles/src/monitor.test.ts": {
|
||||
"durationMs": 453.485595703125,
|
||||
"testCount": 64
|
||||
@@ -124,12 +160,8 @@
|
||||
"testCount": 4
|
||||
},
|
||||
"extensions/line/src/bot-message-context.test.ts": {
|
||||
"durationMs": 155.55517578125,
|
||||
"testCount": 11
|
||||
},
|
||||
"extensions/matrix/src/matrix/client/storage.test.ts": {
|
||||
"durationMs": 153.7314453125,
|
||||
"testCount": 12
|
||||
"durationMs": 9155,
|
||||
"testCount": 14
|
||||
},
|
||||
"extensions/acpx/src/runtime-internals/mcp-proxy.test.ts": {
|
||||
"durationMs": 152.1640625,
|
||||
@@ -211,10 +243,6 @@
|
||||
"durationMs": 88.814453125,
|
||||
"testCount": 20
|
||||
},
|
||||
"extensions/matrix/src/cli.test.ts": {
|
||||
"durationMs": 87.73828125,
|
||||
"testCount": 25
|
||||
},
|
||||
"extensions/tlon/src/core.test.ts": {
|
||||
"durationMs": 87.28759765625,
|
||||
"testCount": 11
|
||||
@@ -507,10 +535,6 @@
|
||||
"durationMs": 11.179931640625,
|
||||
"testCount": 5
|
||||
},
|
||||
"extensions/line/src/bot-handlers.test.ts": {
|
||||
"durationMs": 10.427490234375,
|
||||
"testCount": 23
|
||||
},
|
||||
"extensions/github-copilot/models.test.ts": {
|
||||
"durationMs": 10.251953125,
|
||||
"testCount": 20
|
||||
@@ -571,10 +595,6 @@
|
||||
"durationMs": 7.52587890625,
|
||||
"testCount": 4
|
||||
},
|
||||
"extensions/matrix/src/matrix/accounts.test.ts": {
|
||||
"durationMs": 7.3603515625,
|
||||
"testCount": 12
|
||||
},
|
||||
"extensions/acpx/src/config.test.ts": {
|
||||
"durationMs": 7.31298828125,
|
||||
"testCount": 13
|
||||
|
||||
@@ -115,7 +115,40 @@ describe("test planner", () => {
|
||||
expect(plan.runtimeCapabilities.runtimeProfileName).toBe("ci-linux");
|
||||
expect(plan.executionBudget.topLevelParallelLimitNoIsolate).toBe(4);
|
||||
expect(sharedExtensionBatches.length).toBeGreaterThan(1);
|
||||
expect(plan.topLevelParallelLimit).toBe(2);
|
||||
expect(plan.topLevelParallelLimit).toBe(3);
|
||||
artifacts.cleanupTempArtifacts();
|
||||
});
|
||||
|
||||
it("auto-isolates timed-heavy extension suites in CI", () => {
|
||||
const env = {
|
||||
CI: "true",
|
||||
GITHUB_ACTIONS: "true",
|
||||
RUNNER_OS: "Linux",
|
||||
OPENCLAW_TEST_HOST_CPU_COUNT: "4",
|
||||
OPENCLAW_TEST_HOST_MEMORY_GIB: "16",
|
||||
};
|
||||
const artifacts = createExecutionArtifacts(env);
|
||||
const plan = buildExecutionPlan(
|
||||
{
|
||||
profile: null,
|
||||
mode: "ci",
|
||||
surfaces: ["extensions"],
|
||||
passthroughArgs: [],
|
||||
},
|
||||
{
|
||||
env,
|
||||
platform: "linux",
|
||||
writeTempJsonArtifact: artifacts.writeTempJsonArtifact,
|
||||
},
|
||||
);
|
||||
|
||||
const hotspotUnit = plan.selectedUnits.find(
|
||||
(unit) => unit.id === "extensions-plugin-entry.runtime-isolated",
|
||||
);
|
||||
|
||||
expect(hotspotUnit).toBeTruthy();
|
||||
expect(hotspotUnit?.isolate).toBe(true);
|
||||
expect(hotspotUnit?.reasons).toContain("extensions-timed-heavy");
|
||||
artifacts.cleanupTempArtifacts();
|
||||
});
|
||||
|
||||
@@ -385,6 +418,25 @@ describe("test planner", () => {
|
||||
expect(absoluteExplanation.reasons).toEqual(relativeExplanation.reasons);
|
||||
});
|
||||
|
||||
it("explains timed-heavy extension suites as isolated", () => {
|
||||
const explanation = explainExecutionTarget(
|
||||
{
|
||||
mode: "ci",
|
||||
fileFilters: ["extensions/matrix/src/plugin-entry.runtime.test.ts"],
|
||||
},
|
||||
{
|
||||
env: {
|
||||
CI: "true",
|
||||
GITHUB_ACTIONS: "true",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(explanation.surface).toBe("extensions");
|
||||
expect(explanation.isolate).toBe(true);
|
||||
expect(explanation.reasons).toContain("extensions-timed-heavy");
|
||||
});
|
||||
|
||||
it("does not leak default-plan shard assignments into targeted units with the same id", () => {
|
||||
const artifacts = createExecutionArtifacts({});
|
||||
const plan = buildExecutionPlan(
|
||||
|
||||
Reference in New Issue
Block a user