Skip to content
Merged
90 changes: 90 additions & 0 deletions ddprof-lib/src/main/cpp/arguments.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "arguments.h"
#include "vmEntry.h"

#include <algorithm>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
Expand Down Expand Up @@ -81,6 +82,24 @@ static const Multiplier UNIVERSAL[] = {
// and keep the liveness track of 10% of the allocation
// samples
// generations - track surviving generations
// referencechains[=BOOL[:hops=N][:budget=N][:ttl=N][:framecap=N][:pausetarget=N][:painbudget=N][:firstpassbudget=N]]
// - (off by default) tag/BFS-walk live-heap
// samples' referrer chains back toward a GC root.
// pausetarget=N (ms) is the pause-time-SLO ceiling
// the engine's pacing controller adapts the effective
// budget/cadence toward. painbudget=N (percent)
// bounds how much
// wall-clock time a *restarted* search (one begun
// after a prior search already completed/abandoned)
// may spend on average (the restarted-search pain budget).
// firstpassbudget=N overrides just the search's
// one-shot, root-seeded first pass's edge budget
// (default 0 - the engine auto-scales it from
// budget=N instead) since that pass alone
// decides which GC roots ever enter the frontier at
// all, unlike every later pass's cheap, incremental
// per-node expansion.
// Sub-options are placeholders pending future tuning
// lightweight[=BOOL] - enable lightweight profiling - events without
// stacktraces (default: true)
// remotesym[=BOOL] - enable remote symbolication for native frames
Expand Down Expand Up @@ -444,6 +463,77 @@ Error Arguments::parse(const char *args) {
_nativesocket = true;
}

CASE("referencechains")
{
// Sub-options are colon-delimited key=value pairs after the boolean,
// e.g. "referencechains=true:hops=64:budget=2000". Parsed manually
// (not via strtok) because the outer arg loop above is itself mid
// strtok(..., ",") over the same buffer - a nested strtok call would
// clobber its saved state.
char *config = value ? strchr(value, ':') : nullptr;
if (config) {
*(config++) = 0;
}
if (value != NULL) {
switch (value[0]) {
case 'n': // no
case 'f': // false
case '0': // 0
_reference_chains = false;
break;
default:
_reference_chains = true;
}
} else {
_reference_chains = true;
}
char *cursor = config;
while (cursor != NULL) {
char *next = strchr(cursor, ':');
if (next) {
*(next++) = 0;
}
char *eq = strchr(cursor, '=');
if (eq) {
*(eq++) = 0;
// Floor every sub-option (and ceiling-clamp hops/budget/framecap)
// here: a negative hops value would wrap to ~4e9 as u32 and
// silently disable the hop cap, a negative budget truncates every
// pass to nothing, and unbounded values flow straight into loop
// bounds or the frontier table's allocation. One validation boundary
// for all sub-options instead of scattered downstream clamps.
if (strcasecmp(cursor, "hops") == 0) {
_reference_chains_hop_cap =
std::min(std::max(atoi(eq), 1), MAX_REFERENCE_CHAINS_HOP_CAP);
_reference_chains_tuned_mask |= REF_CHAINS_TUNED_HOP_CAP;
} else if (strcasecmp(cursor, "budget") == 0) {
_reference_chains_budget =
std::min(std::max(atoi(eq), 1), MAX_REFERENCE_CHAINS_BUDGET);
_reference_chains_tuned_mask |= REF_CHAINS_TUNED_BUDGET;
} else if (strcasecmp(cursor, "ttl") == 0) {
_reference_chains_ttl_ms = std::max(atol(eq), 0L);
_reference_chains_tuned_mask |= REF_CHAINS_TUNED_TTL;
} else if (strcasecmp(cursor, "framecap") == 0) {
_reference_chains_frontier_cap = std::min(
std::max(atoi(eq), 1), MAX_REFERENCE_CHAINS_FRONTIER_CAP);
_reference_chains_tuned_mask |= REF_CHAINS_TUNED_FRONTIER_CAP;
} else if (strcasecmp(cursor, "pausetarget") == 0) {
_reference_chains_pause_target_ms = std::max(atol(eq), 0L);
_reference_chains_tuned_mask |= REF_CHAINS_TUNED_PAUSE_TARGET;
} else if (strcasecmp(cursor, "painbudget") == 0) {
_reference_chains_pain_budget_percent =
std::min(std::max(atoi(eq), 0), 100);
_reference_chains_tuned_mask |= REF_CHAINS_TUNED_PAIN_BUDGET;
} else if (strcasecmp(cursor, "firstpassbudget") == 0) {
_reference_chains_first_pass_budget = std::min(
std::max(atoi(eq), 0), MAX_REFERENCE_CHAINS_FIRST_PASS_BUDGET);
_reference_chains_tuned_mask |= REF_CHAINS_TUNED_FIRST_PASS_BUDGET;
}
}
cursor = next;
}
}

DEFAULT()
if (_unknown_arg == NULL)
_unknown_arg = arg;
Expand Down
85 changes: 85 additions & 0 deletions ddprof-lib/src/main/cpp/arguments.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,71 @@
#include <string>
#include <vector>

#include "arch.h"

const long DEFAULT_CPU_INTERVAL = 10 * 1000 * 1000; // 10 ms
const long DEFAULT_WALL_INTERVAL = 50 * 1000 * 1000; // 50 ms
const long DEFAULT_ALLOC_INTERVAL = 524287; // 512 KiB
const int DEFAULT_WALL_THREADS_PER_TICK = 16;
const int DEFAULT_JSTACKDEPTH = 2048;

// Every constant below is a provisional default pending empirical tuning -
// none are backed by a benchmark run against this codebase; replacing them
// with measured values is outstanding JMH/async-profiler benchmark work.
// Per-constant notes only carry what a name does not already say.
//
// Hop cap: mirrors HotSpot's own JFR leak-profiler chain cap (~200 hops,
// split 100/100 from leaf and from root) - the closest real-world precedent
// for a referrer-type chain length.
// Sub-option bitmask for the auto-tuner: tracks which referencechains
// sub-options were explicitly set by the operator, so the auto-tuner
// only overrides defaults that weren't.
constexpr u8 REF_CHAINS_TUNED_HOP_CAP = 1 << 0;
constexpr u8 REF_CHAINS_TUNED_BUDGET = 1 << 1;
constexpr u8 REF_CHAINS_TUNED_TTL = 1 << 2;
constexpr u8 REF_CHAINS_TUNED_FRONTIER_CAP = 1 << 3;
constexpr u8 REF_CHAINS_TUNED_PAUSE_TARGET = 1 << 4;
constexpr u8 REF_CHAINS_TUNED_PAIN_BUDGET = 1 << 5;
constexpr u8 REF_CHAINS_TUNED_FIRST_PASS_BUDGET = 1 << 6;

const int DEFAULT_REFERENCE_CHAINS_HOP_CAP = 200;
// Per-pass edge budget: a round middle value keeping a single
// FollowReferences-triggered safepoint short without forcing an impractical
// number of passes.
const int DEFAULT_REFERENCE_CHAINS_BUDGET = 1000; // edges expanded per BFS pass
// Per-search TTL: bounds a slow or stalled search to a human-noticeable
// lifetime.
const long DEFAULT_REFERENCE_CHAINS_TTL_MS = 60000; // per-search wall-clock TTL
// Frontier-size cap: same order of magnitude as LivenessTracker's tuned
// ceiling (MAX_TRACKING_TABLE_SIZE = 262144, livenessTracker.h), quartered -
// a frontier entry is smaller but per-hop fan-out can be large. Conservative
// guess, pending a frontier peak-occupancy measurement.
const int DEFAULT_REFERENCE_CHAINS_FRONTIER_CAP = 65536; // max live frontier entries per search
// Pause-time-SLO ceiling (pause-time pacing controller): target ceiling,
// per pass, on wall-clock time spent inside the safepoint-triggering
// FollowReferences/GetObjectsWithTags call
// the pause-time pacing controller adapts the effective budget/cadence toward.
const long DEFAULT_REFERENCE_CHAINS_PAUSE_TARGET_MS = 50; // ms per pass
// Pain budget refill rate (the restarted-search pain budget):
// percent (1 = 1%) of wall-clock time a *restarted* search may spend inside
// safepointing calls, on average, before a later restart waits for the
// previous search's debt to drain.
const int DEFAULT_REFERENCE_CHAINS_PAIN_BUDGET_PERCENT = 1;
// First-pass edge budget override for the search's one-and-only root-seeded
// FollowReferences call: unlike the per-pass budget, this spends once per
// search, so a much larger one-time ceiling is affordable. 0 means no
// override - the engine auto-scales it from the per-pass budget at startup.
const int DEFAULT_REFERENCE_CHAINS_FIRST_PASS_BUDGET = 0;
const int MAX_REFERENCE_CHAINS_FIRST_PASS_BUDGET =
DEFAULT_REFERENCE_CHAINS_BUDGET * 1000;
// Upper clamps: large enough that no legitimate configuration hits them,
// small enough to fail a mistyped value safely instead of feeding it into a
// loop bound or an allocation.
const int MAX_REFERENCE_CHAINS_HOP_CAP = DEFAULT_REFERENCE_CHAINS_HOP_CAP * 1000;
const int MAX_REFERENCE_CHAINS_BUDGET = DEFAULT_REFERENCE_CHAINS_BUDGET * 1000;
const int MAX_REFERENCE_CHAINS_FRONTIER_CAP =
DEFAULT_REFERENCE_CHAINS_FRONTIER_CAP * 1000;

const char *const EVENT_NOOP = "noop";
const char *const EVENT_CPU = "cpu";
const char *const EVENT_ALLOC = "alloc";
Expand Down Expand Up @@ -177,6 +236,23 @@ class Arguments {
double _live_samples_ratio;
bool _record_heap_usage;
bool _gc_generations;
// Reference-chain tracking. Read by the reference-chain engine at startup
// to size the frontier table and seed the per-search hop/budget/TTL
// tunables and the pause-time-SLO ceiling its pacing controller adapts
// the effective budget/cadence toward.
bool _reference_chains;
int _reference_chains_hop_cap;
int _reference_chains_budget;
long _reference_chains_ttl_ms;
int _reference_chains_frontier_cap;
long _reference_chains_pause_target_ms;
int _reference_chains_pain_budget_percent;
int _reference_chains_first_pass_budget;
// Bitmask of REF_CHAINS_TUNED_*: which sub-options were explicitly
// set by the operator, so the auto-tuner knows which defaults it may
// override. 0 = all defaults, none explicitly set.
u8 _reference_chains_tuned_mask;
// Explicit opt-in for the legacy whole-graph JVMTI FollowReferences walk.
long _nativemem;
int _jstackdepth;
int _safe_mode;
Expand Down Expand Up @@ -219,6 +295,15 @@ class Arguments {
_live_samples_ratio(0.1), // default to liveness-tracking 10% of the allocation samples
_record_heap_usage(false),
_gc_generations(false),
_reference_chains(false),
_reference_chains_hop_cap(DEFAULT_REFERENCE_CHAINS_HOP_CAP),
_reference_chains_budget(DEFAULT_REFERENCE_CHAINS_BUDGET),
_reference_chains_ttl_ms(DEFAULT_REFERENCE_CHAINS_TTL_MS),
_reference_chains_frontier_cap(DEFAULT_REFERENCE_CHAINS_FRONTIER_CAP),
_reference_chains_pause_target_ms(DEFAULT_REFERENCE_CHAINS_PAUSE_TARGET_MS),
_reference_chains_pain_budget_percent(DEFAULT_REFERENCE_CHAINS_PAIN_BUDGET_PERCENT),
_reference_chains_first_pass_budget(DEFAULT_REFERENCE_CHAINS_FIRST_PASS_BUDGET),
_reference_chains_tuned_mask(0),
_nativemem(-1),
_jstackdepth(DEFAULT_JSTACKDEPTH),
_safe_mode(0),
Expand Down
75 changes: 75 additions & 0 deletions ddprof-lib/src/main/cpp/event.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
#include <cstring>
#include <memory>
#include <stdint.h>
#include <string>
#include <vector>
using namespace std;

#define MAX_STRING_LEN 8191
Expand Down Expand Up @@ -87,9 +89,82 @@ class ObjectLivenessEvent : public Event {
u64 _skipped;
u64 _start_time;
u64 _age;
// 0 = untagged; leak tag from LivenessTracker pool. Default-initialized so
// every construction path (current and future) serializes a defined value -
// flush_table() overwrites it from the entry's own tag (track() zeroes it
// at insert).
int64_t leak_tag = 0;
Context _ctx;
};

// Reporting surface for the reference-chain engine's bounded BFS.
// `_target_tag` is the frontier tag the chain was reconstructed for;
// `_hops`
// holds the reconstructed chain in the same leaf(target)-to-root order:
// hop[i].klass_id is the referrer-klass StringDictionary id and
// hop[i].edge_label is the edge by which hop[i] is retained by its parent
// (the field name of its parent hop for FIELD/STATIC_FIELD edges, the
// edge-kind label otherwise, resolved by the collector filling this event).
// For a static-field-rooted chain the root-side end is the static field's
// holder instance followed by the declaring class (the ROOT TYPE, appended
// by the collector from the root-attached entry's declaring-class tag) -
// the chain then reads, root-first, as the root type retaining the holder
// through its static field, on down to the target. `_depth` is the target
// entry's own frontier depth (hop count from the search's root-side
// seed). `_root_kind` is the jvmtiHeapReferenceKind of whichever edge first
// admitted this chain into the frontier - labels *why* the
// chain is reachable at all (JNI global, thread stack, static field, ...),
// written out as a string (Recording::recordReferenceChain(),
// flightRecorder.cpp) rather than a synthetic node in `_hops` itself, since
// that array is a T_CLASS cpool array with no room for a non-class
// placeholder.
// Byte cap for one hop's retention-edge label in ReferenceChainHop
// (label resolution truncates to this; recordReferenceChain() reserves
// against it) - a shared constant so the collector and the writer cannot
// drift apart on the worst-case event size.
static constexpr size_t MAX_REFERENCE_CHAIN_EDGE_LABEL = 96;

// One retained hop of a reconstructed chain. edge_label is empty when label
// resolution is unavailable (partial mock environments); production events
// carry either all labels or none (canary events), so the writer treats a
// single empty label as "no labels at all".
struct ReferenceChainHop {
u32 klass_id;
std::string edge_label;
};

class ReferenceChainEvent : public Event {
public:
u64 _start_time;
u64 _target_tag;
u32 _depth;
u8 _root_kind;
std::vector<ReferenceChainHop> _hops;

ReferenceChainEvent()
: Event(), _start_time(0), _target_tag(0), _depth(0), _root_kind(0) {}
};

// Search-level abandonment signal: reports why the reference-chain search
// stopped before every frontier entry could be resolved, using the same
// counters the search itself already maintains.
class ReferenceChainAbandonedEvent : public Event {
public:
u64 _start_time;
u8 _reason; // SearchAbandonReason (the engine's reason enum)
u32 _passes_run;
u32 _frontier_size;
int _hop_cap;
int _budget;
long _ttl_ms;
u64 _elapsed_ns;

ReferenceChainAbandonedEvent()
: Event(), _start_time(0), _reason(0), _passes_run(0),
_frontier_size(0), _hop_cap(0), _budget(0), _ttl_ms(0),
_elapsed_ns(0) {}
};

class MallocEvent : public Event {
public:
u64 _start_time;
Expand Down
8 changes: 8 additions & 0 deletions ddprof-lib/src/main/cpp/faultInjection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@

#include <stdint.h>
void crashNow() {
#if defined(__clang_analyzer__)
// The null-pointer store below is the deliberate, never-returning crash,
// but clang scan-build flags writing through a null pointer. Model the
// stop with a trap instead - the analyzer treats __builtin_trap() as a
// halt, so no false path past the crash is explored.
__builtin_trap();
#else
volatile uintptr_t* p = (volatile uintptr_t*)nullptr;
*p = 0xBAD;
__builtin_unreachable(); // the store above never returns.
#endif
}
#endif

Expand Down
Loading
Loading