]> git.proxmox.com Git - libgit2.git/commitdiff
Include stacktrace summary in memory leak output.
authorJeff Hostetler <jeffhostetler@me.com>
Tue, 9 Jun 2015 21:38:30 +0000 (14:38 -0700)
committerJeff Hostetler <jeffhost@microsoft.com>
Mon, 29 Jun 2015 20:39:11 +0000 (16:39 -0400)
CMakeLists.txt
src/common.h
src/global.c
src/util.h
src/win32/w32_crtdbg_stacktrace.c [new file with mode: 0644]
src/win32/w32_crtdbg_stacktrace.h [new file with mode: 0644]
src/win32/w32_stack.c [new file with mode: 0644]
src/win32/w32_stack.h [new file with mode: 0644]
tests/clar_libgit2_trace.c
tests/main.c
tests/trace/windows/stacktrace.c [new file with mode: 0644]

index 5c55ddd1a2b3c1a4c1d4a031bdae8df77fa8bd82..994eed5e11eb0e4d314c88aa9601bc43054badfe 100644 (file)
@@ -336,6 +336,7 @@ IF (MSVC)
 
        IF (MSVC_CRTDBG)
                SET(CRT_FLAG_DEBUG "${CRT_FLAG_DEBUG} /DGIT_MSVC_CRTDBG")
+               SET(CMAKE_C_STANDARD_LIBRARIES "${CMAKE_C_STANDARD_LIBRARIES}" "Dbghelp.lib")
        ENDIF()
 
        # /Zi - Create debugging information
index cdb0b514d6f86f271c03d615b9b58bbfed2b6d57..2b1dedc01463a34a9d25c0f49f10e77c4ae5168a 100644 (file)
 # ifdef GIT_THREADS
 #      include "win32/pthread.h"
 # endif
+# if defined(GIT_MSVC_CRTDBG)
+#   include "win32/w32_stack.h"
+#   include "win32/w32_crtdbg_stacktrace.h"
+# endif
 
 #else
 
index 3f20bfd31954772629d9bd9f6188f6434232169a..fc6337adc0bfb99a89edb9420a848ae2ecd1e6ae 100644 (file)
 #include "git2/global.h"
 #include "git2/sys/openssl.h"
 #include "thread-utils.h"
-
+#if defined(GIT_MSVC_CRTDBG)
+#include "win32/w32_stack.h"
+#include "win32/w32_crtdbg_stacktrace.h"
+#endif
 
 git_mutex git__mwindow_mutex;
 
@@ -225,6 +228,11 @@ int git_libgit2_init(void)
 
        /* Only do work on a 0 -> 1 transition of the refcount */
        if ((ret = git_atomic_inc(&git__n_inits)) == 1) {
+#if defined(GIT_MSVC_CRTDBG)
+               git_win32__crtdbg_stacktrace_init();
+               git_win32__stack_init();
+#endif
+
                if (synchronized_threads_init() < 0)
                        ret = -1;
        }
@@ -254,9 +262,15 @@ int git_libgit2_shutdown(void)
        while (InterlockedCompareExchange(&_mutex, 1, 0)) { Sleep(0); }
 
        /* Only do work on a 1 -> 0 transition of the refcount */
-       if ((ret = git_atomic_dec(&git__n_inits)) == 0)
+       if ((ret = git_atomic_dec(&git__n_inits)) == 0) {
                synchronized_threads_shutdown();
 
+#if defined(GIT_MSVC_CRTDBG)
+               git_win32__crtdbg_stacktrace_cleanup();
+               git_win32__stack_cleanup();
+#endif
+       }
+
        /* Exit the lock */
        InterlockedExchange(&_mutex, 0);
 
index b2abbe6a6525f6f855f61a8bf5d38fd815daa302..8c336e3ef8e2f3f8b987d5483dc0044b05739d60 100644 (file)
@@ -35,6 +35,7 @@
  */
 #include <stdlib.h>
 #include <crtdbg.h>
+#include "win32/w32_crtdbg_stacktrace.h"
 #endif
 
 #include "common.h"
 #define CONST_STRLEN(x) ((sizeof(x)/sizeof(x[0])) - 1)
 
 #if defined(GIT_MSVC_CRTDBG)
+
 GIT_INLINE(void *) git__crtdbg__malloc(size_t len, const char *file, int line)
 {
-       void *ptr = _malloc_dbg(len, _NORMAL_BLOCK, file, line);
+       void *ptr = _malloc_dbg(len, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
        if (!ptr) giterr_set_oom();
        return ptr;
 }
 
 GIT_INLINE(void *) git__crtdbg__calloc(size_t nelem, size_t elsize, const char *file, int line)
 {
-       void *ptr = _calloc_dbg(nelem, elsize, _NORMAL_BLOCK, file, line);
+       void *ptr = _calloc_dbg(nelem, elsize, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
        if (!ptr) giterr_set_oom();
        return ptr;
 }
 
 GIT_INLINE(char *) git__crtdbg__strdup(const char *str, const char *file, int line)
 {
-       char *ptr = _strdup_dbg(str, _NORMAL_BLOCK, file, line);
+       char *ptr = _strdup_dbg(str, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
        if (!ptr) giterr_set_oom();
        return ptr;
 }
@@ -118,7 +120,7 @@ GIT_INLINE(char *) git__crtdbg__substrdup(const char *start, size_t n, const cha
 
 GIT_INLINE(void *) git__crtdbg__realloc(void *ptr, size_t size, const char *file, int line)
 {
-       void *new_ptr = _realloc_dbg(ptr, size, _NORMAL_BLOCK, file, line);
+       void *new_ptr = _realloc_dbg(ptr, size, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
        if (!new_ptr) giterr_set_oom();
        return new_ptr;
 }
@@ -126,8 +128,9 @@ GIT_INLINE(void *) git__crtdbg__realloc(void *ptr, size_t size, const char *file
 GIT_INLINE(void *) git__crtdbg__reallocarray(void *ptr, size_t nelem, size_t elsize, const char *file, int line)
 {
        size_t newsize;
+
        return GIT_MULTIPLY_SIZET_OVERFLOW(&newsize, nelem, elsize) ?
-               NULL : _realloc_dbg(ptr, newsize, _NORMAL_BLOCK, file, line);
+               NULL : _realloc_dbg(ptr, newsize, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
 }
 
 GIT_INLINE(void *) git__crtdbg__mallocarray(size_t nelem, size_t elsize, const char *file, int line)
diff --git a/src/win32/w32_crtdbg_stacktrace.c b/src/win32/w32_crtdbg_stacktrace.c
new file mode 100644 (file)
index 0000000..a778f41
--- /dev/null
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#if defined(GIT_MSVC_CRTDBG)
+#include "w32_stack.h"
+#include "w32_crtdbg_stacktrace.h"
+
+#define CRTDBG_STACKTRACE__UID_LEN (15)
+
+/**
+ * The stacktrace of an allocation can be distilled
+ * to a unique id based upon the stackframe pointers
+ * and ignoring any size arguments. We will use these
+ * UIDs as the (char const*) __FILE__ argument we
+ * give to the CRT malloc routines.
+ */
+typedef struct {
+       char uid[CRTDBG_STACKTRACE__UID_LEN + 1];
+} git_win32__crtdbg_stacktrace__uid;
+
+/**
+ * All mallocs with the same stacktrace will be de-duped
+ * and aggregated into this row.
+ */
+typedef struct {
+       git_win32__crtdbg_stacktrace__uid uid; /* must be first */
+       git_win32__stack__raw_data raw_data;
+       unsigned int count_allocs; /* times this alloc signature seen since init */
+       unsigned int count_allocs_at_last_checkpoint; /* times since last mark */
+       unsigned int transient_count_leaks; /* sum of leaks */
+} git_win32__crtdbg_stacktrace__row;
+
+static CRITICAL_SECTION g_crtdbg_stacktrace_cs;
+
+/**
+ * CRTDBG memory leak tracking takes a "char const * const file_name"
+ * and stores the pointer in the heap data (instead of allocing a copy
+ * for itself).  Normally, this is not a problem, since we usually pass
+ * in __FILE__.  But I'm going to lie to it and pass in the address of
+ * the UID in place of the file_name.  Also, I do not want to alloc the
+ * stacktrace data (because we are called from inside our alloc routines).
+ * Therefore, I'm creating a very large static pool array to store row
+ * data. This also eliminates the temptation to realloc it (and move the
+ * UID pointers).
+ *
+ * And to efficiently look for duplicates we need an index on the rows
+ * so we can bsearch it.  Again, without mallocing.
+ *
+ * If we observe more than MY_ROW_LIMIT unique malloc signatures, we
+ * fall through and use the traditional __FILE__ processing and don't
+ * try to de-dup them.  If your testing hits this limit, just increase
+ * it and try again.
+ */
+
+#define MY_ROW_LIMIT (1024 * 1024)
+static git_win32__crtdbg_stacktrace__row  g_cs_rows[MY_ROW_LIMIT];
+static git_win32__crtdbg_stacktrace__row *g_cs_index[MY_ROW_LIMIT];
+
+static unsigned int g_cs_end = MY_ROW_LIMIT;
+static unsigned int g_cs_ins = 0; /* insertion point == unique allocs seen */
+static unsigned int g_count_total_allocs = 0; /* number of allocs seen */
+static unsigned int g_transient_count_total_leaks = 0; /* number of total leaks */
+static unsigned int g_transient_count_dedup_leaks = 0; /* number of unique leaks */
+static bool g_limit_reached = false; /* had allocs after we filled row table */
+
+static unsigned int g_checkpoint_id = 0; /* to better label leak checkpoints */
+static bool g_transient_leaks_since_mark = false; /* payload for hook */
+
+/**
+ * Compare function for bsearch on g_cs_index table.
+ */
+static int row_cmp(const void *v1, const void *v2)
+{
+       git_win32__stack__raw_data *d1 = (git_win32__stack__raw_data*)v1;
+       git_win32__crtdbg_stacktrace__row *r2 = (git_win32__crtdbg_stacktrace__row *)v2;
+
+       return (git_win32__stack_compare(d1, &r2->raw_data));
+}
+
+/**
+ * Unique insert the new data into the row and index tables.
+ * We have to sort by the stackframe data itself, not the uid.
+ */
+static git_win32__crtdbg_stacktrace__row * insert_unique(
+       const git_win32__stack__raw_data *pdata)
+{
+       size_t pos;
+       if (git__bsearch(g_cs_index, g_cs_ins, pdata, row_cmp, &pos) < 0) {
+               /* Append new unique item to row table. */
+               memcpy(&g_cs_rows[g_cs_ins].raw_data, pdata, sizeof(*pdata));
+               sprintf(g_cs_rows[g_cs_ins].uid.uid, "##%08lx", g_cs_ins);
+
+               /* Insert pointer to it into the proper place in the index table. */
+               if (pos < g_cs_ins)
+                       memmove(&g_cs_index[pos+1], &g_cs_index[pos], (g_cs_ins - pos)*sizeof(g_cs_index[0]));
+               g_cs_index[pos] = &g_cs_rows[g_cs_ins];
+
+               g_cs_ins++;
+       }
+
+       g_cs_index[pos]->count_allocs++;
+
+       return g_cs_index[pos];
+}
+
+/**
+ * Hook function to receive leak data from the CRT. (This includes
+ * both "<file_name>:(<line_number>)" data, but also each of the
+ * various headers and fields.
+ *
+ * Scan this for the special "##<pos>" UID forms that we substituted
+ * for the "<file_name>".  Map <pos> back to the row data and
+ * increment its leak count.
+ *
+ * See https://msdn.microsoft.com/en-us/library/74kabxyx.aspx
+ *
+ * We suppress the actual crtdbg output.
+ */
+static int __cdecl report_hook(int nRptType, char *szMsg, int *retVal)
+{
+       static int hook_result = TRUE; /* FALSE to get stock dump; TRUE to suppress. */
+       unsigned int pos;
+
+       *retVal = 0; /* do not invoke debugger */
+
+       if ((szMsg[0] != '#') || (szMsg[1] != '#'))
+               return hook_result;
+
+       if (sscanf(&szMsg[2], "%08lx", &pos) < 1)
+               return hook_result;
+       if (pos >= g_cs_ins)
+               return hook_result;
+
+       if (g_transient_leaks_since_mark) {
+               if (g_cs_rows[pos].count_allocs == g_cs_rows[pos].count_allocs_at_last_checkpoint)
+                       return hook_result;
+       }
+
+       g_cs_rows[pos].transient_count_leaks++;
+
+       if (g_cs_rows[pos].transient_count_leaks == 1)
+               g_transient_count_dedup_leaks++;
+
+       g_transient_count_total_leaks++;
+
+       return hook_result;
+}
+
+/**
+ * Write leak data to all of the various places we need.
+ * We force the caller to sprintf() the message first
+ * because we want to avoid fprintf() because it allocs.
+ */
+static void my_output(const char *buf)
+{
+       fwrite(buf, strlen(buf), 1, stderr);
+       OutputDebugString(buf);
+}
+
+/**
+ * For each row with leaks, dump a stacktrace for it.
+ */
+static void dump_summary(const char *label)
+{
+       unsigned int k;
+       char buf[10 * 1024];
+
+       if (g_transient_count_total_leaks == 0)
+               return;
+
+       fflush(stdout);
+       fflush(stderr);
+       my_output("\n");
+
+       if (g_limit_reached) {
+               sprintf(buf,
+                               "LEAK SUMMARY: de-dup row table[%d] filled. Increase MY_ROW_LIMIT.\n",
+                               MY_ROW_LIMIT);
+               my_output(buf);
+       }
+
+       if (!label)
+               label = "";
+
+       if (g_transient_leaks_since_mark) {
+               sprintf(buf, "LEAK CHECKPOINT %d: leaks %d unique %d: %s\n",
+                               g_checkpoint_id, g_transient_count_total_leaks, g_transient_count_dedup_leaks, label);
+               my_output(buf);
+       } else {
+               sprintf(buf, "LEAK SUMMARY: TOTAL leaks %d de-duped %d: %s\n",
+                               g_transient_count_total_leaks, g_transient_count_dedup_leaks, label);
+               my_output(buf);
+       }
+       my_output("\n");
+
+       for (k = 0; k < g_cs_ins; k++) {
+               if (g_cs_rows[k].transient_count_leaks > 0) {
+                       sprintf(buf, "LEAK: %s leaked %d of %d times:\n",
+                                       g_cs_rows[k].uid.uid,
+                                       g_cs_rows[k].transient_count_leaks,
+                                       g_cs_rows[k].count_allocs);
+                       my_output(buf);
+
+                       if (git_win32__stack_format(
+                                       buf, sizeof(buf), &g_cs_rows[k].raw_data,
+                                       NULL, NULL) >= 0) {
+                               my_output(buf);
+                       }
+
+                       my_output("\n");
+               }
+       }
+
+       fflush(stderr);
+}
+
+void git_win32__crtdbg_stacktrace_init(void)
+{
+       InitializeCriticalSection(&g_crtdbg_stacktrace_cs);
+
+       EnterCriticalSection(&g_crtdbg_stacktrace_cs);
+
+       _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
+
+       _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE);
+       _CrtSetReportMode(_CRT_ERROR,  _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE);
+       _CrtSetReportMode(_CRT_WARN,   _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE);
+
+       _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);
+       _CrtSetReportFile(_CRT_ERROR,  _CRTDBG_FILE_STDERR);
+       _CrtSetReportFile(_CRT_WARN,   _CRTDBG_FILE_STDERR);
+
+       LeaveCriticalSection(&g_crtdbg_stacktrace_cs);
+}
+
+int git_win32__crtdbg_stacktrace__dump(
+       git_win32__crtdbg_stacktrace_options opt,
+       const char *label)
+{
+       _CRT_REPORT_HOOK old;
+       unsigned int k;
+       int r = 0;
+
+#define IS_BIT_SET(o,b) (((o) & (b)) != 0)
+
+       bool b_set_mark         = IS_BIT_SET(opt, GIT_WIN32__CRTDBG_STACKTRACE__SET_MARK);
+       bool b_leaks_since_mark = IS_BIT_SET(opt, GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_SINCE_MARK);
+       bool b_leaks_total      = IS_BIT_SET(opt, GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_TOTAL);
+       bool b_quiet            = IS_BIT_SET(opt, GIT_WIN32__CRTDBG_STACKTRACE__QUIET);
+
+       if (b_leaks_since_mark && b_leaks_total) {
+               giterr_set(GITERR_INVALID, "Cannot combine LEAKS_SINCE_MARK and LEAKS_TOTAL.");
+               return GIT_ERROR;
+       }
+       if (!b_set_mark && !b_leaks_since_mark && !b_leaks_total) {
+               giterr_set(GITERR_INVALID, "Nothing to do.");
+               return GIT_ERROR;
+       }
+
+       EnterCriticalSection(&g_crtdbg_stacktrace_cs);
+
+       if (b_leaks_since_mark || b_leaks_total) {
+               /* All variables with "transient" in the name are per-dump counters
+                * and reset before each dump.  This lets us handle checkpoints.
+                */
+               g_transient_count_total_leaks = 0;
+               g_transient_count_dedup_leaks = 0;
+               for (k = 0; k < g_cs_ins; k++) {
+                       g_cs_rows[k].transient_count_leaks = 0;
+               }
+       }
+
+       g_transient_leaks_since_mark = b_leaks_since_mark;
+
+       old = _CrtSetReportHook(report_hook);
+       _CrtDumpMemoryLeaks();
+       _CrtSetReportHook(old);
+
+       if (b_leaks_since_mark || b_leaks_total) {
+               r = g_transient_count_dedup_leaks;
+
+               if (!b_quiet)
+                       dump_summary(label);
+       }
+
+       if (b_set_mark) {
+               for (k = 0; k < g_cs_ins; k++) {
+                       g_cs_rows[k].count_allocs_at_last_checkpoint = g_cs_rows[k].count_allocs;
+               }
+
+               g_checkpoint_id++;
+       }
+
+       LeaveCriticalSection(&g_crtdbg_stacktrace_cs);
+
+       return r;
+}
+
+void git_win32__crtdbg_stacktrace_cleanup(void)
+{
+       /* At shutdown/cleanup, dump cummulative leak info
+        * with everything since startup.  This might generate
+        * extra noise if the caller has been doing checkpoint
+        * dumps, but it might also eliminate some false
+        * positives for resources previously reported during
+        * checkpoints.
+        */
+       git_win32__crtdbg_stacktrace__dump(
+               GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_TOTAL,
+               "CLEANUP");
+
+       DeleteCriticalSection(&g_crtdbg_stacktrace_cs);
+}
+
+const char *git_win32__crtdbg_stacktrace(int skip, const char *file)
+{
+       git_win32__stack__raw_data new_data;
+       git_win32__crtdbg_stacktrace__row *row;
+       const char * result = file;
+
+       if (git_win32__stack_capture(&new_data, skip+1) < 0)
+               return result;
+
+       EnterCriticalSection(&g_crtdbg_stacktrace_cs);
+
+       if (g_cs_ins < g_cs_end) {
+               row = insert_unique(&new_data);
+               result = row->uid.uid;
+       } else {
+               g_limit_reached = true;
+       }
+
+       g_count_total_allocs++;
+
+       LeaveCriticalSection(&g_crtdbg_stacktrace_cs);
+
+       return result;
+}
+#endif
diff --git a/src/win32/w32_crtdbg_stacktrace.h b/src/win32/w32_crtdbg_stacktrace.h
new file mode 100644 (file)
index 0000000..40ca60d
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+#ifndef INCLUDE_w32_crtdbg_stacktrace_h__
+#define INCLUDE_w32_crtdbg_stacktrace_h__
+
+#if defined(GIT_MSVC_CRTDBG)
+
+/**
+ * Initialize our memory leak tracking and de-dup data structures.
+ * This should ONLY be called by git_libgit2_init().
+ */
+void git_win32__crtdbg_stacktrace_init(void);
+
+/**
+ * Shutdown our memory leak tracking and dump summary data.
+ * This should ONLY be called by git_libgit2_shutdown().
+ *
+ * We explicitly call _CrtDumpMemoryLeaks() during here so
+ * that we can compute summary data for the leaks. We print
+ * the stacktrace of each unique leak.
+ *
+ * This cleanup does not happen if the app calls exit()
+ * without calling the libgit2 shutdown code.
+ *
+ * This info we print here is independent of any automatic
+ * reporting during exit() caused by _CRTDBG_LEAK_CHECK_DF.
+ * Set it in your app if you also want traditional reporting.
+ */
+void git_win32__crtdbg_stacktrace_cleanup(void);
+
+/**
+ * Checkpoint options.
+ */
+typedef enum git_win32__crtdbg_stacktrace_options {
+       /**
+        * Set checkpoint marker.
+        */
+       GIT_WIN32__CRTDBG_STACKTRACE__SET_MARK         = (1 << 0),
+
+       /**
+        * Dump leaks since last checkpoint marker.
+        * May not be combined with __LEAKS_TOTAL.
+        *
+        * Note that this may generate false positives for global TLS
+        * error state and other global caches that aren't cleaned up
+        * until the thread/process terminates.  So when using this
+        * around a region of interest, also check the final (at exit)
+        * dump before digging into leaks reported here.
+        */
+       GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_SINCE_MARK = (1 << 1),
+
+       /**
+        * Dump leaks since init.  May not be combined
+        * with __LEAKS_SINCE_MARK.
+        */
+       GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_TOTAL      = (1 << 2),
+
+       /**
+        * Suppress printing during dumps.
+        * Just return leak count.
+        */
+       GIT_WIN32__CRTDBG_STACKTRACE__QUIET            = (1 << 3),
+
+} git_win32__crtdbg_stacktrace_options;
+
+/**
+ * Checkpoint memory state and/or dump unique stack traces of
+ * current memory leaks.
+ *
+ * @return number of unique leaks (relative to requested starting
+ * point) or error.
+ */
+GIT_EXTERN(int) git_win32__crtdbg_stacktrace__dump(
+       git_win32__crtdbg_stacktrace_options opt,
+       const char *label);
+
+/**
+ * Construct stacktrace and append it to the global buffer.
+ * Return pointer to start of this string.  On any error or
+ * lack of buffer space, just return the given file buffer
+ * so it will behave as usual.
+ *
+ * This should ONLY be called by our internal memory allocations
+ * routines.
+ */
+const char *git_win32__crtdbg_stacktrace(int skip, const char *file);
+
+#endif
+#endif
diff --git a/src/win32/w32_stack.c b/src/win32/w32_stack.c
new file mode 100644 (file)
index 0000000..f1e5895
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#if defined(GIT_MSVC_CRTDBG)
+#include "Windows.h"
+#include "Dbghelp.h"
+#include "win32/posix.h"
+#include "w32_stack.h"
+#include "hash.h"
+
+/**
+ * This is supposedly defined in WinBase.h (from Windows.h) but there were linker issues.
+ */
+USHORT WINAPI RtlCaptureStackBackTrace(ULONG, ULONG, PVOID*, PULONG);
+
+static bool   g_win32_stack_initialized = false;
+static HANDLE g_win32_stack_process = INVALID_HANDLE_VALUE;
+static git_win32__stack__aux_cb_alloc  g_aux_cb_alloc  = NULL;
+static git_win32__stack__aux_cb_lookup g_aux_cb_lookup = NULL;
+
+int git_win32__stack__set_aux_cb(
+       git_win32__stack__aux_cb_alloc cb_alloc,
+       git_win32__stack__aux_cb_lookup cb_lookup)
+{
+       g_aux_cb_alloc = cb_alloc;
+       g_aux_cb_lookup = cb_lookup;
+
+       return 0;
+}
+
+void git_win32__stack_init(void)
+{
+       if (!g_win32_stack_initialized) {
+               g_win32_stack_process = GetCurrentProcess();
+               SymSetOptions(SYMOPT_LOAD_LINES);
+               SymInitialize(g_win32_stack_process, NULL, TRUE);
+               g_win32_stack_initialized = true;
+       }
+}
+
+void git_win32__stack_cleanup(void)
+{
+       if (g_win32_stack_initialized) {
+               SymCleanup(g_win32_stack_process);
+               g_win32_stack_process = INVALID_HANDLE_VALUE;
+               g_win32_stack_initialized = false;
+       }
+}
+
+int git_win32__stack_capture(git_win32__stack__raw_data *pdata, int skip)
+{
+       if (!g_win32_stack_initialized) {
+               giterr_set(GITERR_INVALID, "git_win32_stack not initialized.");
+               return GIT_ERROR;
+       }
+
+       memset(pdata, 0, sizeof(*pdata));
+       pdata->nr_frames = RtlCaptureStackBackTrace(
+               skip+1, GIT_WIN32__STACK__MAX_FRAMES, pdata->frames, NULL);
+
+       /* If an "aux" data provider was registered, ask it to capture
+        * whatever data it needs and give us an "aux_id" to it so that
+        * we can refer to it later when reporting.
+        */
+       if (g_aux_cb_alloc)
+               (g_aux_cb_alloc)(&pdata->aux_id);
+
+       return 0;
+}
+
+int git_win32__stack_compare(
+       git_win32__stack__raw_data *d1,
+       git_win32__stack__raw_data *d2)
+{
+       return memcmp(d1, d2, sizeof(d1));
+}
+
+int git_win32__stack_format(
+       char *pbuf, int buf_len,
+       const git_win32__stack__raw_data *pdata,
+       const char *prefix, const char *suffix)
+{
+#define MY_MAX_FILENAME 255
+
+       /* SYMBOL_INFO has char FileName[1] at the end.  The docs say to
+        * to malloc it with extra space for your desired max filename.
+        * We use a union to do the same thing without mallocing.
+        */
+       struct {
+               SYMBOL_INFO symbol;
+               char extra[MY_MAX_FILENAME + 1];
+       } s;
+
+       IMAGEHLP_LINE64 line;
+       int buf_used = 0;
+       unsigned int k;
+       char detail[MY_MAX_FILENAME * 2]; /* filename plus space for function name and formatting */
+       int detail_len;
+
+       if (!g_win32_stack_initialized) {
+               giterr_set(GITERR_INVALID, "git_win32_stack not initialized.");
+               return GIT_ERROR;
+       }
+
+       if (!prefix)
+               prefix = "\t";
+       if (!suffix)
+               suffix = "\n";
+
+       memset(pbuf, 0, buf_len);
+
+       memset(&s, 0, sizeof(s));
+       s.symbol.MaxNameLen = MY_MAX_FILENAME;
+       s.symbol.SizeOfStruct = sizeof(SYMBOL_INFO);
+
+       memset(&line, 0, sizeof(line));
+       line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
+
+       for (k=0; k < pdata->nr_frames; k++) {
+               DWORD64 frame_k = (DWORD64)pdata->frames[k];
+               DWORD dwUnused;
+
+               if (SymFromAddr(g_win32_stack_process, frame_k, 0, &s.symbol) &&
+                       SymGetLineFromAddr64(g_win32_stack_process, frame_k, &dwUnused, &line)) {
+                       const char *pslash;
+                       const char *pfile;
+
+                       pslash = strrchr(line.FileName, '\\');
+                       pfile = ((pslash) ? (pslash+1) : line.FileName);
+                       p_snprintf(detail, sizeof(detail), "%s%s:%d> %s%s",
+                                          prefix, pfile, line.LineNumber, s.symbol.Name, suffix);
+               } else {
+                       /* This happens when we cross into another module.
+                        * For example, in CLAR tests, this is typically
+                        * the CRT startup code.  Just print an unknown
+                        * frame and continue.
+                        */
+                       p_snprintf(detail, sizeof(detail), "%s??%s", prefix, suffix);
+               }
+               detail_len = strlen(detail);
+
+               if (buf_len < (buf_used + detail_len + 1)) {
+                       /* we don't have room for this frame in the buffer, so just stop. */
+                       break;
+               }
+
+               memcpy(&pbuf[buf_used], detail, detail_len);
+               buf_used += detail_len;
+       }
+
+       /* If an "aux" data provider was registered, ask it to append its detailed
+        * data to the end of ours using the "aux_id" it gave us when this de-duped
+        * item was created.
+        */
+       if (g_aux_cb_lookup)
+               (g_aux_cb_lookup)(pdata->aux_id, &pbuf[buf_used], (buf_len - buf_used - 1));
+
+       return GIT_OK;
+}
+
+int git_win32__stack(
+       char * pbuf, int buf_len,
+       int skip,
+       const char *prefix, const char *suffix)
+{
+       git_win32__stack__raw_data data;
+       int error;
+
+       if ((error = git_win32__stack_capture(&data, skip)) < 0)
+               return error;
+       if ((error = git_win32__stack_format(pbuf, buf_len, &data, prefix, suffix)) < 0)
+               return error;
+       return 0;
+}
+
+#endif
diff --git a/src/win32/w32_stack.h b/src/win32/w32_stack.h
new file mode 100644 (file)
index 0000000..b457d2e
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#ifndef INCLUDE_w32_stack_h__
+#define INCLUDE_w32_stack_h__
+
+#if defined(GIT_MSVC_CRTDBG)
+
+/**
+ * This type defines a callback to be used to augment a C stacktrace
+ * with "aux" data. This can be used, for example, to allow LibGit2Sharp
+ * (or other interpreted consumer libraries) to give us C# stacktrace
+ * data for the PInvoke.
+ *
+ * This callback will be called during crtdbg-instrumented allocs.
+ *
+ * @param aux_id [out] A returned "aux_id" representing a unique
+ * (de-duped at the C# layer) stacktrace.
+ */
+typedef void (*git_win32__stack__aux_cb_alloc)(unsigned int *aux_id);
+
+/**
+ * This type defines a callback to be used to augment the output of
+ * a stacktrace.  This will be used to request the C# layer format
+ * the C# stacktrace associated with "aux_id" into the provided
+ * buffer.
+ *
+ * This callback will be called during leak reporting.
+ *
+ * @param aux_id The "aux_id" key associated with a stacktrace.
+ * @param aux_msg A buffer where a formatted message should be written.
+ * @param aux_msg_len The size of the buffer.
+ */
+typedef void (*git_win32__stack__aux_cb_lookup)(unsigned int aux_id, char *aux_msg, unsigned int aux_msg_len);
+
+/**
+ * Register an "aux" data provider to augment our C stacktrace data.
+ *
+ * This can be used, for example, to allow LibGit2Sharp (or other
+ * interpreted consumer libraries) to give us the C# stacktrace of
+ * the PInvoke.
+ *
+ * If you choose to use this feature, it should be registered during
+ * initialization and not changed for the duration of the process.
+ */
+GIT_EXTERN(int) git_win32__stack__set_aux_cb(
+       git_win32__stack__aux_cb_alloc cb_alloc,
+       git_win32__stack__aux_cb_lookup cb_lookup);
+
+/**
+ * Maximum number of stackframes to record for a
+ * single stacktrace.
+ */
+#define GIT_WIN32__STACK__MAX_FRAMES 30
+
+/**
+ * Wrapper containing the raw unprocessed stackframe
+ * data for a single stacktrace and any "aux_id".
+ */
+typedef struct {
+       void *frames[GIT_WIN32__STACK__MAX_FRAMES];
+       unsigned int nr_frames;
+       unsigned int aux_id;
+} git_win32__stack__raw_data;
+
+
+/**
+ * Load symbol table data.  This should be done in the primary
+ * thread at startup (under a lock if there are other threads
+ * active).
+ */
+void git_win32__stack_init(void);
+
+/**
+ * Cleanup symbol table data.  This should be done in the
+ * primary thead at shutdown (under a lock if there are other
+ * threads active).
+ */
+void git_win32__stack_cleanup(void);
+
+
+/**
+ * Capture raw stack trace data for the current process/thread.
+ *
+ * @param skip Number of initial frames to skip.  Pass 0 to
+ * begin with the caller of this routine. Pass 1 to begin
+ * with its caller.  And so on.
+ */
+int git_win32__stack_capture(git_win32__stack__raw_data *pdata, int skip);
+
+/**
+ * Compare 2 raw stacktraces with the usual -1,0,+1 result.
+ * This includes any "aux_id" values in the comparison, so that
+ * our de-dup is also "aux" context relative.
+ */
+int git_win32__stack_compare(
+       git_win32__stack__raw_data *d1,
+       git_win32__stack__raw_data *d2);
+
+/**
+ * Format raw stacktrace data into buffer WITHOUT using any mallocs.
+ *
+ * @param prefix String written before each frame; defaults to "\t".
+ * @param suffix String written after each frame; defaults to "\n".
+ */
+int git_win32__stack_format(
+       char *pbuf, int buf_len,
+       const git_win32__stack__raw_data *pdata,
+       const char *prefix, const char *suffix);
+
+/**
+ * Convenience routine to capture and format stacktrace into
+ * a buffer WITHOUT using any mallocs.  This is primarily a
+ * wrapper for testing.
+ *
+ * @param skip Number of initial frames to skip. Pass 0 to
+ * begin with the caller of this routine.  Pass 1 to begin
+ * with its caller.  And so on.
+ * @param prefix String written before each frame; defaults to "\t".
+ * @param suffix String written after each frame; defaults to "\n".
+ */
+int git_win32__stack(
+       char * pbuf, int buf_len,
+       int skip,
+       const char *prefix, const char *suffix);
+
+#endif /* GIT_MSVC_CRTDBG */
+#endif /* INCLUDE_w32_stack_h__ */
index ae582d1cb4abc76de06e4e0cd3e195e6d9d72928..aaeeb78106c8a9b4b22973408e6954f6d5ea9fa6 100644 (file)
@@ -142,9 +142,28 @@ void _cl_trace_cb__event_handler(
        switch (ev) {
        case CL_TRACE__SUITE_BEGIN:
                git_trace(GIT_TRACE_TRACE, "\n\n%s\n%s: Begin Suite", HR, suite_name);
+#if 0 && defined(GIT_MSVC_CRTDBG)
+               git_win32__crtdbg_stacktrace__dump(
+                       GIT_WIN32__CRTDBG_STACKTRACE__SET_MARK,
+                       suite_name);
+#endif
                break;
 
        case CL_TRACE__SUITE_END:
+#if 0 && defined(GIT_MSVC_CRTDBG)
+               /* As an example of checkpointing, dump leaks within this suite.
+                * This may generate false positives for things like the global
+                * TLS error state and maybe the odb cache since they aren't
+                * freed until the global shutdown and outside the scope of this
+                * set of tests.
+                *
+                * This may under-report if the test itself uses a checkpoint.
+                * See tests/trace/windows/stacktrace.c
+                */
+               git_win32__crtdbg_stacktrace__dump(
+                       GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_SINCE_MARK,
+                       suite_name);
+#endif
                git_trace(GIT_TRACE_TRACE, "\n\n%s: End Suite\n%s", suite_name, HR);
                break;
 
index 56326da1c1035d01d78df54c3fd7f677420774bc..f67c8ffbc73dbabd7140eac18dd981f8afc1baf2 100644 (file)
@@ -1,10 +1,3 @@
-
-#if defined(GIT_MSVC_CRTDBG)
-/* Enable MSVC CRTDBG memory leak reporting.  See src/util.h for details. */
-#include <stdlib.h>
-#include <crtdbg.h>
-#endif
-
 #include "clar_libgit2.h"
 #include "clar_libgit2_trace.h"
 
@@ -16,18 +9,6 @@ int main(int argc, char *argv[])
 {
        int res;
 
-#if defined(GIT_MSVC_CRTDBG)
-       _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
-
-       _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE);
-       _CrtSetReportMode(_CRT_ERROR,  _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE);
-       _CrtSetReportMode(_CRT_WARN,   _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE);
-
-       _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);
-       _CrtSetReportFile(_CRT_ERROR,  _CRTDBG_FILE_STDERR);
-       _CrtSetReportFile(_CRT_WARN,   _CRTDBG_FILE_STDERR);
-#endif
-
        clar_test_init(argc, argv);
 
        git_libgit2_init();
diff --git a/tests/trace/windows/stacktrace.c b/tests/trace/windows/stacktrace.c
new file mode 100644 (file)
index 0000000..c00c1b7
--- /dev/null
@@ -0,0 +1,151 @@
+#include "clar_libgit2.h"
+#include "win32/w32_stack.h"
+
+#if defined(GIT_MSVC_CRTDBG)
+static void a(void)
+{
+       char buf[10000];
+
+       cl_assert(git_win32__stack(buf, sizeof(buf), 0, NULL, NULL) == 0);
+
+#if 0
+       fprintf(stderr, "Stacktrace from [%s:%d]:\n%s\n", __FILE__, __LINE__, buf);
+#endif
+}
+
+static void b(void)
+{
+       a();
+}
+
+static void c(void)
+{
+       b();
+}
+#endif
+
+void test_trace_windows_stacktrace__basic(void)
+{
+#if defined(GIT_MSVC_CRTDBG)
+       c();
+#endif
+}
+
+
+void test_trace_windows_stacktrace__leaks(void)
+{
+#if defined(GIT_MSVC_CRTDBG)
+       void * p1;
+       void * p2;
+       void * p3;
+       void * p4;
+       int before, after;
+       int leaks;
+       int error;
+
+       /* remember outstanding leaks due to set setup
+        * and set mark/checkpoint.
+        */
+       before = git_win32__crtdbg_stacktrace__dump(
+               GIT_WIN32__CRTDBG_STACKTRACE__QUIET |
+               GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_TOTAL |
+               GIT_WIN32__CRTDBG_STACKTRACE__SET_MARK,
+               NULL);
+
+       p1 = git__malloc(5);
+       leaks = git_win32__crtdbg_stacktrace__dump(
+               GIT_WIN32__CRTDBG_STACKTRACE__QUIET |
+               GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_SINCE_MARK,
+               "p1");
+       cl_assert((leaks == 1));
+
+       p2 = git__malloc(5);
+       leaks = git_win32__crtdbg_stacktrace__dump(
+               GIT_WIN32__CRTDBG_STACKTRACE__QUIET |
+               GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_SINCE_MARK,
+               "p1,p2");
+       cl_assert((leaks == 2));
+
+       p3 = git__malloc(5);
+       leaks = git_win32__crtdbg_stacktrace__dump(
+               GIT_WIN32__CRTDBG_STACKTRACE__QUIET |
+               GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_SINCE_MARK,
+               "p1,p2,p3");
+       cl_assert((leaks == 3));
+
+       git__free(p2);
+       leaks = git_win32__crtdbg_stacktrace__dump(
+               GIT_WIN32__CRTDBG_STACKTRACE__QUIET |
+               GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_SINCE_MARK,
+               "p1,p3");
+       cl_assert((leaks == 2));
+
+       /* move the mark. only new leaks should appear afterwards */
+       error = git_win32__crtdbg_stacktrace__dump(
+               GIT_WIN32__CRTDBG_STACKTRACE__SET_MARK,
+               NULL);
+       cl_assert((error == 0));
+
+       leaks = git_win32__crtdbg_stacktrace__dump(
+               GIT_WIN32__CRTDBG_STACKTRACE__QUIET |
+               GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_SINCE_MARK,
+               "not_p1,not_p3");
+       cl_assert((leaks == 0));
+
+       p4 = git__malloc(5);
+       leaks = git_win32__crtdbg_stacktrace__dump(
+               GIT_WIN32__CRTDBG_STACKTRACE__QUIET |
+               GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_SINCE_MARK,
+               "p4,not_p1,not_p3");
+       cl_assert((leaks == 1));
+
+       git__free(p1);
+       git__free(p3);
+       leaks = git_win32__crtdbg_stacktrace__dump(
+               GIT_WIN32__CRTDBG_STACKTRACE__QUIET |
+               GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_SINCE_MARK,
+               "p4");
+       cl_assert((leaks == 1));
+
+       git__free(p4);
+       leaks = git_win32__crtdbg_stacktrace__dump(
+               GIT_WIN32__CRTDBG_STACKTRACE__QUIET |
+               GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_SINCE_MARK,
+               "end");
+       cl_assert((leaks == 0));
+
+       /* confirm current absolute leaks count matches beginning value. */
+       after = git_win32__crtdbg_stacktrace__dump(
+               GIT_WIN32__CRTDBG_STACKTRACE__QUIET |
+               GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_TOTAL,
+               "total");
+       cl_assert((before == after));
+#endif
+}
+
+#if defined(GIT_MSVC_CRTDBG)
+static void aux_cb_alloc__1(unsigned int *aux_id)
+{
+       static unsigned int aux_counter = 0;
+
+       *aux_id = aux_counter++;
+}
+
+static void aux_cb_lookup__1(unsigned int aux_id, char *aux_msg, unsigned int aux_msg_len)
+{
+       p_snprintf(aux_msg, aux_msg_len, "\tQQ%08x\n", aux_id);
+}
+
+#endif
+
+void test_trace_windows_stacktrace__aux1(void)
+{
+#if defined(GIT_MSVC_CRTDBG)
+       git_win32__stack__set_aux_cb(aux_cb_alloc__1, aux_cb_lookup__1);
+       c();
+       c();
+       c();
+       c();
+       git_win32__stack__set_aux_cb(NULL, NULL);
+#endif
+}