aboutsummaryrefslogtreecommitdiff
path: root/docs/native_allocator.md
blob: 139d664d96b7b65cfe88657bb36455bd77bb0805 (plain)
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
# Native Memory Allocator Verification
This document describes how to verify the native memory allocator on Android.
This procedure should be followed when upgrading or moving to a new allocator.
A small minor upgrade might not need to run all of the benchmarks, however,
at least the
[SQL Allocation Trace Benchmark](#sql-allocation-trace-benchmark),
[Memory Replay Benchmarks](#memory-replay-benchmarks) and
[Performance Trace Benchmarks](#performance-trace-benchmarks) should be run.

It is important to note that there are two modes for a native allocator
to run in on Android. The first is the normal allocator, the second is
called the svelte config, which is designed to run on memory constrained
systems and be a bit slower, but take less RSS. To enable the svelte config,
add this line to the `BoardConfig.mk` for the given target:

    MALLOC_SVELTE := true

The `BoardConfig.mk` file is usually found in the directory
`device/<DEVICE_NAME>/` or in a sub directory.

When evaluating a native allocator, make sure that you benchmark both
versions.

## Android Extensions
Android supports a few non-standard functions and mallopt controls that
a native allocator needs to implement.

### Iterator Functions
These are functions that are used to implement a memory leak detector
called `libmemunreachable`.

#### malloc\_disable
This function, when called, should pause all threads that are making a
call to an allocation function (malloc/free/etc). When a call
is made to `malloc_enable`, the paused threads should start running again.

#### malloc\_enable
This function, when called, does nothing unless there was a previous call
to `malloc_disable`. This call will unpause any thread which is making
a call to an allocation function (malloc/free/etc) when `malloc_disable`
was called previously.

#### malloc\_iterate
This function enumerates all of the allocations currently live in the
system. It is meant to be called after a call to `malloc_disable` to
prevent further allocations while this call is being executed. To
see what is expected for this function, the best description is the
tests for this funcion in `bionic/tests/malloc_itearte_test.cpp`.

### Mallopt Extensions
These are mallopt options that Android requires for a native allocator
to work efficiently.

#### M\_DECAY\_TIME
When set to zero, `mallopt(M_DECAY_TIME, 0)`, it is expected that an
allocator will attempt to purge and release any unused memory back to the
kernel on free calls. This is important in Android to avoid consuming extra
RSS.

When set to non-zero, `mallopt(M_DECAY_TIME, 1)`, an allocator can delay the
purge and release action. The amount of delay is up to the allocator
implementation, but it should be a reasonable amount of time. The jemalloc
allocator was implemented to have a one second delay.

The drawback to this option is that most allocators do not have a separate
thread to handle the purge, so the decay is only handled when an
allocation operation occurs. For server processes, this can mean that
RSS is slightly higher when the server is waiting for the next connection
and no other allocation calls are made. The `M_PURGE` option is used to
force a purge in this case.

For all applications on Android, the call `mallopt(M_DECAY_TIME, 1)` is
made by default. The idea is that it allows application frees to run a
bit faster, while only increasing RSS a bit.

#### M\_PURGE
When called, `mallopt(M_PURGE, 0)`, an allocator should purge and release
any unused memory immediately. The argument for this call is ignored. If
possible, this call should clear thread cached memory if it exists. The
idea is that this can be called to purge memory that has not been
purged when `M_DECAY_TIME` is set to one. This is useful if you have a
server application that does a lot of native allocations and the
application wants to purge that memory before waiting for the next connection.

## Correctness Tests
These are the tests that should be run to verify an allocator is
working properly according to Android.

### Bionic Unit Tests
The bionic unit tests contain a small number of allocator tests. These
tests are primarily verifying Android extensions and non-standard behavior
of allocation routines such as what happens when a non-power of two alignment
is passed to memalign.

To run all of the compliance tests:

    adb shell /data/nativetest64/bionic-unit-tests/bionic-unit-tests --gtest_filter="malloc*"
    adb shell /data/nativetest/bionic-unit-tests/bionic-unit-tests --gtest_filter="malloc*"

The allocation tests are not meant to be complete, so it is expected
that a native allocator will have its own set of tests that can be run.

### Libmemunreachable Tests
The libmemunreachable tests verify that the iterator functions are working
properly.

To run all of the tests:

    adb shell /data/nativetest64/memunreachable_binder_test/memunreachable_binder_test
    adb shell /data/nativetest/memunreachable_binder_test/memunreachable_binder_test
    adb shell /data/nativetest64/memunreachable_test/memunreachable_test
    adb shell /data/nativetest/memunreachable_test/memunreachable_test
    adb shell /data/nativetest64/memunreachable_unit_test/memunreachable_unit_test
    adb shell /data/nativetest/memunreachable_unit_test/memunreachable_unit_test

### CTS Entropy Test
In addition to the bionic tests, there is also a CTS test that is designed
to verify that the addresses returned by malloc are sufficiently randomized
to help defeat potential security bugs.

Run this test thusly:

    atest AslrMallocTest

If there are multiple devices connected to the system, use `-s <SERIAL>`
to specify a device.

## Performance
There are multiple different ways to evaluate the performance of a native
allocator on Android. One is allocation speed in various different scenarios,
another is total RSS taken by the allocator.

The last is virtual address space consumed in 32 bit applications. There is
a limited amount of address space available in 32 bit apps, and there have
been allocator bugs that cause memory failures when too much virtual
address space is consumed. For 64 bit executables, this can be ignored.

### Bionic Benchmarks
These are the microbenchmarks that are part of the bionic benchmarks suite of
benchmarks. These benchmarks can be built using this command:

    mmma -j bionic/benchmarks

These benchmarks are only used to verify the speed of the allocator and
ignore anything related to RSS and virtual address space consumed.

For all of these benchmark runs, it can be useful to add these two options:

    --benchmark_repetitions=XX
    --benchmark_report_aggregates_only=true

This will run the benchmark XX times and then give a mean, median, and stddev
and helps to get a number that can be compared to the new allocator.

In addition, there is another option:

    --bionic_cpu=XX

Which will lock the benchmark to only run on core XX. This also avoids
any issue related to the code migrating from one core to another
with different characteristics. For example, on a big-little cpu, if the
benchmark moves from big to little or vice-versa, this can cause scores
to fluctuate in indeterminate ways.

For most runs, the best set of options to add is:

    --benchmark_repetitions=10 --benchmark_report_aggregates_only=true --bionic_cpu=3

On most phones with a big-little cpu, the third core is the little core.
Choosing to run on the little core can tend to highlight any performance
differences.

#### Allocate/Free Benchmarks
These are the benchmarks to verify the allocation speed of a loop doing a
single allocation, touching every page in the allocation to make it resident
and then freeing the allocation.

To run the benchmarks with `mallopt(M_DECAY_TIME, 0)`, use these commands:

    adb shell /data/benchmarktest64/bionic-benchmarks/bionic-benchmarks --benchmark_filter=stdlib_malloc_free_default
    adb shell /data/benchmarktest/bionic-benchmarks/bionic-benchmarks --benchmark_filter=malloc_free_default

To run the benchmarks with `mallopt(M_DECAY_TIME, 1)`, use these commands:

    adb shell /data/benchmarktest64/bionic-benchmarks/bionic-benchmarks --benchmark_filter=stdlib_malloc_free_decay1
    adb shell /data/benchmarktest/bionic-benchmarks/bionic-benchmarks --benchmark_filter=malloc_free_decay1

The last value in the output is the size of the allocation in bytes. It is
useful to look at these kinds of benchmarks to make sure that there are
no outliers, but these numbers should not be used to make a final decision.
If these numbers are slightly worse than the current allocator, the
single thread numbers from trace data is a better representative of
real world situations.

#### Multiple Allocations Retained Benchmarks
These are the benchmarks that examine how the allocator handles multiple
allocations of the same size at the same time.

The first set of these benchmarks does a set number of 8192 byte allocations
in one loop, and then frees all of the allocations at the end of the loop.
Only the time it takes to do the allocations is recorded, the frees are not
counted. The value of 8192 was chosen since the jemalloc native allocator
had issues with this size. It is possible other sizes might show different
results, but, as mentioned before, these microbenchmark numbers should
not be used as absolutes for determining if an allocator is worth using.

This benchmark is designed to verify that there is no performance issue
related to having multiple allocations alive at the same time.

To run the benchmarks with `mallopt(M_DECAY_TIME, 0)`, use these commands:

    adb shell /data/benchmarktest64/bionic-benchmarks/bionic-benchmarks --benchmark_filter=stdlib_malloc_multiple_8192_allocs_default
    adb shell /data/benchmarktest/bionic-benchmarks/bionic-benchmarks --benchmark_filter=stdlib_malloc_multiple_8192_allocs_default

To run the benchmarks with `mallopt(M_DECAY_TIME, 1)`, use these commands:

    adb shell /data/benchmarktest64/bionic-benchmarks/bionic-benchmarks --benchmark_filter=stdlib_malloc_multiple_8192_allocs_decay1
    adb shell /data/benchmarktest/bionic-benchmarks/bionic-benchmarks --benchmark_filter=stdlib_malloc_multiple_8192_allocs_decay1

For these benchmarks, the last parameter is the total number of allocations to
do in each loop.

The other variation of this benchmark is to always do forty allocations in
each loop, but vary the size of the forty allocations. As with the other
benchmark, only the time it takes to do the allocations is tracked, the
frees are not counted. Forty allocations is an arbitrary number that could
be modified in the future. It was chosen because a version of the native
allocator, jemalloc, showed a problem at forty allocations.

To run the benchmarks with `mallopt(M_DECAY_TIME, 0)`, use these commands:

    adb shell /data/benchmarktest64/bionic-benchmarks/bionic-benchmarks --benchmark_filter=stdlib_malloc_forty_default
    adb shell /data/benchmarktest/bionic-benchmarks/bionic-benchmarks --benchmark_filter=stdlib_malloc_forty_default

To run the benchmarks with `mallopt(M_DECAY_TIME, 1)`, use these command:

    adb shell /data/benchmarktest64/bionic-benchmarks/bionic-benchmarks --benchmark_filter=stdlib_malloc_forty_decay1
    adb shell /data/benchmarktest/bionic-benchmarks/bionic-benchmarks --benchmark_filter=stdlib_malloc_forty_decay1

For these benchmarks, the last parameter in the output is the size of the
allocation in bytes.

As with the other microbenchmarks, an allocator with numbers in the same
proximity of the current values is usually sufficient to consider making
a switch. The trace benchmarks are more important than these benchmarks
since they simulate real world allocation profiles.

#### SQL Allocation Trace Benchmark
This benchmark is a trace of the allocations performed when running
the SQLite BenchMark app.

This benchmark is designed to verify that the allocator will be performant
in a real world allocation scenario. SQL operations were chosen as a
benchmark because these operations tend to do lots of malloc/realloc/free
calls, and they tend to be on the critical path of applications.

To run the benchmarks with `mallopt(M_DECAY_TIME, 0)`, use these commands:

    adb shell /data/benchmarktest64/bionic-benchmarks/bionic-benchmarks --benchmark_filter=malloc_sql_trace_default
    adb shell /data/benchmarktest/bionic-benchmarks/bionic-benchmarks --benchmark_filter=malloc_sql_trace_default

To run the benchmarks with `mallopt(M_DECAY_TIME, 1)`, use these commands:

    adb shell /data/benchmarktest64/bionic-benchmarks/bionic-benchmarks --benchmark_filter=malloc_sql_trace_decay1
    adb shell /data/benchmarktest/bionic-benchmarks/bionic-benchmarks --benchmark_filter=malloc_sql_trace_decay1

These numbers should be as performant as the current allocator.

#### mallinfo Benchmark
This benchmark only verifies that mallinfo is still close to the performance
of the current allocator.

To run the benchmark, use these commands:

    adb shell /data/benchmarktest64/bionic-benchmarks/bionic-benchmarks --benchmark_filter=BM_mallinfo
    adb shell /data/benchmarktest/bionic-benchmarks/bionic-benchmarks --benchmark_filter=BM_mallinfo

Calls to mallinfo are used in ART so a new allocator is required to be
nearly as performant as the current allocator.

#### mallopt M\_PURGE Benchmark
This benchmark tracks the cost of calling `mallopt(M_PURGE, 0)`. As with the
mallinfo benchmark, it's not necessary for this to be better than the previous
allocator, only that the performance be in the same order of magnitude.

To run the benchmark, use these commands:

    adb shell /data/benchmarktest64/bionic-benchmarks/bionic-benchmarks --benchmark_filter=BM_mallopt_purge
    adb shell /data/benchmarktest/bionic-benchmarks/bionic-benchmarks --benchmark_filter=BM_mallopt_purge

These calls are used to free unused memory pages back to the kernel.

### Memory Trace Benchmarks
These benchmarks measure all three axes of a native allocator, RSS, virtual
address space consumed, speed of allocation. They are designed to
run on a trace of the allocations from a real world application or system
process.

To build this benchmark:

    mmma -j system/extras/memory_replay

This will build two executables:

    /system/bin/memory_replay32
    /system/bin/memory_replay64

And these two benchmark executables:

    /data/benchmarktest64/trace_benchmark/trace_benchmark
    /data/benchmarktest/trace_benchmark/trace_benchmark

#### Memory Replay Benchmarks
These benchmarks display RSS, virtual memory consumed (VA space), and do a
bit of performance testing on actual traces taken from running applications.

The trace data includes what thread does each operation, so the replay
mechanism will simulate this by creating threads and replaying the operations
on a thread as if it was rerunning the real trace. The only issue is that
this is a worst case scenario for allocations happening at the same time
in all threads since it collapses all of the allocation operations to occur
one after another. This will cause a lot of threads allocating at the same
time. The trace data does not include timestamps,
so it is not possible to create a completely accurate replay.

To generate these traces, see the [Malloc Debug documentation](https://android.googlesource.com/platform/bionic/+/master/libc/malloc_debug/README.md),
the option [record\_allocs](https://android.googlesource.com/platform/bionic/+/master/libc/malloc_debug/README.md#record_allocs_total_entries).

To run these benchmarks, first copy the trace files to the target using
these commands:

    adb shell push system/extras/traces /data/local/tmp

Since all of the traces come from applications, the `memory_replay` program
will always call `mallopt(M_DECAY_TIME, 1)' before running the trace.

Run the benchmark thusly:

    adb shell memory_replay64 /data/local/tmp/traces/XXX.zip
    adb shell memory_replay32 /data/local/tmp/traces/XXX.zip

Where XXX.zip is the name of a zipped trace file. The `memory_replay`
program also can process text files, but all trace files are currently
checked in as zip files.

Every 100000 allocation operations, a dump of the RSS and VA space will be
performed. At the end, a final RSS and VA space number will be printed.
For the most part, the intermediate data can be ignored, but it is always
a good idea to look over the data to verify that no strange spikes are
occurring.

The performance number is a measure of the time it takes to perform all of
the allocation calls (malloc/memalign/posix_memalign/realloc/free/etc).
For any call that allocates a pointer, the time for the call and the time
it takes to make the pointer completely resident in memory is included.

The performance numbers for these runs tend to have a wide variability so
they should not be used as absolute value for comparison against the
current allocator. But, they should be in the same range as the current
values.

When evaluating an allocator, one of the most important traces is the
camera.txt trace. The camera application does very large allocations,
and some allocators might leave large virtual address maps around
rather than delete them. When that happens, it can lead to allocation
failures and would cause the camera app to abort/crash. It is
important to verify that when running this trace using the 32 bit replay
executable, the virtual address space consumed is not much larger than the
current allocator. A small increase (on the order of a few MBs) would be okay.

There is no specific benchmark for memory fragmentation, instead, the RSS
when running the memory traces acts as a proxy for this. An allocator that
is fragmenting badly will show an increase in RSS. The best trace for
tracking fragmentation is system\_server.txt which is an extremely long
trace (~13 million operations). The total number of live allocations goes
up and down a bit, but stays mostly the same so an allocator that fragments
badly would likely show an abnormal increase in RSS on this trace.

NOTE: When a native allocator calls mmap, it is expected that the allocator
will name the map using the call:

    prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, <PTR>, <SIZE>, "libc_malloc");

If the native allocator creates a different name, then it necessary to
modify the file:

    system/extras/memory_replay/NativeInfo.cpp

The `GetNativeInfo` function needs to be modified to include the name
of the maps that this allocator includes.

In addition, in order for the frameworks code to keep track of the memory
of a process, any named maps must be added to the file:

    frameworks/base/core/jni/android_os_Debug.cpp

Modify the `load_maps` function and add a check of the new expected name.

#### Performance Trace Benchmarks
This is a benchmark that treats the trace data as if all allocations
occurred in a single thread. This is the scenario that could
happen if all of the allocations are spaced out in time so no thread
every does an allocation at the same time as another thread.

Run these benchmarks thusly:

    adb shell /data/benchmarktest64/trace_benchmark/trace_benchmark
    adb shell /data/benchmarktest/trace_benchmark/trace_benchmark

When run without any arguments, the benchmark will run over all of the
traces and display data. It takes many minutes to complete these runs in
order to get as accurate a number as possible.