|
7 | 7 | #include "_memalloc_reentrant.h" |
8 | 8 | #include "_memalloc_tb.h" |
9 | 9 |
|
| 10 | +/* |
| 11 | + How heap profiler sampling works: |
| 12 | +
|
| 13 | + This is mostly derived from |
| 14 | + https://github.com/google/tcmalloc/blob/master/docs/sampling.md#detailed-treatment-of-weighting-weighting |
| 15 | +
|
| 16 | + We want to explain memory used by the program. We can't track every |
| 17 | + allocation with reasonable overhead, so we sample. We'd like the heap to |
| 18 | + represent what's taking up the most memory. We'd like to see large live |
| 19 | + allocations, or when many small allocations in some part of the code add up |
| 20 | + to a lot of memory usage. So, we choose to sample based on bytes allocated. |
| 21 | + We basically want every byte allocated to have the same probability of being |
| 22 | + represented in the profile. Assume we want an average of one byte out of |
| 23 | + every R allocated sampled. Call R the "sampling interval". In a simplified |
| 24 | + world where every allocation is 1 byte, we can just do a 1/R coin toss for |
| 25 | + every allocation. This can be simplified by observing that the interval |
| 26 | + between samples done this way follows a geometric distribution with average |
| 27 | + R. We can draw from a geometric distribution to pick the next sample point. |
| 28 | + For computational simplicity, we use an exponential distribution, which is |
| 29 | + essentially the limit of the geometric distribution if we were to divide each |
| 30 | + byte into smaller and smaller sub-bytes. We set a target for sampling, T, |
| 31 | + drawn from the exponential distribution with average R. We count the number |
| 32 | + of bytes allocated, C. For each allocation, we increment C by the size of the |
| 33 | + allocation, and when C >= T, we take a sample, reset C to 0, and re-draw T. |
| 34 | +
|
| 35 | + If we reported just the sampled allocation's sizes, we would significantly |
| 36 | + misrepresent the actual heap size. We're probably going to hit some small |
| 37 | + allocations with our sampling, and reporting their actual size would |
| 38 | + under-represent the size of the heap. Each sampled allocation represents |
| 39 | + roughly R bytes of actual allocated memory. We want to weight our samples |
| 40 | + accordingly, and account for the fact that large allocations are more likely |
| 41 | + to be sampled than small allocations. |
| 42 | +
|
| 43 | + The math for weighting is described in more detail in the tcmalloc docs. |
| 44 | + Basically, any sampled allocation should get an average weight of R, our |
| 45 | + sampling interval. However, this would under-weight allocations larger than R |
| 46 | + bytes, our sampling interval. When we pick the next sampling point, it's |
| 47 | + probably going to be in the middle of an allocation. Bytes of the sampled |
| 48 | + allocation past that point are going to be skipped by our sampling method, |
| 49 | + since we re-draw the target _after_ the allocation. We can correct for this |
| 50 | + by looking at how big the allocation was, and how much it would drive the |
| 51 | + counter C past the target T. The formula W = R + (C - T) expresses this, |
| 52 | + where C is the counter including the sampled allocation. If the allocation |
| 53 | + was large, we are likely to have significantly exceeded T, so the weight will |
| 54 | + be larger. Conversely, if the allocation was small, C - T will likely be |
| 55 | + small, so the allocation gets less weight, and as we get closer to our |
| 56 | + hypothetical 1-byte allocations we'll get closer to a weight of R for each |
| 57 | + allocation. The current code simplifies this a bit. We can also express the |
| 58 | + weight as C + (R - T), and note that on average T should equal R, and just |
| 59 | + drop the (R - T) term and use C as the weight. We might want to use the full |
| 60 | + formula if more testing shows us to be too inaccurate. |
| 61 | + */ |
| 62 | + |
10 | 63 | typedef struct |
11 | 64 | { |
12 | | - /* Granularity of the heap profiler in bytes */ |
| 65 | + /* Heap profiler sampling interval */ |
13 | 66 | uint64_t sample_size; |
14 | | - /* Current sample size of the heap profiler in bytes */ |
| 67 | + /* Next heap sample target, in bytes allocated */ |
15 | 68 | uint64_t current_sample_size; |
16 | 69 | /* Tracked allocations */ |
17 | 70 | traceback_array_t allocs; |
18 | | - /* Allocated memory counter in bytes */ |
| 71 | + /* Bytes allocated since the last sample was collected */ |
19 | 72 | uint64_t allocated_memory; |
20 | 73 | /* True if the heap tracker is frozen */ |
21 | 74 | bool frozen; |
@@ -78,6 +131,12 @@ memheap_init() |
78 | 131 | static uint32_t |
79 | 132 | heap_tracker_next_sample_size(uint32_t sample_size) |
80 | 133 | { |
| 134 | + /* We want to draw a sampling target from an exponential distribution with |
| 135 | + average sample_size. We use the standard technique of inverse transform |
| 136 | + sampling, where we take uniform randomness, which is easy to get, and |
| 137 | + transform it by the inverse of the cumulative distribution function for |
| 138 | + the distribution we want to sample. |
| 139 | + See https://en.wikipedia.org/wiki/Inverse_transform_sampling. */ |
81 | 140 | /* Get a value between [0, 1[ */ |
82 | 141 | double q = (double)rand() / ((double)RAND_MAX + 1); |
83 | 142 | /* Get a value between ]-inf, 0[, more likely close to 0 */ |
@@ -245,6 +304,11 @@ memalloc_heap_track(uint16_t max_nframe, void* ptr, size_t size, PyMemAllocatorD |
245 | 304 | return false; |
246 | 305 | } |
247 | 306 |
|
| 307 | + /* The weight of the allocation is described above, but briefly: it's the |
| 308 | + count of bytes allocated since the last sample, including this one, which |
| 309 | + will tend to be larger for large allocations and smaller for small |
| 310 | + allocations, and close to the average sampling interval so that the sum |
| 311 | + of sample live allocations stays close to the actual heap size */ |
248 | 312 | traceback_t* tb = memalloc_get_traceback(max_nframe, ptr, global_heap_tracker.allocated_memory, domain); |
249 | 313 | if (tb) { |
250 | 314 | if (global_heap_tracker.frozen) |
|
0 commit comments