summaryrefslogtreecommitdiffstats
path: root/memory/replace/logalloc
diff options
context:
space:
mode:
Diffstat (limited to 'memory/replace/logalloc')
-rw-r--r--memory/replace/logalloc/FdPrintf.cpp199
-rw-r--r--memory/replace/logalloc/FdPrintf.h27
-rw-r--r--memory/replace/logalloc/LogAlloc.cpp236
-rw-r--r--memory/replace/logalloc/README95
-rw-r--r--memory/replace/logalloc/moz.build30
-rw-r--r--memory/replace/logalloc/replay/Makefile.in42
-rw-r--r--memory/replace/logalloc/replay/Replay.cpp739
-rw-r--r--memory/replace/logalloc/replay/expected_output_minimal.log17
-rw-r--r--memory/replace/logalloc/replay/logalloc_munge.py151
-rw-r--r--memory/replace/logalloc/replay/moz.build69
-rw-r--r--memory/replace/logalloc/replay/replay.log18
11 files changed, 1623 insertions, 0 deletions
diff --git a/memory/replace/logalloc/FdPrintf.cpp b/memory/replace/logalloc/FdPrintf.cpp
new file mode 100644
index 0000000000..d8e27ea133
--- /dev/null
+++ b/memory/replace/logalloc/FdPrintf.cpp
@@ -0,0 +1,199 @@
+/* -*- 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/. */
+
+#include <cstdarg>
+
+#ifdef _WIN32
+# include <windows.h>
+#else
+# include <unistd.h>
+#endif
+#include <cmath>
+#include <cstring>
+#include "mozilla/Assertions.h"
+#include "mozilla/Unused.h"
+
+/* Template class allowing a limited number of increments on a value */
+template <typename T>
+class CheckedIncrement {
+ public:
+ CheckedIncrement(T aValue, size_t aMaxIncrement)
+ : mValue(aValue), mMaxIncrement(aMaxIncrement) {}
+
+ T operator++(int) {
+ if (!mMaxIncrement) {
+ MOZ_CRASH("overflow detected");
+ }
+ mMaxIncrement--;
+ return mValue++;
+ }
+
+ T& operator++() {
+ (*this)++;
+ return mValue;
+ }
+
+ void advance(T end) {
+ // Only makes sense if T is a pointer type.
+ size_t diff = end - mValue;
+ if (diff > mMaxIncrement) {
+ MOZ_CRASH("overflow detected");
+ }
+ mMaxIncrement -= diff;
+ mValue = end;
+ };
+
+ void rewind(T pos) {
+ size_t diff = mValue - pos;
+ mMaxIncrement += diff;
+ mValue = pos;
+ }
+
+ operator T() { return mValue; }
+ T value() { return mValue; }
+
+ private:
+ T mValue;
+ size_t mMaxIncrement;
+};
+
+template <typename T>
+static unsigned NumDigits(T n) {
+ if (n < 1) {
+ // We want one digit, it will be 0.
+ return 1;
+ }
+
+ double l = log10(static_cast<double>(n));
+ double cl = ceil(l);
+ return l == cl ? unsigned(cl) + 1 : unsigned(cl);
+}
+
+static void LeftPad(CheckedIncrement<char*>& b, size_t pad) {
+ while (pad-- > 0) {
+ *(b++) = ' ';
+ }
+}
+
+// Write the digits into the buffer.
+static void WriteDigits(CheckedIncrement<char*>& b, size_t i,
+ size_t num_digits) {
+ size_t x = pow(10, double(num_digits - 1));
+ do {
+ *(b++) = "0123456789"[(i / x) % 10];
+ x /= 10;
+ } while (x > 0);
+}
+
+void FdPrintf(intptr_t aFd, const char* aFormat, ...) {
+ if (aFd == 0) {
+ return;
+ }
+ char buf[256];
+ CheckedIncrement<char*> b(buf, sizeof(buf));
+ CheckedIncrement<const char*> f(aFormat, strlen(aFormat) + 1);
+ va_list ap;
+ va_start(ap, aFormat);
+ while (true) {
+ switch (*f) {
+ case '\0':
+ goto out;
+
+ case '%': {
+ // The start of the format specifier is used if this specifier is
+ // invalid.
+ const char* start = f;
+
+ // Read the field width
+ f++;
+ char* end = nullptr;
+ size_t width = strtoul(f, &end, 10);
+ // If strtol can't find a number that's okay, that means 0 in our
+ // case, but we must advance f).
+ f.advance(end);
+
+ switch (*f) {
+ case 'z': {
+ if (*(++f) == 'u') {
+ size_t i = va_arg(ap, size_t);
+
+ size_t num_digits = NumDigits(i);
+ LeftPad(b, width > num_digits ? width - num_digits : 0);
+ WriteDigits(b, i, num_digits);
+ } else {
+ // If the format specifier is unknown then write out '%' and
+ // rewind to the beginning of the specifier causing it to be
+ // printed normally.
+ *(b++) = '%';
+ f.rewind(start);
+ }
+ break;
+ }
+
+ case 'p': {
+ intptr_t ptr = va_arg(ap, intptr_t);
+ *(b++) = '0';
+ *(b++) = 'x';
+ int x = sizeof(intptr_t) * 8;
+ bool wrote_msb = false;
+ do {
+ x -= 4;
+ size_t hex_digit = ptr >> x & 0xf;
+ if (hex_digit || wrote_msb) {
+ *(b++) = "0123456789abcdef"[hex_digit];
+ wrote_msb = true;
+ }
+ } while (x > 0);
+ if (!wrote_msb) {
+ *(b++) = '0';
+ }
+ break;
+ }
+
+ case 's': {
+ const char* str = va_arg(ap, const char*);
+ size_t len = strlen(str);
+
+ LeftPad(b, width > len ? width - len : 0);
+
+ while (*str) {
+ *(b++) = *(str++);
+ }
+
+ break;
+ }
+
+ case '%':
+ // Print a single raw '%'.
+ *(b++) = '%';
+ break;
+
+ default:
+ // If the format specifier is unknown then write out '%' and
+ // rewind to the beginning of the specifier causing it to be
+ // printed normally.
+ *(b++) = '%';
+ f.rewind(start);
+ break;
+ }
+ break;
+ }
+ default:
+ *(b++) = *f;
+ break;
+ }
+ f++;
+ }
+out:
+#ifdef _WIN32
+ // See comment in FdPrintf.h as to why WriteFile is used.
+ DWORD written;
+ WriteFile(reinterpret_cast<HANDLE>(aFd), buf, b - buf, &written, nullptr);
+#else
+ MOZ_UNUSED(write(aFd, buf, b - buf));
+#endif
+ va_end(ap);
+}
diff --git a/memory/replace/logalloc/FdPrintf.h b/memory/replace/logalloc/FdPrintf.h
new file mode 100644
index 0000000000..f390d57ed5
--- /dev/null
+++ b/memory/replace/logalloc/FdPrintf.h
@@ -0,0 +1,27 @@
+/* -*- 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/. */
+
+#ifndef __FdPrintf_h__
+#define __FdPrintf_h__
+
+/* We can't use libc's (f)printf because it would reenter in replace_malloc,
+ * So use a custom and simplified version. Only %p, %zu, %s and %% are
+ * supported, %zu, %s, support width specifiers.
+ *
+ * /!\ This function used a fixed-size internal buffer. The caller is
+ * expected to not use a format string that may overflow.
+ * The aFd argument is a file descriptor on UNIX and a native win32 file
+ * handle on Windows (from CreateFile). We can't use the windows POSIX
+ * APIs is that they don't support O_APPEND in a multi-process-safe way,
+ * while CreateFile does.
+ */
+extern void FdPrintf(intptr_t aFd, const char* aFormat, ...)
+#ifdef __GNUC__
+ __attribute__((format(printf, 2, 3)))
+#endif
+ ;
+
+#endif /* __FdPrintf_h__ */
diff --git a/memory/replace/logalloc/LogAlloc.cpp b/memory/replace/logalloc/LogAlloc.cpp
new file mode 100644
index 0000000000..8d6aad0675
--- /dev/null
+++ b/memory/replace/logalloc/LogAlloc.cpp
@@ -0,0 +1,236 @@
+/* -*- 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/. */
+
+#include <cstdlib>
+#include <cstdio>
+#include <fcntl.h>
+
+#ifdef _WIN32
+# include <windows.h>
+# include <io.h>
+# include <process.h>
+#else
+# include <unistd.h>
+# include <pthread.h>
+#endif
+
+#include "replace_malloc.h"
+#include "FdPrintf.h"
+#include "Mutex.h"
+
+static malloc_table_t sFuncs;
+static intptr_t sFd = 0;
+static bool sStdoutOrStderr = false;
+
+static Mutex sMutex;
+
+#ifndef _WIN32
+static void prefork() { sMutex.Lock(); }
+
+static void postfork() { sMutex.Unlock(); }
+#endif
+
+static size_t GetPid() { return size_t(getpid()); }
+
+static size_t GetTid() {
+#if defined(_WIN32)
+ return size_t(GetCurrentThreadId());
+#else
+ return size_t(pthread_self());
+#endif
+}
+
+#ifdef ANDROID
+/* Android doesn't have pthread_atfork defined in pthread.h */
+extern "C" MOZ_EXPORT int pthread_atfork(void (*)(void), void (*)(void),
+ void (*)(void));
+#endif
+
+class LogAllocBridge : public ReplaceMallocBridge {
+ virtual void InitDebugFd(mozilla::DebugFdRegistry& aRegistry) override {
+ if (!sStdoutOrStderr) {
+ aRegistry.RegisterHandle(sFd);
+ }
+ }
+};
+
+/* Do a simple, text-form, log of all calls to replace-malloc functions.
+ * Use locking to guarantee that an allocation that did happen is logged
+ * before any other allocation/free happens.
+ */
+
+static void* replace_malloc(size_t aSize) {
+ MutexAutoLock lock(sMutex);
+ void* ptr = sFuncs.malloc(aSize);
+ FdPrintf(sFd, "%zu %zu malloc(%zu)=%p\n", GetPid(), GetTid(), aSize, ptr);
+ return ptr;
+}
+
+static int replace_posix_memalign(void** aPtr, size_t aAlignment,
+ size_t aSize) {
+ MutexAutoLock lock(sMutex);
+ int ret = sFuncs.posix_memalign(aPtr, aAlignment, aSize);
+ FdPrintf(sFd, "%zu %zu posix_memalign(%zu,%zu)=%p\n", GetPid(), GetTid(),
+ aAlignment, aSize, (ret == 0) ? *aPtr : nullptr);
+ return ret;
+}
+
+static void* replace_aligned_alloc(size_t aAlignment, size_t aSize) {
+ MutexAutoLock lock(sMutex);
+ void* ptr = sFuncs.aligned_alloc(aAlignment, aSize);
+ FdPrintf(sFd, "%zu %zu aligned_alloc(%zu,%zu)=%p\n", GetPid(), GetTid(),
+ aAlignment, aSize, ptr);
+ return ptr;
+}
+
+static void* replace_calloc(size_t aNum, size_t aSize) {
+ MutexAutoLock lock(sMutex);
+ void* ptr = sFuncs.calloc(aNum, aSize);
+ FdPrintf(sFd, "%zu %zu calloc(%zu,%zu)=%p\n", GetPid(), GetTid(), aNum, aSize,
+ ptr);
+ return ptr;
+}
+
+static void* replace_realloc(void* aPtr, size_t aSize) {
+ MutexAutoLock lock(sMutex);
+ void* new_ptr = sFuncs.realloc(aPtr, aSize);
+ FdPrintf(sFd, "%zu %zu realloc(%p,%zu)=%p\n", GetPid(), GetTid(), aPtr, aSize,
+ new_ptr);
+ return new_ptr;
+}
+
+static void replace_free(void* aPtr) {
+ MutexAutoLock lock(sMutex);
+ FdPrintf(sFd, "%zu %zu free(%p)\n", GetPid(), GetTid(), aPtr);
+ sFuncs.free(aPtr);
+}
+
+static void* replace_memalign(size_t aAlignment, size_t aSize) {
+ MutexAutoLock lock(sMutex);
+ void* ptr = sFuncs.memalign(aAlignment, aSize);
+ FdPrintf(sFd, "%zu %zu memalign(%zu,%zu)=%p\n", GetPid(), GetTid(),
+ aAlignment, aSize, ptr);
+ return ptr;
+}
+
+static void* replace_valloc(size_t aSize) {
+ MutexAutoLock lock(sMutex);
+ void* ptr = sFuncs.valloc(aSize);
+ FdPrintf(sFd, "%zu %zu valloc(%zu)=%p\n", GetPid(), GetTid(), aSize, ptr);
+ return ptr;
+}
+
+static void replace_jemalloc_stats(jemalloc_stats_t* aStats,
+ jemalloc_bin_stats_t* aBinStats) {
+ MutexAutoLock lock(sMutex);
+ sFuncs.jemalloc_stats_internal(aStats, aBinStats);
+ FdPrintf(sFd, "%zu %zu jemalloc_stats()\n", GetPid(), GetTid());
+}
+
+void replace_init(malloc_table_t* aTable, ReplaceMallocBridge** aBridge) {
+ /* Initialize output file descriptor from the MALLOC_LOG environment
+ * variable. Numbers up to 9999 are considered as a preopened file
+ * descriptor number. Other values are considered as a file name. */
+#ifdef _WIN32
+ wchar_t* log = _wgetenv(L"MALLOC_LOG");
+#else
+ char* log = getenv("MALLOC_LOG");
+#endif
+ if (log && *log) {
+ int fd = 0;
+ const auto* fd_num = log;
+ while (*fd_num) {
+ /* Reject non digits. */
+ if (*fd_num < '0' || *fd_num > '9') {
+ fd = -1;
+ break;
+ }
+ fd = fd * 10 + (*fd_num - '0');
+ /* Reject values >= 10000. */
+ if (fd >= 10000) {
+ fd = -1;
+ break;
+ }
+ fd_num++;
+ }
+ if (fd == 1 || fd == 2) {
+ sStdoutOrStderr = true;
+ }
+#ifdef _WIN32
+ // See comment in FdPrintf.h as to why CreateFile is used.
+ HANDLE handle;
+ if (fd > 0) {
+ handle = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
+ } else {
+ handle =
+ CreateFileW(log, FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE,
+ nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
+ }
+ if (handle != INVALID_HANDLE_VALUE) {
+ sFd = reinterpret_cast<intptr_t>(handle);
+ }
+#else
+ if (fd == -1) {
+ fd = open(log, O_WRONLY | O_CREAT | O_APPEND, 0644);
+ }
+ if (fd > 0) {
+ sFd = fd;
+ }
+#endif
+ }
+
+ // Don't initialize if we weren't passed a valid MALLOC_LOG.
+ if (sFd == 0) {
+ return;
+ }
+
+ sMutex.Init();
+ static LogAllocBridge bridge;
+ sFuncs = *aTable;
+#define MALLOC_FUNCS MALLOC_FUNCS_MALLOC_BASE
+#define MALLOC_DECL(name, ...) aTable->name = replace_##name;
+#include "malloc_decls.h"
+ aTable->jemalloc_stats_internal = replace_jemalloc_stats;
+ if (!getenv("MALLOC_LOG_MINIMAL")) {
+ aTable->posix_memalign = replace_posix_memalign;
+ aTable->aligned_alloc = replace_aligned_alloc;
+ aTable->valloc = replace_valloc;
+ }
+ *aBridge = &bridge;
+
+#ifndef _WIN32
+ /* When another thread has acquired a lock before forking, the child
+ * process will inherit the lock state but the thread, being nonexistent
+ * in the child process, will never release it, leading to a dead-lock
+ * whenever the child process gets the lock. We thus need to ensure no
+ * other thread is holding the lock before forking, by acquiring it
+ * ourselves, and releasing it after forking, both in the parent and child
+ * processes.
+ * Windows doesn't have this problem since there is no fork().
+ * The real allocator, however, might be doing the same thing (jemalloc
+ * does). But pthread_atfork `prepare` handlers (first argument) are
+ * processed in reverse order they were established. But replace_init
+ * runs before the real allocator has had any chance to initialize and
+ * call pthread_atfork itself. This leads to its prefork running before
+ * ours. This leads to a race condition that can lead to a deadlock like
+ * the following:
+ * - thread A forks.
+ * - libc calls real allocator's prefork, so thread A holds the real
+ * allocator lock.
+ * - thread B calls malloc, which calls our replace_malloc.
+ * - consequently, thread B holds our lock.
+ * - thread B then proceeds to call the real allocator's malloc, and
+ * waits for the real allocator's lock, which thread A holds.
+ * - libc calls our prefork, so thread A waits for our lock, which
+ * thread B holds.
+ * To avoid this race condition, the real allocator's prefork must be
+ * called after ours, which means it needs to be registered before ours.
+ * So trick the real allocator into initializing itself without more side
+ * effects by calling malloc with a size it can't possibly allocate. */
+ sFuncs.malloc(-1);
+ pthread_atfork(prefork, postfork, postfork);
+#endif
+}
diff --git a/memory/replace/logalloc/README b/memory/replace/logalloc/README
new file mode 100644
index 0000000000..c2e8cf66ce
--- /dev/null
+++ b/memory/replace/logalloc/README
@@ -0,0 +1,95 @@
+Logalloc is a replace-malloc library for Firefox (see
+memory/build/replace_malloc.h) that dumps a log of memory allocations to a
+given file descriptor or file name. That log can then be replayed against
+Firefox's default memory allocator independently or through another
+replace-malloc library, allowing the testing of other allocators under the
+exact same workload.
+
+To get an allocation log the following environment variable when starting
+Firefox:
+ MALLOC_LOG=/path/to/log-file
+ or
+ MALLOC_LOG=number
+
+When MALLOC_LOG is a number below 10000, it is considered as a file
+descriptor number that is fed to Firefox when it is started. Otherwise,
+it is considered as a file name.
+
+As those allocation logs can grow large quite quickly, it can be useful
+to pipe the output to a compression tool.
+
+MALLOC_LOG=1 would send to Firefox's stdout, MALLOC_LOG=2 would send to
+its stderr. Since in both cases that could be mixed with other output
+from Firefox, it is usually better to use another file descriptor
+by shell redirections, such as:
+
+ MALLOC_LOG=3 firefox 3>&1 1>&2 | gzip -c > log.gz
+
+(3>&1 copies the `| gzip` pipe file descriptor to file descriptor #3, 1>&2
+then copies stderr to stdout. This leads to: fd1 and fd2 sending to stderr
+of the parent process (the shell), and fd3 sending to gzip.)
+
+Each line of the allocations log is formatted as follows:
+ <pid> <tid> <function>([<args>])[=<result>]
+where <args> is a comma separated list of values. The number of <args> and
+the presence of <result> depend on the <function>.
+
+Example log:
+ 18545 18545 malloc(32)=0x7f90495120e0
+ 18545 18545 calloc(1,148)=0x7f9049537480
+ 18545 18545 realloc(0x7f90495120e0,64)=0x7f9049536680
+ 18545 18545 posix_memalign(256,240)=0x7f9049583300
+ 18545 18545 jemalloc_stats()
+ 18545 18545 free(0x7f9049536680)
+
+This log can be replayed with the logalloc-replay tool in
+memory/replace/logalloc/replay. However, as the goal of that tool is to
+reproduce the recorded memory allocations, it needs to avoid as much as
+possible doing its own allocations for bookkeeping. Reading the logs as
+they are would require data structures and memory allocations. As a
+consequence, the logs need to be preprocessed beforehand.
+
+The logalloc_munge.py script is responsible for that preprocessing. It simply
+takes a raw log on its stdin, and outputs the preprocessed log on its stdout.
+It replaces pointer addresses with indexes the logalloc-replay tool can use
+in a large (almost) linear array of allocation tracking slots (prefixed with
+'#'). It also replaces the pids with numbers starting from 1 (such as the
+first seen pid number is 1, the second is 2, etc.).
+
+The above example log would become the following, once preprocessed:
+ 1 1 malloc(32)=#1
+ 1 1 calloc(1,148)=#2
+ 1 1 realloc(#1,64)=#1
+ 1 1 posix_memalign(256,240)=#3
+ 1 1 jemalloc_stats()
+ 1 1 free(#1)
+
+The logalloc-replay tool then takes the preprocessed log on its stdin and
+replays the allocations printed there, but will only replay those with the
+same process id as the first line (which normally is 1).
+
+As the log files are simple text files, though, it is easy to separate out
+the different processes log with e.g. grep, and feed the separate processes
+logs to logalloc-replay.
+
+The logalloc-replay program won't output anything unless jemalloc_stats
+records appears in the log. You can expect those to be recorded when going
+to about:memory in Firefox, but they can also be added after preprocessing.
+
+Here is an example of what one can do:
+
+ gunzip -c log.gz | python logalloc_munge.py | \
+ awk '$1 == "2" { print $0 } !(NR % 10000) { print "2 1 jemalloc_stats()" }' | \
+ ./logalloc-replay
+
+The above command replays the allocations of process #2, with some stats
+output every 10000 records.
+
+The logalloc-replay tool itself being hooked with replace-malloc, it is possible
+to set LD_PRELOAD/DYLD_INSERT_LIBRARIES/MOZ_REPLACE_MALLOC_LIB and replay a log
+through a different allocator. For example:
+
+ LD_PRELOAD=libreplace_jemalloc.so logalloc-replay < log
+
+Will replay the log against jemalloc4 (which is, as of writing, what
+libreplace_jemalloc.so contains).
diff --git a/memory/replace/logalloc/moz.build b/memory/replace/logalloc/moz.build
new file mode 100644
index 0000000000..c52d9e69e0
--- /dev/null
+++ b/memory/replace/logalloc/moz.build
@@ -0,0 +1,30 @@
+# -*- 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/.
+
+ReplaceMalloc("logalloc")
+
+SOURCES += [
+ "FdPrintf.cpp",
+ "LogAlloc.cpp",
+]
+
+DisableStlWrapping()
+NO_PGO = True
+DEFINES["MOZ_NO_MOZALLOC"] = True
+
+LOCAL_INCLUDES += [
+ "/memory/build",
+]
+
+# Android doesn't have pthread_atfork, but we have our own in mozglue.
+if CONFIG["OS_TARGET"] == "Android" and FORCE_SHARED_LIB:
+ USE_LIBS += [
+ "mozglue",
+ ]
+
+DIRS += [
+ "replay",
+]
diff --git a/memory/replace/logalloc/replay/Makefile.in b/memory/replace/logalloc/replay/Makefile.in
new file mode 100644
index 0000000000..8f5b5589b6
--- /dev/null
+++ b/memory/replace/logalloc/replay/Makefile.in
@@ -0,0 +1,42 @@
+# 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/.
+
+ifndef CROSS_COMPILE
+ifndef MOZ_CODE_COVERAGE
+
+ifeq ($(OS_TARGET),WINNT)
+LOGALLOC_VAR = MOZ_REPLACE_MALLOC_LIB
+else
+ifeq ($(OS_TARGET),Darwin)
+LOGALLOC_VAR = DYLD_INSERT_LIBRARIES
+else
+LOGALLOC_VAR = LD_PRELOAD
+endif
+endif
+
+ifndef MOZ_REPLACE_MALLOC_STATIC
+LOGALLOC = $(LOGALLOC_VAR)=$(CURDIR)/../$(DLL_PREFIX)logalloc$(DLL_SUFFIX)
+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 $(srcdir)/expected_output_minimal.log
+# Test with MALLOC_LOG as a file descriptor number
+# We filter out anything happening before the first jemalloc_stats (first
+# command in replay.log) because starting with libstdc++ 5, a static
+# initializer in the STL allocates memory, which we obviously don't have
+# in expected_output.log.
+ MALLOC_LOG=1 $(LOGALLOC) ./$(PROGRAM) < $< | sed -n '/jemalloc_stats/,$$p' | $(PYTHON3) $(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) < $<
+ sed -n '/jemalloc_stats/,$$p' test_output.log | $(PYTHON3) $(srcdir)/logalloc_munge.py | diff -w - expected_output.log
+
+ MALLOC_LOG=1 MALLOC_LOG_MINIMAL=1 $(LOGALLOC) ./$(PROGRAM) < $< | sed -n '/jemalloc_stats/,$$p' | $(PYTHON3) $(srcdir)/logalloc_munge.py | diff -w - $(srcdir)/expected_output_minimal.log
+
+endif
+endif
diff --git a/memory/replace/logalloc/replay/Replay.cpp b/memory/replace/logalloc/replay/Replay.cpp
new file mode 100644
index 0000000000..daf0580389
--- /dev/null
+++ b/memory/replace/logalloc/replay/Replay.cpp
@@ -0,0 +1,739 @@
+/* -*- 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 intptr_t ssize_t;
+#else
+# include <sys/mman.h>
+# include <unistd.h>
+#endif
+#include <algorithm>
+#include <cmath>
+#include <cstdio>
+#include <cstring>
+
+#include "mozilla/Assertions.h"
+#include "mozilla/MathAlgorithms.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;
+
+ // mRequest is only valid if mPtr is non-null. It doesn't need to be cleared
+ // when memory is freed or realloc()ed.
+ size_t mRequest;
+};
+
+/* 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 67M 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(__VA_ARGS__);
+#define MALLOC_FUNCS MALLOC_FUNCS_JEMALLOC
+#include "malloc_decls.h"
+
+#ifdef ANDROID
+
+/* 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),
+ mNumUsedSlots(0),
+ mTotalRequestedSize(0),
+ mTotalAllocatedSize(0),
+ mCalculateSlop(false) {
+#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
+ }
+
+ void enableSlopCalculation() { mCalculateSlop = true; }
+
+ MemSlot& operator[](size_t index) const { return mSlots[index]; }
+
+ void malloc(Buffer& aArgs, Buffer& aResult) {
+ MemSlot& aSlot = SlotForResult(aResult);
+ mOps++;
+ size_t size = parseNumber(aArgs);
+ aSlot.mPtr = ::malloc_impl(size);
+ if (aSlot.mPtr) {
+ aSlot.mRequest = size;
+ if (mCalculateSlop) {
+ mTotalRequestedSize += size;
+ mTotalAllocatedSize += ::malloc_usable_size_impl(aSlot.mPtr);
+ }
+ }
+ }
+
+ void posix_memalign(Buffer& aArgs, Buffer& aResult) {
+ MemSlot& aSlot = SlotForResult(aResult);
+ 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.mRequest = size;
+ if (mCalculateSlop) {
+ mTotalRequestedSize += size;
+ mTotalAllocatedSize += ::malloc_usable_size_impl(aSlot.mPtr);
+ }
+ } else {
+ aSlot.mPtr = nullptr;
+ }
+ }
+
+ void aligned_alloc(Buffer& aArgs, Buffer& aResult) {
+ MemSlot& aSlot = SlotForResult(aResult);
+ mOps++;
+ size_t alignment = parseNumber(aArgs.SplitChar(','));
+ size_t size = parseNumber(aArgs);
+ aSlot.mPtr = ::aligned_alloc_impl(alignment, size);
+ if (aSlot.mPtr) {
+ aSlot.mRequest = size;
+ if (mCalculateSlop) {
+ mTotalRequestedSize += size;
+ mTotalAllocatedSize += ::malloc_usable_size_impl(aSlot.mPtr);
+ }
+ }
+ }
+
+ void calloc(Buffer& aArgs, Buffer& aResult) {
+ MemSlot& aSlot = SlotForResult(aResult);
+ mOps++;
+ size_t num = parseNumber(aArgs.SplitChar(','));
+ size_t size = parseNumber(aArgs);
+ aSlot.mPtr = ::calloc_impl(num, size);
+ if (aSlot.mPtr) {
+ aSlot.mRequest = num * size;
+ if (mCalculateSlop) {
+ mTotalRequestedSize += num * size;
+ mTotalAllocatedSize += ::malloc_usable_size_impl(aSlot.mPtr);
+ }
+ }
+ }
+
+ void realloc(Buffer& aArgs, Buffer& aResult) {
+ MemSlot& aSlot = SlotForResult(aResult);
+ 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;
+ aSlot.mPtr = ::realloc_impl(old_ptr, size);
+ if (aSlot.mPtr) {
+ aSlot.mRequest = size;
+ if (mCalculateSlop) {
+ mTotalRequestedSize += size;
+ mTotalAllocatedSize += ::malloc_usable_size_impl(aSlot.mPtr);
+ }
+ }
+ }
+
+ void free(Buffer& aArgs, Buffer& aResult) {
+ if (aResult) {
+ die("Malformed input");
+ }
+ 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;
+ }
+
+ void memalign(Buffer& aArgs, Buffer& aResult) {
+ MemSlot& aSlot = SlotForResult(aResult);
+ mOps++;
+ size_t alignment = parseNumber(aArgs.SplitChar(','));
+ size_t size = parseNumber(aArgs);
+ aSlot.mPtr = ::memalign_impl(alignment, size);
+ if (aSlot.mPtr) {
+ aSlot.mRequest = size;
+ if (mCalculateSlop) {
+ mTotalRequestedSize += size;
+ mTotalAllocatedSize += ::malloc_usable_size_impl(aSlot.mPtr);
+ }
+ }
+ }
+
+ void valloc(Buffer& aArgs, Buffer& aResult) {
+ MemSlot& aSlot = SlotForResult(aResult);
+ mOps++;
+ size_t size = parseNumber(aArgs);
+ aSlot.mPtr = ::valloc_impl(size);
+ if (aSlot.mPtr) {
+ aSlot.mRequest = size;
+ if (mCalculateSlop) {
+ mTotalRequestedSize += size;
+ mTotalAllocatedSize += ::malloc_usable_size_impl(aSlot.mPtr);
+ }
+ }
+ }
+
+ void jemalloc_stats(Buffer& aArgs, Buffer& aResult) {
+ if (aArgs || aResult) {
+ die("Malformed input");
+ }
+ mOps++;
+ jemalloc_stats_t stats;
+ jemalloc_bin_stats_t bin_stats[JEMALLOC_MAX_STATS_BINS];
+ ::jemalloc_stats_internal(&stats, bin_stats);
+
+ size_t num_objects = 0;
+ size_t num_sloppy_objects = 0;
+ size_t total_allocated = 0;
+ size_t total_slop = 0;
+ size_t large_slop = 0;
+ size_t large_used = 0;
+ size_t huge_slop = 0;
+ size_t huge_used = 0;
+ size_t bin_slop[JEMALLOC_MAX_STATS_BINS] = {0};
+
+ for (size_t slot_id = 0; slot_id < mNumUsedSlots; slot_id++) {
+ MemSlot& slot = mSlots[slot_id];
+ if (slot.mPtr) {
+ size_t used = ::malloc_usable_size_impl(slot.mPtr);
+ size_t slop = used - slot.mRequest;
+ total_allocated += used;
+ total_slop += slop;
+ num_objects++;
+ if (slop) {
+ num_sloppy_objects++;
+ }
+
+ if (used <= stats.page_size / 2) {
+ // We know that this is an inefficient linear search, but there's a
+ // small number of bins and this is simple.
+ for (unsigned i = 0; i < JEMALLOC_MAX_STATS_BINS; i++) {
+ auto& bin = bin_stats[i];
+ if (used == bin.size) {
+ bin_slop[i] += slop;
+ break;
+ }
+ }
+ } else if (used <= stats.large_max) {
+ large_slop += slop;
+ large_used += used;
+ } else {
+ huge_slop += slop;
+ huge_used += used;
+ }
+ }
+ }
+
+ FdPrintf(mStdErr, "\n");
+ FdPrintf(mStdErr, "Objects: %9zu\n", num_objects);
+ FdPrintf(mStdErr, "Slots: %9zu\n", mNumUsedSlots);
+ FdPrintf(mStdErr, "Ops: %9zu\n", mOps);
+ FdPrintf(mStdErr, "mapped: %9zu\n", stats.mapped);
+ FdPrintf(mStdErr, "allocated: %9zu\n", stats.allocated);
+ FdPrintf(mStdErr, "waste: %9zu\n", stats.waste);
+ FdPrintf(mStdErr, "dirty: %9zu\n", stats.page_cache);
+ FdPrintf(mStdErr, "bookkeep: %9zu\n", stats.bookkeeping);
+ FdPrintf(mStdErr, "bin-unused: %9zu\n", stats.bin_unused);
+ FdPrintf(mStdErr, "quantum-max: %9zu\n", stats.quantum_max);
+ FdPrintf(mStdErr, "subpage-max: %9zu\n", stats.page_size / 2);
+ FdPrintf(mStdErr, "large-max: %9zu\n", stats.large_max);
+ if (mCalculateSlop) {
+ size_t slop = mTotalAllocatedSize - mTotalRequestedSize;
+ FdPrintf(mStdErr,
+ "Total slop for all allocations: %zuKiB/%zuKiB (%zu%%)\n",
+ slop / 1024, mTotalAllocatedSize / 1024,
+ percent(slop, mTotalAllocatedSize));
+ }
+ FdPrintf(mStdErr, "Live sloppy objects: %zu/%zu (%zu%%)\n",
+ num_sloppy_objects, num_objects,
+ percent(num_sloppy_objects, num_objects));
+ FdPrintf(mStdErr, "Live sloppy bytes: %zuKiB/%zuKiB (%zu%%)\n",
+ total_slop / 1024, total_allocated / 1024,
+ percent(total_slop, total_allocated));
+
+ FdPrintf(mStdErr, "\n%8s %11s %10s %8s %9s %9s %8s\n", "bin-size",
+ "unused (c)", "total (c)", "used (c)", "non-full (r)", "total (r)",
+ "used (r)");
+ for (auto& bin : bin_stats) {
+ if (bin.size) {
+ FdPrintf(mStdErr, "%8zu %8zuKiB %7zuKiB %7zu%% %12zu %9zu %7zu%%\n",
+ bin.size, bin.bytes_unused / 1024, bin.bytes_total / 1024,
+ percent(bin.bytes_total - bin.bytes_unused, bin.bytes_total),
+ bin.num_non_full_runs, bin.num_runs,
+ percent(bin.num_runs - bin.num_non_full_runs, bin.num_runs));
+ }
+ }
+
+ FdPrintf(mStdErr, "\n%5s %8s %9s %7s\n", "bin", "slop", "used", "percent");
+ for (unsigned i = 0; i < JEMALLOC_MAX_STATS_BINS; i++) {
+ auto& bin = bin_stats[i];
+ if (bin.size) {
+ size_t used = bin.bytes_total - bin.bytes_unused;
+ FdPrintf(mStdErr, "%5zu %8zu %9zu %6zu%%\n", bin.size, bin_slop[i],
+ used, percent(bin_slop[i], used));
+ }
+ }
+ FdPrintf(mStdErr, "%5s %8zu %9zu %6zu%%\n", "large", large_slop, large_used,
+ percent(large_slop, large_used));
+ FdPrintf(mStdErr, "%5s %8zu %9zu %6zu%%\n", "huge", huge_slop, huge_used,
+ percent(huge_slop, huge_used));
+
+ unsigned last_size = 0;
+ for (auto& bin : bin_stats) {
+ if (bin.size == 0) {
+ break;
+ }
+
+ if (bin.size <= 16) {
+ // 1 byte buckets.
+ print_distribution(bin.size, last_size, 1);
+ } else if (bin.size <= stats.quantum_max) {
+ // 4 buckets, (4 bytes per bucket with a 16 byte quantum).
+ print_distribution(bin.size, last_size, stats.quantum / 4);
+ } else {
+ // 16 buckets.
+ print_distribution(bin.size, last_size, (bin.size - last_size) / 16);
+ }
+
+ last_size = bin.size;
+ }
+
+ // 16 buckets.
+ print_distribution(stats.page_size, last_size,
+ (stats.page_size - last_size) / 16);
+
+ // Buckets are 1/4 of the page size (12 buckets).
+ print_distribution(stats.page_size * 4, stats.page_size,
+ stats.page_size / 4);
+
+ /* TODO: Add more data, like actual RSS as measured by OS, but compensated
+ * for the replay internal data. */
+ }
+
+ private:
+ const size_t MAX_NUM_BUCKETS = 16;
+
+ /*
+ * Create and print frequency distributions of memory requests.
+ */
+ void print_distribution(size_t size, size_t next_smallest,
+ size_t bucket_size) {
+ unsigned shift = mozilla::CeilingLog2(bucket_size);
+
+ // The number of slots.
+ const unsigned array_slots = (size - next_smallest) >> shift;
+
+ // The translation to turn a slot index into a memory request size.
+ const unsigned array_offset = 1 + next_smallest;
+ const size_t array_offset_add = (1 << shift) + next_smallest;
+
+ // Avoid a variable length array.
+ MOZ_RELEASE_ASSERT(array_slots <= MAX_NUM_BUCKETS);
+ size_t requests[MAX_NUM_BUCKETS];
+ memset(requests, 0, sizeof(size_t) * array_slots);
+ size_t total_requests = 0;
+
+ for (size_t slot_id = 0; slot_id < mNumUsedSlots; slot_id++) {
+ MemSlot& slot = mSlots[slot_id];
+ if (slot.mPtr && slot.mRequest > next_smallest && slot.mRequest <= size) {
+ requests[(slot.mRequest - array_offset) >> shift]++;
+ total_requests++;
+ }
+ }
+
+ FdPrintf(mStdErr, "\n%zu-bin Distribution:\n", size);
+ FdPrintf(mStdErr, " request : count percent\n");
+ size_t range_start = next_smallest + 1;
+ for (size_t j = 0; j < array_slots; j++) {
+ size_t range_end = (j << shift) + array_offset_add;
+ FdPrintf(mStdErr, "%5zu - %5zu: %6zu %6zu%%\n", range_start, range_end,
+ requests[j], percent(requests[j], total_requests));
+ range_start = range_end + 1;
+ }
+ }
+
+ static size_t percent(size_t a, size_t b) {
+ if (!b) {
+ return 0;
+ }
+ return size_t(round(double(a) / double(b) * 100.0));
+ }
+
+ MemSlot& SlotForResult(Buffer& aResult) {
+ /* Parse result value and get the corresponding slot. */
+ Buffer dummy = aResult.SplitChar('=');
+ Buffer dummy2 = aResult.SplitChar('#');
+ if (dummy || dummy2) {
+ die("Malformed input");
+ }
+
+ size_t slot_id = parseNumber(aResult);
+ mNumUsedSlots = std::max(mNumUsedSlots, slot_id + 1);
+
+ return mSlots[slot_id];
+ }
+
+ intptr_t mStdErr;
+ size_t mOps;
+
+ // The number of slots that have been used. It is used to iterate over slots
+ // without accessing those we haven't initialised.
+ size_t mNumUsedSlots;
+
+ MemSlotList mSlots;
+ size_t mTotalRequestedSize;
+ size_t mTotalAllocatedSize;
+ // Whether to calculate slop for all allocations over the runtime of a
+ // process.
+ bool mCalculateSlop;
+};
+
+int main(int argc, const char* argv[]) {
+ size_t first_pid = 0;
+ FdReader reader(0);
+ Replay replay;
+
+ for (int i = 1; i < argc; i++) {
+ const char* option = argv[i];
+ if (strcmp(option, "-s") == 0) {
+ replay.enableSlopCalculation();
+ } else {
+ fprintf(stderr, "Unknown command line option: %s\n", option);
+ return EXIT_FAILURE;
+ }
+ }
+
+ /* Read log from stdin and dispatch function calls to the Replay instance.
+ * The log format is essentially:
+ * <pid> <tid> <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(')');
+
+ if (func == Buffer("jemalloc_stats")) {
+ replay.jemalloc_stats(args, line);
+ } else if (func == Buffer("free")) {
+ replay.free(args, line);
+ } else if (func == Buffer("malloc")) {
+ replay.malloc(args, line);
+ } else if (func == Buffer("posix_memalign")) {
+ replay.posix_memalign(args, line);
+ } else if (func == Buffer("aligned_alloc")) {
+ replay.aligned_alloc(args, line);
+ } else if (func == Buffer("calloc")) {
+ replay.calloc(args, line);
+ } else if (func == Buffer("realloc")) {
+ replay.realloc(args, line);
+ } else if (func == Buffer("memalign")) {
+ replay.memalign(args, line);
+ } else if (func == Buffer("valloc")) {
+ replay.valloc(args, line);
+ } else {
+ die("Malformed input");
+ }
+ }
+
+ return 0;
+}
diff --git a/memory/replace/logalloc/replay/expected_output_minimal.log b/memory/replace/logalloc/replay/expected_output_minimal.log
new file mode 100644
index 0000000000..332fe20957
--- /dev/null
+++ b/memory/replace/logalloc/replay/expected_output_minimal.log
@@ -0,0 +1,17 @@
+1 1 jemalloc_stats()
+1 1 malloc(42)=#1
+1 1 malloc(24)=#2
+1 1 free(#1)
+1 1 memalign(4096,1024)=#1
+1 1 calloc(4,42)=#3
+1 1 free(#2)
+1 1 realloc(#3,84)=#2
+1 1 memalign(256,1024)=#3
+1 1 memalign(512,1024)=#4
+1 1 memalign(4096,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)
diff --git a/memory/replace/logalloc/replay/logalloc_munge.py b/memory/replace/logalloc/replay/logalloc_munge.py
new file mode 100644
index 0000000000..bea397d505
--- /dev/null
+++ b/memory/replace/logalloc/replay/logalloc_munge.py
@@ -0,0 +1,151 @@
+# 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 absolute_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 Exception:
+ 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 0000000000..3e0c8c395a
--- /dev/null
+++ b/memory/replace/logalloc/replay/moz.build
@@ -0,0 +1,69 @@
+# -*- 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 += [
+ "/mfbt/Assertions.cpp",
+ "/mfbt/Poison.cpp",
+ "/mfbt/RandomNum.cpp",
+ "/mfbt/TaggedAnonymousMemory.cpp",
+ "/mfbt/Unused.cpp",
+ "Replay.cpp",
+]
+
+if CONFIG["OS_TARGET"] == "Darwin":
+ # Work around "warning: 'aligned_alloc' is only available on macOS 10.15 or newer"
+ # when building with MACOSX_DEPLOYMENT_TARGET < 10.15 with >= 10.15 SDK.
+ # We have our own definition of the function, so it doesn't matter what the SDK says.
+ SOURCES["Replay.cpp"].flags += ["-Wno-unguarded-availability-new"]
+
+if CONFIG["MOZ_REPLACE_MALLOC_STATIC"] and (CONFIG["MOZ_DMD"] or CONFIG["MOZ_PHC"]):
+ UNIFIED_SOURCES += [
+ "/mfbt/HashFunctions.cpp",
+ "/mfbt/JSONWriter.cpp",
+ "/mozglue/misc/StackWalk.cpp",
+ ]
+ if CONFIG["OS_ARCH"] == "WINNT":
+ OS_LIBS += [
+ "dbghelp",
+ ]
+ if CONFIG["MOZ_LINKER"] and CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
+ LOCAL_INCLUDES += [
+ "/mozglue/linker",
+ ]
+ DEFINES["__wrap_dladdr"] = "dladdr"
+
+
+if CONFIG["MOZ_DMD"] or CONFIG["MOZ_PHC"]:
+ if CONFIG["MOZ_BUILD_APP"] == "memory":
+ EXPORTS.mozilla += [
+ "/mozglue/misc/StackWalk.h",
+ ]
+
+if not CONFIG["MOZ_REPLACE_MALLOC_STATIC"]:
+ SOURCES += [
+ "../FdPrintf.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
+
+if CONFIG["MOZ_NEEDS_LIBATOMIC"]:
+ OS_LIBS += ["atomic"]
+
+DisableStlWrapping()
+
+include("/mozglue/build/replace_malloc.mozbuild")
diff --git a/memory/replace/logalloc/replay/replay.log b/memory/replace/logalloc/replay/replay.log
new file mode 100644
index 0000000000..f1e6de788b
--- /dev/null
+++ b/memory/replace/logalloc/replay/replay.log
@@ -0,0 +1,18 @@
+1 1 jemalloc_stats()
+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(256,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)