Memory leak when using WeakMap (heap grows despite GC)
Build platform
esp32 idf (5.4.1)
Test case
#include "jerryscript.h"
#include <stdio.h>
#include <string.h>
#include "assert.h"
void
app_main() {
assert(jerry_feature_enabled(JERRY_FEATURE_HEAP_STATS));
assert(jerry_feature_enabled(JERRY_FEATURE_WEAKMAP));
const jerry_char_t script[] =
"var wm = new WeakMap();\n"
"for (var i = 0; i < 1000; i++) {\n"
" wm.set({}, 1);\n"
"}\n";
jerry_init(JERRY_INIT_MEM_STATS );
jerry_value_t parsed_code = jerry_parse(script, sizeof(script) - 1, NULL);
jerry_heap_stats_t heapStats;
jerry_heap_stats(&heapStats);
printf("Heap allocated before: %zu\n", heapStats.allocated_bytes); // 424
jerry_value_t run_val = jerry_run(parsed_code);
jerry_value_free(run_val);
jerry_heap_gc(JERRY_GC_PRESSURE_HIGH);
jerry_heap_stats(&heapStats);
printf("Heap allocated after: %zu\n", heapStats.allocated_bytes); // 8600
jerry_value_free(parsed_code);
jerry_cleanup();
}
Observed behavior
Example run:
Heap allocated before: 424 Heap allocated after: 8600
Even after forcing jerry_heap_gc(JERRY_GC_PRESSURE_HIGH), the allocated heap size remains significantly higher than before script execution.
Expected behavior
Since the keys passed to WeakMap.set are ephemeral object literals ({}) with no references kept outside, they should be eligible for garbage collection. Memory usage should return close to the baseline after gc().
As far as I can understand, weak map container buffer always grow (ecma_collection_append), but never shrinks (GCed items are replaced by ECMA_VALUE_EMPTY)
note that all containers are affected (Map, Set, WeakMap, WeakSet)
quick POC: defragment and shrink container buffer.
update ecma_op_internal_buffer_delete() :
https://github.com/jerryscript-project/jerryscript/blob/355ab24cdc0501e0fdb3a97be69ea94835301eea/jerry-core/ecma/operations/ecma-container-object.c#L100-L121
with:
/**
* Delete element from the internal buffer.
*/
#define CONTAINER_SHRINK_FACTOR_THRESHOLD 2
#define ECMA_CONTAINER_SET_ENTRY_COUNT(collection_p, count) (collection_p->item_count = (count) + 1) // see ECMA_CONTAINER_ENTRY_COUNT
static void
ecma_op_internal_buffer_delete (ecma_collection_t *container_p, /**< internal container pointer */
ecma_container_pair_t *entry_p, /**< entry pointer */
lit_magic_string_id_t lit_id) /**< class id */
{
JERRY_ASSERT (container_p != NULL);
JERRY_ASSERT (entry_p != NULL);
ecma_free_value_if_not_object (entry_p->key);
entry_p->key = ECMA_VALUE_EMPTY;
if (lit_id == LIT_MAGIC_STRING_WEAKMAP_UL || lit_id == LIT_MAGIC_STRING_MAP_UL)
{
ecma_free_value_if_not_object (entry_p->value);
entry_p->value = ECMA_VALUE_EMPTY;
}
ECMA_CONTAINER_SET_SIZE (container_p, ECMA_CONTAINER_GET_SIZE (container_p) - 1);
/* maybe defragment the container */
uint32_t pair_count = ECMA_CONTAINER_ENTRY_COUNT(container_p) / ECMA_CONTAINER_PAIR_SIZE;
if ( ECMA_CONTAINER_GET_SIZE (container_p) < pair_count / CONTAINER_SHRINK_FACTOR_THRESHOLD && pair_count > ECMA_COLLECTION_INITIAL_CAPACITY )
{
ecma_container_pair_t *start_p = (ecma_container_pair_t*)ECMA_CONTAINER_START (container_p);
uint32_t pair_write_index = 0;
for ( uint32_t pair_read_index = 0; pair_read_index < pair_count; ++pair_read_index )
{
ecma_container_pair_t *read_entry_p = start_p + pair_read_index;
if ( !ecma_is_value_empty (read_entry_p->key) )
{
if ( pair_write_index != pair_read_index )
{
*(start_p + pair_write_index) = *read_entry_p;
}
pair_write_index++;
}
}
ECMA_CONTAINER_SET_ENTRY_COUNT(container_p, pair_write_index * ECMA_CONTAINER_PAIR_SIZE);
/* realloc container buffer */
const uint32_t newCapacity = container_p->item_count + ECMA_COLLECTION_INITIAL_CAPACITY;
const uint32_t old_size = ECMA_COLLECTION_ALLOCATED_SIZE(container_p->capacity);
const uint32_t new_size = ECMA_COLLECTION_ALLOCATED_SIZE(newCapacity);
container_p->buffer_p = jmem_heap_realloc_block (container_p->buffer_p, old_size, new_size);
JERRY_ASSERT (container_p->buffer_p != NULL);
container_p->capacity = newCapacity;
}
} /* ecma_op_internal_buffer_delete */
result
Heap allocated before: 504 Heap allocated after: 688