17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-24 18:01:53 +02:00

perf(debug): debug.memorystats switch for the heap counting (off by default) and the places of the large heap blocks in the memory section

This commit is contained in:
2026-10-23 23:10:32 +02:00
parent 16ee4ff65b
commit 336690ad8a
6 changed files with 482 additions and 41 deletions

View File

@@ -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 <chrono>
@@ -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};

View File

@@ -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<char const *>( ::glGetStringi( GL_EXTENSIONS, static_cast<GLuint>( 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::uint64_t>( std::max( dedicated - available, 0 ) ) * 1024 ) + " used of "
+ mb( static_cast<std::uint64_t>( dedicated ) * 1024 ) + ( evicted > 0 ? ", " + mb( static_cast<std::uint64_t>( 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<char const *>( ::glGetStringi( GL_EXTENSIONS, static_cast<GLuint>( 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::uint64_t>( std::max( dedicated - available, 0 ) ) * 1024 ) + " used of "
+ mb( static_cast<std::uint64_t>( dedicated ) * 1024 ) + ( evicted > 0 ? ", " + mb( static_cast<std::uint64_t>( 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() };

View File

@@ -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 );

View File

@@ -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

View File

@@ -15,29 +15,76 @@ http://mozilla.org/MPL/2.0/.
#include <cstdlib>
#include <new>
#include <mutex>
#include <sstream>
#include <thread>
#include <unordered_map>
#if defined(_WIN32)
#include <malloc.h>
#include <psapi.h>
#include <dbghelp.h>
#pragma comment(lib, "psapi.lib")
#elif defined(__APPLE__)
#pragma comment(lib, "dbghelp.lib")
#else
#include <cxxabi.h>
#include <dlfcn.h>
#include <execinfo.h>
#if defined(__APPLE__)
#include <mach/mach.h>
#include <malloc/malloc.h>
#else
#include <fstream>
#include <malloc.h>
#include <string>
#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<std::uint64_t> g_heapbytes{0};
std::atomic<std::uint64_t> g_heapblocks{0};
std::atomic<bool> g_counting{false};
// signed: the blocks taken before the counting began are given back with it on
std::atomic<std::int64_t> g_heapbytes{0};
std::atomic<std::int64_t> g_heapblocks{0};
// by the size of the blocks, see heap_memory
std::array<std::atomic<std::uint64_t>, heap_memory::CLASSES> g_classbytes{};
std::array<std::atomic<std::uint64_t>, heap_memory::CLASSES> g_classblocks{};
std::array<std::atomic<std::int64_t>, heap_memory::CLASSES> g_classbytes{};
std::array<std::atomic<std::int64_t>, 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<void *, LARGE_FRAMES> frames{};
std::uint32_t framecount{0};
};
std::array<large_record, LARGE_CAPACITY> 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<void *, LARGE_FRAMES> &Frames)
{
#if defined(_WIN32)
return ::RtlCaptureStackBackTrace(0, static_cast<DWORD>(Frames.size()), Frames.data(), nullptr);
#else
return static_cast<std::uint32_t>(std::max(0, ::backtrace(Frames.data(), static_cast<int>(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<std::int64_t>(size), std::memory_order_relaxed);
g_heapblocks.fetch_add(1, std::memory_order_relaxed);
g_classbytes[size_class(size)].fetch_add(static_cast<std::int64_t>(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<std::int64_t>(size), std::memory_order_relaxed);
g_heapblocks.fetch_sub(1, std::memory_order_relaxed);
g_classbytes[size_class(size)].fetch_sub(static_cast<std::int64_t>(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<std::int64_t> const &Counter) {
return static_cast<std::uint64_t>(std::max<std::int64_t>(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<void *, LARGE_FRAMES> 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<void *, LARGE_FRAMES> 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<std::uintptr_t>(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<SYMBOL_INFO *>(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<LPCSTR>(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<std::uintptr_t>(module));
result.function = name.str();
}
}
#else
Dl_info info{};
if (::dladdr(reinterpret_cast<void *>(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 <file> <offset> 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<std::uintptr_t>(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<large_record, LARGE_CAPACITY> 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<void *, frame_name> 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<std::string, std::size_t> 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;

View File

@@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/.
#include <array>
#include <cstdint>
#include <string>
#include <vector>
// 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<heap_site> 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
{