diff --git a/ethereum/api/build.gradle b/ethereum/api/build.gradle index 80a3265603e..1ba3ad68f1d 100644 --- a/ethereum/api/build.gradle +++ b/ethereum/api/build.gradle @@ -34,6 +34,8 @@ dependencies { api 'org.slf4j:slf4j-api' api 'org.apache.logging.log4j:log4j-api' + annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess' + compileOnly 'org.jspecify:jspecify' implementation project(':config') diff --git a/ethereum/api/src/jmh/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/QuantityLongToHexBenchmark.java b/ethereum/api/src/jmh/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/QuantityLongToHexBenchmark.java new file mode 100644 index 00000000000..acbacc920ae --- /dev/null +++ b/ethereum/api/src/jmh/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/QuantityLongToHexBenchmark.java @@ -0,0 +1,126 @@ +/* + * Copyright contributors to Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.internal.results; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.apache.tuweni.units.bigints.UInt256; +import org.apache.tuweni.units.bigints.UInt256Value; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@State(Scope.Thread) +@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(value = TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +public class QuantityLongToHexBenchmark { + private static final String HEX_PREFIX = "0x"; + public static final String HEX_ZERO = "0x0"; + private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); + + @Param({"0", "255", "65535", "1000000", "9007199254740991", "9223372036854775807"}) + public long value; + + @Benchmark + public void current(final Blackhole blackhole) { + blackhole.consume(uint256ToHex(UInt256.fromHexString(Long.toHexString(value)))); + } + + @Benchmark + public void simpleHexString(final Blackhole blackhole) { + blackhole.consume(simpleHexString(value)); + } + + @Benchmark + public void directCharArray(final Blackhole blackhole) { + blackhole.consume(directCharArray(value)); + } + + @Benchmark + public void unrolled(final Blackhole blackhole) { + blackhole.consume(unrolled(value)); + } + + private static String simpleHexString(final long value) { + return HEX_PREFIX + Long.toHexString(value); + } + + private static String unrolled(final long value) { + if (value == 0L) { + return HEX_ZERO; + } + final int leadingZeroNibbles = Long.numberOfLeadingZeros(value) >>> 2; + final char[] buf = new char[18]; + buf[2] = HEX_DIGITS[(int) ((value >>> 60) & 0xF)]; + buf[3] = HEX_DIGITS[(int) ((value >>> 56) & 0xF)]; + buf[4] = HEX_DIGITS[(int) ((value >>> 52) & 0xF)]; + buf[5] = HEX_DIGITS[(int) ((value >>> 48) & 0xF)]; + buf[6] = HEX_DIGITS[(int) ((value >>> 44) & 0xF)]; + buf[7] = HEX_DIGITS[(int) ((value >>> 40) & 0xF)]; + buf[8] = HEX_DIGITS[(int) ((value >>> 36) & 0xF)]; + buf[9] = HEX_DIGITS[(int) ((value >>> 32) & 0xF)]; + buf[10] = HEX_DIGITS[(int) ((value >>> 28) & 0xF)]; + buf[11] = HEX_DIGITS[(int) ((value >>> 24) & 0xF)]; + buf[12] = HEX_DIGITS[(int) ((value >>> 20) & 0xF)]; + buf[13] = HEX_DIGITS[(int) ((value >>> 16) & 0xF)]; + buf[14] = HEX_DIGITS[(int) ((value >>> 12) & 0xF)]; + buf[15] = HEX_DIGITS[(int) ((value >>> 8) & 0xF)]; + buf[16] = HEX_DIGITS[(int) ((value >>> 4) & 0xF)]; + buf[17] = HEX_DIGITS[(int) (value & 0xF)]; + buf[leadingZeroNibbles] = '0'; + buf[leadingZeroNibbles + 1] = 'x'; + return new String(buf, leadingZeroNibbles, 18 - leadingZeroNibbles); + } + + private static String directCharArray(final long value) { + if (value == 0L) { + return HEX_ZERO; + } + int nibbles = 16; + while (nibbles > 1 && ((value >>> ((nibbles - 1) * 4)) & 0xFL) == 0L) { + nibbles--; + } + final char[] buf = new char[2 + nibbles]; + buf[0] = '0'; + buf[1] = 'x'; + for (int i = 0; i < nibbles; i++) { + buf[2 + i] = HEX_DIGITS[(int) ((value >>> ((nibbles - 1 - i) * 4)) & 0xFL)]; + } + return new String(buf); + } + + private static String uint256ToHex(final UInt256Value value) { + return value == null ? null : formatMinimalValue(value.toMinimalBytes().toShortHexString()); + } + + private static String formatMinimalValue(final String hexValue) { + final String prefixedHexString = prefixHexNotation(hexValue); + return Objects.equals(prefixedHexString, HEX_PREFIX) ? HEX_ZERO : prefixedHexString; + } + + private static String prefixHexNotation(final String hexValue) { + return hexValue.startsWith(HEX_PREFIX) ? hexValue : HEX_PREFIX + hexValue; + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/Quantity.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/Quantity.java index c9d06e509f5..c47e0b385b1 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/Quantity.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/Quantity.java @@ -44,29 +44,29 @@ public static String create(final UInt64Value value) { } public static String create(final int value) { - return uint256ToHex(UInt256.valueOf(value)); + return HEX_PREFIX + Integer.toHexString(value); } public static String create(final long value) { - return uint256ToHex(UInt256.fromHexString(Long.toHexString(value))); + return HEX_PREFIX + Long.toHexString(value); + } + + public static String create(final byte value) { + return HEX_PREFIX + Integer.toHexString(value); } public static String create(final Bytes value) { - return create(value.toArrayUnsafe()); + return uint256ToHex(UInt256.fromBytes(Bytes32.leftPad(value))); } public static String create(final byte[] value) { - return uint256ToHex(UInt256.fromBytes(Bytes32.leftPad(Bytes.wrap(value)))); + return create(Bytes.wrap(value)); } public static String create(final BigInteger value) { return uint256ToHex(UInt256.valueOf(value)); } - public static String create(final byte value) { - return formatMinimalValue(Integer.toHexString(value)); - } - /** * Fixed-length bytes sequences and should be returned as hex strings zero-padded to the expected * length. diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/QuantityTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/QuantityTest.java new file mode 100644 index 00000000000..28f6239fc57 --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/QuantityTest.java @@ -0,0 +1,216 @@ +/* + * Copyright contributors to Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.jsonrpc.internal.results; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt256; +import org.apache.tuweni.units.bigints.UInt64; +import org.junit.jupiter.api.Test; + +public class QuantityTest { + + // --- create(int) --- + + @Test + public void createInt_zero() { + assertThat(Quantity.create(0)).isEqualTo("0x0"); + } + + @Test + public void createInt_one() { + assertThat(Quantity.create(1)).isEqualTo("0x1"); + } + + @Test + public void createInt_maxValue() { + assertThat(Quantity.create(Integer.MAX_VALUE)) + .isEqualTo("0x" + Integer.toHexString(Integer.MAX_VALUE)); + } + + // --- create(long) --- + + @Test + public void createLong_zero() { + assertThat(Quantity.create(0L)).isEqualTo("0x0"); + } + + @Test + public void createLong_one() { + assertThat(Quantity.create(1L)).isEqualTo("0x1"); + } + + @Test + public void createLong_maxValue() { + assertThat(Quantity.create(Long.MAX_VALUE)).isEqualTo("0x" + Long.toHexString(Long.MAX_VALUE)); + } + + // --- create(byte) --- + + @Test + public void createByte_zero() { + assertThat(Quantity.create((byte) 0)).isEqualTo("0x0"); + } + + @Test + public void createByte_one() { + assertThat(Quantity.create((byte) 1)).isEqualTo("0x1"); + } + + @Test + public void createByte_maxPositive() { + assertThat(Quantity.create(Byte.MAX_VALUE)).isEqualTo("0x7f"); + } + + // --- create(Bytes) --- + + @Test + public void createBytes_empty() { + assertThat(Quantity.create(Bytes.EMPTY)).isEqualTo("0x0"); + } + + @Test + public void createBytes_zero() { + assertThat(Quantity.create(Bytes.of(0x00))).isEqualTo("0x0"); + } + + @Test + public void createBytes_oneByte() { + assertThat(Quantity.create(Bytes.of(0x01))).isEqualTo("0x1"); + } + + @Test + public void createBytes_leadingZerosStripped() { + assertThat(Quantity.create(Bytes.fromHexString("0x000001"))).isEqualTo("0x1"); + } + + @Test + public void createBytes_multipleBytes() { + assertThat(Quantity.create(Bytes.fromHexString("0x0100"))).isEqualTo("0x100"); + } + + // --- create(byte[]) --- + + @Test + public void createByteArray_zero() { + assertThat(Quantity.create(new byte[] {0x00})).isEqualTo("0x0"); + } + + @Test + public void createByteArray_leadingZerosStripped() { + assertThat(Quantity.create(new byte[] {0x00, 0x00, 0x01})).isEqualTo("0x1"); + } + + @Test + public void createByteArray_multipleBytes() { + assertThat(Quantity.create(new byte[] {0x01, 0x00})).isEqualTo("0x100"); + } + + // --- create(BigInteger) --- + + @Test + public void createBigInteger_zero() { + assertThat(Quantity.create(BigInteger.ZERO)).isEqualTo("0x0"); + } + + @Test + public void createBigInteger_one() { + assertThat(Quantity.create(BigInteger.ONE)).isEqualTo("0x1"); + } + + @Test + public void createBigInteger_large() { + final BigInteger value = BigInteger.TWO.pow(128); + assertThat(Quantity.create(value)).isEqualTo("0x" + value.toString(16)); + } + + // --- create(UInt256Value) --- + + @Test + public void createUInt256_null() { + assertThat(Quantity.create((UInt256) null)).isNull(); + } + + @Test + public void createUInt256_zero() { + assertThat(Quantity.create(UInt256.ZERO)).isEqualTo("0x0"); + } + + @Test + public void createUInt256_one() { + assertThat(Quantity.create(UInt256.ONE)).isEqualTo("0x1"); + } + + @Test + public void createUInt256_maxValue() { + assertThat(Quantity.create(UInt256.MAX_VALUE)) + .startsWith("0x") + .hasSize(66); // 0x + 64 hex chars + } + + // --- create(UInt64Value) --- + + @Test + public void createUInt64_null() { + assertThat(Quantity.create((UInt64) null)).isEqualTo("0x0"); + } + + @Test + public void createUInt64_zero() { + assertThat(Quantity.create(UInt64.ZERO)).isEqualTo("0x0"); + } + + @Test + public void createUInt64_one() { + assertThat(Quantity.create(UInt64.ONE)).startsWith("0x"); + } + + // --- longToPaddedHex --- + + @Test + public void longToPaddedHex_zero() { + assertThat(Quantity.longToPaddedHex(0L, 4)).isEqualTo("0x00000000"); + } + + @Test + public void longToPaddedHex_padded() { + assertThat(Quantity.longToPaddedHex(1L, 4)).isEqualTo("0x00000001"); + } + + @Test + public void longToPaddedHex_full() { + assertThat(Quantity.longToPaddedHex(0xdeadbeefL, 4)).isEqualTo("0xdeadbeef"); + } + + // --- isValid --- + + @Test + public void isValid_withPrefix() { + assertThat(Quantity.isValid("0x1")).isTrue(); + } + + @Test + public void isValid_withoutPrefix() { + assertThat(Quantity.isValid("1")).isFalse(); + } + + @Test + public void isValid_zeroHex() { + assertThat(Quantity.isValid("0x0")).isTrue(); + } +}