diff --git a/src/coreclr/gc/CMakeLists.txt b/src/coreclr/gc/CMakeLists.txt index e28391221f66ca..9b48e6ddd4ded6 100644 --- a/src/coreclr/gc/CMakeLists.txt +++ b/src/coreclr/gc/CMakeLists.txt @@ -25,7 +25,9 @@ set(GC_SOURCES gcbridge.cpp handletablecache.cpp) -if(CLR_CMAKE_HOST_UNIX) +if(CLR_CMAKE_TARGET_ARCH_WASM) + add_subdirectory(wasm) +elseif(CLR_CMAKE_HOST_UNIX) add_subdirectory(unix) include(unix/configure.cmake) else() @@ -33,7 +35,7 @@ else() set (GC_SOURCES ${GC_SOURCES} windows/Native.rc) -endif(CLR_CMAKE_HOST_UNIX) +endif() if (CLR_CMAKE_TARGET_ARCH_ARM64 OR CLR_CMAKE_TARGET_ARCH_AMD64) add_subdirectory(vxsort) diff --git a/src/coreclr/gc/init.cpp b/src/coreclr/gc/init.cpp index ccf0b35b3d312c..25727c7cbe4c3a 100644 --- a/src/coreclr/gc/init.cpp +++ b/src/coreclr/gc/init.cpp @@ -914,11 +914,19 @@ HRESULT gc_heap::initialize_gc (size_t soh_segment_size, return E_OUTOFMEMORY; if (use_large_pages_p) { -#ifndef HOST_64BIT +#if !defined(HOST_64BIT) && !defined(HOST_WASM) // Large pages are not supported on 32bit + // except WASM, which uses the large-pages code path to skip decommit assert (false); -#endif //!HOST_64BIT - +#endif //!HOST_64BIT && !HOST_WASM + +#ifndef HOST_WASM + // On real large-page platforms the OS pre-committed all the memory during + // reserve_initial_memory, so we tighten the hard limit to match the + // actual segment sizes. On WASM, use_large_pages_p is only a decommit- + // skip flag — the original auto-detected hard limit (75% of WASM linear + // memory max) must be preserved so that bookkeeping commits and later + // allocations have room within the limit. if (heap_hard_limit_oh[soh]) { heap_hard_limit_oh[soh] = soh_segment_size * number_of_heaps; @@ -931,6 +939,7 @@ HRESULT gc_heap::initialize_gc (size_t soh_segment_size, assert (heap_hard_limit); heap_hard_limit = (soh_segment_size + loh_segment_size + poh_segment_size) * number_of_heaps; } +#endif //HOST_WASM } #endif //USE_REGIONS @@ -1280,6 +1289,10 @@ bool gc_heap::compute_hard_limit() #ifdef HOST_64BIT use_large_pages_p = GCConfig::GetGCLargePages(); +#elif defined(HOST_WASM) + // On WASM, reserve == commit (posix_memalign allocates real memory) and there is + // no way to decommit. Enabling the large-pages path makes the GC skip VirtualDecommit. + use_large_pages_p = true; #endif //HOST_64BIT if (heap_hard_limit_oh[soh] || heap_hard_limit_oh[loh] || heap_hard_limit_oh[poh]) @@ -1368,9 +1381,11 @@ bool gc_heap::compute_hard_limit() bool gc_heap::compute_memory_settings(bool is_initialization, uint32_t& nhp, uint32_t nhp_from_config, size_t& seg_size_from_config, size_t new_current_total_committed) { -#ifdef HOST_64BIT +#if defined(HOST_64BIT) || defined(HOST_WASM) // If the hard limit is specified, the user is saying even if the process is already // running in a container, use this limit for the GC heap. + // On WASM, the linear memory has a hard ceiling set in the .wasm file, enforced by + // the engine — semantically equivalent to a container memory limit. if (!hard_limit_config_p) { if (is_restricted_physical_mem) @@ -1387,7 +1402,7 @@ bool gc_heap::compute_memory_settings(bool is_initialization, uint32_t& nhp, uin } } } -#endif //HOST_64BIT +#endif //HOST_64BIT || HOST_WASM if (heap_hard_limit && (heap_hard_limit < new_current_total_committed)) { @@ -1431,6 +1446,17 @@ bool gc_heap::compute_memory_settings(bool is_initialization, uint32_t& nhp, uin // 0 <= soh_segment_size <= 1Gb size_t limit_to_check = (heap_hard_limit_oh[soh] ? heap_hard_limit_oh[soh] : heap_hard_limit); soh_segment_size = max (adjust_segment_size_hard_limit (limit_to_check, nhp), seg_size_from_config); +#ifdef HOST_WASM + // On WASM, VirtualReserve allocates real memory (no virtual memory). + // Cap segment size so all 3 initial segments (SOH + LOH + POH) fit within + // the hard limit with room to grow. On 32-bit without per-OH limits, + // LOH and POH segments equal soh_segment_size, so total = 3 * soh. + { + size_t max_seg = round_down_power2 (heap_hard_limit / (3 * 2 * nhp)); + max_seg = max (max_seg, (size_t)(1024 * 1024)); + soh_segment_size = min (soh_segment_size, max_seg); + } +#endif //HOST_WASM } else { diff --git a/src/coreclr/gc/interface.cpp b/src/coreclr/gc/interface.cpp index 5300d34848d22d..6aece90dd75391 100644 --- a/src/coreclr/gc/interface.cpp +++ b/src/coreclr/gc/interface.cpp @@ -352,7 +352,10 @@ HRESULT GCHeap::Initialize() #ifdef HOST_64BIT large_seg_size = gc_heap::use_large_pages_p ? gc_heap::soh_segment_size : gc_heap::soh_segment_size * 2; #else //HOST_64BIT +#ifndef HOST_WASM + // Large pages not supported on 32-bit (except WASM which uses it to skip decommit). assert (!gc_heap::use_large_pages_p); +#endif //HOST_WASM large_seg_size = gc_heap::soh_segment_size; #endif //HOST_64BIT pin_seg_size = large_seg_size; diff --git a/src/coreclr/gc/unix/gcenv.unix.cpp b/src/coreclr/gc/unix/gcenv.unix.cpp index 42b73e0611241a..126483bde3ec96 100644 --- a/src/coreclr/gc/unix/gcenv.unix.cpp +++ b/src/coreclr/gc/unix/gcenv.unix.cpp @@ -108,11 +108,6 @@ typedef cpuset_t cpu_set_t; #define SYSCONF_GET_NUMPROCS _SC_NPROCESSORS_ONLN #endif -#ifdef __EMSCRIPTEN__ -#include -#endif // __EMSCRIPTEN__ - - // The cached total number of CPUs that can be used in the OS. uint32_t g_totalCpuCount = 0; @@ -347,7 +342,7 @@ void GCToOSInterface::Sleep(uint32_t sleepMSec) requested.tv_nsec = (sleepMSec - requested.tv_sec * tccSecondsToMilliSeconds) * tccMilliSecondsToNanoSeconds; timespec remaining; - while (nanosleep(&requested, &remaining) == EINTR) + while (nanosleep(&requested, &remaining) == -1 && errno == EINTR) { requested = remaining; } @@ -405,7 +400,7 @@ static void* VirtualReserveInner(size_t size, size_t alignment, uint32_t flags, } pRetVal = pAlignedRetVal; -#if defined(MADV_DONTDUMP) && !defined(TARGET_WASM) +#if defined(MADV_DONTDUMP) // Do not include reserved uncommitted memory in coredump. if (!committing) { @@ -453,13 +448,9 @@ bool GCToOSInterface::VirtualRelease(void* address, size_t size) // true if it has succeeded, false if it has failed static bool VirtualCommitInner(void* address, size_t size, uint16_t node, bool newMemory) { -#ifndef TARGET_WASM bool success = mprotect(address, size, PROT_WRITE | PROT_READ) == 0; -#else - bool success = true; -#endif // !TARGET_WASM -#if defined(MADV_DONTDUMP) && !defined(TARGET_WASM) +#if defined(MADV_DONTDUMP) if (success && !newMemory) { // Include committed memory in coredump. New memory is included by default. @@ -544,13 +535,13 @@ bool GCToOSInterface::VirtualDecommit(void* address, size_t size) #endif bool bRetVal = mmap(address, size, PROT_NONE, mmapFlags, -1, 0) != MAP_FAILED; -#if defined(MADV_DONTDUMP) && !defined(TARGET_WASM) +#if defined(MADV_DONTDUMP) if (bRetVal) { // Do not include freed memory in coredump. madvise(address, size, MADV_DONTDUMP); } -#endif // defined(MADV_DONTDUMP) && !defined(TARGET_WASM) +#endif // defined(MADV_DONTDUMP) return bRetVal; } @@ -565,9 +556,6 @@ bool GCToOSInterface::VirtualDecommit(void* address, size_t size) // true if it has succeeded, false if it has failed bool GCToOSInterface::VirtualReset(void * address, size_t size, bool unlock) { -#ifdef TARGET_WASM - return true; -#else // !TARGET_WASM int st = EINVAL; #ifdef MADV_DONTDUMP @@ -586,7 +574,6 @@ bool GCToOSInterface::VirtualReset(void * address, size_t size, bool unlock) #endif // MADV_FREE return (st == 0); -#endif // !TARGET_WASM } // Check if the OS supports write watching @@ -836,7 +823,7 @@ static uint64_t GetMemorySizeMultiplier(char units) return 1; } -#if !defined(__APPLE__) && !defined(__HAIKU__) && !defined(__EMSCRIPTEN__) +#if !defined(__APPLE__) && !defined(__HAIKU__) // Try to read the MemAvailable entry from /proc/meminfo. // Return true if the /proc/meminfo existed, the entry was present and we were able to parse it. static bool ReadMemAvailable(uint64_t* memAvailable) @@ -1104,8 +1091,6 @@ uint64_t GetAvailablePhysicalMemory() { available = info.free_memory; } -#elif defined(__EMSCRIPTEN__) - available = emscripten_get_heap_max() - emscripten_get_heap_size(); #else // Linux static volatile bool tryReadMemInfo = true; diff --git a/src/coreclr/gc/wasm/CMakeLists.txt b/src/coreclr/gc/wasm/CMakeLists.txt new file mode 100644 index 00000000000000..60fa95feb92e97 --- /dev/null +++ b/src/coreclr/gc/wasm/CMakeLists.txt @@ -0,0 +1,12 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) +include_directories("../env") +include_directories("..") +include_directories("../unix") + +include(../unix/configure.cmake) + +set(GC_PAL_SOURCES + gcenv.cpp + ../unix/events.cpp) + +add_library(gc_pal OBJECT ${GC_PAL_SOURCES} ${VERSION_FILE_PATH}) diff --git a/src/coreclr/gc/wasm/gcenv.cpp b/src/coreclr/gc/wasm/gcenv.cpp new file mode 100644 index 00000000000000..ef4939901c8b40 --- /dev/null +++ b/src/coreclr/gc/wasm/gcenv.cpp @@ -0,0 +1,552 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// WASM-specific GC OS interface implementation. +// Replaces gcenv.unix.cpp when targeting WebAssembly (browser or WASI). + +#include +#include +#include +#include +#include + +#include "config.gc.h" +#include "common.h" + +#include "gcenv.structs.h" +#include "gcenv.base.h" +#include "gcenv.os.h" +#include "gcenv.ee.h" +#include "gcenv.unix.inl" +#include "gcconfig.h" + +#include + +#ifdef FEATURE_MULTITHREADING +#include +#include +#include +#endif + +#include +#include +#include + +#include "globals.h" + +#ifdef TARGET_BROWSER +#include +#endif + +// WASM memory.grow operates in 64KB pages. This is distinct from OS_PAGE_SIZE +// (the GC's page granularity), which we set to 16KB below. +static const size_t WasmPageSize = 64 * 1024; + +// The cached total number of CPUs that can be used in the OS. +// WASM is single-threaded, so this is always 1. +uint32_t g_totalCpuCount = 0; + +uint32_t g_pageSizeUnixInl = 0; + +AffinitySet g_processAffinitySet; + +// NUMA globals - WASM has no NUMA support but these are referenced by the GC. +extern "C" int g_highestNumaNode = 0; +extern "C" bool g_numaAvailable = false; + +static size_t g_RestrictedPhysicalMemoryLimit = 0; + +static int64_t g_totalPhysicalMemSize = 0; + +// Forward declarations +size_t GetRestrictedPhysicalMemoryLimit(); +bool GetPhysicalMemoryUsed(size_t* val); + +// ============================================================================ +// Initialization / Shutdown +// ============================================================================ + +bool GCToOSInterface::Initialize() +{ + g_pageSizeUnixInl = minipal_getpagesize(); + + // WASM is single-threaded + g_totalCpuCount = 1; + + if (!g_processAffinitySet.Initialize(1)) + { + return false; + } + + g_processAffinitySet.Add(0); + + // Get the physical memory size +#ifdef TARGET_BROWSER + g_totalPhysicalMemSize = (int64_t)emscripten_get_heap_max(); +#else // TARGET_WASI + // WASI doesn't have an API to query max memory. + g_totalPhysicalMemSize = 2LL * 1024 * 1024 * 1024; // 2GB +#endif + + assert(g_totalPhysicalMemSize != 0); + + return true; +} + +void GCToOSInterface::Shutdown() +{ +} + +// ============================================================================ +// Thread / Process identification +// ============================================================================ + +uint64_t GCToOSInterface::GetCurrentThreadIdForLogging() +{ + return (uint64_t)minipal_get_current_thread_id(); +} + +uint32_t GCToOSInterface::GetCurrentProcessId() +{ + return getpid(); +} + +bool GCToOSInterface::SetCurrentThreadIdealAffinity(uint16_t srcProcNo, uint16_t dstProcNo) +{ + (void)srcProcNo; + (void)dstProcNo; + return true; +} + +uint32_t GCToOSInterface::GetCurrentProcessorNumber() +{ + return 0; +} + +bool GCToOSInterface::CanGetCurrentProcessorNumber() +{ + return true; +} + +// ============================================================================ +// Debugging / Sleeping / Yielding +// ============================================================================ + +void GCToOSInterface::DebugBreak() +{ +#if __has_builtin(__builtin_debugtrap) + __builtin_debugtrap(); +#else + abort(); +#endif +} + +void GCToOSInterface::Sleep(uint32_t sleepMSec) +{ +#ifdef FEATURE_MULTITHREADING + timespec requested = + { + static_cast(sleepMSec / 1000), + static_cast((sleepMSec % 1000) * 1000000) + }; + + while (nanosleep(&requested, &requested) != 0 && errno == EINTR) + { + } +#else + // On single-threaded WASM, nanosleep is either a no-op or stalls the + // event loop. There are no other threads to wait for, and no signals + // to deliver EINTR. + (void)sleepMSec; +#endif +} + +void GCToOSInterface::YieldThread(uint32_t switchCount) +{ +#ifdef FEATURE_MULTITHREADING + (void)switchCount; + sched_yield(); +#else + // No-op on single-threaded WASM - there are no other threads to yield to. + (void)switchCount; +#endif +} + +// ============================================================================ +// Virtual Memory - WASM-specific (posix_memalign / free) +// ============================================================================ + +// Emscripten does not provide a complete implementation of mmap and munmap: +// munmap cannot unmap partial allocations, mmap(PROT_NONE) still consumes +// linear memory, and MAP_FIXED is broken. +// Emscripten does provide an implementation of posix_memalign which is used here. + +// sbrk optimization: posix_memalign (dlmemalign) obtains memory in one of two ways: +// 1. Recycling a previously free()'d block from the allocator's free list. +// 2. Growing the WASM linear memory via sbrk() → memory.grow. +// +// The WebAssembly spec guarantees that memory.grow zero-initializes new pages, +// so freshly grown memory does not need an explicit memset. Only recycled blocks +// (which may contain stale data from a previous VirtualDecommit→free cycle) must +// be zeroed. +// +// We detect which case occurred by recording sbrk(0) (the current program break) +// before the allocation. If posix_memalign returns a pointer at or above the old +// break, the memory came from heap growth and is already zero. If it falls below, +// it was recycled and must be explicitly zeroed. +// +// This is safe because WASM is single-threaded - no concurrent sbrk calls can +// occur between our sbrk(0) probe and the posix_memalign call. This is the same +// approach used by Mono's WASM mmap implementation (mono-mmap-wasm.c). + +static void* VirtualReserveInner(size_t size, size_t alignment, uint32_t flags) +{ + assert(!(flags & VirtualReserveFlags::WriteWatch) && "WriteWatch not supported on WASM"); + if (alignment < OS_PAGE_SIZE) + { + alignment = OS_PAGE_SIZE; + } + +#ifndef FEATURE_MULTITHREADING + // Capture the program break before allocation to detect heap growth vs recycling. + void* old_brk = sbrk(0); + uintptr_t old_brk_val = (old_brk != (void*)-1) ? (uintptr_t)old_brk : 0; +#endif + + void* pRetVal; + int result = posix_memalign(&pRetVal, alignment, size); + if (result != 0) + { + return nullptr; + } + +#ifndef FEATURE_MULTITHREADING + // Only zero recycled memory. Fresh memory from heap growth (memory.grow) is + // guaranteed to be zero by the WebAssembly spec. + // Compare as uintptr_t to avoid UB from relational comparison of unrelated pointers. + // When sbrk failed (old_brk_val == 0), fall back to unconditional zeroing. + if (old_brk_val == 0 || (uintptr_t)pRetVal < old_brk_val) + { + memset(pRetVal, 0, size); + } +#else + // The sbrk optimization is not safe with multiple threads - another thread + // could call sbrk between our probe and posix_memalign, giving a false + // "fresh memory" result. Fall back to always zeroing. + memset(pRetVal, 0, size); +#endif + + return pRetVal; +} + +void* GCToOSInterface::VirtualReserve(size_t size, size_t alignment, uint32_t flags, uint16_t node) +{ + return VirtualReserveInner(size, alignment, flags); +} + +bool GCToOSInterface::VirtualRelease(void* address, size_t size) +{ + free(address); + return true; +} + +void* GCToOSInterface::VirtualReserveAndCommitLargePages(size_t size, uint16_t node) +{ + // WASM has no large pages - just reserve+commit normally. + return VirtualReserveInner(size, OS_PAGE_SIZE, 0); +} + +bool GCToOSInterface::VirtualCommit(void* address, size_t size, uint16_t node) +{ + // The GC skips this for heap memory when use_large_pages_p is true (which + // it always is on WASM). This is still called for bookkeeping memory. + // If previously decommitted (sentinel at page boundary), zero the range. +#ifdef FEATURE_MULTITHREADING + // Under MT, VirtualDecommit already zeroes the full range on decommit, and + // VirtualReserveInner zeroes on allocation - so commit is a no-op. + (void)address; + (void)size; +#else + if (size && *(uint8_t*)address != 0) + { + memset(address, 0, size); + } +#endif + return true; +} + +bool GCToOSInterface::VirtualDecommit(void* address, size_t size) +{ + // The GC skips this for heap memory when use_large_pages_p is true (which + // it always is on WASM). This is still called for bookkeeping memory. + // On WASM, we cannot return memory to the OS or change page protection. +#ifdef FEATURE_MULTITHREADING + // Under MT, VirtualCommit always zeroes unconditionally, so we just zero + // here immediately - no sentinel trick needed, and no races to worry about. + memset(address, 0, size); +#else + // Instead of zeroing the entire range here (expensive), write a non-zero + // sentinel at each page boundary. VirtualCommit checks the first byte to + // decide whether a zeroing pass is needed. Writing every page guarantees + // the sentinel is visible even if a sub-range is recommitted at an offset. + for (size_t offset = 0; offset < size; offset += OS_PAGE_SIZE) + { + *((uint8_t*)address + offset) = 1; + } +#endif + return true; +} + +bool GCToOSInterface::VirtualReset(void* address, size_t size, bool unlock) +{ + // Return false to indicate reset is not supported. + // This forces the GC to use the decommit+commit fallback path instead: + // VirtualDecommit marks the range with sentinels, and VirtualCommit + // performs the actual zeroing pass when the range is recommitted. + // That behavior is more correct on WASM, where madvise is a no-op. + return false; +} + +// ============================================================================ +// Write Watch (not supported on WASM) +// ============================================================================ + +bool GCToOSInterface::SupportsWriteWatch() +{ + return false; +} + +void GCToOSInterface::ResetWriteWatch(void* address, size_t size) +{ + assert(!"should never call ResetWriteWatch on WASM"); +} + +bool GCToOSInterface::GetWriteWatch(bool resetState, void* address, size_t size, void** pageAddresses, uintptr_t* pageAddressesCount) +{ + assert(!"should never call GetWriteWatch on WASM"); + return false; +} + +// ============================================================================ +// Processor cache +// ============================================================================ + +size_t GCToOSInterface::GetCacheSizePerLogicalCpu(bool trueSize) +{ + // WASM doesn't expose cache topology. + // Return a reasonable default (256 KB). + return 256 * 1024; +} + +// ============================================================================ +// Thread affinity / priority +// ============================================================================ + +bool GCToOSInterface::SetThreadAffinity(uint16_t procNo) +{ + // No thread affinity on WASM + return false; +} + +bool GCToOSInterface::BoostThreadPriority() +{ + // No thread priority on WASM + return false; +} + +const AffinitySet* GCToOSInterface::SetGCThreadsAffinitySet(uintptr_t configAffinityMask, const AffinitySet* configAffinitySet) +{ + (void)configAffinityMask; + (void)configAffinitySet; + return &g_processAffinitySet; +} + +// ============================================================================ +// Virtual / Physical Memory Limits +// ============================================================================ + +static uint64_t GetTotalPhysicalMemory() +{ +#ifdef TARGET_BROWSER + return emscripten_get_heap_max(); +#else // TARGET_WASI + // WASI doesn't have an API to query max memory. + return 2ULL * 1024 * 1024 * 1024; // 2GB +#endif +} + +size_t GCToOSInterface::GetVirtualMemoryLimit() +{ + // On 32-bit WASM, the entire address space is the limit + return (size_t)-1; +} + +size_t GCToOSInterface::GetVirtualMemoryMaxAddress() +{ + return GetTotalPhysicalMemory(); +} + +size_t GetRestrictedPhysicalMemoryLimit() +{ + // WASM linear memory has a hard ceiling set in the .wasm file, enforced by the engine. + // This is semantically equivalent to a container memory limit (cgroups on Linux). + // Returning the total memory here makes is_restricted_physical_mem = true, which + // enables the GC to auto-set heap_hard_limit proportional to available memory. + return GetTotalPhysicalMemory(); +} + +bool GetPhysicalMemoryUsed(size_t* val) +{ + // __builtin_wasm_memory_size(0) returns count of 64KB WASM pages, not GC pages. + *val = __builtin_wasm_memory_size(0) * WasmPageSize; + if (*val == 0) + { + // This overflow can happen when all 4GB of memory are in use. + *val = GetTotalPhysicalMemory(); + } + + return true; +} + +uint64_t GCToOSInterface::GetPhysicalMemoryLimit(bool* is_restricted) +{ + size_t restricted_limit; + if (is_restricted) + *is_restricted = false; + + restricted_limit = GetRestrictedPhysicalMemoryLimit(); + g_RestrictedPhysicalMemoryLimit = restricted_limit; + + if (restricted_limit != 0 && restricted_limit != SIZE_T_MAX) + { + if (is_restricted) + *is_restricted = true; + return restricted_limit; + } + + return g_totalPhysicalMemSize; +} + +static uint64_t GetAvailablePhysicalMemory() +{ +#ifdef TARGET_BROWSER + return emscripten_get_heap_max() - emscripten_get_heap_size(); +#else // TARGET_WASI + // Best approximation: total minus currently used + // __builtin_wasm_memory_size(0) returns count of 64KB WASM pages, not GC pages. + size_t used = __builtin_wasm_memory_size(0) * WasmPageSize; + if (used == 0) + { + // This overflow can happen when all 4GB of memory are in use. + return 0; + } + uint64_t total = GetTotalPhysicalMemory(); + return (total > used) ? (total - used) : 0; +#endif +} + +static uint64_t GetAvailablePageFile() +{ + // No swap on WASM + return 0; +} + +void GCToOSInterface::GetMemoryStatus(uint64_t restricted_limit, uint32_t* memory_load, uint64_t* available_physical, uint64_t* available_page_file) +{ + uint64_t available = 0; + uint32_t load = 0; + + size_t used; + if (restricted_limit != 0) + { + if (GetPhysicalMemoryUsed(&used)) + { + available = restricted_limit > used ? restricted_limit - used : 0; + load = (uint32_t)(((float)used * 100) / (float)restricted_limit); + } + } + else + { + available = GetAvailablePhysicalMemory(); + + if (memory_load != nullptr) + { + uint64_t total = g_totalPhysicalMemSize; + + if (total > available) + { + used = total - available; + load = (uint32_t)(((float)used * 100) / (float)total); + } + } + } + + if (available_physical != nullptr) + *available_physical = available; + + if (memory_load != nullptr) + *memory_load = load; + + if (available_page_file != nullptr) + *available_page_file = GetAvailablePageFile(); +} + +// ============================================================================ +// Time +// ============================================================================ + +int64_t GCToOSInterface::QueryPerformanceCounter() +{ + return minipal_hires_ticks(); +} + +int64_t GCToOSInterface::QueryPerformanceFrequency() +{ + return minipal_hires_tick_frequency(); +} + +uint64_t GCToOSInterface::GetLowPrecisionTimeStamp() +{ + return (uint64_t)minipal_lowres_ticks(); +} + +// ============================================================================ +// Processor count / NUMA / CPU Groups +// ============================================================================ + +uint32_t GCToOSInterface::GetTotalProcessorCount() +{ + return g_totalCpuCount; +} + +uint32_t GCToOSInterface::GetMaxProcessorCount() +{ + return (uint32_t)g_processAffinitySet.MaxCpuCount(); +} + +bool GCToOSInterface::CanEnableGCNumaAware() +{ + return false; +} + +bool GCToOSInterface::CanEnableGCCPUGroups() +{ + return false; +} + +bool GCToOSInterface::GetProcessorForHeap(uint16_t heap_number, uint16_t* proc_no, uint16_t* node_no) +{ + if (heap_number == 0) + { + *proc_no = 0; + *node_no = NUMA_NODE_UNDEFINED; + return true; + } + + return false; +} + +bool GCToOSInterface::ParseGCHeapAffinitizeRangesEntry(const char** config_string, size_t* start_index, size_t* end_index) +{ + return ParseIndexOrRange(config_string, start_index, end_index); +} diff --git a/src/coreclr/pal/src/map/virtual.cpp b/src/coreclr/pal/src/map/virtual.cpp index 043502e916acd1..b0a6d26014cc70 100644 --- a/src/coreclr/pal/src/map/virtual.cpp +++ b/src/coreclr/pal/src/map/virtual.cpp @@ -39,6 +39,7 @@ SET_DEFAULT_DEBUG_CHANNEL(VIRTUAL); // some headers have code with asserts, so d #include #include #include +#include #if HAVE_VM_ALLOCATE #include @@ -165,7 +166,7 @@ extern "C" BOOL VIRTUALInitialize(bool initializeExecutableMemoryAllocator) { - s_virtualPageSize = getpagesize(); + s_virtualPageSize = minipal_getpagesize(); TRACE("Initializing the Virtual Critical Sections. \n"); @@ -531,7 +532,11 @@ static LPVOID VIRTUALReserveMemory( { ASSERT( "Unable to store the structure in the list.\n"); pthrCurrent->SetLastError( ERROR_INTERNAL_ERROR ); +#ifdef TARGET_WASM + free( pRetVal ); +#else munmap( pRetVal, MemSize ); +#endif pRetVal = NULL; } } @@ -565,6 +570,52 @@ static LPVOID ReserveVirtualMemory( TRACE( "Reserving the memory now.\n"); +#ifdef TARGET_WASM + if (lpAddress != nullptr) + { + // Address hints (lpAddress) cannot be honored on WASM. + ERROR("Failed due to unsupported address hint on WASM.\n"); + pthrCurrent->SetLastError(ERROR_INVALID_ADDRESS); + return nullptr; + } + (void)fAllocationType; // Large pages / executable flags are N/A on WASM. + + // WASM has no virtual memory - mmap(PROT_NONE) still consumes linear memory, + // munmap of partial ranges doesn't return memory, and MAP_FIXED is broken. + // Use posix_memalign/free instead. + +#ifndef FEATURE_MULTITHREADING + // sbrk optimization: posix_memalign (dlmemalign) either recycles a free()'d + // block or grows the WASM linear memory via sbrk() → memory.grow. The WASM + // spec guarantees that memory.grow zero-initializes new pages, so only + // recycled blocks need explicit zeroing. We detect which case occurred by + // probing sbrk(0) before the allocation - safe because WASM is single-threaded. + void* old_brk = sbrk(0); + uintptr_t old_brk_val = (old_brk != (void*)-1) ? (uintptr_t)old_brk : 0; +#endif + + LPVOID pRetVal = nullptr; + if (posix_memalign(&pRetVal, GetVirtualPageSize(), MemSize) != 0 || pRetVal == nullptr) + { + ERROR( "Failed due to insufficient memory.\n" ); + pthrCurrent->SetLastError(ERROR_NOT_ENOUGH_MEMORY); + return nullptr; + } + +#ifndef FEATURE_MULTITHREADING + // Only zero recycled memory. Fresh pages from memory.grow are guaranteed zero. + // Compare as uintptr_t to avoid UB from relational comparison of unrelated pointers. + // When sbrk failed (old_brk_val == 0), fall back to unconditional zeroing. + if (old_brk_val == 0 || (uintptr_t)pRetVal < old_brk_val) + { + memset(pRetVal, 0, MemSize); + } +#else + // The sbrk optimization is not safe with multiple threads. Fall back to + // always zeroing. + memset(pRetVal, 0, MemSize); +#endif +#else // !TARGET_WASM // Most platforms will only commit memory if it is dirtied, // so this should not consume too much swap space. int mmapFlags = MAP_ANON | MAP_PRIVATE; @@ -627,13 +678,14 @@ static LPVOID ReserveVirtualMemory( } #endif // MMAP_ANON_IGNORES_PROTECTION -#if defined(MADV_DONTDUMP) && !defined(TARGET_WASM) +#if defined(MADV_DONTDUMP) // Do not include reserved uncommitted memory in coredump. if (!(fAllocationType & MEM_COMMIT)) { madvise(pRetVal, MemSize, MADV_DONTDUMP); } #endif +#endif // !TARGET_WASM return pRetVal; } @@ -724,6 +776,20 @@ VIRTUALCommitMemory( ERROR("mprotect() failed! Error(%d)=%s\n", errno, strerror(errno)); goto error; } +#else + // On WASM, reserve == commit. If this range was previously decommitted, + // sentinels were placed at each page boundary. Check the first byte and + // zero the entire range if needed. +#ifdef FEATURE_MULTITHREADING + // Under MT, VirtualDecommit already zeroes the full range on decommit, and + // reserve already zeroes on allocation - so commit is a no-op. + (void)MemSize; +#else + if (MemSize && *(BYTE*)StartBoundary != 0) + { + ZeroMemory((LPVOID) StartBoundary, MemSize); + } +#endif #endif #if defined(MADV_DONTDUMP) && !defined(TARGET_WASM) @@ -738,7 +804,6 @@ VIRTUALCommitMemory( #ifndef TARGET_WASM error: -#endif if ( flAllocationType & MEM_RESERVE || IsLocallyReserved ) { munmap( pRetVal, MemSize ); @@ -753,6 +818,7 @@ VIRTUALCommitMemory( pInformation = NULL; pRetVal = NULL; +#endif // !TARGET_WASM done: LogVaOperation( @@ -1077,11 +1143,22 @@ VirtualFree( goto VirtualFreeExit; } #else // TARGET_WASM - // We can't decommit the mapping (MAP_FIXED doesn't work in emscripten), and we can't - // MADV_DONTNEED it (madvise doesn't work in emscripten), but we can at least zero - // the memory so that if an attempt is made to reuse it later, the memory will be - // empty as PAL tests expect it to be. +#ifdef FEATURE_MULTITHREADING + // Under MT, VirtualCommit always zeroes unconditionally, so just zero + // here immediately - no sentinel trick needed, and no races. ZeroMemory((LPVOID) StartBoundary, MemSize); +#else + // We can't decommit the mapping (MAP_FIXED doesn't work in emscripten), and we can't + // MADV_DONTNEED it (madvise doesn't work in emscripten). Instead of zeroing the + // entire range here, write a non-zero sentinel at each page boundary. The commit + // path checks the first byte to decide whether a zeroing pass is needed. + // Writing every page guarantees the sentinel is visible even if a sub-range is + // recommitted at an offset. + for (SIZE_T offset = 0; offset < MemSize; offset += GetVirtualPageSize()) + { + *((BYTE*)StartBoundary + offset) = 1; + } +#endif // FEATURE_MULTITHREADING #endif // TARGET_WASM } @@ -1108,25 +1185,26 @@ VirtualFree( TRACE( "Releasing the following memory %d to %d.\n", pMemoryToBeReleased->startBoundary, pMemoryToBeReleased->memSize ); +#ifdef TARGET_WASM + free( (LPVOID)pMemoryToBeReleased->startBoundary ); +#else // !TARGET_WASM if ( munmap( (LPVOID)pMemoryToBeReleased->startBoundary, - pMemoryToBeReleased->memSize ) == 0 ) + pMemoryToBeReleased->memSize ) != 0 ) { - if ( VIRTUALReleaseMemory( pMemoryToBeReleased ) == FALSE ) - { - ASSERT( "Unable to remove the PCMI entry from the list.\n" ); - pthrCurrent->SetLastError( ERROR_INTERNAL_ERROR ); - bRetVal = FALSE; - goto VirtualFreeExit; - } - pMemoryToBeReleased = NULL; + ASSERT( "Unable to unmap the memory, munmap() returned an abnormal value.\n" ); + pthrCurrent->SetLastError( ERROR_INTERNAL_ERROR ); + bRetVal = FALSE; + goto VirtualFreeExit; } - else +#endif // !TARGET_WASM + if ( VIRTUALReleaseMemory( pMemoryToBeReleased ) == FALSE ) { - ASSERT( "Unable to unmap the memory, munmap() returned an abnormal value.\n" ); + ASSERT( "Unable to remove the PCMI entry from the list.\n" ); pthrCurrent->SetLastError( ERROR_INTERNAL_ERROR ); bRetVal = FALSE; goto VirtualFreeExit; } + pMemoryToBeReleased = NULL; } VirtualFreeExit: diff --git a/src/coreclr/pal/src/misc/sysinfo.cpp b/src/coreclr/pal/src/misc/sysinfo.cpp index df2a319b365488..a813a24828cdcf 100644 --- a/src/coreclr/pal/src/misc/sysinfo.cpp +++ b/src/coreclr/pal/src/misc/sysinfo.cpp @@ -24,6 +24,7 @@ Revision History: #include #include #include +#include #define __STDC_FORMAT_MACROS #include #include @@ -233,7 +234,7 @@ GetSystemInfo( PERF_ENTRY(GetSystemInfo); ENTRY("GetSystemInfo (lpSystemInfo=%p)\n", lpSystemInfo); - pagesize = getpagesize(); + pagesize = minipal_getpagesize(); lpSystemInfo->wProcessorArchitecture_PAL_Undefined = 0; lpSystemInfo->wReserved_PAL_Undefined = 0; diff --git a/src/native/minipal/wasm.h b/src/native/minipal/wasm.h new file mode 100644 index 00000000000000..c45b385ff5f04b --- /dev/null +++ b/src/native/minipal/wasm.h @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef MINIPAL_WASM_H +#define MINIPAL_WASM_H + +#ifndef __wasm__ +#include +#endif + +// Cross-platform page size accessor +#ifdef __cplusplus +inline +#else +static inline +#endif +int minipal_getpagesize(void) +{ +#ifdef __wasm__ + // The OS page size used by CoreCLR on WASM (16KB). + // WASM has no hardware pages; getpagesize() returns the 64KB memory.grow granularity, + // which is too coarse for GC alignment and thresholds. + return 16 * 1024; +#else + return getpagesize(); +#endif +} + +#endif // MINIPAL_WASM_H