diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt index c05b9c9ab2..06bd64d4dd 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt @@ -149,7 +149,13 @@ object ConfigurationPresets { config.compilerArgs.set( listOf("-O0", "-g", "-DDEBUG") + commonLinuxCompilerArgs(version) ) - config.linkerArgs.set(commonLinuxLinkerArgs()) + // -z,nodelete: same as configureRelease. Without it, the JVM (observed + // on OpenJ9's DestroyJavaVM path) can unmap this library while a + // profiler-spawned thread (via the patched pthread_create hook in + // LibraryPatcher) is still executing wrapper code inside it, causing a + // SIGSEGV with no hs_err. nodelete pins the mapping for the process's + // lifetime no matter how many times the host dlcloses it. + config.linkerArgs.set(commonLinuxLinkerArgs() + listOf("-Wl,-z,nodelete")) } Platform.MACOS -> { config.compilerArgs.set( diff --git a/ddprof-lib/src/main/cpp/arguments.cpp b/ddprof-lib/src/main/cpp/arguments.cpp index b43f99fccb..93b562bca8 100644 --- a/ddprof-lib/src/main/cpp/arguments.cpp +++ b/ddprof-lib/src/main/cpp/arguments.cpp @@ -410,6 +410,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 { + // A bare 'nosanity' with no value skips the checks. + _skip_sanity_checks = true; + } + CASE("nativemem") _nativemem = value == NULL ? 0 : parseUnits(value, BYTES); if (_nativemem < 0) { diff --git a/ddprof-lib/src/main/cpp/arguments.h b/ddprof-lib/src/main/cpp/arguments.h index 16efe9c8ba..08bd90e572 100644 --- a/ddprof-lib/src/main/cpp/arguments.h +++ b/ddprof-lib/src/main/cpp/arguments.h @@ -193,6 +193,7 @@ class Arguments { bool _lightweight; bool _enable_method_cleanup; bool _remote_symbolication; // Enable remote symbolication for native frames + bool _skip_sanity_checks; bool _jvmtistacks; // Delegate CPU/wall stack walks to HotSpot JFR RequestStackTrace extension bool _nativesocket; long _nativesocket_interval; // initial sampling period in nanoseconds; 0 = engine default @@ -234,6 +235,7 @@ class Arguments { _lightweight(false), _enable_method_cleanup(true), _remote_symbolication(false), + _skip_sanity_checks(false), _jvmtistacks(false), _nativesocket(false), _nativesocket_interval(0), diff --git a/ddprof-lib/src/main/cpp/flightRecorder.cpp b/ddprof-lib/src/main/cpp/flightRecorder.cpp index 301ef662c5..4432fdc692 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.cpp +++ b/ddprof-lib/src/main/cpp/flightRecorder.cpp @@ -1212,6 +1212,12 @@ void Recording::writeSettings(Buffer *buf, Arguments &args) { Log::LEVEL_NAME[Log::level()]); writeBoolSetting(buf, T_ACTIVE_RECORDING, "hotspot", VM::isHotspot()); writeBoolSetting(buf, T_ACTIVE_RECORDING, "openj9", VM::isOpenJ9()); + writeBoolSetting(buf, T_ACTIVE_RECORDING, "sanityCheckFailed", + Profiler::instance()->sanityCheckFailed()); + if (Profiler::instance()->sanityCheckFailed()) { + writeStringSetting(buf, T_ACTIVE_RECORDING, "sanityCheckDetail", + Profiler::instance()->sanityCheckMessage()); + } for (auto attribute : args._context_attributes) { writeStringSetting(buf, T_ACTIVE_RECORDING, "contextattribute", attribute.c_str()); diff --git a/ddprof-lib/src/main/cpp/os.h b/ddprof-lib/src/main/cpp/os.h index 6d50420ec9..a3d0283819 100644 --- a/ddprof-lib/src/main/cpp/os.h +++ b/ddprof-lib/src/main/cpp/os.h @@ -210,6 +210,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 ab59d8f195..bc01fbc40b 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,6 +696,328 @@ int OS::getCpuCount() { return sysconf(_SC_NPROCESSORS_ONLN); } +// Matches `controller` only as a whole, comma-delimited token in +// `controllers` (e.g. "cpu" matches "cpu,cpuacct" but not "cpuacct" or +// "cpuset"). Substring matching would give false positives because several +// v1 controller names share the "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 the caller reads limits 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, this function +// 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; +} + +// 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 when `path` +// already is the mount root. +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; +} + +// 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; +} + +// Walks ancestors the same way as walkCgroupV2CpuMillicores(), but reads +// the cgroup v1 CPU controller's separate quota and period files instead +// of a single "cpu.max". +// +// "/sys/fs/cgroup/cpu" assumes the systemd-created "cpu -> cpu,cpuacct" +// compatibility symlink; /proc/self/cgroup only confirms that "cpu" is a +// controller token, not the real mount directory name. On a non-systemd +// runtime without that symlink this falls through to "unconstrained" +// instead of finding the real quota. +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; +} + +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); + } + } + } + } + + // Fall back to cgroup v1, likewise resolved from the process's own path. + // See walkCgroupV1CpuMillicores() for the systemd-symlink assumption this base path makes. + 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 -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; +} + +// Walks ancestors the same way as walkCgroupV2MemoryLimit(), but reads the +// cgroup v1 memory controller's limit file instead. +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() { + 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); + } + } + } + } + + // 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); + } + } + } + } + + 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 0aee027040..805a11b84f 100644 --- a/ddprof-lib/src/main/cpp/os_macos.cpp +++ b/ddprof-lib/src/main/cpp/os_macos.cpp @@ -376,6 +376,14 @@ int OS::getCpuCount() { return sysctlbyname("hw.logicalcpu", &cpu_count, &size, NULL, 0) == 0 ? cpu_count : 1; } +int OS::getCgroupCpuMillicores() { + return -1; // macOS has no cgroup support. +} + +long OS::getContainerMemoryLimit() { + return -1; // macOS has no cgroup support. +} + 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 ee6f13c315..558d991563 100644 --- a/ddprof-lib/src/main/cpp/profiler.cpp +++ b/ddprof-lib/src/main/cpp/profiler.cpp @@ -40,6 +40,7 @@ #include "wallClock.h" #include "wallClockCounters.h" #include "frames.h" +#include "sanityCheck.h" #include #include @@ -1406,6 +1407,30 @@ Error Profiler::start(Arguments &args, bool reset) { return error; } + // Sanity checks run at most once per process, across start and stop cycles. + // Profiler::start() sets sanity_checked to true before it checks + // _skip_sanity_checks, not after. If it set the flag only when the checks + // ran, a nosanity start would leave sanity_checked false. A later start + // without nosanity would then run the checks unexpectedly and could fail. + // + // A failed check does not abort startup. The resource estimate is + // inherently approximate, so Profiler::start() logs a warning and records + // the failure as a JFR setting (see Recording::writeSettings) instead of + // refusing to profile. + static bool sanity_checked = false; + if (!sanity_checked) { + sanity_checked = true; + if (!args._skip_sanity_checks) { + Error sanity_result = SanityChecker::runChecks(args); + if (sanity_result) { + _sanity_check_failed = true; + _sanity_check_message = sanity_result.message(); + LOG_WARN("Continuing to start profiler despite failed sanity check " + "(see JFR settings for details)."); + } + } + } + error = checkJvmCapabilities(); if (error) { return error; diff --git a/ddprof-lib/src/main/cpp/profiler.h b/ddprof-lib/src/main/cpp/profiler.h index 357506a721..a9563bd0a9 100644 --- a/ddprof-lib/src/main/cpp/profiler.h +++ b/ddprof-lib/src/main/cpp/profiler.h @@ -147,6 +147,10 @@ class alignas(alignof(SpinLock)) Profiler { u32 _num_context_attributes; bool _omit_stacktraces; bool _remote_symbolication; // Enable remote symbolication for native frames + bool _sanity_check_failed; + // _sanity_check_message points into SanityChecker's static error buffer. + // That buffer has process lifetime, so the pointer stays valid. + const char *_sanity_check_message; // dlopen() hook support void **_dlopen_entry; @@ -225,7 +229,8 @@ class alignas(alignof(SpinLock)) Profiler { _max_stack_depth(0), _features(), _safe_mode(0), _cstack(CSTACK_NO), _thread_events_state(JVMTI_DISABLE), _libs(Libraries::instance()), _num_context_attributes(0), _omit_stacktraces(false), - _remote_symbolication(false), _dlopen_entry(NULL) { + _remote_symbolication(false), _sanity_check_failed(false), + _sanity_check_message(NULL), _dlopen_entry(NULL) { for (int i = 0; i < CONCURRENCY_LEVEL; i++) { _calltrace_buffer[i] = NULL; @@ -454,6 +459,8 @@ class alignas(alignof(SpinLock)) Profiler { void writeHeapUsage(long value, bool live); int eventMask() const { return _event_mask; } bool isRemoteSymbolication() const { return _remote_symbolication; } + bool sanityCheckFailed() const { return _sanity_check_failed; } + const char *sanityCheckMessage() const { return _sanity_check_message; } const void *resolveSymbol(const char *name); const char *getLibraryName(const char *native_symbol); diff --git a/ddprof-lib/src/main/cpp/sanityCheck.cpp b/ddprof-lib/src/main/cpp/sanityCheck.cpp new file mode 100644 index 0000000000..0f649e0cd6 --- /dev/null +++ b/ddprof-lib/src/main/cpp/sanityCheck.cpp @@ -0,0 +1,152 @@ +/* + * Copyright 2026, Datadog, Inc. + * 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::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 the error message. This is safe because Profiler::start() + // holds _state_lock for the whole call and never runs runChecks() concurrently + // from two threads. + 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); + + // -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 (effective_cores < 0 || 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; + + // 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 unbounded (max_uintx) on standard HotSpot + // builds, so the flag is present and the default_val fallback above never + // triggers. The exact sentinel value is not 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 is not a real limit, so this normalizes on that instead. + if (metaspace_max > upper) { + metaspace_max = DEFAULT_METASPACE; + } + + 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; + 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 --- + // The profiler refuses to run with fewer 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 < 1); + bool mem_fail = (hotspot && 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/ExternalLauncher.java b/ddprof-test/src/test/java/com/datadoghq/profiler/ExternalLauncher.java index 695412dcb0..cb83816df5 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/ExternalLauncher.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/ExternalLauncher.java @@ -23,6 +23,9 @@ *
  • profiler [comma delimited profiler command list] - starts the profiler
  • *
  • profiler-work: [comma delimited profiler command list] - starts the profiler and runs a CPU-intensive task
  • *
  • profiler-virtual-thread - calls {@link JavaProfiler#getInstance()} for the first time from a virtual thread
  • + *
  • profiler-sequence [';'-delimited steps] - runs a sequence of start/stop calls in this + * process; each step is either the literal {@code STOP} (calls {@link JavaProfiler#stop()}) + * or a comma delimited profiler command list (calls {@link JavaProfiler#execute(String)})
  • * */ public class ExternalLauncher { @@ -66,6 +69,17 @@ public static void main(String[] args) throws Exception { instance.execute(commands); } } + } else if (args[0].equals("profiler-sequence")) { + JavaProfiler instance = JavaProfiler.getInstance(); + if (args.length == 2) { + for (String step : args[1].split(";")) { + if (step.equals("STOP")) { + instance.stop(); + } else if (!step.isEmpty()) { + instance.execute(step); + } + } + } } else if (args[0].startsWith("profiler-work:")) { long expectedCpuTime = Long.parseLong(args[0].substring("profiler-work:".length())); ThreadMXBean thrdBean = ManagementFactory.getThreadMXBean(); 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..0d1b783e25 --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/sanity/SanityCheckTest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.datadoghq.profiler.sanity; + +import com.datadoghq.profiler.AbstractProcessProfilerTest; +import com.datadoghq.profiler.JfrEvent; +import com.datadoghq.profiler.JfrEvents; +import com.datadoghq.profiler.Platform; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * {@code Profiler::start()}'s sanity-check guard ({@code sanity_checked} in {@code profiler.cpp}) + * is a function-local static with process lifetime, not reset by {@code profiler.stop()}. Every + * test here therefore forks a fresh JVM ({@link #launch}) rather than calling + * {@code JavaProfiler.getInstance()} in-process — an in-process test would share that static flag + * (and the {@code Profiler} singleton) with every other test in the same test JVM, so whichever + * test happens to run first would silently decide the outcome for the rest. + */ +public class SanityCheckTest extends AbstractProcessProfilerTest { + + private Path newJfrPath(String prefix) throws Exception { + Path rootDir = Paths.get("/tmp/recordings"); + Files.createDirectories(rootDir); + return Files.createTempFile(rootDir, prefix, ".jfr"); + } + + /** + * nosanity=true bypasses sanity checks. The profiler must start successfully on any host. + */ + @Test + void nosanity_bypasses_checks() throws Exception { + Path jfrDump = newJfrPath("sanity-check-test"); + try { + LaunchResult result = launch("profiler", Collections.emptyList(), + "start,cpu=10ms,jfr,file=" + jfrDump.toAbsolutePath() + ",nosanity", + line -> LineConsumerResult.CONTINUE, line -> LineConsumerResult.CONTINUE); + assertTrue(result.inTime, "forked JVM did not exit in time"); + assertEquals(0, result.exitCode, "forked JVM exited with a non-zero code"); + } finally { + Files.deleteIfExists(jfrDump); + } + } + + /** + * The override flag works regardless of value form (bare keyword vs explicit true). + */ + @Test + void nosanity_explicit_true_bypasses_checks() throws Exception { + Path jfrDump = newJfrPath("sanity-check-test"); + try { + LaunchResult result = launch("profiler", Collections.emptyList(), + "start,cpu=10ms,jfr,file=" + jfrDump.toAbsolutePath() + ",nosanity=true", + line -> LineConsumerResult.CONTINUE, line -> LineConsumerResult.CONTINUE); + assertTrue(result.inTime, "forked JVM did not exit in time"); + assertEquals(0, result.exitCode, "forked JVM exited with a non-zero code"); + } finally { + Files.deleteIfExists(jfrDump); + } + } + + /** + * 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 { + Path jfrDump1 = newJfrPath("sanity-check-test"); + Path jfrDump2 = newJfrPath("sanity-check-test"); + try { + // First start with nosanity to guarantee success regardless of host resources. + // 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. + String sequence = "start,cpu=10ms,jfr,file=" + jfrDump1.toAbsolutePath() + ",nosanity" + + ";STOP;" + + "start,cpu=10ms,jfr,file=" + jfrDump2.toAbsolutePath(); + LaunchResult result = launch("profiler-sequence", Collections.emptyList(), sequence, + line -> LineConsumerResult.CONTINUE, line -> LineConsumerResult.CONTINUE); + assertTrue(result.inTime, "forked JVM did not exit in time"); + assertEquals(0, result.exitCode, "forked JVM exited with a non-zero code"); + } finally { + Files.deleteIfExists(jfrDump1); + Files.deleteIfExists(jfrDump2); + } + } + + /** + * A forced -Xmx far larger than any real host's RAM makes the memory sanity check fail + * deterministically, regardless of the actual host resources. The check is advisory, so + * the profiler must still start, and the JFR recording's settings must show the failure. + */ + @Test + void mem_sanity_check_failure_is_recorded_in_jfr() throws Exception { + // SanityChecker::runChecks() skips the memory check on OpenJ9/Zing, where VMFlag + // lookups are unavailable. An oversized -Xmx alone cannot force a failure there. + assumeFalse(Platform.isJ9() || Platform.isZing()); + // OS::getRamSize() is a stub that always returns 0 on macOS (os_macos.cpp). The + // zero return value keeps the memory check's upper bound at 0, so the check + // always passes on macOS. + assumeTrue(Platform.isLinux()); + + Path rootDir = Paths.get("/tmp/recordings"); + Files.createDirectories(rootDir); + Path forkedJfr = Files.createTempFile(rootDir, "sanity-check-mem-fail", ".jfr"); + try { + // On Linux, Parallel GC's (the JDK 8 default) initial-heap commit is aligned + // to a fraction of -Xmx regardless of -Xms, so -Xms8m alone still eagerly + // commits ~28g and OOMs before the sanity check runs. G1 sizes its initial + // commit in fixed-size regions independent of -Xmx, avoiding that. + LaunchResult result = launch("profiler", Arrays.asList("-XX:+UseG1GC", "-Xmx900g", "-Xms8m"), + "start,jfr,file=" + forkedJfr.toAbsolutePath(), + line -> LineConsumerResult.CONTINUE, line -> LineConsumerResult.CONTINUE); + assertTrue(result.inTime, "forked JVM did not exit in time"); + assertEquals(0, result.exitCode, "forked JVM exited with a non-zero code"); + + JfrEvents settings = JfrEvents.load(forkedJfr, "jdk.ActiveSetting"); + boolean sawFailed = false; + boolean sawDetail = false; + for (JfrEvent item : settings) { + String name = item.getString("name"); + if ("sanityCheckFailed".equals(name)) { + assertEquals("true", item.getString("value")); + sawFailed = true; + } else if ("sanityCheckDetail".equals(name)) { + assertTrue(item.getString("value").startsWith("[sanity]")); + sawDetail = true; + } + } + assertTrue(sawFailed, "sanityCheckFailed setting not found in JFR recording"); + assertTrue(sawDetail, "sanityCheckDetail setting not found in JFR recording"); + } finally { + Files.deleteIfExists(forkedJfr); + } + } +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/MegamorphicCallTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/MegamorphicCallTest.java index d55bd4f524..8970611e8c 100644 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/MegamorphicCallTest.java +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/MegamorphicCallTest.java @@ -74,7 +74,9 @@ private int profiledWork(int iterations, Calculator... calculators) { @RetryingTest(5) public void testITableStubs() { - Assumptions.assumeFalse(Platform.isZing() || Platform.isJ9()); + // itable stub frames are HotSpot-specific; GraalVM's JIT doesn't generate the + // same stub layout, as with the other itable/vtable stub tests in this suite. + Assumptions.assumeFalse(Platform.isZing() || Platform.isJ9() || Platform.isGraal()); registerCurrentThreadForWallClockProfiling(); // Reduce workload under ASAN: combined with the coarser wall rate above, this // bounds the number of samples (and thus the stack-trace strings materialized diff --git a/doc/architecture/SanityChecks.md b/doc/architecture/SanityChecks.md new file mode 100644 index 0000000000..3adfdf3355 --- /dev/null +++ b/doc/architecture/SanityChecks.md @@ -0,0 +1,251 @@ +# Pre-Start Sanity Checks + +`SanityChecker::runChecks()` (`ddprof-lib/src/main/cpp/sanityCheck.cpp`) checks, +on `Profiler::start()`, whether the host is too resource-constrained to run the +profiler safely — insufficient CPU headroom, or insufficient memory headroom +for the JVM's own configured footprint plus the profiler's overhead. A failed +check does not abort startup: the resource estimate is inherently approximate, +so `Profiler::start()` logs a warning and records the failure for the running +JFR recording instead of refusing to profile. + +--- + +## Integration point + +```mermaid +flowchart TD + start["Profiler::start(args)"] --> lock["MutexLocker ml(_state_lock)"] + lock --> checkState["checkState()"] + checkState -->|error| ret1["return error"] + checkState -->|ok| skip{"args._skip_sanity_checks?"} + skip -->|true| cap["checkJvmCapabilities()"] + skip -->|false| cached{"sanity_checked?"} + cached -->|"no: run once"| run["sanity_result = SanityChecker::runChecks(args)"] + run --> setflag["sanity_checked = true"] + setflag --> gate{"sanity_result?"} + cached -->|"yes: skip"| cap + gate -->|error| record["_sanity_check_failed = true; LOG_WARN(...)"] + gate -->|ok| cap + record --> cap +``` + +(`ddprof-lib/src/main/cpp/profiler.cpp`, `Profiler::start()`.) The check runs +immediately after `checkState()` and before `checkJvmCapabilities()`, but a +failing check does not return early — `Profiler::start()` always continues to +`checkJvmCapabilities()` and the rest of startup. + +### One-time check + +```cpp +static bool sanity_checked = false; +if (!sanity_checked) { + sanity_checked = true; + if (!args._skip_sanity_checks) { + Error sanity_result = SanityChecker::runChecks(args); + if (sanity_result) { + _sanity_check_failed = true; + _sanity_check_message = sanity_result.message(); + LOG_WARN("Continuing to start profiler despite failed sanity check " + "(see JFR settings for details)."); + } + } +} +``` + +`sanity_checked` is a function-local static with no lock of its own. This is +safe **only** because every call to this block happens while the caller +already holds `_state_lock` (`MutexLocker ml(_state_lock)` at the top of +`Profiler::start()`) — the lock is what makes "check exactly once" hold, not +anything inside `runChecks()` itself. The check runs at most once per process, +on the assumption that CPU/memory availability does not change during a +profiler session. Adding a new call path to `SanityChecker::runChecks()` that +is not under `_state_lock` (or is not otherwise the sole caller) would break +the once-only guarantee. + +`_sanity_check_failed` and `_sanity_check_message` are `Profiler` instance +fields (`ddprof-lib/src/main/cpp/profiler.h`), read back when the JFR +recording writes its settings (see below). `_sanity_check_message` points into +`SanityChecker::runChecks()`'s static `err_buf`, which has process lifetime, so +storing the raw pointer instead of copying the string is safe. + +### JFR settings + +`Recording::writeSettings()` (`ddprof-lib/src/main/cpp/flightRecorder.cpp`) +writes the check result into every recording's `T_ACTIVE_RECORDING` settings +event, alongside existing settings like `hotspot` and `openj9`: + +```cpp +writeBoolSetting(buf, T_ACTIVE_RECORDING, "sanityCheckFailed", + Profiler::instance()->sanityCheckFailed()); +if (Profiler::instance()->sanityCheckFailed()) { + writeStringSetting(buf, T_ACTIVE_RECORDING, "sanityCheckDetail", + Profiler::instance()->sanityCheckMessage()); +} +``` + +`sanityCheckFailed` is always written; `sanityCheckDetail` (the `[sanity]` +message described below) is written only when the check failed, so a healthy +host's recordings carry no extra string data. + +### `nosanity` flag + +`Arguments::_skip_sanity_checks` (`ddprof-lib/src/main/cpp/arguments.h`) +disables both checks. Parsed in `Arguments::parse()` +(`ddprof-lib/src/main/cpp/arguments.cpp`, `CASE("nosanity")`): + +```cpp +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; +} +``` + +A bare `nosanity` keyword, or any value not starting with `n`/`f`/`0` +(e.g. `true`, `yes`, `1`), skips the checks; `nosanity=no`, `nosanity=false`, +`nosanity=0` explicitly keep them enabled. + +--- + +## Checks performed + +`SanityChecker::runChecks()` gathers system info up front, then evaluates two +independent conditions; either one failing produces a non-OK `Error`. + +### CPU check + +``` +effective_cores = logical_cpus +if cgroup_millicores > 0: + effective_cores = min(effective_cores, cgroup_millicores / 1000) + +cpu_fail = (effective_cores < 1) +``` + +`logical_cpus` comes from `OS::getCpuCount()`; `cgroup_millicores` from +`OS::getCgroupCpuMillicores()`. + +### Memory check + +``` +upper = ram_size > 128MB ? (ram_size - 128MB) : 0 +if container_memory_limit > 0 and container_memory_limit < upper: + upper = container_memory_limit + +gc_overhead = heap_max * 30 / 100 +lower = heap_max + metaspace_max + codecache + gc_overhead + + (thread_count * stack_size) + 64MB # PROFILER_OVERHEAD + +mem_fail = (upper > 0 and lower > upper) +``` + +`heap_max`, `metaspace_max`, `codecache`, and `stack_size` are read from the +live JVM via `getVMSizeFlag()`, which looks up a `VMFlag` by name +(`MaxHeapSize`, `MaxMetaspaceSize`, `ReservedCodeCacheSize`, +`ThreadStackSize`) and falls back to a fixed default if the flag cannot be +resolved: + +| Flag | Default if unresolved | +|---|---| +| `MaxMetaspaceSize` | 256 MB | +| `ReservedCodeCacheSize` | 240 MB | +| `ThreadStackSize` | 512 KB | +| thread count (`OS::getBasicProcessInfo()`) | 200 | + +`heap_max` has no fallback default — an unresolved `MaxHeapSize` flag is +treated as `0`, which also zeroes `gc_overhead` (30% of `heap_max`). + +`getVMSizeFlag()` requires `vmStructs.inline.h` to be included so that +`VMFlag::addr()` is available in release builds. It is an inline accessor, +not part of the exported symbol set otherwise. The type list also includes +`Intx`, because `ThreadStackSize` is declared as `intx` on standard HotSpot +builds — omitting it would make that lookup always fall back to +`default_val`: + +```cpp +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::Type::Intx}); + if (f != NULL && f->addr() != NULL) { + return *static_cast(f->addr()); + } + return default_val; +} +``` + +### Cgroup v1/v2 support (Linux only) + +`OS::getCgroupCpuMillicores()` and `OS::getContainerMemoryLimit()` +(`ddprof-lib/src/main/cpp/os_linux.cpp`) try cgroup v2 first, then fall back to +v1: + +- **CPU v2**: reads `/sys/fs/cgroup/cpu.max` (`" "`, or the + literal string `"max"` for unconstrained) and converts to millicores as + `quota * 1000 / period`. +- **CPU v1**: reads `/sys/fs/cgroup/cpu/cpu.cfs_quota_us` and + `/sys/fs/cgroup/cpu/cpu.cfs_period_us` (defaulting the period to 100ms if + unreadable). +- **Memory v2**: reads `/sys/fs/cgroup/memory.max` (`"max"` for unconstrained). +- **Memory v1**: reads `/sys/fs/cgroup/memory/memory.limit_in_bytes`. A value + at or above `0x7ffffffffffff000` (the platform's effectively-unbounded + cgroup v1 limit) is treated as unconstrained. + +Any missing or unreadable file, or the `"max"` sentinel, causes these +functions to return `-1`, which the memory/CPU formulas above treat as +"unconstrained" (no cgroup ceiling applied). On macOS +(`ddprof-lib/src/main/cpp/os_macos.cpp`) both functions unconditionally return +`-1`, because there is no cgroup support on that platform. + +`OS::getRamSize()` also unconditionally returns `0` on macOS +(`ddprof-lib/src/main/cpp/os_macos.cpp`), so `upper` is always `0` there and +the memory check formula's `upper > 0` guard always disables the check. +Physical RAM and logical CPU count constrain the checks only on Linux. + +--- + +## Error / telemetry format + +On failure, `runChecks()` builds a single structured message (prefixed +`[sanity]`) into a `runChecks`-local `static char err_buf[1024]` (safe under +the same one-call-at-a-time contract as the check above) and returns it as an +`Error`. `Profiler::start()` stores the message pointer in +`_sanity_check_message` for the JFR settings write described above. This is +safe because `err_buf` has static storage duration, so the pointer stays +valid for the life of the process: + +``` +[sanity] cpu=,memory=, +logical_cores=,cgroup_millicores=,effective_cores=, +ram_mb=,container_limit_mb=,upper_mb=,lower_mb=, +heap_mb=,metaspace_mb=,codecache_mb=, +gc_overhead_mb=,threads=,stack_kb=,profiler_mb=, +containerized= +``` + +`containerized` is `true` if either `cgroup_millicores > 0` or +`container_memory_limit > 0`. The key=value shape is meant to be parsed by +downstream telemetry (e.g. dd-trace-java) without re-running the profiler to +get diagnostic context. + +--- + +## Files + +- `ddprof-lib/src/main/cpp/sanityCheck.h` — `SanityChecker::runChecks()` declaration. +- `ddprof-lib/src/main/cpp/sanityCheck.cpp` — check logic and error-message formatting. +- `ddprof-lib/src/main/cpp/os_linux.cpp` — `OS::getCgroupCpuMillicores()`, `OS::getContainerMemoryLimit()` (cgroup v1/v2). +- `ddprof-lib/src/main/cpp/os_macos.cpp` — unconstrained (`-1`) stubs for the same two functions. +- `ddprof-lib/src/main/cpp/arguments.h`, `arguments.cpp` — `_skip_sanity_checks` field and `nosanity` flag parsing. +- `ddprof-lib/src/main/cpp/profiler.h`, `profiler.cpp` — `Profiler::start()` integration, the `_sanity_check_failed`/`_sanity_check_message` fields, and their accessors. +- `ddprof-lib/src/main/cpp/flightRecorder.cpp` — `Recording::writeSettings()` writes the `sanityCheckFailed`/`sanityCheckDetail` JFR settings. +- `ddprof-test/src/test/java/com/datadoghq/profiler/sanity/SanityCheckTest.java` — `nosanity` bypass tests, the run-once-across-stop/start test, and a forked-JVM test that forces the memory check to fail and asserts the JFR settings. The forked-JVM test runs on Linux only, because the macOS RAM stub described above always disables the memory check.