Skip to content

Commit c128d21

Browse files
committed
Organize Buffer's segments as a regular list
Previously, Buffer's segments were organized into a circular list. That allowed storing only a single reference to buffer's head, and also facilitated insertion/removal of new list nodes. The downside of a circular list is that one has to always compare a current node with a head when iterating over segments. That complicates the implementation of a public API for segments iterations. See #135 (comment) for details on segment iteration API.
1 parent 7c4d095 commit c128d21

File tree

11 files changed

+162
-107
lines changed

11 files changed

+162
-107
lines changed

core/apple/src/AppleCore.kt

+2-4
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ private open class OutputStreamSink(
5252
source.size -= bytesWritten
5353

5454
if (head.pos == head.limit) {
55-
source.head = head.pop()
56-
SegmentPool.recycle(head)
55+
source.recycleHead()
5756
}
5857
}
5958
}
@@ -101,8 +100,7 @@ private open class NSInputStreamSource(
101100
if (bytesRead == 0L) {
102101
if (tail.pos == tail.limit) {
103102
// We allocated a tail segment, but didn't end up needing it. Recycle!
104-
sink.head = tail.pop()
105-
SegmentPool.recycle(tail)
103+
sink.recycleTail()
106104
}
107105
return -1
108106
}

core/apple/src/BuffersApple.kt

+5-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
package kotlinx.io
99

1010
import kotlinx.cinterop.*
11-
import platform.Foundation.*
11+
import platform.Foundation.NSData
12+
import platform.Foundation.create
13+
import platform.Foundation.data
1214
import platform.darwin.NSUIntegerMax
1315
import platform.posix.*
1416

@@ -44,8 +46,7 @@ internal fun Buffer.readAtMostTo(sink: CPointer<uint8_tVar>, maxLength: Int): In
4446
size -= toCopy.toLong()
4547

4648
if (s.pos == s.limit) {
47-
head = s.pop()
48-
SegmentPool.recycle(s)
49+
recycleHead()
4950
}
5051

5152
return toCopy
@@ -70,6 +71,6 @@ internal fun Buffer.snapshotAsNSData(): NSData {
7071
}
7172
curr = curr.next
7273
index += length
73-
} while (curr !== head)
74+
} while (curr !== null)
7475
return NSData.create(bytesNoCopy = bytes, length = size.convert())
7576
}

core/common/src/Buffer.kt

+92-41
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public class Buffer : Source, Sink {
4242
@JvmField
4343
internal var head: Segment? = null
4444

45+
@JvmField
46+
internal var tail: Segment? = null
47+
4548
/**
4649
* The number of bytes accessible for read from this buffer.
4750
*/
@@ -76,8 +79,7 @@ public class Buffer : Source, Sink {
7679
val b = data[pos++]
7780
size -= 1L
7881
if (pos == limit) {
79-
head = segment.pop()
80-
SegmentPool.recycle(segment)
82+
recycleHead()
8183
} else {
8284
segment.pos = pos
8385
}
@@ -102,8 +104,7 @@ public class Buffer : Source, Sink {
102104
size -= 2L
103105

104106
if (pos == limit) {
105-
head = segment.pop()
106-
SegmentPool.recycle(segment)
107+
recycleHead()
107108
} else {
108109
segment.pos = pos
109110
}
@@ -138,8 +139,7 @@ public class Buffer : Source, Sink {
138139
size -= 4L
139140

140141
if (pos == limit) {
141-
head = segment.pop()
142-
SegmentPool.recycle(segment)
142+
recycleHead()
143143
} else {
144144
segment.pos = pos
145145
}
@@ -176,8 +176,7 @@ public class Buffer : Source, Sink {
176176
size -= 8L
177177

178178
if (pos == limit) {
179-
head = segment.pop()
180-
SegmentPool.recycle(segment)
179+
recycleHead()
181180
} else {
182181
segment.pos = pos
183182
}
@@ -242,11 +241,10 @@ public class Buffer : Source, Sink {
242241
copy.pos += currentOffset.toInt()
243242
copy.limit = minOf(copy.pos + remainingByteCount.toInt(), copy.limit)
244243
if (out.head == null) {
245-
copy.prev = copy
246-
copy.next = copy.prev
247-
out.head = copy.next
244+
out.head = copy
245+
out.tail = copy
248246
} else {
249-
out.head!!.prev!!.push(copy)
247+
out.tail = out.tail!!.push(copy)
250248
}
251249
remainingByteCount -= (copy.limit - copy.pos).toLong()
252250
currentOffset = 0L
@@ -264,7 +262,7 @@ public class Buffer : Source, Sink {
264262
if (result == 0L) return 0L
265263

266264
// Omit the tail if it's still writable.
267-
val tail = head!!.prev!!
265+
val tail = tail!!
268266
if (tail.limit < Segment.SIZE && tail.owner) {
269267
result -= (tail.limit - tail.pos).toLong()
270268
}
@@ -317,8 +315,7 @@ public class Buffer : Source, Sink {
317315
head.pos += toSkip
318316

319317
if (head.pos == head.limit) {
320-
this.head = head.pop()
321-
SegmentPool.recycle(head)
318+
recycleHead()
322319
}
323320
}
324321
}
@@ -336,8 +333,7 @@ public class Buffer : Source, Sink {
336333
size -= toCopy.toLong()
337334

338335
if (s.pos == s.limit) {
339-
head = s.pop()
340-
SegmentPool.recycle(s)
336+
recycleHead()
341337
}
342338

343339
return toCopy
@@ -377,19 +373,20 @@ public class Buffer : Source, Sink {
377373
internal fun writableSegment(minimumCapacity: Int): Segment {
378374
require(minimumCapacity >= 1 && minimumCapacity <= Segment.SIZE) { "unexpected capacity" }
379375

380-
if (head == null) {
376+
if (tail == null) {
381377
val result = SegmentPool.take() // Acquire a first segment.
382378
head = result
383-
result.prev = result
384-
result.next = result
379+
tail = result
385380
return result
386381
}
387382

388-
var tail = head!!.prev
389-
if (tail!!.limit + minimumCapacity > Segment.SIZE || !tail.owner) {
390-
tail = tail.push(SegmentPool.take()) // Append a new empty segment to fill up.
383+
val t = tail!!
384+
if (t.limit + minimumCapacity > Segment.SIZE || !t.owner) {
385+
val newTail = t.push(SegmentPool.take()) // Append a new empty segment to fill up.
386+
tail = newTail
387+
return newTail
391388
}
392-
return tail
389+
return t
393390
}
394391

395392
override fun write(source: ByteArray, startIndex: Int, endIndex: Int) {
@@ -486,7 +483,7 @@ public class Buffer : Source, Sink {
486483
while (remainingByteCount > 0L) {
487484
// Is a prefix of the source's head segment all that we need to move?
488485
if (remainingByteCount < source.head!!.limit - source.head!!.pos) {
489-
val tail = if (head != null) head!!.prev else null
486+
val tail = tail
490487
if (tail != null && tail.owner &&
491488
remainingByteCount + tail.limit - (if (tail.shared) 0 else tail.pos) <= Segment.SIZE
492489
) {
@@ -498,22 +495,32 @@ public class Buffer : Source, Sink {
498495
} else {
499496
// We're going to need another segment. Split the source's head
500497
// segment in two, then move the first of those two to this buffer.
501-
source.head = source.head!!.split(remainingByteCount.toInt())
498+
val newHead = source.head!!.split(remainingByteCount.toInt())
499+
if (source.head === source.tail) {
500+
source.tail = newHead
501+
}
502+
source.head = newHead
502503
}
503504
}
504505

505506
// Remove the source's head segment and append it to our tail.
506507
val segmentToMove = source.head
507508
val movedByteCount = (segmentToMove!!.limit - segmentToMove.pos).toLong()
508509
source.head = segmentToMove.pop()
510+
if (source.head == null) {
511+
source.tail = null
512+
}
509513
if (head == null) {
510514
head = segmentToMove
511-
segmentToMove.prev = segmentToMove
512-
segmentToMove.next = segmentToMove.prev
515+
tail = segmentToMove
516+
segmentToMove.prev = null
517+
segmentToMove.next = null
513518
} else {
514-
var tail = head!!.prev
515-
tail = tail!!.push(segmentToMove)
516-
tail.compact()
519+
val newTail = tail!!.push(segmentToMove).compact()
520+
tail = newTail
521+
if (newTail.prev == null) {
522+
this.head = newTail
523+
}
517524
}
518525
source.size -= movedByteCount
519526
size += movedByteCount
@@ -582,16 +589,15 @@ public class Buffer : Source, Sink {
582589
val result = Buffer()
583590
if (size == 0L) return result
584591

585-
val head = head!!
592+
val head = this.head!!
586593
val headCopy = head.sharedCopy()
587594

588595
result.head = headCopy
589-
headCopy.prev = result.head
590-
headCopy.next = headCopy.prev
596+
result.tail = headCopy
591597

592598
var s = head.next
593-
while (s !== head) {
594-
headCopy.prev!!.push(s!!.sharedCopy())
599+
while (s != null) {
600+
result.tail = result.tail!!.push(s.sharedCopy())
595601
s = s.next
596602
}
597603

@@ -642,6 +648,48 @@ public class Buffer : Source, Sink {
642648

643649
return "Buffer(size=$size hex=$builder)"
644650
}
651+
652+
/**
653+
* Unlinks and recycles this buffer's head.
654+
*
655+
* If head had a successor, it'll become a new head.
656+
* Otherwise, both [head] and [tail] will be set to null.
657+
*
658+
* It's up to a caller to ensure that the head exists.
659+
*/
660+
internal fun recycleHead() {
661+
val oldHead = head!!
662+
val nextHead = oldHead.next
663+
head = nextHead
664+
if (nextHead == null) {
665+
tail = null
666+
} else {
667+
nextHead.prev = null
668+
}
669+
oldHead.next = null
670+
SegmentPool.recycle(oldHead)
671+
}
672+
673+
/**
674+
* Unlinks and recycles this buffer's tail segment.
675+
*
676+
* If tail had a predecessor, it'll become a new tail.
677+
* Otherwise, both [head] and [tail] will be set to null.
678+
*
679+
* It's up to a caller to ensure that the tail exists.
680+
*/
681+
internal fun recycleTail() {
682+
val oldTail = tail!!
683+
val newTail = oldTail.prev
684+
tail = newTail
685+
if (newTail == null) {
686+
head = null
687+
} else {
688+
newTail.next = null
689+
}
690+
oldTail.prev = null
691+
SegmentPool.recycle(oldTail)
692+
}
645693
}
646694

647695
/**
@@ -652,23 +700,26 @@ internal inline fun <T> Buffer.seek(
652700
fromIndex: Long,
653701
lambda: (Segment?, Long) -> T
654702
): T {
655-
var s: Segment = head ?: return lambda(null, -1L)
703+
if (this.head == null) lambda(null, -1L)
656704

657705
if (size - fromIndex < fromIndex) {
706+
var s = tail
658707
// We're scanning in the back half of this buffer. Find the segment starting at the back.
659708
var offset = size
660-
while (offset > fromIndex) {
661-
s = s.prev!!
709+
while (s != null && offset > fromIndex) {
662710
offset -= (s.limit - s.pos).toLong()
711+
if (offset <= fromIndex) break
712+
s = s.prev
663713
}
664714
return lambda(s, offset)
665715
} else {
716+
var s = this.head
666717
// We're scanning in the front half of this buffer. Find the segment starting at the front.
667718
var offset = 0L
668-
while (true) {
719+
while (s != null) {
669720
val nextOffset = offset + (s.limit - s.pos)
670721
if (nextOffset > fromIndex) break
671-
s = s.next!!
722+
s = s.next
672723
offset = nextOffset
673724
}
674725
return lambda(s, offset)

core/common/src/Buffers.kt

+5-4
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public fun Buffer.snapshot(): ByteString {
2424
check(curr != null) { "Current segment is null" }
2525
append(curr.data, curr.pos, curr.limit)
2626
curr = curr.next
27-
} while (curr !== head)
27+
} while (curr !== null)
2828
}
2929
}
3030

@@ -53,10 +53,11 @@ public fun Buffer.indexOf(byte: Byte, startIndex: Long = 0, endIndex: Long = siz
5353
if (o == -1L) {
5454
return -1L
5555
}
56-
var segment = seg!!
56+
var segment: Segment? = seg!!
5757
var offset = o
5858
do {
5959
check(endOffset > offset)
60+
segment!!
6061
val idx = segment.indexOf(
6162
byte,
6263
// If start index within this segment, the diff will be positive and
@@ -71,8 +72,8 @@ public fun Buffer.indexOf(byte: Byte, startIndex: Long = 0, endIndex: Long = siz
7172
return offset + idx.toLong()
7273
}
7374
offset += segment.size
74-
segment = segment.next!!
75-
} while (segment !== head && offset < endOffset)
75+
segment = segment.next
76+
} while (segment !== null && offset < endOffset)
7677
return -1L
7778
}
7879
}

core/common/src/ByteStrings.kt

+5-4
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,10 @@ public fun Buffer.indexOf(byteString: ByteString, startIndex: Long = 0): Long {
129129
if (o == -1L) {
130130
return -1L
131131
}
132-
var segment = seg!!
132+
var segment: Segment? = seg
133133
var offset = o
134134
do {
135+
segment!!
135136
// If start index within this segment, the diff will be positive and
136137
// we'll scan the segment starting from the corresponding offset.
137138
// Otherwise, the diff will be negative and we'll scan the segment from the beginning.
@@ -147,16 +148,16 @@ public fun Buffer.indexOf(byteString: ByteString, startIndex: Long = 0): Long {
147148
val firstOutboundOffset = maxOf(startOffset, segment.size - byteStringData.size + 1)
148149
// Try to find a pattern in all suffixes shorter than the pattern. These suffixes start
149150
// in the current segment, but ends in the following segments; thus we're using outbound function.
150-
val idx1 = segment.indexOfBytesOutbound(byteStringData, firstOutboundOffset, head)
151+
val idx1 = segment.indexOfBytesOutbound(byteStringData, firstOutboundOffset)
151152
if (idx1 != -1) {
152153
// Offset corresponds to the segment's start, idx - to offset within the segment.
153154
return offset + idx1.toLong()
154155
}
155156

156157
// We scanned the whole segment, so let's go to the next one
157158
offset += segment.size
158-
segment = segment.next!!
159-
} while (segment !== head && offset + byteString.size <= size)
159+
segment = segment.next
160+
} while (segment !== null && offset + byteString.size <= size)
160161
return -1L
161162
}
162163
}

0 commit comments

Comments
 (0)