Skip to content

Commit 0a0cd05

Browse files
authored
Fix emmalloc_trim() to work again (#25126)
Found problems: 1. The convention for sbrk() changed at some point to require 16b alignment. (`__alignof__(max_align_t)`) This can create gaps in the alignment of emmalloc heap end sentinel region and the previous free region. This must require changing the assumption in emmalloc that heap end sentinel region would always be fixed 16b -> allow it to be arbitrary sized (in practice 16-31b). 2. sbrk() implementation had round-up to nearest max_align_t, but didn't handle negative sbrk() values. 3. in 4GB mode, a 32-bit sbrk() is not enough to accommodate support for both sbrk() increases and decreases (unless we take a convention that sbrk() can only be increased by < 2GB amount.. which probably won't hold in use cases that want to claim the whole heap immediately). Add a function sbrk64() that can be used in 4GB builds to be able to express both memory increases and decresases. 4. test case expectations have changed at some point. test_emmalloc_trim was written with an assumption that emmalloc would acquire the whole heap with sbrk() at startup.. but no longer does so. Adjusted test to not assume this. 5. printing bare memory values in test doesn't work well in e.g. ubsan test mode, so remove the direct value prints in the test, and just assert() the expectations. Fixes: #23343
1 parent c4f1a1e commit 0a0cd05

28 files changed

+228
-165
lines changed

system/lib/emmalloc.c

Lines changed: 88 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
#include <emscripten/heap.h>
6262
#include <emscripten/threading.h>
6363

64+
void *_sbrk64(int64_t numBytes);
65+
6466
#ifdef __EMSCRIPTEN_TRACING__
6567
#include <emscripten/trace.h>
6668
#endif
@@ -137,6 +139,7 @@ static volatile uint8_t multithreadingLock = 0;
137139

138140
#define IS_POWER_OF_2(val) (((val) & ((val)-1)) == 0)
139141
#define ALIGN_UP(ptr, alignment) ((uint8_t*)((((uintptr_t)(ptr)) + ((alignment)-1)) & ~((alignment)-1)))
142+
#define ALIGN_DOWN(ptr, alignment) ((uint8_t*)(((uintptr_t)(ptr)) & ~((alignment)-1)))
140143
#define HAS_ALIGNMENT(ptr, alignment) ((((uintptr_t)(ptr)) & ((alignment)-1)) == 0)
141144

142145
static_assert(IS_POWER_OF_2(MALLOC_ALIGNMENT), "MALLOC_ALIGNMENT must be a power of two value!");
@@ -467,7 +470,8 @@ static bool claim_more_memory(size_t numBytes) {
467470
numBytes = (size_t)ALIGN_UP(numBytes, MALLOC_ALIGNMENT);
468471

469472
// Claim memory via sbrk
470-
uint8_t *startPtr = (uint8_t*)sbrk(numBytes);
473+
assert((int64_t)numBytes >= 0);
474+
uint8_t *startPtr = (uint8_t*)_sbrk64((int64_t)numBytes);
471475
if ((intptr_t)startPtr == -1) {
472476
#ifdef EMMALLOC_VERBOSE
473477
MAIN_THREAD_ASYNC_EM_ASM(err('claim_more_memory: sbrk failed!'));
@@ -483,6 +487,9 @@ static bool claim_more_memory(size_t numBytes) {
483487
// Create a sentinel region at the end of the new heap block
484488
Region *endSentinelRegion = (Region*)(endPtr - sizeof(Region));
485489
create_used_region(endSentinelRegion, sizeof(Region));
490+
#ifdef EMMALLOC_VERBOSE
491+
MAIN_THREAD_ASYNC_EM_ASM(out('claim_more_memory: created a sentinel memory region at address ' + ptrToString($0)), endSentinelRegion);
492+
#endif
486493

487494
// If we are the sole user of sbrk(), it will feed us continuous/consecutive memory addresses - take advantage
488495
// of that if so: instead of creating two disjoint memory regions blocks, expand the previous one to a larger size.
@@ -1165,7 +1172,7 @@ struct mallinfo emmalloc_mallinfo() {
11651172
struct mallinfo info;
11661173
// Non-mmapped space allocated (bytes): For emmalloc,
11671174
// let's define this as the difference between heap size and dynamic top end.
1168-
info.arena = emscripten_get_heap_size() - (size_t)sbrk(0);
1175+
info.arena = emscripten_get_heap_size() - (size_t)_sbrk64(0);
11691176
// Number of "ordinary" blocks. Let's define this as the number of highest
11701177
// size blocks. (subtract one from each, since there is a sentinel node in each list)
11711178
info.ordblks = count_linked_list_size(&freeRegionBuckets[NUM_FREE_BUCKETS-1])-1;
@@ -1227,73 +1234,118 @@ struct mallinfo emmalloc_mallinfo() {
12271234
}
12281235
EMMALLOC_ALIAS(mallinfo, emmalloc_mallinfo);
12291236

1230-
#if 0
12311237
// Note! This function is not fully multithreading safe: while this function is running, other threads should not be
12321238
// allowed to call sbrk()!
12331239
static int trim_dynamic_heap_reservation(size_t pad) {
12341240
ASSERT_MALLOC_IS_ACQUIRED();
12351241

12361242
if (!listOfAllRegions) {
1243+
#ifdef EMMALLOC_VERBOSE
1244+
MAIN_THREAD_ASYNC_EM_ASM(out('emmalloc_trim(): cannot trim memory, emmalloc is currently not initialized to manage any dynamic memory at all.'));
1245+
#endif
12371246
return 0; // emmalloc is not controlling any dynamic memory at all - cannot release memory.
12381247
}
12391248
uint8_t *previousSbrkEndAddress = listOfAllRegions->endPtr;
1240-
assert(sbrk(0) == previousSbrkEndAddress);
1249+
void *sbrkAddr = _sbrk64(0);
1250+
#ifdef EMMALLOC_VERBOSE
1251+
MAIN_THREAD_ASYNC_EM_ASM(out('emmalloc_trim(): _sbrk64(0) = ' + ptrToString($0) + ', previousSbrkEndAddress = ' + ptrToString($1)), sbrkAddr, previousSbrkEndAddress);
1252+
#endif
1253+
assert(sbrkAddr == previousSbrkEndAddress);
12411254
size_t lastMemoryRegionSize = ((size_t*)previousSbrkEndAddress)[-1];
1242-
assert(lastMemoryRegionSize == 16); // // The last memory region should be a sentinel node of exactly 16 bytes in size.
1243-
Region *endSentinelRegion = (Region*)(previousSbrkEndAddress - sizeof(Region));
1255+
Region *endSentinelRegion = (Region*)(previousSbrkEndAddress - lastMemoryRegionSize);
12441256
Region *lastActualRegion = prev_region(endSentinelRegion);
12451257

1246-
// Round padding up to multiple of 4 bytes to keep sbrk() and memory region alignment intact.
1247-
// Also have at least 8 bytes of payload so that we can form a full free region.
1248-
size_t newRegionSize = (size_t)ALIGN_UP(pad, 4);
1249-
if (pad > 0) {
1250-
newRegionSize += sizeof(Region) - (newRegionSize - pad);
1258+
if (!region_is_free(lastActualRegion)) {
1259+
#ifdef EMMALLOC_VERBOSE
1260+
MAIN_THREAD_ASYNC_EM_ASM(out('emmalloc_trim(): Last actual region ' + ptrToString($0) + ' is in use, there is nothing to trim from.'), lastActualRegion);
1261+
#endif
1262+
return 0;
12511263
}
12521264

1253-
if (!region_is_free(lastActualRegion) || lastActualRegion->size <= newRegionSize) {
1254-
return 0; // Last actual region is in use, or caller desired to leave more free memory intact than there is.
1265+
// Sanitize odd alignments for padding values - this is the minimum alignment
1266+
// that emmalloc could handle. Align up to be conservative towards caller.
1267+
pad = (size_t)ALIGN_UP(pad, 4);
1268+
1269+
// Calculate how many bytes we can shrink the sbrk() reservation by.
1270+
// Is the last free region smaller than what was requested to be left behind?
1271+
// If so, then there is nothing we can trim.
1272+
if (pad >= lastActualRegion->size) {
1273+
#ifdef EMMALLOC_VERBOSE
1274+
MAIN_THREAD_ASYNC_EM_ASM(out('emmalloc_trim(): Last actual region does not have enough space to leave ' + Number($0) + ' bytes of free memory in it.'), pad);
1275+
#endif
1276+
return 0;
12551277
}
12561278

1257-
// This many bytes will be shrunk away.
1258-
size_t shrinkAmount = lastActualRegion->size - newRegionSize;
1259-
assert(HAS_ALIGNMENT(shrinkAmount, 4));
1279+
// Subtract region size members off to calculate the excess bytes in payload.
1280+
size_t shrinkAmount = lastActualRegion->size - pad - 2*sizeof(size_t);
1281+
// sbrk() alignment is multiple of __alignof__(max_align_t), so round the
1282+
// trimming down to ensure that alignment is preserved.
1283+
#ifdef EMMALLOC_VERBOSE
1284+
MAIN_THREAD_ASYNC_EM_ASM(out('emmalloc_trim(): shrinkAmount ' + Number($0) + '.'), shrinkAmount);
1285+
#endif
1286+
shrinkAmount = (size_t)ALIGN_DOWN(shrinkAmount, __alignof__(max_align_t));
1287+
#ifdef EMMALLOC_VERBOSE
1288+
MAIN_THREAD_ASYNC_EM_ASM(out('emmalloc_trim(): shrinkAmount2 ' + Number($0) + '.'), shrinkAmount);
1289+
#endif
1290+
// Nothing left to trim?
1291+
if (!shrinkAmount) {
1292+
#ifdef EMMALLOC_VERBOSE
1293+
MAIN_THREAD_ASYNC_EM_ASM(out('emmalloc_trim(): Aligning for sbrk() requirements removed opportunity to trim.'));
1294+
#endif
1295+
return 0;
1296+
}
12601297

12611298
unlink_from_free_list(lastActualRegion);
1262-
// If pad == 0, we should delete the last free region altogether. If pad > 0,
1263-
// shrink the last free region to the desired size.
1264-
if (newRegionSize > 0) {
1299+
1300+
size_t newRegionSize = lastActualRegion->size - shrinkAmount;
1301+
1302+
#ifdef EMMALLOC_VERBOSE
1303+
MAIN_THREAD_ASYNC_EM_ASM(out('emmalloc_trim(): Shrinking ' + Number($0) + ' bytes off the free heap end region. New free heap end region size: ' + Number($1) + ' bytes.'), shrinkAmount, newRegionSize);
1304+
#endif
1305+
// If we can't fit a free Region in the shrunk space, we should delete the
1306+
// the last free region altogether.
1307+
if (newRegionSize >= sizeof(Region)) {
12651308
create_free_region(lastActualRegion, newRegionSize);
12661309
link_to_free_list(lastActualRegion);
1310+
#ifdef EMMALLOC_VERBOSE
1311+
MAIN_THREAD_ASYNC_EM_ASM(out('emmalloc_trim(): Created new free heap end region at ' + ptrToString($0) + '. Size: ' + Number($1) + ' bytes.'), lastActualRegion, newRegionSize);
1312+
#endif
1313+
} else {
1314+
#ifdef EMMALLOC_VERBOSE
1315+
MAIN_THREAD_ASYNC_EM_ASM(out('emmalloc_trim(): Not enough room to fit a free heap end region. Discarding it altogether.'));
1316+
#endif
1317+
newRegionSize = 0;
12671318
}
12681319

1269-
// Recreate the sentinel region at the end of the last free region
1270-
endSentinelRegion = (Region*)((uint8_t*)lastActualRegion + newRegionSize);
1271-
create_used_region(endSentinelRegion, sizeof(Region));
1320+
// Call sbrk() to shrink the memory area.
1321+
void *oldSbrk = _sbrk64(-(int64_t)shrinkAmount);
1322+
assert((intptr_t)oldSbrk != -1); // Shrinking with sbrk() should never fail.
12721323

1273-
// And update the size field of the whole region block.
1274-
listOfAllRegions->endPtr = (uint8_t*)endSentinelRegion + sizeof(Region);
1324+
// Ask where sbrk got us at.
1325+
void *sbrkNow = _sbrk64(0);
12751326

1276-
// Finally call sbrk() to shrink the memory area.
1277-
void *oldSbrk = sbrk(-(intptr_t)shrinkAmount);
1278-
assert((intptr_t)oldSbrk != -1); // Shrinking with sbrk() should never fail.
1279-
assert(oldSbrk == previousSbrkEndAddress); // Another thread should not have raced to increase sbrk() on us!
1327+
// Recreate the sentinel region at the end of the last free region.
1328+
Region *newEndSentinelRegion = (Region*)((uint8_t*)lastActualRegion + newRegionSize);
1329+
size_t newEndSentinelRegionSize = (uintptr_t)sbrkNow - (uintptr_t)newEndSentinelRegion;
1330+
1331+
#ifdef EMMALLOC_VERBOSE
1332+
MAIN_THREAD_ASYNC_EM_ASM(out('emmalloc_trim(): Created new sentinel end region at ' + ptrToString($0) + '. Size: ' + Number($1) + ' bytes.'), newEndSentinelRegion, newEndSentinelRegionSize);
1333+
#endif
1334+
1335+
create_used_region(newEndSentinelRegion, newEndSentinelRegionSize);
1336+
1337+
// And update the size field of the whole region block.
1338+
listOfAllRegions->endPtr = (uint8_t*)newEndSentinelRegion + newEndSentinelRegionSize;
12801339

12811340
// All successful, and we actually trimmed memory!
12821341
return 1;
12831342
}
1284-
#endif
12851343

12861344
int emmalloc_trim(size_t pad) {
1287-
// Reducing the size of the sbrk region is currently broken.
1288-
// See https://github.com/emscripten-core/emscripten/issues/23343
1289-
// And https://github.com/emscripten-core/emscripten/pull/13442
1290-
return 0;
1291-
/*
12921345
MALLOC_ACQUIRE();
12931346
int success = trim_dynamic_heap_reservation(pad);
12941347
MALLOC_RELEASE();
12951348
return success;
1296-
*/
12971349
}
12981350
EMMALLOC_ALIAS(malloc_trim, emmalloc_trim)
12991351

@@ -1376,5 +1428,5 @@ void emmalloc_dump_free_dynamic_memory_fragmentation_map() {
13761428
}
13771429

13781430
size_t emmalloc_unclaimed_heap_memory(void) {
1379-
return emscripten_get_heap_max() - (size_t)sbrk(0);
1431+
return emscripten_get_heap_max() - (size_t)_sbrk64(0);
13801432
}

system/lib/libc/sbrk.c

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,25 @@ uintptr_t* emscripten_get_sbrk_ptr() {
5656
#define READ_SBRK_PTR(sbrk_ptr) (*(sbrk_ptr))
5757
#endif
5858

59-
void *sbrk(intptr_t increment_) {
60-
uintptr_t increment = (uintptr_t)increment_;
61-
increment = (increment + (SBRK_ALIGNMENT-1)) & ~(SBRK_ALIGNMENT-1);
59+
void *_sbrk64(int64_t increment) {
60+
if (increment >= 0) {
61+
increment = (increment + (SBRK_ALIGNMENT-1)) & ~((int64_t)SBRK_ALIGNMENT-1);
62+
} else {
63+
increment = -(-increment & ~((int64_t)SBRK_ALIGNMENT-1));
64+
}
65+
6266
uintptr_t *sbrk_ptr = (uintptr_t*)emscripten_get_sbrk_ptr();
6367

6468
// To make sbrk thread-safe, implement a CAS loop to update the
6569
// value of sbrk_ptr.
6670
while (1) {
6771
uintptr_t old_brk = READ_SBRK_PTR(sbrk_ptr);
68-
uintptr_t new_brk = old_brk + increment;
69-
// Check for a) an overflow, which would indicate that we are trying to
70-
// allocate over maximum addressable memory. and b) if necessary,
72+
int64_t new_brk64 = (int64_t)old_brk + increment;
73+
uintptr_t new_brk = (uintptr_t)new_brk64;
74+
// Check for a) an over/underflow, which would indicate that we are
75+
// allocating over maximum addressable memory. and b) if necessary,
7176
// increase the WebAssembly Memory size, and abort if that fails.
72-
if ((increment > 0 && new_brk <= old_brk)
77+
if (new_brk < 0 || new_brk64 != (int64_t)new_brk
7378
|| (new_brk > emscripten_get_heap_size() && !emscripten_resize_heap(new_brk))) {
7479
errno = ENOMEM;
7580
return (void*)-1;
@@ -93,6 +98,26 @@ void *sbrk(intptr_t increment_) {
9398
}
9499
}
95100

101+
void *sbrk(intptr_t increment_) {
102+
#if defined(__wasm64__) // TODO || !defined(wasm2gb)
103+
// In the correct https://linux.die.net/man/2/sbrk spec, sbrk() parameter is
104+
// intended to be treated as signed, meaning that it is not possible in a
105+
// 32-bit program to sbrk alloc (or dealloc) more than 2GB of memory at once.
106+
107+
// Treat sbrk() parameter as signed.
108+
return _sbrk64((int64_t)increment_);
109+
#else
110+
// BUG: Currently the Emscripten test suite codifies expectations that sbrk()
111+
// values passed to this function are to be treated as unsigned, which means
112+
// that in 2GB and 4GB build modes, it is not possible to shrink memory.
113+
// To satisfy that mode, treat sbrk() parameters in 32-bit builds as unsigned.
114+
// https://github.com/emscripten-core/emscripten/issues/25138
115+
116+
// Treat sbrk() parameter as unsigned.
117+
return _sbrk64((int64_t)(uintptr_t)increment_);
118+
#endif
119+
}
120+
96121
int brk(void* ptr) {
97122
#ifdef __EMSCRIPTEN_SHARED_MEMORY__
98123
// FIXME

test/code_size/test_codesize_cxx_ctors1.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.out.js": 19754,
33
"a.out.js.gz": 8162,
4-
"a.out.nodebug.wasm": 129504,
5-
"a.out.nodebug.wasm.gz": 49232,
6-
"total": 149258,
7-
"total_gz": 57394,
4+
"a.out.nodebug.wasm": 129517,
5+
"a.out.nodebug.wasm.gz": 49253,
6+
"total": 149271,
7+
"total_gz": 57415,
88
"sent": [
99
"__cxa_throw",
1010
"_abort_js",

test/code_size/test_codesize_cxx_ctors2.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.out.js": 19732,
33
"a.out.js.gz": 8148,
4-
"a.out.nodebug.wasm": 128931,
5-
"a.out.nodebug.wasm.gz": 48876,
6-
"total": 148663,
7-
"total_gz": 57024,
4+
"a.out.nodebug.wasm": 128944,
5+
"a.out.nodebug.wasm.gz": 48891,
6+
"total": 148676,
7+
"total_gz": 57039,
88
"sent": [
99
"__cxa_throw",
1010
"_abort_js",

test/code_size/test_codesize_cxx_except.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.out.js": 23415,
33
"a.out.js.gz": 9145,
4-
"a.out.nodebug.wasm": 171266,
5-
"a.out.nodebug.wasm.gz": 57323,
6-
"total": 194681,
7-
"total_gz": 66468,
4+
"a.out.nodebug.wasm": 171279,
5+
"a.out.nodebug.wasm.gz": 57348,
6+
"total": 194694,
7+
"total_gz": 66493,
88
"sent": [
99
"__cxa_begin_catch",
1010
"__cxa_end_catch",

test/code_size/test_codesize_cxx_except_wasm.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.out.js": 19643,
33
"a.out.js.gz": 8112,
4-
"a.out.nodebug.wasm": 144625,
5-
"a.out.nodebug.wasm.gz": 54883,
6-
"total": 164268,
7-
"total_gz": 62995,
4+
"a.out.nodebug.wasm": 144638,
5+
"a.out.nodebug.wasm.gz": 54898,
6+
"total": 164281,
7+
"total_gz": 63010,
88
"sent": [
99
"_abort_js",
1010
"_tzset_js",

test/code_size/test_codesize_cxx_except_wasm_legacy.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.out.js": 19643,
33
"a.out.js.gz": 8112,
4-
"a.out.nodebug.wasm": 142214,
5-
"a.out.nodebug.wasm.gz": 54349,
6-
"total": 161857,
7-
"total_gz": 62461,
4+
"a.out.nodebug.wasm": 142227,
5+
"a.out.nodebug.wasm.gz": 54362,
6+
"total": 161870,
7+
"total_gz": 62474,
88
"sent": [
99
"_abort_js",
1010
"_tzset_js",

test/code_size/test_codesize_cxx_mangle.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.out.js": 23465,
33
"a.out.js.gz": 9164,
4-
"a.out.nodebug.wasm": 235307,
5-
"a.out.nodebug.wasm.gz": 78924,
6-
"total": 258772,
7-
"total_gz": 88088,
4+
"a.out.nodebug.wasm": 235320,
5+
"a.out.nodebug.wasm.gz": 78938,
6+
"total": 258785,
7+
"total_gz": 88102,
88
"sent": [
99
"__cxa_begin_catch",
1010
"__cxa_end_catch",

test/code_size/test_codesize_cxx_noexcept.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.out.js": 19754,
33
"a.out.js.gz": 8162,
4-
"a.out.nodebug.wasm": 131921,
5-
"a.out.nodebug.wasm.gz": 50229,
6-
"total": 151675,
7-
"total_gz": 58391,
4+
"a.out.nodebug.wasm": 131934,
5+
"a.out.nodebug.wasm.gz": 50245,
6+
"total": 151688,
7+
"total_gz": 58407,
88
"sent": [
99
"__cxa_throw",
1010
"_abort_js",

test/code_size/test_codesize_cxx_wasmfs.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.out.js": 7143,
33
"a.out.js.gz": 3338,
4-
"a.out.nodebug.wasm": 169792,
5-
"a.out.nodebug.wasm.gz": 63078,
6-
"total": 176935,
7-
"total_gz": 66416,
4+
"a.out.nodebug.wasm": 169805,
5+
"a.out.nodebug.wasm.gz": 63096,
6+
"total": 176948,
7+
"total_gz": 66434,
88
"sent": [
99
"__cxa_throw",
1010
"_abort_js",

0 commit comments

Comments
 (0)