From cb808a3d5849f1c9224dc03746cb49c8cc02a7f0 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 24 Jun 2024 20:19:19 -0700 Subject: [PATCH 1/5] Fix #1284: avoid buffering-as-String for JsonParser.getFloat()/getDouble()/getDecimal() --- release-notes/VERSION-2.x | 2 + .../jackson/core/base/ParserBase.java | 25 ++++++----- .../jackson/core/util/TextBuffer.java | 36 +++++++++++---- .../core/base/NumberReadDeferralTest.java | 45 +++++++++++++++++++ 4 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 src/test/java/com/fasterxml/jackson/core/base/NumberReadDeferralTest.java diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 728102e847..7bfb448282 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -33,6 +33,8 @@ a pure JSON library. #1274: `NUL`-corrupted keys, values on JSON serialization (reported, fix contributed by Jared S) #1277: Add back Java 22 optimisation in FastDoubleParser +#1284: Optimize `JsonParser.getDoubleValue()/getFloatValue()/getDecimalValue()` + to avoid String allocation #1305: Make helper methods of `WriterBasedJsonGenerator` non-final to allow overriding (contributed by @zhangOranges) #1310: Add new `StreamReadConstraints` (`maxTokenCount`) to limit maximum number diff --git a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java index b987d84623..3f38535707 100644 --- a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java +++ b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java @@ -847,6 +847,7 @@ public double getDoubleValue() throws IOException if (_numTypesValid == NR_UNKNOWN) { _parseNumericValue(NR_DOUBLE); } + // if underlying type not FP, need conversion: if ((_numTypesValid & NR_DOUBLE) == 0) { convertNumberToDouble(); } @@ -987,17 +988,19 @@ private void _parseSlowFloat(int expType) throws IOException if (expType == NR_BIGDECIMAL) { // 04-Dec-2022, tatu: Let's defer actual decoding until it is certain // value is actually needed. - _numberBigDecimal = null; - _numberString = _textBuffer.contentsAsString(); + // 24-Jun-2024, tatu: No; we shouldn't have to defer unless specifically + // request w/ `getNumberValueDeferred()` or so + _numberBigDecimal = _textBuffer.contentsAsDecimal(isEnabled(StreamReadFeature.USE_FAST_DOUBLE_PARSER)); _numTypesValid = NR_BIGDECIMAL; + } else if (expType == NR_DOUBLE) { + _numberDouble = _textBuffer.contentsAsDouble(isEnabled(StreamReadFeature.USE_FAST_DOUBLE_PARSER)); + _numTypesValid = NR_DOUBLE; } else if (expType == NR_FLOAT) { - _numberFloat = 0.0f; - _numberString = _textBuffer.contentsAsString(); + _numberFloat = _textBuffer.contentsAsFloat(isEnabled(StreamReadFeature.USE_FAST_DOUBLE_PARSER)); _numTypesValid = NR_FLOAT; - } else { - // Otherwise double has to do - // 04-Dec-2022, tatu: We can get all kinds of values here, NR_DOUBLE - // but also NR_INT or even NR_UNKNOWN. Shouldn't we try further + } else { // NR_UNKOWN, or one of int types + // 04-Dec-2022, tatu: We can get all kinds of values here + // (NR_INT, NR_LONG or even NR_UNKNOWN). Should we try further // deferring some typing? _numberDouble = 0.0; _numberString = _textBuffer.contentsAsString(); @@ -1248,7 +1251,8 @@ protected BigInteger _convertBigDecimalToBigInteger(BigDecimal bigDec) throws IO protected BigInteger _getBigInteger() throws JsonParseException { if (_numberBigInt != null) { return _numberBigInt; - } else if (_numberString == null) { + } + if (_numberString == null) { throw new IllegalStateException("cannot get BigInteger from current parser state"); } try { @@ -1276,7 +1280,8 @@ protected BigInteger _getBigInteger() throws JsonParseException { protected BigDecimal _getBigDecimal() throws JsonParseException { if (_numberBigDecimal != null) { return _numberBigDecimal; - } else if (_numberString == null) { + } + if (_numberString == null) { throw new IllegalStateException("cannot get BigDecimal from current parser state"); } try { diff --git a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java index 1c49aa8380..11980976f4 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java +++ b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java @@ -643,18 +643,38 @@ public float contentsAsFloat(final boolean useFastParser) throws NumberFormatExc } /** - * @return Buffered text value parsed as a {@link BigDecimal}, if possible - * @throws NumberFormatException if contents are not a valid Java number - * - * @deprecated Since 2.15 just access String contents if necessary, call - * {@link NumberInput#parseBigDecimal(String, boolean)} (or other overloads) - * directly instead + * @deprecated Since 2.15 use {@link #contentsAsDecimal(boolean)} instead. */ @Deprecated public BigDecimal contentsAsDecimal() throws NumberFormatException { - // Was more optimized earlier, removing special handling due to deprecation + return contentsAsDecimal(false); + } + + /** + * @since 2.18 + */ + public BigDecimal contentsAsDecimal(final boolean useFastParser) throws NumberFormatException + { + // Order in which check is somewhat arbitrary... try likeliest ones + // that do not require allocation first + + // except _resultString first since it works best with JDK (non-fast parser) + if (_resultString != null) { + return NumberInput.parseBigDecimal(_resultString, useFastParser); + } + if (_inputStart >= 0) { // shared? + return NumberInput.parseBigDecimal(_inputBuffer, _inputStart, _inputLen, useFastParser); + } + if (_currentSize == 0) { // all content in current segment! + return NumberInput.parseBigDecimal(_currentSegment, 0, _currentSize, useFastParser); + } + if (_resultArray != null) { + return NumberInput.parseBigDecimal(_resultArray, useFastParser); + } + + // Otherwise, segmented so need to use slow path try { - return NumberInput.parseBigDecimal(contentsAsArray()); + return NumberInput.parseBigDecimal(contentsAsArray(), useFastParser); } catch (IOException e) { // JsonParseException is used to denote a string that is too long throw new NumberFormatException(e.getMessage()); diff --git a/src/test/java/com/fasterxml/jackson/core/base/NumberReadDeferralTest.java b/src/test/java/com/fasterxml/jackson/core/base/NumberReadDeferralTest.java new file mode 100644 index 0000000000..d8764d07a2 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/base/NumberReadDeferralTest.java @@ -0,0 +1,45 @@ +package com.fasterxml.jackson.core.base; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.core.JsonParser.NumberType; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +// Tests to verify [core#1284]: no buffering for specific +// access +public class NumberReadDeferralTest extends JUnit5TestBase +{ + private final JsonFactory JSON_F = newStreamFactory(); + + private final String NUM_DOC = "[ 0.1 ]"; + + @Test + public void testDoubleWrtBuffering() throws Exception + { + try (ParserBase p = (ParserBase) JSON_F.createParser(NUM_DOC)) { + assertNull(p._numberString); + assertToken(JsonToken.START_ARRAY, p.nextToken()); + + assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertEquals(NumberType.DOUBLE, p.getNumberType()); +// assertNull(p._numberString); + assertEquals(0.1, p.getDoubleValue()); + assertNull(p._numberString); + } + } + + @Test + public void testFloatWrtBuffering() throws Exception + { + + } + + @Test + public void testBigDecimalWrtBuffering() throws Exception + { + + } +} From d0f48d80b496241c6d15f725f5e68bc9917139b3 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 24 Jun 2024 20:45:49 -0700 Subject: [PATCH 2/5] Fix a bug wrt TextBuffer optimization --- .../java/com/fasterxml/jackson/core/util/TextBuffer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java index 11980976f4..06c96fe48b 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java +++ b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java @@ -551,7 +551,7 @@ public double contentsAsDouble(final boolean useFastParser) throws NumberFormatE if (_inputStart >= 0) { // shared? return NumberInput.parseDouble(_inputBuffer, _inputStart, _inputLen, useFastParser); } - if (_currentSize == 0) { // all content in current segment! + if (!_hasSegments) { // all content in current segment! return NumberInput.parseDouble(_currentSegment, 0, _currentSize, useFastParser); } if (_resultArray != null) { @@ -626,7 +626,7 @@ public float contentsAsFloat(final boolean useFastParser) throws NumberFormatExc if (_inputStart >= 0) { // shared? return NumberInput.parseFloat(_inputBuffer, _inputStart, _inputLen, useFastParser); } - if (_currentSize == 0) { // all content in current segment! + if (!_hasSegments) { // all content in current segment! return NumberInput.parseFloat(_currentSegment, 0, _currentSize, useFastParser); } if (_resultArray != null) { @@ -665,7 +665,7 @@ public BigDecimal contentsAsDecimal(final boolean useFastParser) throws NumberFo if (_inputStart >= 0) { // shared? return NumberInput.parseBigDecimal(_inputBuffer, _inputStart, _inputLen, useFastParser); } - if (_currentSize == 0) { // all content in current segment! + if (!_hasSegments) { // all content in current segment! return NumberInput.parseBigDecimal(_currentSegment, 0, _currentSize, useFastParser); } if (_resultArray != null) { From 875f930a30a7cb8963dda8b18adc6e8dde894400 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 24 Jun 2024 20:48:25 -0700 Subject: [PATCH 3/5] Drop non-useful planned test --- .../core/base/NumberReadDeferralTest.java | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 src/test/java/com/fasterxml/jackson/core/base/NumberReadDeferralTest.java diff --git a/src/test/java/com/fasterxml/jackson/core/base/NumberReadDeferralTest.java b/src/test/java/com/fasterxml/jackson/core/base/NumberReadDeferralTest.java deleted file mode 100644 index d8764d07a2..0000000000 --- a/src/test/java/com/fasterxml/jackson/core/base/NumberReadDeferralTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.fasterxml.jackson.core.base; - -import com.fasterxml.jackson.core.*; -import com.fasterxml.jackson.core.JsonParser.NumberType; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -import org.junit.jupiter.api.Test; - -// Tests to verify [core#1284]: no buffering for specific -// access -public class NumberReadDeferralTest extends JUnit5TestBase -{ - private final JsonFactory JSON_F = newStreamFactory(); - - private final String NUM_DOC = "[ 0.1 ]"; - - @Test - public void testDoubleWrtBuffering() throws Exception - { - try (ParserBase p = (ParserBase) JSON_F.createParser(NUM_DOC)) { - assertNull(p._numberString); - assertToken(JsonToken.START_ARRAY, p.nextToken()); - - assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); - assertEquals(NumberType.DOUBLE, p.getNumberType()); -// assertNull(p._numberString); - assertEquals(0.1, p.getDoubleValue()); - assertNull(p._numberString); - } - } - - @Test - public void testFloatWrtBuffering() throws Exception - { - - } - - @Test - public void testBigDecimalWrtBuffering() throws Exception - { - - } -} From b47d8eba635fee4798bcae063be6b65367529776 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 24 Jun 2024 20:57:20 -0700 Subject: [PATCH 4/5] Minor fix --- src/main/java/com/fasterxml/jackson/core/base/ParserBase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java index 3f38535707..65f169badc 100644 --- a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java +++ b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java @@ -990,7 +990,7 @@ private void _parseSlowFloat(int expType) throws IOException // value is actually needed. // 24-Jun-2024, tatu: No; we shouldn't have to defer unless specifically // request w/ `getNumberValueDeferred()` or so - _numberBigDecimal = _textBuffer.contentsAsDecimal(isEnabled(StreamReadFeature.USE_FAST_DOUBLE_PARSER)); + _numberBigDecimal = _textBuffer.contentsAsDecimal(isEnabled(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER)); _numTypesValid = NR_BIGDECIMAL; } else if (expType == NR_DOUBLE) { _numberDouble = _textBuffer.contentsAsDouble(isEnabled(StreamReadFeature.USE_FAST_DOUBLE_PARSER)); From c79528e23d15faee707311066f8ea54c0daaecac Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 24 Jun 2024 21:03:19 -0700 Subject: [PATCH 5/5] ... --- .../java/com/fasterxml/jackson/core/io/NumberInput.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java b/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java index 585a8dd78b..5cef8dbb33 100644 --- a/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java +++ b/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java @@ -569,9 +569,10 @@ public static BigDecimal parseBigDecimal(final char[] ch) throws NumberFormatExc * @since v2.15 */ public static BigDecimal parseBigDecimal(final char[] ch, final boolean useFastParser) throws NumberFormatException { - return useFastParser ? - BigDecimalParser.parseWithFastParser(ch, 0, ch.length) : - BigDecimalParser.parse(ch); + if (useFastParser) { + return BigDecimalParser.parseWithFastParser(ch, 0, ch.length); + } + return BigDecimalParser.parse(ch); } /**