Skip to content

Commit be2bfca

Browse files
authored
SegmentPool improvements (#352)
- Fixed shared segment leakage from the pool - Fixed segment leakage / excessive allocation when multiple threads taking / recycling them concurrently - Supported configurable second-level pool (4 MB by default, could be configured via `kotlinx.io.pool.size.bytes` system property)
1 parent 3eb8117 commit be2bfca

File tree

11 files changed

+394
-55
lines changed

11 files changed

+394
-55
lines changed

core/api/kotlinx-io-core.api

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public abstract interface class kotlinx/io/RawSource : java/lang/AutoCloseable {
105105
}
106106

107107
public final class kotlinx/io/Segment {
108-
public synthetic fun <init> ([BIIZZLkotlin/jvm/internal/DefaultConstructorMarker;)V
108+
public synthetic fun <init> ([BIILkotlinx/io/SegmentCopyTracker;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V
109109
public final synthetic fun dataAsByteArray (Z)[B
110110
public final synthetic fun getLimit ()I
111111
public final synthetic fun getNext ()Lkotlinx/io/Segment;

core/common/src/Segment.kt

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,47 @@ package kotlinx.io
2323
import kotlin.jvm.JvmField
2424
import kotlin.jvm.JvmSynthetic
2525

26+
/**
27+
* Tracks shared segment copies.
28+
*
29+
* A new [SegmentCopyTracker] instance should be not shared by default (i.e. `shared == false`).
30+
* Any further [addCopy] calls should move the tracker to a shared state (i.e. `shared == true`).
31+
* Once a shared segment copy is recycled, [removeCopy] should be called.
32+
* Depending on implementation, calling [removeCopy] the same number of times as [addCopy] may
33+
* or may not transition the tracked back to unshared stated.
34+
*
35+
* The class is not intended for public use and currently designed to fit the only use case - within JVM SegmentPool
36+
* implementation.
37+
*/
38+
internal abstract class SegmentCopyTracker {
39+
/**
40+
* `true` if a tracker shared by multiple segment copies.
41+
*/
42+
abstract val shared: Boolean
43+
44+
/**
45+
* Track a new copy created by sharing an associated segment.
46+
*/
47+
abstract fun addCopy()
48+
49+
/**
50+
* Records reclamation of a shared segment copy associated with this tracker.
51+
* If a tracker was in unshared state, this call should not affect an internal state.
52+
*
53+
* @return `true` if the segment was not shared *before* this called.
54+
*/
55+
abstract fun removeCopy(): Boolean
56+
}
57+
58+
/**
59+
* Simple [SegmentCopyTracker] that always reports shared state.
60+
*/
61+
internal object AlwaysSharedCopyTracker : SegmentCopyTracker() {
62+
override val shared: Boolean = true
63+
override fun addCopy() = Unit
64+
override fun removeCopy(): Boolean = true
65+
}
66+
2667
/**
2768
* A segment of a buffer.
2869
*
@@ -59,8 +100,17 @@ public class Segment {
59100
internal var limit: Int = 0
60101

61102
/** True if other segments or byte strings use the same byte array. */
62-
@JvmField
63-
internal var shared: Boolean = false
103+
internal val shared: Boolean
104+
get() = copyTracker?.shared ?: false
105+
106+
/**
107+
* Tracks number shared copies
108+
*
109+
* Note that this reference is not `@Volatile` as segments are not thread-safe and it's an error
110+
* to modify the same segment concurrently.
111+
* At the same time, an object [copyTracker] refers to could be modified concurrently.
112+
*/
113+
internal var copyTracker: SegmentCopyTracker? = null
64114

65115
/** True if this segment owns the byte array and can append to it, extending `limit`. */
66116
@JvmField
@@ -81,14 +131,14 @@ public class Segment {
81131
private constructor() {
82132
this.data = ByteArray(SIZE)
83133
this.owner = true
84-
this.shared = false
134+
this.copyTracker = null
85135
}
86136

87-
private constructor(data: ByteArray, pos: Int, limit: Int, shared: Boolean, owner: Boolean) {
137+
private constructor(data: ByteArray, pos: Int, limit: Int, shareToken: SegmentCopyTracker?, owner: Boolean) {
88138
this.data = data
89139
this.pos = pos
90140
this.limit = limit
91-
this.shared = shared
141+
this.copyTracker = shareToken
92142
this.owner = owner
93143
}
94144

@@ -98,8 +148,10 @@ public class Segment {
98148
* prevents it from being pooled.
99149
*/
100150
internal fun sharedCopy(): Segment {
101-
shared = true
102-
return Segment(data, pos, limit, true, false)
151+
val t = copyTracker ?: SegmentPool.tracker().also {
152+
copyTracker = it
153+
}
154+
return Segment(data, pos, limit, t.also { it.addCopy() }, false)
103155
}
104156

105157
/**
@@ -284,8 +336,13 @@ public class Segment {
284336
internal fun new(): Segment = Segment()
285337

286338
@JvmSynthetic
287-
internal fun new(data: ByteArray, pos: Int, limit: Int, shared: Boolean, owner: Boolean): Segment
288-
= Segment(data, pos, limit, shared, owner)
339+
internal fun new(
340+
data: ByteArray,
341+
pos: Int,
342+
limit: Int,
343+
copyTracker: SegmentCopyTracker?,
344+
owner: Boolean
345+
): Segment = Segment(data, pos, limit, copyTracker, owner)
289346
}
290347
}
291348

core/common/src/SegmentPool.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,11 @@ internal expect object SegmentPool {
3838

3939
/** Recycle a segment that the caller no longer needs. */
4040
fun recycle(segment: Segment)
41+
42+
/**
43+
* Allocates a new copy tracker that'll be associated with a segment from this pool.
44+
* For performance reasons, there's no tracker attached to a segment initially.
45+
* Instead, it's allocated lazily on the first sharing attempt.
46+
*/
47+
fun tracker(): SegmentCopyTracker
4148
}

core/common/src/unsafe/UnsafeBufferOperations.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ public object UnsafeBufferOperations {
3838
public fun moveToTail(buffer: Buffer, bytes: ByteArray, startIndex: Int = 0, endIndex: Int = bytes.size) {
3939
checkBounds(bytes.size, startIndex, endIndex)
4040
val segment = Segment.new(
41-
bytes, startIndex, endIndex, shared = true /* to prevent recycling */,
41+
bytes, startIndex, endIndex,
42+
AlwaysSharedCopyTracker, /* to prevent recycling */
4243
owner = false /* can't append to it */
4344
)
4445
val tail = buffer.tail
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.io
7+
8+
import kotlin.test.Test
9+
import kotlin.test.assertTrue
10+
11+
class AlwaysSharedCopyTrackerTest {
12+
@Test
13+
fun stateTransition() {
14+
val tracker = AlwaysSharedCopyTracker
15+
assertTrue(tracker.shared)
16+
17+
assertTrue(tracker.removeCopy())
18+
assertTrue(tracker.shared)
19+
20+
tracker.addCopy()
21+
assertTrue(tracker.shared)
22+
23+
assertTrue(tracker.removeCopy())
24+
assertTrue(tracker.shared)
25+
}
26+
}

core/js/src/SegmentPool.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ internal actual object SegmentPool {
1414

1515
actual fun recycle(segment: Segment) {
1616
}
17+
18+
actual fun tracker(): SegmentCopyTracker = AlwaysSharedCopyTracker
1719
}

0 commit comments

Comments
 (0)