diff --git a/.gitignore b/.gitignore index 0dea6af5..8d9bea4a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +.DS_Store + ## User settings xcuserdata/ diff --git a/Benchmarks/Package.resolved b/Benchmarks/Package.resolved index b48e8308..b7658529 100644 --- a/Benchmarks/Package.resolved +++ b/Benchmarks/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "f1d359a544b71b52c6788ad2e4cd2952f7f166b62ddb07316768f66be7ba4099", "pins" : [ { "identity" : "hdrhistogram-swift", @@ -18,15 +19,6 @@ "version" : "1.0.2" } }, - { - "identity" : "package-jemalloc", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ordo-one/package-jemalloc", - "state" : { - "revision" : "e8a5db026963f5bfeac842d9d3f2cc8cde323b49", - "version" : "1.0.0" - } - }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", @@ -41,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics", "state" : { - "revision" : "cd142fd2f64be2100422d658e7411e39489da985", - "version" : "1.2.0" + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" } }, { @@ -73,5 +65,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/LocalPackages/MallocInterposerC/.gitignore b/LocalPackages/MallocInterposerC/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/LocalPackages/MallocInterposerC/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/LocalPackages/MallocInterposerC/Package.swift b/LocalPackages/MallocInterposerC/Package.swift new file mode 100644 index 00000000..6dba7002 --- /dev/null +++ b/LocalPackages/MallocInterposerC/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MallocInterposer", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "MallocInterposerC", + type: .dynamic, + targets: ["MallocInterposerC"]) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "MallocInterposerC", + linkerSettings: [ + .linkedLibrary("dl") + ]) + ] +) diff --git a/LocalPackages/MallocInterposerC/Sources/MallocInterposerC/include/interposer.h b/LocalPackages/MallocInterposerC/Sources/MallocInterposerC/include/interposer.h new file mode 100644 index 00000000..f4d4118f --- /dev/null +++ b/LocalPackages/MallocInterposerC/Sources/MallocInterposerC/include/interposer.h @@ -0,0 +1,99 @@ +// +// Copyright (c) 2022 Ordo One AB. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef INTERPOSER_H +#define INTERPOSER_H + +#include +#include +#include +#if __APPLE__ +# include +#endif + +// Hook function types +typedef void (*malloc_hook_t)(size_t size); +typedef void (*free_hook_t)(void* ptr); +typedef void (*calloc_hook_t)(size_t nmemb, size_t size); +typedef void (*realloc_hook_t)(void* ptr, size_t size); +typedef void (*valloc_hook_t)(size_t size); +typedef void (*posix_memalign_hook_t)(void **memptr, size_t alignment, size_t size); + +#if __APPLE__ +typedef void (*malloc_zone_hook_t)(malloc_zone_t *zone, size_t size); +typedef void (*malloc_zone_calloc_hook_t)(malloc_zone_t *zone, size_t num_items, size_t size); +typedef void (*malloc_zone_realloc_hook_t)(malloc_zone_t *zone, void *ptr, size_t size); +typedef void (*malloc_zone_memalign_hook_t)(malloc_zone_t *zone, size_t alignment, size_t size); +typedef void (*malloc_zone_valloc_hook_t)(malloc_zone_t *zone, size_t size); +typedef void (*malloc_zone_free_hook_t)(malloc_zone_t *zone, void *ptr); +#endif + +// Hook management functions +void set_malloc_hook(malloc_hook_t hook); +void set_free_hook(free_hook_t hook); +void set_calloc_hook(calloc_hook_t hook); +void set_realloc_hook(realloc_hook_t hook); +void set_posix_memalign_hook(posix_memalign_hook_t hook); + +#if __APPLE__ +void set_malloc_zone_hook(malloc_zone_hook_t hook); +void set_malloc_zone_calloc_hook(malloc_zone_calloc_hook_t hook); +void set_malloc_zone_realloc_hook(malloc_zone_realloc_hook_t hook); +void set_malloc_zone_memalign_hook(malloc_zone_memalign_hook_t hook); +void set_malloc_zone_valloc_hook(malloc_zone_valloc_hook_t hook); +void set_malloc_zone_free_hook(malloc_zone_free_hook_t hook); +#endif + +void clear_malloc_hook(void); +void clear_free_hook(void); +void clear_calloc_hook(void); +void clear_realloc_hook(void); + +#if __APPLE__ +void clear_malloc_zone_hook(void); +void clear_malloc_zone_calloc_hook(void); +void clear_malloc_zone_realloc_hook(void); +void clear_malloc_zone_memalign_hook(void); +void clear_malloc_zone_valloc_hook(void); +void clear_malloc_zone_free_hook(void); +#endif + +// Replacement functions +void *replacement_malloc(size_t size); +void replacement_free(void *ptr); +void *replacement_calloc(size_t nmemb, size_t size); +void *replacement_realloc(void *ptr, size_t size); +void *replacement_reallocf(void *ptr, size_t size); +void *replacement_valloc(size_t size); +int replacement_posix_memalign(void **memptr, size_t alignment, size_t size); + +// On Linux we use LD_PRELOAD to interpose the standard malloc functions +// and we have to declare them ourselves +#if !__APPLE__ +void free(void *ptr); +void *malloc(size_t size); +void *calloc(size_t nmemb, size_t size); +void *realloc(void *ptr, size_t size); +void *reallocf(void *ptr, size_t size); +void *valloc(size_t size); +int posix_memalign(void **memptr, size_t alignment, size_t size); +#endif + + +#if __APPLE__ +void *replacement_malloc_zone_malloc(malloc_zone_t *zone, size_t size); +void *replacement_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size); +void *replacement_malloc_zone_valloc(malloc_zone_t *zone, size_t size); +void *replacement_malloc_zone_realloc(malloc_zone_t *zone, void *ptr, size_t size); +void *replacement_malloc_zone_memalign(malloc_zone_t *zone, size_t alignment, size_t size); +void replacement_malloc_zone_free(malloc_zone_t *zone, void *ptr); +#endif + +#endif diff --git a/LocalPackages/MallocInterposerC/Sources/MallocInterposerC/src/interposer-darwin.c b/LocalPackages/MallocInterposerC/Sources/MallocInterposerC/src/interposer-darwin.c new file mode 100644 index 00000000..03ea9a14 --- /dev/null +++ b/LocalPackages/MallocInterposerC/Sources/MallocInterposerC/src/interposer-darwin.c @@ -0,0 +1,276 @@ +// +// Copyright (c) 2022 Ordo One AB. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#include +#if __APPLE__ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Global hooks +static malloc_hook_t g_malloc_hook = NULL; +static free_hook_t g_free_hook = NULL; +static calloc_hook_t g_calloc_hook = NULL; +static realloc_hook_t g_realloc_hook = NULL; +static valloc_hook_t g_valloc_hook = NULL; +static posix_memalign_hook_t g_posix_memalign_hook = NULL; +static malloc_zone_hook_t g_malloc_zone_hook = NULL; +static malloc_zone_realloc_hook_t g_malloc_zone_realloc_hook = NULL; +static malloc_zone_calloc_hook_t g_malloc_zone_calloc_hook = NULL; +static malloc_zone_valloc_hook_t g_malloc_zone_valloc_hook = NULL; +static malloc_zone_memalign_hook_t g_malloc_zone_memalign_hook = NULL; +static malloc_zone_free_hook_t g_malloc_zone_free_hook = NULL; + +// Statistics +static pthread_mutex_t hook_mutex = PTHREAD_MUTEX_INITIALIZER; + +#define DYLD_INTERPOSE(_replacement,_replacee) \ + __attribute__((used)) static struct { const void *replacement; const void *replacee; } _interpose_##_replacee \ + __attribute__ ((section("__DATA,__interpose"))) = { (const void *)(unsigned long)&_replacement, (const void *)(unsigned long)&_replacee }; + +/* on Darwin calling the original function is super easy, just call it, done. */ +#define JUMP_INTO_LIBC_FUN(_fun, ...) /* \ +*/ do { /* \ +*/ return _fun(__VA_ARGS__); /* \ +*/ } while(0) + +// Hook management functions +void set_malloc_hook(malloc_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_malloc_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_free_hook(free_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_free_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_calloc_hook(calloc_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_calloc_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_realloc_hook(realloc_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_realloc_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_valloc_hook(valloc_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_valloc_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_posix_memalign_hook(posix_memalign_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_posix_memalign_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_malloc_zone_hook(malloc_zone_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_malloc_zone_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_malloc_zone_realloc_hook(malloc_zone_realloc_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_malloc_zone_realloc_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_malloc_zone_calloc_hook(malloc_zone_calloc_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_malloc_zone_calloc_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_malloc_zone_valloc_hook(malloc_zone_valloc_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_malloc_zone_valloc_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_malloc_zone_memalign_hook(malloc_zone_memalign_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_malloc_zone_memalign_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_malloc_zone_free_hook(malloc_zone_free_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_malloc_zone_free_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +// Clear hooks +void clear_malloc_hook(void) { set_malloc_hook(NULL); } +void clear_free_hook(void) { set_free_hook(NULL); } +void clear_calloc_hook(void) { set_calloc_hook(NULL); } +void clear_realloc_hook(void) { set_realloc_hook(NULL); } +void clear_valloc_hook(void) { set_valloc_hook(NULL); } +void clear_posix_memalign_hook(void) { set_posix_memalign_hook(NULL); } +void clear_malloc_zone_hook(void) { set_malloc_zone_hook(NULL); } +void clear_malloc_zone_realloc_hook(void) { set_malloc_zone_realloc_hook(NULL); } +void clear_malloc_zone_calloc_hook(void) { set_malloc_zone_calloc_hook(NULL); } +void clear_malloc_zone_valloc_hook(void) { set_malloc_zone_valloc_hook(NULL); } +void clear_malloc_zone_memalign_hook(void) { set_malloc_zone_memalign_hook(NULL); } +void clear_malloc_zone_free_hook(void) { set_malloc_zone_free_hook(NULL); } + +// Replacement functions +void replacement_free(void *ptr) { + + // Call hook if set + if (g_free_hook) { + g_free_hook(ptr); + } + + JUMP_INTO_LIBC_FUN(free, ptr); +} + +void *replacement_malloc(size_t size) { + + // Call hook if set + if (g_malloc_hook) { + g_malloc_hook(size); + } + + JUMP_INTO_LIBC_FUN(malloc, size); +} + +void *replacement_realloc(void *ptr, size_t size) { + if (g_realloc_hook) { + g_realloc_hook(ptr, size); + } + + JUMP_INTO_LIBC_FUN(realloc, ptr, size); +} + +void *replacement_calloc(size_t count, size_t size) { + if (g_calloc_hook) { + g_calloc_hook(count, size); + } + + JUMP_INTO_LIBC_FUN(calloc, count, size); +} + +void *replacement_malloc_zone_malloc(malloc_zone_t *zone, size_t size) { + if (g_malloc_zone_hook) { + g_malloc_zone_hook(zone, size); + } + + JUMP_INTO_LIBC_FUN(malloc_zone_malloc, zone, size); +} + +void *replacement_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size) { + if (g_malloc_zone_calloc_hook) { + g_malloc_zone_calloc_hook(zone, num_items, size); + } + + JUMP_INTO_LIBC_FUN(malloc_zone_calloc, zone, num_items, size); +} + +void *replacement_malloc_zone_valloc(malloc_zone_t *zone, size_t size) { + if (g_malloc_zone_valloc_hook) { + g_malloc_zone_valloc_hook(zone, size); + } + + JUMP_INTO_LIBC_FUN(malloc_zone_valloc, zone, size); +} + +void *replacement_malloc_zone_realloc(malloc_zone_t *zone, void *ptr, size_t size) { + if (0 == size) { + replacement_free(ptr); + return NULL; + } + if (!ptr) { + return replacement_malloc(size); + } + + if (g_malloc_zone_realloc_hook) { + g_malloc_zone_realloc_hook(zone, ptr, size); + } + + JUMP_INTO_LIBC_FUN(realloc, ptr, size); +} + +void *replacement_malloc_zone_memalign(malloc_zone_t *zone, size_t alignment, size_t size) { + if (g_malloc_zone_memalign_hook) { + g_malloc_zone_memalign_hook(zone, alignment, size); + } + + JUMP_INTO_LIBC_FUN(malloc_zone_memalign, zone, alignment, size); +} + +void replacement_malloc_zone_free(malloc_zone_t *zone, void *ptr) { + if (g_malloc_zone_free_hook) { + g_malloc_zone_free_hook(zone, ptr); + } + + JUMP_INTO_LIBC_FUN(malloc_zone_free, zone, ptr); +} + +void *replacement_reallocf(void *ptr, size_t size) { + void *new_ptr = replacement_realloc(ptr, size); + if (!new_ptr) { + replacement_free(new_ptr); + } + return new_ptr; +} + +void *replacement_valloc(size_t size) { + if (g_valloc_hook) { + g_valloc_hook(size); + } + + JUMP_INTO_LIBC_FUN(valloc, size); +} + +int replacement_posix_memalign(void **memptr, size_t alignment, size_t size) { + if (g_posix_memalign_hook) { + g_posix_memalign_hook(memptr, alignment, size); + } + + JUMP_INTO_LIBC_FUN(posix_memalign, memptr, alignment, size); +} + +DYLD_INTERPOSE(replacement_free, free) +DYLD_INTERPOSE(replacement_malloc, malloc) +DYLD_INTERPOSE(replacement_realloc, realloc) +DYLD_INTERPOSE(replacement_calloc, calloc) +DYLD_INTERPOSE(replacement_reallocf, reallocf) +DYLD_INTERPOSE(replacement_valloc, valloc) +DYLD_INTERPOSE(replacement_posix_memalign, posix_memalign) +DYLD_INTERPOSE(replacement_malloc_zone_malloc, malloc_zone_malloc) +DYLD_INTERPOSE(replacement_malloc_zone_calloc, malloc_zone_calloc) +DYLD_INTERPOSE(replacement_malloc_zone_valloc, malloc_zone_valloc) +DYLD_INTERPOSE(replacement_malloc_zone_realloc, malloc_zone_realloc) +DYLD_INTERPOSE(replacement_malloc_zone_memalign, malloc_zone_memalign) +DYLD_INTERPOSE(replacement_malloc_zone_free, malloc_zone_free) +#endif diff --git a/LocalPackages/MallocInterposerC/Sources/MallocInterposerC/src/interposer-unix.c b/LocalPackages/MallocInterposerC/Sources/MallocInterposerC/src/interposer-unix.c new file mode 100644 index 00000000..6e17ae55 --- /dev/null +++ b/LocalPackages/MallocInterposerC/Sources/MallocInterposerC/src/interposer-unix.c @@ -0,0 +1,322 @@ +// +// Copyright (c) 2022 Ordo One AB. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#ifndef __APPLE__ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/* a big block of memory that we'll use for recursive mallocs */ +static char g_recursive_malloc_mem[10 * 1024 * 1024] = {0}; +/* the index of the first free byte */ +static _Atomic ptrdiff_t g_recursive_malloc_next_free_ptr = ATOMIC_VAR_INIT(0); + +#define LIBC_SYMBOL(_fun) "" # _fun + +/* Some thread-local flags we use to check if we're recursively in a hooked function. */ +static __thread bool g_in_malloc = false; +static __thread bool g_in_realloc = false; +static __thread bool g_in_free = false; +static __thread bool g_in_socket = false; +static __thread bool g_in_accept = false; +static __thread bool g_in_accept4 = false; +static __thread bool g_in_close = false; + +/* The types of the variables holding the libc function pointers. */ +typedef void *(*type_libc_malloc)(size_t); +typedef void *(*type_libc_realloc)(void *, size_t); +typedef void (*type_libc_free)(void *); +typedef int (*type_libc_socket)(int, int, int); +typedef int (*type_libc_accept)(int, struct sockaddr*, socklen_t *); +typedef int (*type_libc_accept4)(int, struct sockaddr *, socklen_t *, int); +typedef int (*type_libc_close)(int); + +/* The (atomic) globals holding the pointer to the original libc implementation. */ +_Atomic type_libc_malloc g_libc_malloc; +_Atomic type_libc_realloc g_libc_realloc; +_Atomic type_libc_free g_libc_free; +_Atomic type_libc_socket g_libc_socket; +_Atomic type_libc_accept g_libc_accept; +_Atomic type_libc_accept4 g_libc_accept4; +_Atomic type_libc_close g_libc_close; + +// Global hooks +static malloc_hook_t g_malloc_hook = NULL; +static free_hook_t g_free_hook = NULL; +static calloc_hook_t g_calloc_hook = NULL; +static realloc_hook_t g_realloc_hook = NULL; +static valloc_hook_t g_valloc_hook = NULL; +static posix_memalign_hook_t g_posix_memalign_hook = NULL; + +// Statistics +static pthread_mutex_t hook_mutex = PTHREAD_MUTEX_INITIALIZER; + +// Hook management functions +void set_malloc_hook(malloc_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_malloc_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_free_hook(free_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_free_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_calloc_hook(calloc_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_calloc_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_realloc_hook(realloc_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_realloc_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_valloc_hook(valloc_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_valloc_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +void set_posix_memalign_hook(posix_memalign_hook_t hook) { + pthread_mutex_lock(&hook_mutex); + g_posix_memalign_hook = hook; + pthread_mutex_unlock(&hook_mutex); +} + +// Clear hooks +void clear_malloc_hook(void) { set_malloc_hook(NULL); } +void clear_free_hook(void) { set_free_hook(NULL); } +void clear_calloc_hook(void) { set_calloc_hook(NULL); } +void clear_realloc_hook(void) { set_realloc_hook(NULL); } +void clear_valloc_hook(void) { set_valloc_hook(NULL); } +void clear_posix_memalign_hook(void) { set_posix_memalign_hook(NULL); } + +// this is called if malloc is called whilst trying to resolve libc's realloc. +// we just vend out pointers to a large block in the BSS (which we never free). +// This block should be large enough because it's only used when malloc is +// called from dlsym which should only happen once per thread. +static void *recursive_malloc(size_t size_in) { + size_t size = size_in; + if ((size & 0xf) != 0) { + // make size 16 byte aligned + size = (size + 0xf) & (~(size_t)0xf); + } + + ptrdiff_t next = atomic_fetch_add_explicit(&g_recursive_malloc_next_free_ptr, + size, + memory_order_relaxed); + if ((size_t)next >= sizeof(g_recursive_malloc_mem)) { + // we ran out of memory + return NULL; + } + return (void *)((intptr_t)g_recursive_malloc_mem + next); +} + +static bool is_recursive_malloc_block(void *ptr) { + uintptr_t block_begin = (uintptr_t)g_recursive_malloc_mem; + uintptr_t block_end = block_begin + sizeof(g_recursive_malloc_mem); + uintptr_t user_ptr = (uintptr_t)ptr; + + return user_ptr >= block_begin && user_ptr < block_end; +} + +// this is called if realloc is called whilst trying to resolve libc's realloc. +static void *recursive_realloc(void *ptr, size_t size) { + // not implemented yet... + abort(); +} + +// this is called if free is called whilst trying to resolve libc's free. +static void recursive_free(void *ptr) { + // not implemented yet... + abort(); +} + +// this is called if socket is called whilst trying to resolve libc's socket. +static int recursive_socket(int domain, int type, int protocol) { + // not possible + abort(); +} + +// this is called if accept is called whilst trying to resolve libc's accept. +static int recursive_accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len) { + // not possible + abort(); +} + +// this is called if accept4 is called whilst trying to resolve libc's accept4. +static int recursive_accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags) { + // not possible + abort(); +} + +// this is called if close is called whilst trying to resolve libc's close. +static int recursive_close(int fildes) { + // not possible + abort(); +} + +/* On Apple platforms getting to the original libc function from a hooked + * function is easy. On other UNIX systems this is slightly harder because we + * have to look up the function with the dynamic linker. Because that isn't + * super performant we cache the lookup result in an (atomic) global. + * + * Calling into the libc function if we have already cached it is easy, we + * (atomically) load it and call into it. If have not yet cached it, we need to + * resolve it which we do by using dlsym and then write it into the (atomic) + * global. There's only one slight problem: dlsym might call back into the + * function we're just trying to resolve (dlsym does call malloc). In that case + * we need to emulate that function (named recursive_*). But that's all then. + */ +#define JUMP_INTO_LIBC_FUN(_fun, ...) /* \ +*/ do { /* \ +*/ /* Let's see if somebody else already resolved that function for us */ /* \ +*/ type_libc_ ## _fun local_fun = atomic_load(&g_libc_ ## _fun); /* \ +*/ if (!local_fun) { /* \ +*/ /* No, we're the first ones to use this function. */ /* \ +*/ if (!g_in_ ## _fun) { /* \ +*/ g_in_ ## _fun = true; /* \ +*/ /* If we're here, we're at least not recursively in ourselves. */ /* \ +*/ /* That means we can use dlsym to resolve the libc function. */ /* \ +*/ type_libc_ ## _fun desired = dlsym(RTLD_NEXT, LIBC_SYMBOL(_fun)); /* \ +*/ if (atomic_compare_exchange_strong(&g_libc_ ## _fun, &local_fun, desired)) { /* \ +*/ /* If we're here, we won the race, so let's use our resolved function. */ /* \ +*/ local_fun = desired; /* \ +*/ } else { /* \ +*/ /* Lost the race, let's load the global again */ /* \ +*/ local_fun = atomic_load(&g_libc_ ## _fun); /* \ +*/ } /* \ +*/ } else { /* \ +*/ /* Okay, we can't jump into libc here and need to use our own version. */ /* \ +*/ return recursive_ ## _fun (__VA_ARGS__); /* \ +*/ } /* \ +*/ } /* \ +*/ return local_fun(__VA_ARGS__); /* \ +*/ } while(0) + +void replacement_free(void *ptr) { + if (ptr) { + + if (g_free_hook) { + g_free_hook(ptr); + } + + if (!is_recursive_malloc_block(ptr)) { + JUMP_INTO_LIBC_FUN(free, ptr); + } + } +} + +void *replacement_malloc(size_t size) { + if (g_malloc_hook) { + g_malloc_hook(size); + } + + JUMP_INTO_LIBC_FUN(malloc, size); +} + +void *replacement_realloc(void *ptr, size_t size) { + if (0 == size) { + replacement_free(ptr); + return NULL; + } + if (!ptr) { + return replacement_malloc(size); + } + + if (g_realloc_hook) { + g_realloc_hook(ptr, size); + } + + JUMP_INTO_LIBC_FUN(realloc, ptr, size); +} + +void *replacement_calloc(size_t count, size_t size) { + void *ptr = replacement_malloc(count * size); + memset(ptr, 0, count * size); + + if (g_calloc_hook) { + g_calloc_hook(count, size); + } + + return ptr; +} + +void *replacement_reallocf(void *ptr, size_t size) { + void *new_ptr = replacement_realloc(ptr, size); + if (!new_ptr) { + replacement_free(new_ptr); + } + return new_ptr; +} + +void *replacement_valloc(size_t size) { + if (g_valloc_hook) { + g_valloc_hook(size); + } + // not aligning correctly (should be PAGE_SIZE) but good enough + return replacement_malloc(size); +} + +int replacement_posix_memalign(void **memptr, size_t alignment, size_t size) { + if (g_posix_memalign_hook) { + g_posix_memalign_hook(memptr, alignment, size); + } + + // not aligning correctly (should be `alignment`) but good enough + void *ptr = replacement_malloc(size); + if (ptr && memptr) { + *memptr = ptr; + return 0; + } else { + return 1; + } +} + +void free(void *ptr) { + replacement_free(ptr); +} +void *malloc(size_t size) { + return replacement_malloc(size); +} +void *calloc(size_t nmemb, size_t size) { + return replacement_calloc(nmemb, size); +} +void *realloc(void *ptr, size_t size) { + return replacement_realloc(ptr, size); +} +void *reallocf(void *ptr, size_t size) { + return replacement_reallocf(ptr, size); +} +void *valloc(size_t size) { + return replacement_valloc(size); +} +int posix_memalign(void **memptr, size_t alignment, size_t size) { + return replacement_posix_memalign(memptr, alignment, size); +} +#endif diff --git a/LocalPackages/MallocInterposerSwift/Package.resolved b/LocalPackages/MallocInterposerSwift/Package.resolved new file mode 100644 index 00000000..30a41cda --- /dev/null +++ b/LocalPackages/MallocInterposerSwift/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "f9d52b4684b4f378f6711fa01082569f9206a98fc7e9e15cb2fc72bbeafb9737", + "pins" : [ + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" + } + } + ], + "version" : 3 +} diff --git a/LocalPackages/MallocInterposerSwift/Package.swift b/LocalPackages/MallocInterposerSwift/Package.swift new file mode 100644 index 00000000..bc7edc3d --- /dev/null +++ b/LocalPackages/MallocInterposerSwift/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MallocInterposerSwift", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "MallocInterposerSwift", + type: .dynamic, + targets: ["MallocInterposerSwift"]) + ], + dependencies: [ + .package(path: "../MallocInterposerC"), + .package(url: "https://github.com/apple/swift-atomics.git", from: "1.3.0"), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "MallocInterposerSwift", + dependencies: [ + .product(name: "MallocInterposerC", package: "MallocInterposerC"), + .product(name: "Atomics", package: "swift-atomics"), + ]), + .executableTarget( + name: "SwiftTestClient", + dependencies: ["MallocInterposerSwift"] + ), + ] +) diff --git a/LocalPackages/MallocInterposerSwift/Sources/MallocInterposerSwift/MallocInterposerSwift.swift b/LocalPackages/MallocInterposerSwift/Sources/MallocInterposerSwift/MallocInterposerSwift.swift new file mode 100644 index 00000000..14bcb56a --- /dev/null +++ b/LocalPackages/MallocInterposerSwift/Sources/MallocInterposerSwift/MallocInterposerSwift.swift @@ -0,0 +1,257 @@ +// +// Copyright (c) 2022 Ordo One AB. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// + +import Atomics +import Foundation +import MallocInterposerC + +#if canImport(Glibc) +import Glibc +// We need to expose malloc_usable_size manually since it's not exposed through Glibc +@_silgen_name("malloc_usable_size") +func malloc_usable_size(_ ptr: UnsafeMutableRawPointer?) -> Int +#endif + +/// Swift-friendly hook types +public typealias MallocHook = @convention(c) (Int) -> Void +public typealias FreeHook = @convention(c) (UnsafeMutableRawPointer?) -> Void +public typealias CallocHook = @convention(c) (Int, Int) -> Void +public typealias ReallocHook = @convention(c) (UnsafeMutableRawPointer?, Int) -> Void +public typealias PosixMemalignHook = @convention(c) (UnsafeMutablePointer?, Int, Int) -> Void + +#if canImport(Darwin) +public typealias MallocZoneHook = @convention(c) (UnsafeMutablePointer?, Int) -> Void +public typealias MallocZoneFreeHook = @convention(c) (UnsafeMutablePointer?, UnsafeMutableRawPointer?) -> Void +public typealias MallocZoneCallocHook = @convention(c) (UnsafeMutablePointer?, Int, Int) -> Void +public typealias MallocZoneReallocHook = @convention(c) (UnsafeMutablePointer?, UnsafeMutableRawPointer?, Int) -> Void +public typealias MallocZoneVallocHook = @convention(c) (UnsafeMutablePointer?, Int) -> Void +public typealias MallocZoneMemalignHook = @convention(c) (UnsafeMutablePointer?, Int, Int) -> Void +#endif + +/// Main class for managing malloc interposition +public class MallocInterposerSwift: @unchecked Sendable { + /// We use `UnsafeAtomic` in order to avoid malloc calls during interposition + nonisolated(unsafe) private static var mallocCount: ManagedAtomic! + nonisolated(unsafe) private static var mallocBytesCount: ManagedAtomic! + nonisolated(unsafe) private static var freeCount: ManagedAtomic! + nonisolated(unsafe) private static var freeBytesCount: ManagedAtomic! + nonisolated(unsafe) private static var mallocSmallCount: ManagedAtomic! + nonisolated(unsafe) private static var mallocLargeCount: ManagedAtomic! + static let pageSize = getpagesize() + + private init() {} + + // Initialize the atomic counters before hooking + // because ManagedAtomic calls into malloc + public static func initialize() { + mallocCount = ManagedAtomic(0) + mallocBytesCount = ManagedAtomic(0) + freeCount = ManagedAtomic(0) + freeBytesCount = ManagedAtomic(0) + mallocSmallCount = ManagedAtomic(0) + mallocLargeCount = ManagedAtomic(0) + } + + public static func hook() { + + let mallocHook: MallocHook = { size in + MallocInterposerSwift.mallocCount.wrappingIncrement(ordering: .relaxed) + MallocInterposerSwift.mallocBytesCount.wrappingIncrement(by: size, ordering: .relaxed) + + if size > MallocInterposerSwift.pageSize { + MallocInterposerSwift.mallocLargeCount.wrappingIncrement(ordering: .relaxed) + } else { + MallocInterposerSwift.mallocSmallCount.wrappingIncrement(ordering: .relaxed) + } + } + + let freeHook: FreeHook = { pointer in + MallocInterposerSwift.freeCount.wrappingIncrement(ordering: .relaxed) + #if canImport(Darwin) + let size = malloc_size(pointer) + #else + let size = malloc_usable_size(pointer) + #endif + MallocInterposerSwift.freeBytesCount.wrappingIncrement(by: size, ordering: .relaxed) + } + + let callocHook: CallocHook = { num, size in + MallocInterposerSwift.mallocCount.wrappingIncrement(ordering: .relaxed) + let total = num * size + MallocInterposerSwift.mallocBytesCount.wrappingIncrement(by: total, ordering: .relaxed) + + if total > MallocInterposerSwift.pageSize { + MallocInterposerSwift.mallocLargeCount.wrappingIncrement(ordering: .relaxed) + } else { + MallocInterposerSwift.mallocSmallCount.wrappingIncrement(ordering: .relaxed) + } + } + + let reallocHook: ReallocHook = { pointer, size in + MallocInterposerSwift.freeCount.wrappingIncrement(ordering: .relaxed) + MallocInterposerSwift.mallocCount.wrappingIncrement(ordering: .relaxed) + MallocInterposerSwift.mallocBytesCount.wrappingIncrement(by: size, ordering: .relaxed) + + if size > MallocInterposerSwift.pageSize { + MallocInterposerSwift.mallocLargeCount.wrappingIncrement(ordering: .relaxed) + } else { + MallocInterposerSwift.mallocSmallCount.wrappingIncrement(ordering: .relaxed) + } + } + + let posixMemalignHook: PosixMemalignHook = { pointer, alignment, size in + MallocInterposerSwift.mallocCount.wrappingIncrement(ordering: .relaxed) + MallocInterposerSwift.mallocBytesCount.wrappingIncrement(by: size, ordering: .relaxed) + + if size > MallocInterposerSwift.pageSize { + MallocInterposerSwift.mallocLargeCount.wrappingIncrement(ordering: .relaxed) + } else { + MallocInterposerSwift.mallocSmallCount.wrappingIncrement(ordering: .relaxed) + } + } + + #if canImport(Darwin) + let mallocZoneHook: MallocZoneHook = { zone, size in + MallocInterposerSwift.mallocCount.wrappingIncrement(ordering: .relaxed) + MallocInterposerSwift.mallocBytesCount.wrappingIncrement(by: size, ordering: .relaxed) + + if size > MallocInterposerSwift.pageSize { + MallocInterposerSwift.mallocLargeCount.wrappingIncrement(ordering: .relaxed) + } else { + MallocInterposerSwift.mallocSmallCount.wrappingIncrement(ordering: .relaxed) + } + } + let mallocZoneFreeHook: MallocZoneFreeHook = { zone, pointer in + MallocInterposerSwift.freeCount.wrappingIncrement(ordering: .relaxed) + } + let mallocZoneCallocHook: MallocZoneCallocHook = { zone, num, size in + MallocInterposerSwift.mallocCount.wrappingIncrement(ordering: .relaxed) + let total = num * size + MallocInterposerSwift.mallocBytesCount.wrappingIncrement(by: total, ordering: .relaxed) + + if total > MallocInterposerSwift.pageSize { + MallocInterposerSwift.mallocLargeCount.wrappingIncrement(ordering: .relaxed) + } else { + MallocInterposerSwift.mallocSmallCount.wrappingIncrement(ordering: .relaxed) + } + } + let mallocZoneReallocHook: MallocZoneReallocHook = { zone, pointer, size in + MallocInterposerSwift.freeCount.wrappingIncrement(ordering: .relaxed) + MallocInterposerSwift.mallocCount.wrappingIncrement(ordering: .relaxed) + MallocInterposerSwift.mallocBytesCount.wrappingIncrement(by: size, ordering: .relaxed) + + if size > MallocInterposerSwift.pageSize { + MallocInterposerSwift.mallocLargeCount.wrappingIncrement(ordering: .relaxed) + } else { + MallocInterposerSwift.mallocSmallCount.wrappingIncrement(ordering: .relaxed) + } + } + let mallocZoneVallocHook: MallocZoneVallocHook = { zone, size in + MallocInterposerSwift.mallocCount.wrappingIncrement(ordering: .relaxed) + MallocInterposerSwift.mallocBytesCount.wrappingIncrement(by: size, ordering: .relaxed) + + if size > MallocInterposerSwift.pageSize { + MallocInterposerSwift.mallocLargeCount.wrappingIncrement(ordering: .relaxed) + } else { + MallocInterposerSwift.mallocSmallCount.wrappingIncrement(ordering: .relaxed) + } + } + let mallocZoneMemalignHook: MallocZoneMemalignHook = { zone, alignment, size in + MallocInterposerSwift.mallocCount.wrappingIncrement(ordering: .relaxed) + MallocInterposerSwift.mallocBytesCount.wrappingIncrement(by: size, ordering: .relaxed) + + if size > MallocInterposerSwift.pageSize { + MallocInterposerSwift.mallocLargeCount.wrappingIncrement(ordering: .relaxed) + } else { + MallocInterposerSwift.mallocSmallCount.wrappingIncrement(ordering: .relaxed) + } + } + + set_malloc_zone_hook(mallocZoneHook) + set_malloc_zone_free_hook(mallocZoneFreeHook) + set_malloc_zone_calloc_hook(mallocZoneCallocHook) + set_malloc_zone_realloc_hook(mallocZoneReallocHook) + set_malloc_zone_valloc_hook(mallocZoneVallocHook) + set_malloc_zone_memalign_hook(mallocZoneMemalignHook) + #endif + + set_malloc_hook(mallocHook) + set_free_hook(freeHook) + set_calloc_hook(callocHook) + set_realloc_hook(reallocHook) + set_posix_memalign_hook(posixMemalignHook) + } + + public static func unhook() { + set_malloc_hook(nil) + set_free_hook(nil) + set_calloc_hook(nil) + set_realloc_hook(nil) + set_posix_memalign_hook(nil) + + #if canImport(Darwin) + set_malloc_zone_hook(nil) + set_malloc_zone_free_hook(nil) + set_malloc_zone_calloc_hook(nil) + set_malloc_zone_realloc_hook(nil) + set_malloc_zone_valloc_hook(nil) + set_malloc_zone_memalign_hook(nil) + #endif + } + + public static func reset() { + mallocCount.store(0, ordering: .relaxed) + mallocBytesCount.store(0, ordering: .relaxed) + freeCount.store(0, ordering: .relaxed) + freeBytesCount.store(0, ordering: .relaxed) + mallocSmallCount.store(0, ordering: .relaxed) + mallocLargeCount.store(0, ordering: .relaxed) + } + + public static func getStatistics() -> Statistics { + let stats = Statistics( + mallocCount: mallocCount.load(ordering: .relaxed), + mallocBytesCount: mallocBytesCount.load(ordering: .relaxed), + mallocSmallCount: mallocSmallCount.load(ordering: .relaxed), + mallocLargeCount: mallocLargeCount.load(ordering: .relaxed), + freeCount: freeCount.load(ordering: .relaxed), + freeBytesCount: freeBytesCount.load(ordering: .relaxed) + ) + + return stats + } +} + +public extension MallocInterposerSwift { + struct Statistics { + public let mallocCount: Int + public let mallocBytesCount: Int + public let mallocSmallCount: Int + public let mallocLargeCount: Int + public let freeCount: Int + public let freeBytesCount: Int + + public init( + mallocCount: Int = 0, + mallocBytesCount: Int = 0, + mallocSmallCount: Int = 0, + mallocLargeCount: Int = 0, + freeCount: Int = 0, + freeBytesCount: Int = 0 + ) { + self.mallocCount = mallocCount + self.mallocBytesCount = mallocBytesCount + self.mallocSmallCount = mallocSmallCount + self.mallocLargeCount = mallocLargeCount + self.freeCount = freeCount + self.freeBytesCount = freeBytesCount + } + } +} diff --git a/LocalPackages/MallocInterposerSwift/Sources/SwiftTestClient/SwiftTestClient.swift b/LocalPackages/MallocInterposerSwift/Sources/SwiftTestClient/SwiftTestClient.swift new file mode 100644 index 00000000..c398d4d7 --- /dev/null +++ b/LocalPackages/MallocInterposerSwift/Sources/SwiftTestClient/SwiftTestClient.swift @@ -0,0 +1,66 @@ +// +// Copyright (c) 2022 Ordo One AB. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// + +import Foundation +import MallocInterposerC +import MallocInterposerSwift + +@main +enum TestClient { + + @_optimize(none) + static func blackHole(_ value: Any) { + + } + + static func performAllocations(count: Int, size: Int, shouldFree: Bool = true) { + var index = 0 + repeat { + let x = malloc(size) + if shouldFree { + free(x) + } + index += 1 + } while index < count + } + + @_optimize(none) + static func main() { + print("=== MallocInterposerSwift Test ===") + // Reset statistics to start clean + MallocInterposerSwift.initialize() + MallocInterposerSwift.hook() + + // let ptr = malloc(1000) + // let ptr2 = malloc(500) + // + // free(ptr) + // free(ptr2) + + // let x: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 5000) + + performAllocations(count: 1, size: 11 * 1024 * 1024) + //performAllocations(count: 1, size: 32 * 1024 * 1024, shouldFree: false) + + MallocInterposerSwift.unhook() + + // Print final statistics + let stats = MallocInterposerSwift.getStatistics() + + print("Total malloc count: \(stats.mallocCount)") + print("Malloc small count: \(stats.mallocSmallCount)") + print("Malloc large count: \(stats.mallocLargeCount)") + print("Total allocated memory: \(stats.mallocBytesCount) bytes") + print("Total free count: \(stats.freeCount)") + print("Total freed memory: \(stats.freeBytesCount) bytes") + + print("\n--- Test complete ---") + } +} diff --git a/Package.resolved b/Package.resolved index 9f4c1b20..4b021085 100644 --- a/Package.resolved +++ b/Package.resolved @@ -9,22 +9,13 @@ "version" : "0.1.3" } }, - { - "identity" : "package-jemalloc", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ordo-one/package-jemalloc.git", - "state" : { - "revision" : "e8a5db026963f5bfeac842d9d3f2cc8cde323b49", - "version" : "1.0.0" - } - }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "41982a3656a71c768319979febd796c6fd111d5c", - "version" : "1.5.0" + "revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3", + "version" : "1.6.1" } }, { @@ -32,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "cd142fd2f64be2100422d658e7411e39489da985", - "version" : "1.2.0" + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" } }, { @@ -50,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "a34201439c74b53f0fd71ef11741af7e7caf01e1", - "version" : "1.4.2" + "revision" : "890830fff1a577dc83134890c7984020c5f6b43b", + "version" : "1.6.2" } }, { diff --git a/Package.swift b/Package.swift index e6790598..878047dd 100644 --- a/Package.swift +++ b/Package.swift @@ -2,11 +2,6 @@ import PackageDescription -import class Foundation.ProcessInfo - -// If the environment variable BENCHMARK_DISABLE_JEMALLOC is set, we'll build the package without Jemalloc support -let disableJemalloc = ProcessInfo.processInfo.environment["BENCHMARK_DISABLE_JEMALLOC"] - let package = Package( name: "Benchmark", platforms: [ @@ -27,6 +22,7 @@ let package = Package( .package(url: "https://github.com/ordo-one/TextTable.git", .upToNextMajor(from: "0.0.1")), .package(url: "https://github.com/HdrHistogram/hdrhistogram-swift.git", .upToNextMajor(from: "0.1.0")), .package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.0.0")), + .package(path: "LocalPackages/MallocInterposerSwift"), ], targets: [ // Plugins used by users of the package @@ -115,23 +111,8 @@ let package = Package( ), ] ) -// Check if this is a SPI build, then we need to disable jemalloc for macOS - -let macOSSPIBuild: Bool // Disables jemalloc for macOS SPI builds as the infrastructure doesn't have jemalloc there - -#if canImport(Darwin) -if let spiBuildEnvironment = ProcessInfo.processInfo.environment["SPI_BUILD"], spiBuildEnvironment == "1" { - macOSSPIBuild = true - print("Building for SPI@macOS, disabling Jemalloc") -} else { - macOSSPIBuild = false -} -#else -macOSSPIBuild = false -#endif // Add Benchmark target dynamically - // Shared dependencies var dependencies: [PackageDescription.Target.Dependency] = [ .product(name: "Histogram", package: "hdrhistogram-swift"), @@ -142,19 +123,7 @@ var dependencies: [PackageDescription.Target.Dependency] = [ .product(name: "Atomics", package: "swift-atomics"), "SwiftRuntimeHooks", "BenchmarkShared", + "MallocInterposerSwift", ] -if macOSSPIBuild == false { // jemalloc always disable for macOSSPIBuild - if let disableJemalloc, disableJemalloc != "false", disableJemalloc != "0" { - print("Jemalloc disabled through environment variable.") - } else { - package.dependencies += [ - .package(url: "https://github.com/ordo-one/package-jemalloc.git", .upToNextMajor(from: "1.0.0")) - ] - dependencies += [ - .product(name: "jemalloc", package: "package-jemalloc", condition: .when(platforms: [.macOS, .linux])) - ] - } -} - package.targets += [.target(name: "Benchmark", dependencies: dependencies)] diff --git a/Plugins/BenchmarkCommandPlugin/BenchmarkCommandPlugin.swift b/Plugins/BenchmarkCommandPlugin/BenchmarkCommandPlugin.swift index 319c0586..9c2821cb 100644 --- a/Plugins/BenchmarkCommandPlugin/BenchmarkCommandPlugin.swift +++ b/Plugins/BenchmarkCommandPlugin/BenchmarkCommandPlugin.swift @@ -11,6 +11,7 @@ // 'Benchmark' plugin that is responsible for gathering command line arguments and then // Running the `BenchmarkTool` for each benchmark target. +import Foundation import PackagePlugin #if canImport(Darwin) @@ -149,6 +150,7 @@ import Glibc let packageBenchmarkIdentifier = "package-benchmark" let benchmarkToolName = "BenchmarkTool" let benchmarkTool: PackagePlugin.Path // = try context.tool(named: benchmarkToolName) + let interposerLib: String var args: [String] = [ benchmarkToolName, @@ -363,10 +365,7 @@ import Glibc } // Build the BenchmarkTool manually in release mode to work around https://github.com/apple/swift-package-manager/issues/7210 - guard - let benchmarkToolModule = benchmarkToolModuleTargets.first(where: { - $0.kind == .executable && $0.name == benchmarkToolName - }) + guard let benchmarkToolModule = benchmarkToolModuleTargets.first(where: { $0.kind == .executable && $0.name == benchmarkToolName }) else { print("Benchmark failed to find the BenchmarkTool target.") throw MyError.buildFailed @@ -401,6 +400,7 @@ import Glibc } benchmarkTool = tool.path + interposerLib = tool.path.removingLastComponent().appending(subpath: "libMallocInterposerC.so").string let filteredTargets = swiftSourceModuleTargets @@ -474,8 +474,27 @@ import Glibc return } + // On Linux we need to set LD_PRELOAD to get the malloc interposer working + // while on Darwin this is done with DYLD interpose mechanism + #if os(Linux) + var environment = ProcessInfo.processInfo.environment + environment["LD_PRELOAD"] = interposerLib + + let envp = environment.map { "\($0.key)=\($0.value)" }.map { $0.withCString(strdup) } + [nil] + defer { + for i in 0.. Bool { switch metric { - case .mallocCountLarge: - return true case .memoryLeaked: return true - case .mallocCountSmall: + case .memoryLeakedBytes: return true case .mallocCountTotal: return true case .allocatedResidentMemory: return true + case .freeCountTotal: + return true default: return false } diff --git a/Sources/Benchmark/BenchmarkExecutor.swift b/Sources/Benchmark/BenchmarkExecutor.swift index 99e07086..04f30398 100644 --- a/Sources/Benchmark/BenchmarkExecutor.swift +++ b/Sources/Benchmark/BenchmarkExecutor.swift @@ -8,6 +8,8 @@ // http://www.apache.org/licenses/LICENSE-2.0 // +import MallocInterposerSwift + #if canImport(OSLog) import OSLog #endif @@ -25,8 +27,8 @@ struct BenchmarkExecutor { // swiftlint:disable:this type_body_length // swiftlint:disable cyclomatic_complexity function_body_length func run(_ benchmark: Benchmark) -> [BenchmarkResult] { var wallClockDuration: Duration = .zero - var startMallocStats = MallocStats() - var stopMallocStats = MallocStats() + var startMallocStats = MallocInterposerSwift.Statistics() + var stopMallocStats = MallocInterposerSwift.Statistics() var startOperatingSystemStats = OperatingSystemStats() var stopOperatingSystemStats = OperatingSystemStats() var startPerformanceCounters = PerformanceCounters() @@ -106,9 +108,6 @@ struct BenchmarkExecutor { // swiftlint:disable:this type_body_length var iterations = 0 let initialStartTime = BenchmarkClock.now - // 'Warmup' to remove initial mallocs from stats in p100 - _ = MallocStatsProducer.makeMallocStats() // baselineMallocStats - // Calculate typical sys call check overhead and deduct that to get 'clean' stats for the actual benchmark var operatingSystemStatsOverhead = OperatingSystemStats() var baselinePeakMemoryResidentDelta = 0 @@ -154,7 +153,7 @@ struct BenchmarkExecutor { // swiftlint:disable:this type_body_length #endif if mallocStatsRequested { - startMallocStats = MallocStatsProducer.makeMallocStats() + startMallocStats = MallocInterposerSwift.getStatistics() } if arcStatsRequested { @@ -191,7 +190,7 @@ struct BenchmarkExecutor { // swiftlint:disable:this type_body_length } if mallocStatsRequested { - stopMallocStats = MallocStatsProducer.makeMallocStats() + stopMallocStats = MallocInterposerSwift.getStatistics() } #if canImport(OSLog) @@ -239,21 +238,30 @@ struct BenchmarkExecutor { // swiftlint:disable:this type_body_length } if mallocStatsRequested { - delta = stopMallocStats.mallocCountTotal - startMallocStats.mallocCountTotal - statistics[BenchmarkMetric.mallocCountTotal.index].add(Int(delta)) + let mallocCount = stopMallocStats.mallocCount - startMallocStats.mallocCount + statistics[BenchmarkMetric.mallocCountTotal.index].add(mallocCount) + + let mallocBytesCount = stopMallocStats.mallocBytesCount - startMallocStats.mallocBytesCount + statistics[BenchmarkMetric.mallocBytesCount.index].add(mallocBytesCount) + + // For backwards compatibility we keep allocatedResidentMemory as the total malloc bytes + statistics[BenchmarkMetric.allocatedResidentMemory.index].add(mallocBytesCount) + + let mallocSmallCount = stopMallocStats.mallocSmallCount - startMallocStats.mallocSmallCount + statistics[BenchmarkMetric.mallocCountSmall.index].add(mallocSmallCount) - delta = stopMallocStats.mallocCountSmall - startMallocStats.mallocCountSmall - statistics[BenchmarkMetric.mallocCountSmall.index].add(Int(delta)) + let mallocLargeCount = stopMallocStats.mallocLargeCount - startMallocStats.mallocLargeCount + statistics[BenchmarkMetric.mallocCountLarge.index].add(mallocLargeCount) - delta = stopMallocStats.mallocCountLarge - startMallocStats.mallocCountLarge - statistics[BenchmarkMetric.mallocCountLarge.index].add(Int(delta)) + let freeCount = stopMallocStats.freeCount - startMallocStats.freeCount + statistics[BenchmarkMetric.freeCountTotal.index].add(freeCount) - delta = stopMallocStats.allocatedResidentMemory - startMallocStats.allocatedResidentMemory - statistics[BenchmarkMetric.memoryLeaked.index].add(Int(delta)) + let memoryLeakedCount = mallocCount - freeCount + statistics[BenchmarkMetric.memoryLeaked.index].add(Int(memoryLeakedCount)) - // delta = stopMallocStats.allocatedResidentMemory - baselineMallocStats.allocatedResidentMemory // baselineMallocStats! - statistics[BenchmarkMetric.allocatedResidentMemory.index] - .add(Int(stopMallocStats.allocatedResidentMemory)) + let freeBytes = stopMallocStats.freeBytesCount - startMallocStats.freeBytesCount + let memoryLeakedBytes = mallocBytesCount - freeBytes + statistics[BenchmarkMetric.memoryLeakedBytes.index].add(Int(memoryLeakedBytes)) } if operatingSystemStatsRequested { @@ -335,6 +343,10 @@ struct BenchmarkExecutor { // swiftlint:disable:this type_body_length ARCStatsProducer.hook() } + if mallocStatsRequested { + MallocInterposerSwift.hook() + } + if benchmark.configuration.metrics.contains(.threads) || benchmark.configuration.metrics.contains(.threadsRunning) || benchmark.configuration.metrics.contains(.peakMemoryResident) @@ -425,6 +437,10 @@ struct BenchmarkExecutor { // swiftlint:disable:this type_body_length ARCStatsProducer.unhook() } + if mallocStatsRequested { + MallocInterposerSwift.unhook() + } + #if canImport(OSLog) signPost.endInterval("Benchmark", benchmarkInterval, "\(iterations)") #endif diff --git a/Sources/Benchmark/BenchmarkMetric+Defaults.swift b/Sources/Benchmark/BenchmarkMetric+Defaults.swift index 6ec46b88..ece0725a 100644 --- a/Sources/Benchmark/BenchmarkMetric+Defaults.swift +++ b/Sources/Benchmark/BenchmarkMetric+Defaults.swift @@ -33,7 +33,13 @@ public extension BenchmarkMetric { [ .wallClock, .cpuTotal, + .mallocCountSmall, + .mallocCountLarge, .mallocCountTotal, + .freeCountTotal, + .mallocBytesCount, + .memoryLeaked, + .memoryLeakedBytes, .throughput, .instructions, .peakMemoryResident, @@ -46,10 +52,15 @@ public extension BenchmarkMetric { .wallClock, .cpuUser, .cpuTotal, + .mallocCountSmall, + .mallocCountLarge, .mallocCountTotal, + .freeCountTotal, + .mallocBytesCount, .throughput, .peakMemoryResident, .memoryLeaked, + .memoryLeakedBytes, .syscalls, .instructions, ] @@ -64,7 +75,10 @@ public extension BenchmarkMetric { .mallocCountSmall, .mallocCountLarge, .mallocCountTotal, + .mallocBytesCount, + .freeCountTotal, .memoryLeaked, + .memoryLeakedBytes, .allocatedResidentMemory, ] } @@ -117,7 +131,10 @@ public extension BenchmarkMetric { .mallocCountSmall, .mallocCountLarge, .mallocCountTotal, + .freeCountTotal, + .mallocBytesCount, .memoryLeaked, + .memoryLeakedBytes, .syscalls, .contextSwitches, .threads, diff --git a/Sources/Benchmark/BenchmarkMetric.swift b/Sources/Benchmark/BenchmarkMetric.swift index b5d06096..0672eb05 100644 --- a/Sources/Benchmark/BenchmarkMetric.swift +++ b/Sources/Benchmark/BenchmarkMetric.swift @@ -35,13 +35,21 @@ public enum BenchmarkMetric: Hashable, Equatable, Codable, CustomStringConvertib case mallocCountSmall /// Number of large malloc calls case mallocCountLarge - /// Number of small+large mallocs + /// Number of total malloc calls (small+large) case mallocCountTotal + /// Number of totatl free calls + case freeCountTotal + /// The amount of memory allocated in bytes through malloc calls + case mallocBytesCount /// The amount of allocated resident memory according to the memory allocator /// by the application (does not include metadata overhead etc) + /// **Deprecated** in favour of ``mallocBytesCount``. It value is equal to ``mallocBytesCount``. + @available(*, deprecated, message: "Deprecated in favor of mallocBytesCount") case allocatedResidentMemory /// Number of small+large mallocs - small+large frees in resident memory case memoryLeaked + /// Leaked memeory in bytes + case memoryLeakedBytes /// Measure number of syscalls made during the test case syscalls /// Measure number of context switches made during the test @@ -120,7 +128,7 @@ public extension BenchmarkMetric { switch self { case .cpuSystem, .cpuTotal, .cpuUser, .wallClock: return true - case .mallocCountLarge, .mallocCountSmall, .mallocCountTotal, .memoryLeaked: + case .mallocCountTotal, .memoryLeaked, .memoryLeakedBytes: return true case .syscalls: return true @@ -132,7 +140,7 @@ public extension BenchmarkMetric { return true case .objectAllocCount, .retainCount, .releaseCount, .retainReleaseDelta: return true - case let .custom(_, _, useScaleFactor): + case .custom(_, _, let useScaleFactor): return useScaleFactor default: return false @@ -144,7 +152,7 @@ public extension BenchmarkMetric { switch self { case .throughput: return .prefersLarger - case let .custom(_, polarity, _): + case .custom(_, let polarity, _): return polarity default: return .prefersSmaller @@ -175,10 +183,14 @@ public extension BenchmarkMetric { return "Malloc (large)" case .mallocCountTotal: return "Malloc (total)" + case .mallocBytesCount: + return "Malloc (bytes total)" case .allocatedResidentMemory: return "Memory (allocated resident)" case .memoryLeaked: return "Malloc / free Δ" + case .memoryLeakedBytes: + return "Malloc / free Δ (bytes)" case .syscalls: return "Syscalls (total)" case .contextSwitches: @@ -213,8 +225,10 @@ public extension BenchmarkMetric { return "Δ" case .deltaPercentage: return "Δ %" - case let .custom(name, _, _): + case .custom(let name, _, _): return name + case .freeCountTotal: + return "Free (total)" } } @@ -244,47 +258,53 @@ public extension BenchmarkMetric { return 10 case .mallocCountTotal: return 11 - case .allocatedResidentMemory: + case .freeCountTotal: return 12 - case .memoryLeaked: + case .mallocBytesCount: return 13 - case .syscalls: + case .allocatedResidentMemory: return 14 - case .contextSwitches: + case .memoryLeaked: return 15 - case .threads: + case .memoryLeakedBytes: return 16 - case .threadsRunning: + case .syscalls: return 17 - case .readSyscalls: + case .contextSwitches: return 18 - case .writeSyscalls: + case .threads: return 19 - case .readBytesLogical: + case .threadsRunning: return 20 - case .writeBytesLogical: + case .readSyscalls: return 21 - case .readBytesPhysical: + case .writeSyscalls: return 22 - case .writeBytesPhysical: + case .readBytesLogical: return 23 - case .objectAllocCount: + case .writeBytesLogical: return 24 - case .retainCount: + case .readBytesPhysical: return 25 - case .releaseCount: + case .writeBytesPhysical: return 26 - case .retainReleaseDelta: + case .objectAllocCount: return 27 - case .instructions: + case .retainCount: return 28 + case .releaseCount: + return 29 + case .retainReleaseDelta: + return 30 + case .instructions: + return 31 default: return 0 // custom payloads must be stored in dictionary } } @_documentation(visibility: internal) - static var maxIndex: Int { 28 } // + static var maxIndex: Int { 31 } // // Used by the Benchmark Executor for efficient indexing into results @_documentation(visibility: internal) @@ -313,38 +333,44 @@ public extension BenchmarkMetric { case 11: return .mallocCountTotal case 12: - return .allocatedResidentMemory + return .freeCountTotal case 13: - return .memoryLeaked + return .mallocBytesCount case 14: - return .syscalls + return .allocatedResidentMemory case 15: - return .contextSwitches + return .memoryLeaked case 16: - return .threads + return .memoryLeakedBytes case 17: - return .threadsRunning + return .syscalls case 18: - return .readSyscalls + return .contextSwitches case 19: - return .writeSyscalls + return .threads case 20: - return .readBytesLogical + return .threadsRunning case 21: - return .writeBytesLogical + return .readSyscalls case 22: - return .readBytesPhysical + return .writeSyscalls case 23: - return .writeBytesPhysical + return .readBytesLogical case 24: - return .objectAllocCount + return .writeBytesLogical case 25: - return .retainCount + return .readBytesPhysical case 26: - return .releaseCount + return .writeBytesPhysical case 27: - return .retainReleaseDelta + return .objectAllocCount case 28: + return .retainCount + case 29: + return .releaseCount + case 30: + return .retainReleaseDelta + case 31: return .instructions default: break @@ -379,10 +405,16 @@ public extension BenchmarkMetric { return "mallocCountLarge" case .mallocCountTotal: return "mallocCountTotal" + case .freeCountTotal: + return "freeCountTotal" + case .mallocBytesCount: + return "mallocBytesCount" case .allocatedResidentMemory: return "allocatedResidentMemory" case .memoryLeaked: return "memoryLeaked" + case .memoryLeakedBytes: + return "memoryLeakedBytes" case .syscalls: return "syscalls" case .contextSwitches: @@ -417,7 +449,7 @@ public extension BenchmarkMetric { return "Δ" case .deltaPercentage: return "Δ %" - case let .custom(name, _, _): + case .custom(let name, _, _): return name } } @@ -451,10 +483,16 @@ public extension BenchmarkMetric { self = BenchmarkMetric.mallocCountLarge case "mallocCountTotal": self = BenchmarkMetric.mallocCountTotal + case "freeCountTotal": + self = BenchmarkMetric.freeCountTotal + case "mallocBytesCount": + self = BenchmarkMetric.mallocBytesCount case "allocatedResidentMemory": self = BenchmarkMetric.allocatedResidentMemory case "memoryLeaked": self = BenchmarkMetric.memoryLeaked + case "memoryLeakedBytes": + self = BenchmarkMetric.memoryLeakedBytes case "syscalls": self = BenchmarkMetric.syscalls case "contextSwitches": diff --git a/Sources/Benchmark/BenchmarkRunner.swift b/Sources/Benchmark/BenchmarkRunner.swift index 8ed30363..3a14f343 100644 --- a/Sources/Benchmark/BenchmarkRunner.swift +++ b/Sources/Benchmark/BenchmarkRunner.swift @@ -10,6 +10,7 @@ import ArgumentParser import BenchmarkShared +import MallocInterposerSwift #if canImport(Darwin) import Darwin @@ -112,6 +113,7 @@ public struct BenchmarkRunner: AsyncParsableCommand, BenchmarkRunnerReadWrite { var debugIterator = Benchmark.benchmarks.makeIterator() var benchmarkCommand: BenchmarkCommandRequest + MallocInterposerSwift.initialize() let benchmarkExecutor = BenchmarkExecutor(quiet: quiet) var benchmark: Benchmark? var results: [BenchmarkResult] = [] @@ -147,7 +149,7 @@ public struct BenchmarkRunner: AsyncParsableCommand, BenchmarkRunnerReadWrite { } try channel.write(.end) - case let .run(benchmarkToRun): + case .run(let benchmarkToRun): benchmark = Benchmark.benchmarks.first { $0.name == benchmarkToRun.name } if let benchmark { diff --git a/Sources/Benchmark/MallocStats/MallocStats+jemalloc-support.swift b/Sources/Benchmark/MallocStats/MallocStats+jemalloc-support.swift deleted file mode 100644 index 38e34761..00000000 --- a/Sources/Benchmark/MallocStats/MallocStats+jemalloc-support.swift +++ /dev/null @@ -1,363 +0,0 @@ -// swiftlint:disable all -// -// Copyright (c) 2022 Ordo One AB. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// - -// This file was generated from JSON Schema using quicktype, do not modify it directly. - -// Generated using https://app.quicktype.io with paired down output from -// let optionString = "J" -// malloc_stats_print(nil, nil, optionString) - -// MARK: - Pokedex - -struct Pokedex: Codable { - let jemalloc: Jemalloc -} - -// MARK: - Jemalloc - -struct Jemalloc: Codable { - let version: String - let config: Config - let opt: Opt - let arenas: Arenas - let stats: Stats - let statsArenas: StatsArenas - - enum CodingKeys: String, CodingKey { - case version, config, opt, arenas, stats - case statsArenas = "stats.arenas" - } -} - -// MARK: - Arenas - -struct Arenas: Codable { - let narenas, dirtyDecayMS, muzzyDecayMS, quantum: Int - let page, tcacheMax, nbins, nhbins: Int - let bin: [ArenasBin] - let nlextents: Int - let lextent: [ArenasLextent] - - enum CodingKeys: String, CodingKey { - case narenas - case dirtyDecayMS = "dirty_decay_ms" - case muzzyDecayMS = "muzzy_decay_ms" - case quantum, page - case tcacheMax = "tcache_max" - case nbins, nhbins, bin, nlextents, lextent - } -} - -// MARK: - ArenasBin - -struct ArenasBin: Codable { - let size, nregs, slabSize, nshards: Int - - enum CodingKeys: String, CodingKey { - case size, nregs - case slabSize = "slab_size" - case nshards - } -} - -// MARK: - ArenasLextent - -struct ArenasLextent: Codable { - let size: Double -} - -// MARK: - Config - -struct Config: Codable { - let cacheOblivious, debug, fill, lazyLock: Bool - let mallocConf: String - let optSafetyChecks, prof, profLibgcc, profLibunwind: Bool - let stats, utrace, xmalloc: Bool - - enum CodingKeys: String, CodingKey { - case cacheOblivious = "cache_oblivious" - case debug, fill - case lazyLock = "lazy_lock" - case mallocConf = "malloc_conf" - case optSafetyChecks = "opt_safety_checks" - case prof - case profLibgcc = "prof_libgcc" - case profLibunwind = "prof_libunwind" - case stats, utrace, xmalloc - } -} - -// MARK: - Opt - -struct Opt: Codable { - let abort, abortConf, cacheOblivious, confirmConf: Bool - let retain: Bool - let dss: String - let narenas: Int - let percpuArena: String - let oversizeThreshold: Int - let hpa: Bool - let hpaSlabMaxAlloc, hpaHugificationThreshold, hpaHugifyDelayMS, hpaMinPurgeIntervalMS: Int - let hpaDirtyMult: String - let hpaSECNshards, hpaSECMaxAlloc, hpaSECMaxBytes, hpaSECBytesAfterFlush: Int - let hpaSECBatchFillExtra: Int - let metadataThp: String - let mutexMaxSpin, dirtyDecayMS, muzzyDecayMS, lgExtentMaxActiveFit: Int - let junk: String - let zero, experimentalInfallibleNew, tcache: Bool - let tcacheMax, tcacheNslotsSmallMin, tcacheNslotsSmallMax, tcacheNslotsLarge: Int - let lgTcacheNslotsMul, tcacheGcIncrBytes, tcacheGcDelayBytes, lgTcacheFlushSmallDiv: Int - let lgTcacheFlushLargeDiv: Int - let thp: String - let statsPrint: Bool - let statsPrintOpts: String - let statsInterval: Int - let statsIntervalOpts, zeroRealloc: String - - enum CodingKeys: String, CodingKey { - case abort - case abortConf = "abort_conf" - case cacheOblivious = "cache_oblivious" - case confirmConf = "confirm_conf" - case retain, dss, narenas - case percpuArena = "percpu_arena" - case oversizeThreshold = "oversize_threshold" - case hpa - case hpaSlabMaxAlloc = "hpa_slab_max_alloc" - case hpaHugificationThreshold = "hpa_hugification_threshold" - case hpaHugifyDelayMS = "hpa_hugify_delay_ms" - case hpaMinPurgeIntervalMS = "hpa_min_purge_interval_ms" - case hpaDirtyMult = "hpa_dirty_mult" - case hpaSECNshards = "hpa_sec_nshards" - case hpaSECMaxAlloc = "hpa_sec_max_alloc" - case hpaSECMaxBytes = "hpa_sec_max_bytes" - case hpaSECBytesAfterFlush = "hpa_sec_bytes_after_flush" - case hpaSECBatchFillExtra = "hpa_sec_batch_fill_extra" - case metadataThp = "metadata_thp" - case mutexMaxSpin = "mutex_max_spin" - case dirtyDecayMS = "dirty_decay_ms" - case muzzyDecayMS = "muzzy_decay_ms" - case lgExtentMaxActiveFit = "lg_extent_max_active_fit" - case junk, zero - case experimentalInfallibleNew = "experimental_infallible_new" - case tcache - case tcacheMax = "tcache_max" - case tcacheNslotsSmallMin = "tcache_nslots_small_min" - case tcacheNslotsSmallMax = "tcache_nslots_small_max" - case tcacheNslotsLarge = "tcache_nslots_large" - case lgTcacheNslotsMul = "lg_tcache_nslots_mul" - case tcacheGcIncrBytes = "tcache_gc_incr_bytes" - case tcacheGcDelayBytes = "tcache_gc_delay_bytes" - case lgTcacheFlushSmallDiv = "lg_tcache_flush_small_div" - case lgTcacheFlushLargeDiv = "lg_tcache_flush_large_div" - case thp - case statsPrint = "stats_print" - case statsPrintOpts = "stats_print_opts" - case statsInterval = "stats_interval" - case statsIntervalOpts = "stats_interval_opts" - case zeroRealloc = "zero_realloc" - } -} - -// MARK: - Stats - -struct Stats: Codable { - let allocated, active, metadata, metadataThp: Int - let resident, mapped, retained, zeroReallocs: Int - let backgroundThread: StatsBackgroundThread - let mutexes: Mutexes - - enum CodingKeys: String, CodingKey { - case allocated, active, metadata - case metadataThp = "metadata_thp" - case resident, mapped, retained - case zeroReallocs = "zero_reallocs" - case backgroundThread = "background_thread" - case mutexes - } -} - -// MARK: - StatsBackgroundThread - -struct StatsBackgroundThread: Codable { - let numThreads, numRuns, runInterval: Int - - enum CodingKeys: String, CodingKey { - case numThreads = "num_threads" - case numRuns = "num_runs" - case runInterval = "run_interval" - } -} - -// MARK: - Mutexes - -struct Mutexes: Codable { - let backgroundThread, maxPerBgThd, ctl, prof: BackgroundThreadValue - let profThdsData, profDump, profRecentAlloc, profRecentDump: BackgroundThreadValue - let profStats: BackgroundThreadValue - - enum CodingKeys: String, CodingKey { - case backgroundThread = "background_thread" - case maxPerBgThd = "max_per_bg_thd" - case ctl, prof - case profThdsData = "prof_thds_data" - case profDump = "prof_dump" - case profRecentAlloc = "prof_recent_alloc" - case profRecentDump = "prof_recent_dump" - case profStats = "prof_stats" - } -} - -// MARK: - BackgroundThreadValue - -struct BackgroundThreadValue: Codable { - let numOps, numWait, numSpinAcq, numOwnerSwitch: Int - let totalWaitTime, maxWaitTime, maxNumThds: Int - - enum CodingKeys: String, CodingKey { - case numOps = "num_ops" - case numWait = "num_wait" - case numSpinAcq = "num_spin_acq" - case numOwnerSwitch = "num_owner_switch" - case totalWaitTime = "total_wait_time" - case maxWaitTime = "max_wait_time" - case maxNumThds = "max_num_thds" - } -} - -// MARK: - StatsArenas - -struct StatsArenas: Codable { - let merged: Merged -} - -// MARK: - Merged - -struct Merged: Codable { - let nthreads, uptimeNS: Int - let dss: String - let dirtyDecayMS, muzzyDecayMS, pactive, pdirty: Int - let pmuzzy, dirtyNpurge, dirtyNmadvise, dirtyPurged: Int - let muzzyNpurge, muzzyNmadvise, muzzyPurged: Int - let small, large: Large - let mapped, retained, base, mergedInternal: Int - let metadataThp, tcacheBytes, tcacheStashedBytes, resident: Int - let abandonedVM, extentAvail: Int - let mutexes: [String: BackgroundThreadValue] - let bins: [MergedBin] - let lextents: [MergedLextent] - let extents: [Extent] - let secBytes: Int - let hpaShard: HpaShard - - enum CodingKeys: String, CodingKey { - case nthreads - case uptimeNS = "uptime_ns" - case dss - case dirtyDecayMS = "dirty_decay_ms" - case muzzyDecayMS = "muzzy_decay_ms" - case pactive, pdirty, pmuzzy - case dirtyNpurge = "dirty_npurge" - case dirtyNmadvise = "dirty_nmadvise" - case dirtyPurged = "dirty_purged" - case muzzyNpurge = "muzzy_npurge" - case muzzyNmadvise = "muzzy_nmadvise" - case muzzyPurged = "muzzy_purged" - case small, large, mapped, retained, base - case mergedInternal = "internal" - case metadataThp = "metadata_thp" - case tcacheBytes = "tcache_bytes" - case tcacheStashedBytes = "tcache_stashed_bytes" - case resident - case abandonedVM = "abandoned_vm" - case extentAvail = "extent_avail" - case mutexes, bins, lextents, extents - case secBytes = "sec_bytes" - case hpaShard = "hpa_shard" - } -} - -// MARK: - MergedBin - -struct MergedBin: Codable { - let nmalloc, ndalloc, curregs, nrequests: Int - let nfills, nflushes, nreslabs, curslabs: Int - let nonfullSlabs: Int - let mutex: BackgroundThreadValue - - enum CodingKeys: String, CodingKey { - case nmalloc, ndalloc, curregs, nrequests, nfills, nflushes, nreslabs, curslabs - case nonfullSlabs = "nonfull_slabs" - case mutex - } -} - -// MARK: - Extent - -struct Extent: Codable { - let ndirty, nmuzzy, nretained, dirtyBytes: Int - let muzzyBytes, retainedBytes: Int - - enum CodingKeys: String, CodingKey { - case ndirty, nmuzzy, nretained - case dirtyBytes = "dirty_bytes" - case muzzyBytes = "muzzy_bytes" - case retainedBytes = "retained_bytes" - } -} - -// MARK: - HpaShard - -struct HpaShard: Codable { - let npurgePasses, npurges, nhugifies, ndehugifies: Int - let fullSlabs, emptySlabs: EmptySlabs - let nonfullSlabs: [EmptySlabs] - - enum CodingKeys: String, CodingKey { - case npurgePasses = "npurge_passes" - case npurges, nhugifies, ndehugifies - case fullSlabs = "full_slabs" - case emptySlabs = "empty_slabs" - case nonfullSlabs = "nonfull_slabs" - } -} - -// MARK: - EmptySlabs - -struct EmptySlabs: Codable { - let npageslabsHuge, nactiveHuge, npageslabsNonhuge, nactiveNonhuge: Int - let ndirtyNonhuge: Int - let ndirtyHuge: Int? - - enum CodingKeys: String, CodingKey { - case npageslabsHuge = "npageslabs_huge" - case nactiveHuge = "nactive_huge" - case npageslabsNonhuge = "npageslabs_nonhuge" - case nactiveNonhuge = "nactive_nonhuge" - case ndirtyNonhuge = "ndirty_nonhuge" - case ndirtyHuge = "ndirty_huge" - } -} - -// MARK: - Large - -struct Large: Codable { - let allocated, nmalloc, ndalloc, nrequests: Int - let nfills, nflushes: Int -} - -// MARK: - MergedLextent - -struct MergedLextent: Codable { - let curlextents: Int -} - -// swiftlint:enable all diff --git a/Tests/BenchmarkTests/BenchmarkMetricsTests.swift b/Tests/BenchmarkTests/BenchmarkMetricsTests.swift index e3822010..889128cf 100644 --- a/Tests/BenchmarkTests/BenchmarkMetricsTests.swift +++ b/Tests/BenchmarkTests/BenchmarkMetricsTests.swift @@ -22,9 +22,9 @@ final class BenchmarkMetricsTests: XCTestCase { .peakMemoryResident, .peakMemoryResidentDelta, .peakMemoryVirtual, - .mallocCountSmall, - .mallocCountLarge, .mallocCountTotal, + .mallocBytesCount, + .freeCountTotal, .allocatedResidentMemory, .memoryLeaked, .syscalls, @@ -55,9 +55,9 @@ final class BenchmarkMetricsTests: XCTestCase { "peakMemoryResident", "peakMemoryResidentDelta", "peakMemoryVirtual", - "mallocCountSmall", - "mallocCountLarge", "mallocCountTotal", + "mallocBytesCount", + "freeCountTotal", "allocatedResidentMemory", "memoryLeaked", "syscalls", diff --git a/Tests/BenchmarkTests/OperatingSystemAndMallocTests.swift b/Tests/BenchmarkTests/OperatingSystemAndMallocTests.swift index 11ab6f83..60d69d75 100644 --- a/Tests/BenchmarkTests/OperatingSystemAndMallocTests.swift +++ b/Tests/BenchmarkTests/OperatingSystemAndMallocTests.swift @@ -60,24 +60,6 @@ final class OperatingSystemAndMallocTests: XCTestCase { blackHole(operatingSystemStatsProducer.metricSupported(.throughput)) } - #if canImport(jemalloc) - func testMallocProducerLeaks() throws { - let startMallocStats = MallocStatsProducer.makeMallocStats() - - for outerloop in 1...100 { - blackHole(malloc(outerloop * 1_024)) - } - - let stopMallocStats = MallocStatsProducer.makeMallocStats() - - XCTAssertGreaterThanOrEqual(stopMallocStats.mallocCountTotal - startMallocStats.mallocCountTotal, 100) - XCTAssertGreaterThanOrEqual( - stopMallocStats.allocatedResidentMemory - startMallocStats.allocatedResidentMemory, - 100 * 1_024 - ) - } - #endif - func testARCStatsProducer() throws { let array = [3] ARCStatsProducer.hook()