-
-
Notifications
You must be signed in to change notification settings - Fork 798
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
introducing BufferRecyclerPool #1061
Conversation
|
||
@Override | ||
public void close() { | ||
if (closed.compareAndSet(false, true)) { |
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 supposed to be concurrent? If not, and just "ordered" you can use a plain bool
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.
For now I implemented it in a concurrent way, but I agree that this constraint can be relaxed if not necessary.
* | ||
* @return BufferRecycler instance to use | ||
*/ | ||
public BufferRecycler _getBufferRecycler() |
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 unlikely that we will accept having any public APIs removed - you will ideally need to keep them and possibly deprecate them.
Otherwise, this PR will need to be retargeted and master and aimed at the 3.0 release which has no release date set for it.
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 see this is targeted at master - but be aware, it could be years before 3.0 is released.
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 indeed sent this PR against master, as I expect this to be ready and merged for the 3.0 release. I don't think that we could keep alive both this method and the solution based on the new object pool at the same time, because using one will break the assumptions made by the other. That said my understanding was that the methods and fields starting with an underscore were intended only for internal use and not considered part of the public API, am I missing something?
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 see this is targeted at master - but be aware, it could be years before 3.0 is released.
Ok, if so I guess this won't be acceptable for our usage purposes in Quarkus. Should I retarget this against 2.16 branch? Also can you clarify the naming convention around method names starting with an underscore? Is it safe to assume that they are not part of the public API?
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.
public means public. @cowtowncoder may consider allowing an exception - he's the BDFL.
Since it's following the underscore naming convention, you might say it is public but not guaranteed not to be removed at short notice (not up to me to make this call though).
@@ -99,6 +101,8 @@ public class IOContext | |||
*/ | |||
protected char[] _nameCopyBuffer; | |||
|
|||
private volatile AtomicBoolean closed = new AtomicBoolean(false); |
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.
as per later, maybe not needed any atomic type, if really need it - prefer AtomicIntegerFieldUpdater
(no bool sadly :"( unless using VarHandle
, which is probably not available for the base supported JDK version) or make it final and not volatile (itself) too.
The suggestion for the field updater is due to the number of IOContext
instances alive, to reduce the footprint.
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.
Not quite sure how one could reduce footprint as IOContext
s are independent and independently handled?
Ok, first of all, thank you for offering this, @mariofusco! I need time to actually read the draft, hoping to get to that soon. Some comments:
|
@Override | ||
public void close() { | ||
if (closed.compareAndSet(false, true)) { | ||
BufferRecyclerPool.offerBufferRecycler(_bufferRecycler); |
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.
If final
definition was removed, we could perhaps use null-ness of _bufferRecycler
itself (i.e. only offer if not null)
|
||
public class BufferRecyclerPool { | ||
|
||
private static final ObjectPool<BufferRecycler> pool = ObjectPool.newLockFreePool(BufferRecycler::new); |
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.
Ok; since this is now global pool (across all factories), there definitely should be maximum size. But then again, having such setting would lead to need for configuration.
And static singletons are generally a bad idea and not suitable for use by (low-level) libraries.
In fact, now that I think about this, I think pooling (or default implementation at any rate) really needs to be segmented by factory instance -- no buffers should be shared (by default impl) across TokenStreamFactory
instances.
return new LockFreeObjectPool<>(factory, destroyer); | ||
} | ||
|
||
class LockFreeObjectPool<T> implements ObjectPool<T> { |
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 that any pool to use should have maximum size, as a general principle.
Ok so... first, apologies if I misread code. I tried to go over it with limited amount of time so I may well have misunderstood something. But with that, I think my main concern is that it seems that buffer recycling would now become global across all With per-factory recycling/pooling, configurability could then also be exposed on per-factory basis -- otherwise any configurability would have to be global (static), affecting all use by all frameworks. To do this, I think factory would need to hold on to factory pools Does this make sense? |
I suspect this should be closed for now; we should be able to merge #1061 up to |
Yes, I will close this pull request. I will keep working on |
This pull request is a work in progress and at the moment it is not intended to be merged as it is. This is mostly to start a discussion and proposing a first tentative fix for the problem reported here.
In particular this is trying to implement the solution 1. suggested by @franz1981. The solution 2. would have the advantage of being the less invasive for jackson (and would be probably guarantee better performances), but it is totally impossible now and even in a near or far future it is very unlikely that the JDK team will agree to make the per-carrier-thread cache generally available since it would break some of the virtual threads encapsulations.
Also following what suggested in this comment by @cowtowncoder, I'm trying to better define the lifecycle of the
JsonParser
andJsonGenerator
making theirclose
methods to even close the underlyingIOContext
. TheIOContext
is now the only owner of itsBufferRecycler
: instead of being injected with aBufferRecycler
instance as an argument of its constructor, it borrows theBufferRecycler
directly from the newObjectPool
, and put it back into the pool when it is closed.This solution of course relies on the fact that the
close
methods of theJsonParser
andJsonGenerator
are always called correctly. I haven't found a place when this doesn't happen and in reality I conversely found at least one situation when this happens twice, without any apparent valid reason.The
ObjectPool
implementation is extremely naive and the main reason why this pull request has to be considered just a draft. However it only has theborrow
andoffer
methods, so I believe that it could be easily replaced by any real-world and battle tested high performance implementation. To this purpose I'd prefer to avoid reinventing the wheel and reusing one of the object pool implementations already available like the one in JCTools suggested by @franz1981 or one of the many alternatives listed here, even though not all of them are really an option because some also internally relies onThreadLocal
s in some way.So here the first questions: is it ok for
jackson-core
to directly depend on one of those libraries providing a production ready object pool or do we need/want to have an implementation directly inside jackson codebase? Do you know any of those libraries/implementations or have a specific preference for any of them? /cc @cowtowncoder @pjfanningFinally I also made a first attempt to check the performance of this solution, despite and regardless the poor object pool implementation, using a benchmark that I'm pasting at the end of this comment. Running this benchmark against the current master branch I obtained the following results:
while rerunning again using this pull request I got:
As expected the native threads execution is suffering of the worse performing object pool implementation, compared with the current
ThreadLocal
based one, while the one running on virtual threads is already faster in my version, so I'm confident of being on the right path for a good solution once the object pooling problem will be properly addressed.Any remark or suggestion to improve this (including any concern on why this solution couldn't work) is warmly welcome. The code of the benchmark I implemented and used follows: