Skip to content

Add C# stacktrace info to CRTDBG leak reporting. #1125

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions LibGit2Sharp.Tests/CrtDbgFixture.cs
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\xunit.runner.visualstudio.2.0.0-rc1-build1030\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\packages\xunit.runner.visualstudio.2.0.0-rc1-build1030\build\net20\xunit.runner.visualstudio.props')" />
<PropertyGroup>
Expand Down Expand Up @@ -61,6 +61,7 @@
<Compile Include="ArchiveTarFixture.cs" />
<Compile Include="CheckoutFixture.cs" />
<Compile Include="CherryPickFixture.cs" />
<Compile Include="CrtDbgFixture.cs" />
<Compile Include="DescribeFixture.cs" />
<Compile Include="FileHistoryFixture.cs" />
<Compile Include="FilterFixture.cs" />
Expand Down
182 changes: 182 additions & 0 deletions LibGit2Sharp/Core/CrtDbg.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
public static class CrtDbg
{
private static readonly object _lock = new object();
/// <summary>
/// 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.
/// </summary>
private static Dictionary<string, uint> _dict = new Dictionary<string, uint>();
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;

/// <summary>
/// Uniquely insert into the dictionary.
/// </summary>
/// <param name="key">Formatted C# stack trace</param>
/// <returns>Unique aux_id</returns>
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;
}
}

/// <summary>
/// Reverse lookup on dictionary.
/// </summary>
/// <param name="value">aux_id</param>
/// <param name="key">Formatted C# stack trace</param>
/// <returns>true if aux_id found</returns>
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<string, uint> p in _dict)
{
if (p.Value == value)
{
key = p.Key;
return true;
}
}

key = null;
return false;
}
}

/// <summary>
/// Callback used by C layer to get an "aux_id" for the current
/// C# stack context. (Internally adds data to dictionary if
/// required.0
/// </summary>
/// <param name="aux_id"></param>
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);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="aux_id"></param>
/// <param name="buf"></param>
/// <param name="buf_len"></param>
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);
}
}
}

/// <summary>
/// Register CRTDBG AUX callbacks.
/// </summary>
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);
}

/// <summary>
/// Flags/options to control checkpoint dump of leaks
/// </summary>
[Flags]
public enum CrtDbgDumpFlags
{
/// <summary>
/// Checkpoint current memory state.
/// </summary>
SET_MARK = (1 << 0),
/// <summary>
/// Count and/or print leaks since last checkpoint.
/// </summary>
LEAKS_SINCE_MARK = (1 << 1),
/// <summary>
/// Count and/or print leaks since startup.
/// </summary>
LEAKS_TOTAL = (1 << 2),
/// <summary>
/// Suppress output.
/// </summary>
QUIET = (1 << 3)
};

/// <summary>
/// Checkpoint C memory state and/or dump current leaks.
/// </summary>
/// <param name="flags"></param>
/// <param name="label">Message to be printed with checkpoint dump.</param>
/// <returns>Count of current leaks when a LEAK_ flag given. Otherwise 0 or error.</returns>
public static int Dump(CrtDbgDumpFlags flags, string label)
{
int r = NativeMethods.git_win32__crtdbg_stacktrace__dump(flags, label);

return r;
}
}
}
#endif
17 changes: 17 additions & 0 deletions LibGit2Sharp/Core/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions LibGit2Sharp/LibGit2Sharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
<Compile Include="CommitOptions.cs" />
<Compile Include="CommitSortStrategies.cs" />
<Compile Include="CompareOptions.cs" />
<Compile Include="Core\CrtDbg.cs" />
<Compile Include="Core\FileHistory.cs" />
<Compile Include="Core\GitFetchOptions.cs" />
<Compile Include="Core\GitPushUpdate.cs" />
Expand Down