Skip to content

Commit 8e16a79

Browse files
authored
Merge branch 'main' into jb/resolve_method
2 parents 4a9fba4 + 4e0e178 commit 8e16a79

20 files changed

Lines changed: 1199 additions & 67 deletions

.gitlab/reliability/chaos_check.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ case $ALLOCATOR in
123123
tcmalloc)
124124
echo "Running with tcmalloc"
125125
export LD_PRELOAD=$(find /usr/lib/ -name 'libtcmalloc_minimal.so.4')
126+
# thread-churn/dump-storm antagonists cycle many short-lived threads;
127+
# tcmalloc's defaults are slow to return their per-thread caches to the
128+
# OS, which was inflating container RSS past the OOM limit on aarch64.
129+
export TCMALLOC_RELEASE_RATE=10
130+
export TCMALLOC_AGGRESSIVE_DECOMMIT=1
126131
;;
127132
jemalloc)
128133
echo "Running with jemalloc"

README.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,23 +169,40 @@ By default, the script clones the repository at the current commit for clean bui
169169
# Force rebuild of base image only (useful after Alpine/Ubuntu updates)
170170
./utils/run-containers-tests.sh --libc=musl --rebuild-base
171171

172+
# Preview all supported musl cells (sanitizer configs are skipped, matching CI policy)
173+
./utils/run-containers-tests.sh --matrix --libc=musl
174+
175+
# Run all supported musl cells without an interactive prompt
176+
./utils/run-containers-tests.sh --matrix --libc=musl --run
177+
178+
# Preview selected JDKs across every supported libc/architecture pair
179+
./utils/run-containers-tests.sh --libc=all --jdk=8,17,21 --arch=all
180+
181+
# Run all OpenJ9 cells
182+
./utils/run-containers-tests.sh --matrix --jdk=j9 --run
183+
172184
# Show options
173185
./utils/run-containers-tests.sh --help
174186
```
175187

176188
Supported options:
177-
- `--libc=glibc|musl` (default: glibc)
178-
- `--jdk=8|11|17|21|25|8-j9|11-j9|17-j9|21-j9|17-graal|21-graal|25-graal` (default: 21)
179-
- `--arch=x64|aarch64` (default: auto-detect)
180-
- `--config=debug|release|asan|tsan` (default: debug)
189+
- `--libc=glibc|musl|all[,..]` (default: glibc)
190+
- `--jdk=8|11|17|21|25|8-j9|11-j9|17-j9|21-j9|17-graal|21-graal|25-graal|regular|j9|graal|all[,..]` (default: 21)
191+
- `--arch=x64|aarch64|all[,..]` (default: auto-detect)
192+
- `--config=debug|release|asan|tsan|all[,..]` (default: debug)
181193
- `--container=podman|docker` (default: podman)
182194
- `--tests="TestPattern"`
183195
- `--gtest` (enable C++ gtests, disabled by default for faster runs)
184-
- `--gtest-task=Task` (run one C++ gtest task; accepts `elfparser_ut` or a full task path like `:ddprof-lib:gtestAsan_elfparser_ut`)
196+
- `--gtest-task=Task` (run one C++ gtest task; matrix runs require a short name like `elfparser_ut`, while single-cell runs also accept a full task path like `:ddprof-lib:gtestAsan_elfparser_ut`)
185197
- `--shell` (interactive shell instead of running tests)
186198
- `--mount` (mount local repo instead of cloning - faster but may have stale artifacts)
187199
- `--rebuild` (force rebuild of all container images)
188200
- `--rebuild-base` (force rebuild of base image only)
201+
- `--matrix` (preview a full test matrix; unset dimensions expand to `all`)
202+
- `--run` (execute an inferred matrix without prompting)
203+
- `--fail-fast` (stop matrix execution on first failure)
204+
205+
Single-value commands run one configuration immediately. When any dimension expands to multiple cells, the script prints a compact status table first; interactive terminals ask for confirmation, while non-interactive runs require `--run` to execute. Matrix execution prints the status table again after all cells finish and writes summaries to `build/reports/container-matrix/summary.md` and `build/reports/container-matrix/summary.json`. Cells not run because of `--fail-fast` are reported as cancelled separately from unsupported cells that are skipped.
189206

190207
## Unwinding Validation Tool
191208

ddprof-lib/src/main/cpp/counters.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
X(DICTIONARY_ENDPOINTS_ARENA_WASTE_BYTES, "dictionary_endpoints_arena_waste_bytes") \
4343
X(DICTIONARY_CONTEXT_ARENA_WASTE_BYTES, "dictionary_context_arena_waste_bytes") \
4444
X(DICTIONARY_DRAIN_TIMEOUTS, "dictionary_drain_timeouts") \
45+
X(CLASS_MAP_AT_CAPACITY, "class_map_at_capacity_drops") \
4546
X(CONTEXT_STORAGE_BYTES, "context_storage_bytes") \
4647
X(CONTEXT_STORAGE_PAGES, "context_storage_pages") \
4748
X(CONTEXT_BOUNDS_MISS_INITS, "context_bounds_miss_inits") \

ddprof-lib/src/main/cpp/flightRecorder.cpp

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ SharedLineNumberTable::~SharedLineNumberTable() {
6868

6969
void Lookup::fillNativeMethodInfo(MethodInfo *mi, const char *name,
7070
const char *lib_name) {
71-
mi->_class = _classes->lookupDuringDump("", 0);
71+
mi->_class = _classes->lookupDuringDump("", 0, Profiler::maxClassMapSize());
7272
// TODO return the library name once we figured out how to cooperate with the
7373
// backend
7474
// if (lib_name == NULL) {
@@ -117,7 +117,8 @@ void Lookup::fillNativeMethodInfo(MethodInfo *mi, const char *name,
117117

118118
void Lookup::fillRemoteFrameInfo(MethodInfo *mi, const RemoteFrameInfo *rfi) {
119119
// Store build-id in the class name field
120-
mi->_class = _classes->lookupDuringDump(rfi->build_id, strlen(rfi->build_id));
120+
mi->_class = _classes->lookupDuringDump(rfi->build_id, strlen(rfi->build_id),
121+
Profiler::maxClassMapSize());
121122

122123
// Store PC offset in hex format in the signature field
123124
char offset_hex[32];
@@ -269,35 +270,40 @@ void Lookup::fillJavaMethodInfo(MethodInfo *mi, jmethodID method,
269270
"Ljdk/internal/reflect/GeneratedConstructorAccessor")) {
270271
class_name_id = _classes->lookupDuringDump(
271272
"jdk/internal/reflect/GeneratedConstructorAccessor",
272-
strlen("jdk/internal/reflect/GeneratedConstructorAccessor"));
273+
strlen("jdk/internal/reflect/GeneratedConstructorAccessor"),
274+
Profiler::maxClassMapSize());
273275
method_name_id =
274276
_symbols.lookup("Object "
275277
"jdk.internal.reflect.GeneratedConstructorAccessor."
276278
"newInstance(Object[])");
277279
method_sig_id = _symbols.lookup(method_sig);
278280
} else if (has_prefix(class_name,
279281
"Lsun/reflect/GeneratedConstructorAccessor")) {
280-
class_name_id =
281-
_classes->lookupDuringDump("sun/reflect/GeneratedConstructorAccessor",
282-
strlen("sun/reflect/GeneratedConstructorAccessor"));
282+
class_name_id = _classes->lookupDuringDump(
283+
"sun/reflect/GeneratedConstructorAccessor",
284+
strlen("sun/reflect/GeneratedConstructorAccessor"),
285+
Profiler::maxClassMapSize());
283286
method_name_id = _symbols.lookup(
284287
"Object "
285288
"sun.reflect.GeneratedConstructorAccessor.newInstance(Object[])");
286289
method_sig_id = _symbols.lookup(method_sig);
287290
} else if (has_prefix(class_name,
288291
"Ljdk/internal/reflect/GeneratedMethodAccessor")) {
289-
class_name_id =
290-
_classes->lookupDuringDump("jdk/internal/reflect/GeneratedMethodAccessor",
291-
strlen("jdk/internal/reflect/GeneratedMethodAccessor"));
292+
class_name_id = _classes->lookupDuringDump(
293+
"jdk/internal/reflect/GeneratedMethodAccessor",
294+
strlen("jdk/internal/reflect/GeneratedMethodAccessor"),
295+
Profiler::maxClassMapSize());
292296
method_name_id =
293297
_symbols.lookup("Object "
294298
"jdk.internal.reflect.GeneratedMethodAccessor."
295299
"invoke(Object, Object[])");
296300
method_sig_id = _symbols.lookup(method_sig);
297301
} else if (has_prefix(class_name,
298302
"Lsun/reflect/GeneratedMethodAccessor")) {
299-
class_name_id = _classes->lookupDuringDump("sun/reflect/GeneratedMethodAccessor",
300-
strlen("sun/reflect/GeneratedMethodAccessor"));
303+
class_name_id = _classes->lookupDuringDump(
304+
"sun/reflect/GeneratedMethodAccessor",
305+
strlen("sun/reflect/GeneratedMethodAccessor"),
306+
Profiler::maxClassMapSize());
301307
method_name_id = _symbols.lookup(
302308
"Object sun.reflect.GeneratedMethodAccessor.invoke(Object, "
303309
"Object[])");
@@ -308,30 +314,38 @@ void Lookup::fillJavaMethodInfo(MethodInfo *mi, jmethodID method,
308314
// we want to normalise to java/lang/invoke/LambdaForm$MH,
309315
// java/lang/invoke/LambdaForm$DMH, java/lang/invoke/LambdaForm$BMH,
310316
if (has_prefix(class_name + lambdaFormPrefixLength, "MH")) {
311-
class_name_id = _classes->lookupDuringDump("java/lang/invoke/LambdaForm$MH",
312-
strlen("java/lang/invoke/LambdaForm$MH"));
317+
class_name_id = _classes->lookupDuringDump(
318+
"java/lang/invoke/LambdaForm$MH",
319+
strlen("java/lang/invoke/LambdaForm$MH"),
320+
Profiler::maxClassMapSize());
313321
} else if (has_prefix(class_name + lambdaFormPrefixLength, "BMH")) {
314-
class_name_id = _classes->lookupDuringDump("java/lang/invoke/LambdaForm$BMH",
315-
strlen("java/lang/invoke/LambdaForm$BMH"));
322+
class_name_id = _classes->lookupDuringDump(
323+
"java/lang/invoke/LambdaForm$BMH",
324+
strlen("java/lang/invoke/LambdaForm$BMH"),
325+
Profiler::maxClassMapSize());
316326
} else if (has_prefix(class_name + lambdaFormPrefixLength, "DMH")) {
317-
class_name_id = _classes->lookupDuringDump("java/lang/invoke/LambdaForm$DMH",
318-
strlen("java/lang/invoke/LambdaForm$DMH"));
327+
class_name_id = _classes->lookupDuringDump(
328+
"java/lang/invoke/LambdaForm$DMH",
329+
strlen("java/lang/invoke/LambdaForm$DMH"),
330+
Profiler::maxClassMapSize());
319331
} else {
320332
// don't recognise the suffix, so don't normalise
321333
class_name_id = _classes->lookupDuringDump(
322-
normalized_class_name, normalized_class_name_len);
334+
normalized_class_name, normalized_class_name_len,
335+
Profiler::maxClassMapSize());
323336
}
324337
method_name_id = _symbols.lookup(method_name);
325338
method_sig_id = _symbols.lookup(method_sig);
326339
} else {
327-
class_name_id = _classes->lookupDuringDump(normalized_class_name,
328-
normalized_class_name_len);
340+
class_name_id = _classes->lookupDuringDump(
341+
normalized_class_name, normalized_class_name_len,
342+
Profiler::maxClassMapSize());
329343
method_name_id = _symbols.lookup(method_name);
330344
method_sig_id = _symbols.lookup(method_sig);
331345
}
332346
} else {
333347
Counters::increment(JMETHODID_SKIPPED);
334-
class_name_id = _classes->lookupDuringDump("", 0);
348+
class_name_id = _classes->lookupDuringDump("", 0, Profiler::maxClassMapSize());
335349
method_name_id = _symbols.lookup("jvmtiError");
336350
method_sig_id = _symbols.lookup("()L;");
337351
}
@@ -441,7 +455,7 @@ bool Lookup::resolveVTableReceiver(VMSymbol *sym, char *buf, size_t bufsize,
441455
// the stack frame's class_id absent from this chunk's class pool.
442456
// (Plain lookup() remains correct for non-dump callers — e.g. Profiler::
443457
// lookupClass on JVM threads — where the next rotate() will propagate.)
444-
u32 class_id = _classes->lookupDuringDump(buf, len);
458+
u32 class_id = _classes->lookupDuringDump(buf, len, Profiler::maxClassMapSize());
445459
// Apply synthetic-accessor/LambdaForm normalisation so that the many
446460
// distinct names HotSpot generates for these families (..Accessor1234,
447461
// LambdaForm$MH/0x...) collapse to one bucket each in the JFR class pool.
@@ -451,32 +465,32 @@ bool Lookup::resolveVTableReceiver(VMSymbol *sym, char *buf, size_t bufsize,
451465
if (has_prefix_n(buf, len,
452466
"jdk/internal/reflect/GeneratedConstructorAccessor")) {
453467
static const char kName[] = "jdk/internal/reflect/GeneratedConstructorAccessor";
454-
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1);
468+
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1, Profiler::maxClassMapSize());
455469
} else if (has_prefix_n(buf, len, "sun/reflect/GeneratedConstructorAccessor")) {
456470
static const char kName[] = "sun/reflect/GeneratedConstructorAccessor";
457-
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1);
471+
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1, Profiler::maxClassMapSize());
458472
} else if (has_prefix_n(buf, len,
459473
"jdk/internal/reflect/GeneratedMethodAccessor")) {
460474
static const char kName[] = "jdk/internal/reflect/GeneratedMethodAccessor";
461-
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1);
475+
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1, Profiler::maxClassMapSize());
462476
} else if (has_prefix_n(buf, len, "sun/reflect/GeneratedMethodAccessor")) {
463477
static const char kName[] = "sun/reflect/GeneratedMethodAccessor";
464-
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1);
478+
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1, Profiler::maxClassMapSize());
465479
} else if (has_prefix_n(buf, len, "java/lang/invoke/LambdaForm$")) {
466480
size_t prefix_len = strlen("java/lang/invoke/LambdaForm$");
467481
const char *suffix = buf + prefix_len;
468482
size_t suffix_len = len - prefix_len;
469483
if (suffix_len >= 2 && suffix[0] == 'M' && suffix[1] == 'H') {
470484
static const char kName[] = "java/lang/invoke/LambdaForm$MH";
471-
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1);
485+
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1, Profiler::maxClassMapSize());
472486
} else if (suffix_len >= 3 && suffix[0] == 'B' && suffix[1] == 'M' &&
473487
suffix[2] == 'H') {
474488
static const char kName[] = "java/lang/invoke/LambdaForm$BMH";
475-
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1);
489+
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1, Profiler::maxClassMapSize());
476490
} else if (suffix_len >= 3 && suffix[0] == 'D' && suffix[1] == 'M' &&
477491
suffix[2] == 'H') {
478492
static const char kName[] = "java/lang/invoke/LambdaForm$DMH";
479-
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1);
493+
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1, Profiler::maxClassMapSize());
480494
}
481495
}
482496
*out_class_id = class_id;
@@ -500,7 +514,7 @@ u32 Lookup::resolveVTableReceiverCached(void *sym) {
500514
// marker instead of an empty class name (which is indistinguishable
501515
// from a parser/encoder error downstream).
502516
static const char kName[] = "<unresolved_vtable_receiver>";
503-
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1);
517+
class_id = _classes->lookupDuringDump(kName, sizeof(kName) - 1, Profiler::maxClassMapSize());
504518
}
505519
_vtable_receiver_cache[sym] = class_id;
506520
return class_id;

ddprof-lib/src/main/cpp/livenessTracker.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,19 @@ void LivenessTracker::flush_table(std::set<int> *tracked_thread_ids) {
117117
env->DeleteLocalRef(clz);
118118
jniExceptionCheck(env);
119119
const char *name = env->GetStringUTFChars(name_str, nullptr);
120-
event._id = name != nullptr
121-
? Profiler::instance()->lookupClass(name, strlen(name))
122-
: 0;
120+
int class_id = name != nullptr
121+
? Profiler::instance()->lookupClass(name, strlen(name))
122+
: 0;
123123
env->ReleaseStringUTFChars(name_str, name);
124124

125-
Profiler::instance()->recordDeferredSample(_table[i].tid, _table[i].call_trace_id, BCI_LIVENESS, &event);
125+
// lookupClass() returns -1 when the class map is at capacity; do not
126+
// assign it to the u32 event id (it would wrap to 0xFFFFFFFF and
127+
// corrupt liveness attribution in the JFR output) — drop the sample
128+
// instead, matching ObjectSampler's convention for the same failure.
129+
if (class_id >= 0) {
130+
event._id = class_id;
131+
Profiler::instance()->recordDeferredSample(_table[i].tid, _table[i].call_trace_id, BCI_LIVENESS, &event);
132+
}
126133
}
127134

128135
env->DeleteLocalRef(ref);

ddprof-lib/src/main/cpp/profiler.cpp

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1832,10 +1832,16 @@ void Profiler::shutdown(Arguments &args) {
18321832
}
18331833

18341834
int Profiler::lookupClass(const char *key, size_t length) {
1835-
// StringDictionary::lookup() is internally thread-safe via _accepting +
1836-
// RefCountGuard; no external lock required (unlike the old Dictionary).
1837-
u32 id = _class_map.lookup(key, length);
1838-
return id != 0 ? static_cast<int>(id) : -1;
1835+
// StringDictionary::bounded_lookup() is internally thread-safe via
1836+
// _accepting + RefCountGuard; no external lock required. Bounded to
1837+
// MAX_CLASS_MAP_SIZE so unbounded distinct-class churn cannot grow this
1838+
// dictionary for the lifetime of the process.
1839+
u32 id = _class_map.bounded_lookup(key, length, MAX_CLASS_MAP_SIZE);
1840+
if (id == 0) {
1841+
Counters::increment(CLASS_MAP_AT_CAPACITY);
1842+
return -1;
1843+
}
1844+
return static_cast<int>(id);
18391845
}
18401846

18411847
int Profiler::status(char* status, int max_len) {

ddprof-lib/src/main/cpp/profiler.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ class alignas(alignof(SpinLock)) Profiler {
8181
NotifyClassUnloadedFunc _notify_class_unloaded_func;
8282
// --
8383

84+
// Caps the number of distinct class names _class_map will ever hold.
85+
// rotate()/clearStandby() carry the full accumulated set forward on every
86+
// JFR chunk (only clearAll() on profiler restart truly resets it), so
87+
// without a cap a workload with unbounded distinct-class churn (e.g. heavy
88+
// dynamic class generation) grows this dictionary for the lifetime of the
89+
// process. 256K distinct classes is far beyond any real application's
90+
// class count.
91+
static const int MAX_CLASS_MAP_SIZE = 1 << 18;
92+
8493
ThreadInfo _thread_info;
8594
StringDictionary _class_map{1};
8695
StringDictionary _string_label_map{2};
@@ -262,6 +271,10 @@ class alignas(alignof(SpinLock)) Profiler {
262271
Engine *wallEngine() { return _wall_engine; }
263272

264273
StringDictionary *classMap() { return &_class_map; }
274+
// Same cap lookupClass() enforces for sample-time inserts; dump-time
275+
// insertions (Lookup::_classes->lookupDuringDump in flightRecorder.cpp)
276+
// must use this too so _class_map cannot grow past it via that path.
277+
static int maxClassMapSize() { return MAX_CLASS_MAP_SIZE; }
265278
SharedLockGuard classMapSharedGuard() { return SharedLockGuard(&_class_map_lock); }
266279
StringDictionary *stringLabelMap() { return &_string_label_map; }
267280
StringDictionary *contextValueMap() { return &_context_value_map; }

ddprof-lib/src/main/cpp/stringDictionary.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "tripleBuffer.h"
1313
#include "arch.h"
1414
#include <atomic>
15+
#include <climits>
1516
#include <map>
1617
#include <stddef.h>
1718
#include <stdlib.h>
@@ -550,7 +551,11 @@ class StringDictionary {
550551
// Resolve a key during the dump phase. Safe to call from the dump thread
551552
// after rotate(); must NOT be called from signal handlers or concurrently
552553
// with another lookupDuringDump call.
553-
u32 lookupDuringDump(const char* key, size_t len) {
554+
// size_limit bounds new insertions the same way bounded_lookup() does:
555+
// once active->size() reaches size_limit, previously-known keys still
556+
// resolve (dump/active hits above), but a genuinely new key returns 0
557+
// instead of growing the dictionary further.
558+
u32 lookupDuringDump(const char* key, size_t len, int size_limit = INT_MAX) {
554559
StringDictionaryBuffer* dump = _rot.dumpBuffer();
555560

556561
u32 id = dump->lookup(key, len);
@@ -571,6 +576,7 @@ class StringDictionary {
571576
StringDictionaryBuffer* active = _rot.active();
572577
RefCountGuard guard(active);
573578
if (!guard.isActive()) return 0;
579+
if (active->size() >= size_limit) return 0;
574580
u32 new_id = nextId();
575581
new_id = active->insert_with_id(key, len, new_id);
576582
if (new_id != 0) dump->insert_with_id(key, len, new_id);

0 commit comments

Comments
 (0)