diff options
Diffstat (limited to 'memory/replace/logalloc/replay')
-rw-r--r-- | memory/replace/logalloc/replay/Makefile.in | 32 | ||||
-rw-r--r-- | memory/replace/logalloc/replay/Replay.cpp | 558 | ||||
-rw-r--r-- | memory/replace/logalloc/replay/logalloc_munge.py | 147 | ||||
-rw-r--r-- | memory/replace/logalloc/replay/moz.build | 27 | ||||
-rw-r--r-- | memory/replace/logalloc/replay/replay.log | 17 |
5 files changed, 781 insertions, 0 deletions
diff --git a/memory/replace/logalloc/replay/Makefile.in b/memory/replace/logalloc/replay/Makefile.in new file mode 100644 index 000000000..47e8c43ca --- /dev/null +++ b/memory/replace/logalloc/replay/Makefile.in @@ -0,0 +1,32 @@ +# 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/. + +include $(topsrcdir)/mozglue/build/replace_malloc.mk + +ifndef CROSS_COMPILE + +ifeq ($(OS_TARGET),WINNT) +LOGALLOC = MOZ_REPLACE_MALLOC_LIB=$(CURDIR)/../logalloc.dll +else +ifeq ($(OS_TARGET),Darwin) +LOGALLOC = DYLD_INSERT_LIBRARIES=$(CURDIR)/../liblogalloc.dylib +else +LOGALLOC = LD_PRELOAD=$(CURDIR)/../$(DLL_PREFIX)logalloc$(DLL_SUFFIX) +endif +endif + +expected_output.log: $(srcdir)/replay.log +# The logalloc-replay program will only replay entries from the first pid, +# so the expected output only contains entries beginning with "1 " + grep "^1 " $< > $@ + +check:: $(srcdir)/replay.log expected_output.log +# Test with MALLOC_LOG as a file descriptor number + MALLOC_LOG=1 $(LOGALLOC) ./$(PROGRAM) < $< | $(PYTHON) $(srcdir)/logalloc_munge.py | diff -w - expected_output.log +# Test with MALLOC_LOG as a file name + $(RM) test_output.log + MALLOC_LOG=test_output.log $(LOGALLOC) ./$(PROGRAM) < $< + $(PYTHON) $(srcdir)/logalloc_munge.py < test_output.log | diff -w - expected_output.log + +endif diff --git a/memory/replace/logalloc/replay/Replay.cpp b/memory/replace/logalloc/replay/Replay.cpp new file mode 100644 index 000000000..30fcd21e5 --- /dev/null +++ b/memory/replace/logalloc/replay/Replay.cpp @@ -0,0 +1,558 @@ +/* -*- 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 <windows.h> +#include <io.h> +typedef int ssize_t; +#else +#include <sys/mman.h> +#include <unistd.h> +#endif +#include <algorithm> +#include <cstdio> +#include <cstring> + +#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 <typename T, size_t Len> +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<T*>(VirtualAlloc(nullptr, sizeof(T) * Len, + MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)); + if (mPtr == nullptr) { + die("VirtualAlloc error"); + } +#else + mPtr = reinterpret_cast<T*>(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<MemSlot, kGroupSize> mSlots[kGroups]; + MappedArray<MemSlotList, 1> 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<const char*>(aBuf)), mLength(aLength) + {} + + /* Constructor for string literals. */ + template <size_t Size> + 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<char*>(mBuf); + char* c = reinterpret_cast<char*>(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<char*>(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<char*>(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<intptr_t>(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: + * <pid> <function>([<args>])[=<result>] + * <args> 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. + * + * <result>, 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; +} diff --git a/memory/replace/logalloc/replay/logalloc_munge.py b/memory/replace/logalloc/replay/logalloc_munge.py new file mode 100644 index 000000000..a244c3c3d --- /dev/null +++ b/memory/replace/logalloc/replay/logalloc_munge.py @@ -0,0 +1,147 @@ +# 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/. + +""" +This script takes a log from the replace-malloc logalloc library on stdin +and munges it so that it can be used with the logalloc-replay tool. + +Given the following output: + 13663 malloc(42)=0x7f0c33502040 + 13663 malloc(24)=0x7f0c33503040 + 13663 free(0x7f0c33502040) +The resulting output is: + 1 malloc(42)=#1 + 1 malloc(24)=#2 + 1 free(#1) + +See README for more details. +""" + +from __future__ import print_function +import sys +from collections import ( + defaultdict, + deque, +) + +class IdMapping(object): + """Class to map values to ids. + + Each value is associated to an increasing id, starting from 1. + When a value is removed, its id is recycled and will be reused for + subsequent values. + """ + def __init__(self): + self.id = 1 + self._values = {} + self._recycle = deque() + + def __getitem__(self, value): + if value not in self._values: + if self._recycle: + self._values[value] = self._recycle.popleft() + else: + self._values[value] = self.id + self.id += 1 + return self._values[value] + + def __delitem__(self, value): + if value == 0: + return + self._recycle.append(self._values[value]) + del self._values[value] + + def __contains__(self, value): + return value == 0 or value in self._values + + +class Ignored(Exception): pass + + +def split_log_line(line): + try: + # The format for each line is: + # <pid> [<tid>] <function>([<args>])[=<result>] + # + # The original format didn't include the tid, so we try to parse + # lines whether they have one or not. + pid, func_call = line.split(' ', 1) + call, result = func_call.split(')') + func, args = call.split('(') + args = args.split(',') if args else [] + if result: + if result[0] != '=': + raise Ignored('Malformed input') + result = result[1:] + if ' ' in func: + tid, func = func.split(' ', 1) + else: + tid = pid + return pid, tid, func, args, result + except: + raise Ignored('Malformed input') + + +NUM_ARGUMENTS = { + 'jemalloc_stats': 0, + 'free': 1, + 'malloc': 1, + 'posix_memalign': 2, + 'aligned_alloc': 2, + 'calloc': 2, + 'realloc': 2, + 'memalign': 2, + 'valloc': 1, +} + + +def main(): + pids = IdMapping() + processes = defaultdict(lambda: { 'pointers': IdMapping(), + 'tids': IdMapping() }) + for line in sys.stdin: + line = line.strip() + + try: + pid, tid, func, args, result = split_log_line(line) + + # Replace pid with an id. + pid = pids[int(pid)] + + process = processes[pid] + tid = process['tids'][int(tid)] + + pointers = process['pointers'] + + if func not in NUM_ARGUMENTS: + raise Ignored('Unknown function') + + if len(args) != NUM_ARGUMENTS[func]: + raise Ignored('Malformed input') + + if func in ('jemalloc_stats', 'free') and result: + raise Ignored('Malformed input') + + if func in ('free', 'realloc'): + ptr = int(args[0], 16) + if ptr and ptr not in pointers: + raise Ignored('Did not see an alloc for pointer') + args[0] = "#%d" % pointers[ptr] + del pointers[ptr] + + if result: + result = int(result, 16) + if not result: + raise Ignored('Result is NULL') + result = "#%d" % pointers[result] + + print('%d %d %s(%s)%s' % (pid, tid, func, ','.join(args), + '=%s' % result if result else '')) + + except Exception as e: + print('Ignored "%s": %s' % (line, e.message), file=sys.stderr) + + +if __name__ == '__main__': + main() diff --git a/memory/replace/logalloc/replay/moz.build b/memory/replace/logalloc/replay/moz.build new file mode 100644 index 000000000..bb976908f --- /dev/null +++ b/memory/replace/logalloc/replay/moz.build @@ -0,0 +1,27 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Program('logalloc-replay') + +SOURCES += [ + '../FdPrintf.cpp', + '/mfbt/Assertions.cpp', + 'Replay.cpp', +] + +LOCAL_INCLUDES += [ + '..', +] + +# Link replace-malloc and the default allocator. +USE_LIBS += [ + 'memory', +] + +# The memory library defines this, so it's needed here too. +DEFINES['IMPL_MFBT'] = True + +DISABLE_STL_WRAPPING = True diff --git a/memory/replace/logalloc/replay/replay.log b/memory/replace/logalloc/replay/replay.log new file mode 100644 index 000000000..c56dfab12 --- /dev/null +++ b/memory/replace/logalloc/replay/replay.log @@ -0,0 +1,17 @@ +1 1 malloc(42)=#1 +1 1 malloc(24)=#2 +2 2 malloc(42)=#1 +1 1 free(#1) +1 1 posix_memalign(4096,1024)=#1 +1 1 calloc(4,42)=#3 +1 1 free(#2) +1 1 realloc(#3,84)=#2 +1 1 aligned_alloc(512,1024)=#3 +1 1 memalign(512,1024)=#4 +1 1 valloc(1024)=#5 +1 1 jemalloc_stats() +1 1 free(#5) +1 1 free(#4) +1 1 free(#3) +1 1 free(#2) +1 1 free(#1) |