Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Improve binary serialization performance #460

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
Allow Java records to become beans.
Add `RecordBean` interface that can be implemented by records.
</action>
<action dev="jodastephen" type="add">
Add `LinkedByteArrayOutputStream`, which is like `ByteArrayOutputStream` but faster.
</action>
<action dev="jodastephen" type="add">
Add `ResolvedType`, which allows generic type information to be managed in a simple way.
</action>
<action dev="jodastephen" type="add" issue="232">
Potentially incompatible change:
Manual equals, hashCode and toString methods must now be located *before* the autogenerated block.
Expand Down Expand Up @@ -45,7 +51,7 @@
</action>
<action dev="jodastephen" type="add" issue="446">
Potentially incompatible change:
The standard binary and simple JSON formats now handle `Iterable` as a collection type.
The standard binary and JSON formats now handle `Iterable` as a collection type.
</action>
<action dev="jodastephen" type="update">
Incompatible change:
Expand Down
188 changes: 188 additions & 0 deletions src/main/java/org/joda/beans/ser/LinkedByteArrayOutputStream.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright 2001-present Stephen Colebourne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.joda.beans.ser;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.Objects;

/**
* An optimised byte array output stream.
* <p>
* This class holds a number of smaller byte arrays internally.
* Each array is typically 1024 bytes, but if a large byte array is written
* the class will hold it as a single large array.
* <p>
* Calling {@link #toByteArray()} returns a single combined byte array.
* Calling {@link #writeTo(OutputStream)} writes the internal arrays without needing to create a combined array.
* <p>
* This class is not thread-safe.
*/
public class LinkedByteArrayOutputStream extends OutputStream {

// segment holding one byte array, the current position in the array, and the next segment when it is full
private static final class ByteSegment {
private final byte[] bytes;
private int pos;
private ByteSegment next;

private ByteSegment(byte[] bytes) {
this.bytes = bytes;
}
}

// the head/root segment
private final ByteSegment head = new ByteSegment(new byte[1024]);
// the current tail
private ByteSegment tail = head;
// the total number of bytes written
private int total;

/**
* Creates an instance.
*/
public LinkedByteArrayOutputStream() {
}

//-------------------------------------------------------------------------
/**
* Writes a single byte to the output stream.
*
* @param val the value
*/
@Override
public void write(int val) {
var tailRemaining = tail.bytes.length - tail.pos;
if (tailRemaining == 0) {
tail.next = new ByteSegment(new byte[1024]);
tail = tail.next;
}
tail.bytes[tail.pos] = (byte) val;
tail.pos++;
total++;
}

/**
* Writes all or part of a byte array to the output stream.
*
* @param bytes the byte array to write, not null
* @param offset the offset from the start of the array
* @param length the number of bytes to write
* @throws IndexOutOfBoundsException if the offset or length is invalid
*/
@Override
public void write(byte[] bytes, int offset, int length) {
Objects.checkFromIndexSize(offset, length, bytes.length);
var tailRemaining = tail.bytes.length - tail.pos;
// first part
var firstPartLength = Math.min(tailRemaining, length);
System.arraycopy(bytes, offset, tail.bytes, tail.pos, firstPartLength);
tail.pos += firstPartLength;
// remainder
var newLength = length - firstPartLength;
if (newLength > 0) {
var newOffset = offset + firstPartLength;
if (newLength >= 1024) {
tail.next = new ByteSegment(Arrays.copyOfRange(bytes, newOffset, length));
} else {
tail.next = new ByteSegment(new byte[1024]);
System.arraycopy(bytes, newOffset, tail.next.bytes, 0, newLength);
}
tail = tail.next;
tail.pos = newLength;
}
total += length;
}

/**
* Writes a byte array to the output stream.
*
* @param bytes the byte array to write, not null
*/
@Override
public void write(byte[] bytes) {
write(bytes, 0, bytes.length);
}

/**
* Writes all the bytes to the specified output stream.
*
* @param out the output stream to write to
* @throws IOException if an IO error occurs
*/
public void writeTo(OutputStream out) throws IOException {
for (var segment = head; segment != null; segment = segment.next) {
out.write(segment.bytes, 0, segment.pos);
}
}

/**
* Returns a single byte array containing all the bytes written to the output stream.
* <p>
* The returned array contains a copy of the internal state of this class.
* <p>
* It is not expected that callers will call this method multiple times, although it is safe to do so.
*
* @return the combined byte array
*/
public byte[] toByteArray() {
var result = new byte[total];
var pos = 0;
for (var segment = head; segment != null; segment = segment.next) {
System.arraycopy(segment.bytes, 0, result, pos, segment.pos);
pos += segment.pos;
}
return result;
}

/**
* Gets the current number of bytes written.
*
* @return the number of bytes written
*/
public int size() {
return total;
}

/**
* A no-op, as this class does not need flushing.
*/
@Override
public void flush() {
}

/**
* A no-op, as this class does not need closing.
*/
@Override
public void close() {
}

/**
* Returns a hex-formatted string of the bytes that have been written.
*/
@Override
public String toString() {
var hex = HexFormat.of();
var buf = new StringBuilder(total * 2);
for (var segment = head; segment != null; segment = segment.next) {
hex.formatHex(buf, segment.bytes, 0, segment.pos);
}
return buf.toString();
}
}
4 changes: 2 additions & 2 deletions src/main/java/org/joda/beans/ser/bin/JodaBeanBinWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
import static org.joda.beans.ser.bin.JodaBeanBinFormat.REFERENCING;
import static org.joda.beans.ser.bin.JodaBeanBinFormat.STANDARD;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Objects;

import org.joda.beans.Bean;
import org.joda.beans.ser.JodaBeanSer;
import org.joda.beans.ser.LinkedByteArrayOutputStream;

/**
* Provides the ability for a Joda-Bean to be written to a binary format.
Expand Down Expand Up @@ -100,7 +100,7 @@ public byte[] write(Bean bean) {
* @return the binary data, not null
*/
public byte[] write(Bean bean, boolean rootType) {
var baos = new ByteArrayOutputStream(1024);
var baos = new LinkedByteArrayOutputStream();
try {
write(bean, rootType, baos);
} catch (IOException ex) {
Expand Down
120 changes: 120 additions & 0 deletions src/test/java/org/joda/beans/ser/TestLinkedByteArrayOutputStream.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright 2001-present Stephen Colebourne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.joda.beans.ser;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Arrays;

import org.junit.jupiter.api.Test;

/**
* Test {@link LinkedByteArrayOutputStream}.
*/
class TestLinkedByteArrayOutputStream {

@Test
void test_empty() {
try (var test = new LinkedByteArrayOutputStream()) {
assertThat(test).hasToString("");
assertThat(test.toByteArray()).isEqualTo(new byte[0]);
assertThat(test.size()).isEqualTo(0);
}
}

@Test
void test_writeByte() {
try (var test = new LinkedByteArrayOutputStream()) {
test.write(33);
assertThat(test).hasToString("21");
assertThat(test.toByteArray()).isEqualTo(new byte[] {33});
assertThat(test.size()).isEqualTo(1);
}
}

@Test
void test_writeByte_growCapacity() {
try (var test = new LinkedByteArrayOutputStream()) {
test.write(new byte[1024]);
test.write(33);
assertThat(test.toString()).hasSize(2050).endsWith("0021");
assertThat(test.toByteArray()).hasSize(1025).endsWith(new byte[] {33});
}
}

@Test
void test_writeByteArray_empty() {
try (var test = new LinkedByteArrayOutputStream()) {
test.write(new byte[0]);
assertThat(test).hasToString("");
assertThat(test.toByteArray()).isEqualTo(new byte[0]);
assertThat(test.size()).isEqualTo(0);
}
}

@Test
void test_writeByteArray_normal() {
try (var test = new LinkedByteArrayOutputStream()) {
var bytes = new byte[] {33, 34, 35, 36, 37};
test.write(bytes, 1, 3);
assertThat(test).hasToString("222324");
assertThat(test.toByteArray()).isEqualTo(new byte[] {34, 35, 36});
assertThat(test.size()).isEqualTo(3);
}
}

@Test
void test_writeByteArray_growCapacityExact() {
try (var test = new LinkedByteArrayOutputStream()) {
var bytes = new byte[] {33, 34, 35, 36, 37};
test.write(new byte[1024]);
test.write(bytes, 1, 4);
assertThat(test.toString()).hasSize(2056).endsWith("22232425");
assertThat(test.toByteArray()).hasSize(1028).endsWith(new byte[] {34, 35, 36, 37});
assertThat(test.size()).isEqualTo(1028);
assertThat(test.toByteArray()).isEqualTo(test.toByteArray());
}
}

@Test
void test_writeByteArray_growCapacitySplit() {
try (var test = new LinkedByteArrayOutputStream()) {
var bytes = new byte[] {33, 34, 35, 36, 37};
test.write(new byte[1022]);
test.write(bytes, 0, 3);
assertThat(test.toString()).hasSize(2050).endsWith("212223");
assertThat(test.toByteArray()).hasSize(1025).endsWith(new byte[] {33, 34, 35});
assertThat(test.size()).isEqualTo(1025);
assertThat(test.toByteArray()).isEqualTo(test.toByteArray());
}
}

@Test
void test_writeByteArray_large() {
try (var test = new LinkedByteArrayOutputStream()) {
var bytes = new byte[2048];
Arrays.fill(bytes, (byte) 33);
test.write(new byte[1022]);
test.write(bytes);
test.write(34);
assertThat(test.toString()).hasSize((1022 + 2048 + 1) * 2).endsWith("212122");
assertThat(test.toByteArray()).hasSize(1022 + 2048 + 1).endsWith(new byte[] {33, 33, 34});
assertThat(test.size()).isEqualTo(1022 + 2048 + 1);
assertThat(test.toByteArray()).isEqualTo(test.toByteArray());
}
}

}
Loading
Loading