Slide 1

Slide 1 text

@jessewilson https://square.github.io/okio/ Nerding Out on Okio

Slide 2

Slide 2 text

Okio is Fun • Computer Science • Software Engineering • Widely Deployed & Consequential • Brazen!

Slide 3

Slide 3 text

CS + SWE Computer science: a branch of mathematics. Concerned with algorithms, datastructures, and measuring computation. Software engineering: the work of developing and operating software. Concerned with quality, agility, planning, mentorship, and collaboration.

Slide 4

Slide 4 text

Widely Deployed In Android OS since 2014 Used by Retrofit, OkHttp, Coil, Apollo GraphQL, Moshi, Wire

Slide 5

Slide 5 text

Brazen • Java already has a pe ectly good I/O library, java.io • Java already has a pe ectly good java.io replacement, java.nio • A blocking library in the era of non-blocking • Switched to Kotlin in 2018!

Slide 6

Slide 6 text

I/O

Slide 7

Slide 7 text

java.io • Destinations: File, Socket • Roads: InputStream, OutputStream • Cars: Byte, ByteArray

Slide 8

Slide 8 text

class Socket { val inputStream: InputStream val outputStream: OutputStream ... }

Slide 9

Slide 9 text

abstract class InputStream { /** * Consumes bytes from this stream and copy them to [sink]. * Returns the number of bytes that were read, or -1 if this * input stream is exhausted. */ abstract fun read(sink: ByteArray): Int } abstract class OutputStream { /** * Copies all the data in [source] to this. */ abstract fun write(source: ByteArray) }

Slide 10

Slide 10 text

OkHttp Needs I/O

Slide 11

Slide 11 text

OkHttp’s Job Was Easy 1. Encode an HTTP request as a ByteArray 2. Write that ByteArray to a socket’s OutputStream 3. Read a ByteArray from a socket’s InputStream 4. Decode that ByteArray as an HTTP response

Slide 12

Slide 12 text

Adding HTTP/2 • HTTP/2 is multiplexed: 1. Chop each HTTP request into frames 2. Write each frame to the socket’s OuputStream 3. Read frames from the socket’s InputStream 4. Assemble frames into an HTTP response • Frames from different responses are interleaved!

Slide 13

Slide 13 text

class Http2Connection { private val streams = mutableMapOf() private fun processNextFrame(in: InputStream) { when (val frame = readFrame(in)) { is Frame.DataFrame -> { streams[frame.streamId]!!.receive(frame.data) } ... } } }

Slide 14

Slide 14 text

class Stream : InputStream() { internal fun receive(data: ByteArray) { ... } override fun read(sink: ByteArray): Int { ... } }

Slide 15

Slide 15 text

6 Buffers

Slide 16

Slide 16 text

Buffer as a List — IMPLEMENTATION 1 —

Slide 17

Slide 17 text

class Buffer { private val buffer = mutableListOf() fun write(source: ByteArray) { for (b in source) buffer += b } fun read(sink: ByteArray): Int { if (buffer.isEmpty()) return -1 val byteCount = minOf(sink.size, buffer.size) for (i in 0 until byteCount) { sink[i] = buffer.removeFirst() } return byteCount } }

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

buffer.write(data)

Slide 20

Slide 20 text

buffer.write(data)

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

buffer.read(data)

Slide 23

Slide 23 text

buffer.read(data)

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

A List of Bytes • Easy to get right! • Extremely slow • Autoboxing conve s from JVM byte primitive type to JVM java.lang.Byte object type • Byte-at-a-time requires too many instructions and too many function calls

Slide 26

Slide 26 text

Buffer as a ByteArray — IMPLEMENTATION 2 —

Slide 27

Slide 27 text

class Buffer { private var buffer = ByteArray(0) fun write(source: ByteArray) { val newBuffer = ByteArray(buffer.size + source.size) buffer.copyInto(newBuffer, destinationOffset = 0) source.copyInto(newBuffer, destinationOffset = buffer.size) buffer = newBuffer } fun read(sink: ByteArray): Int { if (buffer.isEmpty()) return -1 val byteCount = minOf(sink.size, buffer.size) val newBuffer = ByteArray(buffer.size - byteCount) buffer.copyInto(sink, endIndex = byteCount) buffer.copyInto(newBuffer, startIndex = byteCount, endIndex = buffer.size) buffer = newBuffer return byteCount } }

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

buffer.write(data)

Slide 30

Slide 30 text

buffer.write(data)

Slide 31

Slide 31 text

buffer.write(data)

Slide 32

Slide 32 text

buffer.write(data)

Slide 33

Slide 33 text

buffer.write(data)

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

buffer.read(data)

Slide 36

Slide 36 text

buffer.read(data)

Slide 37

Slide 37 text

buffer.read(data)

Slide 38

Slide 38 text

buffer.read(data)

Slide 39

Slide 39 text

buffer.read(data)

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

A Simple ByteArray • Easy to get right • Slow • Lots of allocations • Every byte gets copied around a lot

Slide 42

Slide 42 text

A Slice of a ByteArray — IMPLEMENTATION 3 —

Slide 43

Slide 43 text

class Buffer { private var buffer = ByteArray(0) private var pos = 0 private var limit = 0 fun write(source: ByteArray) { val requiredSize = limit - pos + source.size if (requiredSize > buffer.size) { val newBuffer = ByteArray(size = maxOf(requiredSize, buffer.size * 2)) buffer.copyInto(newBuffer, startIndex = pos, endIndex = limit) limit -= pos pos = 0 } else if (limit + source.size > buffer.size) { buffer.copyInto(buffer, startIndex = pos, endIndex = limit) limit -= pos pos = 0 } source.copyInto(buffer, destinationOffset = limit) limit += source.size } ... }

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

buffer.write(data)

Slide 46

Slide 46 text

buffer.write(data)

Slide 47

Slide 47 text

buffer.write(data)

Slide 48

Slide 48 text

buffer.write(data)

Slide 49

Slide 49 text

buffer.write(data)

Slide 50

Slide 50 text

No content

Slide 51

Slide 51 text

buffer.read(data)

Slide 52

Slide 52 text

buffer.read(data)

Slide 53

Slide 53 text

buffer.read(data)

Slide 54

Slide 54 text

buffer.read(data)

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

A Slice of a ByteArray • More difficult to get right • Getting Faster • Need to defend against worst-case access patterns • Copies to shift the data within the buffer

Slide 57

Slide 57 text

Circular Slice — IMPLEMENTATION 4 —

Slide 58

Slide 58 text

class Buffer { private var buffer = ByteArray(0) private var pos = 0 private var byteCount = 0 fun write(source: ByteArray) { val requiredSize = byteCount + source.size if (requiredSize > buffer.size) { val newBuffer = ByteArray(size = maxOf(requiredSize, buffer.size * 2)) if (pos + byteCount > buffer.size) { buffer.copyInto( newBuffer, startIndex = pos, ) buffer.copyInto( newBuffer, destinationOffset = buffer.size - pos, endIndex = byteCount - (buffer.size - pos), ) } else { buffer.copyInto( newBuffer,

Slide 59

Slide 59 text

source.copyInto( buffer, destinationOffset = offset ) } else { source.copyInto( buffer, destinationOffset = offset, endIndex = buffer.size - offset, ) source.copyInto( buffer, destinationOffset = 0, startIndex = buffer.size - offset, ) byteCount += buffer.size } } ... }

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

buffer.write(data)

Slide 62

Slide 62 text

buffer.write(data)

Slide 63

Slide 63 text

buffer.write(data)

Slide 64

Slide 64 text

buffer.write(data)

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

buffer.read(data)

Slide 67

Slide 67 text

buffer.read(data)

Slide 68

Slide 68 text

buffer.read(data)

Slide 69

Slide 69 text

buffer.read(data)

Slide 70

Slide 70 text

buffer.read(data)

Slide 71

Slide 71 text

No content

Slide 72

Slide 72 text

Circular Slice • Even more difficult to get right • Faster still • Every byte is copied once on the way in, once on the way out • Buffers never shrink their memory use

Slide 73

Slide 73 text

Transfer Array Ownership — IMPLEMENTATION 5 —

Slide 74

Slide 74 text

Java I/O Streams Gotta Copy abstract class InputStream { /** * Consumes bytes from this stream and copy them to [sink]. * Returns the number of bytes that were read, or -1 if this * input stream is exhausted. */ abstract fun read(sink: ByteArray): Int }

Slide 75

Slide 75 text

class Buffer { /** * Transfers all bytes from [source] to this. */ fun write(source: Buffer) /** * Transfers all bytes from this to [sink]. */ fun read(sink: Buffer): Int } I/O Without Copies

Slide 76

Slide 76 text

[ ]

Slide 77

Slide 77 text

buffer.write(data) [ ] [ ]

Slide 78

Slide 78 text

buffer.write(data) [ ] [] ,

Slide 79

Slide 79 text

[ ] ,

Slide 80

Slide 80 text

[ ] , buffer.read(data, 10) []

Slide 81

Slide 81 text

[ ] buffer.read(data, 10) [ ]

Slide 82

Slide 82 text

[ ] buffer.read(data, 10) [ ]

Slide 83

Slide 83 text

[ ] buffer.read(data, 10) [ ]

Slide 84

Slide 84 text

[ ] buffer.read(data, 10) [ ]

Slide 85

Slide 85 text

[ ] buffer.read(data, 10) [ ]

Slide 86

Slide 86 text

buffer.read(data, 10) [ ] , [ ]

Slide 87

Slide 87 text

[ ] buffer.read(data, 10) [ ] ,

Slide 88

Slide 88 text

[ ]

Slide 89

Slide 89 text

class Buffer { private var segments = mutableListOf() var size: Int = 0 /** ... */ fun write(source: Buffer) { size += source.size segments += source.segments source.size = 0 source.segments.clear() } /** ... */ fun read(sink: Buffer): Int { val result = size sink.write(this) return result } }

Slide 90

Slide 90 text

Transferring Ownership • A depa ure from java.io APIs • Fast? • Writing pa of a Buffer requires copies to split arrays • Worst-case pe ormance is bad! Things behave like the first implementation (List) if the arrays are small

Slide 91

Slide 91 text

OkBuffer — IMPLEMENTATION 6 —

Slide 92

Slide 92 text

class OkBuffer { private class Segment( val data: ByteArray, val pos: Int, val limit: Int, ) private var segments = mutableListOf() private var size: Int = 0 fun write(source: Buffer, byteCount: Int) { ... } fun read(sink: Buffer, byteCount: Int): Int { ... } }

Slide 93

Slide 93 text

] x[

Slide 94

Slide 94 text

buffer.write(data) ] [ ] x[

Slide 95

Slide 95 text

buffer.write(data) ] [] , [

Slide 96

Slide 96 text

] , [

Slide 97

Slide 97 text

] , buffer.read(data, 10) [] [

Slide 98

Slide 98 text

x[ ] buffer.read(data, 10) [ ]

Slide 99

Slide 99 text

x[ ] buffer.read(data, 10) [ ] ,

Slide 100

Slide 100 text

x[ ] buffer.read(data, 10) [ ] ,

Slide 101

Slide 101 text

x[ ] buffer.read(data, 10) [ ] ,

Slide 102

Slide 102 text

x[ ] buffer.read(data, 10) [ ] ,

Slide 103

Slide 103 text

x[ ] buffer.read(data, 10) [ ] ,

Slide 104

Slide 104 text

x, x[ ] buffer.read(data, 10) [ ]

Slide 105

Slide 105 text

x[ ]

Slide 106

Slide 106 text

OkBuffer • Borrows from transfer ownership + array slice strategies • All arrays are the same size – 8 KiB – which we call a segment • Three ways to move data between buffers: • Transfer ownership of a segment • Copy data between segments • Split a segment so both halves share a ByteArray, but maintain independent pos and limit

Slide 107

Slide 107 text

OkBuffer in OkHttp

Slide 108

Slide 108 text

class Stream { val buffer = OkBuffer() fun receive(source: OkBuffer, byteCount: Long) { synchronized(this) { buffer.write(source, byteCount) } } fun read(sink: OkBuffer, byteCount: Long): Long { synchronized(this) { if (buffer.size == 0L) return -1L val result = minOf(byteCount, buffer.size) sink.write(buffer, result) return result } } }

Slide 109

Slide 109 text

Let’s Open Source This!

Slide 110

Slide 110 text

No content

Slide 111

Slide 111 text

okio • Destinations: File, Socket • Roads: Source, Sink • Cars: Buffer

Slide 112

Slide 112 text

interface Source : Closeable { fun read(sink: Buffer, byteCount: Long): Long fun timeout(): Timeout } interface Sink : Closeable { fun write(source: Buffer, byteCount: Long) fun flush() fun timeout(): Timeout }

Slide 113

Slide 113 text

Zero Fill

Slide 114

Slide 114 text

Fresh New Arrays • What does ByteArray(8192) do? • Asks the memory manager for some memory (8192 + 16 bytes) • Writes an object header (16 bytes) • Writes 0 to each of the remaining 8192 bytes • Calling ByteArray(8192) takes 8x longer than ByteArray(1024) https://publicobject.com/2020/07/26/optimizing-new-byte/

Slide 115

Slide 115 text

Segment Pooling • When a Buffer is done with a Segment, Okio ‘recycles’ it in a private shared List • This makes writing data faster • It also saves work for the garbage collector

Slide 116

Slide 116 text

Fast Clone

Slide 117

Slide 117 text

Reading is Destructive • Because buffers transfer data rather than copying it, once you read a byte it’s gone! • Mitigate with Buffer.clone() • But how to make clone fast?

Slide 118

Slide 118 text

class Buffer { private class Segment( val pos: Int, val limit: Int, val data: ByteArray, /** True if other segments use the same byte array. */ val shared: Boolean, ) ... }

Slide 119

Slide 119 text

Copy Metadata, Not Data • Buffer.clone() creates new Segment metadata objects • No bytes are copied! • There are implications for pooling

Slide 120

Slide 120 text

Read & Write Whatever

Slide 121

Slide 121 text

interface Buffer { fun write(ByteArray) fun writeByte(Int) fun writeShort(Int) fun writeInt(Int) fun writeLong(Long) fun writeDecimalLong(Long) fun writeHexadecimalUnsignedLong(Long) fun writeString(String, Charset) fun writeUtf8(String) fun writeUtf8CodePoint(Int) fun writeAll(Source): Long } fun write(ByteArray, Int, Int) fun writeShortLe(Int) fun writeIntLe(Int) fun writeLongLe(Long) fun writeString(String, Int, Int, Charset) fun writeUtf8(String, Int, Int) fun write(Source, Long)

Slide 122

Slide 122 text

interface Buffer { fun write(ByteArray) fun writeByte(Int) fun writeShort(Int) fun writeInt(Int) fun writeLong(Long) fun writeDecimalLong(Long) fun writeHexadecimalUnsignedLong(Long) fun writeString(String, Charset) fun writeUtf8(String) fun writeUtf8CodePoint(Int) fun writeAll(Source): Long } fun write(ByteArray, Int, Int) fun writeShortLe(Int) fun writeIntLe(Int) fun writeLongLe(Long) fun writeString(String, Int, Int, Charset) fun writeUtf8(String, Int, Int) fun write(Source, Long)

Slide 123

Slide 123 text

interface Buffer { fun write(ByteArray) fun writeByte(Int) fun writeShort(Int) fun writeInt(Int) fun writeLong(Long) fun writeDecimalLong(Long) fun writeHexadecimalUnsignedLong(Long) fun writeString(String, Charset) fun writeUtf8(String) fun writeUtf8CodePoint(Int) fun writeAll(Source): Long } fun write(ByteArray, Int, Int) fun writeShortLe(Int) fun writeIntLe(Int) fun writeLongLe(Long) fun writeString(String, Int, Int, Charset) fun writeUtf8(String, Int, Int) fun write(Source, Long)

Slide 124

Slide 124 text

interface Buffer { fun write(ByteArray) fun writeByte(Int) fun writeShort(Int) fun writeInt(Int) fun writeLong(Long) fun writeDecimalLong(Long) fun writeHexadecimalUnsignedLong(Long) fun writeString(String, Charset) fun writeUtf8(String) fun writeUtf8CodePoint(Int) fun writeAll(Source): Long } fun write(ByteArray, Int, Int) fun writeShortLe(Int) fun writeIntLe(Int) fun writeLongLe(Long) fun writeString(String, Int, Int, Charset) fun writeUtf8(String, Int, Int) fun write(Source, Long)

Slide 125

Slide 125 text

interface Buffer { fun write(ByteArray) fun writeByte(Int) fun writeShort(Int) fun writeInt(Int) fun writeLong(Long) fun writeDecimalLong(Long) fun writeHexadecimalUnsignedLong(Long) fun writeString(String, Charset) fun writeUtf8(String) fun writeUtf8CodePoint(Int) fun writeAll(Source): Long } fun write(ByteArray, Int, Int) fun writeShortLe(Int) fun writeIntLe(Int) fun writeLongLe(Long) fun writeString(String, Int, Int, Charset) fun writeUtf8(String, Int, Int) fun write(Source, Long)

Slide 126

Slide 126 text

interface Buffer { fun write(ByteArray) fun writeByte(Int) fun writeShort(Int) fun writeInt(Int) fun writeLong(Long) fun writeDecimalLong(Long) fun writeHexadecimalUnsignedLong(Long) fun writeString(String, Charset) fun writeUtf8(String) fun writeUtf8CodePoint(Int) fun writeAll(Source): Long } fun write(ByteArray, Int, Int) fun writeShortLe(Int) fun writeIntLe(Int) fun writeLongLe(Long) fun writeString(String, Int, Int, Charset) fun writeUtf8(String, Int, Int) fun write(Source, Long)

Slide 127

Slide 127 text

interface Buffer { fun readByteArray() fun readByteArray(Long) fun readByte() fun readShort() fun readShortLe() fun readInt() fun readIntLe() fun readLong() fun readLongLe() fun readDecimalLong() fun readHexadecimalUnsignedLong() fun readString(Charset) fun readString(Long, Charset) fun readUtf8() fun readUtf8(Long) fun readUtf8CodePoint() fun readAll(Sink) } /** * Reads until the next `\r\n`, `\n`, or the * end of the file. Returns null at the end. */ fun readUtf8Line(): String? /** * Reads until the next `\r\n` or `\n`. Use * this for machine-generated text. */ fun readUtf8LineStrict(): String /** * Like readUtf8LineStrict() but throws if * no newline is within [limit] bytes. */ fun readUtf8LineStrict(limit: Long): String

Slide 128

Slide 128 text

interface Buffer { fun readByteArray() fun readByteArray(Long) fun readByte() fun readShort() fun readShortLe() fun readInt() fun readIntLe() fun readLong() fun readLongLe() fun readDecimalLong() fun readHexadecimalUnsignedLong() fun readString(Charset) fun readString(Long, Charset) fun readUtf8() fun readUtf8(Long) fun readUtf8CodePoint() fun readAll(Sink) } /** * Reads until the next `\r\n`, `\n`, or the * end of the file. Returns null at the end. */ fun readUtf8Line(): String? /** * Reads until the next `\r\n` or `\n`. Use * this for machine-generated text. */ fun readUtf8LineStrict(): String /** * Like readUtf8LineStrict() but throws if * no newline is within [limit] bytes. */ fun readUtf8LineStrict(limit: Long): String

Slide 129

Slide 129 text

interface Buffer { fun readByteArray() fun readByteArray(Long) fun readByte() fun readShort() fun readShortLe() fun readInt() fun readIntLe() fun readLong() fun readLongLe() fun readDecimalLong() fun readHexadecimalUnsignedLong() fun readString(Charset) fun readString(Long, Charset) fun readUtf8() fun readUtf8(Long) fun readUtf8CodePoint() fun readAll(Sink) } /** * Reads until the next `\r\n`, `\n`, or the * end of the file. Returns null at the end. */ fun readUtf8Line(): String? /** * Reads until the next `\r\n` or `\n`. Use * this for machine-generated text. */ fun readUtf8LineStrict(): String /** * Like readUtf8LineStrict() but throws if * no newline is within [limit] bytes. */ fun readUtf8LineStrict(limit: Long): String

Slide 130

Slide 130 text

interface Buffer { fun readByteArray() fun readByteArray(Long) fun readByte() fun readShort() fun readShortLe() fun readInt() fun readIntLe() fun readLong() fun readLongLe() fun readDecimalLong() fun readHexadecimalUnsignedLong() fun readString(Charset) fun readString(Long, Charset) fun readUtf8() fun readUtf8(Long) fun readUtf8CodePoint() fun readAll(Sink) } /** * Reads until the next `\r\n`, `\n`, or the * end of the file. Returns null at the end. */ fun readUtf8Line(): String? /** * Reads until the next `\r\n` or `\n`. Use * this for machine-generated text. */ fun readUtf8LineStrict(): String /** * Like readUtf8LineStrict() but throws if * no newline is within [limit] bytes. */ fun readUtf8LineStrict(limit: Long): String

Slide 131

Slide 131 text

Source Buffer + Sink

Slide 132

Slide 132 text

interface BufferedSink : Sink { override fun write(Buffer, Long) fun write(ByteArray) fun writeByte(Int) fun writeShort(Int) fun writeInt(Int) ... } interface BufferedSource : Source { override fun read(Buffer, Long): Long fun readByteArray(): ByteArray fun readByte(): Byte fun readShort(): Short fun readInt(): Int ... } interface Buffer : BufferedSource, BufferedSink { ... }

Slide 133

Slide 133 text

Buffering Streams • Better usability • Friendly methods like writeDecimalLong(), readUtf8Line() • Better pe ormance • Moves data 8 KiB at a time • ~ Zero overhead • Buffers don’t add copying!

Slide 134

Slide 134 text

COOL THINGS 10

Slide 135

Slide 135 text

// True if the stream has at least 100 more bytes. if (source.request(100)) { // ... } // Like request() but throws if there isn't enough data. source.require(100) // True once there's nothing left. Like !request(1). if (source.exhausted()) { // ... } END OF STREAM HANDLING #1

Slide 136

Slide 136 text

/** * Call [BufferedSource.peek] to do an arbitrarily-long * lookahead. It uses the same segment sharing stuff as * clone to keep things fast! * * Moshi's JSON uses this when polymorphic decoding to * look ahead at the type. */ fun readCelestial(source: BufferedSource): Celestial { val peek = source.peek() val type = findType(peek) peek.close() return decode(source, type) } PEEK IS LIKE A STREAMING CLONE #2

Slide 137

Slide 137 text

private val celestialTypes = Options.of( "star".encodeUtf8(), "planet".encodeUtf8(), "moon".encodeUtf8(), ) fun readCelestialType(source: BufferedSource): KClass? { return when (source.select(celestialTypes)) { 0 -> Celestial.Star::class 1 -> Celestial.Planet::class 2 -> Celestial.Moon::class else -> null } } SELECT USES a TRIE FOR FAST READING #3 https://speakerdeck.com/swankjesse/json-explained-chicago-roboto-2019

Slide 138

Slide 138 text

/** * Create input and output streams from Okio. Buffer can replace * both [ByteArrayOutputStream] and [ByteArrayInputStream] ! */ fun interopWithJavaIo(file: File) { val source = file.source().buffer() val bitmap = source.use { BitmapFactory.decodeStream(source.inputStream()) } addFunnyMoustaches(bitmap) val sink = file.sink().buffer() sink.use { bitmap.compress(JPEG, 100, sink.outputStream()) } } READ & WRITE AS JAVA.IO STREAMS #4

Slide 139

Slide 139 text

fun connectThreads(): Long { val pipe = Pipe(maxBufferSize = 1024) Thread { pipe.sink.buffer().use { sink -> for (i in 0L until 1000L) { sink.writeLong(i) } } }.start() var total = 0L pipe.source.buffer().use { source -> while (!source.exhausted()) { total += source.readLong() } } return total } PIPE CONNECTS A READER & A WRITER #5

Slide 140

Slide 140 text

THROTTLER SLOWS THINGS DOWN #6

Slide 141

Slide 141 text

THROTTLER SLOWS THINGS DOWN #6

Slide 142

Slide 142 text

CURSORS OFFER BYTEARRAY ACCESS #7 /** * Connect Okio's cursor to Guava's Murmur3F hash function. This uses * Buffer.UnsafeCursor to access the buffer's byte arrays. */ fun Buffer.murmur3(): HashCode { val hasher = Hashing.murmur3_128().newHasher() readUnsafe().use { cursor -> while (cursor.next() != -1) { hasher.putBytes( cursor.data!!, cursor.start, cursor.end - cursor.start ) } } return hasher.hash() }

Slide 143

Slide 143 text

fun runProcess() { val process = ProcessBuilder() .command("find", "/", "-name", "README.md") .start() val timeout = object : AsyncTimeout() { override fun timedOut() { process.destroyForcibly() } } timeout.deadline(5, TimeUnit.SECONDS) timeout.withTimeout { val source = process.inputStream.source().buffer() while (true) { println(source.readUtf8Line() ?: break) } } } TIMEOUTS WORK EVERYWHERE #8

Slide 144

Slide 144 text

/** * This uses [BufferedSource.readByteString] to read an entire stream * into a single immutable value. ByteString is a great container for * encoded data like protobufs, messages, and snapshots of files. */ private fun handleResponse(response: Response): HandledResponse<*> { if (!response.isSuccessful) { val source = response.body.source() return HandledResponse.UnexpectedStatus( response.code, response.headers, source.readByteString(), ) } ... } BYTESTRING IS A VALUE #9

Slide 145

Slide 145 text

/** * This uses [ByteString.hmacSha256] to takes a HMAC of a request * body to authenticate a webhook call. Okio includes SHA-1 and * SHA-256 hashes for byte strings, buffers, and streams. */ fun webHookSignatureCheck( headers: Headers, requestBody: ByteString, ) { val hmacSha256 = requestBody.hmacSha256(secret).hex() if (headers["X-Hub-Signature-256"] != "sha256=$hmacSha256") { throw IOException("signature check failed") } } HASHING CAN BE EASY #10

Slide 146

Slide 146 text

COOL THINGS 10 REQUIRE PEEK SELECT JAVA.IO 1. 2. 3. 4. 5. 6. 7. PIPE THROTTLER CURSORS 8. 9. TIMEOUTS BYTESTRING 10. HASHING

Slide 147

Slide 147 text

Okio 3’s FileSystem

Slide 148

Slide 148 text

Why? • Kotlin Multiplatform needs a file system! • JVM file APIs fight you if you try to write tests • We thought we could do better

Slide 149

Slide 149 text

Challenges • Multiplatform is difficult when the platforms are very different! • Deliberately not suppo ing everything! No Volume management, permissions, watches, or locking • Testing real implementations was tough

Slide 150

Slide 150 text

fun writeSequence(fileSystem: FileSystem, path: Path) { fileSystem.write(path, mustCreate = true) { for (i in 0L until 1000L) { writeDecimalLong(i) writeByte('\n'.code) } } } fun readSequence(fileSystem: FileSystem, path: Path): Long { fileSystem.read(path) { var total = 0L while (!exhausted()) { total += readDecimalLong() readByte() } return total } }

Slide 151

Slide 151 text

Highlights • FakeFileSystem • FileSystem.openZip() • ForwardingFileSystem • Kotlin for APIs! Like mustCreate & mustExist as optional parameters

Slide 152

Slide 152 text

Regrets

Slide 153

Slide 153 text

BufferedSource is a Bad Name • We have two inte aces: • Source is the easy-to-implement one • BufferedSource is the easy-to-call one • We should have saved the good name (Source) for the inte ace you use all the time • Similarly for Sink and BufferedSink

Slide 154

Slide 154 text

Timeout vs. Cancel • Every Source and Sink in Okio comes with a Timeout • A cancel() method would have been better! https://github.com/python-trio/trio

Slide 155

Slide 155 text

Controversies

Slide 156

Slide 156 text

Controversy 1: It’s Blocking • Java server I/O trend: everything asynchronous with Futures, callbacks, and event loops • Okio: everything is blocking

Slide 157

Slide 157 text

Blocking vs. Non-Blocking • Non-blocking lets you can service N concurrent callers with fewer than N threads • Non-blocking is not otherwise faster • Overhead of abstractions that move work between threads, plus cost of context-switching

Slide 158

Slide 158 text

No content

Slide 159

Slide 159 text

Loom is Coming! • Rather than making async better, why not make threads cheaper? • Vi ual threads are coming soon to the JVM • Currently in preview! https://openjdk.java.net/jeps/425

Slide 160

Slide 160 text

Also Not Suspending? • Cost to suspend byte-by-byte • Suspend in Retrofit / Wire / Coil instead

Slide 161

Slide 161 text

Controversy 2: Kotlin Switch • In 2018 we pressed ⌥⇧⌘K and conve ed Okio from Java to Kotlin, introducing a dependency on the Kotlin standard library • Java programmers are suspicious of alternative JVM languages https://speakerdeck.com/swankjesse/ok-multiplatform-droidcon-nyc-2018

Slide 162

Slide 162 text

No content

Slide 163

Slide 163 text

No Regrets on Kotlin • Kotlin’s been really good to us • We’re doing exciting things with multiplatform • Kotlin maintainers’ devotion to compatibility means none of the feared problems have materialized

Slide 164

Slide 164 text

Next Steps

Slide 165

Slide 165 text

Okio in 2022 • Okio’s healthy, stable, and the happy kind of boring • Enjoy!

Slide 166

Slide 166 text

@jessewilson Thanks https://square.github.io/okio/