-
Notifications
You must be signed in to change notification settings - Fork 17
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
Delete global refs in an attached thread #150
Conversation
fptr <- newForeignPtr ptr $ finalize jobj ptr | ||
`finally` deleteGlobalRefNonFinalized jobj | ||
`finally` runInAttachedThread (deleteGlobalRefNonFinalized jobj) |
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.
This is well spotted. Note though, that finalize jobj ptr
might do jni calls, so it needs to be wrapped as well.
newForeignPtr ptr $ runInAttachedThread $
finalize jobj ptr `finally` deleteGlobalRefNonFinalized jobj
Also, this finalizer is only used for vectors. The finalizer of global refs in Foreign/JNI/Unsafe.hs hasn't been updated.
jvm/benchmarks/Main.hs
Outdated
return () | ||
, bench "delete global reference (no finalizer, safe)" $ nfIO $ do | ||
ref <- newGlobalRefNonFinalized jobj | ||
_ <- runInAttachedThread (deleteGlobalRefNonFinalized ref) |
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.
runInAttachedThread
does not attach the thread if the thread is already attached. You would have to try running this benchmark on a different thread.
jvm/benchmarks/Main.hs
Outdated
, bench "delete global reference (not attached)" $ nfIO $ forkWait $ do | ||
ref <- newGlobalRefNonFinalized jobj | ||
_ <- deleteGlobalRefNonFinalized ref | ||
return () |
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.
I'd expect this code to fail to run, as it is running in a thread which is not attached. It doesn't look like forkWait
would propagate any failures so the benchmark would silently pass.
jvm/benchmarks/Main.hs
Outdated
-- | Execute the given IO in a separate thread, then wait for it to finish | ||
forkWait :: IO a -> IO () | ||
forkWait io = do | ||
handle <- newEmptyMVar | ||
_ <- forkFinally (runInBoundThread io) (\_ -> putMVar handle ()) | ||
takeMVar handle >>= return |
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.
I'm not entirely sure what the reason for spawning a thread then immediately waiting for it would be. I'd imagine this has exactly the same semantics as just doing io
. But probably I'm missing something.
At any rate, you shouldn't implement this sort of logic yourself. The async implements high level primitives which do the MVar
thing, and error handling, and such. You should reach for these.
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.
At any rate, you shouldn't implement this sort of logic yourself. The async implements high level primitives which do the MVar thing, and error handling, and such. You should reach for these.
That's good advice in general. In this particular case, I think it is better to not spawn any threads. The need to spawn the thread comes from trying to measure how much time attaching and detaching the thread takes. The main thread is already attached to the JVM so we can't measure it there.
I think the best solution is to have the benchmark setup detach the main thread before running the benchmark.
The naive solution of attaching a thread to the JVM before calling deleteGlobalRefNonFinalized, then detaching it afterward, has been rejecting due to adding too much overhead:
The extra cost of 25 μs led us to try to have a thread, attached to the JVM startup, which would be dedicated to deleting global references. A preliminary, still nonfunctional version, is currently on commit 8c4a7f4 |
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.
Some comments are for polishing, and some are for correctness. The polishing comments can be deferred until we are happy with the benchmarks.
jni/src/common/Foreign/JNI/Unsafe.hs
Outdated
stopGlobalRefCleaner :: GlobalRefCleaner -> IO () | ||
stopGlobalRefCleaner cleaner = do | ||
putMVar (nextAction cleaner) Terminate | ||
wait (cleanerAsync cleaner) |
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.
I think I would just cancel the thread here. Is it necessary to delete the pending global references before terminating the JVM?
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.
Also, probably we should use uninterruptibleMask_
to make this code safe to asynchronous exceptions.
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.
It's probably not necessary to delete pending global references before terminating the JVM.
In any case, we cannot guarantee that all pending references will be deleted in the current state of the code anyway.
Shall we keep the CleanerAction type then ? It looks like we won't need Terminate
Here are the results of the benchmarks:
It looks to me that having a separate thread do the deletion is the way to go. |
Note that the results of the third benchmark do not measure the time needed to delete a reference, but only the overhead of sending the references to a dedicated thread, which does the actual deleting. |
commit b250314 |
jni/src/common/Foreign/JNI/Unsafe.hs
Outdated
@@ -376,6 +380,7 @@ attachCurrentThreadAsDaemon = do | |||
[CU.exp| jint { | |||
(*$(JavaVM* jvm))->AttachCurrentThreadAsDaemon($(JavaVM* jvm), (void**)&jniEnv, NULL) | |||
} |] | |||
readIORef mainThreadClassLoader >>= setCurrentThreadClassLoader |
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.
@mboes, I'm struggling with a design choice here. With the change to delete global references in a background thread we need to use the threaded runtime, and tests in jvm-streaming will fail in the threaded runtime because we need to set the context ClassLoader on some of the OS threads that we use.
I proposed to @nbacquey to set it here, but I'm noticing that this runs against the principle of keeping the bindings in jni simple.
Maybe the best place to put it would be in a new convenience package jvm-threads, where we can provide a version of runInAttachedThread
and withJVM
that deal with setting context class loaders for the user. Otherwise, the user would need to be responsible for doing this in each application.
Any thoughts?
If the main class loader is not explicitely used, extra classpaths given when invoking the JVM will be forgotten.
…emon" This reverts commit 5625b6f.
42bc342
to
565a6d7
Compare
I squashed commits. The old tip of the branch is 42bc342. I undid the provision to set the context class loader when attaching threads. I'm using loadJavaWrappers in test to load the classes ahead of calling JNI in other threads. Also, I added a troubleshooting section to the readme, to discuss this and other problems. |
No description provided.