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

Resolve some concurrency issues #1200

Merged
merged 23 commits into from
Apr 3, 2020
Merged

Resolve some concurrency issues #1200

merged 23 commits into from
Apr 3, 2020

Conversation

mattleibow
Copy link
Contributor

@mattleibow mattleibow commented Mar 29, 2020

Description of Change

Resolve some concurrency issues. In some cases the construction and/or destruction overlap and this causes invalid states. This PR improves the locking to avoid this, but still preserves the read and write level of locking.

It also includes a few other improvements, such as making sure objects that require their "parent" object to not be GC'ed add a reference to it. In most cases it is simple, such as a canvas being owned by a surface. But, in the case of a pixmap, the ownership is a little different in that they are copies, but actually require the parent object to stick around. So, the ownership is "reversed".

There is also a few other tweaks to improve general GC reliability/interop performance.

Bugs Fixed

API Changes

None.

Behavioral Changes

Less crashes due to correct object management.

PR Checklist

  • Has tests (if omitted, state reason in description)
  • Rebased on top of master at time of PR
  • Changes adhere to coding standard
  • Updated documentation

internal static readonly ConcurrentDictionary<Type, ConstructorInfo> constructors = new ConcurrentDictionary<Type, ConstructorInfo> ();
internal static readonly Dictionary<IntPtr, WeakReference> instances = new Dictionary<IntPtr, WeakReference> ();

internal static readonly ReaderWriterLockSlim instancesLock = new ReaderWriterLockSlim ();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Instead of pure locking and concurrent dictionaries, just use a reader/writer lock as that is what we actually want. And, when reading and we find that there is no handle, then we can upgrade in place.

internal static class HandleDictionary
{
private static readonly Type IntPtrType = typeof (IntPtr);
private static readonly Type BoolType = typeof (bool);
Copy link
Contributor Author

@mattleibow mattleibow Mar 29, 2020

Choose a reason for hiding this comment

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

Caching on .NET Core is a major perf boost. Major as in 3-4x, but still in ns.

}

var type = typeof (TSkiaImplementation);
var constructor = constructors.GetOrAdd (type, t => GetConstructor (t));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Anonymous delegates are cached rather than direct method references.

Comment on lines 13 to 14
internal ConcurrentDictionary<IntPtr, SKObject> OwnedObjects =>
ownedObjects ??= new ConcurrentDictionary<IntPtr, SKObject> ();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Lazy-instantiate this as it is hardly ever used. Typically for documents and some for some streams, codecs and typefaces.

@@ -12,18 +12,13 @@
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="XunitXml.TestLogger" Version="2.1.26" />
<PackageReference Include="xunit.assemblyfixture" Version="2.0.3" />
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This never worked for .NET Core anyway. Use the xUnit-approved way from the xUnit samples.

Copy link
Contributor

@ziriax ziriax left a comment

Choose a reason for hiding this comment

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

I might be missing something, but at first sight getting OwnedObjects and KeepAliveObjects is not thread-safe, and can suffer from a race condition?

You could use double checked locking:
https://en.m.wikipedia.org/wiki/Double-checked_locking

Or maybe just Interlocked.CompareExchange

I could be wrong, I just briefly looked at the code, maybe some higher level locking is performed

@mattleibow
Copy link
Contributor Author

Thanks for the review!

Technically, the objects should only be referenced from a single thread at any given time. However, I will add some locking to prevent any future issues.

- Lazy<T> still allocates, so not useful
- Technically not needed due to objects not being thread-safe
return SkiaApi.sk_bitmap_peek_pixels (Handle, pixmap.Handle);
var result = SkiaApi.sk_bitmap_peek_pixels (Handle, pixmap.Handle);
if (result)
Referenced (pixmap, this);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The GC sometimes likes to collect the bitmap in use by a pixmap. This case is important because the bitmap OWNS the pixmap, and the reference is not counted. So a disposal destroys the pixmap.

@@ -251,6 +251,7 @@ public void SaveTo (Stream target)
} finally {
ArrayPool<byte>.Shared.Return (buffer);
}
GC.KeepAlive (this);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just prevent the GC from going to town, because we are not actually using the object, but a value from one of the properties.

#if THROW_OBJECT_EXCEPTIONS
internal static readonly ConcurrentBag<Exception> exceptions = new ConcurrentBag<Exception> ();
#endif
private readonly object locker = new object ();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A single locker as this doesn't need to be too complicated.

@mattleibow
Copy link
Contributor Author

I think this is it. Fixed some other bugs. Concurrency due to construction and destruction, but also when the GC starts to collect things too soon.

@mattleibow
Copy link
Contributor Author

I have pushed what I think is now a good version to the preview feed: https://aka.ms/skiasharp-eap/index.json
Check out the PR version: 1.68.2-pr.1200.20

@Gillibald
Copy link
Contributor

Will have a look shortly 👍

@mattleibow
Copy link
Contributor Author

@Gillibald have you been able to have a look and see if things are better? I am thinking of just merging and pushing to nuget.org so that the mapsui folks can check. But, I just want to make sure that I get your opinion/hands on the pie here.

@Gillibald
Copy link
Contributor

Did not have time today. Could do tomorrow morning. In a few hours 😀. My current test creates and disposes millions of SKObjects. So minimal improvements should come out clearly. Mostly text related objects.

@mattleibow mattleibow closed this Apr 2, 2020
@mattleibow mattleibow reopened this Apr 2, 2020
@mattleibow
Copy link
Contributor Author

Ah, millions? Have you noticed any random crashes or weird behavior at all 😄

The issues that mostly come up is with concurrent execution because objects were being created with the same handle as an object being destroyed. As a result, the managed side got all mixed up as it was busy nuking things and then it is added back, or worse the reverse. Most of the cases coming now are when people use the GL views because that is usually for high frequency updates and often on separate threads.

So this would be interesting to see if you were seeing weird things, or even having to do some weird things to prevent weird things 😄

@Gillibald
Copy link
Contributor

Luckily I only have to deal with single threaded processing. I might want to put text processing on a dedicated thread in the near future.

@Gillibald
Copy link
Contributor

The preview runs just fine. After this is merged I will work on the changes to SKObject.GetObject
Using cached delegates instead of reflection should improve things a bit.

Do we really need to keep track of all SKObject instances? Ideally, we could just turn off caching.

@mattleibow
Copy link
Contributor Author

mattleibow commented Apr 3, 2020

We have to track them because if I call SKSurface.Canvas twice, I need to find it again. I can't really cache it directly, because some operations might destroy the canvas and then the object is destroyed in native code, but we still got a valid object in managed world. Sometimes we don't even know when it will happen, as it may be a result of another operation. It is easy from a developer's perspective because certain operations may do things, but to get it right it is going to be more difficult.

I know @kekekeks was asking for a pointer-only API: #1205

But, this is not to say we can't actually have a look at certain APIs and see if we can just store things in a field. For example, a surface will always have the same canvas, so no need to even go through the interop. But this is not true for SKBitmap.PeekPixels because a copy is made of the pixmap, and you can always change the underlying pixel pointer at time - and this will not reflect in the copied pixmap.

Overall, the slowest point of this library is the lookup. And this is made harder because some objects are reference counted, others are not. And then, some are reference counted, but not used like one. The API is not quite perfect.

@mattleibow mattleibow merged commit 14813f8 into master Apr 3, 2020
@mattleibow mattleibow deleted the dev/fix-1188 branch April 3, 2020 11:25
@mattleibow
Copy link
Contributor Author

Thanks for having a look. Merging this now so I can get a preview out.

@mattleibow mattleibow added this to the v1.68.2 milestone Apr 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[BUG] macOS native crashes in gr_backendrendertarget_get_gl_framebufferinfo
3 participants