-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathlivenessTracker.cpp
414 lines (352 loc) · 12.3 KB
/
livenessTracker.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
/*
* Copyright 2021, 2023 Datadog, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <algorithm>
#include <random>
#include <set>
#include <thread>
#include "arch.h"
#include "context.h"
#include "incbin.h"
#include "jniHelper.h"
#include "livenessTracker.h"
#include "log.h"
#include "os.h"
#include "profiler.h"
#include "thread.h"
#include "tsc.h"
#include "vmStructs.h"
#include <jni.h>
#include <string.h>
constexpr int LivenessTracker::MAX_TRACKING_TABLE_SIZE;
constexpr int LivenessTracker::MIN_SAMPLING_INTERVAL;
void LivenessTracker::cleanup_table(bool forced) {
u64 current = loadAcquire(_last_gc_epoch);
u64 target_gc_epoch = loadAcquire(_gc_epoch);
if ((target_gc_epoch == _last_gc_epoch ||
!__sync_bool_compare_and_swap(&_last_gc_epoch, current,
target_gc_epoch)) &&
!forced) {
// if the last processed GC epoch hasn't changed, or if we failed to update
// it, there's nothing to do
return;
}
JNIEnv *env = VM::jni();
int epoch_diff = (int)(target_gc_epoch - current);
_table_lock.lock();
u32 sz = _table_size;
if (sz > 0) {
u64 start = OS::nanotime(), end;
u32 newsz = 0;
std::set<jclass> kept_classes;
for (u32 i = 0; i < sz; i++) {
if (_table[i].ref != nullptr &&
!env->IsSameObject(_table[i].ref, nullptr)) {
// it survived one more GarbageCollectionFinish event
u32 target = newsz++;
if (target != i) {
_table[target] = _table[i]; // will clone TrackingEntry at 'i'
_table[i].ref = nullptr; // will nullify the original ref
assert(_table[i].frames == _table[target].frames);
_table[i].frames = nullptr; // will nullify the original frames
assert(_table[target].frames != nullptr);
}
assert(_table[target].ref != nullptr &&
_table[target].frames != nullptr);
_table[target].age += epoch_diff;
} else {
jweak tmpRef = _table[i].ref;
_table[i].ref = nullptr;
env->DeleteWeakGlobalRef(tmpRef);
jvmtiFrameInfo *tmpFrames = _table[i].frames;
_table[i].frames = nullptr;
assert(_table[i].ref == nullptr && _table[i].frames == nullptr);
delete[] tmpFrames;
}
}
_table_size = newsz;
end = OS::nanotime();
Log::debug("Liveness tracker cleanup took %.2fms (%.2fus/element)",
1.0f * (end - start) / 1000 / 1000,
1.0f * (end - start) / 1000 / sz);
}
_table_lock.unlock();
}
void LivenessTracker::flush(std::set<int> &tracked_thread_ids) {
if (!_enabled) {
// disabled
return;
}
flush_table(&tracked_thread_ids);
}
void LivenessTracker::flush_table(std::set<int> *tracked_thread_ids) {
JNIEnv *env = VM::jni();
u64 start = OS::nanotime(), end;
// make sure that the tracking table is cleaned up before we start flushing it
// this is to make sure we are including as few false 'live' objects as
// possible
cleanup_table();
_table_lock.lock();
u32 sz;
for (int i = 0; i < (sz = _table_size); i++) {
jobject ref = env->NewLocalRef(_table[i].ref);
if (ref != nullptr) {
assert(_table[i].frames != nullptr);
if (tracked_thread_ids != nullptr) {
tracked_thread_ids->insert(_table[i].tid);
}
ObjectLivenessEvent event;
event._start_time = _table[i].time;
event._age = _table[i].age;
event._alloc = _table[i].alloc;
event._skipped = _table[i].skipped;
event._ctx = _table[i].ctx;
jclass clz = env->GetObjectClass(ref);
jstring name_str = (jstring)env->CallObjectMethod(clz, _Class_getName);
env->DeleteLocalRef(clz);
jniExceptionCheck(env);
const char *name = env->GetStringUTFChars(name_str, nullptr);
event._id = name != nullptr
? Profiler::instance()->lookupClass(name, strlen(name))
: 0;
env->ReleaseStringUTFChars(name_str, name);
Profiler::instance()->recordExternalSample(
1, _table[i].tid, _table[i].frames, _table[i].frames_size,
/*truncated=*/false, BCI_LIVENESS, &event);
}
env->DeleteLocalRef(ref);
}
_table_lock.unlock();
if (_record_heap_usage) {
bool isLastGc = HeapUsage::isLastGCUsageSupported();
size_t used = isLastGc ? HeapUsage::get()._used_at_last_gc
: loadAcquire(_used_after_last_gc);
if (used == 0) {
used = HeapUsage::get()._used;
isLastGc = false;
}
Profiler::instance()->writeHeapUsage(used, isLastGc);
}
end = OS::nanotime();
if (sz) {
Log::debug("Liveness tracker flush took %.2fms (%.2fus/element)",
1.0f * (end - start) / 1000 / 1000,
1.0f * (end - start) / 1000 / sz);
}
}
Error LivenessTracker::initialize_table(JNIEnv *jni, int sampling_interval) {
_table_max_cap = 0;
jlong max_heap = HeapUsage::getMaxHeap(jni);
if (max_heap == -1) {
return Error("Can not track liveness for allocation samples without heap "
"size information.");
}
int required_table_capacity =
sampling_interval > 0 ? max_heap / sampling_interval : max_heap;
if (required_table_capacity > MAX_TRACKING_TABLE_SIZE) {
Log::warn("Tracking liveness for allocation samples with interval %d can "
"not cover full heap.",
sampling_interval);
}
_table_max_cap = std::min(MAX_TRACKING_TABLE_SIZE, required_table_capacity);
_table_cap = std::max(
2048,
_table_max_cap /
8); // the table will grow at most 3 times before fully covering heap
return Error::OK;
}
Error LivenessTracker::start(Arguments &args) {
Error err = initialize(args);
if (err) {
return err;
}
if (!_enabled) {
// disabled
return Error::OK;
}
// Enable Java Object Sample events
jvmtiEnv *jvmti = VM::jvmti();
jvmti->SetEventNotificationMode(
JVMTI_ENABLE, JVMTI_EVENT_GARBAGE_COLLECTION_FINISH, nullptr);
return Error::OK;
}
void LivenessTracker::stop() {
if (!_enabled) {
// disabled
return;
}
cleanup_table();
flush_table(nullptr);
// do not disable GC notifications here - the tracker is supposed to survive
// multiple recordings
}
static int _min(int a, int b) { return a < b ? a : b; }
Error LivenessTracker::initialize(Arguments &args) {
_enabled = args._gc_generations || args._record_liveness;
if (!_enabled) {
return Error::OK;
}
if (_initialized) {
// if the tracker was previously initialized return the stored result for
// consistency this hack also means that if the profiler is started with
// different arguments for liveness tracking those will be ignored it is
// required in order to be able to track the object liveness across many
// recordings
return _stored_error;
}
_initialized = true;
if (VM::hotspot_version() < 11) {
Log::warn("Liveness tracking requires Java 11+");
// disable liveness tracking
_table_max_cap = 0;
return _stored_error = Error::OK;
}
JNIEnv *env = VM::jni();
Error err = initialize_table(env, args._memory);
if (err) {
Log::warn("Liveness tracking requires heap size information");
// disable liveness tracking
_table_max_cap = 0;
return _stored_error = Error::OK;
}
if (!(_Class = env->FindClass("java/lang/Class"))) {
jniExceptionCheck(env, true);
err = Error("Unable to find java/lang/Class");
} else if (!(_Class_getName = env->GetMethodID(_Class, "getName",
"()Ljava/lang/String;"))) {
jniExceptionCheck(env, true);
err = Error("Unable to find java/lang/Class.getName");
}
if (err) {
Log::warn("Liveness tracking requires access to java.lang.Class#getName()");
// disable liveness tracking
_table_max_cap = 0;
return _stored_error = Error::OK;
}
_subsample_ratio = args._live_samples_ratio;
_table_size = 0;
_table_cap =
std::min(2048, _table_max_cap); // with default 512k sampling interval, it's
// enough for 1G of heap
_table = (TrackingEntry *)malloc(sizeof(TrackingEntry) * _table_cap);
_record_heap_usage = args._record_heap_usage;
_gc_epoch = 0;
_last_gc_epoch = 0;
return _stored_error = Error::OK;
}
void LivenessTracker::track(JNIEnv *env, AllocEvent &event, jint tid,
jobject object, int num_frames,
jvmtiFrameInfo *frames) {
if (!_enabled) {
// disabled
return;
}
if (_table_max_cap == 0) {
// we are not to store any objects
return;
}
static thread_local std::mt19937 gen(std::random_device{}());
static thread_local std::uniform_real_distribution<> dis(0, 1.0);
static thread_local double skipped = 0;
if (_subsample_ratio < 1.0 && dis(gen) > _subsample_ratio) {
skipped += static_cast<double>(event._weight) * event._size;
return;
}
jweak ref = env->NewWeakGlobalRef(object);
if (ref == nullptr) {
return;
}
bool retried = false;
retry:
if (!_table_lock.tryLockShared()) {
// we failed to add the weak reference to the table so it won't get cleaned
// up otherwise
env->DeleteWeakGlobalRef(ref);
return;
}
// Increment _table_size in a thread-safe manner (CAS) and store the new value
// in idx It bails out if _table_size would overflow _table_cap
int idx;
do {
idx = __atomic_load_n(&_table_size, __ATOMIC_RELAXED);
} while (idx < _table_cap &&
!__sync_bool_compare_and_swap(&_table_size, idx, idx + 1));
if (idx < _table_cap) {
_table[idx].tid = tid;
_table[idx].time = TSC::ticks();
_table[idx].ref = ref;
_table[idx].alloc = event;
_table[idx].skipped = skipped;
_table[idx].age = 0;
_table[idx].frames_size = num_frames;
_table[idx].frames = new jvmtiFrameInfo[_table[idx].frames_size];
if (frames != nullptr) {
memcpy(_table[idx].frames, frames,
sizeof(jvmtiFrameInfo) * _table[idx].frames_size);
}
_table[idx].ctx = Contexts::get(tid);
}
_table_lock.unlockShared();
if (idx == _table_cap) {
if (!retried) {
// guarantees we don't busy loop until memory exhaustion
retried = true;
// try cleanup before resizing - there is a good chance it will free some
// space
cleanup_table(true);
if (_table_cap < _table_max_cap) {
// Let's increase the size of the table
// This should only ever happen when sampling interval * size of table
// is smaller than maximum heap size. So we only support increasing
// the size of the table, not decreasing it.
_table_lock.lock();
// Only increase the size of the table to _table_max_cap elements
int newcap = std::min(_table_cap * 2, _table_max_cap);
if (_table_cap != newcap) {
TrackingEntry *tmp = (TrackingEntry *)realloc(
_table, sizeof(TrackingEntry) * (_table_cap = newcap));
if (tmp != nullptr) {
_table = tmp;
Log::debug(
"Increased size of Liveness tracking table to %d entries",
_table_cap);
} else {
Log::debug("Cannot add sampled object to Liveness tracking table, "
"resize attempt failed, the table is overflowing");
}
}
_table_lock.unlock();
goto retry;
} else {
Log::debug("Cannot add sampled object to Liveness tracking table, it's "
"overflowing");
}
}
}
skipped = 0; // reset the subsampling skipped bytes
}
void JNICALL LivenessTracker::GarbageCollectionFinish(jvmtiEnv *jvmti_env) {
LivenessTracker::instance()->onGC();
}
void LivenessTracker::onGC() {
if (!_initialized) {
return;
}
// just increment the epoch
atomicInc(_gc_epoch, 1);
if (!HeapUsage::isLastGCUsageSupported()) {
storeRelease(_used_after_last_gc, HeapUsage::get(false)._used);
}
}