jerryscript icon indicating copy to clipboard operation
jerryscript copied to clipboard

Memory leak when using WeakMap (heap grows despite GC)

Open FranckFreiburger opened this issue 4 months ago • 2 comments

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().

FranckFreiburger avatar Aug 23 '25 20:08 FranckFreiburger

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)

FranckFreiburger avatar Aug 28 '25 08:08 FranckFreiburger

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

FranckFreiburger avatar Aug 28 '25 16:08 FranckFreiburger