square / okio

A modern I/O library for Android, Java, and Kotlin Multiplatform.
https://square.github.io/okio/
Apache License 2.0
8.77k stars 1.18k forks source link

Zero-copy NIO channel read & write #1172

Open swankjesse opened 1 year ago

swankjesse commented 1 year ago

I’m experimenting using okio.Buffer with SocketChannel.

One challenge is SocketChannel doesn’t offer APIs that fit Okio:

public class SocketChannel ... {
    ...
    public int read(ByteBuffer dst);
    public long read(ByteBuffer[] dsts, int offset, int length);
    public long read(ByteBuffer[] dsts);

    public int write(ByteBuffer src);
    public long write(ByteBuffer[] srcs, int offset, int length);
    public long write(ByteBuffer[] srcs);
}

I can accomplish my goals by creating a temporary ByteBuffer, then copying its results into an Okio Buffer with these existing functions from the ByteChannel supertype:

class Buffer {
  ...
  fun read(sink: ByteBuffer): Int
  fun write(source: ByteBuffer): Int
}

But copying an extra time sucks, let’s not do that!

A better alternative is @bnorm’s awesome ByteChannelSink sample, which uses ByteBuffer.wrap().

I’d like to get something like that into Okio, as extension functions on the appropriate NIO channel types:

  /**
   * Reads up to [byteCount] bytes from this into [sink].
   * 
   * This will read fewer bytes if this channel is exhausted.
   * 
   * It will also read fewer bytes if this channel is in non-blocking mode, and
   * reading more bytes would require blocking.
   * 
   * @return the number of bytes read, possibly 0. Returns -1 if zero bytes were
   *     read and this channel is exhausted.
   */
  fun ReadableByteChannel.read(sink: Buffer, byteCount: Long): Long

  /**
   * Writes up to [byteCount] bytes from [source] into into this.
   * 
   * This will write fewer bytes if this channel is ready to accept new data.  This
   * typically occurs when this channel is in blocking mode and writing more
   * bytes would require blocking.
   * 
   * @return the number of bytes written, possibly 0.
   */
  fun WritableByteChannel.write(source: Buffer, byteCount: Long = source.size): Long

Cache ByteBuffer instances?

Should we cache result of ByteBuffer.wrap() as a field on Segment ? It has the potential to be a frequent allocation, though it’s also one that the VM should be able to escape-analysis away. Looking at JOL, we could add a ByteBuffer field without immediately harming the size of Segment.

 ~/Development/jol (master) $ java -cp jol-samples/target/jol-samples.jar org.openjdk.jol.samples.JOLSample_01_Basic
 OFF  SZ         TYPE DESCRIPTION               VALUE
   0   8              (object header: mark)     N/A
   8   4              (object header: class)    N/A
  12   4          int Segment.pos               N/A
  16   4          int Segment.limit             N/A
  20   1      boolean Segment.shared            N/A
  21   1      boolean Segment.owner             N/A
  22   2              (alignment/padding gap)
  24   4       byte[] Segment.data              N/A
  28   4      Segment Segment.next              N/A
  32   4      Segment Segment.prev              N/A
- 36   4              (object alignment gap)
+ 36   4   ByteBuffer Segment.byteBuffer        N/A
 Instance size: 40 bytes
-Space losses: 2 bytes internal + 4 bytes external = 6 bytes total
+Space losses: 2 bytes internal + 0 bytes external = 2 bytes total

For now I’d like to start by not caching, especially since doing so would require Segment to be split into JVM and non-JVM declarations.

yschimke commented 1 year ago

I've been hoping okio would have some socket like interface for a while. Would make things like OkHttp/Wire working multiplatform possible. Possible implementations, 1) Socket, 2) TLS Socket, ...

Could you gain anything by expressing this problem with an Okio socket like interface?

swankjesse commented 1 year ago

Yep, I think that’s appropriate. Particularly for Kotlin/Native, where we don’t yet have a multiplatform socket.