diff --git a/pbj-core/pbj-runtime/src/jmh/java/com/hedera/pbj/runtime/io/BufferedDataGetBytes.java b/pbj-core/pbj-runtime/src/jmh/java/com/hedera/pbj/runtime/io/BufferedDataGetBytes.java new file mode 100644 index 00000000..39b65a2c --- /dev/null +++ b/pbj-core/pbj-runtime/src/jmh/java/com/hedera/pbj/runtime/io/BufferedDataGetBytes.java @@ -0,0 +1,196 @@ +package com.hedera.pbj.runtime.io; + +import com.hedera.pbj.runtime.io.buffer.BufferedData; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import java.nio.ByteBuffer; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +@Fork(value = 1) +@State(Scope.Benchmark) +public class BufferedDataGetBytes { + + @Param({"10000"}) + public int size = 10000; + + @Param({"1000"}) + public int window = 1000; + + private BufferedData heapData; + private BufferedData directData; + + private boolean printSum; + + @Setup(Level.Trial) + public void init() { + heapData = BufferedData.allocate(size); + directData = BufferedData.allocateOffHeap(size); + for (int i = 0; i < size; i++) { + heapData.writeByte((byte) (i % 111)); + directData.writeByte((byte) (i % 111)); + } + } + + @Setup(Level.Iteration) + public void initEach() { + printSum = true; + } + + private static long sum(final byte[] arr) { + long result = 0; + for (int i = 0; i < arr.length; i++) { + result += arr[i]; + } + return result; + } + + private static long sum(final ByteBuffer buf) { + long result = 0; + for (int i = 0; i < buf.capacity(); i++) { + result += buf.get(i); + } + return result; + } + + private static long sum(final Bytes bytes) { + long result = 0; + for (int i = 0; i < bytes.length(); i++) { + result += bytes.getByte(i); + } + return result; + } + + @Benchmark + public void heapToByteArray(final Blackhole blackhole) { + final byte[] dst = new byte[window]; + long sum = 0; + for (int i = 0; i < size - window; i++) { + heapData.getBytes(i, dst); +// sum += sum(dst); + blackhole.consume(dst); + } + if (printSum) { +// System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + @Benchmark + public void heapToHeapByteBuffer(final Blackhole blackhole) { + final ByteBuffer dst = ByteBuffer.allocate(window); + long sum = 0; + for (int i = 0; i < size - window; i++) { + heapData.getBytes(i, dst); +// sum += sum(dst); + blackhole.consume(dst); + } + if (printSum) { +// System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + @Benchmark + public void heapToDirectByteBuffer(final Blackhole blackhole) { + final ByteBuffer dst = ByteBuffer.allocateDirect(window); + long sum = 0; + for (int i = 0; i < size - window; i++) { + heapData.getBytes(i, dst); +// sum += sum(dst); + blackhole.consume(dst); + } + if (printSum) { +// System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + @Benchmark + public void heapToBytes(final Blackhole blackhole) { + long sum = 0; + for (int i = 0; i < size - window; i++) { + final Bytes bytes = heapData.getBytes(i, window); +// sum += sum(bytes); + blackhole.consume(bytes); + } + if (printSum) { +// System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + @Benchmark + public void directToByteArray(final Blackhole blackhole) { + final byte[] dst = new byte[window]; + long sum = 0; + for (int i = 0; i < size - window; i++) { + directData.getBytes(i, dst); +// sum += sum(dst); + blackhole.consume(dst); + } + if (printSum) { +// System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + @Benchmark + public void directToHeapByteBuffer(final Blackhole blackhole) { + final ByteBuffer dst = ByteBuffer.allocate(window); + long sum = 0; + for (int i = 0; i < size - window; i++) { + directData.getBytes(i, dst); +// sum += sum(dst); + blackhole.consume(dst); + } + if (printSum) { +// System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + @Benchmark + public void directToDirectByteBuffer(final Blackhole blackhole) { + final ByteBuffer dst = ByteBuffer.allocateDirect(window); + long sum = 0; + for (int i = 0; i < size - window; i++) { + directData.getBytes(i, dst); +// sum += sum(dst); + blackhole.consume(dst); + } + if (printSum) { +// System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + @Benchmark + public void directToBytes(final Blackhole blackhole) { + long sum = 0; + for (int i = 0; i < size - window; i++) { + final Bytes bytes = directData.getBytes(i, window); +// sum += sum(bytes); + blackhole.consume(bytes); + } + if (printSum) { +// System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + +} diff --git a/pbj-core/pbj-runtime/src/jmh/java/com/hedera/pbj/runtime/io/ByteBufferGetByte.java b/pbj-core/pbj-runtime/src/jmh/java/com/hedera/pbj/runtime/io/ByteBufferGetByte.java new file mode 100644 index 00000000..081a3398 --- /dev/null +++ b/pbj-core/pbj-runtime/src/jmh/java/com/hedera/pbj/runtime/io/ByteBufferGetByte.java @@ -0,0 +1,131 @@ +package com.hedera.pbj.runtime.io; + +import java.nio.ByteBuffer; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +@Fork(value = 1) +@State(Scope.Benchmark) +public class ByteBufferGetByte { + + @Param({"10000"}) + public int size = 10000; + + private ByteBuffer heapBuffer; + private ByteBuffer directBuffer; + + private boolean printSum; + + @Setup(Level.Trial) + public void init() { + heapBuffer = ByteBuffer.allocate(size); + directBuffer = ByteBuffer.allocateDirect(size); + for (int i = 0; i < size; i++) { + heapBuffer.put((byte) (i % 111)); + directBuffer.put((byte) (i % 111)); + } + } + + @Setup(Level.Iteration) + public void initEach() { + printSum = true; + } + + @Setup(Level.Invocation) + public void initEachInvocation() { + heapBuffer.clear(); + directBuffer.clear(); + } + + @Benchmark + public void heapArrayGet(final Blackhole blackhole) { + long sum = 0; + final byte[] array = heapBuffer.array(); + for (int i = 0; i < size; i++) { +// sum += array[i]; + blackhole.consume(array[i]); + } + if (printSum) { + System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + @Benchmark + public void heapBufferGet(final Blackhole blackhole) { + long sum = 0; + for (int i = 0; i < size; i++) { +// sum += heapBuffer.get(i); + blackhole.consume(heapBuffer.get(i)); + } + if (printSum) { + System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + @Benchmark + public void heapBufferRead(final Blackhole blackhole) { + long sum = 0; + for (int i = 0; i < size; i++) { +// sum += heapBuffer.get(); + blackhole.consume(heapBuffer.get()); + } + if (printSum) { + System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + @Benchmark + public void directBufferGet(final Blackhole blackhole) { + long sum = 0; + for (int i = 0; i < size; i++) { +// sum += directBuffer.get(i); + blackhole.consume(directBuffer.get(i)); + } + if (printSum) { + System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + @Benchmark + public void heapUnsafeGet(final Blackhole blackhole) { + long sum = 0; + for (int i = 0; i < size; i++) { +// sum += UnsafeUtils.getByteHeap(heapBuffer, i); + blackhole.consume(UnsafeUtils.getByteHeap(heapBuffer, i)); + } + if (printSum) { + System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + @Benchmark + public void directUnsafeGet(final Blackhole blackhole) { + long sum = 0; + for (int i = 0; i < size; i++) { +// sum += UnsafeUtils.getByteDirect(directBuffer, i); + blackhole.consume(UnsafeUtils.getByteDirect(directBuffer, i)); + } + if (printSum) { + System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + +} diff --git a/pbj-core/pbj-runtime/src/jmh/java/com/hedera/pbj/runtime/io/ByteBufferGetBytes.java b/pbj-core/pbj-runtime/src/jmh/java/com/hedera/pbj/runtime/io/ByteBufferGetBytes.java new file mode 100644 index 00000000..35a3d5a9 --- /dev/null +++ b/pbj-core/pbj-runtime/src/jmh/java/com/hedera/pbj/runtime/io/ByteBufferGetBytes.java @@ -0,0 +1,163 @@ +package com.hedera.pbj.runtime.io; + +import java.nio.ByteBuffer; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +@Fork(value = 1) +@State(Scope.Benchmark) +public class ByteBufferGetBytes { + + @Param({"10000"}) + public int size = 10000; + + @Param({"1000"}) + public int window = 1000; + + private ByteBuffer heapBuffer; + private ByteBuffer directBuffer; + + private boolean printSum; + + @Setup(Level.Trial) + public void init() { + heapBuffer = ByteBuffer.allocate(size); + directBuffer = ByteBuffer.allocateDirect(size); + for (int i = 0; i < size; i++) { + heapBuffer.put((byte) (i % 111)); + directBuffer.put((byte) (i % 111)); + } + } + + @Setup(Level.Iteration) + public void initEach() { + printSum = true; + } + + private static long sum(final byte[] arr) { + long result = 0; + for (int i = 0; i < arr.length; i++) { + result += arr[i]; + } + return result; + } + + private static long sum(final ByteBuffer buf) { + long result = 0; + for (int i = 0; i < buf.capacity(); i++) { + result += buf.get(i); + } + return result; + } + + // Heap buffer -> byte[] using System.arraycopy() + @Benchmark + public void arrayCopy(final Blackhole blackhole) { + final byte[] dst = new byte[window]; + final byte[] src = heapBuffer.array(); + long sum = 0; + for (int i = 0; i < size - window; i++) { + System.arraycopy(src, i, dst, 0, window); +// sum += sum(dst); + blackhole.consume(dst); + } + if (printSum) { + System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + // Heap buffer -> byte[] using ByteBuffer.get() + @Benchmark + public void heapBufferGet(final Blackhole blackhole) { + final byte[] dst = new byte[window]; + long sum = 0; + for (int i = 0; i < size - window; i++) { + heapBuffer.position(i); + heapBuffer.get(dst, 0, window); +// sum += sum(dst); + blackhole.consume(dst); + } + if (printSum) { + System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + // Direct buffer -> byte[] using ByteBuffer.get() + @Benchmark + public void directBufferGet(final Blackhole blackhole) { + final byte[] dst = new byte[window]; + long sum = 0; + for (int i = 0; i < size - window; i++) { + directBuffer.position(i); + directBuffer.get(dst, 0, window); +// sum += sum(dst); + blackhole.consume(dst); + } + if (printSum) { + System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + // Heap buffer -> byte[] using Unsafe + @Benchmark + public void unsafeHeapGet(final Blackhole blackhole) { + final byte[] dst = new byte[window]; + long sum = 0; + for (int i = 0; i < size - window; i++) { + UnsafeUtils.getHeapBufferToArray(heapBuffer, i, dst, 0, window); +// sum += sum(dst); + blackhole.consume(dst); + } + if (printSum) { + System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + // Direct buffer -> byte[] using Unsafe + @Benchmark + public void unsafeDirectBytes(final Blackhole blackhole) { + final byte[] dst = new byte[window]; + long sum = 0; + for (int i = 0; i < size - window; i++) { + UnsafeUtils.getDirectBufferToArray(directBuffer, i, dst, 0, window); +// sum += sum(dst); + blackhole.consume(dst); + } + if (printSum) { + System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } + + // Direct buffer -> direct buffer using Unsafe + @Benchmark + public void unsafeDirectBuffer(final Blackhole blackhole) { + final ByteBuffer dst = ByteBuffer.allocateDirect(window); + long sum = 0; + for (int i = 0; i < size - window; i++) { + UnsafeUtils.getDirectBufferToDirectBuffer(directBuffer, i, dst, 0, window); +// sum += sum(dst); + blackhole.consume(dst); + } + if (printSum) { + System.out.println("sum = " + sum); + printSum = false; + } + blackhole.consume(sum); + } +} diff --git a/pbj-core/pbj-runtime/src/jmh/java/com/hedera/pbj/runtime/io/ByteOrderEquals.java b/pbj-core/pbj-runtime/src/jmh/java/com/hedera/pbj/runtime/io/ByteOrderEquals.java new file mode 100644 index 00000000..9be722c6 --- /dev/null +++ b/pbj-core/pbj-runtime/src/jmh/java/com/hedera/pbj/runtime/io/ByteOrderEquals.java @@ -0,0 +1,46 @@ +package com.hedera.pbj.runtime.io; + +import java.nio.ByteOrder; +import java.util.Random; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +@Fork(value = 1) +@State(Scope.Benchmark) +public class ByteOrderEquals { + + private static final int SIZE = 10000; + + final ByteOrder[] array = new ByteOrder[SIZE]; + + @Setup(Level.Trial) + public void init() { + final Random r = new Random(1024); + for (int i = 0; i < SIZE; i++) { + array[i] = r.nextBoolean() ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN; + } + } + + @Benchmark + public void testEqualsEquals(final Blackhole blackhole) { + boolean value = false; + for (int i = 0; i < SIZE; i++) { + value = value ^ (array[i] == ByteOrder.BIG_ENDIAN); + } + blackhole.consume(value); + } + + @Benchmark + public void testObjectEquals(final Blackhole blackhole) { + boolean value = false; + for (int i = 0; i < SIZE; i++) { + value = value ^ (array[i].equals(ByteOrder.BIG_ENDIAN)); + } + blackhole.consume(value); + } +} diff --git a/pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/UnsafeUtils.java b/pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/UnsafeUtils.java index 4606f821..f843cc98 100644 --- a/pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/UnsafeUtils.java +++ b/pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/UnsafeUtils.java @@ -1,7 +1,9 @@ package com.hedera.pbj.runtime.io; import java.lang.reflect.Field; +import java.nio.Buffer; import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; import java.nio.ByteOrder; import sun.misc.Unsafe; @@ -12,17 +14,32 @@ public class UnsafeUtils { private static final Unsafe UNSAFE; + /** + * Java and PBJ use BIG_ENDIAN, while native byte order used by Unsafe may or may not + * be BIG_ENDIAN. This flag indicates that if they don't match + */ private static final boolean NEED_CHANGE_BYTE_ORDER; + /** + * Field offset of the byte[] class + */ private static final int BYTE_ARRAY_BASE_OFFSET; + /** + * Direct byte buffer "address" field offset. This is not the address of the buffer, + * but the offset of the field, which contains the address of the buffer + */ + private static final long DIRECT_BYTEBUFFER_ADDRESS_OFFSET; + static { try { - final Field f = Unsafe.class.getDeclaredField("theUnsafe"); - f.setAccessible(true); - UNSAFE = (Unsafe) f.get(null); + final Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafeField.setAccessible(true); + UNSAFE = (Unsafe) theUnsafeField.get(null); NEED_CHANGE_BYTE_ORDER = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN; BYTE_ARRAY_BASE_OFFSET = UNSAFE.arrayBaseOffset(byte[].class); + final Field addressField = Buffer.class.getDeclaredField("address"); + DIRECT_BYTEBUFFER_ADDRESS_OFFSET = UNSAFE.objectFieldOffset(addressField); } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { throw new InternalError(e); } @@ -31,6 +48,40 @@ public class UnsafeUtils { private UnsafeUtils() { } + /** + * Get byte array element at a given offset. Identical to arr[offset], but slightly + * faster on some systems + */ + public static byte getByte(final byte[] arr, final int offset) { + if (arr.length < offset + 1) { + throw new BufferUnderflowException(); + } + return UNSAFE.getByte(arr, BYTE_ARRAY_BASE_OFFSET + offset); + } + + /** + * Get heap byte buffer element at a given offset. Identical to buf.get(offset), but + * slightly faster on some systems. May only be called for Java heap byte buffers + */ + public static byte getByteHeap(final ByteBuffer buf, final int offset) { + if (buf.limit() < offset + 1) { + throw new BufferUnderflowException(); + } + return UNSAFE.getByte(buf.array(), BYTE_ARRAY_BASE_OFFSET + offset); + } + + /** + * Get direct byte buffer element at a given offset. Identical to buf.get(offset), but + * slightly faster on some systems. May only be called for direct byte buffers + */ + public static byte getByteDirect(final ByteBuffer buf, final int offset) { + if (buf.limit() < offset + 1) { + throw new BufferUnderflowException(); + } + final long address = UNSAFE.getLong(buf, DIRECT_BYTEBUFFER_ADDRESS_OFFSET); + return UNSAFE.getByte(null, address + offset); + } + /** * Reads an integer from the given array starting at the given offset. Array bytes are * interpreted in BIG_ENDIAN order. @@ -64,4 +115,45 @@ public static long getLong(final byte[] arr, final int offset) { final long value = UNSAFE.getLong(arr, BYTE_ARRAY_BASE_OFFSET + offset); return NEED_CHANGE_BYTE_ORDER ? Long.reverseBytes(value) : value; } + + /** + * Copies heap byte buffer bytes to a given byte array. May only be called for heap + * byte buffers + */ + public static void getHeapBufferToArray( + final ByteBuffer buffer, final long offset, final byte[] dst, final int dstOffset, final int length) { + UNSAFE.copyMemory(buffer.array(), BYTE_ARRAY_BASE_OFFSET + offset, + dst, BYTE_ARRAY_BASE_OFFSET + dstOffset, length); + } + + /** + * Copies direct byte buffer bytes to a given byte array. May only be called for direct + * byte buffers + */ + public static void getDirectBufferToArray( + final ByteBuffer buffer, final long offset, final byte[] dst, final int dstOffset, final int length) { + final long address = UNSAFE.getLong(buffer, DIRECT_BYTEBUFFER_ADDRESS_OFFSET); + UNSAFE.copyMemory(null, address + offset, + dst, BYTE_ARRAY_BASE_OFFSET + dstOffset, length); + } + + /** + * Copies direct byte buffer bytes to another direct byte buffer. May only be called for + * direct byte buffers + */ + public static void getDirectBufferToDirectBuffer( + final ByteBuffer buffer, final long offset, final ByteBuffer dst, final int dstOffset, final int length) { + final long address = UNSAFE.getLong(buffer, DIRECT_BYTEBUFFER_ADDRESS_OFFSET); + final long dstAddress = UNSAFE.getLong(dst, DIRECT_BYTEBUFFER_ADDRESS_OFFSET); + UNSAFE.copyMemory(null, address + offset, null, dstAddress, length); + } + + /** + * Copies a byte array to a direct byte buffer. May only be called for direct byte buffers + */ + public static void putByteArrayToDirectBuffer( + final ByteBuffer buffer, final long offset, final byte[] src, final int srcOffset, final int length) { + final long address = UNSAFE.getLong(buffer, DIRECT_BYTEBUFFER_ADDRESS_OFFSET); + UNSAFE.copyMemory(src, BYTE_ARRAY_BASE_OFFSET + srcOffset, null, address + offset, length); + } } diff --git a/pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/buffer/BufferedData.java b/pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/buffer/BufferedData.java index 19bae4bd..b6c08cc4 100644 --- a/pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/buffer/BufferedData.java +++ b/pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/buffer/BufferedData.java @@ -22,7 +22,9 @@ * *
This class is the most commonly used for buffered read/write data. */ -public class BufferedData implements BufferedSequentialData, ReadableSequentialData, WritableSequentialData, RandomAccessData { +public sealed class BufferedData + implements BufferedSequentialData, ReadableSequentialData, WritableSequentialData, RandomAccessData + permits ByteArrayBufferedData, DirectBufferedData { /** Single instance of an empty buffer we can use anywhere we need an empty read only buffer */ @SuppressWarnings("unused") @@ -35,19 +37,15 @@ public class BufferedData implements BufferedSequentialData, ReadableSequentialD * an inner array, which can be accessed directly. If it is, you MUST BE VERY CAREFUL to take the array offset into * account, otherwise you will read out of bounds of the view. */ - private final ByteBuffer buffer; - - /** Locally cached value of {@link ByteBuffer#isDirect()}. This value is queried by the varInt methods. */ - private final boolean direct; + protected final ByteBuffer buffer; /** * Wrap an existing allocated {@link ByteBuffer}. No copy is made. * * @param buffer the {@link ByteBuffer} to wrap */ - private BufferedData(@NonNull final ByteBuffer buffer) { + protected BufferedData(@NonNull final ByteBuffer buffer) { this.buffer = buffer; - this.direct = buffer.isDirect(); // We switch the buffer to BIG_ENDIAN so that all our normal "get/read" methods can assume they are in // BIG_ENDIAN mode, reducing the boilerplate around those methods. This necessarily means the LITTLE_ENDIAN // methods will be slower. We're assuming BIG_ENDIAN is what we want to optimize for. @@ -66,57 +64,70 @@ private BufferedData(@NonNull final ByteBuffer buffer) { */ @NonNull public static BufferedData wrap(@NonNull final ByteBuffer buffer) { - return new BufferedData(buffer); + if (buffer.hasArray()) { + return new ByteArrayBufferedData(buffer); + } else if (buffer.isDirect()) { + return new DirectBufferedData(buffer); + } else { + // It must be read-only heap byte buffer + return new BufferedData(buffer); + } } /** - * Wrap an existing allocated byte[]. No copy is made. The length of the {@link BufferedData} will be - * the *ENTIRE* length of the byte array. DO NOT modify this array after having wrapped it. + * Wrap an existing allocated byte[]. No copy is made. DO NOT modify this array after having wrapped it. + * + *
The current position of the created {@link BufferedData} will be 0, the length and capacity will + * be the length of the wrapped byte array. * * @param array the byte[] to wrap - * @return new DataBuffer using {@code array} as its data buffer + * @return new BufferedData using {@code array} as its data buffer */ @NonNull public static BufferedData wrap(@NonNull final byte[] array) { - return new BufferedData(ByteBuffer.wrap(array)); + return new ByteArrayBufferedData(ByteBuffer.wrap(array)); } /** * Wrap an existing allocated byte[]. No copy is made. DO NOT modify this array after having wrapped it. * + *
The current position of the created {@link BufferedData} will be {@code offset}, the length will be + * set to {@code offset} + {@code len}, and capacity will be the length of the wrapped byte array. + * * @param array the byte[] to wrap * @param offset the offset into the byte array which will form the origin of this {@link BufferedData}. * @param len the length of the {@link BufferedData} in bytes. - * @return new DataBuffer using {@code array} as its data buffer + * @return new BufferedData using {@code array} as its data buffer */ @NonNull public static BufferedData wrap(@NonNull final byte[] array, final int offset, final int len) { - return new BufferedData(ByteBuffer.wrap(array, offset, len)); + return new ByteArrayBufferedData(ByteBuffer.wrap(array, offset, len)); } /** - * Allocate a new DataBuffer with new memory, on the Java heap. + * Allocate a new buffered data object with new memory, on the Java heap. * * @param size size of new buffer in bytes - * @return a new allocated DataBuffer + * @return a new allocated BufferedData */ @NonNull public static BufferedData allocate(final int size) { - return new BufferedData(ByteBuffer.allocate(size)); + return new ByteArrayBufferedData(ByteBuffer.allocate(size)); } /** - * Allocate a new DataBuffer with new memory, off the Java heap. Off heap has higher cost of allocation and garbage - * collection but is much faster to read and write to. It should be used for long-lived buffers where performance is - * critical. On heap is slower for read and writes but cheaper to allocate and garbage collect. Off-heap comes from - * different memory allocation that needs to be manually managed so make sure we have space for it before using. + * Allocate a new buffered data object with new memory, off the Java heap. Off heap has higher cost of allocation + * and garbage collection but is much faster to read and write to. It should be used for long-lived buffers where + * performance is critical. On heap is slower for read and writes but cheaper to allocate and garbage collect. + * Off-heap comes from different memory allocation that needs to be manually managed so make sure we have space + * for it before using. * * @param size size of new buffer in bytes - * @return a new allocated DataBuffer + * @return a new allocated BufferedData */ @NonNull public static BufferedData allocateOffHeap(final int size) { - return new BufferedData(ByteBuffer.allocateDirect(size)); + return new DirectBufferedData(ByteBuffer.allocateDirect(size)); } // ================================================================================================================ @@ -180,7 +191,8 @@ public int read(@NonNull final byte[] b, final int off, final int len) throws IO public String toString() { // build string StringBuilder sb = new StringBuilder(); - sb.append("BufferedData["); + sb.append(getClass().getSimpleName()); + sb.append("["); for (int i = 0; i < buffer.limit(); i++) { int v = buffer.get(i) & 0xFF; sb.append(v); @@ -198,13 +210,22 @@ public String toString() { */ @Override public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - BufferedData that = (BufferedData) o; - if (this.capacity() != that.capacity()) return false; - if (this.limit() != that.limit()) return false; + if (this == o) { + return true; + } + if (!(o instanceof BufferedData that)) { + return false; + } + if (this.capacity() != that.capacity()) { + return false; + } + if (this.limit() != that.limit()) { + return false; + } for (int i = 0; i < this.limit(); i++) { - if (buffer.get(i) != that.buffer.get(i)) return false; + if (buffer.get(i) != that.buffer.get(i)) { + return false; + } } return true; } @@ -346,6 +367,7 @@ public long getBytes(final long offset, @NonNull final byte[] dst, final int dst throw new IllegalArgumentException("Negative maxLength not allowed"); } + // FUTURE: why offset + length is checked for this object, but not for dst? final var len = Math.min(maxLength, length() - offset); buffer.get(Math.toIntExact(offset), dst, dstOffset, Math.toIntExact(len)); return len; @@ -370,9 +392,14 @@ public long getBytes(final long offset, @NonNull final BufferedData dst) { /** {@inheritDoc} */ @NonNull @Override - public Bytes getBytes(long offset, long length) { + public Bytes getBytes(final long offset, final long length) { final var len = Math.toIntExact(length); - if(len < 0) throw new IllegalArgumentException("Length cannot be negative"); + if (len < 0) { + throw new IllegalArgumentException("Length cannot be negative"); + } + if (length() - offset < length) { + throw new BufferUnderflowException(); + } // It is vital that we always copy here, we can never assume ownership of the underlying buffer final var copy = new byte[len]; buffer.get(Math.toIntExact(offset), copy, 0, len); @@ -383,7 +410,7 @@ public Bytes getBytes(long offset, long length) { @Override @NonNull public BufferedData slice(final long offset, final long length) { - return new BufferedData(buffer.slice(Math.toIntExact(offset), Math.toIntExact(length))); + return BufferedData.wrap(buffer.slice(Math.toIntExact(offset), Math.toIntExact(length))); } /** {@inheritDoc} */ @@ -550,6 +577,7 @@ public Bytes readBytes(final int length) { @Override public BufferedData view(final int length) { if (length < 0) { + // FUTURE: change to AIOOBE throw new IllegalArgumentException("Length cannot be negative"); } @@ -558,7 +586,7 @@ public BufferedData view(final int length) { } final var pos = Math.toIntExact(position()); - final var buf = new BufferedData(buffer.slice(pos, length)); + final var buf = BufferedData.wrap(buffer.slice(pos, length)); position((long) pos + length); return buf; } @@ -657,116 +685,6 @@ public double readDouble(@NonNull final ByteOrder byteOrder) { } } - /** - * {@inheritDoc} - */ - @Override - public int readVarInt(final boolean zigZag) { - if (direct) return ReadableSequentialData.super.readVarInt(zigZag); - - final int arrayOffset = buffer.arrayOffset(); - int tempPos = buffer.position() + arrayOffset; - final byte[] buff = buffer.array(); - int lastPos = buffer.limit() + arrayOffset; - - if (lastPos == tempPos) { - throw new BufferUnderflowException(); - } - - int x; - if ((x = buff[tempPos++]) >= 0) { - buffer.position(tempPos - arrayOffset); - return zigZag ? (x >>> 1) ^ -(x & 1) : x; - } else if (lastPos - tempPos < 9) { - return (int) readVarIntLongSlow(zigZag); - } else if ((x ^= (buff[tempPos++] << 7)) < 0) { - x ^= (~0 << 7); - } else if ((x ^= (buff[tempPos++] << 14)) >= 0) { - x ^= (~0 << 7) ^ (~0 << 14); - } else if ((x ^= (buff[tempPos++] << 21)) < 0) { - x ^= (~0 << 7) ^ (~0 << 14) ^ (~0 << 21); - } else { - int y = buff[tempPos++]; - x ^= y << 28; - x ^= (~0 << 7) ^ (~0 << 14) ^ (~0 << 21) ^ (~0 << 28); - if (y < 0 - && buff[tempPos++] < 0 - && buff[tempPos++] < 0 - && buff[tempPos++] < 0 - && buff[tempPos++] < 0 - && buff[tempPos++] < 0) { - return (int) readVarIntLongSlow(zigZag); - } - } - buffer.position(tempPos - arrayOffset); - return zigZag ? (x >>> 1) ^ -(x & 1) : x; - } - - /** - * {@inheritDoc} - */ - @Override - public long readVarLong(boolean zigZag) { - if (direct) return ReadableSequentialData.super.readVarLong(zigZag); - - final int arrayOffset = buffer.arrayOffset(); - int tempPos = buffer.position() + arrayOffset; - final byte[] buff = buffer.array(); - int lastPos = buffer.limit() + arrayOffset; - - if (lastPos == tempPos) { - throw new BufferUnderflowException(); - } - - long x; - int y; - if ((y = buff[tempPos++]) >= 0) { - buffer.position(tempPos - arrayOffset); - return zigZag ? (y >>> 1) ^ -(y & 1) : y; - } else if (lastPos - tempPos < 9) { - return readVarIntLongSlow(zigZag); - } else if ((y ^= (buff[tempPos++] << 7)) < 0) { - x = y ^ (~0 << 7); - } else if ((y ^= (buff[tempPos++] << 14)) >= 0) { - x = y ^ ((~0 << 7) ^ (~0 << 14)); - } else if ((y ^= (buff[tempPos++] << 21)) < 0) { - x = y ^ ((~0 << 7) ^ (~0 << 14) ^ (~0 << 21)); - } else if ((x = y ^ ((long) buff[tempPos++] << 28)) >= 0L) { - x ^= (~0L << 7) ^ (~0L << 14) ^ (~0L << 21) ^ (~0L << 28); - } else if ((x ^= ((long) buff[tempPos++] << 35)) < 0L) { - x ^= (~0L << 7) ^ (~0L << 14) ^ (~0L << 21) ^ (~0L << 28) ^ (~0L << 35); - } else if ((x ^= ((long) buff[tempPos++] << 42)) >= 0L) { - x ^= (~0L << 7) ^ (~0L << 14) ^ (~0L << 21) ^ (~0L << 28) ^ (~0L << 35) ^ (~0L << 42); - } else if ((x ^= ((long) buff[tempPos++] << 49)) < 0L) { - x ^= - (~0L << 7) - ^ (~0L << 14) - ^ (~0L << 21) - ^ (~0L << 28) - ^ (~0L << 35) - ^ (~0L << 42) - ^ (~0L << 49); - } else { - x ^= ((long) buff[tempPos++] << 56); - x ^= - (~0L << 7) - ^ (~0L << 14) - ^ (~0L << 21) - ^ (~0L << 28) - ^ (~0L << 35) - ^ (~0L << 42) - ^ (~0L << 49) - ^ (~0L << 56); - if (x < 0L) { - if (buff[tempPos++] < 0L) { - return readVarIntLongSlow(zigZag); - } - } - } - buffer.position(tempPos - arrayOffset); - return zigZag ? (x >>> 1) ^ -(x & 1) : x; - } - // ================================================================================================================ // DataOutput Write Methods @@ -1019,4 +937,28 @@ public void writeVarLong(long value, final boolean zigZag) { } } } + + // Helper methods + + protected void validateLen(final long len) { + if (len < 0) { + throw new IllegalArgumentException("Negative length not allowed"); + } + } + + protected void validateCanRead(final long offset, final long len) { + if (offset < 0) { + throw new ArrayIndexOutOfBoundsException(); + } + if (offset > length() - len) { + // FUTURE: change to AIOOBE, too + throw new BufferUnderflowException(); + } + } + + protected void validateCanWrite(final long len) { + if (remaining() < len) { + throw new BufferOverflowException(); + } + } } diff --git a/pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/buffer/ByteArrayBufferedData.java b/pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/buffer/ByteArrayBufferedData.java new file mode 100644 index 00000000..b9cac4d6 --- /dev/null +++ b/pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/buffer/ByteArrayBufferedData.java @@ -0,0 +1,335 @@ +package com.hedera.pbj.runtime.io.buffer; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * BufferedData subclass for instances backed by a byte array. Provides slightly more optimized + * versions of several methods to get / read / write bytes using {@link System#arraycopy} and + * direct array reads / writes. + */ +final class ByteArrayBufferedData extends BufferedData { + + // Backing byte array + private final byte[] array; + + // This data buffer's offset into the backing array. See ByteBuffer.arrayOffset() for details + private final int arrayOffset; + + ByteArrayBufferedData(final ByteBuffer buffer) { + super(buffer); + if (!buffer.hasArray()) { + throw new IllegalArgumentException("Cannot create a ByteArrayBufferedData over a buffer with no array"); + } + this.array = buffer.array(); + this.arrayOffset = buffer.arrayOffset(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()); + sb.append("["); + for (int i = 0; i < buffer.limit(); i++) { + int v = array[arrayOffset + i] & 0xFF; + sb.append(v); + if (i < (buffer.limit() - 1)) { + sb.append(','); + } + } + sb.append(']'); + return sb.toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean contains(final long offset, @NonNull final byte[] bytes) { + if (offset < 0 || offset >= length()) { + throw new IndexOutOfBoundsException(); + } + + final int len = bytes.length; + if (length() - offset < len) { + return false; + } + + final int fromThisIndex = Math.toIntExact(arrayOffset + offset); + final int fromToIndex = fromThisIndex + len; + return Arrays.equals(array, fromThisIndex, fromToIndex, bytes, 0, len); + } + + /** + * {@inheritDoc} + */ + @Override + public int readVarInt(final boolean zigZag) { + int tempPos = buffer.position() + arrayOffset; + int lastPos = buffer.limit() + arrayOffset; + + if (lastPos == tempPos) { + throw new BufferUnderflowException(); + } + + int x; + if ((x = array[tempPos++]) >= 0) { + buffer.position(tempPos - arrayOffset); + return zigZag ? (x >>> 1) ^ -(x & 1) : x; + } else if (lastPos - tempPos < 9) { + return (int) readVarIntLongSlow(zigZag); + } else if ((x ^= (array[tempPos++] << 7)) < 0) { + x ^= (~0 << 7); + } else if ((x ^= (array[tempPos++] << 14)) >= 0) { + x ^= (~0 << 7) ^ (~0 << 14); + } else if ((x ^= (array[tempPos++] << 21)) < 0) { + x ^= (~0 << 7) ^ (~0 << 14) ^ (~0 << 21); + } else { + int y = array[tempPos++]; + x ^= y << 28; + x ^= (~0 << 7) ^ (~0 << 14) ^ (~0 << 21) ^ (~0 << 28); + if (y < 0 + && array[tempPos++] < 0 + && array[tempPos++] < 0 + && array[tempPos++] < 0 + && array[tempPos++] < 0 + && array[tempPos++] < 0) { + return (int) readVarIntLongSlow(zigZag); + } + } + buffer.position(tempPos - arrayOffset); + return zigZag ? (x >>> 1) ^ -(x & 1) : x; + } + + /** + * {@inheritDoc} + */ + @Override + public long readVarLong(final boolean zigZag) { + int tempPos = buffer.position() + arrayOffset; + int lastPos = buffer.limit() + arrayOffset; + + if (lastPos == tempPos) { + throw new BufferUnderflowException(); + } + + long x; + int y; + if ((y = array[tempPos++]) >= 0) { + buffer.position(tempPos - arrayOffset); + return zigZag ? (y >>> 1) ^ -(y & 1) : y; + } else if (lastPos - tempPos < 9) { + return readVarIntLongSlow(zigZag); + } else if ((y ^= (array[tempPos++] << 7)) < 0) { + x = y ^ (~0 << 7); + } else if ((y ^= (array[tempPos++] << 14)) >= 0) { + x = y ^ ((~0 << 7) ^ (~0 << 14)); + } else if ((y ^= (array[tempPos++] << 21)) < 0) { + x = y ^ ((~0 << 7) ^ (~0 << 14) ^ (~0 << 21)); + } else if ((x = y ^ ((long) array[tempPos++] << 28)) >= 0L) { + x ^= (~0L << 7) ^ (~0L << 14) ^ (~0L << 21) ^ (~0L << 28); + } else if ((x ^= ((long) array[tempPos++] << 35)) < 0L) { + x ^= (~0L << 7) ^ (~0L << 14) ^ (~0L << 21) ^ (~0L << 28) ^ (~0L << 35); + } else if ((x ^= ((long) array[tempPos++] << 42)) >= 0L) { + x ^= (~0L << 7) ^ (~0L << 14) ^ (~0L << 21) ^ (~0L << 28) ^ (~0L << 35) ^ (~0L << 42); + } else if ((x ^= ((long) array[tempPos++] << 49)) < 0L) { + x ^= + (~0L << 7) + ^ (~0L << 14) + ^ (~0L << 21) + ^ (~0L << 28) + ^ (~0L << 35) + ^ (~0L << 42) + ^ (~0L << 49); + } else { + x ^= ((long) array[tempPos++] << 56); + x ^= + (~0L << 7) + ^ (~0L << 14) + ^ (~0L << 21) + ^ (~0L << 28) + ^ (~0L << 35) + ^ (~0L << 42) + ^ (~0L << 49) + ^ (~0L << 56); + if (x < 0L) { + if (array[tempPos++] < 0L) { + return readVarIntLongSlow(zigZag); + } + } + } + buffer.position(tempPos - arrayOffset); + return zigZag ? (x >>> 1) ^ -(x & 1) : x; + } + + /** + * {@inheritDoc} + */ + @Override + public byte getByte(final long offset) { + validateCanRead(offset, 0); + return array[Math.toIntExact(arrayOffset + offset)]; + } + + /** + * {@inheritDoc} + */ + @Override + public long getBytes(final long offset, @NonNull final byte[] dst, final int dstOffset, final int maxLength) { + validateLen(maxLength); + final long len = Math.min(maxLength, length() - offset); + validateCanRead(offset, len); + if (len == 0) { + return 0; + } + System.arraycopy(array, Math.toIntExact(arrayOffset + offset), dst, dstOffset, Math.toIntExact(len)); + return len; + } + + /** + * {@inheritDoc} + */ + @Override + public long getBytes(final long offset, @NonNull final ByteBuffer dst) { + if (!dst.hasArray()) { + return super.getBytes(offset, dst); + } + final long len = Math.min(length() - offset, dst.remaining()); + final byte[] dstArr = dst.array(); + final int dstPos = dst.position(); + final int dstArrOffset = dst.arrayOffset(); + System.arraycopy( + array, Math.toIntExact(arrayOffset + offset), dstArr, dstArrOffset + dstPos, Math.toIntExact(len)); + return len; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public Bytes getBytes(final long offset, final long len) { + validateLen(len); + if (len == 0) { + return Bytes.EMPTY; + } + if (length() - offset < len) { + throw new BufferUnderflowException(); + } + final byte[] res = new byte[Math.toIntExact(len)]; + System.arraycopy(array, Math.toIntExact(arrayOffset + offset), res, 0, res.length); + return Bytes.wrap(res); + } + + /** + * {@inheritDoc} + */ + @Override + public byte readByte() { + if (remaining() == 0) { + throw new BufferUnderflowException(); + } + final int pos = buffer.position(); + final byte res = array[arrayOffset + pos]; + buffer.position(pos + 1); + return res; + } + + /** + * {@inheritDoc} + */ + @Override + public long readBytes(@NonNull byte[] dst, int offset, int maxLength) { + validateLen(maxLength); + final var len = Math.toIntExact(Math.min(maxLength, remaining())); + if (len == 0) { + return 0; + } + final int pos = buffer.position(); + System.arraycopy(array, arrayOffset + pos, dst, offset, len); + buffer.position(pos + len); + return len; + } + + /** + * {@inheritDoc} + */ + @Override + public long readBytes(@NonNull final ByteBuffer dst) { + if (!dst.hasArray()) { + return super.readBytes(dst); + } + final long len = Math.min(remaining(), dst.remaining()); + final int pos = buffer.position(); + final byte[] dstArr = dst.array(); + final int dstPos = dst.position(); + final int dstArrOffset = dst.arrayOffset(); + System.arraycopy(array, arrayOffset + pos, dstArr, dstArrOffset + dstPos, Math.toIntExact(len)); + buffer.position(Math.toIntExact(pos + len)); + dst.position(Math.toIntExact(dstPos + len)); + return len; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public Bytes readBytes(final int len) { + validateLen(len); + final int pos = buffer.position(); + validateCanRead(pos, len); + if (len == 0) { + return Bytes.EMPTY; + } + final byte[] res = new byte[len]; + System.arraycopy(array, arrayOffset + pos, res, 0, len); + buffer.position(pos + len); + return Bytes.wrap(res); + } + + /** + * {@inheritDoc} + */ + @Override + public void writeByte(final byte b) { + validateCanWrite(1); + final int pos = buffer.position(); + array[arrayOffset + pos] = b; + buffer.position(pos + 1); + } + + /** + * {@inheritDoc} + */ + @Override + public void writeBytes(@NonNull final byte[] src, final int offset, final int len) { + validateLen(len); + validateCanWrite(len); + final int pos = buffer.position(); + System.arraycopy(src, offset, array, arrayOffset + pos, len); + buffer.position(pos + len); + } + + /** + * {@inheritDoc} + */ + @Override + public void writeBytes(@NonNull final ByteBuffer src) { + if (!src.hasArray()) { + super.writeBytes(src); + return; + } + final long len = src.remaining(); + validateCanWrite(len); + final int pos = buffer.position(); + final byte[] srcArr = src.array(); + final int srcArrOffset = src.arrayOffset(); + final int srcPos = src.position(); + System.arraycopy(srcArr, srcArrOffset + srcPos, array, arrayOffset + pos, Math.toIntExact(len)); + src.position(Math.toIntExact(srcPos + len)); + buffer.position(Math.toIntExact(pos + len)); + } +} diff --git a/pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/buffer/DirectBufferedData.java b/pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/buffer/DirectBufferedData.java new file mode 100644 index 00000000..6a767727 --- /dev/null +++ b/pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/buffer/DirectBufferedData.java @@ -0,0 +1,163 @@ +package com.hedera.pbj.runtime.io.buffer; + +import com.hedera.pbj.runtime.io.UnsafeUtils; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import java.util.Objects; + +/** + * BufferedData subclass for instances backed by direct byte buffers. Provides slightly more optimized + * versions of several methods to get / read / write bytes using {@link UnsafeUtils} methods. + */ +final class DirectBufferedData extends BufferedData { + + DirectBufferedData(final ByteBuffer buffer) { + super(buffer); + if (!buffer.isDirect()) { + throw new IllegalArgumentException("Cannot create a DirectBufferedData over a heap byte buffer"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public long getBytes(final long offset, @NonNull final byte[] dst, final int dstOffset, final int maxLength) { + validateLen(maxLength); + final long len = Math.min(maxLength, length() - offset); + validateCanRead(offset, len); + if (len == 0) { + return 0; + } + if ((dstOffset < 0) || (dst.length - dstOffset < len)) { + throw new ArrayIndexOutOfBoundsException(); + } + UnsafeUtils.getDirectBufferToArray(buffer, offset, dst, dstOffset, Math.toIntExact(len)); + return len; + } + + /** + * {@inheritDoc} + */ + @Override + public long getBytes(final long offset, @NonNull final ByteBuffer dst) { + if (!dst.hasArray()) { + return super.getBytes(offset, dst); + } + final long len = Math.min(length() - offset, dst.remaining()); + final byte[] dstArr = dst.array(); + final int dstPos = dst.position(); + final int dstArrOffset = dst.arrayOffset(); + UnsafeUtils.getDirectBufferToArray(buffer, offset, dstArr, dstArrOffset + dstPos, Math.toIntExact(len)); + return len; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public Bytes getBytes(final long offset, final long len) { + validateLen(len); + if (len == 0) { + return Bytes.EMPTY; + } + validateCanRead(offset, len); + final byte[] res = new byte[Math.toIntExact(len)]; + UnsafeUtils.getDirectBufferToArray(buffer, offset, res, 0, Math.toIntExact(len)); + return Bytes.wrap(res); + } + + /** + * {@inheritDoc} + */ + @Override + public long readBytes(@NonNull final byte[] dst, final int dstOffset, final int maxLength) { + validateLen(maxLength); + final long len = Math.min(maxLength, remaining()); + final int pos = buffer.position(); + validateCanRead(pos, len); + if (len == 0) { + return 0; + } + if ((dstOffset < 0) || (dst.length - dstOffset < len)) { + throw new ArrayIndexOutOfBoundsException(); + } + UnsafeUtils.getDirectBufferToArray(buffer, pos, dst, dstOffset, Math.toIntExact(len)); + buffer.position(Math.toIntExact(pos + len)); + return len; + } + + @Override + public long readBytes(@NonNull final ByteBuffer dst) { + final long len = Math.min(remaining(), dst.remaining()); + final int pos = buffer.position(); + final int dstPos = dst.position(); + if (dst.hasArray()) { + final byte[] dstArr = dst.array(); + final int dstArrOffset = dst.arrayOffset(); + UnsafeUtils.getDirectBufferToArray(buffer, pos, dstArr, dstArrOffset + dstPos, Math.toIntExact(len)); + buffer.position(Math.toIntExact(pos + len)); + dst.position(Math.toIntExact(dstPos + len)); + return len; + } else if (dst.isDirect()) { + UnsafeUtils.getDirectBufferToDirectBuffer(buffer, pos, dst, dstPos, Math.toIntExact(len)); + buffer.position(Math.toIntExact(pos + len)); + dst.position(Math.toIntExact(dstPos + len)); + return len; + } else { + return super.readBytes(dst); + } + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public Bytes readBytes(final int len) { + validateLen(len); + final int pos = buffer.position(); + validateCanRead(pos, len); + final byte[] res = new byte[len]; + UnsafeUtils.getDirectBufferToArray(buffer, pos, res, 0, len); + buffer.position(pos + len); + return Bytes.wrap(res); + } + + /** + * {@inheritDoc} + */ + @Override + public void writeBytes(@NonNull final byte[] src, final int offset, final int len) { + Objects.requireNonNull(src); + if ((offset < 0) || (offset > src.length - len)) { + throw new ArrayIndexOutOfBoundsException(); + } + validateLen(len); + validateCanWrite(len); + final int pos = buffer.position(); + UnsafeUtils.putByteArrayToDirectBuffer(buffer, pos, src, offset, len); + buffer.position(pos + len); + } + + /** + * {@inheritDoc} + */ + @Override + public void writeBytes(@NonNull final ByteBuffer src) { + if (!src.hasArray()) { + super.writeBytes(src); + return; + } + final int len = src.remaining(); + validateCanWrite(len); + final int pos = buffer.position(); + final byte[] srcArr = src.array(); + final int srcPos = src.position(); + final int srcArrOffset = src.arrayOffset(); + UnsafeUtils.putByteArrayToDirectBuffer(buffer, pos, srcArr, srcArrOffset + srcPos, len); + buffer.position(pos + len); + src.position(srcPos + len); + } +} diff --git a/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/BufferedDataTest.java b/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/BufferedDataTest.java index 846e26a0..401319b9 100644 --- a/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/BufferedDataTest.java +++ b/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/BufferedDataTest.java @@ -1,275 +1,25 @@ package com.hedera.pbj.runtime.io.buffer; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import com.hedera.pbj.runtime.io.ReadableSequentialData; -import com.hedera.pbj.runtime.io.ReadableTestBase; -import com.hedera.pbj.runtime.io.WritableSequentialData; -import com.hedera.pbj.runtime.io.WritableTestBase; import edu.umd.cs.findbugs.annotations.NonNull; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -final class BufferedDataTest { - // FUTURE Test that "view" shows the updated data when it is changed on the fly - // FUTURE Verify capacity is never negative (it can't be, maybe nothing to test here) - - @Test - @DisplayName("toString() is safe") - void toStringIsSafe() { - final var buf = BufferedData.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); - buf.skip(5); - buf.limit(10); - - @SuppressWarnings("unused") - final var ignored = buf.toString(); - - assertEquals(5, buf.position()); - assertEquals(10, buf.limit()); - } +import java.nio.ByteBuffer; - @ParameterizedTest - @ValueSource(ints = {0, 1, 2, 4, 7, 8, 15, 16, 31, 32, 33, 127, 128, 512, 1000, 1024, 4000, - 16384, 65535, 65536, 65537, 0xFFFFFF, 0x1000000, 0x1000001, 0x7FFFFFFF, - -1, -7, -8, -9, -127, -128, -129, -65535, -65536, -0xFFFFFF, -0x1000000, -0x1000001, -0x80000000}) - @DisplayName("readVarInt() works with views") - void sliceThenReadVarInt(final int num) { - final var buf = BufferedData.allocate(100); - buf.writeVarInt(num, false); - final long afterFirstIntPos = buf.position(); - buf.writeVarInt(num + 1, false); - final long afterSecondIntPos = buf.position(); +final class BufferedDataTest extends BufferedDataTestBase { - final var slicedBuf = buf.slice(afterFirstIntPos, afterSecondIntPos - afterFirstIntPos); - final int readback = slicedBuf.readVarInt(false); - assertEquals(num + 1, readback); - assertEquals(afterSecondIntPos - afterFirstIntPos, slicedBuf.position()); + @NonNull + @Override + protected BufferedData allocate(final int size) { + return new BufferedData(ByteBuffer.allocate(size)); } - @ParameterizedTest - @ValueSource(longs = {0, 1, 7, 8, 9, 127, 128, 129, 1023, 1024, 1025, 65534, 65535, 65536, - 0xFFFFFFFFL, 0x100000000L, 0x100000001L, 0xFFFFFFFFFFFFL, 0x1000000000000L, 0x1000000000001L, - -1, -7, -8, -9, -127, -128, -129, -65534, -65535, -65536, -0xFFFFFFFFL, -0x100000000L, -0x100000001L}) - @DisplayName("readVarLong() works with views") - void sliceThenReadVarLong(final long num) { - final var buf = BufferedData.allocate(256); - buf.writeVarLong(num, false); - final long afterFirstIntPos = buf.position(); - buf.writeVarLong(num + 1, false); - final long afterSecondIntPos = buf.position(); - - final var slicedBuf = buf.slice(afterFirstIntPos, afterSecondIntPos - afterFirstIntPos); - final long readback = slicedBuf.readVarLong(false); - assertEquals(num + 1, readback); - assertEquals(afterSecondIntPos - afterFirstIntPos, slicedBuf.position()); + @NonNull + @Override + protected BufferedData wrap(final byte[] arr) { + return new BufferedData(ByteBuffer.wrap(arr)); } - @Test - @DisplayName("readBytes() works with views") - void sliceThenReadBytes() { - final int SIZE = 100; - final var buf = BufferedData.allocate(SIZE); - for (int i = 0; i < SIZE; i++) { - buf.writeByte((byte) i); - } - - final int START = 10; - final int LEN = 10; - buf.reset(); - buf.skip(START); - final var bytes = buf.readBytes(LEN); - assertEquals(START + LEN, buf.position()); - for (int i = START; i < START + LEN; i++) { - assertEquals((byte) i, bytes.getByte(i - START)); - } - - final var slicedBuf = buf.slice(START, LEN); - final var slicedBytes = slicedBuf.readBytes(LEN); - assertEquals(LEN, slicedBuf.position()); - for (int i = 0; i < LEN; i++) { - assertEquals((byte) (i + START), slicedBytes.getByte(i)); - } - } - - @Nested - final class ReadableSequentialDataTest extends ReadableTestBase { - - @NonNull - @Override - protected ReadableSequentialData emptySequence() { - return BufferedData.EMPTY_BUFFER; - } - - @NonNull - @Override - protected ReadableSequentialData fullyUsedSequence() { - final var buf = BufferedData.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); - buf.skip(10); - return buf; - } - - @NonNull - @Override - protected ReadableSequentialData sequence(@NonNull byte[] arr) { - return BufferedData.wrap(arr); - } - } - - @Nested - final class RandomAccessDataTest extends ReadableTestBase { - - @NonNull - @Override - protected ReadableSequentialData emptySequence() { - return new RandomAccessSequenceAdapter(BufferedData.EMPTY_BUFFER); - } - - @NonNull - @Override - protected ReadableSequentialData fullyUsedSequence() { - final var buf = BufferedData.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); - buf.skip(10); - return new RandomAccessSequenceAdapter(buf, 10); - } - - @NonNull - @Override - protected ReadableSequentialData sequence(@NonNull byte[] arr) { - return new RandomAccessSequenceAdapter(BufferedData.wrap(arr)); - } - } - - @Nested - final class WritableSequentialDataTest extends WritableTestBase { - @NonNull - @Override - protected WritableSequentialData sequence() { - return BufferedData.allocate(1024 * 1024 * 2); // the largest expected test value is 1024 * 1024 - } - - @NonNull - @Override - protected WritableSequentialData eofSequence() { - return BufferedData.wrap(new byte[0]); - } - - @NonNull - @Override - protected byte[] extractWrittenBytes(@NonNull WritableSequentialData seq) { - final var buf = (BufferedData) seq; - final var bytes = new byte[Math.toIntExact(buf.position())]; - buf.getBytes(0, bytes); - return bytes; - } - } - - @Nested - final class ReadableSequentialDataViewTest extends ReadableTestBase { - - @NonNull - @Override - protected ReadableSequentialData emptySequence() { - final var buf = BufferedData.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); - buf.position(7); - return buf.view(0); - } - - @NonNull - @Override - protected ReadableSequentialData fullyUsedSequence() { - final var buf = BufferedData.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); - buf.position(2); - final var view = buf.view(5); - view.skip(5); - return view; - } - - @NonNull - @Override - protected ReadableSequentialData sequence(@NonNull byte[] arr) { - BufferedData buf = BufferedData.allocate(arr.length + 20); - for (int i = 0; i < 10; i++) { - buf.writeByte((byte) i); - } - buf.position(buf.length() - 10); - for (int i = 0; i < 10; i++) { - buf.writeByte((byte) i); - } - buf.position(10); - buf.writeBytes(arr); - buf.position(10); - return buf.view(arr.length); - } - } - - @Nested - final class RandomAccessDataViewTest extends ReadableTestBase { - - @NonNull - @Override - protected ReadableSequentialData emptySequence() { - final var buf = BufferedData.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); - buf.position(7); - return new RandomAccessSequenceAdapter(buf.view(0)); - } - - @NonNull - @Override - protected ReadableSequentialData fullyUsedSequence() { - final var buf = BufferedData.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); - buf.position(2); - final var view = buf.view(5); - view.skip(5); - return new RandomAccessSequenceAdapter(view, 5); - } - - @NonNull - @Override - protected ReadableSequentialData sequence(@NonNull byte[] arr) { - BufferedData buf = BufferedData.allocate(arr.length + 20); - for (int i = 0; i < 10; i++) { - buf.writeByte((byte) i); - } - buf.position(buf.length() - 10); - for (int i = 0; i < 10; i++) { - buf.writeByte((byte) i); - } - buf.position(10); - buf.writeBytes(arr); - buf.position(10); - return new RandomAccessSequenceAdapter(buf.view(arr.length)); - } - } - - @Nested - final class WritableSequentialDataViewTest extends WritableTestBase { - @NonNull - @Override - protected WritableSequentialData sequence() { - final var mb = 1024 * 1024; - final var buf = BufferedData.allocate(mb * 4); // the largest expected test value is 1 mb - buf.position(mb); - return buf.view(mb * 2); - } - - @NonNull - @Override - protected WritableSequentialData eofSequence() { - final var buf = BufferedData.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); - buf.position(7); - return buf.view(0); - } - - @NonNull - @Override - protected byte[] extractWrittenBytes(@NonNull WritableSequentialData seq) { - final var buf = (BufferedData) seq; - final var bytes = new byte[Math.toIntExact(buf.position())]; - buf.getBytes(0, bytes); - return bytes; - } + @NonNull + @Override + protected BufferedData wrap(final byte[] arr, final int offset, final int len) { + return new BufferedData(ByteBuffer.wrap(arr, offset, len)); } } diff --git a/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/BufferedDataTestBase.java b/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/BufferedDataTestBase.java new file mode 100644 index 00000000..75a3973f --- /dev/null +++ b/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/BufferedDataTestBase.java @@ -0,0 +1,371 @@ +package com.hedera.pbj.runtime.io.buffer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.hedera.pbj.runtime.io.ReadableSequentialData; +import com.hedera.pbj.runtime.io.ReadableTestBase; +import com.hedera.pbj.runtime.io.WritableSequentialData; +import com.hedera.pbj.runtime.io.WritableTestBase; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +abstract class BufferedDataTestBase { + + // FUTURE Test that "view" shows the updated data when it is changed on the fly + // FUTURE Verify capacity is never negative (it can't be, maybe nothing to test here) + + @NonNull + protected abstract BufferedData allocate(final int size); + + @NonNull + protected abstract BufferedData wrap(final byte[] arr); + + @NonNull + protected abstract BufferedData wrap(final byte[] arr, final int offset, final int len); + + @Test + @DisplayName("allocated data length test") + void allocateLength() { + final var buf = allocate(12); + assertThat(buf.length()).isEqualTo(12); + } + + @Test + @DisplayName("wrapped data length test") + void wrapLength() { + final var buf = wrap(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}); + assertThat(buf.position()).isEqualTo(0); + assertThat(buf.length()).isEqualTo(8); + } + + @Test + @DisplayName("wrapped data with offset length test") + void wrapOffsetLength() { + final var buf = wrap(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}, 1, 3); + assertThat(buf.length()).isEqualTo(4); + } + + @Test + @DisplayName("wrapped sliced data length test") + void wrapSliceLength() { + final var buf = wrap(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}).slice(1, 3); + assertThat(buf.length()).isEqualTo(3); + } + + @Test + @DisplayName("toString() is safe") + void toStringIsSafe() { + final var buf = wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); + buf.skip(5); + buf.limit(10); + + assertThat(buf.toString()).endsWith("BufferedData[1,2,3,4,5,6,7,8,9,10]"); + + assertEquals(5, buf.position()); + assertEquals(10, buf.limit()); + } + + @Test + void toStringWithOffsetAndLen() { + final var buf = wrap(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 2, 4); + // toString() doesn't depend on position, but respects limit + assertThat(buf.toString()).endsWith("BufferedData[0,1,2,3,4,5]"); + buf.limit(10); + assertThat(buf.toString()).endsWith("BufferedData[0,1,2,3,4,5,6,7,8,9]"); + } + + @Test + void toStringWithSlice() { + final var buf = wrap(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}).slice(2, 4); + assertThat(buf.toString()).endsWith("BufferedData[2,3,4,5]"); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 4, 7, 8, 15, 16, 31, 32, 33, 127, 128, 512, 1000, 1024, 4000, + 16384, 65535, 65536, 65537, 0xFFFFFF, 0x1000000, 0x1000001, 0x7FFFFFFF, + -1, -7, -8, -9, -127, -128, -129, -65535, -65536, -0xFFFFFF, -0x1000000, -0x1000001, -0x80000000}) + @DisplayName("readVarInt() works with views") + void sliceThenReadVarInt(final int num) { + final var buf = allocate(100); + buf.writeVarInt(num, false); + final long afterFirstIntPos = buf.position(); + buf.writeVarInt(num + 1, false); + final long afterSecondIntPos = buf.position(); + + final var slicedBuf = buf.slice(afterFirstIntPos, afterSecondIntPos - afterFirstIntPos); + final int readback = slicedBuf.readVarInt(false); + assertEquals(num + 1, readback); + assertEquals(afterSecondIntPos - afterFirstIntPos, slicedBuf.position()); + } + + @ParameterizedTest + @ValueSource(longs = {0, 1, 7, 8, 9, 127, 128, 129, 1023, 1024, 1025, 65534, 65535, 65536, + 0xFFFFFFFFL, 0x100000000L, 0x100000001L, 0xFFFFFFFFFFFFL, 0x1000000000000L, 0x1000000000001L, + -1, -7, -8, -9, -127, -128, -129, -65534, -65535, -65536, -0xFFFFFFFFL, -0x100000000L, -0x100000001L}) + @DisplayName("readVarLong() works with views") + void sliceThenReadVarLong(final long num) { + final var buf = allocate(256); + buf.writeVarLong(num, false); + final long afterFirstIntPos = buf.position(); + buf.writeVarLong(num + 1, false); + final long afterSecondIntPos = buf.position(); + + final var slicedBuf = buf.slice(afterFirstIntPos, afterSecondIntPos - afterFirstIntPos); + final long readback = slicedBuf.readVarLong(false); + assertEquals(num + 1, readback); + assertEquals(afterSecondIntPos - afterFirstIntPos, slicedBuf.position()); + } + + @Test + @DisplayName("readBytes() works with views") + void sliceThenReadBytes() { + final int SIZE = 100; + final var buf = allocate(SIZE); + for (int i = 0; i < SIZE; i++) { + buf.writeByte((byte) i); + } + + final int START = 10; + final int LEN = 10; + buf.reset(); + buf.skip(START); + final var bytes = buf.readBytes(LEN); + assertEquals(START + LEN, buf.position()); + for (int i = START; i < START + LEN; i++) { + assertEquals((byte) i, bytes.getByte(i - START)); + } + + final var slicedBuf = buf.slice(START, LEN); + final var slicedBytes = slicedBuf.readBytes(LEN); + assertEquals(LEN, slicedBuf.position()); + for (int i = 0; i < LEN; i++) { + assertEquals((byte) (i + START), slicedBytes.getByte(i)); + } + } + + @Test + @DisplayName("readBytes() does always copy") + void readBytesCopies() { + byte[] bytes = new byte[] {1, 2, 3, 4, 5}; + byte[] bytesOrig = bytes.clone(); + final var buf = wrap(bytes); + final Bytes readBytes = buf.readBytes(5); + assertArrayEquals(bytesOrig, readBytes.toByteArray()); + bytes[0] = 127; + bytes[1] = 127; + bytes[2] = 127; + bytes[3] = 127; + bytes[4] = 127; + assertArrayEquals(bytesOrig, readBytes.toByteArray()); + } + + @Test + void getBytesLeavesPositionBufferedData() { + final var buf = wrap(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}); + final var dst = BufferedData.allocate(4); + buf.getBytes(1, dst); + assertThat(buf.position()).isEqualTo(0); + assertThat(dst.position()).isEqualTo(0); + } + + @Test + void getBytesLeavesPositionByteBuffer() { + final var buf = wrap(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}); + final var dst = ByteBuffer.allocate(4); + buf.getBytes(1, dst); + assertThat(buf.position()).isEqualTo(0); + assertThat(dst.position()).isEqualTo(0); + } + + @Nested + final class ReadableSequentialDataTest extends ReadableTestBase { + + @NonNull + @Override + protected ReadableSequentialData emptySequence() { + return BufferedData.EMPTY_BUFFER; + } + + @NonNull + @Override + protected ReadableSequentialData fullyUsedSequence() { + final var buf = wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); + buf.skip(10); + return buf; + } + + @NonNull + @Override + protected ReadableSequentialData sequence(@NonNull byte[] arr) { + return wrap(arr); + } + } + + @Nested + final class RandomAccessDataTest extends RandomAccessTestBase { + + @NonNull + @Override + protected ReadableSequentialData emptySequence() { + return new RandomAccessSequenceAdapter(BufferedData.EMPTY_BUFFER); + } + + @NonNull + @Override + protected ReadableSequentialData fullyUsedSequence() { + final var buf = wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); + buf.skip(10); + return new RandomAccessSequenceAdapter(buf, 10); + } + + @NonNull + @Override + protected ReadableSequentialData sequence(@NonNull byte[] arr) { + return new RandomAccessSequenceAdapter(wrap(arr)); + } + + @NonNull + @Override + protected RandomAccessData randomAccessData(@NonNull byte[] arr) { + return wrap(arr); + } + } + + @Nested + final class WritableSequentialDataTest extends WritableTestBase { + @NonNull + @Override + protected WritableSequentialData sequence() { + return allocate(1024 * 1024 * 2); // the largest expected test value is 1024 * 1024 + } + + @NonNull + @Override + protected WritableSequentialData eofSequence() { + return wrap(new byte[0]); + } + + @NonNull + @Override + protected byte[] extractWrittenBytes(@NonNull WritableSequentialData seq) { + final var buf = (BufferedData) seq; + final var bytes = new byte[Math.toIntExact(buf.position())]; + buf.getBytes(0, bytes); + return bytes; + } + } + + @Nested + final class ReadableSequentialDataViewTest extends ReadableTestBase { + + @NonNull + @Override + protected ReadableSequentialData emptySequence() { + final var buf = wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); + buf.position(7); + return buf.view(0); + } + + @NonNull + @Override + protected ReadableSequentialData fullyUsedSequence() { + final var buf = wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); + buf.position(2); + final var view = buf.view(5); + view.skip(5); + return view; + } + + @NonNull + @Override + protected ReadableSequentialData sequence(@NonNull byte[] arr) { + BufferedData buf = allocate(arr.length + 20); + for (int i = 0; i < 10; i++) { + buf.writeByte((byte) i); + } + buf.position(buf.length() - 10); + for (int i = 0; i < 10; i++) { + buf.writeByte((byte) i); + } + buf.position(10); + buf.writeBytes(arr); + buf.position(10); + return buf.view(arr.length); + } + } + + @Nested + final class RandomAccessDataViewTest extends ReadableTestBase { + + @NonNull + @Override + protected ReadableSequentialData emptySequence() { + final var buf = wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); + buf.position(7); + return new RandomAccessSequenceAdapter(buf.view(0)); + } + + @NonNull + @Override + protected ReadableSequentialData fullyUsedSequence() { + final var buf = wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); + buf.position(2); + final var view = buf.view(5); + view.skip(5); + return new RandomAccessSequenceAdapter(view, 5); + } + + @NonNull + @Override + protected ReadableSequentialData sequence(@NonNull byte[] arr) { + BufferedData buf = allocate(arr.length + 20); + for (int i = 0; i < 10; i++) { + buf.writeByte((byte) i); + } + buf.position(buf.length() - 10); + for (int i = 0; i < 10; i++) { + buf.writeByte((byte) i); + } + buf.position(10); + buf.writeBytes(arr); + buf.position(10); + return new RandomAccessSequenceAdapter(buf.view(arr.length)); + } + } + + @Nested + final class WritableSequentialDataViewTest extends WritableTestBase { + @NonNull + @Override + protected WritableSequentialData sequence() { + final var mb = 1024 * 1024; + final var buf = allocate(mb * 4); // the largest expected test value is 1 mb + buf.position(mb); + return buf.view(mb * 2); + } + + @NonNull + @Override + protected WritableSequentialData eofSequence() { + final var buf = wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); + buf.position(7); + return buf.view(0); + } + + @NonNull + @Override + protected byte[] extractWrittenBytes(@NonNull WritableSequentialData seq) { + final var buf = (BufferedData) seq; + final var bytes = new byte[Math.toIntExact(buf.position())]; + buf.getBytes(0, bytes); + return bytes; + } + } +} diff --git a/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/ByteArrayBufferedDataTest.java b/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/ByteArrayBufferedDataTest.java new file mode 100644 index 00000000..6c509351 --- /dev/null +++ b/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/ByteArrayBufferedDataTest.java @@ -0,0 +1,25 @@ +package com.hedera.pbj.runtime.io.buffer; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; + +final class ByteArrayBufferedDataTest extends BufferedDataTestBase { + + @NonNull + @Override + protected BufferedData allocate(final int size) { + return new ByteArrayBufferedData(ByteBuffer.allocate(size)); + } + + @NonNull + @Override + protected BufferedData wrap(final byte[] arr) { + return new ByteArrayBufferedData(ByteBuffer.wrap(arr)); + } + + @NonNull + @Override + protected BufferedData wrap(final byte[] arr, final int offset, final int len) { + return new ByteArrayBufferedData(ByteBuffer.wrap(arr, offset, len)); + } +} diff --git a/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/BytesTest.java b/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/BytesTest.java similarity index 92% rename from pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/BytesTest.java rename to pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/BytesTest.java index 7205a7cc..85291f0a 100644 --- a/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/BytesTest.java +++ b/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/BytesTest.java @@ -1,9 +1,8 @@ -package com.hedera.pbj.runtime.io; +package com.hedera.pbj.runtime.io.buffer; -import com.hedera.pbj.runtime.io.buffer.BufferedData; -import com.hedera.pbj.runtime.io.buffer.Bytes; -import com.hedera.pbj.runtime.io.buffer.RandomAccessData; +import com.hedera.pbj.runtime.io.ReadableSequentialData; import com.hedera.pbj.runtime.io.stream.WritableStreamingData; +import edu.umd.cs.findbugs.annotations.NonNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -535,44 +534,31 @@ void writeToMessageDigestNo0OffsetPartial() throws NoSuchAlgorithmException { // assertEquals(value, bytes.getDouble(Double.BYTES, ByteOrder.LITTLE_ENDIAN)); // } - @Test - void matchesPrefixByteArrayTest() { - RandomAccessData primary = Bytes.wrap(new byte[]{0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09}); - assertTrue(primary.matchesPrefix(new byte[]{0x01})); - assertTrue(primary.matchesPrefix(new byte[]{0x01,0x02})); - assertTrue(primary.matchesPrefix(new byte[]{0x01,0x02,0x03,0x04,})); - assertTrue(primary.matchesPrefix(new byte[]{0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09})); - - assertFalse(primary.matchesPrefix(new byte[]{0x02})); - assertFalse(primary.matchesPrefix(new byte[]{0x01,0x02,0x03,0x02})); - assertFalse(primary.matchesPrefix(new byte[]{0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x00})); - } + @Nested + class BytesRandomAccessDataTest extends RandomAccessTestBase { + @NonNull + @Override + protected ReadableSequentialData emptySequence() { + return Bytes.EMPTY.toReadableSequentialData(); + } - @Test - void matchesPrefixEmpty_issue37() { - final var bytes1 = Bytes.wrap(new byte[0]); - final var bytes2 = Bytes.wrap(new byte[0]); - assertTrue(bytes1.matchesPrefix(bytes2)); - } + @NonNull + @Override + protected ReadableSequentialData fullyUsedSequence() { + return new RandomAccessSequenceAdapter(Bytes.wrap(new byte[] {0, 1, 2, 4}), 4); + } - @Test - void matchesPrefixBytesTest() { - RandomAccessData primary = Bytes.wrap(new byte[]{0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09}); - RandomAccessData prefixGood1 = Bytes.wrap(new byte[]{0x01}); - assertTrue(primary.matchesPrefix(prefixGood1)); - RandomAccessData prefixGood2 = Bytes.wrap(new byte[]{0x01,0x02}); - assertTrue(primary.matchesPrefix(prefixGood2)); - RandomAccessData prefixGood3 = Bytes.wrap(new byte[]{0x01,0x02,0x03,0x04,}); - assertTrue(primary.matchesPrefix(prefixGood3)); - RandomAccessData prefixGood4 = Bytes.wrap(new byte[]{0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09}); - assertTrue(primary.matchesPrefix(prefixGood4)); - - RandomAccessData prefixBad1 = Bytes.wrap(new byte[]{0x02}); - assertFalse(primary.matchesPrefix(prefixBad1)); - RandomAccessData prefixBad2 = Bytes.wrap(new byte[]{0x01,0x02,0x03,0x02}); - assertFalse(primary.matchesPrefix(prefixBad2)); - RandomAccessData prefixBad3 = Bytes.wrap(new byte[]{0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x00}); - assertFalse(primary.matchesPrefix(prefixBad3)); + @NonNull + @Override + protected ReadableSequentialData sequence(@NonNull byte[] arr) { + return Bytes.wrap(arr).toReadableSequentialData(); + } + + @NonNull + @Override + protected RandomAccessData randomAccessData(@NonNull byte[] bytes) { + return Bytes.wrap(bytes); + } } @ParameterizedTest diff --git a/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/DirectBufferedDataTest.java b/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/DirectBufferedDataTest.java new file mode 100644 index 00000000..d41c4820 --- /dev/null +++ b/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/DirectBufferedDataTest.java @@ -0,0 +1,32 @@ +package com.hedera.pbj.runtime.io.buffer; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; + +final class DirectBufferedDataTest extends BufferedDataTestBase { + + @NonNull + @Override + protected BufferedData allocate(final int size) { + return new DirectBufferedData(ByteBuffer.allocateDirect(size)); + } + + @NonNull + @Override + protected BufferedData wrap(final byte[] arr) { + final BufferedData buf = new DirectBufferedData(ByteBuffer.allocateDirect(arr.length)); + buf.writeBytes(arr); + buf.reset(); + return buf; + } + + @NonNull + @Override + protected BufferedData wrap(byte[] arr, int offset, int len) { + final ByteBuffer buf = ByteBuffer.allocateDirect(arr.length); + buf.put(arr); + buf.position(offset); + buf.limit(offset + len); + return new DirectBufferedData(buf); + } +} diff --git a/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/RandomAccessTestBase.java b/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/RandomAccessTestBase.java new file mode 100644 index 00000000..6d5faa78 --- /dev/null +++ b/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/RandomAccessTestBase.java @@ -0,0 +1,94 @@ +package com.hedera.pbj.runtime.io.buffer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hedera.pbj.runtime.io.ReadableTestBase; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public abstract class RandomAccessTestBase extends ReadableTestBase { + + @NonNull + protected abstract RandomAccessData randomAccessData(@NonNull final byte[] bytes); + + @Test + void sliceLength() { + final var buf = randomAccessData(TEST_BYTES); + assertThat(buf.slice(2, 5).length()).isEqualTo(5); + } + + @ParameterizedTest + @ValueSource(strings = { "", "a", "ab", "abc", "✅" }) + void utf8Strings(final String s) { + final var buf = randomAccessData(s.getBytes(StandardCharsets.UTF_8)); + assertThat(buf.asUtf8String()).isEqualTo(s); + } + + @Test + void getBytesGoodLength() { + final var buf = randomAccessData(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}); + final byte[] dst = new byte[8]; + Arrays.fill(dst, (byte) 0); + assertThat(buf.getBytes(4, dst, 0, 4)).isEqualTo(4); + assertThat(dst).isEqualTo(new byte[] {4, 5, 6, 7, 0, 0, 0, 0}); + } + + @Test + void getBytesExtraSrcLength() { + final var buf = randomAccessData(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}); + final byte[] dst = new byte[8]; + Arrays.fill(dst, (byte) 0); + assertThat(buf.getBytes(3, dst, 0, 6)).isEqualTo(5); + assertThat(dst).isEqualTo(new byte[] {3, 4, 5, 6, 7, 0, 0, 0}); + } + + @Test + void getBytesExtraDstLength() { + final var buf = randomAccessData(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}); + final byte[] dst = new byte[8]; + Arrays.fill(dst, (byte) 0); + assertThatThrownBy(() -> buf.getBytes(4, dst, 6, 4)) + .isInstanceOf(IndexOutOfBoundsException.class); + } + + @Test + void matchesPrefixByteArray() { + final RandomAccessData data = randomAccessData(new byte[]{0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09}); + + assertTrue(data.matchesPrefix(new byte[]{0x01})); + assertTrue(data.matchesPrefix(new byte[]{0x01,0x02})); + assertTrue(data.matchesPrefix(new byte[]{0x01,0x02,0x03,0x04,})); + assertTrue(data.matchesPrefix(new byte[]{0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09})); + + assertFalse(data.matchesPrefix(new byte[]{0x02})); + assertFalse(data.matchesPrefix(new byte[]{0x01,0x02,0x03,0x02})); + assertFalse(data.matchesPrefix(new byte[]{0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x00})); + } + + @Test + void matchesPrefixBytes() { + final RandomAccessData data = randomAccessData(new byte[]{0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09}); + assertTrue(data.matchesPrefix(Bytes.wrap(new byte[]{0x01}))); + assertTrue(data.matchesPrefix(Bytes.wrap(new byte[]{0x01,0x02}))); + assertTrue(data.matchesPrefix(Bytes.wrap(new byte[]{0x01,0x02,0x03,0x04,}))); + assertTrue(data.matchesPrefix(Bytes.wrap(new byte[]{0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09}))); + + assertFalse(data.matchesPrefix(Bytes.wrap(new byte[]{0x02}))); + assertFalse(data.matchesPrefix(Bytes.wrap(new byte[]{0x01,0x02,0x03,0x02}))); + assertFalse(data.matchesPrefix(Bytes.wrap(new byte[]{0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x00}))); + } + + @Test + void matchesPrefixEmpty_issue37() { + final var data1 = randomAccessData(new byte[0]); + final var data2 = randomAccessData(new byte[0]); + assertTrue(data1.matchesPrefix(data2)); + } +} diff --git a/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/RandomAccessDataTest.java b/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/StubbedRandomAccessDataTest.java similarity index 67% rename from pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/RandomAccessDataTest.java rename to pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/StubbedRandomAccessDataTest.java index 0a1afbf9..e58c7ce1 100644 --- a/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/RandomAccessDataTest.java +++ b/pbj-core/pbj-runtime/src/test/java/com/hedera/pbj/runtime/io/buffer/StubbedRandomAccessDataTest.java @@ -1,15 +1,11 @@ package com.hedera.pbj.runtime.io.buffer; import com.hedera.pbj.runtime.io.ReadableSequentialData; -import com.hedera.pbj.runtime.io.ReadableTestBase; import edu.umd.cs.findbugs.annotations.NonNull; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import java.nio.charset.StandardCharsets; + import static org.assertj.core.api.Assertions.assertThat; -public class RandomAccessDataTest extends ReadableTestBase { +public class StubbedRandomAccessDataTest extends RandomAccessTestBase { @NonNull @Override @@ -31,11 +27,10 @@ protected ReadableSequentialData sequence(@NonNull final byte[] arr) { return new RandomAccessSequenceAdapter(new StubbedRandomAccessData(arr)); } - @ParameterizedTest - @ValueSource(strings = { "", "a", "ab", "abc", "✅" }) - void utf8Strings(final String s) { - final var buf = new StubbedRandomAccessData(s.getBytes(StandardCharsets.UTF_8)); - assertThat(buf.asUtf8String()).isEqualTo(s); + @NonNull + @Override + protected RandomAccessData randomAccessData(@NonNull byte[] bytes) { + return new StubbedRandomAccessData(bytes); } private record StubbedRandomAccessData(@NonNull byte[] bytes) implements RandomAccessData {