Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ jobs:
sudo apt update -y
sudo apt remove -y g++
sudo apt autoremove -y
sudo apt install -y curl zip unzip clang make build-essential binutils gdb
sudo apt install -y curl zip unzip clang make build-essential binutils gdb libgtest-dev libgmock-dev
# Install debug symbols for system libraries
sudo apt install -y libc6-dbg
if [[ ${{ matrix.java_version }} =~ "-zing" ]]; then
Expand Down
88 changes: 88 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,18 @@ 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]]
// - (PROF-15341, 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
// ReferenceChainTracker::updatePacing() adapts the
// effective budget/cadence toward (pause-time pacing
// controller). 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 - see PainBudget (painBudget.h).
// Sub-options are placeholders pending future tuning;
// see doc/architecture/LiveHeapReferenceChains*.md
// lightweight[=BOOL] - enable lightweight profiling - events without
// stacktraces (default: true)
// remotesym[=BOOL] - enable remote symbolication for native frames
Expand Down Expand Up @@ -428,6 +441,81 @@ 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 at the parse boundary rather than
// trusting a downstream cast/clamp to make an operator-supplied
// negative value safe: a negative hops value in particular gets
// compared as `depth >= (u32)ctx->hop_cap` (referenceChains.cpp),
// so an unclamped negative wraps to ~4e9 and silently disables
// the hop cap entirely - the opposite of the flag's intent, and
// it removes the one guard that otherwise bounds how long a
// single reference chain (and therefore its
// datadog.ReferenceChain JFR event) can grow. A negative budget
// similarly collapses ReferenceChainTracker::_effective_budget
// to 0 (updatePacing()'s own PID-clamp logic), which truncates
// every pass immediately and leaves the search RUNNING
// (re-walking the whole graph each cadence) until TTL instead of
// making progress. A negative framecap is handed straight to
// FrontierTable's constructor, which floors it to a
// zero-capacity table (that class's own std::max(max_cap, 0)),
// silently disabling tracking rather than erroring. ttl/
// pausetarget/painbudget already have incidental downstream
// clamps (runPass()'s `_ttl_ms > 0` gate, this class's own
// PidController/PainBudget std::max(..., 0) calls) but are
// floored here too so every sub-option's validation lives at one
// boundary instead of being split between here and several
// unrelated call sites.
if (strcasecmp(cursor, "hops") == 0) {
_reference_chains_hop_cap = std::max(atoi(eq), 1);
} else if (strcasecmp(cursor, "budget") == 0) {
_reference_chains_budget = std::max(atoi(eq), 1);
} else if (strcasecmp(cursor, "ttl") == 0) {
_reference_chains_ttl_ms = std::max(atol(eq), 0L);
} else if (strcasecmp(cursor, "framecap") == 0) {
_reference_chains_frontier_cap = std::max(atoi(eq), 1);
} else if (strcasecmp(cursor, "pausetarget") == 0) {
_reference_chains_pause_target_ms = std::max(atol(eq), 0L);
} else if (strcasecmp(cursor, "painbudget") == 0) {
_reference_chains_pain_budget_percent =
std::min(std::max(atoi(eq), 0), 100);
}
}
cursor = next;
}
}

DEFAULT()
if (_unknown_arg == NULL)
_unknown_arg = arg;
Expand Down
81 changes: 81 additions & 0 deletions ddprof-lib/src/main/cpp/arguments.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,67 @@ 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 (see doc/architecture/LiveHeapReferenceChains-ImplementationPlan.md)
// - none of these values are backed by a benchmark run against this
// codebase. Each is chosen conservatively from cited precedent or from the
// shape of an existing, already-tuned subsystem, per the rationale below;
// a future JMH/async-profiler benchmark matrix (see
// doc/architecture/LiveHeapReferenceChains-BenchmarkPlan.md) is the intended
// path to replacing them with measured values.
//
// Hop cap: mirrors HotSpot's own JFR leak-profiler chain cap (~200 hops,
// split 100/100 from leaf and from root), cited in
// doc/architecture/LiveHeapReferenceChains.md's "Approach B" section - the
// closest real-world precedent for "how many hops does a referrer-type
// chain typically need" that this codebase can cite without measuring it
// itself.
const int DEFAULT_REFERENCE_CHAINS_HOP_CAP = 200;
// Per-pass edge budget: no cited precedent gives a number for this (JFR's
// leak profiler does not bound itself by a per-pass edge count - it runs to
// completion inside one already-scheduled GC pause). Chosen as a round,
// conservative middle value intended to keep a single FollowReferences-
// triggered safepoint short without so small a budget that a search needs
// an impractical number of passes to make progress. A future benchmark pass
// should measure per-pass wall-clock pause distribution at this value and adjust.
const int DEFAULT_REFERENCE_CHAINS_BUDGET = 1000; // edges expanded per BFS pass
// Per-search TTL: a conservative round number (one minute) chosen so a
// slow-moving or stalled search is bounded to a human-noticeable but not
// excessive lifetime, in the absence of any measured "passes needed to
// reach a target sample at various depths" data (a future benchmark's stated goal).
const long DEFAULT_REFERENCE_CHAINS_TTL_MS = 60000; // per-search wall-clock TTL
// Frontier-size cap: sized relative to LivenessTracker's own tuned ceiling
// (MAX_TRACKING_TABLE_SIZE = 262144, livenessTracker.h) rather than derived
// from any BFS-specific measurement - the design doc explicitly flags that
// LivenessTracker's allocation-sample-rate sizing formula does not transfer
// to a graph-search frontier (Open Question 2), so this only borrows the
// same order of magnitude, quartered as a conservative starting point since
// a FrontierEntry is smaller than a TrackingEntry but per-hop fan-out could
// still be large. Not a scaled/derived value - just a conservative guess
// pending a future frontier-table 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, doc/architecture/
// LiveHeapReferenceChains-RemainingWorkPlan.md): target ceiling, per pass, on
// wall-clock time spent inside the safepoint-triggering
// FollowReferences/GetObjectsWithTags call
// (ReferenceChainTracker::updatePacing(), referenceChains.cpp). Like every
// other constant in this block this is a round, provisional default with no
// benchmark behind it - picking the real number is explicitly a future
// measurement question (design doc's Open Question 2), not a value to guess
// here; this only exists so the feedback loop this ceiling drives has
// something to target end-to-end before that measurement happens.
const long DEFAULT_REFERENCE_CHAINS_PAUSE_TARGET_MS = 5; // ms per pass
// Pain budget refill rate (ReferenceChainTracker::PainBudget, painBudget.h):
// the fraction of wall-clock time a *restarted* search is allowed to spend
// inside FollowReferences/GetObjectsWithTags safepoints, on average, before a
// later restart must wait for the debt from the previous search's cost to
// drain. Expressed as an integer percent (1 = 1%) for readability - see
// PainBudget's own header comment for why this single ratio needs no
// benchmark-derived tuning the way the per-pass constants above do, only a
// choice of how much background cost is acceptable. Round, provisional
// default like every other constant in this block.
const int DEFAULT_REFERENCE_CHAINS_PAIN_BUDGET_PERCENT = 1;

const char *const EVENT_NOOP = "noop";
const char *const EVENT_CPU = "cpu";
const char *const EVENT_ALLOC = "alloc";
Expand Down Expand Up @@ -177,6 +238,19 @@ class Arguments {
double _live_samples_ratio;
bool _record_heap_usage;
bool _gc_generations;
// Reference-chain tracking (PROF-15341 - see
// doc/architecture/LiveHeapReferenceChains-ImplementationPlan.md and
// -RemainingWorkPlan.md). Read by ReferenceChainTracker::start()
// (referenceChains.cpp) to size the frontier table and seed the per-search
// hop/budget/TTL tunables and the pause-time-SLO ceiling that
// updatePacing() 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;
long _nativemem;
int _jstackdepth;
int _safe_mode;
Expand Down Expand Up @@ -218,6 +292,13 @@ 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),
_nativemem(-1),
_jstackdepth(DEFAULT_JSTACKDEPTH),
_safe_mode(0),
Expand Down
19 changes: 18 additions & 1 deletion ddprof-lib/src/main/cpp/counters.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,24 @@
* paths (delegated and direct) go into SAMPLES_DROPPED_REC_LOCK. */ \
X(JVMTI_STACKS_DROPPED_LOCK, "jvmti_stacks_dropped_lock") \
X(SAMPLES_DROPPED_REC_LOCK, "samples_dropped_rec_lock") \
X(SAMPLES_DROPPED_THREAD_LOCAL, "samples_dropped_thread_local")
X(SAMPLES_DROPPED_THREAD_LOCAL, "samples_dropped_thread_local") \
/* A pending datadog.ReferenceChain event was evicted from \
* ReferenceChainTracker::_pending_chain_events (referenceChains.h) before \
* any dump() drained it - permanently lost, since _emitted_target_tags \
* marks its tag as emitted the moment it is queued, not when it is \
* actually written. See MAX_PENDING_CHAIN_EVENTS' own comment. */ \
X(REFERENCE_CHAIN_EVENTS_DROPPED, "reference_chain_events_dropped") \
/* ReferenceChainTracker::releaseSearchTags() (referenceChains.cpp) failed \
* to call GetObjectsWithTags() for at least one batch - the search's tag \
* release is retried on a later call rather than proceeding, but this \
* counts how often that retry path is taken. */ \
X(REFERENCE_CHAIN_TAG_RELEASE_FAILED, "reference_chain_tag_release_failed") \
/* Profiler::writeReferenceChain() (profiler.cpp) could not acquire a \
* sample-record lock within its bounded retry budget and dropped the \
* already-dequeued datadog.ReferenceChain event - permanently lost, same \
* as REFERENCE_CHAIN_EVENTS_DROPPED above but from the write side rather \
* than the pending-queue side. */ \
X(REFERENCE_CHAIN_WRITE_DROPPED, "reference_chain_write_dropped")
#define X_ENUM(a, b) a,
typedef enum CounterId : int {
DD_COUNTER_TABLE(X_ENUM) DD_NUM_COUNTERS
Expand Down
41 changes: 41 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,7 @@
#include <cstring>
#include <memory>
#include <stdint.h>
#include <vector>
using namespace std;

#define MAX_STRING_LEN 8191
Expand Down Expand Up @@ -89,6 +90,46 @@ class ObjectLivenessEvent : public Event {
Context _ctx;
};

// PROF-15341 Phase 6: reporting surface for ReferenceChainTracker's bounded
// BFS (referenceChains.h/.cpp). `_target_tag` is the FrontierTable tag the
// chain was reconstructed for (FrontierTable::reconstructChain()); `_chain`
// holds the referrer-klass StringDictionary ids it returns, in the same
// leaf(target)-to-root order. `_depth` is the target entry's own
// FrontierEntry::depth (hop count from the search's root-side seed).
class ReferenceChainEvent : public Event {
public:
u64 _start_time;
u64 _target_tag;
u32 _depth;
std::vector<u32> _chain;

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

// Search-level abandonment signal (design doc's Termination section:
// "explicit reporting of abandoned searches ... no silent truncation").
// Unlike ReferenceChainEvent this does not report any one object's chain -
// it reports why ReferenceChainTracker's current search stopped before
// every frontier entry could be resolved, using the same counters
// runPass()/expandFrontier() already maintain (referenceChains.h/.cpp).
class ReferenceChainAbandonedEvent : public Event {
public:
u64 _start_time;
u8 _reason; // SearchAbandonReason (referenceChains.h)
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
Loading
Loading