改进 LogWindow (#1971)

* Add CircularArrayList

* fix test

* update

* update
This commit is contained in:
Glavo 2023-02-06 22:55:16 +08:00 committed by GitHub
parent 94b54f3dce
commit 461cde3282
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 592 additions and 1 deletions

View File

@ -26,6 +26,7 @@ import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -37,6 +38,7 @@ import javafx.stage.Stage;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.util.CircularArrayList;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@ -134,7 +136,6 @@ public final class LogWindow extends Stage {
while (logs.size() > config().getLogLines()) {
Log removedLog = logs.removeFirst();
if (!impl.listView.getItems().isEmpty() && impl.listView.getItems().get(0) == removedLog) {
// TODO: fix O(n)
impl.listView.getItems().remove(0);
}
}
@ -162,6 +163,8 @@ public final class LogWindow extends Stage {
LogWindowImpl() {
getStyleClass().add("log-window");
listView.setItems(FXCollections.observableList(new CircularArrayList<>(config().getLogLines() + 1)));
boolean flag = false;
cboLines.getItems().setAll("10000", "5000", "2000", "500");
for (String i : cboLines.getItems())

View File

@ -0,0 +1,348 @@
package org.jackhuang.hmcl.util;
import org.jetbrains.annotations.Contract;
import java.util.*;
/**
* @author Glavo
*/
@SuppressWarnings("unchecked")
public final class CircularArrayList<E> extends AbstractList<E> implements RandomAccess {
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ARRAY = new Object[0];
private Object[] elements;
private int begin = -1;
private int end = 0;
public CircularArrayList() {
this.elements = EMPTY_ARRAY;
}
public CircularArrayList(int initialCapacity) {
if (initialCapacity < 0) {
throw new IllegalArgumentException("illegal initialCapacity: " + initialCapacity);
}
this.elements = initialCapacity == 0 ? EMPTY_ARRAY : new Object[initialCapacity];
}
private static int inc(int i, int capacity) {
return i + 1 >= capacity ? 0 : i + 1;
}
private static int inc(int i, int distance, int capacity) {
if ((i += distance) - capacity >= 0) {
i -= capacity;
}
return i;
}
private static int dec(int i, int capacity) {
return i - 1 < 0 ? capacity - 1 : i - 1;
}
private static int sub(int i, int distance, int capacity) {
if ((i -= distance) < 0) {
i += capacity;
}
return i;
}
private void grow() {
grow(elements.length + 1);
}
private void grow(int minCapacity) {
final int oldCapacity = elements.length;
final int size = size();
final int newCapacity = newCapacity(oldCapacity, minCapacity);
final Object[] newElements;
if (size == 0) {
newElements = new Object[newCapacity];
} else if (begin < end) {
newElements = Arrays.copyOf(elements, newCapacity, Object[].class);
} else {
newElements = new Object[newCapacity];
System.arraycopy(elements, begin, newElements, 0, elements.length - begin);
System.arraycopy(elements, 0, newElements, elements.length - begin, end);
begin = 0;
end = size;
}
this.elements = newElements;
}
private static int newCapacity(int oldCapacity, int minCapacity) {
return oldCapacity == 0
? Math.max(DEFAULT_CAPACITY, minCapacity)
: Math.max(Math.max(oldCapacity, minCapacity), oldCapacity + (oldCapacity >> 1));
}
private static void checkElementIndex(int index, int size) throws IndexOutOfBoundsException {
if (index < 0 || index >= size) {
// Optimized for execution by hotspot
checkElementIndexFailed(index, size);
}
}
@Contract("_, _ -> fail")
private static void checkElementIndexFailed(int index, int size) {
if (size < 0) {
throw new IllegalArgumentException("size(" + size + ") < 0");
}
if (index < 0) {
throw new IndexOutOfBoundsException("index(" + index + ") < 0");
}
if (index >= size) {
throw new IndexOutOfBoundsException("index(" + index + ") >= size(" + size + ")");
}
throw new AssertionError();
}
private static void checkPositionIndex(int index, int size) throws IndexOutOfBoundsException {
if (index < 0 || index > size) {
// Optimized for execution by hotspot
checkPositionIndexFailed(index, size);
}
}
@Contract("_, _ -> fail")
private static void checkPositionIndexFailed(int index, int size) {
if (size < 0) {
throw new IllegalArgumentException("size(" + size + ") < 0");
}
if (index < 0) {
throw new IndexOutOfBoundsException("index(" + index + ") < 0");
}
if (index > size) {
throw new IndexOutOfBoundsException("index(" + index + ") > size(" + size + ")");
}
throw new AssertionError();
}
@Override
public boolean isEmpty() {
return begin == -1;
}
@Override
public int size() {
if (isEmpty()) {
return 0;
} else if (begin < end) {
return end - begin;
} else {
return elements.length - begin + end;
}
}
@Override
public E get(int index) {
if (isEmpty()) {
throw new IndexOutOfBoundsException("Index out of range: " + index);
} else if (begin < end) {
checkElementIndex(index, end - begin);
return (E) elements[begin + index];
} else {
checkElementIndex(index, elements.length - begin + end);
return (E) elements[inc(begin, index, elements.length)];
}
}
@Override
public E set(int index, E element) {
int arrayIndex;
if (isEmpty()) {
throw new IndexOutOfBoundsException();
} else if (begin < end) {
checkElementIndex(index, end - begin);
arrayIndex = begin + index;
} else {
final int size = elements.length - begin + end;
checkElementIndex(index, size);
arrayIndex = inc(begin, index, elements.length);
}
E oldValue = (E) elements[arrayIndex];
elements[arrayIndex] = element;
return oldValue;
}
@Override
public void add(int index, E element) {
if (index == 0) {
addFirst(element);
return;
}
final int oldSize = size();
if (index == oldSize) {
addLast(element);
return;
}
checkPositionIndex(index, oldSize);
if (oldSize == elements.length) {
grow();
}
if (begin < end) {
final int targetIndex = begin + index;
if (end < elements.length) {
System.arraycopy(elements, targetIndex, elements, targetIndex + 1, end - targetIndex);
end++;
} else {
System.arraycopy(elements, begin, elements, begin - 1, targetIndex - begin + 1);
begin--;
}
elements[targetIndex] = element;
} else {
int targetIndex = inc(begin, index, elements.length);
if (targetIndex <= end) {
System.arraycopy(elements, targetIndex, elements, targetIndex + 1, end - targetIndex);
elements[targetIndex] = element;
end++;
} else {
System.arraycopy(elements, begin, elements, begin - 1, targetIndex - begin);
elements[targetIndex - 1] = element;
begin--;
}
}
}
@Override
public E remove(int index) {
final int oldSize = size();
checkElementIndex(index, oldSize);
if (index == 0) {
return removeFirst();
}
if (index == oldSize - 1) {
return removeLast();
}
final Object res;
if (begin < end) {
final int targetIndex = begin + index;
res = elements[targetIndex];
System.arraycopy(elements, targetIndex + 1, elements, targetIndex, end - targetIndex - 1);
end--;
} else {
final int targetIndex = inc(begin, index, elements.length);
res = elements[targetIndex];
if (targetIndex < end) {
System.arraycopy(elements, targetIndex + 1, elements, targetIndex, end - targetIndex - 1);
end--;
} else {
System.arraycopy(elements, begin, elements, begin + 1, targetIndex - begin);
begin = inc(begin, elements.length);
}
}
return (E) res;
}
@Override
public void clear() {
if (isEmpty()) {
return;
}
if (begin < end) {
Arrays.fill(elements, begin, end, null);
} else {
Arrays.fill(elements, 0, end, null);
Arrays.fill(elements, begin, elements.length, null);
}
begin = -1;
end = 0;
}
// Deque
public void addFirst(E e) {
final int oldSize = size();
if (oldSize == elements.length) {
grow();
}
if (oldSize == 0) {
begin = elements.length - 1;
} else {
begin = dec(begin, elements.length);
}
elements[begin] = e;
}
public void addLast(E e) {
final int oldSize = size();
if (oldSize == elements.length) {
grow();
}
elements[end] = e;
end = inc(end, elements.length);
if (oldSize == 0) {
begin = 0;
}
}
public E removeFirst() {
final int oldSize = size();
if (oldSize == 0) {
throw new NoSuchElementException();
}
Object res = elements[begin];
elements[begin] = null;
if (oldSize == 1) {
begin = -1;
end = 0;
} else {
begin = inc(begin, elements.length);
}
return (E) res;
}
public E removeLast() {
final int oldSize = size();
if (oldSize == 0) {
throw new NoSuchElementException();
}
final int lastIdx = dec(end, elements.length);
E res = (E) elements[lastIdx];
elements[lastIdx] = null;
if (oldSize == 1) {
begin = -1;
end = 0;
} else {
end = lastIdx;
}
return res;
}
public E getFirst() {
if (isEmpty())
throw new NoSuchElementException();
return get(0);
}
public E getLast() {
if (isEmpty())
throw new NoSuchElementException();
return get(size() - 1);
}
}

View File

@ -0,0 +1,240 @@
package org.jackhuang.hmcl.util;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* @author Glavo
*/
public class CircularArrayListTest {
private static void assertEmpty(CircularArrayList<?> list) {
assertEquals(0, list.size());
assertTrue(list.isEmpty());
assertThrows(NoSuchElementException.class, () -> list.getFirst());
assertThrows(NoSuchElementException.class, () -> list.getLast());
assertThrows(NoSuchElementException.class, () -> list.removeFirst());
assertThrows(NoSuchElementException.class, () -> list.removeLast());
assertThrows(IndexOutOfBoundsException.class, () -> list.get(0));
assertThrows(IndexOutOfBoundsException.class, () -> list.get(10));
assertThrows(IndexOutOfBoundsException.class, () -> list.get(-1));
}
private static void assertListEquals(List<?> expected, CircularArrayList<?> actual) {
assertIterableEquals(expected, actual);
if (expected.isEmpty()) {
assertEmpty(actual);
} else {
assertEquals(expected.get(0), actual.getFirst());
assertEquals(expected.get(expected.size() - 1), actual.getLast());
assertThrows(IndexOutOfBoundsException.class, () -> actual.get(-1));
assertThrows(IndexOutOfBoundsException.class, () -> actual.get(actual.size()));
}
}
@Test
public void testEmpty() {
CircularArrayList<String> list = new CircularArrayList<>();
assertEmpty(list);
}
@Test
public void testSequential() {
Helper<String> helper = new Helper<>();
helper.addAll("str0", "str1", "str2");
helper.add("str3");
helper.add(2, "str4");
helper.remove(1);
helper.remove(0);
helper.removeFirst();
helper.removeLast();
helper.remove(0);
assertEmpty(helper.list);
}
@Test
public void testSequentialExpansion() {
Helper<String> helper = new Helper<>();
Random random = new Random(0);
for (int i = 0; i < 5; i++) {
helper.add("str" + i);
}
for (int i = 5; i < 100; i++) {
helper.add(random.nextInt(helper.size()) + 1, "str" + i);
}
for (int i = 0; i < 100; i++) {
helper.set(random.nextInt(helper.size()), "new str " + i);
}
for (int i = 0; i < 20; i++) {
helper.remove(random.nextInt(helper.size()));
}
for (int i = 0; i < 20; i++) {
helper.removeFirst();
helper.removeLast();
}
int remaining = helper.size();
for (int i = 0; i < remaining; i++) {
helper.removeLast();
}
}
@Test
public void testLoopback() {
Helper<String> helper = new Helper<>();
helper.addAll("str3", "str4", "str5");
helper.addAll(0, "str0", "str1", "str2");
helper.remove(1);
helper.remove(4);
helper.removeFirst();
helper.removeLast();
helper.remove(1);
helper.remove(0);
assertEmpty(helper.list);
}
@Test
public void testLoopbackExpansion() {
Helper<String> helper = new Helper<>();
Random random = new Random(0);
for (int i = 5; i < 10; i++) {
helper.add("str" + i);
}
for (int i = 4; i >= 0; i--) {
helper.add(0, "str" + i);
}
for (int i = 10; i < 100; i++) {
helper.add(random.nextInt(helper.size() + 1), "str" + i);
}
for (int i = 0; i < 100; i++) {
helper.set(random.nextInt(helper.size()), "new str " + i);
}
for (int i = 0; i < 20; i++) {
helper.remove(random.nextInt(helper.size()));
}
for (int i = 0; i < 20; i++) {
helper.removeFirst();
helper.removeLast();
}
int remaining = helper.size();
for (int i = 0; i < remaining; i++) {
helper.removeLast();
}
}
@Test
public void testClear() {
CircularArrayList<String> list = new CircularArrayList<>();
list.clear();
assertEmpty(list);
for (int i = 0; i < 20; i++) {
list.add("str" + i);
}
list.clear();
assertEmpty(list);
for (int i = 10; i < 20; i++) {
list.add("str" + i);
}
for (int i = 9; i >= 0; i--) {
list.addFirst("str" + i);
}
list.clear();
assertEmpty(list);
}
private static final class Helper<E> {
final List<E> expected;
final CircularArrayList<E> list;
Helper() {
this.expected = new ArrayList<>();
this.list = new CircularArrayList<>();
assertStatus();
}
Helper(List<E> expected, CircularArrayList<E> list) {
this.expected = expected;
this.list = list;
assertStatus();
}
void assertStatus() {
assertListEquals(expected, list);
}
int size() {
return expected.size();
}
void set(int i, E e) {
assertEquals(expected.set(i, e), list.set(i, e));
assertStatus();
}
void add(E e) {
expected.add(e);
list.add(e);
assertStatus();
}
void add(int i, E e) {
expected.add(i, e);
list.add(i, e);
assertStatus();
}
@SafeVarargs
final void addAll(E... values) {
Collections.addAll(expected, values);
Collections.addAll(list, values);
assertStatus();
}
@SafeVarargs
final void addAll(int i, E... values) {
List<E> valuesList = Arrays.asList(values);
assertEquals(expected.addAll(i, valuesList), list.addAll(i, valuesList));
assertStatus();
}
void remove(int idx) {
assertEquals(expected.remove(idx), list.remove(idx));
assertStatus();
}
void removeFirst() {
assertEquals(expected.remove(0), list.removeFirst());
assertStatus();
}
void removeLast() {
assertEquals(expected.remove(expected.size() - 1), list.removeLast());
assertStatus();
}
void clear() {
expected.clear();
list.clear();
assertEmpty(list);
}
}
}