diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/collections/SegmentedLongArray.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/collections/SegmentedLongArray.java index c551895c51a92..f796ccf0c1239 100644 --- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/collections/SegmentedLongArray.java +++ b/pulsar-common/src/main/java/org/apache/pulsar/common/util/collections/SegmentedLongArray.java @@ -18,111 +18,259 @@ */ package org.apache.pulsar.common.util.collections; -import io.netty.buffer.ByteBuf; -import java.util.ArrayList; -import java.util.List; +import static com.google.common.base.Preconditions.checkArgument; +import com.google.common.annotations.VisibleForTesting; +import java.util.Arrays; import javax.annotation.concurrent.NotThreadSafe; import lombok.Getter; -import org.apache.pulsar.common.allocator.PulsarByteBufAllocator; +/** + * A growable array of {@code long} values backed by heap-allocated segments. + * + *
This class provides a logical contiguous {@code long[]} whose size may exceed + * {@link Integer#MAX_VALUE}. Internally, the storage is split into fixed-size + * segments, allowing capacities larger than a single Java array while keeping + * element access in constant time. + * + *
Segment layout invariant: + *
The segment invariant guarantees that the bit based address mapping + * ({@code offset >>> segmentShift}, {@code offset & segmentMask}) remains + * valid for every logical offset. + * + *
Growing and shrinking preserve existing contents while maintaining the + * segment layout invariant. + * + *
This class is not thread-safe.
+ */
@NotThreadSafe
public class SegmentedLongArray implements AutoCloseable {
- private static final int SIZE_OF_LONG = 8;
+ /**
+ * Default number of {@code long} values in a full segment. Production behavior
+ * is fixed by this constant; tests may override it via the
+ * {@link #SegmentedLongArray(long, int) package-private constructor}.
+ */
+ static final int DEFAULT_SEGMENT_SIZE = 2 * 1024 * 1024;
+
+ static {
+ assert Integer.bitCount(DEFAULT_SEGMENT_SIZE) == 1
+ : "DEFAULT_SEGMENT_SIZE must be a power of 2";
+ }
+
+ private final int segmentSize;
+ private final int segmentShift;
+ private final int segmentMask;
- private static final int MAX_SEGMENT_SIZE = 2 * 1024 * 1024; // 2M longs -> 16 MB
- private final List Intended for unit tests that exercise multi-segment grow/shrink behavior
+ * without allocating production-sized (16 MiB) backing arrays. Production
+ * callers should use {@link #SegmentedLongArray(long)}.
+ *
+ * @param initialCapacity initial capacity in {@code long} elements
+ * @param segmentSize number of {@code long} values per full segment; must be
+ * a positive power of two
+ * @throws IllegalArgumentException if {@code initialCapacity <= 0} or
+ * {@code segmentSize} is not a positive power of two
+ */
+ @VisibleForTesting
+ SegmentedLongArray(long initialCapacity, int segmentSize) {
+ checkArgument(initialCapacity > 0, "initialCapacity must be positive");
+ checkArgument(segmentSize > 0 && Integer.bitCount(segmentSize) == 1,
+ "segmentSize must be a positive power of two");
+ this.segmentSize = segmentSize;
+ this.segmentShift = Integer.numberOfTrailingZeros(segmentSize);
+ this.segmentMask = segmentSize - 1;
this.initialCapacity = initialCapacity;
- this.capacity = this.initialCapacity;
+ this.capacity = initialCapacity;
+ allocateSegments(initialCapacity);
+ }
+
+ /**
+ * Allocates the initial segment layout.
+ */
+ private void allocateSegments(long longCapacity) {
+ segmentCount = Math.max(1, (int) ((longCapacity + segmentSize - 1) / segmentSize));
+ segments = new long[segmentCount][];
+
+ long remaining = longCapacity;
+ long bytes = 0;
+
+ for (int i = 0; i < segmentCount; i++) {
+ int size = (int) Math.min(segmentSize, remaining);
+ segments[i] = new long[size];
+ bytes += (long) size * Long.BYTES;
+ remaining -= size;
+ }
+
+ allocatedBytes = bytes;
}
public void writeLong(long offset, long value) {
- int bufferIdx = (int) (offset / MAX_SEGMENT_SIZE);
- int internalIdx = (int) (offset % MAX_SEGMENT_SIZE);
- buffers.get(bufferIdx).setLong(internalIdx * SIZE_OF_LONG, value);
+ long[] segment = segments[(int) (offset >>> segmentShift)];
+ segment[(int) (offset & segmentMask)] = value;
}
public long readLong(long offset) {
- int bufferIdx = (int) (offset / MAX_SEGMENT_SIZE);
- int internalIdx = (int) (offset % MAX_SEGMENT_SIZE);
- return buffers.get(bufferIdx).getLong(internalIdx * SIZE_OF_LONG);
+ long[] segment = segments[(int) (offset >>> segmentShift)];
+ return segment[(int) (offset & segmentMask)];
+ }
+
+ /**
+ * Ensures that the backing storage can hold at least {@code required}
+ * elements.
+ *
+ * @param required minimum required capacity in {@code long} elements
+ */
+ public void ensureCapacity(long required) {
+ if (required <= capacity) {
+ return;
+ }
+
+ long geometric;
+ if (capacity < segmentSize) {
+ geometric = Math.min(
+ capacity + (capacity <= 256 ? capacity : capacity / 2),
+ segmentSize);
+ } else {
+ geometric = capacity + segmentSize;
+ }
+
+ growTo(Math.max(required, geometric));
}
public void increaseCapacity() {
- if (capacity < MAX_SEGMENT_SIZE) {
- // Resize the current buffer to bigger capacity
- capacity += (capacity <= 256 ? capacity : capacity / 2);
- capacity = Math.min(capacity, MAX_SEGMENT_SIZE);
- buffers.get(0).capacity((int) this.capacity * SIZE_OF_LONG);
- buffers.get(0).writerIndex((int) this.capacity * SIZE_OF_LONG);
+ ensureCapacity(capacity + 1);
+ }
+
+ /**
+ * Expands the backing storage to exactly {@code newCapacity}.
+ */
+ private void growTo(long newCapacity) {
+ if (newCapacity <= capacity) {
+ return;
+ }
+
+ int newSegmentCount = (int) ((newCapacity + segmentSize - 1) / segmentSize);
+
+ if (segments.length < newSegmentCount) {
+ segments = Arrays.copyOf(segments, newSegmentCount);
+ }
+
+ // If the current last segment becomes an interior segment,
+ // it must be expanded to preserve the bit-based address mapping.
+ if (newSegmentCount > segmentCount && segmentCount >= 1) {
+ int oldLastIdx = segmentCount - 1;
+ if (segments[oldLastIdx].length < segmentSize) {
+ resizeLastSegment(oldLastIdx, segmentSize);
+ }
+ }
+
+ for (int i = segmentCount; i < newSegmentCount - 1; i++) {
+ segments[i] = new long[segmentSize];
+ allocatedBytes += (long) segmentSize * Long.BYTES;
+ }
+
+ int newLastIdx = newSegmentCount - 1;
+ int newLastSize = (int) (newCapacity - (long) newLastIdx * segmentSize);
+
+ if (newLastIdx >= segmentCount) {
+ segments[newLastIdx] = new long[newLastSize];
+ allocatedBytes += (long) newLastSize * Long.BYTES;
} else {
- // Let's add 1 mode buffer to the list
- int bufferSize = MAX_SEGMENT_SIZE * SIZE_OF_LONG;
- ByteBuf buffer = PulsarByteBufAllocator.DEFAULT.directBuffer(bufferSize, bufferSize);
- buffer.writerIndex(bufferSize);
- buffers.add(buffer);
- capacity += MAX_SEGMENT_SIZE;
+ resizeLastSegment(newLastIdx, newLastSize);
}
+
+ segmentCount = newSegmentCount;
+ capacity = newCapacity;
}
+ private void resizeLastSegment(int idx, int newSize) {
+ long[] old = segments[idx];
+ if (old.length == newSize) {
+ return;
+ }
+
+ allocatedBytes += (long) (newSize - old.length) * Long.BYTES;
+ segments[idx] = Arrays.copyOf(old, newSize);
+ }
+
+ /**
+ * Shrinks the backing storage to {@code newCapacity}.
+ *
+ * @param newCapacity target capacity in {@code long} elements
+ */
public void shrink(long newCapacity) {
if (newCapacity >= capacity || newCapacity < initialCapacity) {
return;
}
- long sizeToReduce = capacity - newCapacity;
- while (sizeToReduce >= MAX_SEGMENT_SIZE && buffers.size() > 1) {
- ByteBuf b = buffers.remove(buffers.size() - 1);
- b.release();
- capacity -= MAX_SEGMENT_SIZE;
- sizeToReduce -= MAX_SEGMENT_SIZE;
+ int newSegmentCount = (int) ((newCapacity + segmentSize - 1) / segmentSize);
+ int newLastIdx = newSegmentCount - 1;
+ int newLastSize = (int) (newCapacity - (long) newLastIdx * segmentSize);
+
+ for (int i = newSegmentCount; i < segmentCount; i++) {
+ allocatedBytes -= (long) segments[i].length * Long.BYTES;
+ segments[i] = null;
}
- if (buffers.size() == 1 && sizeToReduce > 0) {
- // We should also reduce the capacity of the first buffer
- capacity -= sizeToReduce;
- ByteBuf oldBuffer = buffers.get(0);
- ByteBuf newBuffer = PulsarByteBufAllocator.DEFAULT.directBuffer((int) capacity * SIZE_OF_LONG);
- oldBuffer.getBytes(0, newBuffer, (int) capacity * SIZE_OF_LONG);
- oldBuffer.release();
- buffers.set(0, newBuffer);
+ resizeLastSegment(newLastIdx, newLastSize);
+
+ segmentCount = newSegmentCount;
+ capacity = newCapacity;
+
+ if (segments.length > Math.max(segmentCount * 2L, 16)) {
+ segments = Arrays.copyOf(segments, segmentCount);
}
}
@Override
public void close() {
- buffers.forEach(ByteBuf::release);
+ segments = null;
+ segmentCount = 0;
+ capacity = 0;
+ allocatedBytes = 0;
}
/**
- * The amount of memory used to back the array of longs.
+ * Returns the physical heap memory reserved by the backing arrays.
+ *
+ * @return allocated bytes occupied by all backing segments
*/
public long bytesCapacity() {
- return capacity * SIZE_OF_LONG;
+ return allocatedBytes;
}
}
diff --git a/pulsar-common/src/main/java/org/apache/pulsar/common/util/collections/TripleLongPriorityQueue.java b/pulsar-common/src/main/java/org/apache/pulsar/common/util/collections/TripleLongPriorityQueue.java
index cd878c6428459..97fb3b2460d63 100644
--- a/pulsar-common/src/main/java/org/apache/pulsar/common/util/collections/TripleLongPriorityQueue.java
+++ b/pulsar-common/src/main/java/org/apache/pulsar/common/util/collections/TripleLongPriorityQueue.java
@@ -23,7 +23,7 @@
/**
* Provides a priority-queue implementation specialized on items composed by 3 longs.
*
- * This class is not thread safe and the items are stored in direct memory.
+ * This class is not thread safe.
*
* Algorithm
*
@@ -109,9 +109,7 @@ public void close() {
*/
public void add(long n1, long n2, long n3) {
long arrayIdx = tuplesCount * ITEMS_COUNT;
- if ((arrayIdx + 2) >= array.getCapacity()) {
- array.increaseCapacity();
- }
+ array.ensureCapacity(arrayIdx + ITEMS_COUNT);
siftUp(tuplesCount, n1, n2, n3);
++tuplesCount;
diff --git a/pulsar-common/src/test/java/org/apache/pulsar/common/util/collections/SegmentedLongArrayTest.java b/pulsar-common/src/test/java/org/apache/pulsar/common/util/collections/SegmentedLongArrayTest.java
index f6c216c439c21..ac41dd0be79e1 100644
--- a/pulsar-common/src/test/java/org/apache/pulsar/common/util/collections/SegmentedLongArrayTest.java
+++ b/pulsar-common/src/test/java/org/apache/pulsar/common/util/collections/SegmentedLongArrayTest.java
@@ -19,12 +19,16 @@
package org.apache.pulsar.common.util.collections;
import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.fail;
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.assertTrue;
import lombok.Cleanup;
import org.testng.annotations.Test;
public class SegmentedLongArrayTest {
+ /** Small segment size so tests can exercise many-segment behavior without 16 MiB allocations. */
+ private static final int TEST_SEGMENT_SIZE = 1024;
+
@Test
public void testArray() {
@Cleanup
@@ -38,15 +42,9 @@ public void testArray() {
a.writeLong(2, 2);
a.writeLong(3, Long.MAX_VALUE);
- try {
- a.writeLong(4, Long.MIN_VALUE);
- fail("should have failed");
- } catch (IndexOutOfBoundsException e) {
- // Expected
- }
+ assertThrows(IndexOutOfBoundsException.class, () -> a.writeLong(4, Long.MIN_VALUE));
a.increaseCapacity();
-
a.writeLong(4, Long.MIN_VALUE);
assertEquals(a.getCapacity(), 8);
@@ -67,10 +65,10 @@ public void testArray() {
@Test
public void testLargeArray() {
- long initialCap = 3 * 1024 * 1024;
+ long initialCap = TEST_SEGMENT_SIZE + TEST_SEGMENT_SIZE / 2;
@Cleanup
- SegmentedLongArray a = new SegmentedLongArray(initialCap);
+ SegmentedLongArray a = new SegmentedLongArray(initialCap, TEST_SEGMENT_SIZE);
assertEquals(a.getCapacity(), initialCap);
assertEquals(a.bytesCapacity(), initialCap * 8);
assertEquals(a.getInitialCapacity(), initialCap);
@@ -85,8 +83,9 @@ public void testLargeArray() {
a.increaseCapacity();
- assertEquals(a.getCapacity(), 5 * 1024 * 1024);
- assertEquals(a.bytesCapacity(), 5 * 1024 * 1024 * 8);
+ long expectedCap = initialCap + TEST_SEGMENT_SIZE;
+ assertEquals(a.getCapacity(), expectedCap);
+ assertEquals(a.bytesCapacity(), expectedCap * 8);
assertEquals(a.getInitialCapacity(), initialCap);
assertEquals(a.readLong(baseOffset), 0);
@@ -100,4 +99,339 @@ public void testLargeArray() {
assertEquals(a.bytesCapacity(), initialCap * 8);
assertEquals(a.getInitialCapacity(), initialCap);
}
+
+ @Test
+ public void testIncreaseCapacityGrowthPattern() {
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(4);
+
+ a.increaseCapacity();
+ assertEquals(a.getCapacity(), 8);
+ a.increaseCapacity();
+ assertEquals(a.getCapacity(), 16);
+ a.increaseCapacity();
+ assertEquals(a.getCapacity(), 32);
+ }
+
+ @Test
+ public void testIncreaseCapacityReachesSegmentBoundary() {
+ long start = TEST_SEGMENT_SIZE - 100;
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(start, TEST_SEGMENT_SIZE);
+ assertEquals(a.getCapacity(), start);
+
+ a.increaseCapacity();
+ assertEquals(a.getCapacity(), TEST_SEGMENT_SIZE);
+
+ a.increaseCapacity();
+ assertEquals(a.getCapacity(), TEST_SEGMENT_SIZE * 2L);
+ }
+
+ @Test
+ public void testMultiSegmentIncreaseCapacity() {
+ long initialCap = TEST_SEGMENT_SIZE * 3;
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(initialCap, TEST_SEGMENT_SIZE);
+
+ for (int i = 0; i < 20; i++) {
+ a.increaseCapacity();
+ }
+
+ long expectedCap = TEST_SEGMENT_SIZE * 23L;
+ assertEquals(a.getCapacity(), expectedCap);
+
+ for (int i = 0; i < 23; i++) {
+ long offset = (long) i * TEST_SEGMENT_SIZE + 42;
+ a.writeLong(offset, i);
+ assertEquals(a.readLong(offset), i);
+ }
+ }
+
+ @Test
+ public void testShrinkDropsWholeSegments() {
+ long segSize = TEST_SEGMENT_SIZE;
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(segSize, TEST_SEGMENT_SIZE);
+ for (int i = 0; i < 4; i++) {
+ a.increaseCapacity();
+ }
+ assertEquals(a.getCapacity(), segSize * 5);
+
+ for (int i = 0; i < 5; i++) {
+ a.writeLong((long) i * segSize, 100L + i);
+ }
+
+ a.shrink(segSize * 3);
+ assertEquals(a.getCapacity(), segSize * 3);
+
+ for (int i = 0; i < 3; i++) {
+ assertEquals(a.readLong((long) i * segSize), 100L + i);
+ }
+ }
+
+ @Test
+ public void testShrinkToInitialCapacity() {
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(4);
+ a.increaseCapacity();
+ a.increaseCapacity();
+
+ a.shrink(4);
+ assertEquals(a.getCapacity(), 4);
+ assertEquals(a.getInitialCapacity(), 4);
+ }
+
+ @Test
+ public void testShrinkBelowInitialFails() {
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(100);
+ a.shrink(50);
+ assertEquals(a.getCapacity(), 100);
+ }
+
+ @Test
+ public void testSegmentBoundaryReadWrite() {
+ long segSize = TEST_SEGMENT_SIZE;
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(segSize * 2, TEST_SEGMENT_SIZE);
+
+ a.writeLong(segSize - 1, 111L);
+ a.writeLong(segSize, 222L);
+
+ assertEquals(a.readLong(segSize - 1), 111L);
+ assertEquals(a.readLong(segSize), 222L);
+ }
+
+ @Test
+ public void testRoundTripAllValues() {
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(1000);
+
+ long[] testValues = {0, 1, -1, Long.MAX_VALUE, Long.MIN_VALUE,
+ 255L, 256L, 65535L, 65536L,
+ Integer.MAX_VALUE, Integer.MIN_VALUE};
+
+ for (int i = 0; i < testValues.length; i++) {
+ a.writeLong(i, testValues[i]);
+ }
+ for (int i = 0; i < testValues.length; i++) {
+ assertEquals(a.readLong(i), testValues[i]);
+ }
+ }
+
+ @Test
+ public void testCloseReleasesMemory() {
+ SegmentedLongArray a = new SegmentedLongArray(100);
+ a.close();
+ assertThrows(NullPointerException.class, () -> a.readLong(0));
+ }
+
+ @Test
+ public void testZeroCapacityRejected() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ @Cleanup
+ SegmentedLongArray ignored = new SegmentedLongArray(0);
+ });
+ }
+
+ @Test
+ public void testNegativeCapacityRejected() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ @Cleanup
+ SegmentedLongArray ignored = new SegmentedLongArray(-1);
+ });
+ }
+
+ @Test
+ public void testGrowAfterShrink() {
+ long segSize = TEST_SEGMENT_SIZE;
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(segSize, TEST_SEGMENT_SIZE);
+ a.increaseCapacity();
+ a.increaseCapacity();
+ a.shrink(segSize);
+ assertEquals(a.getCapacity(), segSize);
+
+ a.writeLong(0, 42L);
+
+ a.increaseCapacity();
+ a.writeLong(segSize, 99L);
+
+ assertEquals(a.readLong(0), 42L);
+ assertEquals(a.readLong(segSize), 99L);
+ }
+
+ @Test
+ public void testShrinkNoOpWhenEqual() {
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(100);
+ a.increaseCapacity(); // 200
+ a.shrink(200); // newCapacity == capacity, no-op
+ assertEquals(a.getCapacity(), 200);
+ }
+
+ @Test
+ public void testShrinkNoOpWhenExceeds() {
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(100);
+ a.increaseCapacity(); // 200
+ a.shrink(300); // newCapacity > capacity, no-op
+ assertEquals(a.getCapacity(), 200);
+ }
+
+ @Test
+ public void testNegativeOffset() {
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(10);
+ assertThrows(IndexOutOfBoundsException.class, () -> a.readLong(-1));
+ }
+
+ @Test
+ public void testSmallInitialCapacityDoesNotAllocateFullSegment() {
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(48, TEST_SEGMENT_SIZE);
+ assertEquals(a.getCapacity(), 48);
+ assertEquals(a.bytesCapacity(), 48 * 8);
+ assertTrue(a.bytesCapacity() < TEST_SEGMENT_SIZE * 8);
+ }
+
+ @Test
+ public void testPartialLastSegmentMapping() {
+ long seg = TEST_SEGMENT_SIZE;
+ long cap = seg + 1000;
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(cap, TEST_SEGMENT_SIZE);
+ assertEquals(a.getCapacity(), cap);
+ assertEquals(a.bytesCapacity(), cap * 8);
+ a.writeLong(0, 1L);
+ a.writeLong(seg - 1, 2L);
+ a.writeLong(seg, 3L);
+ a.writeLong(cap - 1, 4L);
+ assertEquals(a.readLong(0), 1L);
+ assertEquals(a.readLong(seg - 1), 2L);
+ assertEquals(a.readLong(seg), 3L);
+ assertEquals(a.readLong(cap - 1), 4L);
+ }
+
+ @Test
+ public void testIncreaseCapacityPromotesPartialLastToFull() {
+ long seg = TEST_SEGMENT_SIZE;
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(seg * 2 + seg / 2, TEST_SEGMENT_SIZE);
+ long partialBase = seg * 2;
+ a.writeLong(partialBase, 99L);
+ a.writeLong(partialBase + seg / 2 - 1, 100L);
+ long capacityBefore = a.getCapacity();
+ a.increaseCapacity();
+ assertTrue(a.getCapacity() > capacityBefore);
+ assertEquals(a.readLong(partialBase), 99L);
+ assertEquals(a.readLong(partialBase + seg / 2 - 1), 100L);
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+ }
+
+ @Test
+ public void testBytesCapacityTracksPhysicalThroughGrowAndShrink() {
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(1000);
+ assertEquals(a.getCapacity(), 1000L);
+ assertEquals(a.bytesCapacity(), 8000L);
+ for (int i = 0; i < 5; i++) {
+ a.increaseCapacity();
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+ }
+ a.shrink(2000);
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+ }
+
+ @Test
+ public void testOffsetMappingAndCapacityAcrossOperations() {
+ long seg = TEST_SEGMENT_SIZE;
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(50, TEST_SEGMENT_SIZE);
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+
+ for (int i = 0; i < 5; i++) {
+ a.increaseCapacity();
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+ }
+ a.ensureCapacity(seg * 3 + 1000);
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+ a.writeLong(0, 1L);
+ a.writeLong(seg - 1, 2L);
+ a.writeLong(seg, 3L);
+ a.writeLong(a.getCapacity() - 1, 4L);
+ assertEquals(a.readLong(0), 1L);
+ assertEquals(a.readLong(seg - 1), 2L);
+ assertEquals(a.readLong(seg), 3L);
+ assertEquals(a.readLong(a.getCapacity() - 1), 4L);
+
+ a.shrink(seg * 2 + 500);
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+ a.shrink(seg);
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+ a.ensureCapacity(seg * 2 + 100);
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+ a.writeLong(seg, 7L);
+ assertEquals(a.readLong(seg), 7L);
+ }
+
+ @Test
+ public void testAllocatedBytesDeltaAtEveryMutationSite() {
+ long seg = TEST_SEGMENT_SIZE;
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(100, TEST_SEGMENT_SIZE);
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+
+ for (int i = 0; i < 4; i++) {
+ a.increaseCapacity();
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+ }
+ a.ensureCapacity(seg * 3 + 1000);
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+ a.shrink(seg + 500);
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+ a.shrink(seg / 2);
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+ a.ensureCapacity(seg * 2 + 100);
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+ }
+
+ @Test
+ public void testShrinkTrimsOverallocatedContainer() {
+ long seg = TEST_SEGMENT_SIZE;
+ @Cleanup
+ SegmentedLongArray a = new SegmentedLongArray(50, TEST_SEGMENT_SIZE);
+ a.ensureCapacity(seg * 30);
+ assertTrue(a.getCapacity() >= seg * 30);
+ a.shrink(seg / 2);
+ assertEquals(a.bytesCapacity(), a.getCapacity() * 8);
+ a.writeLong(0, 1L);
+ a.writeLong(a.getCapacity() - 1, 2L);
+ assertEquals(a.readLong(0), 1L);
+ assertEquals(a.readLong(a.getCapacity() - 1), 2L);
+ }
+
+ @Test
+ public void testCustomSegmentSizeRejectsNonPowerOfTwo() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ @Cleanup
+ SegmentedLongArray ignored = new SegmentedLongArray(100, 1000);
+ });
+ }
+
+ @Test
+ public void testCustomSegmentSizeRejectsZero() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ @Cleanup
+ SegmentedLongArray ignored = new SegmentedLongArray(100, 0);
+ });
+ }
+
+ @Test
+ public void testCustomSegmentSizeRejectsNegative() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ @Cleanup
+ SegmentedLongArray ignored = new SegmentedLongArray(100, -1024);
+ });
+ }
}