diff --git a/scripts/agent.mjs b/scripts/agent.mjs index ca4e7bda731c..10cf71d866f9 100755 --- a/scripts/agent.mjs +++ b/scripts/agent.mjs @@ -13,6 +13,7 @@ import { getAwsSecret, getAzureSecret, getCloud, + getCloudLaunchedInstanceType, getCloudMetadataTag, getDistro, getDistroVersion, @@ -383,6 +384,12 @@ async function doBuildkiteAgent(action, cliOptions = {}) { tags[tag] = value; } } + // Deliberately not `instance-type`: that key is the type the job asked + // for (.buildkite/ci.mjs), and under capacity pressure this machine may + // be a different one. The runner reads this back from + // BUILDKITE_AGENT_META_DATA_LAUNCHED_INSTANCE_TYPE for the job log header + // and failure annotations. + tags["launched-instance-type"] = await getCloudLaunchedInstanceType(cloud); } options["tags"] = Object.entries(tags) diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index 9ef779b2ad96..2fc852e14f1e 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -43,6 +43,7 @@ import { getBuildLabel, getBuildMetadata, getBuildUrl, + getCloudInstanceType, getCommit, getDistro, getDistroVersion, @@ -1131,7 +1132,7 @@ async function runTests() { context: "flaky", label: title, style: "warning", - content: `
${title} - ${reason} (in the parallel batch on ${getBuildLabel()}; passed alone)${detail}
`, + content: `
${title} - ${reason} (in the parallel batch on ${getTestLabel()}; passed alone)${detail}
`, }); } } @@ -2789,10 +2790,19 @@ function addPath(...paths) { } /** + * The lane a failure is reported against, plus the machine type the job + * actually ran on when that is known, e.g. ":debian: 13 x64-asan (r7i.2xlarge)". + * The type is what tells a timeout on fallback hardware apart from one on the + * hardware the lane asks for, so it goes in the annotation next to the lane. * @returns {string | undefined} */ function getTestLabel() { - return getBuildLabel()?.replace(" - test-bun", ""); + const label = getBuildLabel()?.replace(" - test-bun", ""); + if (!label) { + return; + } + const instanceType = getCloudInstanceType(); + return instanceType ? `${label} (${instanceType})` : label; } /** diff --git a/scripts/utils.mjs b/scripts/utils.mjs index a1a204c09e7b..1134170f2480 100755 --- a/scripts/utils.mjs +++ b/scripts/utils.mjs @@ -15,7 +15,15 @@ import { writeFileSync, } from "node:fs"; import { connect } from "node:net"; -import { hostname, homedir as nodeHomedir, tmpdir as nodeTmpdir, release, userInfo } from "node:os"; +import { + availableParallelism, + cpus, + hostname, + homedir as nodeHomedir, + tmpdir as nodeTmpdir, + release, + userInfo, +} from "node:os"; import { basename, dirname, join, relative, resolve } from "node:path"; import { normalize as normalizeWindows } from "node:path/win32"; @@ -1870,6 +1878,79 @@ export function getPublicIp() { } } +/** + * The CPU model and the number of CPUs this process may run on, e.g. + * "Intel(R) Xeon(R) Platinum 8488C x8" or "Apple M2 Ultra x24". + * @returns {string | undefined} + */ +export function getCpuDescription() { + const [cpu] = cpus(); + const model = cpu?.model?.replace(/\s+/g, " ").trim(); + if (!model) { + return; + } + return `${model} x${availableParallelism()}`; +} + +/** @type {string | undefined} "" once looked up and not available */ +let cloudInstanceType; + +/** + * The instance type the cloud provider actually launched for this machine, + * e.g. "r7i.2xlarge" on EC2 or "Standard_D8s_v5" on Azure. CI asks for one + * type per lane (.buildkite/ci.mjs), but under capacity pressure the + * machine may be a different, slower type. scripts/agent.mjs records the + * launched type as the agent's `launched-instance-type` tag, which Buildkite + * exposes to the job as an environment variable. Looked up once per process. + * @returns {string | undefined} + */ +export function getCloudInstanceType() { + if (typeof cloudInstanceType !== "string") { + cloudInstanceType = + getEnv("BUILDKITE_AGENT_META_DATA_LAUNCHED_INSTANCE_TYPE", false) || fetchCloudInstanceType() || ""; + } + return cloudInstanceType || undefined; +} + +/** + * Images baked before agent.mjs set the tag only have the `cloud` tag, so + * ask the metadata service directly. Synchronous and bounded, since this + * runs while printing a log header. + * @returns {string | undefined} + */ +function fetchCloudInstanceType() { + // Without a `cloud` tag (the darwin fleet, GitHub Actions) this is not a + // machine whose type we chose, so there is nothing to compare against. + const cloud = getEnv("BUILDKITE_AGENT_META_DATA_CLOUD", false); + let request; + if (cloud === "aws") { + request = ["http://169.254.169.254/latest/meta-data/instance-type"]; + } else if (cloud === "azure") { + request = [ + "-H", + "Metadata: true", + "http://169.254.169.254/metadata/instance/compute/vmSize?api-version=2021-02-01&format=text", + ]; + } else { + return; + } + + const { error, stdout } = spawnSync([ + "curl", + "-sf", + "--noproxy", + "*", + "--connect-timeout", + "1", + "--max-time", + "3", + ...request, + ]); + if (!error) { + return stdout.trim() || undefined; + } +} + /** * @returns {string} */ @@ -2266,6 +2347,34 @@ export async function getCloudMetadataTag(tag, cloud) { return getCloudMetadata(metadata, cloud); } +/** + * The instance type this machine was launched as, e.g. "r7i.2xlarge" or + * "Standard_D8s_v5". agent.mjs tags the agent with it at start; jobs read it + * back through getCloudInstanceType(). + * @param {Cloud} [cloud] + * @returns {Promise} + */ +export async function getCloudLaunchedInstanceType(cloud) { + cloud ??= await getCloud(); + + if (cloud === "azure") { + const body = await getCloudMetadata("", cloud); + if (!body) return; + try { + return JSON.parse(body)?.compute?.vmSize || undefined; + } catch {} + return; + } + + const metadata = { + "aws": "instance-type", + // "projects//machineTypes/" + "google": "machine-type", + }; + + return (await getCloudMetadata(metadata, cloud))?.split("/").pop() || undefined; +} + /** * @typedef {Object} AwsCredentials * @property {string} AccessKeyId @@ -3095,8 +3204,10 @@ export function printEnvironment() { } console.log("Distro:", getDistro()); console.log("Distro Version:", getDistroVersion()); + console.log("CPU:", getCpuDescription()); console.log("Hostname:", getHostname()); if (isCI) { + console.log("Instance Type:", getCloudInstanceType()); console.log("Tailscale IP:", getTailscaleIp()); console.log("Public IP:", getPublicIp()); }