Skip to content

fix(utils): advance the CPU tick baseline in systemInfoV2 getCurrentCpuTicks#3824

Open
anxkhn wants to merge 1 commit into
apify:masterfrom
anxkhn:fix/system-info-v2-cpu-ticks-baseline
Open

fix(utils): advance the CPU tick baseline in systemInfoV2 getCurrentCpuTicks#3824
anxkhn wants to merge 1 commit into
apify:masterfrom
anxkhn:fix/system-info-v2-cpu-ticks-baseline

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

What

getCurrentCpuTicks() in packages/utils/src/internals/systemInfoV2/cpu-info.ts
computes CPU load as the delta between the current os.cpus() tick counts and a
module-level previousTicks baseline, but it never wrote the new sample back to
previousTicks. The baseline therefore stayed at { idle: 0, total: 0 } for the
whole process, so every call returned the average CPU load since boot instead
of the load since the previous sample.

const previousTicks = { idle: 0, total: 0 };

export function getCurrentCpuTicks() {
    const cpusCores = os.cpus();
    const ticks = cpusCores.reduce(/* sum idle + total across cores */);
    const idleTicksDelta = ticks.idle - previousTicks.idle;
    const totalTicksDelta = ticks.total - previousTicks.total;
    // <- previousTicks was never updated here, so the delta is always measured from boot
    return totalTicksDelta ? 1 - idleTicksDelta / totalTicksDelta : 0;
}

Why it matters

systemInfoV2 is enabled by default, and getCurrentCpuTicks() is the CPU
fallback used in uncontainerized environments, cgroup-less containers and AWS
Lambda (the other branches use cgroup stats). Its result feeds
cpuCurrentUsage / isCpuOverloaded, which drive the AutoscaledPool. With a
frozen boot-average signal the CPU value barely moves for the rest of a run, so
CPU-based autoscaling never reacts to real load.

The intended contract is already established twice in the codebase:

  • the V1 path LocalEventManager.createCpuInfo
    (packages/core/src/events/local_event_manager.ts) does the identical delta
    math and then Object.assign(this.previousTicks, ticks);
  • the containerized branch in this same file advances its baseline with
    previousSample = sample.

getCurrentCpuTicks() was simply missing the equivalent write-back.

The fix

Write the sample back after computing the delta, mirroring both siblings:

const idleTicksDelta = ticks.idle - previousTicks.idle;
const totalTicksDelta = ticks.total - previousTicks.total;
Object.assign(previousTicks, ticks);
return totalTicksDelta ? 1 - idleTicksDelta / totalTicksDelta : 0;

The now-redundant non-null assertions (previousTicks!) on the non-null const are
dropped.

Test

Adds a regression test to test/utils/cpu-infoV2.test.ts that takes two samples
and asserts the second reflects the inter-sample delta (0.75) rather than the
cumulative-since-boot ratio (~0.583, which the buggy version returned). A
beforeEach resets the shared module baseline so the singleton state cannot leak
between tests.

Red -> green: against the unpatched source the new test fails
(expected 0.5833333333333333 to be close to 0.75); with the fix the file passes
18/18. test/core/autoscaling/ (snapshotter + system status + autoscaled pool)
also stays green at 39/39.

…puTicks

getCurrentCpuTicks() computes the CPU load as the delta between the current
os.cpus() ticks and a module-level `previousTicks` baseline, but it never wrote
the new sample back to `previousTicks`. The baseline therefore stayed at
{ idle: 0, total: 0 } for the whole process, so every call returned the average
CPU load since boot instead of the load since the previous sample.

Because systemInfoV2 is enabled by default and getCurrentCpuTicks() is the
fallback used in uncontainerized environments, cgroup-less containers and AWS
Lambda, its result feeds cpuCurrentUsage / isCpuOverloaded and drives the
AutoscaledPool. With a frozen boot-average signal the CPU overload flag barely
moves during a run, which defeats CPU-based autoscaling.

Write the sample back after computing the delta (Object.assign(previousTicks,
ticks)), mirroring the V1 path in local_event_manager.createCpuInfo and the
containerized branch in this same file. The now-redundant non-null assertions on
the `previousTicks` const are dropped.

Adds a regression test that takes two samples and asserts the second reflects
the inter-sample delta rather than the cumulative-since-boot ratio, and resets
the shared baseline before each test so the module state no longer leaks between
tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants