From d981e163085d1e6bc9315197ca848de462744e96 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Thu, 16 Apr 2026 11:56:21 +0200 Subject: [PATCH 1/6] Add sanity checks for profiler (PROF-13027) Co-Authored-By: Claude Sonnet 4.6 --- ddprof-lib/src/main/cpp/arguments.cpp | 16 +++ ddprof-lib/src/main/cpp/arguments.h | 4 +- ddprof-lib/src/main/cpp/os.h | 2 + ddprof-lib/src/main/cpp/os_linux.cpp | 84 +++++++++++++ ddprof-lib/src/main/cpp/os_macos.cpp | 8 ++ ddprof-lib/src/main/cpp/profiler.cpp | 13 ++ ddprof-lib/src/main/cpp/sanityCheck.cpp | 111 ++++++++++++++++++ ddprof-lib/src/main/cpp/sanityCheck.h | 16 +++ .../profiler/sanity/SanityCheckTest.java | 80 +++++++++++++ 9 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 ddprof-lib/src/main/cpp/sanityCheck.cpp create mode 100644 ddprof-lib/src/main/cpp/sanityCheck.h create mode 100644 ddprof-test/src/test/java/com/datadoghq/profiler/sanity/SanityCheckTest.java diff --git a/ddprof-lib/src/main/cpp/arguments.cpp b/ddprof-lib/src/main/cpp/arguments.cpp index 72b8aec224..9543337585 100644 --- a/ddprof-lib/src/main/cpp/arguments.cpp +++ b/ddprof-lib/src/main/cpp/arguments.cpp @@ -374,6 +374,22 @@ Error Arguments::parse(const char *args) { } } + CASE("nosanity") + if (value != NULL) { + switch (value[0]) { + case 'n': // no + case 'f': // false + case '0': // 0 + _skip_sanity_checks = false; + break; + default: + _skip_sanity_checks = true; + } + } else { + // bare 'nosanity' with no value means skip checks + _skip_sanity_checks = true; + } + DEFAULT() if (_unknown_arg == NULL) _unknown_arg = arg; diff --git a/ddprof-lib/src/main/cpp/arguments.h b/ddprof-lib/src/main/cpp/arguments.h index 462be5b534..a8fa52189b 100644 --- a/ddprof-lib/src/main/cpp/arguments.h +++ b/ddprof-lib/src/main/cpp/arguments.h @@ -189,6 +189,7 @@ class Arguments { bool _lightweight; bool _enable_method_cleanup; bool _remote_symbolication; // Enable remote symbolication for native frames + bool _skip_sanity_checks; Arguments(bool persistent = false) : _buf(NULL), @@ -223,7 +224,8 @@ class Arguments { _context_attributes({}), _lightweight(false), _enable_method_cleanup(true), - _remote_symbolication(false) {} + _remote_symbolication(false), + _skip_sanity_checks(false) {} ~Arguments(); diff --git a/ddprof-lib/src/main/cpp/os.h b/ddprof-lib/src/main/cpp/os.h index 1c16af80cc..3eceb3f7c3 100644 --- a/ddprof-lib/src/main/cpp/os.h +++ b/ddprof-lib/src/main/cpp/os.h @@ -132,6 +132,8 @@ class OS { static bool getCpuDescription(char* buf, size_t size); static int getCpuCount(); + static int getCgroupCpuMillicores(); + static long getContainerMemoryLimit(); static u64 getProcessCpuTime(u64* utime, u64* stime); static u64 getTotalCpuTime(u64* utime, u64* stime); diff --git a/ddprof-lib/src/main/cpp/os_linux.cpp b/ddprof-lib/src/main/cpp/os_linux.cpp index 8786b26442..0dcc55c345 100644 --- a/ddprof-lib/src/main/cpp/os_linux.cpp +++ b/ddprof-lib/src/main/cpp/os_linux.cpp @@ -361,6 +361,90 @@ int OS::getCpuCount() { return sysconf(_SC_NPROCESSORS_ONLN); } +int OS::getCgroupCpuMillicores() { + // Try cgroup v2 first + int fd = open("/sys/fs/cgroup/cpu.max", O_RDONLY); + if (fd != -1) { + char buf[64] = {0}; + ssize_t r = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (r > 0) { + if (strncmp(buf, "max", 3) == 0) { + return -1; // unconstrained + } + long quota, period; + if (sscanf(buf, "%ld %ld", "a, &period) == 2 && period > 0) { + return (int)(quota * 1000 / period); + } + } + } + + // Fall back to cgroup v1 + long quota = -1; + long period = 100000; // default 100ms + + fd = open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us", O_RDONLY); + if (fd != -1) { + char buf[32] = {0}; + ssize_t r = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (r > 0) { + quota = atol(buf); + } + } + + if (quota <= 0) { + return -1; // unconstrained or unavailable + } + + fd = open("/sys/fs/cgroup/cpu/cpu.cfs_period_us", O_RDONLY); + if (fd != -1) { + char buf[32] = {0}; + ssize_t r = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (r > 0) { + long p = atol(buf); + if (p > 0) period = p; + } + } + + return (int)(quota * 1000 / period); +} + +long OS::getContainerMemoryLimit() { + // Try cgroup v2 first + int fd = open("/sys/fs/cgroup/memory.max", O_RDONLY); + if (fd != -1) { + char buf[32] = {0}; + ssize_t r = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (r > 0) { + if (strncmp(buf, "max", 3) == 0) { + return -1; // unconstrained + } + long limit = atol(buf); + if (limit > 0) return limit; + } + } + + // Fall back to cgroup v1 + fd = open("/sys/fs/cgroup/memory/memory.limit_in_bytes", O_RDONLY); + if (fd != -1) { + char buf[32] = {0}; + ssize_t r = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (r > 0) { + long limit = atol(buf); + // A limit of 9223372036854771712 (LLONG_MAX rounded) means unconstrained + if (limit > 0 && limit < 0x7ffffffffffff000L) { + return limit; + } + } + } + + return -1; +} + u64 OS::getProcessCpuTime(u64* utime, u64* stime) { struct tms buf; clock_t real = times(&buf); diff --git a/ddprof-lib/src/main/cpp/os_macos.cpp b/ddprof-lib/src/main/cpp/os_macos.cpp index 72f978bacf..5b23cd6358 100644 --- a/ddprof-lib/src/main/cpp/os_macos.cpp +++ b/ddprof-lib/src/main/cpp/os_macos.cpp @@ -334,6 +334,14 @@ int OS::getCpuCount() { return sysctlbyname("hw.logicalcpu", &cpu_count, &size, NULL, 0) == 0 ? cpu_count : 1; } +int OS::getCgroupCpuMillicores() { + return -1; // not applicable on macOS +} + +long OS::getContainerMemoryLimit() { + return -1; // not applicable on macOS +} + u64 OS::getProcessCpuTime(u64* utime, u64* stime) { struct tms buf; clock_t real = times(&buf); diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 61722bab87..0455de1ff1 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -35,6 +35,7 @@ #include "utils.h" #include "wallClock.h" #include "frames.h" +#include "sanityCheck.h" #include #include @@ -1019,6 +1020,18 @@ Error Profiler::start(Arguments &args, bool reset) { return Error("Profiler already started"); } + if (!args._skip_sanity_checks) { + static Error sanity_result = Error::OK; + static bool sanity_checked = false; + if (!sanity_checked) { + sanity_checked = true; + sanity_result = SanityChecker::runChecks(args); + } + if (sanity_result) { + return sanity_result; + } + } + Error error = checkJvmCapabilities(); if (error) { return error; diff --git a/ddprof-lib/src/main/cpp/sanityCheck.cpp b/ddprof-lib/src/main/cpp/sanityCheck.cpp new file mode 100644 index 0000000000..71ee0fbbbe --- /dev/null +++ b/ddprof-lib/src/main/cpp/sanityCheck.cpp @@ -0,0 +1,111 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "sanityCheck.h" +#include "common.h" +#include "os.h" +#include "hotspot/vmStructs.h" + +// Returns the value of a size-typed JVM flag, or default_val if not found. +static size_t getVMSizeFlag(const char* name, size_t default_val) { + VMFlag* f = VMFlag::find(name, {VMFlag::Type::Uintx, VMFlag::Type::Size_t, VMFlag::Type::Uint64_t}); + if (f != NULL && f->addr() != NULL) { + return *static_cast(f->addr()); + } + return default_val; +} + +Error SanityChecker::runChecks(const Arguments& /*args*/) { + // Static buffer for error message — safe because runChecks is called under + // _state_lock and the result is cached as a static Error in profiler.cpp. + static char err_buf[1024]; + + // --- Gather all system info upfront --- + int logical_cpus = OS::getCpuCount(); + int cgroup_mc = OS::getCgroupCpuMillicores(); + long container_limit = OS::getContainerMemoryLimit(); + bool containerized = (cgroup_mc > 0 || container_limit > 0); + + int effective_cores = logical_cpus; + if (cgroup_mc > 0) { + int cgroup_cores = cgroup_mc / 1000; + if (cgroup_cores < effective_cores) { + effective_cores = cgroup_cores; + } + } + + const u64 OS_RESERVE = 128ULL * 1024 * 1024; + const u64 PROFILER_OVERHEAD = 64ULL * 1024 * 1024; + + 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; + } + + const size_t DEFAULT_METASPACE = 256ULL * 1024 * 1024; + const size_t DEFAULT_CODECACHE = 240ULL * 1024 * 1024; + const size_t DEFAULT_STACK_SIZE = 512ULL * 1024; + const int DEFAULT_THREAD_COUNT = 200; + + size_t heap_max = getVMSizeFlag("MaxHeapSize", 0); + size_t metaspace_max = getVMSizeFlag("MaxMetaspaceSize", DEFAULT_METASPACE); + size_t codecache = getVMSizeFlag("ReservedCodeCacheSize", DEFAULT_CODECACHE); + size_t stack_size = getVMSizeFlag("ThreadStackSize", DEFAULT_STACK_SIZE / 1024) * 1024; + + int thread_count = DEFAULT_THREAD_COUNT; + ProcessInfo info = {}; + if (OS::getBasicProcessInfo(OS::processId(), &info) && info.threads > 0) { + thread_count = info.threads; + } + + u64 gc_overhead = (u64)heap_max * 30 / 100; + u64 lower = (u64)heap_max + (u64)metaspace_max + (u64)codecache + + gc_overhead + + (u64)thread_count * (u64)stack_size + + PROFILER_OVERHEAD; + + // --- Run checks --- + bool cpu_fail = (effective_cores < 1); + bool mem_fail = (upper > 0 && lower > upper); + + if (!cpu_fail && !mem_fail) { + return Error::OK; + } + + if (cpu_fail) { + LOG_WARN("Sanity check failed: effective CPU count is %d (logical=%d, cgroup=%dmc).", + effective_cores, logical_cpus, cgroup_mc); + } + if (mem_fail) { + LOG_WARN("Sanity check failed: estimated memory requirement (%llu MB) exceeds available memory (%llu MB).", + (unsigned long long)(lower / (1024 * 1024)), + (unsigned long long)(upper / (1024 * 1024))); + } + + snprintf(err_buf, sizeof(err_buf), + "[sanity] cpu=%s,memory=%s," + "logical_cores=%d,cgroup_millicores=%d,effective_cores=%d," + "ram_mb=%llu,container_limit_mb=%lld,upper_mb=%llu,lower_mb=%llu," + "heap_mb=%llu,metaspace_mb=%llu,codecache_mb=%llu," + "gc_overhead_mb=%llu,threads=%d,stack_kb=%llu,profiler_mb=%llu," + "containerized=%s", + cpu_fail ? "fail" : "ok", + mem_fail ? "fail" : "ok", + logical_cpus, cgroup_mc, effective_cores, + (unsigned long long)(ram / (1024 * 1024)), + container_limit > 0 ? (long long)(container_limit / (1024 * 1024)) : -1LL, + (unsigned long long)(upper / (1024 * 1024)), + (unsigned long long)(lower / (1024 * 1024)), + (unsigned long long)(heap_max / (1024 * 1024)), + (unsigned long long)(metaspace_max / (1024 * 1024)), + (unsigned long long)(codecache / (1024 * 1024)), + (unsigned long long)(gc_overhead / (1024 * 1024)), + thread_count, + (unsigned long long)(stack_size / 1024), + (unsigned long long)(PROFILER_OVERHEAD / (1024 * 1024)), + containerized ? "true" : "false"); + return Error(err_buf); +} diff --git a/ddprof-lib/src/main/cpp/sanityCheck.h b/ddprof-lib/src/main/cpp/sanityCheck.h new file mode 100644 index 0000000000..43f0515862 --- /dev/null +++ b/ddprof-lib/src/main/cpp/sanityCheck.h @@ -0,0 +1,16 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _SANITY_CHECK_H +#define _SANITY_CHECK_H + +#include "arguments.h" + +class SanityChecker { + public: + static Error runChecks(const Arguments& args); +}; + +#endif // _SANITY_CHECK_H diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/sanity/SanityCheckTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/sanity/SanityCheckTest.java new file mode 100644 index 0000000000..6d4a23daeb --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/sanity/SanityCheckTest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.datadoghq.profiler.sanity; + +import com.datadoghq.profiler.JavaProfiler; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class SanityCheckTest { + + private JavaProfiler profiler; + private Path jfrDump; + + private String startCommand(String extra) throws IOException { + Path rootDir = Paths.get("/tmp/recordings"); + Files.createDirectories(rootDir); + jfrDump = Files.createTempFile(rootDir, "sanity-check-test", ".jfr"); + String base = "start,cpu=10ms,jfr,file=" + jfrDump.toAbsolutePath(); + return extra == null || extra.isEmpty() ? base : base + "," + extra; + } + + @AfterEach + void cleanup() throws Exception { + if (profiler != null) { + try { + profiler.stop(); + } catch (IllegalStateException ignored) { + // already stopped or never started + } + } + if (jfrDump != null) { + Files.deleteIfExists(jfrDump); + } + } + + /** + * nosanity=true bypasses sanity checks; profiler must start successfully on any host. + */ + @Test + void nosanity_bypasses_checks() throws Exception { + profiler = JavaProfiler.getInstance(); + assertDoesNotThrow(() -> profiler.execute(startCommand("nosanity"))); + } + + /** + * The override flag works regardless of value form (bare keyword vs explicit true). + */ + @Test + void nosanity_explicit_true_bypasses_checks() throws Exception { + profiler = JavaProfiler.getInstance(); + assertDoesNotThrow(() -> profiler.execute(startCommand("nosanity=true"))); + } + + /** + * 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. + // On a normal host this will also pass because normal hosts satisfy the requirements. + // On a pathological host the first start already set sanity_checked=true. + assertDoesNotThrow(() -> profiler.execute(startCommand("nosanity"))); + } +} From 675b604addc559483fa953112f6c75cc815af775 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Tue, 4 Aug 2026 15:46:38 +0200 Subject: [PATCH 2/6] sphinx: address review feedback on PR #483 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. --- ddprof-lib/src/main/cpp/os_linux.cpp | 344 ++++++++++++++++++++---- ddprof-lib/src/main/cpp/profiler.cpp | 20 +- ddprof-lib/src/main/cpp/sanityCheck.cpp | 63 ++++- 3 files changed, 349 insertions(+), 78 deletions(-) diff --git a/ddprof-lib/src/main/cpp/os_linux.cpp b/ddprof-lib/src/main/cpp/os_linux.cpp index 0065af72dc..d4d246fe82 100644 --- a/ddprof-lib/src/main/cpp/os_linux.cpp +++ b/ddprof-lib/src/main/cpp/os_linux.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -695,83 +696,312 @@ int OS::getCpuCount() { return sysconf(_SC_NPROCESSORS_ONLN); } -int OS::getCgroupCpuMillicores() { - // Try cgroup v2 first - int fd = open("/sys/fs/cgroup/cpu.max", O_RDONLY); - if (fd != -1) { - char buf[64] = {0}; - ssize_t r = read(fd, buf, sizeof(buf) - 1); - close(fd); - if (r > 0) { - if (strncmp(buf, "max", 3) == 0) { - return -1; // unconstrained - } - long quota, period; - if (sscanf(buf, "%ld %ld", "a, &period) == 2 && period > 0) { - return (int)(quota * 1000 / period); +// Returns true if `controller` is present as an exact, comma-delimited +// token in `controllers` (e.g. "cpu" matches "cpu,cpuacct" but not +// "cpuacct" or "cpuset"; substring matching would give false positives +// since several v1 controller names share a "cpu" prefix). +static bool hasControllerToken(const char* controllers, const char* controller) { + size_t controller_len = strlen(controller); + const char* p = controllers; + while (*p != 0) { + const char* comma = strchr(p, ','); + size_t tok_len = (comma != NULL) ? (size_t)(comma - p) : strlen(p); + if (tok_len == controller_len && strncmp(p, controller, tok_len) == 0) { + return true; + } + if (comma == NULL) { + break; + } + p = comma + 1; + } + return false; +} + +// Resolves this process's own path within a cgroup hierarchy from +// /proc/self/cgroup, so that limits are read from the process's actual +// (possibly nested, e.g. "/user.slice/...") cgroup rather than from the +// hierarchy mount root. Pass an empty controller for the cgroup v2 unified +// hierarchy (format "0::/path"); pass a controller name (e.g. "cpu", +// "memory") to match a v1 hierarchy whose comma-separated controller list +// contains it (format "N:list:/path"). On success, copies the path (leading +// '/', no trailing '/', NUL-terminated) into `out` and returns true. +static bool getOwnCgroupPath(const char* controller, char* out, size_t out_size) { + int fd = open("/proc/self/cgroup", O_RDONLY); + if (fd == -1) { + return false; + } + char buf[2048]; + ssize_t r = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (r <= 0) { + return false; + } + buf[r] = 0; + + char* line = buf; + while (line != NULL && *line != 0) { + char* nl = strchr(line, '\n'); + if (nl != NULL) { + *nl = 0; + } + char* c1 = strchr(line, ':'); + char* c2 = (c1 != NULL) ? strchr(c1 + 1, ':') : NULL; + if (c1 != NULL && c2 != NULL) { + *c2 = 0; + const char* controllers = c1 + 1; + const char* path = c2 + 1; + bool matches = (controller[0] == 0) ? (controllers[0] == 0) + : hasControllerToken(controllers, controller); + if (matches) { + size_t len = strlen(path); + if (len == 0 || len >= out_size) { + return false; + } + memcpy(out, path, len + 1); + return true; } } + line = (nl != NULL) ? nl + 1 : NULL; } + return false; +} - // Fall back to cgroup v1 - long quota = -1; - long period = 100000; // default 100ms +// Trims the last '/'-separated component from `path` (in place). Refuses to +// trim past `base_len` (the length of the hierarchy mount prefix, which is +// never itself a cgroup boundary to walk beyond). Returns false once the +// mount root has been reached. +static bool trimToParentCgroup(char* path, size_t base_len) { + if (strlen(path) <= base_len) { + return false; + } + char* slash = strrchr(path, '/'); + if (slash == NULL || (size_t)(slash - path) < base_len) { + return false; + } + *slash = 0; + return true; +} - fd = open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us", O_RDONLY); - if (fd != -1) { - char buf[32] = {0}; - ssize_t r = read(fd, buf, sizeof(buf) - 1); - close(fd); - if (r > 0) { - quota = atol(buf); +// Applies the most restrictive cpu.max quota found across this process's +// cgroup v2 group and all of its ancestors up to the mount root — a nested +// group can never be more permissive than a constrained ancestor. +static int walkCgroupV2CpuMillicores(char* path) { + size_t base_len = strlen("/sys/fs/cgroup"); + int best = -1; // unconstrained (or no data) so far + for (;;) { + char file[PATH_MAX]; + if ((size_t)snprintf(file, sizeof(file), "%s/cpu.max", path) < sizeof(file)) { + int fd = open(file, O_RDONLY); + if (fd != -1) { + char buf[64] = {0}; + ssize_t r = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (r > 0 && strncmp(buf, "max", 3) != 0) { + long quota, period; + if (sscanf(buf, "%ld %ld", "a, &period) == 2 && period > 0) { + int mc = (int)(quota * 1000 / period); + if (best < 0 || mc < best) { + best = mc; + } + } + } + } + } + if (!trimToParentCgroup(path, base_len)) { + break; + } + } + return best; +} + +// Same ancestor-walk as walkCgroupV2CpuMillicores(), for the cgroup v1 CPU +// controller (separate quota/period files instead of a single "cpu.max"). +static int walkCgroupV1CpuMillicores(char* path) { + size_t base_len = strlen("/sys/fs/cgroup/cpu"); + int best = -1; + for (;;) { + long quota = -1; + char qfile[PATH_MAX]; + if ((size_t)snprintf(qfile, sizeof(qfile), "%s/cpu.cfs_quota_us", path) < sizeof(qfile)) { + int fd = open(qfile, O_RDONLY); + if (fd != -1) { + char buf[32] = {0}; + ssize_t r = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (r > 0) { + quota = atol(buf); + } + } + } + if (quota > 0) { + long period = 100000; // default 100ms + char pfile[PATH_MAX]; + if ((size_t)snprintf(pfile, sizeof(pfile), "%s/cpu.cfs_period_us", path) < sizeof(pfile)) { + int fd = open(pfile, O_RDONLY); + if (fd != -1) { + char buf[32] = {0}; + ssize_t r = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (r > 0) { + long p = atol(buf); + if (p > 0) period = p; + } + } + } + int mc = (int)(quota * 1000 / period); + if (best < 0 || mc < best) { + best = mc; + } + } + if (!trimToParentCgroup(path, base_len)) { + break; } } + return best; +} - if (quota <= 0) { - return -1; // unconstrained or unavailable +int OS::getCgroupCpuMillicores() { + char subpath[PATH_MAX]; + char path[PATH_MAX]; + + // Try cgroup v2 first, resolved from this process's own cgroup path. + if (getOwnCgroupPath("", subpath, sizeof(subpath))) { + size_t base_len = strlen("/sys/fs/cgroup"); + size_t sub_len = strlen(subpath); + if (base_len + sub_len < sizeof(path)) { + memcpy(path, "/sys/fs/cgroup", base_len); + memcpy(path + base_len, subpath, sub_len + 1); + + char leaf[PATH_MAX]; + if ((size_t)snprintf(leaf, sizeof(leaf), "%s/cpu.max", path) < sizeof(leaf)) { + int fd = open(leaf, O_RDONLY); + if (fd != -1) { + close(fd); + return walkCgroupV2CpuMillicores(path); + } + } + } } - fd = open("/sys/fs/cgroup/cpu/cpu.cfs_period_us", O_RDONLY); - if (fd != -1) { - char buf[32] = {0}; - ssize_t r = read(fd, buf, sizeof(buf) - 1); - close(fd); - if (r > 0) { - long p = atol(buf); - if (p > 0) period = p; + // 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); + } + } } } - return (int)(quota * 1000 / period); + return -1; // unconstrained or unavailable +} + +// Applies the smallest (most restrictive) memory.max found across this +// process's cgroup v2 group and all of its ancestors up to the mount root. +static long walkCgroupV2MemoryLimit(char* path) { + size_t base_len = strlen("/sys/fs/cgroup"); + long best = -1; + for (;;) { + char file[PATH_MAX]; + if ((size_t)snprintf(file, sizeof(file), "%s/memory.max", path) < sizeof(file)) { + int fd = open(file, O_RDONLY); + if (fd != -1) { + char buf[32] = {0}; + ssize_t r = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (r > 0 && strncmp(buf, "max", 3) != 0) { + long limit = atol(buf); + if (limit > 0 && (best < 0 || limit < best)) { + best = limit; + } + } + } + } + if (!trimToParentCgroup(path, base_len)) { + break; + } + } + return best; +} + +// Same ancestor-walk as walkCgroupV2MemoryLimit(), for the cgroup v1 memory +// controller. +static long walkCgroupV1MemoryLimit(char* path) { + size_t base_len = strlen("/sys/fs/cgroup/memory"); + long best = -1; + for (;;) { + char file[PATH_MAX]; + if ((size_t)snprintf(file, sizeof(file), "%s/memory.limit_in_bytes", path) < sizeof(file)) { + int fd = open(file, O_RDONLY); + if (fd != -1) { + char buf[32] = {0}; + ssize_t r = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (r > 0) { + long limit = atol(buf); + // A limit of 9223372036854771712 (LLONG_MAX rounded) means unconstrained. + if (limit > 0 && limit < 0x7ffffffffffff000L && (best < 0 || limit < best)) { + best = limit; + } + } + } + } + if (!trimToParentCgroup(path, base_len)) { + break; + } + } + return best; } long OS::getContainerMemoryLimit() { - // Try cgroup v2 first - int fd = open("/sys/fs/cgroup/memory.max", O_RDONLY); - if (fd != -1) { - char buf[32] = {0}; - ssize_t r = read(fd, buf, sizeof(buf) - 1); - close(fd); - if (r > 0) { - if (strncmp(buf, "max", 3) == 0) { - return -1; // unconstrained + char subpath[PATH_MAX]; + char path[PATH_MAX]; + + // Try cgroup v2 first, resolved from this process's own cgroup path. + if (getOwnCgroupPath("", subpath, sizeof(subpath))) { + size_t base_len = strlen("/sys/fs/cgroup"); + size_t sub_len = strlen(subpath); + if (base_len + sub_len < sizeof(path)) { + memcpy(path, "/sys/fs/cgroup", base_len); + memcpy(path + base_len, subpath, sub_len + 1); + + char leaf[PATH_MAX]; + if ((size_t)snprintf(leaf, sizeof(leaf), "%s/memory.max", path) < sizeof(leaf)) { + int fd = open(leaf, O_RDONLY); + if (fd != -1) { + close(fd); + return walkCgroupV2MemoryLimit(path); + } } - long limit = atol(buf); - if (limit > 0) return limit; } } - // Fall back to cgroup v1 - fd = open("/sys/fs/cgroup/memory/memory.limit_in_bytes", O_RDONLY); - if (fd != -1) { - char buf[32] = {0}; - ssize_t r = read(fd, buf, sizeof(buf) - 1); - close(fd); - if (r > 0) { - long limit = atol(buf); - // A limit of 9223372036854771712 (LLONG_MAX rounded) means unconstrained - if (limit > 0 && limit < 0x7ffffffffffff000L) { - return limit; + // 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); + } } } } diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 212b5bd638..33fa13c0d0 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1379,16 +1379,20 @@ Error Profiler::start(Arguments &args, bool reset) { return error; } - if (!args._skip_sanity_checks) { - static Error sanity_result = Error::OK; - static bool sanity_checked = false; - if (!sanity_checked) { - sanity_checked = true; + // Sanity checks run at most once per process, across start/stop cycles — + // record that the first start was handled regardless of _skip_sanity_checks, + // otherwise a first start with nosanity leaves sanity_checked false and the + // checks unexpectedly run (and can fail) on a later start without nosanity. + static Error sanity_result = Error::OK; + static bool sanity_checked = false; + if (!sanity_checked) { + sanity_checked = true; + if (!args._skip_sanity_checks) { sanity_result = SanityChecker::runChecks(args); } - if (sanity_result) { - return sanity_result; - } + } + if (sanity_result) { + return sanity_result; } error = checkJvmCapabilities(); diff --git a/ddprof-lib/src/main/cpp/sanityCheck.cpp b/ddprof-lib/src/main/cpp/sanityCheck.cpp index cf9e6b059e..9c2e322baa 100644 --- a/ddprof-lib/src/main/cpp/sanityCheck.cpp +++ b/ddprof-lib/src/main/cpp/sanityCheck.cpp @@ -3,21 +3,34 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include + #include "sanityCheck.h" #include "common.h" #include "os.h" +#include "vmEntry.h" #include "hotspot/vmStructs.h" #include "hotspot/vmStructs.inline.h" // Returns the value of a size-typed JVM flag, or default_val if not found. +// ThreadStackSize is declared as `intx` on standard HotSpot builds, so the +// Intx type must be accepted here too — otherwise this call always falls +// back to default_val for that flag. static size_t getVMSizeFlag(const char* name, size_t default_val) { - VMFlag* f = VMFlag::find(name, {VMFlag::Type::Uintx, VMFlag::Type::Size_t, VMFlag::Type::Uint64_t}); + VMFlag* f = VMFlag::find(name, {VMFlag::Type::Uintx, VMFlag::Type::Size_t, + VMFlag::Type::Uint64_t, VMFlag::Type::Intx}); if (f != NULL && f->addr() != NULL) { return *static_cast(f->addr()); } return default_val; } +// Adds b to a, clamping to UINT64_MAX on overflow instead of wrapping. +static u64 addClamped(u64 a, u64 b) { + u64 sum = a + b; + return sum < a ? UINT64_MAX : sum; +} + Error SanityChecker::runChecks(const Arguments& /*args*/) { // Static buffer for error message — safe because runChecks is called under // _state_lock and the result is cached as a static Error in profiler.cpp. @@ -29,10 +42,14 @@ Error SanityChecker::runChecks(const Arguments& /*args*/) { long container_limit = OS::getContainerMemoryLimit(); bool containerized = (cgroup_mc > 0 || container_limit > 0); - int effective_cores = logical_cpus; + // -1 means "unknown" (OS::getCpuCount() failed, and no cgroup CPU limit is + // in effect) — an unknown core count must not fail the check, since that + // would reject on an OS query error rather than an actual resource + // constraint. + int effective_cores = (logical_cpus > 0) ? logical_cpus : -1; if (cgroup_mc > 0) { int cgroup_cores = cgroup_mc / 1000; - if (cgroup_cores < effective_cores) { + if (effective_cores < 0 || cgroup_cores < effective_cores) { effective_cores = cgroup_cores; } } @@ -51,10 +68,25 @@ Error SanityChecker::runChecks(const Arguments& /*args*/) { const size_t DEFAULT_STACK_SIZE = 512ULL * 1024; const int DEFAULT_THREAD_COUNT = 200; - size_t heap_max = getVMSizeFlag("MaxHeapSize", 0); - size_t metaspace_max = getVMSizeFlag("MaxMetaspaceSize", DEFAULT_METASPACE); - size_t codecache = getVMSizeFlag("ReservedCodeCacheSize", DEFAULT_CODECACHE); - size_t stack_size = getVMSizeFlag("ThreadStackSize", DEFAULT_STACK_SIZE / 1024) * 1024; + // VMFlag::find() walks the HotSpot VMStructs flag table, which does not + // exist on OpenJ9/Zing — the calls below would silently report a + // zero-byte heap and fall back to fixed guesses for the other regions. + // Skip the memory estimate entirely on those runtimes rather than fail + // (or pass) the check on numbers that don't reflect the actual JVM. + bool hotspot = !VM::isOpenJ9() && !VM::isZing(); + + 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; + + // MaxMetaspaceSize defaults to SIZE_MAX (unbounded) on standard HotSpot + // builds, so the flag is present and the default_val fallback above never + // triggers. Normalize the sentinel to a finite estimate, otherwise it + // wraps the sum below and silently omits the metaspace allowance. + if (metaspace_max == SIZE_MAX) { + metaspace_max = DEFAULT_METASPACE; + } int thread_count = DEFAULT_THREAD_COUNT; ProcessInfo info = {}; @@ -63,14 +95,19 @@ Error SanityChecker::runChecks(const Arguments& /*args*/) { } u64 gc_overhead = (u64)heap_max * 30 / 100; - u64 lower = (u64)heap_max + (u64)metaspace_max + (u64)codecache - + gc_overhead - + (u64)thread_count * (u64)stack_size - + PROFILER_OVERHEAD; + u64 lower = (u64)heap_max; + 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); // --- Run checks --- - bool cpu_fail = (effective_cores < 1); - bool mem_fail = (upper > 0 && lower > upper); + // 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); if (!cpu_fail && !mem_fail) { return Error::OK; From 784f877ea9ea821440261c9a8243e895cdae216b Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Tue, 4 Aug 2026 15:59:16 +0200 Subject: [PATCH 3/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ddprof-lib/src/main/cpp/profiler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddprof-lib/src/main/cpp/profiler.cpp b/ddprof-lib/src/main/cpp/profiler.cpp index 33fa13c0d0..d3f5870b3e 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -1391,7 +1391,7 @@ Error Profiler::start(Arguments &args, bool reset) { sanity_result = SanityChecker::runChecks(args); } } - if (sanity_result) { + if (sanity_result && !args._skip_sanity_checks) { return sanity_result; } From e8b443d10fa4f36e71806da647abf35312525a7a Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Tue, 4 Aug 2026 16:05:39 +0200 Subject: [PATCH 4/6] fix(ci): bump IBM Java 8 JRE URL to 8.0.8.70 (8.0.8.60 removed by IBM) --- .github/workflows/cache_java.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cache_java.yml b/.github/workflows/cache_java.yml index 17abf88e2c..7bba69ef97 100644 --- a/.github/workflows/cache_java.yml +++ b/.github/workflows/cache_java.yml @@ -34,7 +34,7 @@ env: # jdk1.8.0_361 JAVA_8_ORACLE_URL: "https://javadl.oracle.com/webapps/download/AutoDL?BundleId=247926_0ae14417abb444ebb02b9815e2103550" - JAVA_8_IBM_URL: "https://public.dhe.ibm.com/ibmdl/export/pub/systems/cloud/runtimes/java/8.0.8.60/linux/x86_64/ibm-java-jre-8.0-8.60-linux-x86_64.tgz" + JAVA_8_IBM_URL: "https://public.dhe.ibm.com/ibmdl/export/pub/systems/cloud/runtimes/java/8.0.8.70/linux/x86_64/ibm-java-jre-8.0-8.70-linux-x86_64.tgz" # FIXME: Azul pulled public CDN access to Zing/Prime downloads - all URLs return 404 # JAVA_8_ZING_URL : "https://cdn.azul.com/zing-zvm/ZVM23.05.0.0/zing23.05.0.0-2-jdk8.0.372-linux_x64.tar.gz" From d17bcd06c3d469b6f1612b2ae02f05251ecfae2a Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Tue, 4 Aug 2026 17:07:23 +0200 Subject: [PATCH 5/6] fix: normalize unbounded MaxMetaspaceSize by threshold, not exact SIZE_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. --- ddprof-lib/src/main/cpp/sanityCheck.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ddprof-lib/src/main/cpp/sanityCheck.cpp b/ddprof-lib/src/main/cpp/sanityCheck.cpp index 9c2e322baa..e99db1387a 100644 --- a/ddprof-lib/src/main/cpp/sanityCheck.cpp +++ b/ddprof-lib/src/main/cpp/sanityCheck.cpp @@ -80,11 +80,13 @@ Error SanityChecker::runChecks(const Arguments& /*args*/) { size_t codecache = hotspot ? getVMSizeFlag("ReservedCodeCacheSize", DEFAULT_CODECACHE) : 0; size_t stack_size = hotspot ? getVMSizeFlag("ThreadStackSize", DEFAULT_STACK_SIZE / 1024) * 1024 : 0; - // MaxMetaspaceSize defaults to SIZE_MAX (unbounded) on standard HotSpot + // MaxMetaspaceSize defaults to unbounded (max_uintx) on standard HotSpot // builds, so the flag is present and the default_val fallback above never - // triggers. Normalize the sentinel to a finite estimate, otherwise it - // wraps the sum below and silently omits the metaspace allowance. - if (metaspace_max == SIZE_MAX) { + // triggers. The exact sentinel value isn't reliable to match against — + // debug builds align it down during ergonomics, leaving it astronomically + // large but not bit-identical to SIZE_MAX. Any "limit" larger than total + // available memory isn't a real limit, so normalize on that instead. + if (metaspace_max > upper) { metaspace_max = DEFAULT_METASPACE; } From f68a10dd110f0f9423a18747f44865277bd7128a Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Tue, 4 Aug 2026 18:03:52 +0200 Subject: [PATCH 6/6] fix: lower CPU sanity-check cutoff to <1 core instead of <2 --- ddprof-lib/src/main/cpp/sanityCheck.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ddprof-lib/src/main/cpp/sanityCheck.cpp b/ddprof-lib/src/main/cpp/sanityCheck.cpp index e99db1387a..1154132916 100644 --- a/ddprof-lib/src/main/cpp/sanityCheck.cpp +++ b/ddprof-lib/src/main/cpp/sanityCheck.cpp @@ -106,9 +106,9 @@ Error SanityChecker::runChecks(const Arguments& /*args*/) { // --- 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 + // than 1 core. 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 cpu_fail = (effective_cores >= 0 && effective_cores < 1); bool mem_fail = (hotspot && upper > 0 && lower > upper); if (!cpu_fail && !mem_fail) {