/** * @file llmemory.cpp * @brief Very special memory allocation/deallocation stuff here * * $LicenseInfo:firstyear=2002&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #include "linden_common.h" #include "llthread.h" #if defined(LL_WINDOWS) # include #elif defined(LL_DARWIN) # include # include # include #include #elif LL_LINUX # include # include # include #endif #include "llmemory.h" #include "llsys.h" #include "llframetimer.h" #include "lltrace.h" #include "llerror.h" //---------------------------------------------------------------------------- //static // On windows commit charge information is vital for OOM diagnosis. U32Megabytes LLMemory::sAvailCommitMemInMB(U32_MAX); // most important memory metric for texture streaming // On Windows, this should agree with resource monitor -> performance -> memory -> available // On OS X, this should be activity monitor -> memory -> (physical memory - memory used) // NOTE: this number MAY be less than the actual available memory on systems with more than MaxHeapSize64 GB of physical memory (default 16GB) // In that case, should report min(available, sMaxHeapSizeInKB-sAllocateMemInKB) U32Kilobytes LLMemory::sAvailPhysicalMemInKB(U32_MAX); // Installed physical memory U32Kilobytes LLMemory::sMaxPhysicalMemInKB(0); // Maximimum heap size according to the user's settings (default 16GB) U32Kilobytes LLMemory::sMaxHeapSizeInKB(U32_MAX); // Current memory usage U32Kilobytes LLMemory::sAllocatedMemInKB(0); U32Kilobytes LLMemory::sAllocatedPageSizeInKB(0); LLFrameTimer LLMemory::sMemoryCheckTimer; F32 LLMemory::sSysMemoryFactor = 1.f; U32 LLMemory::sFactorLastFrameCount = 0; static const S32Megabytes MEM_LOW_THRESHOLD = S32Megabytes(256); static LLTrace::SampleStatHandle sAllocatedMem("allocated_mem", "active memory in use by application"); static LLTrace::SampleStatHandle sVirtualMem("virtual_mem", "virtual memory assigned to application"); void ll_assert_aligned_func(uintptr_t ptr,U32 alignment) { #if defined(LL_WINDOWS) && defined(LL_DEBUG_BUFFER_OVERRUN) //do not check return; #else #ifdef SHOW_ASSERT // Redundant, place to set breakpoints. if (ptr%alignment!=0) { LL_WARNS() << "alignment check failed" << LL_ENDL; } llassert(ptr%alignment==0); #endif #endif } //static void LLMemory::initMaxHeapSizeGB(F32Gigabytes max_heap_size) { sMaxHeapSizeInKB = U32Kilobytes::convert(max_heap_size); } //static void LLMemory::updateMemoryInfo() { LL_PROFILE_ZONE_SCOPED; sMemoryCheckTimer.reset(); sMaxPhysicalMemInKB = gSysMemory.getPhysicalMemoryKB(); LLMemoryInfo::updateAvailableMemory(); #if LL_WINDOWS // On windows updateAvailableMemory fills sAvailPhysicalMemInKB, //sAllocatedMemInKB and sAllocatedPageSizeInKB sample(sVirtualMem, sAllocatedPageSizeInKB); #elif defined(LL_DARWIN) task_vm_info info; mach_msg_type_number_t infoCount = TASK_VM_INFO_COUNT; // MACH_TASK_BASIC_INFO reports the same resident_size, but does not tell us the reusable bytes or phys_footprint. if (task_info(mach_task_self(), TASK_VM_INFO, reinterpret_cast(&info), &infoCount) == KERN_SUCCESS) { // Our Windows definition of PagefileUsage is documented by Microsoft as "the total amount of // memory that the memory manager has committed for a running process", which is rss. sAllocatedPageSizeInKB = U32Kilobytes::convert(U64Bytes(info.resident_size)); // Activity Monitor => Inspect Process => Real Memory Size appears to report resident_size // Activity monitor => main window memory column appears to report phys_footprint, which spot checks as at least 30% less. // I think that is because of compression, which isn't going to give us a consistent measurement. We want uncompressed totals. // // In between is resident_size - reusable. This is what Chrome source code uses, with source comments saying it is 'the "Real Memory" value // reported for the app by the Memory Monitor in Instruments.' It is still about 8% bigger than phys_footprint. // // (On Windows, we use WorkingSetSize.) sAllocatedMemInKB = U32Kilobytes::convert(U64Bytes(info.resident_size - info.reusable)); } else { LL_WARNS() << "task_info failed" << LL_ENDL; } #elif defined(LL_LINUX) // Use sysinfo() to get the total physical memory. struct sysinfo info; sysinfo(&info); sAllocatedMemInKB = U32Kilobytes::convert(U64Bytes(LLMemory::getCurrentRSS())); // represents the RAM allocated by this process only (in line with the windows implementation) #else //not valid for other systems for now. LL_WARNS() << "LLMemory::updateMemoryInfo() not implemented for this platform." << LL_ENDL; sAllocatedMemInKB = U64Bytes(LLMemory::getCurrentRSS()); #endif sample(sAllocatedMem, sAllocatedMemInKB); sAvailPhysicalMemInKB = llmin(sAvailPhysicalMemInKB, sMaxHeapSizeInKB - sAllocatedMemInKB); return ; } // //this function is to test if there is enough space with the size in the virtual address space. //it does not do any real allocation //if success, it returns the address where the memory chunk can fit in; //otherwise it returns NULL. // //static void* LLMemory::tryToAlloc(void* address, U32 size) { #if LL_WINDOWS address = VirtualAlloc(address, size, MEM_RESERVE | MEM_TOP_DOWN, PAGE_NOACCESS) ; if(address) { if(!VirtualFree(address, 0, MEM_RELEASE)) { LL_ERRS() << "error happens when free some memory reservation." << LL_ENDL ; } } return address ; #else return (void*)0x01 ; //skip checking #endif } //static void LLMemory::logMemoryInfo(bool update) { LL_PROFILE_ZONE_SCOPED; if(update) { updateMemoryInfo() ; } LL_INFOS() << llformat("Current allocated physical memory: %.2f MB", sAllocatedMemInKB / 1024.0) << LL_ENDL; LL_INFOS() << llformat("Current allocated page size: %.2f MB", sAllocatedPageSizeInKB / 1024.0) << LL_ENDL; LL_INFOS() << llformat("Current available physical memory: %.2f MB", sAvailPhysicalMemInKB / 1024.0) << LL_ENDL; LL_INFOS() << llformat("Current max usable memory: %.2f MB", sMaxPhysicalMemInKB / 1024.0) << LL_ENDL; } void LLMemory::updateFreeSystemMemory() { if (sMemoryCheckTimer.getElapsedTimeF32() >= 1.f) //once per second. { LLMemory::updateMemoryInfo(); // resets the timer } } F32 LLMemory::getSystemMemoryBudgetFactor() { // Only update once per frame U32 current_frame = LLFrameTimer::getFrameCount(); if (sFactorLastFrameCount == current_frame) { return sSysMemoryFactor; } sFactorLastFrameCount = current_frame; updateFreeSystemMemory(); #if LL_WINDOWS S32Megabytes free_sys_mem = getAvailableCommitMemMB(); #else S32Megabytes free_sys_mem = getAvailableMemKB(); #endif bool is_sys_low = free_sys_mem < MEM_LOW_THRESHOLD; static bool was_low = false; // sSysMemoryFactor affects draw distance // // We only decrement when more than 406MB is free, but increment // when below 256MB free. This should provide a stable value // in the 256-406MB range to avoid draw range fluctuations. // // Draw range reduction is a last resort, texture bias is supposed // to free at least some memory before we get here. // Note: textures were mostly moved to vram, we might want to // detach texture bias from system memory. if (is_sys_low) { // debt is a negative value since MIN_FREE_MAIN_MEMORY > free memory. S32Megabytes sys_budget_debt = free_sys_mem - MEM_LOW_THRESHOLD; // Leave some padding, otherwise we will crash out of memory before hitting factor 2. const S32Megabytes PAD_BUFFER(32); S32Megabytes budget_target = MEM_LOW_THRESHOLD - PAD_BUFFER; if (!was_low) { // Result should range from 1 at 0 debt to 2 at -224 debt, 2.14 at -256MB F32 new_factor = 1.f - (F32)sys_budget_debt.value() / (F32)budget_target.value(); sSysMemoryFactor = llmax(sSysMemoryFactor, new_factor); } else { // Slowly ramp up factor to free memory (increasing factor decreases draw range) constexpr F32 MAX_INCREMENT = 0.05f; F32 increment = MAX_INCREMENT * llmax(-(F32)sys_budget_debt.value() / (F32)budget_target.value(), 0.f); sSysMemoryFactor += increment * LLFrameTimer::getFrameDeltaTimeF32(); } sSysMemoryFactor = llclamp(sSysMemoryFactor, 1.f, 2.f); } else { // Only start ramping down when we have breathing room. // This should be under the value of isSystemMemoryLow to not throw texture // bias into 1.5+ territory each time we fluctuate around isSystemMemoryLow's // threshold. const S32Megabytes MEM_THRESHOLD = MEM_LOW_THRESHOLD + S32Megabytes(150); if (free_sys_mem > MEM_THRESHOLD && sSysMemoryFactor > 1.f) { // Ramp down factor over time. constexpr F32 DECREMENT = 0.02f; sSysMemoryFactor -= DECREMENT * LLFrameTimer::getFrameDeltaTimeF32(); sSysMemoryFactor = llclamp(sSysMemoryFactor, 1.f, 2.f); } } was_low = is_sys_low; return sSysMemoryFactor; } #if LL_WINDOWS //static U32Megabytes LLMemory::getAvailableCommitMemMB() { // Commit charge combines page file and ram, // theoretical limit is 128TB on 64bit windows. // Store as MB instead of KB to prevent overflow. return sAvailCommitMemInMB; } #endif //static U32Kilobytes LLMemory::getAvailableMemKB() { return sAvailPhysicalMemInKB ; } //static U32Kilobytes LLMemory::getMaxMemKB() { return sMaxPhysicalMemInKB ; } //static U32Kilobytes LLMemory::getAllocatedMemKB() { return sAllocatedMemInKB ; } //---------------------------------------------------------------------------- #if defined(LL_WINDOWS) //static U64 LLMemory::getCurrentRSS() { PROCESS_MEMORY_COUNTERS counters; if (!GetProcessMemoryInfo(GetCurrentProcess(), &counters, sizeof(counters))) { LL_WARNS() << "GetProcessMemoryInfo failed" << LL_ENDL; return 0; } return counters.WorkingSetSize; } #elif defined(LL_DARWIN) // if (sysctl(ctl, 2, &page_size, &size, NULL, 0) == -1) // { // LL_WARNS() << "Couldn't get page size" << LL_ENDL; // return 0; // } else { // return page_size; // } // } U64 LLMemory::getCurrentRSS() { U64 residentSize = 0; mach_task_basic_info_data_t basicInfo; mach_msg_type_number_t basicInfoCount = MACH_TASK_BASIC_INFO_COUNT; if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&basicInfo, &basicInfoCount) == KERN_SUCCESS) { residentSize = basicInfo.resident_size; // 64-bit macos apps allocate 32 GB or more at startup, and this is reflected in virtual_size. // basicInfo.virtual_size is not what we want. } else { LL_WARNS() << "task_info failed" << LL_ENDL; } return residentSize; } #elif defined(LL_LINUX) U64 LLMemory::getCurrentRSS() { struct rusage usage; if (getrusage(RUSAGE_SELF, &usage) != 0) { // Error handling code could be here return 0; } // ru_maxrss (since Linux 2.6.32) // This is the maximum resident set size used (in kilobytes). return usage.ru_maxrss * 1024; } #else U64 LLMemory::getCurrentRSS() { return 0; } #endif //-------------------------------------------------------------------- #if defined(LL_WINDOWS) && defined(LL_DEBUG_BUFFER_OVERRUN) #include struct mem_info { std::map memory_info; LLMutex mutex; static mem_info& get() { static mem_info instance; return instance; } private: mem_info(){} }; void* ll_aligned_malloc_fallback( size_t size, int align ) { SYSTEM_INFO sysinfo; GetSystemInfo(&sysinfo); unsigned int for_alloc = (size/sysinfo.dwPageSize + !!(size%sysinfo.dwPageSize)) * sysinfo.dwPageSize; void *p = VirtualAlloc(NULL, for_alloc+sysinfo.dwPageSize, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); if(NULL == p) { // call debugger __asm int 3; } DWORD old; bool Res = VirtualProtect((void*)((char*)p + for_alloc), sysinfo.dwPageSize, PAGE_NOACCESS, &old); if(false == Res) { // call debugger __asm int 3; } void* ret = (void*)((char*)p + for_alloc-size); { LLMutexLock lock(&mem_info::get().mutex); mem_info::get().memory_info.insert(std::pair(ret, p)); } return ret; } void ll_aligned_free_fallback( void* ptr ) { LLMutexLock lock(&mem_info::get().mutex); VirtualFree(mem_info::get().memory_info.find(ptr)->second, 0, MEM_RELEASE); mem_info::get().memory_info.erase(ptr); } #endif