From 336690ad8af2c0f143478a26a451c7144635ec7f Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Fri, 23 Oct 2026 23:10:32 +0200 Subject: [PATCH] perf(debug): debug.memorystats switch for the heap counting (off by default) and the places of the large heap blocks in the memory section --- application/application.cpp | 5 + rendering/opengl46/opengl46renderer.cpp | 66 ++-- utilities/Globals.cpp | 7 + utilities/Globals.h | 1 + utilities/memorystats.cpp | 418 ++++++++++++++++++++++-- utilities/memorystats.h | 26 ++ 6 files changed, 482 insertions(+), 41 deletions(-) diff --git a/application/application.cpp b/application/application.cpp index bb8e6bc8..c8cfd0c4 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -28,6 +28,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/translation.h" #include "vehicle/Train.h" #include "utilities/Timer.h" +#include "utilities/memorystats.h" #include "utilities/dictionary.h" #include "version_info.h" #include @@ -1146,6 +1147,10 @@ int eu07_application::init_settings(int Argc, char *Argv[]) { Global.LoadIniFile("eu07.ini"); } + if (Global.debug_memorystats) + { + start_heap_counting(); + } // process command line arguments bool sceneryfromcommandline{false}; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 4fae1d55..99c498af 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -1177,6 +1177,34 @@ void opengl46_renderer::Update_Memory_Info() text += "process: " + mb( process.resident ) + " resident (peak " + mb( process.peak ) + ")" + ( process.committed > 0 ? ", " + mb( process.committed ) + " private" : std::string{} ) + "\n"; } + // the memory of the card, all processes: GL_NVX_gpu_memory_info (in kB) + if( m_nvxmemory < 0 ) { + m_nvxmemory = 0; + GLint extensions { 0 }; + ::glGetIntegerv( GL_NUM_EXTENSIONS, &extensions ); + for( GLint index = 0; index < extensions; ++index ) { + auto const *name { reinterpret_cast( ::glGetStringi( GL_EXTENSIONS, static_cast( index ) ) ) }; + if( ( name != nullptr ) && ( std::strcmp( name, "GL_NVX_gpu_memory_info" ) == 0 ) ) { + m_nvxmemory = 1; + break; + } + } + } + if( m_nvxmemory > 0 ) { + GLint dedicated { 0 }, available { 0 }, evicted { 0 }; + ::glGetIntegerv( 0x9047, &dedicated ); // GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX + ::glGetIntegerv( 0x9049, &available ); // GL_GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX + ::glGetIntegerv( 0x904B, &evicted ); // GL_GPU_MEMORY_INFO_EVICTED_MEMORY_NVX + text += "card memory, all programs: " + mb( static_cast( std::max( dedicated - available, 0 ) ) * 1024 ) + " used of " + + mb( static_cast( dedicated ) * 1024 ) + ( evicted > 0 ? ", " + mb( static_cast( evicted ) * 1024 ) + " evicted" : std::string{} ) + "\n"; + } + + // the rest needs new and delete counted, which slows the loading down: only with debug.memorystats + if( false == heap_counting() ) { + text += "the memory of the parts of the program: off (debug.memorystats yes in eu07.ini, it slows the loading down)"; + m_memorytext = std::move( text ); + return; + } // what the program took with new: the rest of the private memory is of the drivers, libraries and the c runtime auto const heap { query_heap_memory() }; text += "taken with new: " + mb( heap.bytes ) + " in " + std::to_string( heap.blocks ) + " blocks" @@ -1200,28 +1228,28 @@ void opengl46_renderer::Update_Memory_Info() } text += "\n"; } - // the memory of the card, all processes: GL_NVX_gpu_memory_info (in kB) - if( m_nvxmemory < 0 ) { - m_nvxmemory = 0; - GLint extensions { 0 }; - ::glGetIntegerv( GL_NUM_EXTENSIONS, &extensions ); - for( GLint index = 0; index < extensions; ++index ) { - auto const *name { reinterpret_cast( ::glGetStringi( GL_EXTENSIONS, static_cast( index ) ) ) }; - if( ( name != nullptr ) && ( std::strcmp( name, "GL_NVX_gpu_memory_info" ) == 0 ) ) { - m_nvxmemory = 1; - break; + // where the large blocks come from + { + auto const large { query_large_blocks() }; + text += " blocks of " + std::to_string( large_blocks::LARGE_BLOCK / 1024 ) + " KB and more by the place which took them:\n"; + std::size_t const shown { 15 }; + std::uint64_t restbytes { 0 }, restblocks { 0 }; + for( std::size_t index = 0; index < large.sites.size(); ++index ) { + auto const &site { large.sites[ index ] }; + if( index >= shown ) { + restbytes += site.bytes; + restblocks += site.blocks; + continue; } + text += " " + mb( site.bytes ) + " in " + std::to_string( site.blocks ) + ": " + site.place + "\n"; + } + if( restblocks > 0 ) { + text += " " + mb( restbytes ) + " in " + std::to_string( restblocks ) + ": " + std::to_string( large.sites.size() - shown ) + " other places\n"; + } + if( large.untracked > 0 ) { + text += " " + std::to_string( large.untracked ) + " blocks not followed, the table of them was full\n"; } } - if( m_nvxmemory > 0 ) { - GLint dedicated { 0 }, available { 0 }, evicted { 0 }; - ::glGetIntegerv( 0x9047, &dedicated ); // GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX - ::glGetIntegerv( 0x9049, &available ); // GL_GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX - ::glGetIntegerv( 0x904B, &evicted ); // GL_GPU_MEMORY_INFO_EVICTED_MEMORY_NVX - text += "card memory, all programs: " + mb( static_cast( std::max( dedicated - available, 0 ) ) * 1024 ) + " used of " - + mb( static_cast( dedicated ) * 1024 ) + ( evicted > 0 ? ", " + mb( static_cast( evicted ) * 1024 ) + " evicted" : std::string{} ) + "\n"; - } - std::uint64_t accounted { 0 }; // textures: the copies of the data kept in the process after the upload auto const textures { m_textures.memory() }; diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index bf62948e..0b0f1cfc 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1150,6 +1150,12 @@ bool global_settings::ConfigParseDebug(cParser& Parser, const std::string& token return true; } + if (token == "debug.memorystats") + { + ParseOne(Parser, debug_memorystats); + return true; + } + if (token == "debuglog") { std::string value; @@ -1915,6 +1921,7 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "physics.roadlod", physics_roadlod ); export_as_text( Output, "physics.pipewake", physics_pipewake ); export_as_text( Output, "performance.multithreading", performance_multithreading ); + export_as_text( Output, "debug.memorystats", debug_memorystats ); export_as_text( Output, "debuglog", iWriteLogEnabled ); export_as_text( Output, "multiplelogs", MultipleLogs ); export_as_text( Output, "showsystemconsole", ShowSystemConsole ); diff --git a/utilities/Globals.h b/utilities/Globals.h index cbe79954..85b8e11a 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -118,6 +118,7 @@ struct global_settings { bool physics_roadlod{ true }; // road vehicles far from the camera go by one step of the physics in a frame, see vehicle_table::update() float physics_pipewake{ 0.001f }; // bar: air flowing along a pipe wakes the physics of a vehicle when it changes its pressure by more than this. 0: any flow does bool performance_multithreading{ true }; // work split over the logical cores of the machine (worker_pool), as many as each job is worth + bool debug_memorystats{ false }; // the memory of the parts of the program in the debug panel, with new and delete counted (start_heap_counting()). slows the loading down bool bnewAirCouplers{ true }; float fMoveLight{ 0.f }; // numer dnia w roku albo -1 bool FakeLight{ false }; // toggle between fixed and dynamic daylight diff --git a/utilities/memorystats.cpp b/utilities/memorystats.cpp index 3f35bff8..15aaca1f 100644 --- a/utilities/memorystats.cpp +++ b/utilities/memorystats.cpp @@ -15,29 +15,76 @@ http://mozilla.org/MPL/2.0/. #include #include +#include +#include +#include +#include + #if defined(_WIN32) #include #include +#include #pragma comment(lib, "psapi.lib") -#elif defined(__APPLE__) +#pragma comment(lib, "dbghelp.lib") +#else +#include +#include +#include +#if defined(__APPLE__) #include #include #else #include #include -#include +#endif #endif -// the global new and delete of the executable count the memory of the blocks they hand out (query_heap_memory()). the blocks come -// from malloc() as with the default operators; the aligned forms are left to the runtime +// the global new and delete of the executable count the memory of the blocks they hand out (query_heap_memory()), once +// start_heap_counting() turns it on. the blocks come from malloc() as with the default operators; the aligned forms are left to the +// runtime namespace { -std::atomic g_heapbytes{0}; -std::atomic g_heapblocks{0}; +std::atomic g_counting{false}; +// signed: the blocks taken before the counting began are given back with it on +std::atomic g_heapbytes{0}; +std::atomic g_heapblocks{0}; // by the size of the blocks, see heap_memory -std::array, heap_memory::CLASSES> g_classbytes{}; -std::array, heap_memory::CLASSES> g_classblocks{}; +std::array, heap_memory::CLASSES> g_classbytes{}; +std::array, heap_memory::CLASSES> g_classblocks{}; + +// the large blocks live now, with the call stacks which took them. a fixed table: new can't take memory for it +constexpr std::size_t LARGE_FRAMES{16}; +constexpr std::size_t LARGE_CAPACITY{16384}; + +struct large_record +{ + void *block{nullptr}; + std::uint64_t size{0}; + std::array frames{}; + std::uint32_t framecount{0}; +}; + +std::array g_large; // [0, g_largeused) holds the records, with gaps of the blocks given back +std::size_t g_largeused{0}; +std::uint64_t g_largeuntracked{0}; +std::uint32_t g_largeskip{0}; // the frames of new itself at the top of the call stacks, see calibrate_frames() +std::atomic_flag g_largelock = ATOMIC_FLAG_INIT; + +struct large_lock +{ + large_lock() + { + while (g_largelock.test_and_set(std::memory_order_acquire)) + { + std::this_thread::yield(); + } + } + ~large_lock() + { + g_largelock.clear(std::memory_order_release); + } +}; std::size_t size_class(std::size_t const Size) { @@ -55,6 +102,59 @@ std::size_t block_size(void *Block) #endif } +std::uint32_t capture_frames(std::array &Frames) +{ +#if defined(_WIN32) + return ::RtlCaptureStackBackTrace(0, static_cast(Frames.size()), Frames.data(), nullptr); +#else + return static_cast(std::max(0, ::backtrace(Frames.data(), static_cast(Frames.size())))); +#endif +} + +void note_large(void *Block, std::size_t const Size) +{ + large_record record; + record.block = Block; + record.size = Size; + // taken outside the lock: it may take memory with malloc the first time + record.framecount = capture_frames(record.frames); + + large_lock lock; + std::size_t slot{0}; + while ((slot < g_largeused) && (g_large[slot].block != nullptr)) + { + ++slot; + } + if (slot == g_largeused) + { + if (g_largeused == g_large.size()) + { + ++g_largeuntracked; + return; + } + ++g_largeused; + } + g_large[slot] = record; +} + +void forget_large(void *Block) +{ + large_lock lock; + for (std::size_t slot = 0; slot < g_largeused; ++slot) + { + if (g_large[slot].block != Block) + { + continue; + } + g_large[slot].block = nullptr; + while ((g_largeused > 0) && (g_large[g_largeused - 1].block == nullptr)) + { + --g_largeused; + } + return; + } +} + void *counted_new(std::size_t Size) { if (Size == 0) @@ -65,11 +165,18 @@ void *counted_new(std::size_t Size) { if (auto *block{std::malloc(Size)}) { - auto const size{block_size(block)}; - g_heapbytes.fetch_add(size, std::memory_order_relaxed); - g_heapblocks.fetch_add(1, std::memory_order_relaxed); - g_classbytes[size_class(size)].fetch_add(size, std::memory_order_relaxed); - g_classblocks[size_class(size)].fetch_add(1, std::memory_order_relaxed); + if (g_counting.load(std::memory_order_relaxed)) + { + auto const size{block_size(block)}; + g_heapbytes.fetch_add(static_cast(size), std::memory_order_relaxed); + g_heapblocks.fetch_add(1, std::memory_order_relaxed); + g_classbytes[size_class(size)].fetch_add(static_cast(size), std::memory_order_relaxed); + g_classblocks[size_class(size)].fetch_add(1, std::memory_order_relaxed); + if (size >= large_blocks::LARGE_BLOCK) + { + note_large(block, size); + } + } return block; } auto const handler{std::get_new_handler()}; @@ -87,11 +194,18 @@ void counted_delete(void *Block) noexcept { return; } - auto const size{block_size(Block)}; - g_heapbytes.fetch_sub(size, std::memory_order_relaxed); - g_heapblocks.fetch_sub(1, std::memory_order_relaxed); - g_classbytes[size_class(size)].fetch_sub(size, std::memory_order_relaxed); - g_classblocks[size_class(size)].fetch_sub(1, std::memory_order_relaxed); + if (g_counting.load(std::memory_order_relaxed)) + { + auto const size{block_size(Block)}; + g_heapbytes.fetch_sub(static_cast(size), std::memory_order_relaxed); + g_heapblocks.fetch_sub(1, std::memory_order_relaxed); + g_classbytes[size_class(size)].fetch_sub(static_cast(size), std::memory_order_relaxed); + g_classblocks[size_class(size)].fetch_sub(1, std::memory_order_relaxed); + if (size >= large_blocks::LARGE_BLOCK) + { + forget_large(Block); + } + } std::free(Block); } @@ -163,17 +277,277 @@ void operator delete[](void *Block, std::nothrow_t const &) noexcept heap_memory query_heap_memory() { + auto const value = [](std::atomic const &Counter) { + return static_cast(std::max(0, Counter.load(std::memory_order_relaxed))); + }; heap_memory result; - result.bytes = g_heapbytes.load(std::memory_order_relaxed); - result.blocks = g_heapblocks.load(std::memory_order_relaxed); + result.bytes = value(g_heapbytes); + result.blocks = value(g_heapblocks); for (std::size_t index = 0; index < heap_memory::CLASSES; ++index) { - result.classbytes[index] = g_classbytes[index].load(std::memory_order_relaxed); - result.classblocks[index] = g_classblocks[index].load(std::memory_order_relaxed); + result.classbytes[index] = value(g_classbytes[index]); + result.classblocks[index] = value(g_classblocks[index]); } return result; } +namespace +{ + +// how many frames at the top of the stack of a block are of new itself: a block is taken here and its stack is compared with the one +// of this function. the names of the functions, which tell the same, aren't there in every build (linux without -rdynamic) +#if defined(_MSC_VER) +__declspec(noinline) +#else +__attribute__((noinline)) +#endif +void calibrate_frames() +{ + std::array own; + auto const owncount{capture_frames(own)}; + auto *block{::operator new(large_blocks::LARGE_BLOCK)}; + large_record record; + { + large_lock lock; + for (std::size_t slot = 0; slot < g_largeused; ++slot) + { + if (g_large[slot].block == block) + { + record = g_large[slot]; + break; + } + } + } + ::operator delete(block); + // the first frame of the block's stack which is in this one too (with the next one, to be sure) is the caller of this function: + // the frame before it is this function, the ones before that are new + for (std::uint32_t frame = 1; frame + 1 < record.framecount; ++frame) + { + for (std::uint32_t index = 0; index + 1 < owncount; ++index) + { + if ((record.frames[frame] == own[index]) && (record.frames[frame + 1] == own[index + 1])) + { + g_largeskip = frame - 1; + return; + } + } + } +} + +} // namespace + +void start_heap_counting() +{ + if (g_counting.load(std::memory_order_relaxed)) + { + return; + } +#if !defined(_WIN32) + // the first backtrace() loads the library which walks the stack: done here, not in the middle of a new + std::array frames; + capture_frames(frames); +#endif + g_counting.store(true, std::memory_order_relaxed); + calibrate_frames(); +} + +bool heap_counting() +{ + return g_counting.load(std::memory_order_relaxed); +} + +namespace +{ + +// a frame of a call stack, as the symbols of the program name it +struct frame_name +{ + std::string function; + std::string line; // file:line, where known and not in a header of the standard library + bool allocator{false}; // a part of new or of the standard containers: the place which took the block is further down +}; + +frame_name name_frame(void *Address) +{ + frame_name result; + // the address the call returns to: the call itself is a byte before it + auto const address{reinterpret_cast(Address) - 1}; +#if defined(_WIN32) + auto const process{::GetCurrentProcess()}; + static bool const symbols{[process]() { + ::SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES); + return ::SymInitialize(process, nullptr, TRUE) != FALSE; + }()}; + if (symbols) + { + alignas(SYMBOL_INFO) char buffer[sizeof(SYMBOL_INFO) + 512]; + auto *symbol{reinterpret_cast(buffer)}; + symbol->SizeOfStruct = sizeof(SYMBOL_INFO); + symbol->MaxNameLen = 511; + DWORD64 displacement{0}; + if (::SymFromAddr(process, address, &displacement, symbol)) + { + result.function = symbol->Name; + } + IMAGEHLP_LINE64 line{}; + line.SizeOfStruct = sizeof(line); + DWORD linedisplacement{0}; + if (::SymGetLineFromAddr64(process, address, &linedisplacement, &line) && (line.FileName != nullptr)) + { + std::string file{line.FileName}; + file = file.substr(file.find_last_of("\\/") + 1); + // the headers of the standard library have no extension + if (file.find('.') != std::string::npos) + { + result.line = file + ":" + std::to_string(line.LineNumber); + } + } + } + if (result.function.empty()) + { + HMODULE module{nullptr}; + char path[MAX_PATH]{}; + if (::GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(address), &module) && + ::GetModuleFileNameA(module, path, MAX_PATH)) + { + std::string file{path}; + std::ostringstream name; + name << file.substr(file.find_last_of("\\/") + 1) << "+0x" << std::hex << (address - reinterpret_cast(module)); + result.function = name.str(); + } + } +#else + Dl_info info{}; + if (::dladdr(reinterpret_cast(address), &info) != 0) + { + if (info.dli_sname != nullptr) + { + int status{0}; + auto *demangled{abi::__cxa_demangle(info.dli_sname, nullptr, nullptr, &status)}; + result.function = (status == 0 && demangled != nullptr) ? demangled : info.dli_sname; + std::free(demangled); + } + else if (info.dli_fname != nullptr) + { + // without the names of the functions of the executable (no -rdynamic): addr2line -f -C -e tells them + std::string file{info.dli_fname}; + std::ostringstream name; + name << file.substr(file.find_last_of('/') + 1) << "+0x" << std::hex << (address - reinterpret_cast(info.dli_fbase)); + result.function = name.str(); + } + } +#endif + if (result.function.empty()) + { + std::ostringstream name; + name << "0x" << std::hex << address; + result.function = name.str(); + } + // the qualified name, without what the demangled names of gcc have around it: the type returned, the parameters + auto function{result.function}; + { + int depth{0}; + std::size_t start{0}; + for (std::size_t index = 0; index < function.size(); ++index) + { + auto const character{function[index]}; + if (character == '<') + { + ++depth; + } + else if (character == '>') + { + --depth; + } + else if ((depth == 0) && (character == ' ') && ((index < 8) || (function.compare(index - 8, 8, "operator") != 0))) + { + start = index + 1; + } + else if ((depth == 0) && (character == '(') && (index > start)) + { + function = function.substr(start, index - start); + break; + } + } + } + result.allocator = (function.rfind("std::", 0) == 0) || (function.rfind("operator new", 0) == 0) || (function.rfind("__gnu_cxx::", 0) == 0) || + (function.find("counted_new") != std::string::npos) || (function.find("note_large") != std::string::npos) || + (function.find("capture_frames") != std::string::npos) || (function.find("RtlCaptureStackBackTrace") != std::string::npos) || + (function.find("backtrace") != std::string::npos) || (function.find("malloc") != std::string::npos); + return result; +} + +} // namespace + +large_blocks query_large_blocks() +{ + large_blocks result; + // a copy of the table, so the lock isn't held while the stacks get named. static: it's too large for the stack, and a block taken + // for it would land in the table itself + static std::array records; + std::size_t count{0}; + { + large_lock lock; + for (std::size_t slot = 0; slot < g_largeused; ++slot) + { + if (g_large[slot].block != nullptr) + { + records[count++] = g_large[slot]; + } + } + result.untracked = g_largeuntracked; + } + + static std::mutex namesmutex; + static std::unordered_map names; + std::scoped_lock namelock(namesmutex); + auto const name = [](void *Address) -> frame_name const & { + auto lookup{names.find(Address)}; + if (lookup == names.end()) + { + lookup = names.emplace(Address, name_frame(Address)).first; + } + return lookup->second; + }; + auto const place = [](frame_name const &Frame) { return Frame.line.empty() ? Frame.function : Frame.function + " (" + Frame.line + ")"; }; + + std::unordered_map sites; + for (std::size_t index = 0; index < count; ++index) + { + auto const &record{records[index]}; + // the first frame outside new and the containers, and the one which called it + std::string site; + auto frame{g_largeskip}; + while ((frame < record.framecount) && name(record.frames[frame]).allocator) + { + ++frame; + } + if (frame < record.framecount) + { + site = place(name(record.frames[frame])); + if (frame + 1 < record.framecount) + { + site += " <- " + place(name(record.frames[frame + 1])); + } + } + else + { + site = "?"; + } + auto lookup{sites.find(site)}; + if (lookup == sites.end()) + { + lookup = sites.emplace(site, result.sites.size()).first; + result.sites.push_back({site}); + } + auto &target{result.sites[lookup->second]}; + target.bytes += record.size; + ++target.blocks; + } + std::sort(result.sites.begin(), result.sites.end(), [](heap_site const &Left, heap_site const &Right) { return Left.bytes > Right.bytes; }); + return result; +} + process_memory query_process_memory() { process_memory result; diff --git a/utilities/memorystats.h b/utilities/memorystats.h index 0358126e..381a7938 100644 --- a/utilities/memorystats.h +++ b/utilities/memorystats.h @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include // memory of the process, as the system counts it, for the debug panel @@ -38,6 +39,31 @@ struct heap_memory heap_memory query_heap_memory(); +// the counting of new and delete is off until this turns it on (debug.memorystats in the ini): it costs time with every block the +// program takes, which shows most in the loading of a scenery. once on, it stays on; the blocks taken before aren't counted +void start_heap_counting(); +bool heap_counting(); + +// where the large blocks taken with new come from: the blocks of LARGE_BLOCK bytes or more, live now, summed up by the place in the +// program which took them (the first function of the call stack outside the standard library, and the one which called it) +struct heap_site +{ + std::string place; + std::uint64_t bytes{0}; + std::uint64_t blocks{0}; +}; + +struct large_blocks +{ + static constexpr std::uint64_t LARGE_BLOCK{256 * 1024}; + std::vector sites; // the most memory first + std::uint64_t untracked{0}; // blocks taken while the table of them was full, since the counting began +}; + +// goes through the call stacks of the blocks, which takes a while the first time (the symbols of the program get loaded). not for +// more threads at once +large_blocks query_large_blocks(); + // what a part of the program keeps in memory: things and bytes struct memory_count {