-
Notifications
You must be signed in to change notification settings - Fork 387
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
Performance improvement during run of instrumented code #172
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ namespace Coverlet.Tracker | |
public static class CoverageTracker | ||
{ | ||
private static List<Dictionary<string, Dictionary<string, int>>> _events; | ||
private static readonly StringHashSuffixComparer _stringHashSuffixComparer = new StringHashSuffixComparer(); | ||
|
||
[ThreadStatic] | ||
private static Dictionary<string, Dictionary<string, int>> t_events; | ||
|
@@ -25,7 +26,7 @@ public static void MarkExecuted(string file, string evt) | |
{ | ||
if (t_events == null) | ||
{ | ||
t_events = new Dictionary<string, Dictionary<string, int>>(); | ||
t_events = new Dictionary<string, Dictionary<string, int>>(_stringHashSuffixComparer); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You know, now that I think of it there's really no reason to store the path to the hits file as a key in a dictionary. By design, the hits file for an assembly remains the same for an entire test session. Also, the I believe this approach was added when we had multiple separate gzipped hits files because some users were experiencing considerable disk usage due to large hits files. That's no longer the case. In this case we can simply make the hits file a field and assign to it once (if it evaluates to Let me know what you think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It sounds correct to me. I'll give a try during the day. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This requires some risky changes: the same thread can receive events from different modules. In other words while the hits file is the same for a given assembly the same thread may receive markers from different assemblies. I'm going to try some minimal changes to optimize that but perhaps we should go for the safe and simple optimization in the short run, while we work on a more profound one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's currently possible for the
What happens in this case is that both See https://github.com/tonerdo/coverlet/blob/1d92ee66a1bb68629bf31022a300e57c36ce535f/src/coverlet.core/CoverageTracker.cs for the original implementation that shipped. It worked perfectly in a multiple assembly scenario, it just wasn't performant or thread safe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did a test to be sure: check https://github.com/tonerdo/coverlet/compare/master...pjanotti:test.multithread?expand=1 It does hit the exception when running the performance test. The thread static is the same in a single app domain (that even doesn't really exist on core) for a given thread, but, the same is true for any static of The loader is doing the right thing: let's say There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this happen only in a multi-threaded scenario? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, single thread too, you just need different modules on the same thread. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did an experiment to generate a "tracker type" per module and get rid of the dictionaries, it seems to be going well. It needs more testing to ensure correctness and that it doesn't break some scenarios that are working now. If you want to take a peak before I do some clean-up/polishing/fine tunning take a look here. There is a bit of trade-off and it probably it will use more memory in projects with lower coverage numbers but for the project that I tested here it consistently runs the whole test in ~19 seconds (about 6 times faster than the optimization proposed on this PR). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Took a quick peek. Seems like a good crop of changes. Will merge this in now since I'm fine with it, thanks for the change and explanations There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @tonerdo |
||
lock (_events) | ||
{ | ||
_events.Add(t_events); | ||
|
@@ -38,7 +39,7 @@ public static void MarkExecuted(string file, string evt) | |
{ | ||
if (!t_events.TryGetValue(file, out var fileEvents)) | ||
{ | ||
fileEvents = new Dictionary<string, int>(); | ||
fileEvents = new Dictionary<string, int>(_stringHashSuffixComparer); | ||
t_events.Add(file, fileEvents); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
using System.Collections.Generic; | ||
|
||
namespace Coverlet.Tracker | ||
{ | ||
internal class StringHashSuffixComparer : IEqualityComparer<string> | ||
{ | ||
public bool Equals(string x, string y) => string.Equals(x, y); | ||
|
||
public int GetHashCode(string s) | ||
{ | ||
if (s == null || s.Length == 0) | ||
return 0; | ||
|
||
// Hash calculation based on the old implementation of NameTable used in System.Xml | ||
const int SuffixLength = 8; | ||
const int Seed = 1031880390; | ||
int hashCode; | ||
unchecked | ||
{ | ||
hashCode = s.Length + Seed; | ||
int i = s.Length > SuffixLength ? s.Length - SuffixLength : 0; | ||
for (; i<s.Length; ++i) | ||
{ | ||
hashCode += (hashCode << 7) ^ s[i]; | ||
} | ||
|
||
hashCode -= hashCode >> 17; | ||
hashCode -= hashCode >> 11; | ||
hashCode -= hashCode >> 5; | ||
} | ||
|
||
return hashCode; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh shit, why didn't I think of this 😄