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
150 changes: 150 additions & 0 deletions .github/scripts/application-build-plan.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Keep the existing configuration for every plan. Narrow PRs save compilation
// by invoking check targets, whose dependencies remain owned by CMake. The full
// build still compiles ALL, including archived integration compatibility inputs.
export const applicationConfigureFlags = Object.freeze([
'-DARK_BUILD_APPLICATIONS=ON',
'-DARK_BUILD_BENCHMARKS=ON',
'-DARK_BUILD_PLUGIN_HTTP=ON',
'-DARK_BUILD_PLUGIN_CONSUL=ON',
'-DARK_BUILD_PLUGIN_ETCD_CLIENT=ON',
'-DARK_BUILD_PROTO=ON',
'-DARK_BUILD_TESTS=ON',
'-DARK_BUILD_CORE_TESTS=ON',
'-DARK_BUILD_COMPRESSION_TESTS=ON',
'-DARK_BUILD_EXAMPLES=ON',
'-DARK_BUILD_GUEST_ROOM_EXAMPLE=ON',
'-DARK_BUILD_CONFIG_TOOL=ON',
'-DARK_BUILD_CONFIG_TOOL_TESTS=ON',
'-DASIO_STANDALONE=OFF',
]);

const targetsByPath = new Map();
function register(targets, paths) {
for (const file of paths) {
if (targetsByPath.has(file)) throw new Error(`duplicate application path: ${file}`);
targetsByPath.set(file, Object.freeze([...targets]));
}
}

// Exact reviewed fixtures only. A future file cannot silently inherit a plan
// that omits a newly introduced consumer. Test CMake scripts are runtime inputs;
// CMakeLists.txt and configure/build scripts deliberately remain unregistered.
register(['check-app-shutdown-deadlines'], ['tests/app/shutdown_deadlines.cmake']);
register(['check-app-business-runtime'], ['tests/app/business_runtime.cmake']);
register(['check-app-business-runtime', 'check-app-shutdown-deadlines'], ['tests/app/business_runtime_plugin.cpp']);
register(['check-app-shutdown'], ['tests/app/shutdown_request.cpp', 'tests/app/shutdown_request.cmake']);
register(['check-app-startup-failures'], [
'tests/app/startup_failure_plugin.cpp', 'tests/app/missing_entry_plugin.cpp', 'tests/app/startup_failures.cmake',
]);
register(['check-startup-cleanup'], [
'tests/app/startup_cleanup_allocation.cpp', 'tests/app/startup_cleanup_allocation.cmake',
'tests/core/startup_cleanup.cpp', 'tests/core/headers/startup_cleanup.cpp',
]);
register(['check-app-startup-cleanup'], ['tests/app/startup_cleanup_plugin.cpp', 'tests/app/startup_cleanup.cmake']);
register(['check-app-manifest-failures'], ['tests/app/manifest_failures.cmake']);
register(['check-app-module-dependencies'], ['tests/app/module_dependencies.cmake']);
register(['check-app-presets'], [
'tests/app/application_presets.cmake', 'tests/app/application_preset_stop.cpp', 'tests/app/application_preset_stop.sh',
]);
register(['check-app-bus-configuration'], ['tests/app/bus_configuration.cmake']);
register(['check-bus-plugin'], ['tests/app/bus_plugin.cpp', 'tests/app/bus_plugin.cmake']);
register(['check-net-plugin'], ['tests/app/net_plugin.cpp', 'tests/app/net_plugin.cmake']);
register(['check-net-http-drain'], ['tests/app/http_client_drain.cpp', 'tests/app/http_client_drain.cmake']);
register(['check-http-plugin'], ['tests/app/http_plugin.cpp', 'tests/app/http_plugin.cmake']);
register(['check-http-examples'], ['tests/app/http_examples.cpp', 'tests/app/http_examples.cmake']);
register(['check-kernel-plugin'], ['tests/app/kernel_plugin.cpp', 'tests/app/kernel_plugin.cmake']);
register(['check-typed-examples'], ['tests/app/typed_examples.cpp', 'tests/app/typed_examples.cmake']);
register(['check-static-services', 'check-system-composition'], ['tests/app/static_services.cpp']);
register([
'check-bus-plugin', 'check-net-plugin', 'check-net-http-drain', 'check-kernel-plugin',
'check-typed-examples', 'check-static-services', 'check-system-composition',
], ['tests/app/owner_drain.hpp']);

register(['check-business-runtime'], [
'tests/core/business_runtime.cpp', 'tests/core/business_runtime_contract.cpp',
'tests/core/business_runtime_contract.cmake', 'tests/core/headers/business_runtime.cpp',
]);
register(['check-task-sdk'], [
'tests/core/tasks.cpp', 'tests/core/headers/task.cpp',
'tests/core/headers/task_scheduler.cpp', 'tests/core/headers/scheduler_task.cpp',
]);
register(['check-scheduler-drain'], [
'tests/core/scheduler_drain.cpp', 'tests/core/scheduler_failure.cpp', 'tests/core/scheduler_drain.cmake',
]);
register(['check-scheduler-startup-cleanup'], [
'tests/core/scheduler_startup_cleanup.cpp', 'tests/core/scheduler_startup_cleanup.cmake',
]);
register(['check-scheduler-construction'], ['tests/core/thread_pool_construction.cpp']);
register(['check-native-unload'], ['tests/core/dynamic_library.cpp', 'tests/core/plugin_allocation_entry.cpp']);

register(['check-kernel-initialization'], [
'tests/core/module_initialization.cpp', 'tests/core/kernel_callbacks.cpp',
'tests/core/kernel_resource_ownership.cpp', 'tests/core/table_row_allocation.cpp',
'tests/core/entity_config_copy.cpp', 'tests/core/entity_json_loading.cpp',
'tests/core/kernel_delayed_sync.cpp', 'tests/core/entity_nested_snapshot.cpp',
'tests/core/entity_snapshot.cpp', 'tests/core/entity_snapshot_interface.cpp',
'tests/core/kernel_common_class_callbacks.cpp', 'tests/core/kernel_metadata_files.hpp',
'tests/core/kernel_snapshot_test_access.hpp',
]);
register(['check-entity-stream'], ['tests/core/entity_stream.cpp', 'tests/core/entity_nested_stream.cpp']);
register(['check-scalar-stream-allocation'], ['tests/core/entity_scalar_stream_allocation.cpp']);
register(['check-nested-stream-allocation'], ['tests/core/entity_nested_stream_allocation.cpp']);
register(['check-nested-wire'], [
'tests/core/nested_wire.cpp', 'tests/core/nested_wire_macro.cpp', 'tests/core/nested_wire_macro_first.cpp',
]);
register(['check-proto-naming'], ['tests/proto/naming.cpp']);
register(['check-proto-configuration'], ['tests/proto/check_configuration.cmake']);

register(['check-guest-room'], [
'tests/guest_room/codec.cpp', 'tests/guest_room/server.cpp',
'examples/guest_room/guest_room_module.cpp', 'examples/guest_room/guest_room_module.hpp',
'examples/guest_room/guest_room_plugin.cpp', 'examples/guest_room/guest_room_plugin.hpp',
'examples/guest_room/guest_room_protocol.hpp', 'examples/guest_room/guest_room_wire.hpp',
'examples/guest_room/wire-vectors.json', 'examples/guest_room/stage.cmake',
]);
// The composed-services fixture also consumes this example's schema.
register(['check-guest-room', 'check-system-composition'], ['examples/guest_room/resource/meta/entity_schema.json']);

export const applicationTargetPaths = Object.freeze([...targetsByPath.keys()]);
export const applicationCheckTargets = Object.freeze([...new Set([
'check-core', 'check-entity-benchmark-smoke', ...[...targetsByPath.values()].flat(),
])].sort());

export function applicationTargetsForPath(file) {
const targets = targetsByPath.get(file);
return targets ? [...targets] : null;
}

export function fullApplicationBuildPlan(reason = 'full-fallback') {
return {
version: 1, mode: 'full', reason, buildAll: true,
configureFlags: [...applicationConfigureFlags],
checkTargets: ['check-core', 'check-entity-benchmark-smoke'],
};
}

export function selectApplicationBuildPlan({ eventName, files, scope }) {
// Never reduce queue/manual/unknown events, even if their supplied metadata
// resembles a source PR. Invalid or uncertain scope also retains full work.
if (eventName !== 'pull_request') return fullApplicationBuildPlan('full-event');
if (scope?.event_name !== eventName || scope.files_known !== true || scope.site_available !== true ||
!['documentation', 'modules'].includes(scope.validation_stage) ||
!['documentation-only', 'module-paths'].includes(scope.reason) || !Array.isArray(files)) {
return fullApplicationBuildPlan('uncertain-scope');
}
if (scope.applications_check === false) {
return { version: 1, mode: 'skip', reason: 'applications-not-selected', buildAll: false,
configureFlags: [], checkTargets: [] };
}
if (scope.applications_check !== true || files.length === 0) return fullApplicationBuildPlan('empty-or-invalid-application-scope');
const targets = new Set();
for (const file of files) {
const selected = applicationTargetsForPath(file);
if (!selected) return fullApplicationBuildPlan('unrecognized-application-path');
for (const target of selected) targets.add(target);
}
return {
version: 1, mode: 'targets', reason: 'registered-application-paths', buildAll: false,
configureFlags: [...applicationConfigureFlags], checkTargets: [...targets].sort(),
};
}
78 changes: 78 additions & 0 deletions .github/scripts/build-parallelism.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { appendFileSync } from 'node:fs';
import { availableParallelism, totalmem } from 'node:os';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';

const gibibyte = 1024 ** 3;
const maxBuildJobs = 4;

function positiveInteger(value) {
return Number.isSafeInteger(value) && value > 0 ? value : null;
}

function detected(read) {
try {
return positiveInteger(read());
} catch {
return null;
}
}

export function detectBuildResources({
readCpuCount = availableParallelism,
readTotalMemory = totalmem,
readConstrainedMemory = () => process.constrainedMemory(),
} = {}) {
const memories = [detected(readTotalMemory), detected(readConstrainedMemory)].filter(value => value !== null);
return {
cpuCount: detected(readCpuCount),
// constrainedMemory() is zero when there is no known process/cgroup limit.
memoryBytes: memories.length ? Math.min(...memories) : null,
};
}

function applyOverride(value, maximum, fallback, limit) {
if (value === undefined || value === '') return maximum;
return typeof value === 'string' && new RegExp(`^[1-${limit}]$`).test(value)
? Math.min(maximum, Number(value)) : Math.min(maximum, fallback);
}

export function buildParallelismPlan({ cpuCount, memoryBytes, buildJobsOverride, linkJobsOverride } = {}) {
const cpu = positiveInteger(cpuCount);
const memory = positiveInteger(memoryBytes);
// Initial CI budget: reserve 1 GiB and allow 3 GiB per compiler process.
// Keep the existing two-job ceiling when either resource is unknown. This
// leaves 7-8 GiB runners at two jobs and never exceeds four on larger hosts.
// These are initial scheduling estimates, not peak-memory guarantees.
const memoryJobs = memory === null ? 2 : Math.max(1, Math.floor((memory - gibibyte) / (3 * gibibyte)));
const resourceJobs = Math.min(maxBuildJobs, cpu ?? 2, memoryJobs);
const buildJobs = applyOverride(buildJobsOverride, resourceJobs, 2, maxBuildJobs);
// Two six-GiB links can overlap with two three-GiB compilers at four jobs:
// reserve 1 + 2*6 + 2*3 = 19 GiB before admitting both links. Smaller hosts
// retain a single link alongside the existing compile-job heuristic.
const twoLinkMemory = (1 + 2 * 6 + (maxBuildJobs - 2) * 3) * gibibyte;
const resourceLinks = buildJobs === maxBuildJobs && memory !== null && memory >= twoLinkMemory ? 2 : 1;
const linkJobs = applyOverride(linkJobsOverride, resourceLinks, 1, 2);
return { cpuCount: cpu, memoryBytes: memory, buildJobs, linkJobs,
jobPools: `ark_compile=${buildJobs};ark_link=${linkJobs}` };
}

export function exportBuildParallelism({ env, resources = detectBuildResources() }) {
if (!env.GITHUB_ENV || !env.GITHUB_OUTPUT) throw new Error('GitHub file-command channels are required');
const plan = buildParallelismPlan({ ...resources,
buildJobsOverride: env.ARK_BUILD_JOBS_OVERRIDE, linkJobsOverride: env.ARK_LINK_JOBS_OVERRIDE });
appendFileSync(env.GITHUB_ENV, `ARK_BUILD_JOBS=${plan.buildJobs}\nARK_LINK_JOBS=${plan.linkJobs}\nARK_CMAKE_JOB_POOLS=${plan.jobPools}\n`);
appendFileSync(env.GITHUB_OUTPUT, `build_jobs=${plan.buildJobs}\nlink_jobs=${plan.linkJobs}\njob_pools=${plan.jobPools}\ncpu_count=${plan.cpuCount ?? 'unknown'}\nmemory_bytes=${plan.memoryBytes ?? 'unknown'}\n`);
return plan;
}

if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
try {
const plan = exportBuildParallelism({ env: process.env });
const memory = plan.memoryBytes === null ? 'unknown' : `${(plan.memoryBytes / gibibyte).toFixed(2)} GiB`;
console.log(`Build resources: CPUs=${plan.cpuCount ?? 'unknown'}, memory=${memory}, compile jobs=${plan.buildJobs}, link jobs=${plan.linkJobs}`);
} catch (error) {
console.error(`Unable to export build parallelism: ${error.message}`);
process.exitCode = 1;
}
}
84 changes: 60 additions & 24 deletions .github/scripts/cache-stats.mjs
Original file line number Diff line number Diff line change
@@ -1,29 +1,65 @@
import { spawnSync } from 'node:child_process';
import { appendFileSync, readdirSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';

let archives = 'unavailable';
if (process.env.VCPKG_ARCHIVE_DIRECTORY) {
try {
archives = readdirSync(process.env.VCPKG_ARCHIVE_DIRECTORY, { recursive: true })
.filter(file => file.endsWith('.zip')).length;
} catch {
// Cache diagnostics do not change the build or test result.
export function reportCacheStats({ env = process.env, run = spawnSync, append = appendFileSync, log = console.log } = {}) {
let archives = 'unavailable';
if (env.VCPKG_ARCHIVE_DIRECTORY) {
try {
archives = readdirSync(env.VCPKG_ARCHIVE_DIRECTORY, { recursive: true })
.filter(file => file.endsWith('.zip')).length;
} catch {
// Cache diagnostics do not change the build or test result.
}
}
const cacheSize = /^(?:[1-9][0-9]*)M$/.test(env.SCCACHE_CACHE_SIZE ?? '') ? env.SCCACHE_CACHE_SIZE : 'unavailable';
const scope = ['trusted', 'untrusted'].includes(env.ARK_COMPILER_CACHE_ARCHIVE_SCOPE) ? env.ARK_COMPILER_CACHE_ARCHIVE_SCOPE : 'unavailable';
const epoch = /^[0-9]{4}-W[0-9]{2}$/.test(env.ARK_COMPILER_CACHE_EPOCH ?? '') ? env.ARK_COMPILER_CACHE_EPOCH : 'unavailable';
const lines = [
`Dependency archive cache exact hit: ${env.VCPKG_ARCHIVE_HIT || 'false/unavailable'}`,
`Dependency archives after configure/build: ${archives}`,
'vcpkg package restore/build counts are in the Configure step output.',
`Compiler archive cache exact hit: ${env.COMPILER_ARCHIVE_HIT || 'false/unavailable'} (a prefix restore can still provide compiler hits)`,
`Compiler local cache budget: ${cacheSize}`,
`Compiler archive publication allowed: ${env.ARK_COMPILER_CACHE_SAVE_ENABLED === 'true'} (scope=${scope}, epoch=${epoch})`,
];
if (['controlled-cold', 'controlled-warm', 'seed-or-restored-snapshot'].includes(env.ARK_CACHE_SAMPLE)) {
const source = /^[a-f0-9]{40}$/.test(env.GITHUB_SHA ?? '') ? env.GITHUB_SHA : 'unavailable';
const key = /^ark-sccache-[a-zA-Z0-9.-]{1,240}$/.test(env.ARK_CACHE_ARCHIVE_KEY ?? '')
? env.ARK_CACHE_ARCHIVE_KEY : 'unavailable';
lines.unshift(`Cache measurement sample: ${env.ARK_CACHE_SAMPLE}`, `Source revision: ${source}`,
`Requested archive key: ${key}`,
`Project build executed: ${['true', 'false'].includes(env.ARK_CACHE_BUILD_EXECUTED)
? env.ARK_CACHE_BUILD_EXECUTED : 'unavailable'}`);
}
if (env.ARK_COMPILER_LAUNCHER) {
let statistics = 'sccache statistics unavailable';
try {
const result = run(env.ARK_COMPILER_LAUNCHER, ['--show-stats'], {
env, encoding: 'utf8', timeout: 30_000,
});
if (result?.status === 0 && typeof result.stdout === 'string' && result.stdout.trim()) {
const output = result.stdout.trim();
statistics = output.slice(0, 16 * 1024) + (output.length > 16 * 1024 ? '\n[sccache output truncated]' : '');
}
} catch {
// An unavailable optional tool cannot turn successful native checks red.
}
lines.push(statistics);
} else {
lines.push('Compiler cache disabled; compilation uses the compiler directly.');
}
const report = lines.join('\n');
log(report);
if (env.GITHUB_STEP_SUMMARY) {
try {
append(env.GITHUB_STEP_SUMMARY, `\n### Native build cache\n\n\`\`\`text\n${report}\n\`\`\`\n`);
} catch {
// The console still contains the report when a summary is unavailable.
}
}
return report;
}
const lines = [
`Dependency archive cache exact hit: ${process.env.VCPKG_ARCHIVE_HIT || 'false/unavailable'}`,
`Dependency archives after configure/build: ${archives}`,
'vcpkg package restore/build counts are in the Configure step output.',
`Compiler archive cache exact hit: ${process.env.COMPILER_ARCHIVE_HIT || 'false/unavailable'} (a prefix restore can still provide compiler hits)`,
];
if (process.env.ARK_COMPILER_LAUNCHER) {
const result = spawnSync(process.env.ARK_COMPILER_LAUNCHER, ['--show-stats'], {
encoding: 'utf8', timeout: 30_000,
});
lines.push(result.status === 0 ? result.stdout.trim() : 'sccache statistics unavailable');
} else {
lines.push('Compiler cache disabled; compilation uses the compiler directly.');
}
const report = lines.join('\n');
console.log(report);
if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, `\n### Native build cache\n\n\`\`\`text\n${report}\n\`\`\`\n`);

if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) reportCacheStats();
Loading
Loading