Skip to content

Add sanity checks for profiler - #483

Draft
jbachorik wants to merge 8 commits into
mainfrom
muse/impl-20260415-211352
Draft

Add sanity checks for profiler#483
jbachorik wants to merge 8 commits into
mainfrom
muse/impl-20260415-211352

Conversation

@jbachorik

@jbachorik jbachorik commented Apr 16, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?:

Adds two pre-start sanity checks that gate profiler initialization (cpu, wallclock, allocation, live heap):

  1. Core Check: Verifies effective CPU count >= 1 (per [Libretto] Add sanity checks for profiler #480), using min(logical_cores, cgroup_cpu_quota_in_millicores / 1000). An unknown core count (OS query failure with no cgroup limit) never fails the check. Reads cgroup v2 cpu.max with v1 cpu.cfs_quota_us/period fallback, resolved from the process's own cgroup path (via /proc/self/cgroup) and walked up through nested ancestor groups — not just the hierarchy mount root — so a limit set on a parent slice (e.g. /user.slice/...) is still honored.
  2. Memory Check: Compares estimated JVM+profiler memory footprint (heap + metaspace + codecache + 30% GC overhead + threads*stack_size + 64MB profiler) against available memory (min(physical_RAM - 128MB, container_memory_limit)), using the same per-process, ancestor-walked cgroup resolution as the core check for the memory limit. JVM flags (MaxHeapSize, MaxMetaspaceSize, ReservedCodeCacheSize, ThreadStackSize) are read via VMFlag::find(), accepting Uintx/Size_t/Uint64_t/Intx flag types since ThreadStackSize is declared intx on standard HotSpot builds. This lookup is skipped entirely on OpenJ9/Zing, where the HotSpot VMStructs flag table doesn't exist, rather than fail (or silently pass) the check on numbers that don't reflect the actual JVM. An unbounded MaxMetaspaceSize (HotSpot's default) is normalized to a 256MB estimate whenever it exceeds available memory, since debug builds don't reliably expose the raw sentinel as bit-identical to SIZE_MAX.

When either check fails, the profiler logs a WARN to stderr and returns an Error (surfaces as IllegalStateException on the Java side) containing a structured [sanity] message with full system details (cpu=ok/fail, memory=ok/fail, all metrics) for telemetry ingestion by dd-trace-java.

A single nosanity argument disables both checks. Checks run on first initialization only; the result — and the fact that the first start was handled at all, even under nosanity — is cached across start/stop cycles, so a later start without nosanity doesn't unexpectedly re-run and fail the checks.

Resolves #480

Motivation:

JIRA: PROF-13027 — Prevent the profiler from running on systems with insufficient resources, which can cause OOM kills, excessive CPU contention, or degraded application performance.

Additional Notes:

  • New files: sanityCheck.h, sanityCheck.cpp — encapsulates both checks
  • Modified: os.h, os_linux.cpp, os_macos.cpp — cgroup CPU/memory limit APIs, including per-process cgroup path resolution and ancestor-walking for nested cgroups
  • Modified: arguments.h, arguments.cppnosanity flag parsing
  • Modified: profiler.cpp — gate in Profiler::start() with cached result
  • macOS stubs return -1 (unconstrained) for both cgroup methods
  • Thread count from OS::getBasicProcessInfo() with fallback to 200
  • Error message contains structured key=value telemetry: [sanity] cpu=fail,memory=ok,logical_cores=4,cgroup_millicores=500,...,containerized=true

How to test the change?:

  • Integration tests in SanityCheckTest.java: verify nosanity bypasses checks, nosanity=true works, and checks run only once across stop/start cycles
  • Manual: run profiler in a container with --cpus=0.5 or --memory=256m and observe WARN + IllegalStateException with structured telemetry
  • Manual: add nosanity to the profiler arguments to confirm override works

For Datadog employees:

  • If this PR touches code that signs or publishes builds or packages, or handles
    credentials of any kind, I've requested a security review (run the dd:platform-security-review
    skill, or file a request via the PSEC review form).
    bewaire also runs automatically on every PR.
  • This PR doesn't touch any of that.
  • JIRA: PROF-13027

🤖 Generated with Claude Code via muse implement

@jbachorik jbachorik added the AI label Apr 16, 2026
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jbachorik
jbachorik force-pushed the muse/impl-20260415-211352 branch from e7113c2 to d981e16 Compare April 16, 2026 09:58
@dd-octo-sts

dd-octo-sts Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

CI Test Results

Run: #30927324461 | Commit: f6b31cd | Duration: 23m 28s (longest job)

All 32 test jobs passed

Status Overview

JDK glibc-aarch64/debug glibc-amd64/debug musl-aarch64/debug musl-amd64/debug
8 - - -
8-ibm - - -
8-j9 - -
8-librca - -
8-orcl - - -
11 - - -
11-j9 - -
11-librca - -
17 - -
17-graal - -
17-j9 - -
17-librca - -
21 - -
21-graal - -
21-librca - -
25 - -
25-graal - -
25-librca - -

Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled

Summary: Total: 32 | Passed: 32 | Failed: 0


Updated: 2026-08-04 16:30:16 UTC

Resolve conflicts in arguments.{h,cpp} and profiler.cpp (both branches
added new Arguments fields/CLI cases independently). Fix sanity_checks_run_once
to actually exercise the cached-check path, and add the missing
vmStructs.inline.h include that sanityCheck.cpp needed for VMFlag::addr()
to link in release builds.
Copilot AI review requested due to automatic review settings August 4, 2026 10:55
@dd-octo-sts

dd-octo-sts Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Scan-Build Report

User:runner@runnervmvrwv9
Working Directory:/home/runner/work/java-profiler/java-profiler/ddprof-lib/src/test/make
Command Line:make -j4 all
Clang Version:Ubuntu clang version 18.1.3 (1ubuntu1)
Date:Tue Aug 4 16:06:09 2026

Bug Summary

Bug TypeQuantityDisplay?
All Bugs1
Logic error
Dereference of null pointer1

Reports

Bug Group Bug Type ▾ File Function/Method Line Path Length
Logic errorDereference of null pointerprofiler.hfindLibraryByAddress51713

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a native “sanity check” gate to profiler startup to prevent enabling profiling under clearly under-provisioned CPU/memory configurations (especially in containers), with an opt-out nosanity argument and basic Java integration tests.

Changes:

  • Introduces SanityChecker (new C++ module) to estimate effective CPU capacity and JVM+profiler memory footprint and return a structured [sanity] ... error on failure.
  • Adds Linux cgroup v1/v2 CPU quota and memory limit detection APIs to OS, with macOS returning “unconstrained” stubs.
  • Wires the sanity gate into Profiler::start() and adds nosanity parsing plus a new JUnit test class covering the override and start/stop behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
ddprof-test/src/test/java/com/datadoghq/profiler/sanity/SanityCheckTest.java Adds JUnit tests validating nosanity bypass and intended “run once” behavior across stop/start.
ddprof-lib/src/main/cpp/sanityCheck.h Declares the SanityChecker entrypoint.
ddprof-lib/src/main/cpp/sanityCheck.cpp Implements CPU/memory sanity checks and structured telemetry error formatting.
ddprof-lib/src/main/cpp/profiler.cpp Gates Profiler::start() on the sanity-check result with caching.
ddprof-lib/src/main/cpp/os.h Adds OS APIs for cgroup CPU and container memory limits.
ddprof-lib/src/main/cpp/os_macos.cpp Implements macOS stubs for the new OS APIs (returns unconstrained).
ddprof-lib/src/main/cpp/os_linux.cpp Implements Linux cgroup v1/v2 parsing for CPU quota and memory limits.
ddprof-lib/src/main/cpp/arguments.h Adds _skip_sanity_checks argument state.
ddprof-lib/src/main/cpp/arguments.cpp Parses the new nosanity argument (bare and boolean-like forms).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ddprof-lib/src/main/cpp/sanityCheck.cpp
Comment thread ddprof-lib/src/main/cpp/profiler.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/sanityCheck.cpp Outdated
@jbachorik

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8bc3963756

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ddprof-lib/src/main/cpp/os_linux.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/sanityCheck.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/sanityCheck.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/sanityCheck.cpp Outdated
Comment thread ddprof-lib/src/main/cpp/profiler.cpp Outdated
@dd-octo-sts

dd-octo-sts Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (commit 8bc3963)

Pipeline: https://gitlab.ddbuild.io/DataDog/apm-reliability/benchmarking-platform/-/pipelines/128810310 Commit: 8bc39637565a7f596484044f42a5e9d57a573017

⚠️ Significant outliers

  • 🔴 future-genetic (JDK 25): runtime +3.4% (2045→2114 ms)
Runtime details (per benchmark × JDK)
Benchmark JDK Latest Dev Δ (dev vs latest) Issues L/D
akka-uct 21 ✅ 10175 ms (21 iters) ✅ 10306 ms (21 iters) ≈ +1.3% (±11.3%) — / —
akka-uct 25 ✅ 8989 ms (24 iters) ✅ 8894 ms (24 iters) ≈ -1.1% (±10.5%) — / —
finagle-chirper 21 ✅ 6003 ms (33 iters) ✅ 5957 ms (33 iters) ≈ -0.8% (±25.1%) ⚠️ W:4 / ⚠️ W:3
finagle-chirper 25 ✅ 5525 ms (36 iters) ✅ 5457 ms (36 iters) ≈ -1.2% (±25%) ⚠️ W:3 / ⚠️ W:3
fj-kmeans 21 ✅ 2691 ms (70 iters) ✅ 2631 ms (72 iters) ≈ -2.2% (±2.5%) — / —
fj-kmeans 25 ✅ 2799 ms (66 iters) ✅ 2806 ms (66 iters) ≈ +0.3% (±2.7%) — / —
future-genetic 21 ✅ 2116 ms (88 iters) ✅ 2090 ms (89 iters) ≈ -1.2% (±2.7%) — / —
future-genetic 25 ✅ 2045 ms (91 iters) ✅ 2114 ms (87 iters) 🔴 +3.4% — / —
naive-bayes 21 ✅ 1277 ms (134 iters) ✅ 1230 ms (138 iters) ≈ -3.7% (±32.1%) — / —
naive-bayes 25 ✅ 1019 ms (168 iters) ✅ 993 ms (172 iters) ≈ -2.6% (±31%) — / —
reactors 21 ✅ 15909 ms (15 iters) ✅ 16112 ms (15 iters) ≈ +1.3% (±8.2%) — / —
reactors 25 ✅ 18714 ms (15 iters) ✅ 18348 ms (15 iters) ≈ -2% (±3.8%) — / —
Internal counter details (ddprof)

ddprof internal counters, latest / dev (✅ = 0, · = unavailable):

Benchmark JDK Dropped rec Dropped jvmti Dropped trace Skipped WC AGCT fail Unwind fail
akka-uct 21 ✅ / ✅ ✅ / ✅ 1 / 2 2061 / 2000 ✅ / ✅ ✅ / ✅
akka-uct 25 ✅ / ✅ ✅ / ✅ 1 / 4 2340 / 2123 ✅ / ✅ ✅ / ✅
finagle-chirper 21 ✅ / ✅ ✅ / ✅ 5 / 2 8464 / 8352 ✅ / ✅ ✅ / ✅
finagle-chirper 25 ✅ / ✅ ✅ / ✅ ✅ / 4 8655 / 8151 ✅ / ✅ ✅ / ✅
fj-kmeans 21 ✅ / ✅ ✅ / ✅ 7 / ✅ 1267 / 1291 ✅ / ✅ ✅ / ✅
fj-kmeans 25 ✅ / ✅ ✅ / ✅ 1 / 2 1281 / 1251 ✅ / ✅ ✅ / ✅
future-genetic 21 ✅ / ✅ ✅ / ✅ 2 / 4 2955 / 2995 ✅ / ✅ ✅ / ✅
future-genetic 25 ✅ / ✅ ✅ / ✅ ✅ / ✅ 2871 / 2908 ✅ / ✅ ✅ / ✅
naive-bayes 21 ✅ / ✅ ✅ / ✅ 4 / 3 3493 / 3518 ✅ / ✅ ✅ / ✅
naive-bayes 25 ✅ / ✅ ✅ / ✅ 3 / 3 3475 / 3461 ✅ / ✅ ✅ / ✅
reactors 21 ✅ / ✅ ✅ / ✅ 1 / 1 1539 / 1572 ✅ / ✅ ✅ / ✅
reactors 25 ✅ / ✅ ✅ / ✅ ✅ / 2 1929 / 1756 ✅ / ✅ ✅ / ✅

Fix CPU-count and memory sanity-check edge cases: treat OS::getCpuCount()
failure as unknown rather than a hard fail, align the core threshold with
issue #480 (2 cores), skip HotSpot-only memory estimates on OpenJ9/Zing,
normalize the unbounded MaxMetaspaceSize sentinel, accept intx flags for
ThreadStackSize, clamp overflow in the memory total, resolve cgroup limits
from the process's actual (possibly nested) cgroup, and mark the first
profiler start as sanity-checked even when nosanity skips execution.
Copilot AI review requested due to automatic review settings August 4, 2026 13:46
@datadog-prod-us1-6

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

ddprof-lib/src/main/cpp/sanityCheck.cpp:64

  • The available-memory upper bound is derived from OS::getRamSize() - OS_RESERVE. If OS::getRamSize() returns 0 (e.g. macOS currently stubs it to 0, or /proc/meminfo is unavailable), upper becomes 0 and the memory sanity check can never fail—even if getContainerMemoryLimit() provides a strict container limit. This undermines the purpose of the check on such hosts.
    u64 ram = OS::getRamSize();
    u64 upper = (ram > OS_RESERVE) ? (ram - OS_RESERVE) : 0;
    if (container_limit > 0 && (u64)container_limit < upper) {
        upper = (u64)container_limit;
    }

ddprof-lib/src/main/cpp/sanityCheck.cpp:110

  • The PR description says the core check verifies effective CPU count >= 1, but the implementation enforces >= 2 cores (effective_cores < 2). Please reconcile the intended threshold (update the check or update the PR description/spec) so behavior matches the documented requirement.
    // Per DataDog/java-profiler#480, the profiler refuses to run with fewer
    // than 2 cores. An unknown core count (-1) never fails this check — see
    // the effective_cores computation above.
    bool cpu_fail = (effective_cores >= 0 && effective_cores < 2);
    bool mem_fail = (hotspot && upper > 0 && lower > upper);

Comment thread ddprof-lib/src/main/cpp/profiler.cpp Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 4, 2026 13:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (3)

ddprof-lib/src/main/cpp/os_macos.cpp:377

  • OS::getCpuCount() returns 1 when sysctlbyname("hw.logicalcpu", ...) fails. With the new sanity check requiring >= 2 cores, this turns an OS query failure into a deterministic startup failure on macOS. Returning -1 on failure aligns with the new “unknown core count must not fail” logic in sanityCheck.cpp.
int OS::getCpuCount() {
    int cpu_count;
    size_t size = sizeof(cpu_count);
    return sysctlbyname("hw.logicalcpu", &cpu_count, &size, NULL, 0) == 0 ? cpu_count : 1;
}

ddprof-lib/src/main/cpp/sanityCheck.cpp:89

  • HotSpot’s ThreadStackSize flag commonly has value 0 to mean “use platform default”. In that case, the current calculation makes stack_size become 0 and underestimates the JVM footprint (threads * stack_size), potentially letting the profiler start when it should fail the memory sanity check.
    size_t heap_max      = hotspot ? getVMSizeFlag("MaxHeapSize",           0) : 0;
    size_t metaspace_max = hotspot ? getVMSizeFlag("MaxMetaspaceSize",      DEFAULT_METASPACE) : 0;
    size_t codecache     = hotspot ? getVMSizeFlag("ReservedCodeCacheSize", DEFAULT_CODECACHE) : 0;
    size_t stack_size    = hotspot ? getVMSizeFlag("ThreadStackSize",       DEFAULT_STACK_SIZE / 1024) * 1024 : 0;

ddprof-lib/src/main/cpp/sanityCheck.cpp:110

  • The PR description says the core check verifies effective CPU count >= 1, but the implementation fails when effective_cores < 2. Please align the PR description (and any downstream expectations in dd-trace-java telemetry parsing) with the implemented 2-core threshold, or adjust the threshold if >=1 is actually intended.
    // Per DataDog/java-profiler#480, the profiler refuses to run with fewer
    // than 2 cores. An unknown core count (-1) never fails this check — see
    // the effective_cores computation above.
    bool cpu_fail = (effective_cores >= 0 && effective_cores < 2);
    bool mem_fail = (hotspot && upper > 0 && lower > upper);

Copilot AI review requested due to automatic review settings August 4, 2026 14:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (3)

ddprof-lib/src/main/cpp/sanityCheck.cpp:103

  • The memory estimate can overflow before it reaches addClamped(): (u64)thread_count * (u64)stack_size is evaluated with normal unsigned arithmetic and may wrap, causing lower to be under-estimated and the sanity check to incorrectly pass on extreme ThreadStackSize / thread-count combinations. Compute the product with an explicit overflow check and clamp the product before adding.
    lower = addClamped(lower, (u64)metaspace_max);
    lower = addClamped(lower, (u64)codecache);
    lower = addClamped(lower, gc_overhead);
    lower = addClamped(lower, (u64)thread_count * (u64)stack_size);
    lower = addClamped(lower, PROFILER_OVERHEAD);

ddprof-lib/src/main/cpp/os_linux.cpp:891

  • The cgroup v1 fallback assumes fixed mount points (/sys/fs/cgroup/cpu here and in walkCgroupV1CpuMillicores). On many distros the CPU controller is mounted as /sys/fs/cgroup/cpu,cpuacct (or similar), in which case this code will fail to find quota files and incorrectly report "unconstrained" (-1), weakening the sanity gate in exactly the container environments it targets. Consider resolving the actual mount point from /proc/self/mountinfo (preferred), or at least trying common alternate mount paths and ensuring the ancestor-walk stop condition (base_len) matches the chosen mount prefix.
    if (getOwnCgroupPath("cpu", subpath, sizeof(subpath))) {
        const char* base = "/sys/fs/cgroup/cpu";
        size_t base_len = strlen(base);
        size_t sub_len = strlen(subpath);
        if (base_len + sub_len < sizeof(path)) {

ddprof-lib/src/main/cpp/sanityCheck.cpp:110

  • PR description says the core sanity check enforces effective CPU count >= 1, but the implementation fails when effective_cores < 2 (matching issue #480). Please align the PR description (and any external docs/telemetry expectations) with the implemented threshold, or adjust the threshold here if >=1 is actually intended.
    // Per DataDog/java-profiler#480, the profiler refuses to run with fewer
    // than 2 cores. An unknown core count (-1) never fails this check — see
    // the effective_cores computation above.
    bool cpu_fail = (effective_cores >= 0 && effective_cores < 2);
    bool mem_fail = (hotspot && upper > 0 && lower > upper);

Copilot AI review requested due to automatic review settings August 4, 2026 14:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (3)

ddprof-lib/src/main/cpp/sanityCheck.cpp:55

  • OS::getCgroupCpuMillicores() can legitimately return 0 for extremely small CPU quotas (since the quota/period math truncates). The current cgroup_mc > 0 checks treat 0 as “unconstrained/unavailable”, which can incorrectly ignore a real (near-zero) container CPU limit and mark containerized=false in telemetry.
    int logical_cpus = OS::getCpuCount();
    int cgroup_mc = OS::getCgroupCpuMillicores();
    long container_limit = OS::getContainerMemoryLimit();
    bool containerized = (cgroup_mc > 0 || container_limit > 0);

ddprof-lib/src/main/cpp/sanityCheck.cpp:110

  • The PR description says the core sanity check enforces effective CPU count >= 1, but the implementation enforces the Issue #480 requirement of “fewer than 2 cores” via effective_cores < 2. Please align the PR description (or the implementation) so downstream consumers/telemetry expectations match the actual behavior.
    // --- Run checks ---
    // Per DataDog/java-profiler#480, the profiler refuses to run with fewer
    // than 2 cores. An unknown core count (-1) never fails this check — see
    // the effective_cores computation above.
    bool cpu_fail = (effective_cores >= 0 && effective_cores < 2);
    bool mem_fail = (hotspot && upper > 0 && lower > upper);

ddprof-lib/src/main/cpp/os_linux.cpp:904

  • The cgroup v1 fallback assumes the CPU controller is mounted at /sys/fs/cgroup/cpu. On many cgroup v1 systems it is mounted as /sys/fs/cgroup/cpu,cpuacct, so the leaf open() fails and CPU quotas are silently treated as “unavailable”, reducing the effectiveness of the sanity gate and producing misleading containerized=false telemetry.
    // Fall back to cgroup v1, likewise resolved from the process's own path.
    if (getOwnCgroupPath("cpu", subpath, sizeof(subpath))) {
        const char* base = "/sys/fs/cgroup/cpu";
        size_t base_len = strlen(base);
        size_t sub_len = strlen(subpath);

@dd-octo-sts

dd-octo-sts Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (commit 1853124)

Pipeline: https://gitlab.ddbuild.io/DataDog/apm-reliability/benchmarking-platform/-/pipelines/128867390 Commit: 1853124ecf73ab3681da65ed7b85b2d7119bce72

✅ Within expected boundaries

No significant runtime deltas (all within run-to-run noise) and no internal-counter outliers.

Runtime details (per benchmark × JDK)
Benchmark JDK Latest Dev Δ (dev vs latest) Issues L/D
akka-uct 21 ✅ 10302 ms (21 iters) ✅ 10209 ms (21 iters) ≈ -0.9% (±11.1%) — / —
akka-uct 25 ✅ 8828 ms (24 iters) ✅ 8765 ms (24 iters) ≈ -0.7% (±10%) — / —
finagle-chirper 21 ✅ 6005 ms (33 iters) ✅ 5923 ms (33 iters) ≈ -1.4% (±25%) ⚠️ W:3 / ⚠️ W:4
finagle-chirper 25 ✅ 5464 ms (36 iters) ✅ 5443 ms (36 iters) ≈ -0.4% (±24.2%) ⚠️ W:3 / ⚠️ W:3
fj-kmeans 21 ✅ 2635 ms (72 iters) ✅ 2643 ms (72 iters) ≈ +0.3% (±2.5%) — / —
fj-kmeans 25 ✅ 2813 ms (66 iters) ✅ 2820 ms (66 iters) ≈ +0.2% (±2.6%) — / —
future-genetic 21 ✅ 2100 ms (88 iters) ✅ 2075 ms (89 iters) ≈ -1.2% (±2.6%) — / —
future-genetic 25 ✅ 2124 ms (87 iters) ✅ 2072 ms (90 iters) ≈ -2.4% (±2.6%) — / —
naive-bayes 21 ✅ 1307 ms (132 iters) ✅ 1232 ms (138 iters) ≈ -5.7% (±31.6%) — / —
naive-bayes 25 ✅ 996 ms (171 iters) ✅ 1024 ms (167 iters) ≈ +2.8% (±31.9%) — / —
reactors 21 ✅ 16487 ms (15 iters) ✅ 17342 ms (15 iters) ≈ +5.2% (±8.7%) — / —
reactors 25 ✅ 18590 ms (15 iters) ✅ 18293 ms (15 iters) ≈ -1.6% (±3.5%) — / —
Internal counter details (ddprof)

ddprof internal counters, latest / dev (✅ = 0, · = unavailable):

Benchmark JDK Dropped rec Dropped jvmti Dropped trace Skipped WC AGCT fail Unwind fail
akka-uct 21 ✅ / ✅ ✅ / ✅ 2 / 4 2009 / 2010 ✅ / ✅ ✅ / ✅
akka-uct 25 ✅ / ✅ ✅ / ✅ 1 / 4 2344 / 2313 ✅ / ✅ ✅ / ✅
finagle-chirper 21 ✅ / ✅ ✅ / ✅ 3 / 3 8688 / 8348 ✅ / ✅ ✅ / ✅
finagle-chirper 25 ✅ / ✅ ✅ / ✅ 1 / 1 8113 / 8707 ✅ / ✅ ✅ / ✅
fj-kmeans 21 ✅ / ✅ ✅ / ✅ ✅ / ✅ ✅ / ✅ ✅ / ✅ ✅ / ✅
fj-kmeans 25 ✅ / ✅ ✅ / ✅ ✅ / 1 1254 / 1300 ✅ / ✅ ✅ / ✅
future-genetic 21 ✅ / ✅ ✅ / ✅ 1 / 1 2918 / 3070 ✅ / ✅ ✅ / ✅
future-genetic 25 ✅ / ✅ ✅ / ✅ 6 / ✅ 2872 / 2888 ✅ / ✅ ✅ / ✅
naive-bayes 21 ✅ / ✅ ✅ / ✅ 5 / 3 3490 / 3511 ✅ / ✅ ✅ / ✅
naive-bayes 25 ✅ / ✅ ✅ / ✅ 2 / 6 3425 / 3480 ✅ / ✅ ✅ / ✅
reactors 21 ✅ / ✅ ✅ / ✅ ✅ / ✅ 1545 / 1700 ✅ / ✅ ✅ / ✅
reactors 25 ✅ / ✅ ✅ / ✅ ✅ / ✅ 1860 / 1829 ✅ / ✅ ✅ / ✅

…E_MAX match

Debug HotSpot builds align the unbounded sentinel down during ergonomics,
so it no longer bit-matches SIZE_MAX but still dwarfs available memory,
causing the sanity check to fail on debug builds.
Copilot AI review requested due to automatic review settings August 4, 2026 15:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (4)

ddprof-lib/src/main/cpp/os_linux.cpp:1004

  • The cgroup v1 fallback assumes the memory controller is mounted at /sys/fs/cgroup/memory. This is not guaranteed across distros/container runtimes, so the code can incorrectly treat a constrained container as unconstrained and skip the memory gating. Prefer discovering the correct mountpoint via /proc/self/mountinfo (or attempting common v1 mount directory variants) rather than hard-coding the path.
    // Fall back to cgroup v1, likewise resolved from the process's own path.
    if (getOwnCgroupPath("memory", subpath, sizeof(subpath))) {
        const char* base = "/sys/fs/cgroup/memory";
        size_t base_len = strlen(base);
        size_t sub_len = strlen(subpath);
        if (base_len + sub_len < sizeof(path)) {
            memcpy(path, base, base_len);
            memcpy(path + base_len, subpath, sub_len + 1);

            char leaf[PATH_MAX];
            if ((size_t)snprintf(leaf, sizeof(leaf), "%s/memory.limit_in_bytes", path) < sizeof(leaf)) {
                int fd = open(leaf, O_RDONLY);
                if (fd != -1) {
                    close(fd);
                    return walkCgroupV1MemoryLimit(path);
                }

ddprof-lib/src/main/cpp/sanityCheck.cpp:112

  • PR description says the core sanity check gates on effective CPU count >= 1, but the implementation (and Issue #480) gate on < 2 cores. Please align the documented behavior and the implemented threshold (including telemetry expectations) so users and downstream ingestion don’t see inconsistent semantics.
    // --- Run checks ---
    // Per DataDog/java-profiler#480, the profiler refuses to run with fewer
    // than 2 cores. An unknown core count (-1) never fails this check — see
    // the effective_cores computation above.
    bool cpu_fail = (effective_cores >= 0 && effective_cores < 2);
    bool mem_fail = (hotspot && upper > 0 && lower > upper);

ddprof-test/src/test/java/com/datadoghq/profiler/sanity/SanityCheckTest.java:77

  • sanity_checks_run_once() is not a deterministic regression test for the “checks run once” behavior: on sufficiently provisioned CI hosts, the second start would succeed even if the sanity checks were accidentally re-run. To make this test meaningful, it needs an observable signal that the native sanity checks executed (e.g., an explicit native counter exposed via JavaProfiler/JVMAccess, or a test-only argument that forces the sanity check path to record a marker).
    /**
     * Sanity checks run at most once across start/stop cycles.
     * After a successful start with checks enabled, subsequent starts do not re-run checks.
     */
    @Test
    void sanity_checks_run_once() throws Exception {
        profiler = JavaProfiler.getInstance();
        // First start with nosanity to guarantee success regardless of host resources.
        profiler.execute(startCommand("nosanity"));
        profiler.stop();
        // Second start (without nosanity) must not fail due to re-running checks — the
        // static guard in the native layer ensures they only fire on the first invocation.
        assertDoesNotThrow(() -> profiler.execute(startCommand(null)));
    }

ddprof-lib/src/main/cpp/os_linux.cpp:900

  • The cgroup v1 fallback assumes the CPU controller is mounted at /sys/fs/cgroup/cpu. On many cgroup v1 setups the mount directory is different (e.g. cpu,cpuacct), which would make this return -1 (unconstrained) even when a quota is set, weakening the intended gating behavior in containers. Consider resolving the actual controller mountpoint via /proc/self/mountinfo (or trying common alternatives) before constructing path.

This issue also appears on line 989 of the same file.

    // Fall back to cgroup v1, likewise resolved from the process's own path.
    if (getOwnCgroupPath("cpu", subpath, sizeof(subpath))) {
        const char* base = "/sys/fs/cgroup/cpu";
        size_t base_len = strlen(base);
        size_t sub_len = strlen(subpath);
        if (base_len + sub_len < sizeof(path)) {
            memcpy(path, base, base_len);
            memcpy(path + base_len, subpath, sub_len + 1);

            char leaf[PATH_MAX];
            if ((size_t)snprintf(leaf, sizeof(leaf), "%s/cpu.cfs_quota_us", path) < sizeof(leaf)) {
                int fd = open(leaf, O_RDONLY);
                if (fd != -1) {
                    close(fd);
                    return walkCgroupV1CpuMillicores(path);

@dd-octo-sts

dd-octo-sts Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (commit d17bcd0)

Pipeline: https://gitlab.ddbuild.io/DataDog/apm-reliability/benchmarking-platform/-/pipelines/128894969 Commit: d17bcd06c3d469b6f1612b2ae02f05251ecfae2a

✅ Within expected boundaries

No significant runtime deltas (all within run-to-run noise) and no internal-counter outliers.

Runtime details (per benchmark × JDK)
Benchmark JDK Latest Dev Δ (dev vs latest) Issues L/D
akka-uct 21 ✅ 10375 ms (21 iters) ✅ 10433 ms (21 iters) ≈ +0.6% (±11.9%) — / —
akka-uct 25 ✅ 8988 ms (24 iters) ✅ 8822 ms (24 iters) ≈ -1.8% (±10.9%) — / —
finagle-chirper 21 ✅ 5974 ms (33 iters) ✅ 5995 ms (33 iters) ≈ +0.4% (±25.2%) ⚠️ W:3 / ⚠️ W:3
finagle-chirper 25 ✅ 5494 ms (36 iters) ✅ 5426 ms (36 iters) ≈ -1.2% (±24.4%) ⚠️ W:3 / ⚠️ W:3
fj-kmeans 21 ✅ 2703 ms (69 iters) ✅ 2639 ms (72 iters) ≈ -2.4% (±2.6%) — / —
fj-kmeans 25 ✅ 2823 ms (66 iters) ✅ 2766 ms (68 iters) ≈ -2% (±2.8%) — / —
future-genetic 21 ✅ 2092 ms (89 iters) ✅ 2074 ms (89 iters) ≈ -0.9% (±2.7%) — / —
future-genetic 25 ✅ 2037 ms (91 iters) ✅ 2019 ms (92 iters) ≈ -0.9% (±2.6%) — / —
naive-bayes 21 ✅ 1235 ms (138 iters) ✅ 1309 ms (131 iters) ≈ +6% (±33.7%) — / —
naive-bayes 25 ✅ 994 ms (172 iters) ✅ 1025 ms (167 iters) ≈ +3.1% (±32.1%) — / —
reactors 21 ✅ 15769 ms (15 iters) ✅ 16320 ms (15 iters) ≈ +3.5% (±7.9%) — / —
reactors 25 ✅ 18607 ms (15 iters) ✅ 18244 ms (15 iters) ≈ -2% (±4.7%) — / —
Internal counter details (ddprof)

ddprof internal counters, latest / dev (✅ = 0, · = unavailable):

Benchmark JDK Dropped rec Dropped jvmti Dropped trace Skipped WC AGCT fail Unwind fail
akka-uct 21 ✅ / ✅ ✅ / ✅ ✅ / ✅ ✅ / ✅ ✅ / ✅ ✅ / ✅
akka-uct 25 ✅ / ✅ ✅ / ✅ 3 / ✅ 2333 / 2348 ✅ / ✅ ✅ / ✅
finagle-chirper 21 ✅ / ✅ ✅ / ✅ 4 / 3 8650 / 8859 ✅ / ✅ ✅ / ✅
finagle-chirper 25 ✅ / ✅ ✅ / ✅ 1 / ✅ 8388 / 8517 ✅ / ✅ ✅ / ✅
fj-kmeans 21 ✅ / ✅ ✅ / ✅ ✅ / 2 1270 / 1295 ✅ / ✅ ✅ / ✅
fj-kmeans 25 ✅ / ✅ ✅ / ✅ 5 / ✅ 1291 / 1287 ✅ / ✅ ✅ / ✅
future-genetic 21 ✅ / ✅ ✅ / ✅ ✅ / 2 3007 / 2956 ✅ / ✅ ✅ / ✅
future-genetic 25 ✅ / ✅ ✅ / ✅ 2 / 3 2896 / 2918 ✅ / ✅ ✅ / ✅
naive-bayes 21 ✅ / ✅ ✅ / ✅ 7 / 3 3488 / 3504 ✅ / ✅ ✅ / ✅
naive-bayes 25 ✅ / ✅ ✅ / ✅ 2 / 2 3483 / 3451 ✅ / ✅ ✅ / ✅
reactors 21 ✅ / ✅ ✅ / ✅ ✅ / ✅ 1530 / 1504 ✅ / ✅ ✅ / ✅
reactors 25 ✅ / ✅ ✅ / ✅ ✅ / 1 1841 / 1858 ✅ / ✅ ✅ / ✅

Copilot AI review requested due to automatic review settings August 4, 2026 16:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (2)

ddprof-lib/src/main/cpp/sanityCheck.cpp:82

  • ThreadStackSize can legitimately be 0 (which makes stack_size become 0 here), causing the memory estimate to ignore per-thread stack memory and potentially under-estimate the required memory.
    size_t heap_max      = hotspot ? getVMSizeFlag("MaxHeapSize",           0) : 0;
    size_t metaspace_max = hotspot ? getVMSizeFlag("MaxMetaspaceSize",      DEFAULT_METASPACE) : 0;
    size_t codecache     = hotspot ? getVMSizeFlag("ReservedCodeCacheSize", DEFAULT_CODECACHE) : 0;
    size_t stack_size    = hotspot ? getVMSizeFlag("ThreadStackSize",       DEFAULT_STACK_SIZE / 1024) * 1024 : 0;

ddprof-lib/src/main/cpp/sanityCheck.cpp:64

  • If OS::getRamSize() returns 0 (e.g., /proc/meminfo unavailable), upper becomes 0 and the memory sanity check is effectively disabled even when a cgroup/container memory limit is available. This can let the profiler start in memory-constrained containers without running the intended check.

This issue also appears on line 78 of the same file.

    u64 ram = OS::getRamSize();
    u64 upper = (ram > OS_RESERVE) ? (ram - OS_RESERVE) : 0;
    if (container_limit > 0 && (u64)container_limit < upper) {
        upper = (u64)container_limit;
    }

@dd-octo-sts

dd-octo-sts Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (commit f68a10d)

Pipeline: https://gitlab.ddbuild.io/DataDog/apm-reliability/benchmarking-platform/-/pipelines/128906026 Commit: f68a10dd110f0f9423a18747f44865277bd7128a

⚠️ Significant outliers

  • 🔴 future-genetic (JDK 25): runtime +4.9% (1973→2069 ms)
  • 🟢 reactors (JDK 21): runtime -9.8% (17088→15405 ms)
Runtime details (per benchmark × JDK)
Benchmark JDK Latest Dev Δ (dev vs latest) Issues L/D
akka-uct 21 ✅ 10255 ms (21 iters) ✅ 10362 ms (21 iters) ≈ +1% (±11.5%) — / —
akka-uct 25 ✅ 8851 ms (24 iters) ✅ 8813 ms (24 iters) ≈ -0.4% (±10.4%) — / —
finagle-chirper 21 ✅ 5941 ms (33 iters) ✅ 5952 ms (33 iters) ≈ +0.2% (±25%) ⚠️ W:3 / ⚠️ W:4
finagle-chirper 25 ✅ 5453 ms (36 iters) ✅ 5462 ms (36 iters) ≈ +0.2% (±24.5%) ⚠️ W:3 / ⚠️ W:3
fj-kmeans 21 ✅ 2717 ms (68 iters) ✅ 2727 ms (68 iters) ≈ +0.4% (±2.7%) — / —
fj-kmeans 25 ✅ 2814 ms (66 iters) ✅ 2826 ms (66 iters) ≈ +0.4% (±2.6%) — / —
future-genetic 21 ✅ 2043 ms (91 iters) ✅ 2071 ms (90 iters) ≈ +1.4% (±2.6%) — / —
future-genetic 25 ✅ 1973 ms (93 iters) ✅ 2069 ms (90 iters) 🔴 +4.9% — / —
naive-bayes 21 ✅ 1241 ms (137 iters) ✅ 1330 ms (129 iters) ≈ +7.2% (±33.5%) — / —
naive-bayes 25 ✅ 950 ms (179 iters) ✅ 1019 ms (167 iters) ≈ +7.3% (±33%) — / —
reactors 21 ✅ 17088 ms (15 iters) ✅ 15405 ms (15 iters) 🟢 -9.8% — / —
reactors 25 ✅ 18811 ms (15 iters) ✅ 18608 ms (15 iters) ≈ -1.1% (±3.8%) — / —
Internal counter details (ddprof)

ddprof internal counters, latest / dev (✅ = 0, · = unavailable):

Benchmark JDK Dropped rec Dropped jvmti Dropped trace Skipped WC AGCT fail Unwind fail
akka-uct 21 ✅ / ✅ ✅ / ✅ 5 / 5 1926 / 2060 ✅ / ✅ ✅ / ✅
akka-uct 25 ✅ / ✅ ✅ / ✅ 2 / 2 2184 / 2214 ✅ / ✅ ✅ / ✅
finagle-chirper 21 ✅ / ✅ ✅ / ✅ 4 / 2 8523 / 8353 ✅ / ✅ ✅ / ✅
finagle-chirper 25 ✅ / ✅ ✅ / ✅ ✅ / 4 8319 / 8163 ✅ / ✅ ✅ / ✅
fj-kmeans 21 ✅ / ✅ ✅ / ✅ 2 / 1 1242 / 1285 ✅ / ✅ ✅ / ✅
fj-kmeans 25 ✅ / ✅ ✅ / ✅ 2 / 5 1271 / 1287 ✅ / ✅ ✅ / ✅
future-genetic 21 ✅ / ✅ ✅ / ✅ 1 / ✅ 2969 / 2943 ✅ / ✅ ✅ / ✅
future-genetic 25 ✅ / ✅ ✅ / ✅ 1 / ✅ 2874 / 2887 ✅ / ✅ ✅ / ✅
naive-bayes 21 ✅ / ✅ ✅ / ✅ 8 / 9 3487 / 3529 ✅ / ✅ ✅ / ✅
naive-bayes 25 ✅ / ✅ ✅ / ✅ 1 / 4 3499 / 3490 ✅ / ✅ ✅ / ✅
reactors 21 ✅ / ✅ ✅ / ✅ ✅ / 1 1711 / 1525 ✅ / ✅ ✅ / ✅
reactors 25 ✅ / ✅ ✅ / ✅ 1 / ✅ 1939 / 1835 ✅ / ✅ ✅ / ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Libretto] Add sanity checks for profiler

2 participants