Commit Graph

50 Commits

Author SHA1 Message Date
Chris Vest
2dee6f8516 Fix bounds checking bugs when setting bytes
These should not take the read offset into account.
2021-03-09 16:16:38 +01:00
Chris Vest
da70f29ff4 Fix numerous bugs in the ByteBufAdaptor 2021-03-09 12:04:57 +01:00
Chris Vest
56bfa22d4a Align Buffer.get* bounds checks with their documented behaviour
The get* methods bounds checking accesses between 0 and the write offset, and the tests were confirming this behaviour.

This was wrong because it is not symmetric with the set* methods, which bounds check between 0 and the capacity, and does not modify the write offset.

The tests and methods have been updated so the get* methods now bounds check between 0 and the capacity.
2021-03-09 12:02:46 +01:00
Chris Vest
f460c732d0 Make LifecycleTracer thread-safe
The real world may expose the buffers to concurrent accesses even when this is not supposed to be supported.
2021-03-09 11:57:49 +01:00
Chris Vest
602389712c Add working HttpSnoop example 2021-03-06 11:18:14 +01:00
Chris Vest
bf80061335 Also attach traces to ownership errors from compact() and bifurcate() 2021-03-06 11:14:00 +01:00
Chris Vest
0867f99be1 Add LifecycleTracer to help debug lifecycle/ownership issues
Motivation:
With reference counting it can be difficult to keep track of ownership and references.
When bugs arise in this area, it's good to get help from some tooling.

Modification:
Add a LifecycleTracer which records lifecycle changes.
This information can be attached to any lifecycle/ownership exceptions as suppressed exceptions.
The tracing is off by default, unless assertions are enabled.

Result:
It's now easier to debug reference counting/lifecycle/ownership issues.
2021-03-05 16:32:10 +01:00
Chris Vest
78f04eeb49 Update docs and examples 2021-03-01 11:21:25 +01:00
Chris Vest
1b65bf9a23 Make the incubating buffers exposable as ByteBuf
Motivation:
This makes it possible to use the new buffer API in Netty as is.

Modification:
Make the MemSegBuffer implementation class implement AsByteBuf and ReferenceCounted.
The produced ByteBuf instance delegates all calls to the underlying Buffer instance as faithfully as possible.
One area where the two deviates, is that it's not possible to create non-retained duplicates and slices with the new buffer API.

Result:
It is now possible to use the new buffer API on both client and server side.
The Echo* examples demonstrate this, and the EchoIT proves it with a test.
The API is used more directly on the client side, since the server-side allocator in Netty does not know how to allocate buffers with the incubating API.
2021-03-01 10:49:09 +01:00
Chris Vest
6175f8f4c5 Make ReadableComponent expose ByteCursors
Motivation:
Another way to process the readable data in a buffer.
This might be faster for composite buffers, since their byte cursors are a bit slower than the MemSegBuffer due to the indirections and more complicated logic.

Modification:
ReadableComponent now have openCursor method.

Note that we *don't* add an openReverseCursor method on ReadableComponent.
The reason is that forEachReadable iterates the components in the forward direction, and it's really confusing to then iterate the bytes in a backwards direction.
Working with both directions at the same time is very error prone.

Result:
It is now possible to process readable components with byte cursors.
2021-02-27 11:04:44 +01:00
Chris Vest
7185a59f7a Move composite buffer methods to Buffer
Motivation:
There is no reason for `compose()` to be an instance method on `BufferAllocator` since the allocator implementation should not influence how this method is implemented.

Modification:
Make `compose()` a static method and move it to the `Buffer` interface.
Also move its companion methods `extendComposite()` and `isComposite()` to the `Buffer` interface.

Result:
The composite buffer methods are now in a more sensible place.

Also: decided _against_ making `extendComposite()` and `isComposite()` instance methods, because the subtle behaviours of `extendComposite()` means it would behave quite differently for non-composite buffers.
Also: `isComposite()` is not an instance method because it relates to the hard-coded and concrete `CompositeBuffer` implementation.
2021-02-15 17:46:23 +01:00
Chris Vest
5434fa88b4 Explode ComponentProcessor into its contained interfaces 2021-02-12 18:28:29 +01:00
Chris Vest
eef97dd1fd Rename Buf to Buffer and Allocator to BufferAllocator 2021-02-12 18:22:07 +01:00
Chris Vest
492977d9be Introduce Deref abstraction
Motivation:
Sometimes, we wish to operate on both buffers and anything that can produce a buffer.
For instance, when making a composite buffer, we could compose either buffers or sends.

Modification:
Introduce a Deref interface, which is extended by both Rc and Send.
A Deref can be used to acquire an Rc instance, and in doing so will also acquire a reference to the Rc.
That is, dereferencing increases the reference count.
For Rc itself, this just delegates to Rc.acquire, while for Send it delegates to Send.receive, and can only be called once.
The Allocator.compose method has been changed to take Derefs.
This allows us to compose either Bufs or Sends of bufs.
Or a mix.
Extra care and caution has been added to the code, to make sure the reference counts are managed correctly when composing buffers, now that it's a more complicated operation.
A handful of convenience methods for working with Sends have also been added to the Send interface.

Result:
We can now build a composite buffer out of sends of buffers.
2021-02-11 14:26:57 +01:00
Chris Vest
2df000ad9a Update examples
Motivation:
The forEachReadable/Writable permit a cleaner FileCopyExample implementation.

Modification:
Simplify FileCopyExample.
Also add examples of various good and bad ways to transfer buffer ownership between threads.
Update the forEachReadable/Writable APIs to let exceptions pass through.

Result:
Cleaner code and more useful forEachReadable/Writable APIs.
2021-01-25 13:23:26 +01:00
Chris Vest
14d55c3e0b Avoid nesting composite buffers
Motivation:
There is no reason that composite buffers should nest when composed.
Instead, when composite buffers are used to compose or extend other composite buffers, we should unwrap them and copy the references to their constituent buffers.

Modification:
Composite buffers now always unwrap and flatten themselves when they participate in composition or extension of other composite buffers.

Result:
Composite buffers are now always guaranteed* to contain a single level of non-composed leaf buffers.

*assuming no other unknown buffer-wrapping buffer type is in the mix.
2021-01-18 16:06:53 +01:00
Chris Vest
1ff8b4bf5a Remove work-around for slice-of-heap-segment-based-buffer JDK bug that got fixed 2021-01-18 12:11:03 +01:00
Chris Vest
e22b57ddcd Address PR review comments 2021-01-18 11:57:35 +01:00
Chris Vest
1c1149395b Add comments about how component count overflow is prevented
Also add TODOs for flattening composite buffers.
2021-01-18 10:56:33 +01:00
Chris Vest
1c10770e82 Implement segregated readable/writable component interfaces and processing 2021-01-15 21:32:24 +01:00
Chris Vest
46ed14577c Add Buf.forEachWritable
Pass iteration indexes through.
2021-01-15 16:11:21 +01:00
Chris Vest
d382017dc6 Add support for iterating underlying buffer components
Motivation:
It's desirable to be able to access the contents of a Buf via an array or a ByteBuffer.
However, we would also like to have a unified API that works for both composite and non-composite buffers.
Even for nested composite buffers.

Modification:
Add a forEachReadable method, which uses internal iteration to process all buffer components.
The internal iteration allows us to hide any nesting of composite buffers.
The consumer in the internal iteration is presented with a Component object, which exposes the contents in various ways.
The data is exposed from the Component via methods, such that anything that is expensive to create, will not have to be paid for unless it is used.
This mechanism also let us avoid any allocation unnecessary allocation; the ByteBuffers and arrays will necessarily have to be allocated, but the consumer may or may not need allocation depending on how it's implemented, and the component objects do not need to be allocated, because the non-composite buffers can directly implement the Component interface.

Result:
It's now possible to access the contents of Buf instances as arrays or ByteBuffers, without having to copy the data.
2021-01-11 16:31:36 +01:00
Chris Vest
617d9ccef1 Add support for read-only buffers
Motivation:
There are cases where you want a buffer to be "constant."
Buffers are inherently mutable, but it's possible to block off write access to the buffer contents.
This doesn't make it completely safe to share the buffer across multiple threads, but it does catch most races that could occur.

Modification:
Add a method to Buf for toggling read-only mode.
When a buffer is read-only, the write accessors throw exceptions when called.
In the MemSegBuf, this is implemented by having separate read and write references to the underlying memory segment.
In a read-only buffer, the write reference is redirected to point to a closed memory segment, thus preventing all writes to the memory backing the buffer.

Result:
It is now possible to make buffers read-only.
Note, however, that it is also possible to toggle a read-only buffer back to writable.
We need that in order for buffer pools to be able to fully reset the state of a buffer, regardless of the buffer implementation.
2021-01-05 16:53:21 +01:00
Chris Vest
3b1743c7fd Javadoc corrections for buffer accessor methods
Fixes #22
2021-01-05 12:24:46 +01:00
Chris Vest
41b3c02812 Remove thread-confinement of Buffers
Motivation:
Thread-confinement ends up being too confusing to code for, and also prevents some legitimate use cases.
Additionally, thread-confinement exposed implementation specific behavioural differences of buffers, where we would ideally like all buffers to always behave the same, regardless of implementation.

Modification:
All MemorySegment based buffers now always use shared segments.
For heap-based segments, we avoid the overhead associated with the closing of shared segments, by just not closing them, and instead just leave the whole thing for the GC to deal with.

Result:
Buffers can now always be accessed from multiple different threads at the same time.
2020-12-30 15:42:33 +01:00
Chris Vest
dc281c704c Buffers always have a cleaner attached
Motivation:
Although having a cleaner attached adds a bit of overhead when allocating or closing buffers,
it is more important to make our systems, libraries and frameworks misuse resistant and safe by default.

Modification:
Remove the ability to allocate a buffer that does not have a cleaner attached.
Reference counting and the ability to explicitly release memory remains.
This just makes sure that we always have a safety net to fall back on.

Result:
This will make systems less prone to crashes through running out of memory, native or otherwise, even in the face of true memory leaks.
(Leaks through retained strong references cannot be fixed in any way)
2020-12-17 16:13:43 +01:00
Chris Vest
4036dac84d Add a flag to allow Buf.ensureWritable to compact buffer
Motivation:
The main use case with Buf.compact is in conjunction with ensureWritable.
It turns out we can get a simpler API, and faster methods, by combining those two operations, because it allows us to relax some guarantees and skip some steps in certain cases, which wouldn't be as neat or clean if they were two separate steps.

Modification:
Add a new Buf.ensureWritable method, which takes an allowCompaction argument.
In MemSegBuf, we can just delegate to compact() when applicable.
In CompositeBuf, we can sometimes get away with just reorganising the bufs array.

Result:
We can now do ensureWritable without allocating in some cases, and this can in particular make the operation faster for CompositeBuf.
2020-12-17 12:29:55 +01:00
Chris Vest
252ac38305 Simplify MemSegBuf.compact implementation 2020-12-17 12:29:55 +01:00
Chris Vest
0f303c7971 Add a Buf.compact method
Motivation:
Compaction makes more space available at the end of a buffer, by discarding bytes at the beginning that have already been processed.

Modification:
Add a copying compact method to Buf.

Result:
It is now possible to discard read bytes by calling `compact()`.
2020-12-17 12:29:55 +01:00
Chris Vest
008c5ed6ec Make BufHolder protected methods final if they're not meant to be overwritten 2020-12-16 17:06:22 +01:00
Chris Vest
6b91751bea Small polishing that addresses PR comments 2020-12-16 12:31:49 +01:00
Chris Vest
f83e7fa618 Add BufHolder and BufRef helper classes
Motivation:
There are many use cases where other objects will have fields that are buffers.
Since buffers are reference counted, their life cycle needs to be managed carefully.

Modification:
Add the abstract BufHolder, and the concrete sub-class BufRef, as neat building blocks for building other classes that contain field references to buffers.

The behaviours of closed/sent buffers have also been specified in tests, and tightened up in the code.

Result:
It is now easier to create classes/objects that wrap buffers.
2020-12-14 14:22:37 +01:00
Chris Vest
02eb4286fa Better method names and javadocs in RcSupport 2020-12-11 12:09:32 +01:00
Chris Vest
bb2264ac5b Address review comments on bifurcate PR 2020-12-10 12:51:18 +01:00
Chris Vest
b749106c0c Add a Buf.bifurcate method
Motivation:
There are use cases that involve accumulating data into a buffer, then carving out prefix slices and sending them off on their own journey for further processing.

Modification:
Add a Buf.bifurcate API, that split a buffer, and its ownership, in two.
Internally, the API will inject and maintain an atomically reference counted Drop instance, so that the original memory segment is not released until all bifurcated parts are closed.
This works particularly well for composite buffers, where only the buffer (if any) wherein the bifurcation point lands, will actually have its memory split. A composite buffer can otherwise just crack its buffer array in two.

Result:
We now have a safe way of breaking the single ownership of some memory into multiple parts, that can be sent and owned independently.
2020-12-10 10:29:31 +01:00
Chris Vest
4960e8b3e7 Make ByteCursors from CompositeBuf slightly faster 2020-12-09 11:03:30 +01:00
Chris Vest
a7701c04b5 Update ByteCursor method names and related javadocs 2020-12-09 11:03:30 +01:00
Chris Vest
b2d0231f27 Specify the behaviour of ByteCursor.getByte and getLong, when relevant next* methods haven't been called 2020-12-09 11:02:51 +01:00
Chris Vest
6cc49c1c62 Turn ByteIterator into ByteCursor
Motivation:
Cursors are better than iterators in that they only need to check boundary conditions once per iteration, when processed in a loop.
This should make them easier for the compiler to optimise.

Modification:
Change the ByteIterator to a ByteCursor. The API is almost the same, but with a few subtle differences in semantics.
The primary difference is that the cursor movement and boundary condition checking and position movement happen at the same time, and do not need to occur when the values are fetched out of the cursor.
An iterator, on the other hand, needs to throw an exception if "next" is called too many times.

Result:
Simpler code, and hopefully faster code as well.
2020-12-09 11:02:51 +01:00
Chris Vest
3aeebdd058
Merge pull request #12 from netty/extend-composite
Make it possible to extend composite buffers after creation
2020-12-08 19:25:32 +01:00
Chris Vest
2ce8c7dc18 Fix drop race with Cleaner
Motivation:
We were seeing rare test failures where a cleaner had raced to close a memory segment we were using or closing.
The cause was that a single MemorySegment ended up used in multiple Buf instances.
When the SizeClassedMemoryPool was closed, the memory segments could be disposed without closing the gate in the NativeMemoryCleanerDrop.
The gate is important because it prevents double-frees of the memory segment.

Modification:
The fix is to change how the SizeClassedMemoryPool is closed, such that it always releases memory by calling `close()` on its buffers, which in turn will close the gate. The program will then proceed through the SizeClassedMemoryPool.drop implementation, which in turn will observe that the allocator is closed, and *then* dispose of the memory.

Result:
We should hopefully not see any more random test failures, but if we do, they would at least indicate a different bug.
This particular one was mostly showing up inside the cleaner threads, which were ignoring the exception, but occasionally I think the race went the other way, causing a test failure.
2020-12-07 17:35:21 +01:00
Chris Vest
236097e081 Make it possible to extend composite buffers after creation
Motivation:
Composite buffers are uniquely positioned to be able to extend their underlying storage relatively cheaply.
This fact is relied upon in a couple of buffer use cases within Netty, that we wish to support.

Modification:
Add a static `extend` method to Allocator, so that the CompositeBuf class can remain internal.
The `extend` method inserts the extension buffer at the end of the composite buffer as if it had been included from the start.
This involves checking offsets and byte order invariants.
We also require that the composite buffer be in an owned state.

Result:
It's now possible to extend a composite buffer with a specific buffer, after the composite buffer has been created.
2020-12-03 17:48:28 +01:00
Chris Vest
e3c7f9b632 Capture build artifacts for failed builds
Motivation:
When a build fails, it's desirable to have the build artifacts available
so the build failure can be inspected, investigated and hopefully fixed.

Modification:
Change the Makefile and CI workflow such that the build artifacts are
captured and uploaded when the build fails.

Result:
A "target.zip" file is now available for download on failed GitHub
Actions builds.
2020-12-01 14:38:09 +01:00
Chris Vest
a3f6ae6be8 Make Buf.send() faster
When send() a confined buffer, we had to first turn it into a shard buffer, so that it could be claimed by an arbitrary recipient thread.

As we've learned, however, closing shared segments is expensive.
We can speed up the send() call by simply leaving the segment shared.
This weakens the confinement of the received segment, though.
Currently no tests fails on that, but in the future we should re-implement confinement checking inside the Buf implementations themselves any, because pooled buffers also violate the confinement restriction, and we have a guiding principle that all buffers, regardless of implementation, should always behave the same.

The results of this change can be observed in the MemorySegmentClosedByCleanerBenchmark, with the heavy workload.
Explicitly closed segments now run the workload twice as fast, and the cleaner based closing is now 3 times faster.

Before:
Benchmark                                            (workload)  Mode  Cnt   Score   Error  Units
MemorySegmentClosedByCleanerBenchmark.cleanerClose        heavy  avgt  150  42,221 ± 0,943  us/op
MemorySegmentClosedByCleanerBenchmark.explicitClose       heavy  avgt  150  65,215 ± 0,761  us/op

After:
Benchmark                                            (workload)  Mode  Cnt   Score   Error  Units
MemorySegmentClosedByCleanerBenchmark.cleanerClose        heavy  avgt  150  13,871 ± 0,544  us/op
MemorySegmentClosedByCleanerBenchmark.explicitClose       heavy  avgt  150  37,516 ± 0,426  us/op
2020-11-26 11:31:23 +01:00
Chris Vest
023bb64a25 Update MemSegBuf with the latest panama-foreign API changes 2020-11-20 14:01:14 +01:00
Chris Vest
a4ecc1b184 Add toString methods to the buffer implementations 2020-11-20 12:44:09 +01:00
Chris Vest
53d2e4b955 Pooled buffers must reset their state before reuse
Motivation:
Buffers should always behave the same, regardless of their underlying implementation and how they are allocated.

Modification:
The SizeClassedMemoryPool did not properly reset the internal buffer state prior to reusing them.
The offsets, byte order, and contents are now cleared before a buffer is reused.

Result:
There is no way to observe externally whether a buffer was reused or not.
2020-11-20 11:53:26 +01:00
Chris Vest
59b564ddc8 Add a docker-based build
Motivation:
Because of the current dependency on snapshot versions of the Panama Foreign version of OpenJDK 16, this project is fairly involved to build.

Modification:
To make it easier for newcomers to build the binaries for this project, a docker-based build is added.
The docker image is constructed such that it contains a fresh snapshot build of the right fork of Java.
A make file has also been added, which encapsulates the common commands one would use for working with the docker build.

Result:
It is now easy for newcomers to make builds, and run tests, of this project, as long as they have a working docker installation.
2020-11-18 17:16:37 +01:00
Chris Vest
a1785e8161 Move the MemorySegment based Buf implementation to its own package, and break the remaining bits of tight coupling. 2020-11-17 15:53:40 +01:00
Chris Vest
3efa93841e Rename the 'b2' package to 'api' 2020-11-17 15:40:13 +01:00