/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #define MOZ_MEMORY_IMPL #include "mozmemory_wrap.h" #ifdef _WIN32 #include #include typedef int ssize_t; #else #include #include #endif #include #include #include #include "mozilla/Assertions.h" #include "FdPrintf.h" static void die(const char* message) { /* Here, it doesn't matter that fprintf may allocate memory. */ fprintf(stderr, "%s\n", message); exit(1); } /* We don't want to be using malloc() to allocate our internal tracking * data, because that would change the parameters of what is being measured, * so we want to use data types that directly use mmap/VirtualAlloc. */ template class MappedArray { public: MappedArray(): mPtr(nullptr) {} ~MappedArray() { if (mPtr) { #ifdef _WIN32 VirtualFree(mPtr, sizeof(T) * Len, MEM_RELEASE); #else munmap(mPtr, sizeof(T) * Len); #endif } } T& operator[] (size_t aIndex) const { if (mPtr) { return mPtr[aIndex]; } #ifdef _WIN32 mPtr = reinterpret_cast(VirtualAlloc(nullptr, sizeof(T) * Len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)); if (mPtr == nullptr) { die("VirtualAlloc error"); } #else mPtr = reinterpret_cast(mmap(nullptr, sizeof(T) * Len, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0)); if (mPtr == MAP_FAILED) { die("Mmap error"); } #endif return mPtr[aIndex]; } private: mutable T* mPtr; }; /* Type for records of allocations. */ struct MemSlot { void* mPtr; size_t mSize; }; /* An almost infinite list of slots. * In essence, this is a linked list of arrays of groups of slots. * Each group is 1MB. On 64-bits, one group allows to store 64k allocations. * Each MemSlotList instance can store 1023 such groups, which means more * than 65M allocations. In case more would be needed, we chain to another * MemSlotList, and so on. * Using 1023 groups makes the MemSlotList itself page sized on 32-bits * and 2 pages-sized on 64-bits. */ class MemSlotList { static const size_t kGroups = 1024 - 1; static const size_t kGroupSize = (1024 * 1024) / sizeof(MemSlot); MappedArray mSlots[kGroups]; MappedArray mNext; public: MemSlot& operator[] (size_t aIndex) const { if (aIndex < kGroupSize * kGroups) { return mSlots[aIndex / kGroupSize][aIndex % kGroupSize]; } aIndex -= kGroupSize * kGroups; return mNext[0][aIndex]; } }; /* Helper class for memory buffers */ class Buffer { public: Buffer() : mBuf(nullptr), mLength(0) {} Buffer(const void* aBuf, size_t aLength) : mBuf(reinterpret_cast(aBuf)), mLength(aLength) {} /* Constructor for string literals. */ template explicit Buffer(const char (&aStr)[Size]) : mBuf(aStr), mLength(Size - 1) {} /* Returns a sub-buffer up-to but not including the given aNeedle character. * The "parent" buffer itself is altered to begin after the aNeedle * character. * If the aNeedle character is not found, return the entire buffer, and empty * the "parent" buffer. */ Buffer SplitChar(char aNeedle) { char* buf = const_cast(mBuf); char* c = reinterpret_cast(memchr(buf, aNeedle, mLength)); if (!c) { return Split(mLength); } Buffer result = Split(c - buf); // Remove the aNeedle character itself. Split(1); return result; } /* Returns a sub-buffer of at most aLength characters. The "parent" buffer is * amputated of those aLength characters. If the "parent" buffer is smaller * than aLength, then its length is used instead. */ Buffer Split(size_t aLength) { Buffer result(mBuf, std::min(aLength, mLength)); mLength -= result.mLength; mBuf += result.mLength; return result; } /* Move the buffer (including its content) to the memory address of the aOther * buffer. */ void Slide(Buffer aOther) { memmove(const_cast(aOther.mBuf), mBuf, mLength); mBuf = aOther.mBuf; } /* Returns whether the two involved buffers have the same content. */ bool operator ==(Buffer aOther) { return mLength == aOther.mLength && (mBuf == aOther.mBuf || !strncmp(mBuf, aOther.mBuf, mLength)); } /* Returns whether the buffer is empty. */ explicit operator bool() { return mLength; } /* Returns the memory location of the buffer. */ const char* get() { return mBuf; } /* Returns the memory location of the end of the buffer (technically, the * first byte after the buffer). */ const char* GetEnd() { return mBuf + mLength; } /* Extend the buffer over the content of the other buffer, assuming it is * adjacent. */ void Extend(Buffer aOther) { MOZ_ASSERT(aOther.mBuf == GetEnd()); mLength += aOther.mLength; } private: const char* mBuf; size_t mLength; }; /* Helper class to read from a file descriptor line by line. */ class FdReader { public: explicit FdReader(int aFd) : mFd(aFd) , mData(&mRawBuf, 0) , mBuf(&mRawBuf, sizeof(mRawBuf)) {} /* Read a line from the file descriptor and returns it as a Buffer instance */ Buffer ReadLine() { while (true) { Buffer result = mData.SplitChar('\n'); /* There are essentially three different cases here: * - '\n' was found "early". In this case, the end of the result buffer * is before the beginning of the mData buffer (since SplitChar * amputated it). * - '\n' was found as the last character of mData. In this case, mData * is empty, but still points at the end of mBuf. result points to what * used to be in mData, without the last character. * - '\n' was not found. In this case too, mData is empty and points at * the end of mBuf. But result points to the entire buffer that used to * be pointed by mData. * Only in the latter case do both result and mData's end match, and it's * the only case where we need to refill the buffer. */ if (result.GetEnd() != mData.GetEnd()) { return result; } /* Since SplitChar emptied mData, make it point to what it had before. */ mData = result; /* And move it to the beginning of the read buffer. */ mData.Slide(mBuf); FillBuffer(); if (!mData) { return Buffer(); } } } private: /* Fill the read buffer. */ void FillBuffer() { size_t size = mBuf.GetEnd() - mData.GetEnd(); Buffer remainder(mData.GetEnd(), size); ssize_t len = 1; while (remainder && len > 0) { len = ::read(mFd, const_cast(remainder.get()), size); if (len < 0) { die("Read error"); } size -= len; mData.Extend(remainder.Split(len)); } } /* File descriptor to read from. */ int mFd; /* Part of data that was read from the file descriptor but not returned with * ReadLine yet. */ Buffer mData; /* Buffer representation of mRawBuf */ Buffer mBuf; /* read() buffer */ char mRawBuf[4096]; }; MOZ_BEGIN_EXTERN_C /* Function declarations for all the replace_malloc _impl functions. * See memory/build/replace_malloc.c */ #define MALLOC_DECL(name, return_type, ...) \ return_type name ## _impl(__VA_ARGS__); #define MALLOC_FUNCS MALLOC_FUNCS_MALLOC #include "malloc_decls.h" #define MALLOC_DECL(name, return_type, ...) \ return_type name ## _impl(__VA_ARGS__); #define MALLOC_FUNCS MALLOC_FUNCS_JEMALLOC #include "malloc_decls.h" /* mozjemalloc relies on DllMain to initialize, but DllMain is not invoked * for executables, so manually invoke mozjemalloc initialization. */ #if defined(_WIN32) && !defined(MOZ_JEMALLOC4) void malloc_init_hard(void); #endif #ifdef ANDROID /* mozjemalloc uses MozTagAnonymousMemory, which doesn't have an inline * implementation on Android */ void MozTagAnonymousMemory(const void* aPtr, size_t aLength, const char* aTag) {} /* mozjemalloc and jemalloc use pthread_atfork, which Android doesn't have. * While gecko has one in libmozglue, the replay program can't use that. * Since we're not going to fork anyways, make it a dummy function. */ int pthread_atfork(void (*aPrepare)(void), void (*aParent)(void), void (*aChild)(void)) { return 0; } #endif MOZ_END_EXTERN_C size_t parseNumber(Buffer aBuf) { if (!aBuf) { die("Malformed input"); } size_t result = 0; for (const char* c = aBuf.get(), *end = aBuf.GetEnd(); c < end; c++) { if (*c < '0' || *c > '9') { die("Malformed input"); } result *= 10; result += *c - '0'; } return result; } /* Class to handle dispatching the replay function calls to replace-malloc. */ class Replay { public: Replay(): mOps(0) { #ifdef _WIN32 // See comment in FdPrintf.h as to why native win32 handles are used. mStdErr = reinterpret_cast(GetStdHandle(STD_ERROR_HANDLE)); #else mStdErr = fileno(stderr); #endif } MemSlot& operator[] (size_t index) const { return mSlots[index]; } void malloc(MemSlot& aSlot, Buffer& aArgs) { mOps++; size_t size = parseNumber(aArgs); aSlot.mPtr = ::malloc_impl(size); aSlot.mSize = size; Commit(aSlot); } void posix_memalign(MemSlot& aSlot, Buffer& aArgs) { mOps++; size_t alignment = parseNumber(aArgs.SplitChar(',')); size_t size = parseNumber(aArgs); void* ptr; if (::posix_memalign_impl(&ptr, alignment, size) == 0) { aSlot.mPtr = ptr; aSlot.mSize = size; } else { aSlot.mPtr = nullptr; aSlot.mSize = 0; } Commit(aSlot); } void aligned_alloc(MemSlot& aSlot, Buffer& aArgs) { mOps++; size_t alignment = parseNumber(aArgs.SplitChar(',')); size_t size = parseNumber(aArgs); aSlot.mPtr = ::aligned_alloc_impl(alignment, size); aSlot.mSize = size; Commit(aSlot); } void calloc(MemSlot& aSlot, Buffer& aArgs) { mOps++; size_t num = parseNumber(aArgs.SplitChar(',')); size_t size = parseNumber(aArgs); aSlot.mPtr = ::calloc_impl(num, size); aSlot.mSize = size * num; Commit(aSlot); } void realloc(MemSlot& aSlot, Buffer& aArgs) { mOps++; Buffer dummy = aArgs.SplitChar('#'); if (dummy) { die("Malformed input"); } size_t slot_id = parseNumber(aArgs.SplitChar(',')); size_t size = parseNumber(aArgs); MemSlot& old_slot = (*this)[slot_id]; void* old_ptr = old_slot.mPtr; old_slot.mPtr = nullptr; old_slot.mSize = 0; aSlot.mPtr = ::realloc_impl(old_ptr, size); aSlot.mSize = size; Commit(aSlot); } void free(Buffer& aArgs) { mOps++; Buffer dummy = aArgs.SplitChar('#'); if (dummy) { die("Malformed input"); } size_t slot_id = parseNumber(aArgs); MemSlot& slot = (*this)[slot_id]; ::free_impl(slot.mPtr); slot.mPtr = nullptr; slot.mSize = 0; } void memalign(MemSlot& aSlot, Buffer& aArgs) { mOps++; size_t alignment = parseNumber(aArgs.SplitChar(',')); size_t size = parseNumber(aArgs); aSlot.mPtr = ::memalign_impl(alignment, size); aSlot.mSize = size; Commit(aSlot); } void valloc(MemSlot& aSlot, Buffer& aArgs) { mOps++; size_t size = parseNumber(aArgs); aSlot.mPtr = ::valloc_impl(size); aSlot.mSize = size; Commit(aSlot); } void jemalloc_stats(Buffer& aArgs) { if (aArgs) { die("Malformed input"); } jemalloc_stats_t stats; ::jemalloc_stats_impl(&stats); FdPrintf(mStdErr, "#%zu mapped: %zu; allocated: %zu; waste: %zu; dirty: %zu; " "bookkeep: %zu; binunused: %zu\n", mOps, stats.mapped, stats.allocated, stats.waste, stats.page_cache, stats.bookkeeping, stats.bin_unused); /* TODO: Add more data, like actual RSS as measured by OS, but compensated * for the replay internal data. */ } private: void Commit(MemSlot& aSlot) { memset(aSlot.mPtr, 0x5a, aSlot.mSize); } intptr_t mStdErr; size_t mOps; MemSlotList mSlots; }; int main() { size_t first_pid = 0; FdReader reader(0); Replay replay; #if defined(_WIN32) && !defined(MOZ_JEMALLOC4) malloc_init_hard(); #endif /* Read log from stdin and dispatch function calls to the Replay instance. * The log format is essentially: * ([])[=] * is a comma separated list of arguments. * * The logs are expected to be preprocessed so that allocations are * attributed a tracking slot. The input is trusted not to have crazy * values for these slot numbers. * * , as well as some of the args to some of the function calls are * such slot numbers. */ while (true) { Buffer line = reader.ReadLine(); if (!line) { break; } size_t pid = parseNumber(line.SplitChar(' ')); if (!first_pid) { first_pid = pid; } /* The log may contain data for several processes, only entries for the * very first that appears are treated. */ if (first_pid != pid) { continue; } /* The log contains thread ids for manual analysis, but we just ignore them * for now. */ parseNumber(line.SplitChar(' ')); Buffer func = line.SplitChar('('); Buffer args = line.SplitChar(')'); /* jemalloc_stats and free are functions with no result. */ if (func == Buffer("jemalloc_stats")) { replay.jemalloc_stats(args); continue; } else if (func == Buffer("free")) { replay.free(args); continue; } /* Parse result value and get the corresponding slot. */ Buffer dummy = line.SplitChar('='); Buffer dummy2 = line.SplitChar('#'); if (dummy || dummy2) { die("Malformed input"); } size_t slot_id = parseNumber(line); MemSlot& slot = replay[slot_id]; if (func == Buffer("malloc")) { replay.malloc(slot, args); } else if (func == Buffer("posix_memalign")) { replay.posix_memalign(slot, args); } else if (func == Buffer("aligned_alloc")) { replay.aligned_alloc(slot, args); } else if (func == Buffer("calloc")) { replay.calloc(slot, args); } else if (func == Buffer("realloc")) { replay.realloc(slot, args); } else if (func == Buffer("memalign")) { replay.memalign(slot, args); } else if (func == Buffer("valloc")) { replay.valloc(slot, args); } else { die("Malformed input"); } } return 0; }