Skip to content
16 changes: 16 additions & 0 deletions ddprof-lib/src/main/cpp/arguments.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
// bare 'nosanity' with no value means skip checks
_skip_sanity_checks = true;
}

CASE("nativemem")
_nativemem = value == NULL ? 0 : parseUnits(value, BYTES);
if (_nativemem < 0) {
Expand Down
2 changes: 2 additions & 0 deletions ddprof-lib/src/main/cpp/arguments.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions ddprof-lib/src/main/cpp/os.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
314 changes: 314 additions & 0 deletions ddprof-lib/src/main/cpp/os_linux.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <link.h>
#include <pthread.h>
#include <sched.h>
Expand Down Expand Up @@ -695,6 +696,319 @@ int OS::getCpuCount() {
return sysconf(_SC_NPROCESSORS_ONLN);
}

// 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;
}

// 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;
}

// 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", &quota, &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;
}

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.
if (getOwnCgroupPath("cpu", subpath, sizeof(subpath))) {
const char* base = "/sys/fs/cgroup/cpu";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve v1 cgroup CPU mounts before probing quota

When the process is on cgroup v1 with the CPU controller mounted as cpu,cpuacct (or any non-/sys/fs/cgroup/cpu mount), getOwnCgroupPath("cpu") still matches the controller entry from /proc/self/cgroup, but this hard-coded base probes /sys/fs/cgroup/cpu/.../cpu.cfs_quota_us. That file does not exist in the combined hierarchy, so getCgroupCpuMillicores() returns -1 and the new core sanity check ignores sub-core quotas; resolve the controller mount from /proc/self/mountinfo before appending the cgroup path.

Useful? React with 👍 / 👎.

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;
}

// 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() {
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);
Expand Down
8 changes: 8 additions & 0 deletions ddprof-lib/src/main/cpp/os_macos.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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; // 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);
Expand Down
Loading
Loading