Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added "ViewableList.sync" method for optimized updates with a minimal number of change events. #491

Merged
merged 1 commit into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.jetbrains.rd.util.reactive
import com.jetbrains.rd.util.catch
import com.jetbrains.rd.util.lifetime.Lifetime
import com.jetbrains.rd.util.lifetime.isAlive
import java.util.Objects

class ViewableList<T : Any>(private val storage: MutableList<T> = mutableListOf()) : IMutableViewableList<T> {
override val change = Signal<IViewableList.Event<T>>()
Expand Down Expand Up @@ -56,16 +57,18 @@ class ViewableList<T : Any>(private val storage: MutableList<T> = mutableListOf(
return changes.isNotEmpty()
}

override fun addAll(elements: Collection<T>): Boolean {
private fun addAll(iterator: Iterator<T>): Boolean {
val changes = arrayListOf<IViewableList.Event<T>>()
for (element in elements) {
for (element in iterator) {
storage.add(element)
changes.add(IViewableList.Event.Add(size - 1, element))
}
changes.forEach { change.fire(it) }
return changes.isNotEmpty()
}

override fun addAll(elements: Collection<T>) = addAll(elements.iterator())

override fun clear() {
val changes = arrayListOf<IViewableList.Event<T>>()
for (i in (storage.size-1) downTo 0) {
Expand All @@ -79,6 +82,24 @@ class ViewableList<T : Any>(private val storage: MutableList<T> = mutableListOf(
return filterElementsInplace(elements) { index, elementsSet -> storage[index] in elementsSet }
}

fun removeRange(fromIndex: Int, toIndex: Int) {
when (toIndex - fromIndex) {
0 -> Unit
1 -> removeAt(fromIndex)
else -> removeRangeSlow(fromIndex, toIndex)
}
}

private fun removeRangeSlow(fromIndex: Int, toIndex: Int) {
val changes = buildList<IViewableList.Event<T>>(toIndex - fromIndex) {
for (i in (toIndex - 1) downTo fromIndex) {
add(IViewableList.Event.Remove(i, storage[i]))
}
}
storage.subList(fromIndex, toIndex).clear()
changes.forEach { change.fire(it) }
}

private inline fun filterElementsInplace(elements: Collection<T>, predicate: (Int, Set<T>) -> Boolean): Boolean {
val elementsSet = elements.toSet()
val changes = arrayListOf<IViewableList.Event<T>>()
Expand Down Expand Up @@ -107,7 +128,172 @@ class ViewableList<T : Any>(private val storage: MutableList<T> = mutableListOf(
override fun listIterator(): MutableListIterator<T> = MyIterator(storage.listIterator())
override fun listIterator(index: Int): MutableListIterator<T> = MyIterator(storage.listIterator(index))

override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> = throw UnsupportedOperationException()
override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
Objects.checkFromToIndex(fromIndex, toIndex, size)
return MySubList(fromIndex, toIndex - fromIndex)
}

/**
* Synchronizes the viewable list by adding missing elements and removing unmatched elements.
* If the order of equal values is not changed, then they won't be modified.
* However, even if equal elements exist in both lists,
* but order is swapped, then they will be removed and re-added to satisfy the new values order.
* It helps drastically reduce the number of change events if the collection is unmodified at all
* or just a few elements are changed compared to the classical approach with 'clear' and 'addAll'.
*
* @param newValues the new values to be synced with
</T> */
fun sync(newValues: Collection<T>): Boolean {
if (isEmpty()) {
return addAll(newValues)
}

if (newValues.isEmpty()) {
clear()
return true
}

val iterator = iterator()
val newIterator = newValues.iterator()

var index = 0
var newValue: T
while (true) {
newValue = newIterator.next()
if (newValue != iterator.next())
{
replaceTailSlow(index, newValue, newIterator)
return true
}
++index
if (!newIterator.hasNext()) {
removeRange(index, size)
return true
}
if (!iterator.hasNext()) {
return addAll(newIterator)
}
}
}

private fun replaceTailSlow(firstUnmatchedIndex: Int, firstUnmatchedValue: T, newIterator: Iterator<T>) {
fun matchIndex(items: MutableMap<T, Any>, value: T, fromIndex: Int): Int? {
val matchedIndex = items.remove(value)
if (matchedIndex is Int) {
return if (matchedIndex >= fromIndex) matchedIndex else null
}
@Suppress("UNCHECKED_CAST")
(matchedIndex as? ArrayDeque<Int>)?.let {
while (matchedIndex.size > 0) {
val endIndex = matchedIndex.removeFirst()
if (endIndex >= fromIndex) {
if (matchedIndex.size > 0) {
items[value] = matchedIndex
}
return endIndex
}
}
}
return null
}

val items = mutableMapOf<T, Any>()
var newValue = firstUnmatchedValue
for (index in firstUnmatchedIndex until size) {
val item = this[index]
val itemIndex = items[item]
@Suppress("UNCHECKED_CAST")
when (itemIndex) {
is Int -> items[item] = ArrayDeque<Int>().apply {
add(itemIndex)
add(index)
}
is ArrayDeque<*> -> (itemIndex as ArrayDeque<Int>).add(index)
null -> items[item] = index
}
}

val changes = ArrayDeque<IViewableList.Event<T>>()
val originalSize = size
var insertIndex = firstUnmatchedIndex
var processedIndex = firstUnmatchedIndex
var matchedIndex: Any?
while (true) {
matchedIndex = matchIndex(items, newValue, processedIndex)
if (matchedIndex != null) {
val removeCount = matchedIndex - processedIndex
if (removeCount > 0) {
for (removeIndex in processedIndex until matchedIndex) {
changes.addFirst(IViewableList.Event.Remove(removeIndex, storage[removeIndex]))
}
}
processedIndex = matchedIndex + 1
storage.add(storage[matchedIndex])
++insertIndex
}
else {
changes.add(IViewableList.Event.Add(insertIndex++, newValue))
storage.add(newValue)
}
if (!newIterator.hasNext())
break
newValue = newIterator.next()
}

// If last new value was matched then we generate remove events after all "add"
// events so last "remove" event will match the tail for "viewTail" extension property.
// Otherwise, we keep an "add" event for the last element and generate all "remove" events at the beginning
if (matchedIndex != null) {
val addedElementsAdjustment = insertIndex - processedIndex
for (removeIndex in originalSize - 1 downTo processedIndex) {
changes.add(IViewableList.Event.Remove(removeIndex + addedElementsAdjustment, storage[removeIndex]))
}
}
else {
for (removeIndex in processedIndex until originalSize) {
changes.addFirst(IViewableList.Event.Remove(removeIndex, storage[removeIndex]))
}
}

storage.subList(firstUnmatchedIndex, originalSize).clear()

changes.forEach { change.fire(it) }
}

private inner class MySubList(private val fromIndex: Int, size: Int) : AbstractMutableList<T>() {
var mySize = size

override val size get() = mySize

override fun add(index: Int, element: T) {
Objects.checkIndex(index, mySize + 1)
[email protected](fromIndex + index, element).also { ++mySize }
}

override fun get(index: Int): T {
Objects.checkIndex(index, mySize)
return this@ViewableList[index]
}

override fun removeAt(index: Int): T {
Objects.checkIndex(index, mySize)
return [email protected](index).also { --mySize }
}

override fun set(index: Int, element: T): T {
Objects.checkIndex(index, mySize)
return [email protected](index, element)
}

override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
Objects.checkFromToIndex(fromIndex, toIndex, mySize)
return MySubList(this.fromIndex + fromIndex, toIndex - fromIndex)
}

override fun clear() {
[email protected](fromIndex, fromIndex + size).also { mySize = 0 }
}
}

private inner class MyIterator(val baseIterator: MutableListIterator<T>): MutableListIterator<T> by baseIterator {
override fun add(element: T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package com.jetbrains.rd.util.test.cases
import com.jetbrains.rd.util.lifetime.Lifetime
import com.jetbrains.rd.util.lifetime.plusAssign
import com.jetbrains.rd.util.reactive.IMutableViewableList
import com.jetbrains.rd.util.reactive.IViewableList
import com.jetbrains.rd.util.reactive.ViewableList
import com.jetbrains.rd.util.reactive.viewableTail
import com.jetbrains.rd.util.test.framework.RdTestBase
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.*

class ViewableListTest : RdTestBase() {
@Test
Expand Down Expand Up @@ -43,7 +43,7 @@ class ViewableListTest : RdTestBase() {
Lifetime.using { lifetime ->
list.view(lifetime) { lt, value -> log.add("View $value"); lt += { log.add("UnView $value") } }
list.add(0)
list.set(0, 1);
list[0] = 1
list.remove(0)
}

Expand Down Expand Up @@ -151,4 +151,64 @@ class ViewableListTest : RdTestBase() {
assertTrue(list.add(0))
}
}

@Test
fun testSync() {
val items = ViewableList(mutableListOf(1, 2, 3))
items.assertSync(listOf(1, 2, 3), emptyList(), emptyList())
items.assertSync(listOf(3, 2, 1), listOf(2 to 1, 1 to 2), listOf(2 to 1, 1 to 0))
items.assertSync(listOf(4, 3, 2, 1, 0), listOf(4 to 0, 0 to 4), emptyList())
items.assertSync(listOf(3, 2, 1), emptyList(), listOf(4 to 0, 0 to 3))
items.assertSync(listOf(4, 2, 0), listOf(4 to 0, 0 to 2), listOf(1 to 2, 3 to 0))
items.assertSync(emptyList(), emptyList(), listOf(0 to 2, 2 to 1, 4 to 0))
items.assertSync(listOf(1, 2, 3, 4, 5), listOf(1 to 0, 2 to 1, 3 to 2, 4 to 3, 5 to 4), emptyList())
items.assertSync(listOf(2, 1, 3, 5, 4), listOf(1 to 1, 4 to 4), listOf(4 to 3, 1 to 0))
items.assertSync(listOf(2, 1, 4), emptyList(), listOf(5 to 3, 3 to 2))
items.assertSync(listOf(2, 3, 1, 5, 4), listOf(3 to 1, 5 to 3), emptyList())
items.assertSync(listOf(2, 2, 3, 3, 1, 1, 5, 5, 4, 4), listOf(2 to 1, 3 to 3, 1 to 5, 5 to 7, 4 to 9), emptyList())
items.assertSync(listOf(2, 2, 3, 1, 1, 5, 4, 4), emptyList(), listOf(5 to 7, 3 to 3))
items.assertSync(listOf(2, 2, 2, 5, 5, 5), listOf(2 to 2, 5 to 4, 5 to 5), listOf(4 to 7, 4 to 6, 1 to 4, 1 to 3, 3 to 2))
items.assertSync(listOf(2, 5), emptyList(), listOf(2 to 2, 2 to 1, 5 to 3, 5 to 2))
}

@Test
fun testViewableTail() {
val items = ViewableList(mutableListOf(1, 2, 3))
Lifetime.using { lifetime ->
val tail = mutableListOf<Int?>()
items.viewableTail().advise(lifetime) { tail.add(it) }
items.add(4)
items.addAll(listOf(5, 6, 7))
items.remove(6)
items.remove(7)
items.removeAll(listOf(2, 3, 4, 5, 6, 7))
items.sync(listOf(2, 3))
items.sync(listOf(1, 2))
assertContentEquals(listOf(3, 4, 7, 5, 1, 3, 2), tail)
}
}

private fun <T : Any> ViewableList<T>.assertSync(expectedItems: List<T>, expectedAdded: List<Pair<T, Int>>, expectedRemoved: List<Pair<T, Int>>) {
assertItemsAndChanges(expectedItems, expectedAdded, expectedRemoved) {
sync(expectedItems)
}
}

private fun <T : Any> ViewableList<T>.assertItemsAndChanges(expectedItems: List<T>, expectedAdded: List<Pair<T, Int>>, expectedRemoved: List<Pair<T, Int>>, action: ViewableList<T>.() -> Unit) {
Lifetime.using { lifetime ->
val added = mutableListOf<Pair<T, Int>>()
val removed = mutableListOf<Pair<T, Int>>()
change.advise(lifetime) {
when (it) {
is IViewableList.Event.Add -> added.add(it.newValue to it.index)
is IViewableList.Event.Remove -> removed.add(it.oldValue to it.index)
is IViewableList.Event.Update -> {}
}
}
action()
assertContentEquals(expectedItems, this)
assertContentEquals(expectedAdded, added)
assertContentEquals(expectedRemoved, removed)
}
}
}
Loading