Skip to content
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

Memory allocation allocation tracker test helper #2082

Merged
merged 14 commits into from
Apr 12, 2022
Merged

Conversation

tocsoft
Copy link
Member

@tocsoft tocsoft commented Apr 9, 2022

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

This adds some new test helpers APIs + updates the MemoryDiagnostics class to consistently track the memory buffers per test.

Also adds 2 helpers in to the tests

  1. ValidateDisposedMemoryAllocationsAttribute - This attributes a can be applied to test classes and Fact/Theorys directly to automatically apply the memory application tracking and validation.
  2. MemoryAllocatorValidator static class can be used in places the attribute is not appropriate, i.e. inside Remote Executors.

I've also applied some fixes targeting the Decoder tests

this includes the fix from #2081 (which I will merge into this PR so we don't loose attribute of the fix) if we decide this approach is suitable...otherwise we should just merge that one.

@tocsoft
Copy link
Member Author

tocsoft commented Apr 9, 2022

@antonfirsov It would be useful if you could confirm I'm not doing anything too critical/stupid fiddling with the MemoryDiagnostics class.

Comment on lines 206 to 210
catch
{
this.Frame?.Dispose();
throw;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to do a try-catch here I believe as it is handled by the caller here:

public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(stream, nameof(stream));
using var decoder = new JpegDecoderCore(configuration, this);
return decoder.Decode<TPixel>(configuration, stream, cancellationToken);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah looks like you are right... I think I must have added it on first pass trying to find the leaky code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's very tricky because jpeg is the only decoder which has a disposable internal DecoderCore implementation :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make it so it’s doesn’t implement IDisposable?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... Considering it's only used for Frame field which is a class field for the sake of not passing it to a bunch of methods we can make it non-disposable for consistency.

If you do so, try-catch must be replaced by try-finally or Frame would leak in a no-exception scenario. Also don't forget about Identify method - it also creates a Frame field so it should have a proper try-finally too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Ace, that’s what I thought. That can be a separate PR though.

Copy link
Member

@JimBobSquarePants JimBobSquarePants left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah this is fantastic! Exactly what we need. 😍

Copy link
Contributor

@br3aker br3aker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ValidateDisposedMemoryAllocationsAttribute is a blessing, also cool that current coverage reports no leaks 😎

@tocsoft
Copy link
Member Author

tocsoft commented Apr 10, 2022

ValidateDisposedMemoryAllocationsAttribute is a blessing, also cool that current coverage reports no leaks 😎

no leaks (anymore) in the few tests I added it to anyway :D

There will be a need to follow up adding this to a lot more of the test... I didn't want to blow out this PR fixing leaks hiding the framework changes.

Despite this being approved I think its worth leave this one open a bit longer before merging to give anyone with more knowledge on how my changed to the MemoryDiagnostics class might impact perf.

@codecov
Copy link

codecov bot commented Apr 11, 2022

Codecov Report

Merging #2082 (136cc14) into main (136cc14) will not change coverage.
The diff coverage is n/a.

❗ Current head 136cc14 differs from pull request most recent head 558ff99. Consider uploading reports for the commit 558ff99 to get more accurate results

@@          Coverage Diff          @@
##            main   #2082   +/-   ##
=====================================
  Coverage     88%     88%           
=====================================
  Files        990     990           
  Lines      52619   52619           
  Branches    6617    6617           
=====================================
  Hits       46443   46443           
  Misses      5083    5083           
  Partials    1093    1093           
Flag Coverage Δ
unittests 88% <0%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 136cc14...558ff99. Read the comment docs.

Copy link
Member

@antonfirsov antonfirsov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to separate the AsyncLocal test-scoped validation from the global MemoryDiagnostics.TotalUndisposedAllocationCount counter.

/// <summary>
/// Gets a value indicating the total number of memory resource objects leaked to the finalizer.
/// </summary>
public static int TotalUndisposedAllocationCount => totalUndisposedAllocationCount;
public static int TotalUndisposedAllocationCount => Current.TotalUndisposedAllocationCount;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TotalUndisposedAllocationCount was intended to report global allocation count. If I get the docs of AsyncLocal<T> right, the PR changes the semantics, so it becomes specific to an asynchronous execution flow, and will reset outside of the flow. I don't think this public behavior change is beneficial for the users of MemoryDiagnostics.TotalUndisposedAllocationCount . We need a separate API for MemoryAllocatorValidator.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It way its setup it will only report the local context when its been overwritten in tests (internal api) by default the backing fields are backs by the MemoryDiagnostics.Default static instance which will be used in production usage outside of testing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now, I missed the purpose of the branching. Though probably still better to keep the internal API as minimalistic and non-intrusive as possible.

Comment on lines +68 to +69
using var decompressedBuffer = spectralConverterGray.GetPixelBuffer(CancellationToken.None);
CopyImageBytesToBuffer(buffer, decompressedBuffer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we also have separate PR-s for the Jpeg+Tiff+WebP+Bmp fixes or just for PNG?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all the decoder facing fixes are actually in here...but we will need a set of follow up PRs to get full coverage.

{
public static class MemoryAllocatorValidator
{
public static IDisposable MonitorAllocations(int max = 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public static IDisposable MonitorAllocations(int max = 0)
public static TestMemoryAllocatorDisposable MonitorAllocations(int max = 0)

No point for TestMemoryAllocatorDisposable being a struct, if we box it into IDisposable in the end.

return new TestMemoryAllocatorDisposable(max);
}

public static void ValidateAllocation(int max = 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a use case for max > 0? Do we really want to allow any test having leak-validation enabled to leak some memory? Or am I misunderstanding the purpose?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no really I can't remember why I added it really... it was probably a bad idea really, consider it gone.

@antonfirsov
Copy link
Member

What I would do is to expose (for now internal) BufferAllocated / BufferDestroyed events and do something like:

internal static void IncrementTotalUndisposedAllocationCount()
{
    Interlocked.Increment(ref totalUndisposedAllocationCount);
    bufferAllocatedBackingDelegate?.Invoke();
}

Then move the AsyncLocal utility to test code and control it by subscribing to the events above.

@tocsoft
Copy link
Member Author

tocsoft commented Apr 11, 2022

What I would do is to expose (for now internal) BufferAllocated / BufferDestroyed events and do something like:

internal static void IncrementTotalUndisposedAllocationCount()
{
    Interlocked.Increment(ref totalUndisposedAllocationCount);
    bufferAllocatedBackingDelegate?.Invoke();
}

Then move the AsyncLocal utility to test code and control it by subscribing to the events above.

I think I like that better its less impactful on the prod code.... i'll have a play and see how feels.

Copy link
Member

@antonfirsov antonfirsov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better this way I think, thanks! Two minor remarks.


public class TestMemoryDiagnostics : IDisposable
{
public int TotalAllocated { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be unused.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was pre-empting being useful for tests that want to track/verify total number of allocated buffers not just retained buffers... but your right currently not being used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm generally reluctant to add unused/untested features (= stuff to understand, remember and maintain) even to test utility code. We can easily add them later when actual need appears.

But it's really a nit, so I'm not going to block the PR on these two points.


public int TotalRemainingAllocated { get; set; }

public void Validate(int expectedAllocationCount)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expectedAllocationCount seems to be always 0.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our current tests your are right, but I was envisioning use cases where we might want to want to validate before all buffers are returned and this is to allow for testing a specific/expected number of buffers are currently allocated still.

@tocsoft tocsoft merged commit 7db4792 into main Apr 12, 2022
@tocsoft tocsoft deleted the sw/allocation-tracker branch April 12, 2022 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants