-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathJavaProfiler.java
More file actions
511 lines (451 loc) · 17.4 KB
/
JavaProfiler.java
File metadata and controls
511 lines (451 loc) · 17.4 KB
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
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
/*
* Copyright 2018 Andrei Pangin
*
* 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.
*/
package com.datadoghq.profiler;
import sun.misc.Unsafe;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Java API for in-process profiling. Serves as a wrapper around
* java-profiler native library. This class is a singleton.
* The first call to {@link #getInstance()} initiates loading of
* libjavaProfiler.so.
*/
public final class JavaProfiler {
static final Unsafe UNSAFE;
static {
Unsafe unsafe = null;
String version = System.getProperty("java.version");
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
} catch (Exception ignore) { }
UNSAFE = unsafe;
}
static final class TSCFrequencyHolder {
/**
* TSC frequency required to convert ticks into seconds
*/
static final long FREQUENCY = tscFrequency0();
}
private static JavaProfiler instance;
private static final int CONTEXT_SIZE = 64;
// must be kept in sync with PAGE_SIZE in context.h
private static final int PAGE_SIZE = 1024;
private static final int SPAN_OFFSET = 0;
private static final int ROOT_SPAN_OFFSET = 8;
private static final int CHECKSUM_OFFSET = 16;
private static final int DYNAMIC_TAGS_OFFSET = 24;
private static final ThreadLocal<Integer> TID = ThreadLocal.withInitial(JavaProfiler::getTid0);
private ByteBuffer[] contextStorage;
private long[] contextBaseOffsets;
private static final ByteBuffer SENTINEL = ByteBuffer.allocate(0);
private JavaProfiler() {
}
/**
* Get a {@linkplain JavaProfiler} instance backed by the bundled native library and using
* the default temp directory as the scratch where the bundled library will be exploded
* before linking.
*/
public static JavaProfiler getInstance() throws IOException {
return getInstance(null, null);
}
/**
* Get a {@linkplain JavaProfiler} instance backed by the bundled native library and using
* the given directory as the scratch where the bundled library will be exploded
* before linking.
* @param scratchDir directory where the bundled library will be exploded before linking
*/
public static JavaProfiler getInstance(String scratchDir) throws IOException {
return getInstance(null, scratchDir);
}
/**
* Get a {@linkplain JavaProfiler} instance backed by the given native library and using
* the given directory as the scratch where the bundled library will be exploded
* before linking.
* @param libLocation the path to the native library to be used instead of the bundled one
* @param scratchDir directory where the bundled library will be exploded before linking; ignored when 'libLocation' is {@literal null}
*/
public static synchronized JavaProfiler getInstance(String libLocation, String scratchDir) throws IOException {
if (instance != null) {
return instance;
}
JavaProfiler profiler = new JavaProfiler();
LibraryLoader.Result result = LibraryLoader.builder().withLibraryLocation(libLocation).withScratchDir(scratchDir).load();
if (!result.succeeded) {
throw new IOException("Failed to load Datadog Java profiler library", result.error);
}
init0();
ActiveBitmap.initialize();
profiler.initializeContextStorage();
instance = profiler;
String maxArenaValue = System.getProperty("ddprof.debug.malloc_arena_max");
if (maxArenaValue != null) {
try {
mallocArenaMax0(Integer.parseInt(maxArenaValue));
} catch (NumberFormatException e) {
System.out.println("[WARN] Invalid value for ddprof.debug.malloc_arena_max: " + maxArenaValue + ". Expecting an integer.");
}
}
return profiler;
}
private void initializeContextStorage() {
if (this.contextStorage == null) {
int maxPages = getMaxContextPages0();
if (maxPages > 0) {
if (UNSAFE != null) {
contextBaseOffsets = new long[maxPages];
// be sure to choose an illegal address as a sentinel value
Arrays.fill(contextBaseOffsets, Long.MIN_VALUE);
} else {
contextStorage = new ByteBuffer[maxPages];
}
}
}
}
/**
* Stop profiling (without dumping results)
*
* @throws IllegalStateException If profiler is not running
*/
public void stop() throws IllegalStateException {
stop0();
}
/**
* Get the number of samples collected during the profiling session
*
* @return Number of samples
*/
public native long getSamples();
/**
* Get profiler agent version, e.g. "1.0"
*
* @return Version string
*/
public String getVersion() {
try {
return execute0("version");
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
public String getStatus() {
return getStatus0();
}
/**
* Execute an agent-compatible profiling command -
* the comma-separated list of arguments described in arguments.cpp
*
* @param command Profiling command
* @return The command result
* @throws IllegalArgumentException If failed to parse the command
* @throws IOException If failed to create output file
*/
public String execute(String command) throws IllegalArgumentException, IllegalStateException, IOException {
if (command == null) {
throw new NullPointerException();
}
return execute0(command);
}
/**
* Records the completion of the trace root
*/
public boolean recordTraceRoot(long rootSpanId, String endpoint, String operation, int sizeLimit) {
return recordTrace0(rootSpanId, endpoint, operation, sizeLimit);
}
/**
* Records the completion of the trace root
*/
@Deprecated
public boolean recordTraceRoot(long rootSpanId, String endpoint, int sizeLimit) {
return recordTrace0(rootSpanId, endpoint, null, sizeLimit);
}
/**
* Add the given thread to the set of profiled threads.
* 'filter' option must be enabled to use this method.
*/
public void addThread() {
if (UNSAFE != null) {
ActiveBitmap.setActive(TID.get(), true);
} else {
filterThread0(true);
}
}
/**
* Remove the given thread to the set of profiled threads.
* 'filter' option must be enabled to use this method.
*/
public void removeThread() {
if (UNSAFE != null) {
ActiveBitmap.setActive(TID.get(), false);
} else {
filterThread0(false);
}
}
/**
* Passing context identifier to a profiler. This ID is thread-local and is dumped in
* the JFR output only. 0 is a reserved value for "no-context".
*
* @param spanId Span identifier that should be stored for current thread
* @param rootSpanId Root Span identifier that should be stored for current thread
*/
public void setContext(long spanId, long rootSpanId) {
int tid = TID.get();
if (UNSAFE != null) {
setContextUnsafe(tid, spanId, rootSpanId);
} else {
setContextByteBuffer(tid, spanId, rootSpanId);
}
}
private void setContextUnsafe(int tid, long spanId, long rootSpanId) {
if (contextBaseOffsets == null) {
return;
}
long pageOffset = getPageUnsafe(tid);
if (pageOffset == 0) {
return;
}
int index = (tid % PAGE_SIZE) * CONTEXT_SIZE;
long base = pageOffset + index;
UNSAFE.putLong(base + SPAN_OFFSET, spanId);
UNSAFE.putLong(base + ROOT_SPAN_OFFSET, rootSpanId);
UNSAFE.putLong(base + CHECKSUM_OFFSET, spanId ^ rootSpanId);
}
private void setContextByteBuffer(int tid, long spanId, long rootSpanId) {
if (contextStorage == null) {
return;
}
ByteBuffer page = getPage(tid);
if (page == SENTINEL) {
return;
}
int index = (tid % PAGE_SIZE) * CONTEXT_SIZE;
page.putLong(index + SPAN_OFFSET, spanId);
page.putLong(index + ROOT_SPAN_OFFSET, rootSpanId);
page.putLong(index + CHECKSUM_OFFSET, spanId ^ rootSpanId);
}
private ByteBuffer getPage(int tid) {
int pageIndex = tid / PAGE_SIZE;
ByteBuffer page = contextStorage[pageIndex];
if (page == null) {
// the underlying page allocation is atomic so we don't care which view we have over it
ByteBuffer buffer = getContextPage0(tid);
if (buffer == null) {
page = SENTINEL;
} else {
page = buffer.order(ByteOrder.LITTLE_ENDIAN);
}
contextStorage[pageIndex] = page;
}
return page;
}
private long getPageUnsafe(int tid) {
int pageIndex = tid / PAGE_SIZE;
long offset = contextBaseOffsets[pageIndex];
if (offset == Long.MIN_VALUE) {
contextBaseOffsets[pageIndex] = offset = getContextPageOffset0(tid);
}
return offset;
}
/**
* Clears context identifier for current thread.
*/
public void clearContext() {
setContext(0, 0);
}
/**
* Sets a context value
* @param offset the offset
* @param value the encoding of the value. Must have been encoded via @see JavaProfiler#registerConstant
*/
public void setContextValue(int offset, int value) {
int tid = TID.get();
if (UNSAFE != null) {
setContextUnsafe(tid, offset, value);
} else {
setContextByteBuffer(tid, offset, value);
}
}
private void setContextUnsafe(int tid, int offset, int value) {
if (contextBaseOffsets == null) {
return;
}
long pageOffset = getPageUnsafe(tid);
if (pageOffset == 0) {
return;
}
UNSAFE.putInt(pageOffset + addressOf(tid, offset), value);
}
public void setContextByteBuffer(int tid, int offset, int value) {
if (contextStorage == null) {
return;
}
ByteBuffer page = getPage(tid);
if (page == SENTINEL) {
return;
}
page.putInt(addressOf(tid, offset), value);
}
void copyTags(int[] snapshot) {
int tid = TID.get();
if (UNSAFE != null) {
copyTagsUnsafe(tid, snapshot);
} else {
copyTagsByteBuffer(tid, snapshot);
}
}
void copyTagsUnsafe(int tid, int[] snapshot) {
if (contextBaseOffsets == null) {
return;
}
long pageOffset = getPageUnsafe(tid);
if (pageOffset == 0) {
return;
}
long address = pageOffset + addressOf(tid, 0);
for (int i = 0; i < snapshot.length; i++) {
snapshot[i] = UNSAFE.getInt(address);
address += Integer.BYTES;
}
}
void copyTagsByteBuffer(int tid, int[] snapshot) {
if (contextStorage == null) {
return;
}
ByteBuffer page = getPage(tid);
if (page == SENTINEL) {
return;
}
int address = addressOf(tid, 0);
for (int i = 0; i < snapshot.length; i++) {
snapshot[i] = page.getInt(address + i * Integer.BYTES);
}
}
private static int addressOf(int tid, int offset) {
return ((tid % PAGE_SIZE) * CONTEXT_SIZE + DYNAMIC_TAGS_OFFSET)
// TODO - we want to limit cardinality and a great way to enforce that is with the size of these
// fields to a smaller type, say, u16. This would also allow us to pack more data into each thread's
// slot. However, the current implementation of the dictionary trades monotonicity and minimality for
// space, so imposing a limit of 64K values does not impose a limit on the number of bits required per
// value. Condensing this mapping would also result in savings in varint encoded event sizes.
+ offset * Integer.BYTES;
}
/**
* Registers a constant so that its encoding can be used in place of the string
* @param key the key to be written into the attribute value constant pool
*/
int registerConstant(String key) {
return registerConstant0(key);
}
/**
* Dumps the JFR recording at the provided path
* @param recording the path to the recording
* @throws NullPointerException if recording is null
*/
public void dump(Path recording) {
dump0(recording.toAbsolutePath().toString());
}
/**
* Records a datadog.ProfilerSetting event with no unit
* @param name the name
* @param value the value
*/
public void recordSetting(String name, String value) {
recordSetting(name, value, "");
}
/**
* Records a datadog.ProfilerSetting event
* @param name the name
* @param value the value
* @param unit the unit
*/
public void recordSetting(String name, String value, String unit) {
recordSettingEvent0(name, value, unit);
}
/**
* Scales the ticks to milliseconds and applies a threshold
*/
public boolean isThresholdExceeded(long thresholdMillis, long startTicks, long endTicks) {
return endTicks - startTicks > thresholdMillis * TSCFrequencyHolder.FREQUENCY / 1000;
}
/**
* Records when queueing ended
* @param task the name of the enqueue task
* @param scheduler the name of the thread-pool or executor scheduling the task
* @param origin the thread the task was submitted on
*/
public void recordQueueTime(long startTicks,
long endTicks,
Class<?> task,
Class<?> scheduler,
Class<?> queueType,
int queueLength,
Thread origin) {
recordQueueEnd0(startTicks, endTicks, task.getName(), scheduler.getName(), origin, queueType.getName(), queueLength);
}
/**
* Get the ticks for the current thread.
* @return ticks
*/
public long getCurrentTicks() {
return currentTicks0();
}
/**
* If the profiler is built in debug mode, returns counters recorded during profile execution.
* These are for whitebox testing and not intended for production use.
* @return a map of counters
*/
public Map<String, Long> getDebugCounters() {
Map<String, Long> counters = new HashMap<>();
ByteBuffer buffer = getDebugCounters0().order(ByteOrder.LITTLE_ENDIAN);
if (buffer.hasRemaining()) {
String[] names = describeDebugCounters0();
for (int i = 0; i < names.length && i * 128 < buffer.capacity(); i++) {
counters.put(names[i], buffer.getLong(i * 128));
}
}
return counters;
}
private static native boolean init0();
private native void stop0() throws IllegalStateException;
private native String execute0(String command) throws IllegalArgumentException, IllegalStateException, IOException;
private native void filterThread0(boolean enable);
private static native int getTid0();
private static native ByteBuffer getContextPage0(int tid);
// this is only here because ByteBuffer.putLong splits its argument into 8 bytes
// before performing the write, which makes it more likely that writing the context
// will be interrupted by the signal, leading to more rejected context values on samples
// ByteBuffer is simpler and fit for purpose on modern JDKs
private static native long getContextPageOffset0(int tid);
private static native int getMaxContextPages0();
private static native boolean recordTrace0(long rootSpanId, String endpoint, String operation, int sizeLimit);
private static native int registerConstant0(String value);
private static native void dump0(String recordingFilePath);
private static native ByteBuffer getDebugCounters0();
private static native String[] describeDebugCounters0();
private static native void recordSettingEvent0(String name, String value, String unit);
private static native void recordQueueEnd0(long startTicks, long endTicks, String task, String scheduler, Thread origin, String queueType, int queueLength);
private static native long currentTicks0();
private static native long tscFrequency0();
private static native void mallocArenaMax0(int max);
private static native String getStatus0();
}