forked from JuliaLang/julia
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgc-alloc-profiler.c
More file actions
374 lines (322 loc) · 13.3 KB
/
Copy pathgc-alloc-profiler.c
File metadata and controls
374 lines (322 loc) · 13.3 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
// This file is a part of Julia. License is MIT: https://julialang.org/license
#include "gc-alloc-profiler.h"
#include "julia_internal.h"
#include <string.h>
typedef struct {
jl_bt_element_t *data;
size_t size;
} jl_raw_backtrace_t;
typedef struct jl_raw_alloc_t {
jl_datatype_t *type_address;
jl_raw_backtrace_t backtrace;
size_t size;
void *task;
uint64_t timestamp;
} jl_raw_alloc_t;
// Simple typed growable array for alloc records.
typedef struct {
jl_raw_alloc_t *data;
size_t len;
size_t cap;
} alloc_array_t;
static void alloc_array_push(alloc_array_t *a, jl_raw_alloc_t val) JL_NOTSAFEPOINT
{
if (a->len >= a->cap) {
a->cap = a->cap ? a->cap * 2 : 8;
a->data = (jl_raw_alloc_t *)realloc_s(a->data, a->cap * sizeof(jl_raw_alloc_t));
}
a->data[a->len++] = val;
}
static void alloc_array_clear(alloc_array_t *a) JL_NOTSAFEPOINT { a->len = 0; }
typedef struct {
jl_datatype_t **data;
size_t len;
size_t cap;
} type_array_t;
static void type_array_push(type_array_t *a, jl_datatype_t *val) JL_NOTSAFEPOINT
{
if (a->len >= a->cap) {
a->cap = a->cap ? a->cap * 2 : 8;
a->data = (jl_datatype_t **)realloc_s(a->data, a->cap * sizeof(a->data[0]));
}
a->data[a->len++] = val;
}
#define TYPE_FILTER_SIZE 256 // must be a power of two
// Bump arena for the recorded backtraces, to avoid a malloc per sample.
// Blocks are only freed all at once in `jl_free_alloc_profile`, since records
// combined into `g_combined_allocs` keep pointing into them.
typedef struct jl_bt_arena_block_t {
struct jl_bt_arena_block_t *next;
size_t used; // in elements
size_t cap; // in elements
jl_bt_element_t data[];
} jl_bt_arena_block_t;
typedef struct {
jl_bt_arena_block_t *head;
} jl_bt_arena_t;
#define BT_ARENA_BLOCK_ELEMENTS (size_t)(64 * 1024 / sizeof(jl_bt_element_t))
static jl_bt_element_t *bt_arena_alloc(jl_bt_arena_t *arena, size_t n) JL_NOTSAFEPOINT
{
jl_bt_arena_block_t *block = arena->head;
if (n >= BT_ARENA_BLOCK_ELEMENTS) {
// oversized: give it a dedicated block, keeping the current block (and
// its remaining space) as the head
jl_bt_arena_block_t *big = (jl_bt_arena_block_t*)malloc_s(
sizeof(jl_bt_arena_block_t) + n * sizeof(jl_bt_element_t));
big->used = big->cap = n;
if (block == NULL) {
big->next = NULL;
arena->head = big;
}
else {
big->next = block->next;
block->next = big;
}
return big->data;
}
if (block == NULL || block->cap - block->used < n) {
block = (jl_bt_arena_block_t*)malloc_s(
sizeof(jl_bt_arena_block_t) + BT_ARENA_BLOCK_ELEMENTS * sizeof(jl_bt_element_t));
block->next = arena->head;
block->used = 0;
block->cap = BT_ARENA_BLOCK_ELEMENTS;
arena->head = block;
}
jl_bt_element_t *p = &block->data[block->used];
block->used += n;
return p;
}
static void bt_arena_free(jl_bt_arena_t *arena) JL_NOTSAFEPOINT
{
jl_bt_arena_block_t *block = arena->head;
while (block != NULL) {
jl_bt_arena_block_t *next = block->next;
free(block);
block = next;
}
arena->head = NULL;
}
// Per-thread profile: a growable array of alloc records, the arena that owns
// their backtrace memory, and the types they refer to, which have to be
// reported to the GC as roots.
typedef struct {
alloc_array_t allocs;
jl_bt_arena_t bt_arena;
type_array_t type_roots;
// direct-mapped filter over `type_roots`, so that it doesn't grow with every
// sample. Owning thread only; a collision just lets a duplicate through.
jl_datatype_t *type_filter[TYPE_FILTER_SIZE];
} jl_per_thread_alloc_profile_t;
// Global profile state.
typedef struct {
// `cong(UINT64_MAX, ...)` draw at or below which an allocation is sampled. Read
// by recorders that have not yet joined the recorder count, so it has to be
// atomic even though it is only written while the profiler is stopped.
_Atomic(uint64_t) sample_threshold;
jl_per_thread_alloc_profile_t *per_thread_profiles;
size_t num_profiles;
} jl_alloc_profile_t;
// == Global variables manipulated by callbacks ==
static jl_alloc_profile_t g_alloc_profile;
_Atomic(int) g_alloc_profile_enabled = 0;
// number of threads currently inside `_maybe_record_alloc_to_profile`, striped
// over cache-line-padded counters (indexed by tid) to avoid contention on a
// single cache line while recording
#define RECORDER_STRIPES 16
static struct {
_Atomic(int) count;
char _pad[64 - sizeof(_Atomic(int))];
} g_recording_threads[RECORDER_STRIPES];
static alloc_array_t g_combined_allocs; // Will live forever.
static _Atomic(int) *recorder_counter(size_t tid) JL_NOTSAFEPOINT
{
return &g_recording_threads[tid % RECORDER_STRIPES].count;
}
// Must only be called while `g_alloc_profile_enabled` is 0, so that once a
// stripe reaches zero no new recorder on it can start touching the profile
// arrays (the same Dekker-style argument as for a single counter, per stripe).
static void wait_for_recorders(void) JL_NOTSAFEPOINT
{
// a recorder can take a while (unwinding takes locks and mallocs), so back off
for (int i = 0; i < RECORDER_STRIPES; i++) {
for (int spins = 0; jl_atomic_load(&g_recording_threads[i].count) != 0; spins++) {
if (spins < 128)
jl_cpu_pause();
else
jl_cpu_suspend();
}
}
}
// Disable recording and wait for in-flight recorders to leave, so that the profile
// arrays can be read, reallocated or freed. Returns the state for `resume_recording`.
static int suspend_recording(void) JL_NOTSAFEPOINT
{
int was_enabled = jl_atomic_exchange(&g_alloc_profile_enabled, 0);
wait_for_recorders();
return was_enabled;
}
static void resume_recording(int was_enabled) JL_NOTSAFEPOINT
{
if (was_enabled)
jl_atomic_store_release(&g_alloc_profile_enabled, 1);
}
// the recorded "type" is sometimes a sentinel rather than an object pointer;
// keep in sync with `load_type` in stdlib/Profile/src/Allocs.jl
static int is_object_type(jl_datatype_t *type) JL_NOTSAFEPOINT
{
return !((uintptr_t)type < 4096 || (uintptr_t)type == jl_buff_tag ||
type == jl_gc_unknown_type_tag);
}
// keep `type` alive for as long as an alloc record refers to it
static void record_type_root(jl_per_thread_alloc_profile_t *p, jl_datatype_t *type) JL_NOTSAFEPOINT
{
if (!is_object_type(type))
return;
size_t slot = ((uintptr_t)type >> 4) & (TYPE_FILTER_SIZE - 1);
if (p->type_filter[slot] == type)
return;
p->type_filter[slot] = type;
type_array_push(&p->type_roots, type);
}
// === stack stuff ===
static jl_raw_backtrace_t get_raw_backtrace(jl_bt_arena_t *arena) JL_NOTSAFEPOINT
{
// We first record the backtrace onto a MAX-sized buffer, so that we don't have to
// allocate the buffer until we know the size. To ensure thread-safety, we use a
// per-thread backtrace buffer.
jl_ptls_t ptls = jl_current_task->ptls;
jl_bt_element_t *shared_bt_data_buffer = ptls->profiling_bt_buffer;
if (shared_bt_data_buffer == NULL) {
size_t size = sizeof(jl_bt_element_t) * (JL_MAX_BT_SIZE + 1);
shared_bt_data_buffer = (jl_bt_element_t *)malloc_s(size);
ptls->profiling_bt_buffer = shared_bt_data_buffer;
}
size_t bt_size = rec_backtrace(shared_bt_data_buffer, JL_MAX_BT_SIZE, 2);
// Then we copy only the needed bytes out of the buffer into our profile.
jl_bt_element_t *bt_data = bt_arena_alloc(arena, bt_size);
memcpy(bt_data, shared_bt_data_buffer, bt_size * sizeof(jl_bt_element_t));
jl_raw_backtrace_t result;
result.data = bt_data;
result.size = bt_size;
return result;
}
// == exported interface ==
JL_DLLEXPORT void jl_start_alloc_profile(double sample_rate)
{
// in-flight recorders read the per-thread arrays we may be about to reallocate
jl_stop_alloc_profile();
size_t nthreads = jl_atomic_load_acquire(&jl_n_threads);
size_t num_profiles = g_alloc_profile.num_profiles;
if (num_profiles < nthreads) {
jl_per_thread_alloc_profile_t *profiles = g_alloc_profile.per_thread_profiles;
profiles = (jl_per_thread_alloc_profile_t *)realloc_s(
profiles, nthreads * sizeof(profiles[0]));
memset(&profiles[num_profiles],
0, (nthreads - num_profiles) * sizeof(profiles[0]));
g_alloc_profile.per_thread_profiles = profiles;
g_alloc_profile.num_profiles = nthreads;
}
// `cong` draws from the open interval [0, UINT64_MAX), so a threshold of
// UINT64_MAX samples every allocation
uint64_t threshold = sample_rate >= 1.0 ? UINT64_MAX :
sample_rate <= 0.0 ? 0 :
(uint64_t)(sample_rate * (double)UINT64_MAX);
jl_atomic_store_relaxed(&g_alloc_profile.sample_threshold, threshold);
jl_atomic_store_release(&g_alloc_profile_enabled, 1);
}
JL_DLLEXPORT jl_profile_allocs_raw_results_t jl_fetch_alloc_profile(void)
{
// in-flight recorders may be pushing onto (and thus reallocating) the
// per-thread arrays we are about to drain
int was_enabled = suspend_recording();
// combine allocs
// TODO: interleave to preserve ordering
for (size_t i = 0; i < g_alloc_profile.num_profiles; i++) {
alloc_array_t *allocs = &g_alloc_profile.per_thread_profiles[i].allocs;
for (size_t j = 0; j < allocs->len; j++)
alloc_array_push(&g_combined_allocs, allocs->data[j]);
alloc_array_clear(allocs);
}
jl_profile_allocs_raw_results_t result;
result.allocs = g_combined_allocs.data;
result.num_allocs = g_combined_allocs.len;
// recorders only ever push onto the per-thread arrays, never onto
// `g_combined_allocs`, so the result stays valid while recording resumes
resume_recording(was_enabled);
return result;
}
JL_DLLEXPORT void jl_stop_alloc_profile(void)
{
// after this returns, no thread is mid-record and none can start recording,
// so the profile arrays are safe to read and free
suspend_recording();
}
JL_DLLEXPORT int jl_alloc_profile_is_running(void)
{
return jl_atomic_load_acquire(&g_alloc_profile_enabled);
}
JL_DLLEXPORT void jl_free_alloc_profile(void)
{
// in-flight recorders may be pushing onto the arrays we are about to free
int was_enabled = suspend_recording();
// The per-thread arenas own all backtrace memory, including that of allocs
// already combined into `g_combined_allocs` by jl_fetch_alloc_profile().
for (size_t i = 0; i < g_alloc_profile.num_profiles; i++) {
jl_per_thread_alloc_profile_t *p = &g_alloc_profile.per_thread_profiles[i];
alloc_array_clear(&p->allocs);
bt_arena_free(&p->bt_arena);
// no alloc record refers to these types any more
p->type_roots.len = 0;
memset(p->type_filter, 0, sizeof(p->type_filter));
}
alloc_array_clear(&g_combined_allocs);
resume_recording(was_enabled);
}
// == GC root marking ==
// Called during GC root marking (world stopped, so no recorder can be appending to
// `type_roots`); keeps the recorded types alive until `jl_free_alloc_profile`. Walks
// the distinct types seen, not every record, so the mark work is bounded.
void jl_gc_foreach_alloc_profile_root(jl_alloc_profile_root_cb_t f, void *env) JL_NOTSAFEPOINT
{
for (size_t i = 0; i < g_alloc_profile.num_profiles; i++) {
type_array_t *roots = &g_alloc_profile.per_thread_profiles[i].type_roots;
for (size_t j = 0; j < roots->len; j++)
f((jl_value_t*)roots->data[j], env);
}
}
// == callback called into by the outside ==
void _maybe_record_alloc_to_profile(jl_value_t *val, size_t size, jl_datatype_t *type) JL_NOTSAFEPOINT
{
jl_task_t *ct = jl_current_task;
// draw the sample before joining the recorder count: the RNG is thread-local, so
// the common case of an allocation that isn't sampled costs no shared-memory
// traffic beyond the relaxed threshold load. A concurrent restart can at worst
// have this draw against the previous session's rate, which only perturbs the
// sampling of the allocations in flight across the restart.
if (cong(UINT64_MAX, &ct->ptls->rngseed) >
jl_atomic_load_relaxed(&g_alloc_profile.sample_threshold))
return;
size_t thread_id = jl_atomic_load_relaxed(&ct->tid);
_Atomic(int) *recording = recorder_counter(thread_id);
jl_atomic_fetch_add(recording, 1);
// re-check the flag under the recorder count: either we see the stop and
// leave, or the stopping thread waits for us to finish (Dekker-style
// synchronization; both sides use sequentially consistent operations)
if (!jl_atomic_load(&g_alloc_profile_enabled))
goto done;
{
if (thread_id >= g_alloc_profile.num_profiles)
goto done; // ignore allocations on threads started after the alloc-profile started
jl_per_thread_alloc_profile_t *p = &g_alloc_profile.per_thread_profiles[thread_id];
jl_raw_alloc_t alloc;
alloc.type_address = type;
alloc.backtrace = get_raw_backtrace(&p->bt_arena);
alloc.size = size;
alloc.task = (void *)jl_current_task;
alloc.timestamp = cycleclock();
alloc_array_push(&p->allocs, alloc);
record_type_root(p, type);
}
done:
jl_atomic_fetch_add(recording, -1);
}