From 2884b586d68181e5c2574928f5b58ed1c20ee3a7 Mon Sep 17 00:00:00 2001 From: catbref Date: Wed, 12 Aug 2020 14:07:56 +0100 Subject: [PATCH] Try to reduce byte arrays being created/copied wherever possible. Also bump to v1.3.7 due to interface changes. --- Java/pom.xml | 2 +- .../main/java/org/ciyam/at/MachineState.java | 214 +++++++++++++----- .../java/org/ciyam/at/SerializationTests.java | 2 + 3 files changed, 159 insertions(+), 59 deletions(-) diff --git a/Java/pom.xml b/Java/pom.xml index 58aa1a7..b772e6e 100644 --- a/Java/pom.xml +++ b/Java/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.ciyam AT - 1.3.6 + 1.3.7 jar true diff --git a/Java/src/main/java/org/ciyam/at/MachineState.java b/Java/src/main/java/org/ciyam/at/MachineState.java index 46ee345..2d65d15 100644 --- a/Java/src/main/java/org/ciyam/at/MachineState.java +++ b/Java/src/main/java/org/ciyam/at/MachineState.java @@ -3,7 +3,6 @@ package org.ciyam.at; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -65,8 +64,6 @@ public class MachineState { public final short numUserStackPages; public final long minActivationAmount; - private final byte[] headerBytes; - /** Constants set in effect */ private final VersionedConstants constants; @@ -137,15 +134,10 @@ public class MachineState { // Constructors - /** For internal use when recreating a machine state */ - private MachineState(byte[] headerBytes, API api, AtLoggerFactory loggerFactory) { - if (headerBytes.length != HEADER_LENGTH) - throw new IllegalArgumentException("headerBytes length " + headerBytes.length + " incorrect, expected " + HEADER_LENGTH); - - this.headerBytes = headerBytes; - - // Parsing header bytes - ByteBuffer byteBuffer = ByteBuffer.wrap(this.headerBytes); + /** For internal use when recreating a machine state. Leaves ByteBuffer position immediately after header. */ + private MachineState(ByteBuffer byteBuffer) { + if (byteBuffer.remaining() < HEADER_LENGTH) + throw new IllegalArgumentException("ByteBuffer too small (" + byteBuffer.remaining() + "), minimum " + HEADER_LENGTH); this.version = byteBuffer.getShort(); if (this.version < 1) @@ -174,8 +166,13 @@ public class MachineState { throw new IllegalArgumentException("Number of user stack pages must be >= 0"); this.minActivationAmount = byteBuffer.getLong(); + if (this.minActivationAmount < 0) + throw new IllegalArgumentException("Minimum activation amount must be >= 0"); - // Header OK - set up code and data buffers + // Header OK + } + + private void setupSegmentsAndStacks() { this.codeByteBuffer = ByteBuffer.allocate(this.numCodePages * this.constants.CODE_PAGE_SIZE); this.dataByteBuffer = ByteBuffer.allocate(this.numDataPages * this.constants.DATA_PAGE_SIZE); @@ -185,35 +182,29 @@ public class MachineState { this.userStackByteBuffer = ByteBuffer.allocate(this.numUserStackPages * this.constants.USER_STACK_PAGE_SIZE); this.userStackByteBuffer.position(this.userStackByteBuffer.limit()); // Downward-growing stack, so start at the end - - this.api = api; - this.currentBlockHeight = 0; - this.currentBalance = 0; - this.previousBalance = 0; - this.steps = 0; - this.loggerFactory = loggerFactory; - this.logger = loggerFactory.create(MachineState.class); } /** For creating a new machine state */ public MachineState(API api, AtLoggerFactory loggerFactory, byte[] creationBytes) { - this(Arrays.copyOfRange(creationBytes, 0, HEADER_LENGTH), api, loggerFactory); + this(ByteBuffer.wrap(creationBytes)); int expectedLength = HEADER_LENGTH + this.numCodePages * this.constants.CODE_PAGE_SIZE + this.numDataPages * this.constants.DATA_PAGE_SIZE; if (creationBytes.length != expectedLength) throw new IllegalArgumentException("Creation bytes length does not match header values"); + setupSegmentsAndStacks(); + System.arraycopy(creationBytes, HEADER_LENGTH, this.codeByteBuffer.array(), 0, this.numCodePages * this.constants.CODE_PAGE_SIZE); System.arraycopy(creationBytes, HEADER_LENGTH + this.numCodePages * this.constants.CODE_PAGE_SIZE, this.dataByteBuffer.array(), 0, this.numDataPages * this.constants.DATA_PAGE_SIZE); - commonFinalConstruction(); + commonFinalConstruction(api, loggerFactory); } /** For creating a new machine state - used in tests */ public MachineState(API api, AtLoggerFactory loggerFactory, byte[] headerBytes, byte[] codeBytes, byte[] dataBytes) { - this(headerBytes, api, loggerFactory); + this(ByteBuffer.wrap(headerBytes)); if (codeBytes.length > this.numCodePages * this.constants.CODE_PAGE_SIZE) throw new IllegalArgumentException("Number of code pages too small to hold code bytes"); @@ -221,14 +212,25 @@ public class MachineState { if (dataBytes.length > this.numDataPages * this.constants.DATA_PAGE_SIZE) throw new IllegalArgumentException("Number of data pages too small to hold data bytes"); + setupSegmentsAndStacks(); + System.arraycopy(codeBytes, 0, this.codeByteBuffer.array(), 0, codeBytes.length); System.arraycopy(dataBytes, 0, this.dataByteBuffer.array(), 0, dataBytes.length); - commonFinalConstruction(); + commonFinalConstruction(api, loggerFactory); } - private void commonFinalConstruction() { + private void commonFinalConstruction(API api, AtLoggerFactory loggerFactory) { + this.api = api; + this.loggerFactory = loggerFactory; + this.logger = loggerFactory.create(MachineState.class); + + this.currentBlockHeight = 0; + this.currentBalance = 0; + this.previousBalance = 0; + this.steps = 0; + this.programCounter = 0; this.onStopAddress = 0; this.onErrorAddress = null; @@ -443,33 +445,89 @@ public class MachineState { return this.codeByteBuffer.array(); } + private static class NumericByteArrayOutputStream extends ByteArrayOutputStream { + public NumericByteArrayOutputStream(int capacity) { + super(capacity); + } + + public void writeShort(short value) { + this.write((byte) (value >> 8)); + this.write((byte) (value)); + } + + public void writeInt(int value) { + this.write((byte) (value >> 24)); + this.write((byte) (value >> 16)); + this.write((byte) (value >> 8)); + this.write((byte) (value)); + } + + public void writeLong(long value) { + this.write((byte) (value >> 56)); + this.write((byte) (value >> 48)); + this.write((byte) (value >> 40)); + this.write((byte) (value >> 32)); + this.write((byte) (value >> 24)); + this.write((byte) (value >> 16)); + this.write((byte) (value >> 8)); + this.write((byte) (value)); + } + } + /** For serializing a machine state */ public byte[] toBytes() { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int capacity = HEADER_LENGTH + + this.dataByteBuffer.capacity() + + this.callStackByteBuffer.capacity() + + this.userStackByteBuffer.capacity() + + 7 * 4 // Misc ints like stack lengths, PC, PCS, flags, etc. + + 10 * 8; // Misc longs like previous balance, frozen balance, A, B, etc. + + NumericByteArrayOutputStream bytes = new NumericByteArrayOutputStream(capacity); try { // Header first - bytes.write(this.headerBytes); + + // Version + bytes.writeShort(version); + + // Reserved + bytes.writeShort((short) 0); + + // Code length + bytes.writeShort(numCodePages); + + // Data length + bytes.writeShort(numDataPages); + + // Call stack length + bytes.writeShort(numCallStackPages); + + // User stack length + bytes.writeShort(numUserStackPages); + + // Minimum activation amount + bytes.writeLong(minActivationAmount); // Data bytes.write(this.dataByteBuffer.array()); // Call stack length (32bit unsigned int) int callStackLength = this.callStackByteBuffer.limit() - this.callStackByteBuffer.position(); - bytes.write(toByteArray(callStackLength)); + bytes.writeInt(callStackLength); // Call stack (only the bytes actually in use) bytes.write(this.callStackByteBuffer.array(), this.callStackByteBuffer.position(), callStackLength); // User stack length (32bit unsigned int) int userStackLength = this.userStackByteBuffer.limit() - this.userStackByteBuffer.position(); - bytes.write(toByteArray(userStackLength)); + bytes.writeInt(userStackLength); // User stack (only the bytes actually in use) bytes.write(this.userStackByteBuffer.array(), this.userStackByteBuffer.position(), userStackLength); // Actual state - bytes.write(toByteArray(this.programCounter)); - bytes.write(toByteArray(this.onStopAddress)); - bytes.write(toByteArray(this.previousBalance)); + bytes.writeInt(this.programCounter); + bytes.writeInt(this.onStopAddress); + bytes.writeLong(this.previousBalance); // Various flags Flags flags = new Flags(); @@ -489,30 +547,30 @@ public class MachineState { boolean hasNonZeroB = this.b1 != 0 || this.b2 != 0 || this.b3 != 0 || this.b4 != 0; flags.push(hasNonZeroB); - bytes.write(toByteArray(flags.intValue())); + bytes.writeInt(flags.intValue()); // Optional flag-indicated extra info in same order as above if (this.onErrorAddress != null) - bytes.write(toByteArray(this.onErrorAddress)); + bytes.writeInt(this.onErrorAddress); if (this.sleepUntilHeight != null) - bytes.write(toByteArray(this.sleepUntilHeight)); + bytes.writeInt(this.sleepUntilHeight); if (this.frozenBalance != null) - bytes.write(toByteArray(this.frozenBalance)); + bytes.writeLong(this.frozenBalance); if (hasNonZeroA) { - bytes.write(toByteArray(this.a1)); - bytes.write(toByteArray(this.a2)); - bytes.write(toByteArray(this.a3)); - bytes.write(toByteArray(this.a4)); + bytes.writeLong(this.a1); + bytes.writeLong(this.a2); + bytes.writeLong(this.a3); + bytes.writeLong(this.a4); } if (hasNonZeroB) { - bytes.write(toByteArray(this.b1)); - bytes.write(toByteArray(this.b2)); - bytes.write(toByteArray(this.b3)); - bytes.write(toByteArray(this.b4)); + bytes.writeLong(this.b1); + bytes.writeLong(this.b2); + bytes.writeLong(this.b3); + bytes.writeLong(this.b4); } } catch (IOException e) { return null; @@ -525,14 +583,22 @@ public class MachineState { public static MachineState fromBytes(API api, AtLoggerFactory loggerFactory, byte[] stateBytes, byte[] codeBytes) { ByteBuffer byteBuffer = ByteBuffer.wrap(stateBytes); - byte[] headerBytes = new byte[HEADER_LENGTH]; - byteBuffer.get(headerBytes); + MachineState state = new MachineState(byteBuffer); - MachineState state = new MachineState(headerBytes, api, loggerFactory); - - if (codeBytes.length != state.codeByteBuffer.capacity()) + if (codeBytes.length != state.numCodePages * state.constants.CODE_PAGE_SIZE) throw new IllegalStateException("Passed codeBytes does not match length in header"); + state.api = api; + state.loggerFactory = loggerFactory; + state.logger = loggerFactory.create(MachineState.class); + + state.currentBlockHeight = 0; + state.currentBalance = 0; + state.previousBalance = 0; + state.steps = 0; + + state.setupSegmentsAndStacks(); + // Pull in code bytes System.arraycopy(codeBytes, 0, state.codeByteBuffer.array(), 0, codeBytes.length); @@ -557,6 +623,34 @@ public class MachineState { System.arraycopy(stateBytes, byteBuffer.position(), state.userStackByteBuffer.array(), state.userStackByteBuffer.position(), userStackLength); byteBuffer.position(byteBuffer.position() + userStackLength); + extractMisc(byteBuffer, state); + + return state; + } + + /** For restoring only flags from a previously serialized machine state */ + public static MachineState flagsOnlyfromBytes(byte[] stateBytes) { + ByteBuffer byteBuffer = ByteBuffer.wrap(stateBytes); + + MachineState state = new MachineState(byteBuffer); + + // Skip data segment + byteBuffer.position(byteBuffer.position() + state.numDataPages * state.constants.DATA_PAGE_SIZE); + + // Skip call stack + int callStackLength = byteBuffer.getInt(); + byteBuffer.position(byteBuffer.position() + callStackLength); + + // Skip user stack + int userStackLength = byteBuffer.getInt(); + byteBuffer.position(byteBuffer.position() + userStackLength); + + extractMisc(byteBuffer, state); + + return state; + } + + private static void extractMisc(ByteBuffer byteBuffer, MachineState state) { // Actual state state.programCounter = byteBuffer.getInt(); state.onStopAddress = byteBuffer.getInt(); @@ -599,24 +693,23 @@ public class MachineState { state.b3 = byteBuffer.getLong(); state.b4 = byteBuffer.getLong(); } - - return state; } /** Returns data bytes from saved state to allow external analysis, e.g. confirming expected payouts, etc. */ - public static byte[] extractDataBytes(AtLoggerFactory loggerFactory, byte[] stateBytes) { + public static byte[] extractDataBytes(byte[] stateBytes) { ByteBuffer byteBuffer = ByteBuffer.wrap(stateBytes); - byte[] headerBytes = new byte[HEADER_LENGTH]; - byteBuffer.get(headerBytes); + short version = byteBuffer.getShort(0); + VersionedConstants constants = VERSIONED_CONSTANTS.get(version); - MachineState state = new MachineState(headerBytes, null, loggerFactory); + short numDataPages = byteBuffer.getShort(2 /*version*/ + 2 /*reserved*/ + 2 /*code pages*/); // Extract data bytes - int dataBytesLength = state.dataByteBuffer.capacity(); + int dataBytesLength = numDataPages * constants.DATA_PAGE_SIZE; byte[] dataBytes = new byte[dataBytesLength]; + // More efficient than ByteBuffer.get() - System.arraycopy(stateBytes, byteBuffer.position(), dataBytes, 0, dataBytesLength); + System.arraycopy(stateBytes, HEADER_LENGTH, dataBytes, 0, dataBytesLength); return dataBytes; } @@ -649,6 +742,11 @@ public class MachineState { } } + /** Convert short to big-endian byte array */ + public static byte[] toByteArray(short value) { + return new byte[] { (byte) (value >> 8), (byte) (value) }; + } + /** Convert int to big-endian byte array */ public static byte[] toByteArray(int value) { return new byte[] { (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value) }; diff --git a/Java/src/test/java/org/ciyam/at/SerializationTests.java b/Java/src/test/java/org/ciyam/at/SerializationTests.java index 1433bee..49f60e6 100644 --- a/Java/src/test/java/org/ciyam/at/SerializationTests.java +++ b/Java/src/test/java/org/ciyam/at/SerializationTests.java @@ -107,6 +107,8 @@ public class SerializationTests extends ExecutableTest { byte[] packedRestoredSate = restoredState.toBytes(); assertTrue(Arrays.equals(packedState, packedRestoredSate)); + + restoredState = MachineState.fromBytes(api, loggerFactory, packedState, codeBytes); } private byte[] simulate() {