Skip to content

chore: replace deprecated String constructor in StringTools - #1230

Open
pjfanning wants to merge 1 commit into
apache:mainfrom
pjfanning:perf/string-tools-latin1
Open

chore: replace deprecated String constructor in StringTools#1230
pjfanning wants to merge 1 commit into
apache:mainfrom
pjfanning:perf/string-tools-latin1

Conversation

@pjfanning

Copy link
Copy Markdown
Member

Motivation

StringTools.asciiStringFromBytes uses the deprecated new String(byte[], int hibyte) behind a @nowarn, justified by this comment:

Deprecated constructor but also (unfortunately) the fastest way to convert a ASCII encoded byte array into a String without extra copying.

That was true on Java 8, where the alternative expanded the bytes into a char[]. It stopped being true with JDK 9 and compact strings, and this branch requires JDK 17.

The method sits on the HTTP/2 HPACK decode path (shaded/com/twitter/hpack/Decoder.readStringLiteral), so it is worth being sure the replacement changes neither behaviour nor performance.

Modification

-  @nowarn("msg=deprecated")
   def asciiStringFromBytes(bytes: Array[Byte]): String =
-    // Deprecated constructor but also (unfortunately) the fastest way to convert a ASCII encoded byte array
-    // into a String without extra copying.
-    new String(bytes, 0)
+    // ISO-8859-1 rather than US-ASCII: this maps every byte to the character of the same value, which
+    // is what the deprecated `new String(bytes, 0)` this replaces did. Since JDK 9 (compact strings) it
+    // keeps the array as is with a LATIN1 coder, so it is the same single copy.
+    new String(bytes, ISO88591)

plus dropping the now-unused scala.annotation.nowarn import. ISO88591 is the constant already defined in this package (impl/util/package.scala).

ISO-8859-1, not US-ASCII, despite the method name. new String(bytes, 0) sets char = byte & 0xFF for every byte; ISO-8859-1 does exactly the same, so the two agree on every possible input. US-ASCII would turn bytes above 0x7F into U+FFFD, and HPACK string literals are opaque octets (RFC 7541), so such bytes really can arrive here. This matches the same decision made for asciiString in the parser.

Result

No deprecated JDK API, no @nowarn, and no misleading Java-8-era comment, with identical behaviour.

I did not want to take the "compact strings make these identical" reasoning on trust, so:

  • Equivalence: verified in jshell that both forms produce equal Strings for all 256 byte values.
  • Performance: JMH, average time, sizes 12 / 64 / 4096 bytes.
12 B 64 B 4096 B
new String(bytes, 0) 25.3 ± 7.0 ns 30.2 ± 10.7 ns 1260.2 ± 959.8 ns
new String(bytes, ISO_8859_1) 28.2 ± 30.3 ns 43.1 ± 72.5 ns 952.7 ± 171.4 ns

Error bars overlap at every size, so there is no measurable difference in either direction. (A crude jshell loop first suggested the charset form was ~2x slower at small sizes; that turned out to be warmup/ordering noise, which is why this was re-measured under JMH.)

Tests

  • New StringToolsSpec pins the byte-to-character mapping across the whole 0x00-0xFF range - the property that would break if someone later "corrected" the charset to US-ASCII to match the method name - plus a round-trip through asciiStringBytes and the empty-array case. sbt "http-core / Test / testOnly org.apache.pekko.http.impl.util.StringToolsSpec" - 4 passed. It is a regression guard, not a failing-before test; this change is deliberately behaviour preserving, so nothing can fail against it.
  • sbt http-core/mimaReportBinaryIssues - clean.
  • scalafmt --mode diff-ref=upstream/main - clean.
  • I did not run the http2-tests suite that exercises the HPACK codec end to end; leaving that to CI.

References

None - found while reviewing the code base against the JDK 17 baseline

Motivation:
StringTools.asciiStringFromBytes used the deprecated `new String(byte[], int)`
behind a @nowarn, justified by a comment saying it was "the fastest way to
convert a ASCII encoded byte array into a String without extra copying". That
was true on Java 8, where the alternative expanded the bytes into a char array.
Since JDK 9 and compact strings it is not, and this branch requires JDK 17.

The method is on the HTTP/2 HPACK decode path, so it is worth being sure the
replacement is neither a behaviour nor a performance change.

Modification:
Decode with ISO-8859-1, which maps every byte to the character of the same
value, exactly as the deprecated constructor did with hibyte 0. Not US-ASCII,
which would turn bytes above 0x7F into replacement characters; HPACK string
literals are opaque octets. Drops the @nowarn and the scala.annotation.nowarn
import, and reuses the ISO88591 constant already in this package.

Result:
No deprecated JDK API and no misleading comment, with identical behaviour.

Tests:
- New StringToolsSpec pins the byte to character mapping across the whole
  0x00 to 0xFF range, which is what would break if the charset were changed to
  US-ASCII later. It is a regression guard, not a failing-before test: this
  change is deliberately behaviour preserving
- Verified in jshell that the two forms produce equal Strings for all 256 byte
  values
- JMH, average time over sizes 12, 64 and 4096 bytes: 25.3 / 30.2 / 1260.2 ns
  for the deprecated form against 28.2 / 43.1 / 952.7 ns for ISO-8859-1, error
  bars overlapping at every size, so no measurable difference either way
- sbt "http-core / Test / testOnly org.apache.pekko.http.impl.util.StringToolsSpec" - 4 passed
- sbt http-core/mimaReportBinaryIssues - clean
- scalafmt --mode diff-ref=upstream/main - clean

References:
None - found while reviewing the code base against the JDK 17 baseline
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant