diff --git a/LibGit2Sharp.Tests/CrtDbgFixture.cs b/LibGit2Sharp.Tests/CrtDbgFixture.cs new file mode 100644 index 000000000..78a789423 --- /dev/null +++ b/LibGit2Sharp.Tests/CrtDbgFixture.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using LibGit2Sharp.Tests.TestHelpers; +using Xunit; +using LibGit2Sharp.Core; + +#if LEAKS_CRTDBG +namespace LibGit2Sharp.Tests +{ + public class CrtDbgFixture : BaseFixture + { + [Fact] + public void CanDetectLeak() + { + var path = SandboxStandardTestRepoGitDir(); + int count_before = CrtDbg.Dump(CrtDbg.CrtDbgDumpFlags.SET_MARK, "before"); + using (var repo = new Repository(path)) + { + // While the repo safe-handle is holding an actual C repo pointer which + // contains an unknown (to us) number of pointers within it. So we just + // confirm that there are some new yet-to-be freed items. + int count_during = CrtDbg.Dump( + CrtDbg.CrtDbgDumpFlags.LEAKS_SINCE_MARK | CrtDbg.CrtDbgDumpFlags.QUIET, + "during"); + Assert.True(count_during > count_before); + } + // When the repo is released, our memory count should return to what it was. + // Note we may have to force a GC. + int count_after = CrtDbg.Dump( + CrtDbg.CrtDbgDumpFlags.LEAKS_SINCE_MARK | CrtDbg.CrtDbgDumpFlags.QUIET, + "after"); + Assert.Equal(count_after, count_before); + } + } +} +#endif diff --git a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj index 296831989..a1d8d5b2b 100644 --- a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj +++ b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj @@ -1,4 +1,4 @@ - + @@ -61,6 +61,7 @@ + diff --git a/LibGit2Sharp/Core/CrtDbg.cs b/LibGit2Sharp/Core/CrtDbg.cs new file mode 100644 index 000000000..7abccf82a --- /dev/null +++ b/LibGit2Sharp/Core/CrtDbg.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; + +#if LEAKS_CRTDBG +namespace LibGit2Sharp.Core +{ + /// + /// Class CrtDbg is used to augment the MSVC_CRTDBG memory leak reporting in libgit2 + /// by providing stack trace information for the C# stack at the point of the PInvoke. + /// + public static class CrtDbg + { + private static readonly object _lock = new object(); + /// + /// This dictionary maps a unique C# formatted stack trace "aux_data" to a unique "aux_id". + /// Yes, this looks backwards. We use the formatted stack trace as the key. Think of this + /// as a SQL stable with 2 indexed columns. During allocs, we compute the C# portion of the + /// stack trace of the alloc and use that unique key to add/get the associated "aux_id" + /// and give this back to C (because it does not want to marshall the C# stack trace back + /// on every alloc). Later, during leak reporting, the C code will ask us for the "aux_data" + /// for an "aux_id" and we do a reverse lookup. Reverse lookups are linear, but only + /// happen at the end for actual leaks. + /// + private static Dictionary _dict = new Dictionary(); + private static readonly Encoding _enc = new UTF8Encoding(false, false); + private static NativeMethods.git_win32__stack__aux_cb_alloc _cb_alloc = cb_alloc; + private static NativeMethods.git_win32__stack__aux_cb_lookup _cb_lookup = cb_lookup; + + /// + /// Uniquely insert into the dictionary. + /// + /// Formatted C# stack trace + /// Unique aux_id + private static uint AddOrGet(string key) + { + lock (_lock) + { + uint value; + if (!_dict.TryGetValue(key, out value)) + { + value = (uint)_dict.Count + 1; // aux_id 0 is reserved + _dict.Add(key, value); + } + return value; + } + } + + /// + /// Reverse lookup on dictionary. + /// + /// aux_id + /// Formatted C# stack trace + /// true if aux_id found + private static bool ReverseLookup(uint value, out string key) + { + if (value == 0) + { + // aux_id 0 is reserved. + key = null; + return false; + } + + lock (_lock) + { + foreach (KeyValuePair p in _dict) + { + if (p.Value == value) + { + key = p.Key; + return true; + } + } + + key = null; + return false; + } + } + + /// + /// Callback used by C layer to get an "aux_id" for the current + /// C# stack context. (Internally adds data to dictionary if + /// required.0 + /// + /// + private static void cb_alloc(out uint aux_id) + { + StackTrace st = new StackTrace(1, true); + string s = ""; + for (int i = 0; i < st.FrameCount; i++) + { + StackFrame sf = st.GetFrame(i); + s += string.Format("\t\t{0}:{1}> {2}\n", + Path.GetFileName(sf.GetFileName()), + sf.GetFileLineNumber(), + sf.GetMethod()); + } + + aux_id = CrtDbg.AddOrGet(s); + } + + /// + /// Callback used by C layer to get the "aux_data" (the + /// formatted C# stacktrace) for the requested "aux_id". + /// String is converted to UTF8 and copied into the + /// provided buffer. + /// + /// + /// + /// + private static void cb_lookup(uint aux_id, IntPtr buf, uint buf_len) + { + string s; + if (!CrtDbg.ReverseLookup(aux_id, out s)) + return; + + int len_utf8 = _enc.GetByteCount(s); + if (len_utf8 == 0) + return; + + unsafe + { + fixed (char* ps = s) + { + byte* b = (byte*)buf.ToPointer(); + _enc.GetBytes(ps, len_utf8, b, (int)buf_len); + } + } + } + + /// + /// Register CRTDBG AUX callbacks. + /// + public static void SetCallbacks() + { + // We pass private static variables set to the actual functions + // because we need for the function references to not be GC'd + // while C is still using them. + NativeMethods.git_win32__stack__set_aux_cb(_cb_alloc, _cb_lookup); + } + + /// + /// Flags/options to control checkpoint dump of leaks + /// + [Flags] + public enum CrtDbgDumpFlags + { + /// + /// Checkpoint current memory state. + /// + SET_MARK = (1 << 0), + /// + /// Count and/or print leaks since last checkpoint. + /// + LEAKS_SINCE_MARK = (1 << 1), + /// + /// Count and/or print leaks since startup. + /// + LEAKS_TOTAL = (1 << 2), + /// + /// Suppress output. + /// + QUIET = (1 << 3) + }; + + /// + /// Checkpoint C memory state and/or dump current leaks. + /// + /// + /// Message to be printed with checkpoint dump. + /// Count of current leaks when a LEAK_ flag given. Otherwise 0 or error. + public static int Dump(CrtDbgDumpFlags flags, string label) + { + int r = NativeMethods.git_win32__crtdbg_stacktrace__dump(flags, label); + + return r; + } + } +} +#endif diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index 74bc91b52..a1d53ad0b 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -29,6 +29,9 @@ private sealed class LibraryLifetimeObject : CriticalFinalizerObject [MethodImpl(MethodImplOptions.NoInlining)] public LibraryLifetimeObject() { +#if LEAKS_CRTDBG + CrtDbg.SetCallbacks(); +#endif int res = git_libgit2_init(); Ensure.Int32Result(res); if (res == 1) @@ -1721,6 +1724,20 @@ internal static extern int git_treebuilder_insert( [DllImport(libgit2)] internal static extern int git_cherrypick(RepositorySafeHandle repo, GitObjectSafeHandle commit, GitCherryPickOptions options); + +#if LEAKS_CRTDBG + internal delegate void git_win32__stack__aux_cb_alloc(out uint aux_id); + internal delegate void git_win32__stack__aux_cb_lookup(uint aux_id, IntPtr buf, uint buf_len); + + [DllImport(libgit2)] + internal static extern int git_win32__stack__set_aux_cb(git_win32__stack__aux_cb_alloc cb_alloc, git_win32__stack__aux_cb_lookup cb_lookup); + + [DllImport(libgit2)] + internal static extern int git_win32__crtdbg_stacktrace__dump( + CrtDbg.CrtDbgDumpFlags flags, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string label); +#endif + } } // ReSharper restore InconsistentNaming diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 6e952be2e..1b5d62c3b 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -69,6 +69,7 @@ +