Skip to content

[wasm] High memory fragmentation on LOS allocations #118044

@andrew-kulikov

Description

@andrew-kulikov

Description

I am porting a huge .NET application (a game written on custom engine) to browser-wasm runtime and noticed that heap fragmentation is pretty high. The initial idea was that this may be caused by major heap blocks fragmentation. I tried to play with MONO_GC_PARAMS increasing/decreasing nursery-size, evacuation-threshold and soft-heap-limit, and triggering gc frequently. But there was no noticeable difference in FragmentedBytes metric.

Then I used emscripted memory profiler and found that there is a lot of allocation with stack traces like this:

at dotnet.native.wasm.dlmalloc (http://localhost:8081/_framework/dotnet.native.wasm:wasm-function[12628]:0x29d1ca)
at dotnet.native.wasm.internal_memalign (http://localhost:8081/_framework/dotnet.native.wasm:wasm-function[12634]:0x29e12b)
at dotnet.native.wasm.dlposix_memalign (http://localhost:8081/_framework/dotnet.native.wasm:wasm-function[12635]:0x29e2d8)
at dotnet.native.wasm.mono_valloc_aligned (http://localhost:8081/_framework/dotnet.native.wasm:wasm-function[931]:0x4af61)
at dotnet.native.wasm.sgen_alloc_os_memory_aligned (http://localhost:8081/_framework/dotnet.native.wasm:wasm-function[1508]:0x6b380)
at dotnet.native.wasm.sgen_los_alloc_large_inner (http://localhost:8081/_framework/dotnet.native.wasm:wasm-function[1424]:0x6094c)
at dotnet.native.wasm.sgen_alloc_obj_nolock (http://localhost:8081/_framework/dotnet.native.wasm:wasm-function[1280]:0x57658)

I prepared a sample repo with reproduction. The sample is named "Los fragmentation", and it allocates a lot of byte arrays of configured size. After playing with parameters I found that if array size is greater than 7984 (SGEN_MAX_SMALL_OBJ_SIZE - mono object size), fragmentation grows significantly. Sample code:

[JSExport]
public static async Task LosFragmentation(int count, int batchSize, int size)
{
    var losHolder = new List<byte[]>();
    for (int i = 0; i < count; i++)
    {
        PrintMemory($"LF #{i} - start");

        for (int j = 0; j < batchSize; j++)
        {
            losHolder.Add(new byte[size]);
        }
        await Task.Delay(16);
        GC.Collect();
        PrintMemory($"LF #{i} - end");
    }
}

Here is output with different parameters:

1. count=10, batchSize=1000, size=8192
LF #9 - end | Total Memory: 82.37 MB | HeapSizeBytes: 236.64 MB | FragmentedBytes: 158.29 MB | TotalCommittedBytes: 78.35 MB | G0: 0 | G1: 10 | G2: 10 | 

1. count=10, batchSize=1000, size=7984
LF #9 - end | Total Memory: 80.39 MB | HeapSizeBytes: 80.77 MB | FragmentedBytes:  | TotalCommittedBytes: 84.00 MB | G0: 10 | G1: 10 | G2: 10 | 

Configuration

.NET 10.0.100-preview.5.25277.114
Chrome 138.0.7204.158

Data

7984:
Image

8192:
Image

Workaround

Please explain if it is expected behavior, are there any known workarounds (including building custom runtime) and is there any planned work on this issue.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions