Merge changes from topic "log-buffer" into tm-dev am: ff1b7dc538

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/18097841

Change-Id: Ic6fa080a2faa224ccb54f170d7b3874e438622e5
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Ned Burns
2022-05-13 21:38:06 +00:00
committed by Automerger Merge Worker
10 changed files with 316 additions and 95 deletions

View File

@@ -19,13 +19,14 @@ package com.android.systemui.log
import android.os.Trace import android.os.Trace
import android.util.Log import android.util.Log
import com.android.systemui.log.dagger.LogModule import com.android.systemui.log.dagger.LogModule
import com.android.systemui.util.collection.RingBuffer
import java.io.PrintWriter import java.io.PrintWriter
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.ArrayDeque
import java.util.Locale import java.util.Locale
import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.BlockingQueue import java.util.concurrent.BlockingQueue
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.math.max
/** /**
* A simple ring buffer of recyclable log messages * A simple ring buffer of recyclable log messages
@@ -63,29 +64,22 @@ import kotlin.concurrent.thread
* *
* Buffers are provided by [LogModule]. Instances should be created using a [LogBufferFactory]. * Buffers are provided by [LogModule]. Instances should be created using a [LogBufferFactory].
* *
* @param name The name of this buffer * @param name The name of this buffer, printed when the buffer is dumped and in some other
* @param maxLogs The maximum number of messages to keep in memory at any one time, including the * situations.
* unused pool. Must be >= [poolSize]. * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start
* @param poolSize The maximum amount that the size of the buffer is allowed to flex in response to * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches
* sequential calls to [document] that aren't immediately followed by a matching call to [push]. * the maximum, it behaves like a ring buffer.
*/ */
class LogBuffer @JvmOverloads constructor( class LogBuffer @JvmOverloads constructor(
private val name: String, private val name: String,
private val maxLogs: Int, private val maxSize: Int,
private val poolSize: Int,
private val logcatEchoTracker: LogcatEchoTracker, private val logcatEchoTracker: LogcatEchoTracker,
private val systrace: Boolean = true private val systrace: Boolean = true
) { ) {
init { private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() }
if (maxLogs < poolSize) {
throw IllegalArgumentException("maxLogs must be greater than or equal to poolSize, " +
"but maxLogs=$maxLogs < $poolSize=poolSize")
}
}
private val buffer: ArrayDeque<LogMessageImpl> = ArrayDeque() private val echoMessageQueue: BlockingQueue<LogMessage>? =
private val echoMessageQueue: BlockingQueue<LogMessageImpl>? = if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null
if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(poolSize) else null
init { init {
if (logcatEchoTracker.logInBackgroundThread && echoMessageQueue != null) { if (logcatEchoTracker.logInBackgroundThread && echoMessageQueue != null) {
@@ -104,6 +98,9 @@ class LogBuffer @JvmOverloads constructor(
var frozen = false var frozen = false
private set private set
private val mutable
get() = !frozen && maxSize > 0
/** /**
* Logs a message to the log buffer * Logs a message to the log buffer
* *
@@ -138,34 +135,19 @@ class LogBuffer @JvmOverloads constructor(
initializer: LogMessage.() -> Unit, initializer: LogMessage.() -> Unit,
noinline printer: LogMessage.() -> String noinline printer: LogMessage.() -> String
) { ) {
if (!frozen) {
val message = obtain(tag, level, printer)
initializer(message)
push(message)
}
}
/**
* Same as [log], but doesn't push the message to the buffer. Useful if you need to supply a
* "reason" for doing something (the thing you supply the reason to will presumably call [push]
* on that message at some point).
*/
inline fun document(
tag: String,
level: LogLevel,
initializer: LogMessage.() -> Unit,
noinline printer: LogMessage.() -> String
): LogMessage {
val message = obtain(tag, level, printer) val message = obtain(tag, level, printer)
initializer(message) initializer(message)
return message commit(message)
} }
/** /**
* Obtains an instance of [LogMessageImpl], usually from the object pool. If the pool has been * You should call [log] instead of this method.
* exhausted, creates a new instance.
* *
* In general, you should call [log] or [document] instead of this method. * Obtains the next [LogMessage] from the ring buffer. If the buffer is not yet at max size,
* grows the buffer by one.
*
* After calling [obtain], the message will now be at the end of the buffer. The caller must
* store any relevant data on the message and then call [commit].
*/ */
@Synchronized @Synchronized
fun obtain( fun obtain(
@@ -173,28 +155,26 @@ class LogBuffer @JvmOverloads constructor(
level: LogLevel, level: LogLevel,
printer: (LogMessage) -> String printer: (LogMessage) -> String
): LogMessageImpl { ): LogMessageImpl {
val message = when { if (!mutable) {
frozen -> LogMessageImpl.create() return FROZEN_MESSAGE
buffer.size > maxLogs - poolSize -> buffer.removeFirst()
else -> LogMessageImpl.create()
} }
val message = buffer.advance()
message.reset(tag, level, System.currentTimeMillis(), printer) message.reset(tag, level, System.currentTimeMillis(), printer)
return message return message
} }
/** /**
* Pushes a message into buffer, possibly evicting an older message if the buffer is full. * You should call [log] instead of this method.
*
* After acquiring a message via [obtain], call this method to signal to the buffer that you
* have finished filling in its data fields. The message will be echoed to logcat if
* necessary.
*/ */
@Synchronized @Synchronized
fun push(message: LogMessage) { fun commit(message: LogMessage) {
if (frozen) { if (!mutable) {
return return
} }
if (buffer.size == maxLogs) {
Log.e(TAG, "LogBuffer $name has exceeded its pool size")
buffer.removeFirst()
}
buffer.add(message as LogMessageImpl)
// Log in the background thread only if echoMessageQueue exists and has capacity (checking // Log in the background thread only if echoMessageQueue exists and has capacity (checking
// capacity avoids the possibility of blocking this thread) // capacity avoids the possibility of blocking this thread)
if (echoMessageQueue != null && echoMessageQueue.remainingCapacity() > 0) { if (echoMessageQueue != null && echoMessageQueue.remainingCapacity() > 0) {
@@ -210,7 +190,7 @@ class LogBuffer @JvmOverloads constructor(
} }
/** Sends message to echo after determining whether to use Logcat and/or systrace. */ /** Sends message to echo after determining whether to use Logcat and/or systrace. */
private fun echoToDesiredEndpoints(message: LogMessageImpl) { private fun echoToDesiredEndpoints(message: LogMessage) {
val includeInLogcat = logcatEchoTracker.isBufferLoggable(name, message.level) || val includeInLogcat = logcatEchoTracker.isBufferLoggable(name, message.level) ||
logcatEchoTracker.isTagLoggable(message.tag, message.level) logcatEchoTracker.isTagLoggable(message.tag, message.level)
echo(message, toLogcat = includeInLogcat, toSystrace = systrace) echo(message, toLogcat = includeInLogcat, toSystrace = systrace)
@@ -219,19 +199,17 @@ class LogBuffer @JvmOverloads constructor(
/** Converts the entire buffer to a newline-delimited string */ /** Converts the entire buffer to a newline-delimited string */
@Synchronized @Synchronized
fun dump(pw: PrintWriter, tailLength: Int) { fun dump(pw: PrintWriter, tailLength: Int) {
val start = if (tailLength <= 0) { 0 } else { buffer.size - tailLength } val iterationStart = if (tailLength <= 0) { 0 } else { max(0, buffer.size - tailLength) }
for ((i, message) in buffer.withIndex()) { for (i in iterationStart until buffer.size) {
if (i >= start) { dumpMessage(buffer[i], pw)
dumpMessage(message, pw)
}
} }
} }
/** /**
* "Freezes" the contents of the buffer, making them immutable until [unfreeze] is called. * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called.
* Calls to [log], [document], [obtain], and [push] will not affect the buffer and will return * Calls to [log], [obtain], and [commit] will not affect the buffer and will return dummy
* dummy values if necessary. * values if necessary.
*/ */
@Synchronized @Synchronized
fun freeze() { fun freeze() {
@@ -293,3 +271,4 @@ class LogBuffer @JvmOverloads constructor(
private const val TAG = "LogBuffer" private const val TAG = "LogBuffer"
private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
private val FROZEN_MESSAGE = LogMessageImpl.create()

View File

@@ -27,22 +27,21 @@ class LogBufferFactory @Inject constructor(
private val logcatEchoTracker: LogcatEchoTracker private val logcatEchoTracker: LogcatEchoTracker
) { ) {
/* limit the size of maxPoolSize for low ram (Go) devices */ /* limit the size of maxPoolSize for low ram (Go) devices */
private fun poolLimit(maxPoolSize_requested: Int): Int { private fun adjustMaxSize(requestedMaxSize: Int): Int {
if (ActivityManager.isLowRamDeviceStatic()) { return if (ActivityManager.isLowRamDeviceStatic()) {
return minOf(maxPoolSize_requested, 20) /* low ram max log size*/ minOf(requestedMaxSize, 20) /* low ram max log size*/
} else { } else {
return maxPoolSize_requested requestedMaxSize
} }
} }
@JvmOverloads @JvmOverloads
fun create( fun create(
name: String, name: String,
maxPoolSize: Int, maxSize: Int,
flexSize: Int = 10,
systrace: Boolean = true systrace: Boolean = true
): LogBuffer { ): LogBuffer {
val buffer = LogBuffer(name, poolLimit(maxPoolSize), flexSize, logcatEchoTracker, systrace) val buffer = LogBuffer(name, adjustMaxSize(maxSize), logcatEchoTracker, systrace)
dumpManager.registerBuffer(name, buffer) dumpManager.registerBuffer(name, buffer)
return buffer return buffer
} }

View File

@@ -60,7 +60,7 @@ class LogcatEchoTrackerDebug private constructor(
Settings.Global.getUriFor(BUFFER_PATH), Settings.Global.getUriFor(BUFFER_PATH),
true, true,
object : ContentObserver(Handler(mainLooper)) { object : ContentObserver(Handler(mainLooper)) {
override fun onChange(selfChange: Boolean, uri: Uri) { override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri) super.onChange(selfChange, uri)
cachedBufferLevels.clear() cachedBufferLevels.clear()
} }
@@ -70,7 +70,7 @@ class LogcatEchoTrackerDebug private constructor(
Settings.Global.getUriFor(TAG_PATH), Settings.Global.getUriFor(TAG_PATH),
true, true,
object : ContentObserver(Handler(mainLooper)) { object : ContentObserver(Handler(mainLooper)) {
override fun onChange(selfChange: Boolean, uri: Uri) { override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri) super.onChange(selfChange, uri)
cachedTagLevels.clear() cachedTagLevels.clear()
} }
@@ -110,7 +110,7 @@ class LogcatEchoTrackerDebug private constructor(
} }
private fun parseProp(propValue: String?): LogLevel { private fun parseProp(propValue: String?): LogLevel {
return when (propValue?.toLowerCase()) { return when (propValue?.lowercase()) {
"verbose" -> LogLevel.VERBOSE "verbose" -> LogLevel.VERBOSE
"v" -> LogLevel.VERBOSE "v" -> LogLevel.VERBOSE
"debug" -> LogLevel.DEBUG "debug" -> LogLevel.DEBUG

View File

@@ -49,8 +49,7 @@ public class LogModule {
@SysUISingleton @SysUISingleton
@NotificationLog @NotificationLog
public static LogBuffer provideNotificationsLogBuffer(LogBufferFactory factory) { public static LogBuffer provideNotificationsLogBuffer(LogBufferFactory factory) {
return factory.create("NotifLog", 1000 /* maxPoolSize */, return factory.create("NotifLog", 1000 /* maxSize */, false /* systrace */);
10 /* flexSize */, false /* systrace */);
} }
/** Provides a logging buffer for all logs related to the data layer of notifications. */ /** Provides a logging buffer for all logs related to the data layer of notifications. */
@@ -74,8 +73,7 @@ public class LogModule {
@SysUISingleton @SysUISingleton
@NotificationSectionLog @NotificationSectionLog
public static LogBuffer provideNotificationSectionLogBuffer(LogBufferFactory factory) { public static LogBuffer provideNotificationSectionLogBuffer(LogBufferFactory factory) {
return factory.create("NotifSectionLog", 1000 /* maxPoolSize */, return factory.create("NotifSectionLog", 1000 /* maxSize */, false /* systrace */);
10 /* flexSize */, false /* systrace */);
} }
/** Provides a logging buffer for all logs related to the data layer of notifications. */ /** Provides a logging buffer for all logs related to the data layer of notifications. */
@@ -91,8 +89,7 @@ public class LogModule {
@SysUISingleton @SysUISingleton
@QSLog @QSLog
public static LogBuffer provideQuickSettingsLogBuffer(LogBufferFactory factory) { public static LogBuffer provideQuickSettingsLogBuffer(LogBufferFactory factory) {
return factory.create("QSLog", 500 /* maxPoolSize */, return factory.create("QSLog", 500 /* maxSize */, false /* systrace */);
10 /* flexSize */, false /* systrace */);
} }
/** Provides a logging buffer for {@link com.android.systemui.broadcast.BroadcastDispatcher} */ /** Provides a logging buffer for {@link com.android.systemui.broadcast.BroadcastDispatcher} */
@@ -100,8 +97,8 @@ public class LogModule {
@SysUISingleton @SysUISingleton
@BroadcastDispatcherLog @BroadcastDispatcherLog
public static LogBuffer provideBroadcastDispatcherLogBuffer(LogBufferFactory factory) { public static LogBuffer provideBroadcastDispatcherLogBuffer(LogBufferFactory factory) {
return factory.create("BroadcastDispatcherLog", 500 /* maxPoolSize */, return factory.create("BroadcastDispatcherLog", 500 /* maxSize */,
10 /* flexSize */, false /* systrace */); false /* systrace */);
} }
/** Provides a logging buffer for all logs related to Toasts shown by SystemUI. */ /** Provides a logging buffer for all logs related to Toasts shown by SystemUI. */
@@ -139,8 +136,8 @@ public class LogModule {
@SysUISingleton @SysUISingleton
@QSFragmentDisableLog @QSFragmentDisableLog
public static LogBuffer provideQSFragmentDisableLogBuffer(LogBufferFactory factory) { public static LogBuffer provideQSFragmentDisableLogBuffer(LogBufferFactory factory) {
return factory.create("QSFragmentDisableFlagsLog", 10 /* maxPoolSize */, return factory.create("QSFragmentDisableFlagsLog", 10 /* maxSize */,
10 /* flexSize */, false /* systrace */); false /* systrace */);
} }
/** /**

View File

@@ -14,9 +14,9 @@
package com.android.systemui.statusbar.phone package com.android.systemui.statusbar.phone
import android.view.MotionEvent import android.view.MotionEvent
import com.android.internal.util.RingBuffer
import com.android.systemui.dump.DumpsysTableLogger import com.android.systemui.dump.DumpsysTableLogger
import com.android.systemui.dump.Row import com.android.systemui.dump.Row
import com.android.systemui.util.collection.RingBuffer
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@@ -67,16 +67,9 @@ class NPVCDownEventState private constructor(
* Do not use [append] to add new elements. Instead use [insert], as it will recycle if * Do not use [append] to add new elements. Instead use [insert], as it will recycle if
* necessary. * necessary.
*/ */
class Buffer( class Buffer(capacity: Int) {
capacity: Int
) : RingBuffer<NPVCDownEventState>(NPVCDownEventState::class.java, capacity) {
override fun append(t: NPVCDownEventState?) {
throw UnsupportedOperationException("Not supported, use insert instead")
}
override fun createNewItem(): NPVCDownEventState { private val buffer = RingBuffer(capacity) { NPVCDownEventState() }
return NPVCDownEventState()
}
/** /**
* Insert a new element in the buffer. * Insert a new element in the buffer.
@@ -94,7 +87,7 @@ class NPVCDownEventState private constructor(
touchSlopExceededBeforeDown: Boolean, touchSlopExceededBeforeDown: Boolean,
lastEventSynthesized: Boolean lastEventSynthesized: Boolean
) { ) {
nextSlot.apply { buffer.advance().apply {
this.timeStamp = timeStamp this.timeStamp = timeStamp
this.x = x this.x = x
this.y = y this.y = y
@@ -115,7 +108,7 @@ class NPVCDownEventState private constructor(
* @see NPVCDownEventState.asStringList * @see NPVCDownEventState.asStringList
*/ */
fun toList(): List<Row> { fun toList(): List<Row> {
return toArray().map { it.asStringList } return buffer.asSequence().map { it.asStringList }.toList()
} }
} }

View File

@@ -0,0 +1,122 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.util.collection
import kotlin.math.max
/**
* A simple ring buffer implementation
*
* Use [advance] to get the least recent item in the buffer (and then presumably fill it with
* appropriate data). This will cause it to become the most recent item.
*
* As the buffer is used, it will grow, allocating new instances of T using [factory] until it
* reaches [maxSize]. After this point, no new instances will be created. Instead, the "oldest"
* instances will be recycled from the back of the buffer and placed at the front.
*
* @param maxSize The maximum size the buffer can grow to before it begins functioning as a ring.
* @param factory A function that creates a fresh instance of T. Used by the buffer while it's
* growing to [maxSize].
*/
class RingBuffer<T>(
private val maxSize: Int,
private val factory: () -> T
) : Iterable<T> {
private val buffer = MutableList<T?>(maxSize) { null }
/**
* An abstract representation that points to the "end" of the buffer. Increments every time
* [advance] is called and never wraps. Use [indexOf] to calculate the associated index into
* the backing array. Always points to the "next" available slot in the buffer. Before the
* buffer has completely filled, the value pointed to will be null. Afterward, it will be the
* value at the "beginning" of the buffer.
*
* This value is unlikely to overflow. Assuming [advance] is called at rate of 100 calls/ms,
* omega will overflow after a little under three million years of continuous operation.
*/
private var omega: Long = 0
/**
* The number of items currently stored in the buffer. Calls to [advance] will cause this value
* to increase by one until it reaches [maxSize].
*/
val size: Int
get() = if (omega < maxSize) omega.toInt() else maxSize
/**
* Advances the buffer's position by one and returns the value that is now present at the "end"
* of the buffer. If the buffer is not yet full, uses [factory] to create a new item.
* Otherwise, reuses the value that was previously at the "beginning" of the buffer.
*
* IMPORTANT: The value is returned as-is, without being reset. It will retain any data that
* was previously stored on it.
*/
fun advance(): T {
val index = indexOf(omega)
omega += 1
val entry = buffer[index] ?: factory().also {
buffer[index] = it
}
return entry
}
/**
* Returns the value stored at [index], which can range from 0 (the "start", or oldest element
* of the buffer) to [size] - 1 (the "end", or newest element of the buffer).
*/
operator fun get(index: Int): T {
if (index < 0 || index >= size) {
throw IndexOutOfBoundsException("Index $index is out of bounds")
}
// If omega is larger than the maxSize, then the buffer is full, and omega is equivalent
// to the "start" of the buffer. If omega is smaller than the maxSize, then the buffer is
// not yet full and our start should be 0. However, in modspace, maxSize and 0 are
// equivalent, so we can get away with using it as the start value instead.
val start = max(omega, maxSize.toLong())
return buffer[indexOf(start + index)]!!
}
inline fun forEach(action: (T) -> Unit) {
for (i in 0 until size) {
action(get(i))
}
}
override fun iterator(): Iterator<T> {
return object : Iterator<T> {
private var position: Int = 0
override fun next(): T {
if (position >= size) {
throw NoSuchElementException()
}
return get(position).also { position += 1 }
}
override fun hasNext(): Boolean {
return position < size
}
}
}
private fun indexOf(position: Long): Int {
return (position % maxSize).toInt()
}
}

View File

@@ -24,7 +24,7 @@ import com.android.systemui.log.LogcatEchoTracker
* Creates a LogBuffer that will echo everything to logcat, which is useful for debugging tests. * Creates a LogBuffer that will echo everything to logcat, which is useful for debugging tests.
*/ */
fun logcatLogBuffer(name: String = "EchoToLogcatLogBuffer") = fun logcatLogBuffer(name: String = "EchoToLogcatLogBuffer") =
LogBuffer(name, 50, 50, LogcatEchoTrackerAlways()) LogBuffer(name, 50, LogcatEchoTrackerAlways())
/** /**
* A [LogcatEchoTracker] that always allows echoing to the logcat. * A [LogcatEchoTracker] that always allows echoing to the logcat.

View File

@@ -30,7 +30,7 @@ class LSShadeTransitionLoggerTest : SysuiTestCase() {
@Before @Before
fun setup() { fun setup() {
logger = LSShadeTransitionLogger( logger = LSShadeTransitionLogger(
LogBuffer("Test", 10, 10, mock()), LogBuffer("Test", 10, mock()),
gestureLogger, gestureLogger,
displayMetrics) displayMetrics)
} }

View File

@@ -379,7 +379,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
mCommandQueue, mCommandQueue,
mCarrierConfigTracker, mCarrierConfigTracker,
new CollapsedStatusBarFragmentLogger( new CollapsedStatusBarFragmentLogger(
new LogBuffer("TEST", 1, 1, mock(LogcatEchoTracker.class)), new LogBuffer("TEST", 1, mock(LogcatEchoTracker.class)),
new DisableFlagsLogger() new DisableFlagsLogger()
), ),
mOperatorNameViewControllerFactory, mOperatorNameViewControllerFactory,

View File

@@ -0,0 +1,131 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.util.collection
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertSame
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
class RingBufferTest : SysuiTestCase() {
private val buffer = RingBuffer(5) { TestElement() }
private val history = mutableListOf<TestElement>()
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
}
@Test
fun testBarelyFillBuffer() {
fillBuffer(5)
assertEquals(0, buffer[0].id)
assertEquals(1, buffer[1].id)
assertEquals(2, buffer[2].id)
assertEquals(3, buffer[3].id)
assertEquals(4, buffer[4].id)
}
@Test
fun testPartiallyFillBuffer() {
fillBuffer(3)
assertEquals(3, buffer.size)
assertEquals(0, buffer[0].id)
assertEquals(1, buffer[1].id)
assertEquals(2, buffer[2].id)
assertThrows(IndexOutOfBoundsException::class.java) { buffer[3] }
assertThrows(IndexOutOfBoundsException::class.java) { buffer[4] }
}
@Test
fun testSpinBuffer() {
fillBuffer(277)
assertEquals(272, buffer[0].id)
assertEquals(273, buffer[1].id)
assertEquals(274, buffer[2].id)
assertEquals(275, buffer[3].id)
assertEquals(276, buffer[4].id)
assertThrows(IndexOutOfBoundsException::class.java) { buffer[5] }
assertEquals(5, buffer.size)
}
@Test
fun testElementsAreRecycled() {
fillBuffer(23)
assertSame(history[4], buffer[1])
assertSame(history[9], buffer[1])
assertSame(history[14], buffer[1])
assertSame(history[19], buffer[1])
}
@Test
fun testIterator() {
fillBuffer(13)
val iterator = buffer.iterator()
for (i in 0 until 5) {
assertEquals(history[8 + i], iterator.next())
}
assertFalse(iterator.hasNext())
assertThrows(NoSuchElementException::class.java) { iterator.next() }
}
@Test
fun testForEach() {
fillBuffer(13)
var i = 8
buffer.forEach {
assertEquals(history[i], it)
i++
}
assertEquals(13, i)
}
private fun fillBuffer(count: Int) {
for (i in 0 until count) {
val elem = buffer.advance()
elem.id = history.size
history.add(elem)
}
}
}
private class TestElement(var id: Int = 0) {
override fun toString(): String {
return "{TestElement $id}"
}
}