1# heapprofd: Sampling for Memory Profiles 2 3_tomlewin, fmayer **·** 2021-04-14_ 4 5## Background 6 7A heap profiler associates memory allocations with the callstacks on which they 8happen ([example visualization]). It is prohibitively expensive to handle every 9allocation done by a program, so the [Android Heap Profiler] employs a sampling 10approach that handles a statistically meaningful subset of allocations. This 11document explores the statistical properties thereof. 12 13## Conceptual motivation 14Conceptually the sampling is implemented such that each byte has some 15probability p of being sampled. In theory we can think of each byte undergoing a 16Bernoulli trial. The reason we use a random sampling approach, as opposed to 17taking every nth byte, is that there may be regular allocation patterns in the 18code that may be missed by a correspondingly regular sampling. 19 20To scale the sampled bytes to the correct scale, we multiply by 1 / p, i.e. if 21we sample a byte with probability 10%, then each byte sampled represents 10 22bytes allocated. 23 24 25## Implementation 26 27In practice, the [algorithm] works as follows: 28 291. We look at an allocation 30 312. If the size of the allocation is large enough that there’s a greater than 99% 32 chance of it being sampled at least once, we return the size of the 33 allocation directly. This is a performance optimization. 34 353. If the size of the allocation is smaller, then we compute the number of times 36 we would draw a sample if we sampled each byte with the given sampling rate: 37 38 * In practice we do this by keeping track of the arrival time of the next 39 sample. When an allocation happens, we subtract its size from the arrival 40 time of the next sample, and check whether we have brought it below zero. We 41 then repeatedly draw from the exponential distribution (which is the 42 interarrival time of Poisson) until the arrival time is brought back 43 above 0. The amount of times we had to draw from the exponential 44 distribution is the number of samples the allocation should count as. 45 46 * We multiply the number of samples we drew within the allocation by the 47 sampling rate to get an estimate of the size of the allocation 48 49We instead draw directly from the Poisson/binomial distribution to see how many 50samples we get for a given allocation size, but this is not as computationally 51efficient. This is because we don’t sample most of the allocations, due to their 52small size and our low sampling rate. This means it’s more efficient to use the 53exponential draw approach, as for a non-sample, we only need to decrement a 54counter. Sampling from the Poisson for every allocation (rather than drawing 55from exponential for every sample) is more expensive. 56 57## Theoretical performance 58 59If we sample at some rate 1 / p, then to set p reasonably we need to know what 60our probability of sampling an allocation of any size is, as well as our 61expected error when we sample it. If we set p = 1 then we sample everything and 62have no error. Reducing the sampling rate costs us coverage and accuracy. 63 64### Sampling probabilities 65 66We will sample an allocation with probability Exponential(sampling rate) < 67allocation size. This is equivalent to the probability that we do not fail to 68sample all bytes within the allocation if we sample bytes at our sampling rate. 69 70Because the exponential distribution is memoryless, we can add together 71allocations that are the same even if they occur apart for the purposes of 72probability. This means that if we have an allocation of 1MB and then another of 731MB, the probability of us taking at least one sample is the same as the 74probability of us taking at least one sample of a contiguous 2MB allocation. 75 76We can see from the chart below that if we 16X our sampling rate from 32KiB to 77512KiB we still have a 95% chance of sampling anything above 1.5MiB. If we 64X 78it to 2048KiB we still have an 80% chance to sample anything above 3.5MiB. 79 80![](/docs/images/heapprofd-sampling/one-sample.png) 81 82### Expected errors 83We can also consider the expected error we’ll make at given allocation sizes and 84sampling rates. Like before it doesn’t matter where the allocation happens — if 85we have two allocations of the same type occurring at different times they have 86the same statistical properties as a single allocation with size equal to their 87sum. This is because the exponential distribution we use is memoryless. 88 89 90For expected error we report something like the [mean absolute percentage 91error]. This just means we see how wrong we might be in percent relative to the 92true allocation size, and then scale these results according to their 93probability of occurrence. This is an estimator that has some issues (i.e. it’s 94biased such that underestimates are more penalised) but it’s easy to reason 95about and it’s useful as a gauge of how wrong on average we might be with our 96estimates. I would recommend just reading this as analogous to a standard 97deviation. 98 99 100Charts below show both the expected error and the conditional expected error: 101the expected error assuming we have at least one sample within the allocation. 102There’s periodicity in both in line with the sampling rate used. The thing to 103take away is that, while the estimates are unbiased such that their expected 104value is equal to their real value, substantial errors are still possible. 105 106![](/docs/images/heapprofd-sampling/expected-error.png) 107 108Assuming that we take at least one sample of an allocation, what error might we 109expect? We can answer that using the following chart, which shows expected error 110conditional on at least one sample being taken. This is the expected error we 111can expect for the things that end up in our heap profile. It’s important to 112emphasise that this expected error methodology is not exact, and also that the 113underlying sampling remains unbiased — the expected value is the true value. 114This should be considered more as a measure akin to standard deviation. 115 116![](/docs/images/heapprofd-sampling/conditional-expected-error.png) 117 118## Performance Considerations 119### STL Distributions 120 121Benchmarking of the STL distributions on a Pixel 4 reinforces our approach of 122estimating the geometric distribution using an exponential distribution, as its 123performance is ~8x better ([full results]). 124 125 126Draw sample from exponential distribution with p = 1 / 32000: 127 128ARM64: 21.3ns (sd 0.066ns, negligibly small, smaller than a CPU cycle) 129 130ARM32: 33.2ns (sd 0.011ns, negligibly small, smaller than a CPU cycle) 131 132 133Draw sample from geometric distribution with p = 1 / 32000: 134 135ARM64: 169ns (sd 0.023ns, negligibly small, smaller than a CPU cycle) (7.93x) 136 137ARM32: 222ns (sd 0.118ns, negligibly small, smaller than a CPU cycle) (6.69x) 138 139## Appendix 140 141### Improvements made 142 143We previously (before Android 12) returned the size of the allocation accurately 144and immediately if the allocation size was greater than our sampling rate. This 145had several impacts. 146 147 148The most obvious impact is that with the old approach we would expect to sample 149an allocation equal in size to our sampling rate ~63% of the time, rather than 150100%. This means there is a discontinuity in coverage between an allocation a 151byte smaller than our sampling rate, and one a byte larger. This is still 152unbiased from a statistical perspective, but it’s important to note. 153 154 155Another unusual impact is that the sampling rate depends not only on the size of 156the memory being used, but also how it’s allocated. If our sampling rate were 15710KB, and we have an allocation that’s 10KB, we sample it with certainty. If 158instead that 10KB is split among two 5KB allocations, we sample it with 159probability 63%. Again this doesn’t bias our results, but it means that our 160measurements of memory where there are many small allocations might be noisier 161than ones where there are a few large allocations, even if total memory used is 162the same. If we didn’t return allocations greater than the sampling rate every 163time, then the memorylessness property of the exponential distribution would 164mean our method is insensitive to how the memory is split amongst allocations, 165which seems a desirable property. 166 167 168We altered the cutoff at which we simply returned the allocation size. 169Previously, as discussed, the cutoff was at the sampling rate, which led to a 170discontinuity. Now the cutoff is determined by the size at which we have a >99% 171chance of sampling an allocation at least once, given the sampling rate we’re 172using. This resolves the above issues. 173 174[algorithm]: 175 https://cs.android.com/android/platform/superproject/+/master:external/perfetto/src/profiling/memory/sampler.h 176[example visualization]: /docs/images/syssrv-apk-assets-two.png 177[Android Heap Profiler]: /docs/design-docs/heapprofd-design 178[mean absolute percentage error]: https://en.wikipedia.org/wiki/Mean_absolute_percentage_error 179[full results]: https://gist.github.com/fmayer/3aafcaf58f8db09714ba09aa4ac2b1ac