[wasm] [aot] Memory leak when interpreting expressions

Open
#118,675 3 comments 0 reactions 1 assignee View on GitHub

@pavelsavara is already working on this.

Since Aug 14, 2025.

Assessment

This issue has not been assessed yet.

Description

arch-wasm area-Codegen-Interpreter-mono needs-further-triage tenet-performance
Description

I am porting a huge .NET application (a game written on a custom engine) to browser-wasm runtime. In release build we use AOT, and I noticed that in such configuration SBRK pointer slightly grows during app lifetime while managed heap size stays the same. Investigation with emscripten memory profiler helped me to find some problems, so listing them here.

In our app we use Autofac, which uses expressions under the hood a lot (for example, here and here). The typical use case with Autofac is - on feature launch we create a child scope and register feature-specific services in that scope. As the result each feature launch allocates from few hundred KB to few MB, most of them are compiled delegates and generic vtables. Newtonsoft.Json also uses expressions, but I assume they are cached for entire app lifetime.

I played with expressions on wasm and found a few problems and/or unclear moments. Repo with reproduction samples can be found here.

1. Dynamic code is not disabled by default in aot mode

When RunAOTCompilation is set to true, System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported is still set to true, which causes expression compiler to emit new types on each expression compile. I found that is is set to true in sdk targets, but is not overriden in mono.
https://github.com/dotnet/sdk/blob/8f65fb8192e4828338f87cedd19d9a41ace507b6/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets#L658-L661

I found that mono does support overriding this property https://github.com/dotnet/runtime/blob/b86266dc5a2de9add4c34fcea78765cb0eed1ca9/src/mono/mono/mini/intrinsics.c#L2445-L2457, but on wasm it seems to be never executed.

2. Memory leaks even when dynamic code is disabled

Sample code that leaks when dynamic code is enabled:

[JSExport]
public static async Task CompiledFuncExpressions(int count, bool doGc)
{
    for (int i = 0; i < count; i++)
    {
        var objects = new List<object>();
        for (int j = 0; j < 100; j++)
        {
            var func = Expression.Lambda<Func<object, object>>(Expression.Constant(new object()), Expression.Parameter(typeof(object))).Compile();
            var obj = func(new object());
            objects.Add(obj);
        }
        objects = null;
        if (doGc)
        {
            DoGC();
        }
        PrintMemory($"#{i}");
        await Task.Delay(16);
    }
}

I disabled dynamic code with <DynamicCodeSupport>false</DynamicCodeSupport>, then recompiled sample with aot compilation enabled. It significantly reduced amount of memory leaked during test, but there are still leaked allocations:

Allocation sites with more than 16 MB of accumulated allocations, or more than 100 simultaneously outstanding allocations:
1.13 MB/144 allocs: at dotnet.native.wasm.dlmalloc (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[79470]:0xf11ff7)
at dotnet.native.wasm.monoeg_malloc (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[74637]:0xe1839d)
at dotnet.native.wasm.mono_mempool_alloc (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[76984]:0xe95f15)
at dotnet.native.wasm.mono_mempool_alloc0 (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[76985]:0xe95f87)
at dotnet.native.wasm.mono_mem_manager_alloc0 (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[78001]:0xec6a43)
at dotnet.native.wasm.mini_llvmonly_create_ftndesc (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[78848]:0xef26de)
at dotnet.native.wasm.mini_llvmonly_init_delegate (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[78871]:0xef35f6)
at dotnet.native.wasm.mini_init_delegate (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[78398]:0xed7b8f)
at dotnet.native.wasm.mono_delegate_ctor (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[77455]:0xeac02d)
at dotnet.native.wasm.ves_icall_System_Delegate_CreateDelegate_internal (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[76444]:0xe7d603)
at dotnet.native.wasm.ves_icall_System_Delegate_CreateDelegate_internal_raw (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[76483]:0xe7e7de)
at dotnet.native.wasm.aot_wrapper_corlib_System_System_dot_Delegate__CreateDelegate_internal_pinvoke_cls8_Delegate__cls2a_Runtime_dCompilerServices_dQCallTypeHandle_objcls16_Reflection_dMethodInfo_boolcls8_Delegate__cls2a_Runtime_dCompilerServices_dQCallTypeHandle_objcls16_Reflection_dMethodInfo_bool (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[29105]:0x53cf14)
at dotnet.native.wasm.corlib_System_Delegate_CreateDelegate_System_Type_object_System_Reflection_MethodInfo_bool_bool (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[29101]:0x53c1f0)
at dotnet.native.wasm.corlib_System_Delegate_CreateDelegate_System_Type_object_System_Reflection_MethodInfo_bool (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[29100]:0x53bfd4)
at dotnet.native.wasm.corlib_System_Delegate_CreateDelegate_System_Type_object_System_Reflection_MethodInfo (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[29122]:0x53dbc2)
at dotnet.native.wasm.corlib_System_Reflection_RuntimeMethodInfo_CreateDelegate_System_Type_object (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[39493]:0x6a9e40)
at dotnet.native.wasm.System_Linq_Expressions_System_Dynamic_Utils_DelegateHelpers_CreateObjectArrayDelegateRefEmit_System_Type_System_Func_2_object___object (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[27165]:0x4ca883)
at dotnet.native.wasm.System_Linq_Expressions_System_Dynamic_Utils_DelegateHelpers_CreateObjectArrayDelegate_System_Type_System_Func_2_object___object (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[26443]:0x4a9b3d)
at dotnet.native.wasm.System_Linq_Expressions_System_Linq_Expressions_Interpreter_LightLambda_MakeDelegate_System_Type (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[26441]:0x4a99fd)
at dotnet.native.wasm.System_Linq_Expressions_System_Linq_Expressions_Interpreter_LightDelegateCreator_CreateDelegate_System_Runtime_CompilerServices_IStrongBox__ (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[26440]:0x4a98a7)
at dotnet.native.wasm.System_Linq_Expressions_System_Linq_Expressions_Interpreter_LightDelegateCreator_CreateDelegate (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[25274]:0x467cb6)
at dotnet.native.wasm.System_Linq_Expressions_System_Linq_Expressions_Expression_1_TDelegate_REF_Compile (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[25281]:0x467f5e)
at dotnet.native.wasm.memtest_Benchmarks__CompiledFuncExpressionsd__14_MoveNext (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[13500]:0x10db96)
at dotnet.native.wasm.corlib_System_Runtime_CompilerServices_AsyncTaskMethodBuilder_1_AsyncStateMachineBox_1_TResult_GSHAREDVT_TStateMachine_GSHAREDVT_ExecutionContextCallback_object (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[42619]:0x738bb4)
at dotnet.native.wasm.corlib_wrapper_delegate_invoke__Module_invoke_void_object_object (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[43446]:0x75cf40)
at dotnet.native.wasm.corlib_System_Threading_ExecutionContext_RunInternal_System_Threading_ExecutionContext_System_Threading_ContextCallback_object (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[37125]:0x65568d)
at dotnet.native.wasm.corlib_System_Runtime_CompilerServices_AsyncTaskMethodBuilder_1_AsyncStateMachineBox_1_TResult_GSHAREDVT_TStateMachine_GSHAREDVT_MoveNext_System_Threading_Thread (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[42625]:0x738f3b)
at dotnet.native.wasm.corlib_System_Runtime_CompilerServices_AsyncTaskMethodBuilder_1_AsyncStateMachineBox_1_TResult_GSHAREDVT_TStateMachine_GSHAREDVT_MoveNext (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[42624]:0x738e47)
at dotnet.native.wasm.corlib_System_Threading_Tasks_AwaitTaskContinuation_RunOrScheduleAction_System_Runtime_CompilerServices_IAsyncStateMachineBox_bool (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[37847]:0x674929)
at dotnet.native.wasm.corlib_System_Threading_Tasks_Task_RunContinuations_object (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[37845]:0x67412b)
at dotnet.native.wasm.corlib_System_Threading_Tasks_Task_FinishContinuations (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[37708]:0x66d3a1)
at dotnet.native.wasm.corlib_System_Threading_Tasks_Task_TrySetResult (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[37599]:0x66993c)
at dotnet.native.wasm.corlib_System_Threading_Tasks_Task_DelayPromise_CompleteTimedOut (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[37897]:0x6775b7)
at dotnet.native.wasm.corlib_System_Threading_Tasks_Task_DelayPromise_TimerCallback_object (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[37896]:0x677567)
at dotnet.native.wasm.corlib_wrapper_delegate_invoke__Module_invoke_void_object_object (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[43446]:0x75cf40)
at dotnet.native.wasm.corlib_System_Threading_TimerQueueTimer_CallCallback_bool (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[37597]:0x6697a9)
at dotnet.native.wasm.corlib_System_Threading_TimerQueueTimer_Fire_bool (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[37569]:0x667a48)
at dotnet.native.wasm.corlib_System_Threading_TimerQueue_FireNextTimers (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[37566]:0x66765b)
at dotnet.native.wasm.corlib_System_Threading_TimerQueue_PumpTimerQueue_long (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[37577]:0x6684c7)
at dotnet.native.wasm.corlib_System_Threading_TimerQueue_TimerHandler (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[37576]:0x668162)
at dotnet.native.wasm.corlib_wrapper_native_to_managed_System_Threading_TimerQueue_TimerHandler (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[43811]:0x76335b)
at dotnet.native.wasm.mono_wasm_execute_timer (http://localhost:8080/_framework/dotnet.native.wasm:wasm-function[78899]:0xef40f4)
at mono_wasm_schedule_timer_tick (http://localhost:8080/_framework/dotnet.runtime.js:3:57363)
at callUserCallback (http://localhost:8080/_framework/dotnet.native.js:6910:9)
at http://localhost:8080/_framework/dotnet.native.js:6929:9

Output on first and last iterations:

#0 | WHS: 38.438 MB | SBRK: 34.570 MB | TM: 4.015 MB | TAB: 0 B | G0: 0 | G1: 0 | G2: 0 | HSB: 0 B | FB: 0 B | TCB: 0 B
...
#999 | WHS: 38.438 MB | SBRK: 37.230 MB | TM: 4.079 MB | TAB: 123.158 MB | G0: 31 | G1: 0 | G2: 0 | HSB: 688.000 KB | FB: 606.930 KB | TCB: 81.070 KB
Image
3. System.Linq.Expressions.CanEmitObjectArrayDelegate is not supported in mono

From the stack trace I found DelegateHelpers class that declares System.Linq.Expressions.CanEmitObjectArrayDelegate runtime flag - https://github.com/dotnet/runtime/blob/b86266dc5a2de9add4c34fcea78765cb0eed1ca9/src/libraries/System.Linq.Expressions/src/System/Dynamic/Utils/DelegateHelpers.cs#L16-L17

NativeAOT sets this property to false. I tried to disable it in my project with

<ItemGroup>
  <RuntimeHostConfigurationOption Include="System.Linq.Expressions.CanEmitObjectArrayDelegate" Value="false" Trim="true" />
</ItemGroup>

, but it started to crash because of missing native method:

dotnet.runtime.js:3 Uncaught (in promise) ManagedError: Could not find CreateObjectArrayDelegate
Configuration

.NET 10.0.100-preview.5.25277.114
Chrome 138.0.7204.158

Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/runtime

All issues in dotnet/runtime

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.