/* * Copyright (C) 2012-2017 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif /* for malloc_usable_size() */ #ifdef HAVE_MALLOC_H #include #endif #ifdef HAVE_MALLOC_NP_H #include #endif #include #include #include "sciteco.h" #include "memory.h" #include "error.h" #include "undo.h" #ifdef HAVE_WINDOWS_H /* here it shouldn't cause conflicts with other headers */ #define WIN32_LEAN_AND_MEAN #include #include #endif namespace SciTECO { /* * Define this to prefix each heap object allocated * by the custom allocators with a magic value. * This helps to detect non-matching calls to the * overridden new/delete operators which can cause * underruns of the memory counter. */ //#define DEBUG_MAGIC ((guintptr)0xDEAD15DE5E1BEAF0) MemoryLimit memlimit; /* * A discussion of memory measurement techniques on Linux * and UNIXoid operating systems is in order, since this * problem turned out to be rather tricky. * * - UNIX has resource limits, which could be used to enforce * the memory limit, but in case they are hit, malloc() * will return NULL, so g_malloc() would abort(). * Wrapping malloc() to work around that has the same * problems described below. * - glibc has malloc hooks, but they are non-portable and * deprecated. * - It is possible to effectively wrap malloc() by overriding * the libc's implementation, which will even work when * statically linking in libc since malloc() is usually * delcared `weak`. * - When wrapping malloc(), malloc_usable_size() could be * used to count the memory consumption. * This is libc-specific, but available at least in * glibc and jemalloc (FreeBSD). * - glibc exports symbols for the original malloc() implementation * like __libc_malloc() that could be used for wrapping. * This is undocumented and libc-specific, though. * - The GNU ld --wrap option allows us to intercept calls, * but obviously won't work for shared libraries. * - The portable dlsym() could be used to look up the original * library symbol, but it may and does call malloc functions, * eg. calloc() on glibc. * In other words, there is no way to portably and reliably * wrap malloc() and friends when linking dynamically. * - Another difficulty is that, when free() is overridden, every * function that can __independently__ allocate memory that * can be passed to free() must also be overridden. * Otherwise the measurement is not precise and there can even * be underruns. Thus we'd have to guard against underruns. * - malloc() and friends are MT-safe, so any replacement function * would have to be MT-safe as well to avoid memory corruption. * E.g. even in single-threaded builds, glib might use * threads internally. * - There is also the old-school technique of calculating the size * of the program break, ie. the effective size of the DATA segment. * This works under the assumption that all allocations are * performed by extending the program break, as is __traditionally__ * done by malloc() and friends. * - Unfortunately, modern malloc() implementations sometimes * mmap() memory, especially for large allocations. * SciTECO mostly allocates small chunks. * Unfortunately, some malloc implementations like jemalloc * only claim memory using mmap(), thus rendering sbrk(0) * useless. * - Furthermore, some malloc-implementations like glibc will * only shrink the program break when told so explicitly * using malloc_trim(0). * - The sbrk(0) method thus depends on implementation details * of the libc. * - glibc and some other platforms have mallinfo(). * But at least on glibc it can get unbearably slow on programs * with a lot of (virtual/resident) memory. * Besides, mallinfo's API is broken on 64-bit systems, effectively * limiting the enforcable memory limit to 4GB. * Other glibc-specific introspection functions like malloc_info() * can be even slower because of the syscalls required. * - Linux has /proc/self/stat and /proc/self/statm but polling them * is very inefficient. * - FreeBSD/jemalloc has mallctl("stats.allocated") which even when * optimized is significantly slower than the fallback but generally * acceptable. * - On all other platforms we (have to) rely on the fallback * implementation based on C++ allocators/deallocators. * They have been improved significantly to count as much memory * as possible, even using libc-specific APIs like malloc_usable_size(). * Since this has been proven to work sufficiently well even on FreeBSD, * there is no longer any UNIX-specific implementation. * Even the malloc_usable_size() workaround for old or non-GNU * compilers is still faster than mallctl() on FreeBSD. * This might need to change in the future. * - Beginning with C++14 (or earlier with -fsized-deallocation), * it is possible to globally replace sized allocation/deallocation * functions, which could be used to avoid the malloc_usable_size() * workaround. Unfortunately, this may not be used for arrays, * since the compiler may have to call non-sized variants if the * original allocation size is unknown - and there is no way to detect * that when the new[] call is made. * What's worse is that at least G++ STL is broken seriously and * some versions will call the non-sized delete() even when sized-deallocation * is available. Again, this cannot be detected at new() time. * Therefore, I had to remove the sized-deallocation based * optimization. */ #ifdef G_OS_WIN32 /* * Uses the Windows-specific GetProcessMemoryInfo(), * so the entire process heap is measured. * * FIXME: Unfortunately, this is much slower than the portable * fallback implementation. * It may be possible to overwrite malloc() and friends, * counting the chunks with the MSVCRT-specific _minfo(). * Since we will always run against MSVCRT, the disadvantages * discussed above for the UNIX-case may not be important. * We might also just use the fallback implementation with some * additional support for _msize(). */ gsize MemoryLimit::get_usage(void) { PROCESS_MEMORY_COUNTERS info; /* * This __should__ not fail since the current process has * PROCESS_ALL_ACCESS, but who knows... * Since memory limiting cannot be turned off when this * happens, we can just as well terminate abnormally. */ if (G_UNLIKELY(!GetProcessMemoryInfo(GetCurrentProcess(), &info, sizeof(info)))) { gchar *msg = g_win32_error_message(GetLastError()); g_error("Cannot get memory usage: %s", msg); /* shouldn't be reached */ g_free(msg); return 0; } return info.WorkingSetSize; } #else /* * Portable fallback-implementation relying on C++11 sized allocators. * * Unfortunately, in the worst case, this will only measure the heap used * by C++ objects in SciTECO's sources; not even Scintilla, nor all * g_malloc() calls. * Usually, we will be able to use global non-sized deallocators with * libc-specific support to get more accurate results, though. */ #define MEMORY_USAGE_FALLBACK /** * Current memory usage in bytes. * * @bug This only works in single-threaded applications. * Should SciTECO or Scintilla ever use multiple threads, * it will be necessary to use atomic operations. */ static gsize memory_usage = 0; gsize MemoryLimit::get_usage(void) { return memory_usage; } #endif /* MEMORY_USAGE_FALLBACK */ void MemoryLimit::set_limit(gsize new_limit) { gsize memory_usage = get_usage(); if (G_UNLIKELY(new_limit && memory_usage > new_limit)) { gchar *usage_str = g_format_size(memory_usage); gchar *limit_str = g_format_size(new_limit); Error err("Cannot set undo memory limit (%s): " "Current usage too large (%s).", limit_str, usage_str); g_free(limit_str); g_free(usage_str); throw err; } undo.push_var(limit) = new_limit; } void MemoryLimit::check(void) { if (G_UNLIKELY(limit && get_usage() > limit)) { gchar *limit_str = g_format_size(limit); Error err("Memory limit (%s) exceeded. See command.", limit_str); g_free(limit_str); throw err; } } /* * The object-specific sized deallocators allow memory * counting portably, even in strict C++11 mode. * Once we depend on C++14, they and the entire `Object` * class hack may be avoided. * But see above - due to broken STLs, this may not actually * be safe! */ void * Object::operator new(size_t size) noexcept { #ifdef MEMORY_USAGE_FALLBACK memory_usage += size; #endif #ifdef DEBUG_MAGIC guintptr *ptr = (guintptr *)g_malloc(sizeof(guintptr) + size); *ptr = DEBUG_MAGIC; return ptr + 1; #else /* * Since we've got the sized-delete operator * below, we could allocate via g_slice. * * Using g_slice however would render malloc_trim() * ineffective. Also, it has been shown to be * unnecessary on Linux/glibc. * Glib is guaranteed to use the system malloc(), * so g_malloc() cooperates with malloc_trim(). * * On Windows (even Windows 2000), the slice allocator * did not show any significant performance boost * either. Also, since g_slice never seems to return * memory to the OS and we cannot force it to do so, * it will not cooperate with the Windows-specific * memory measurement and it is hard to recover * from memory limit exhaustions. */ return g_malloc(size); #endif } void Object::operator delete(void *ptr, size_t size) noexcept { #ifdef DEBUG_MAGIC if (ptr) { ptr = (guintptr *)ptr - 1; g_assert(*(guintptr *)ptr == DEBUG_MAGIC); } #endif g_free(ptr); #ifdef MEMORY_USAGE_FALLBACK memory_usage -= size; #endif } } /* namespace SciTECO */ /* * In strict C++11, we can still use global non-sized * deallocators. * * On their own, they bring little benefit, but with * some libc-specific functionality, they can be used * to improve the fallback memory measurements to include * all allocations (including Scintilla). * This comes with a moderate runtime penalty. * * Unfortunately, even in C++14, defining replacement * sized deallocators may be very dangerous, so this * seems to be as best as we can get (see above). */ void * operator new(size_t size) { void *ptr = g_malloc(size); #if defined(MEMORY_USAGE_FALLBACK) && defined(HAVE_MALLOC_USABLE_SIZE) /* NOTE: g_malloc() should always use the system malloc(). */ SciTECO::memory_usage += malloc_usable_size(ptr); #endif return ptr; } void operator delete(void *ptr) noexcept { #if defined(MEMORY_USAGE_FALLBACK) && defined(HAVE_MALLOC_USABLE_SIZE) if (ptr) SciTECO::memory_usage -= malloc_usable_size(ptr); #endif g_free(ptr); }