Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.pekko.http.impl.util

import java.util.concurrent.TimeUnit

import scala.annotation.tailrec

import org.openjdk.jmh.annotations._

@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Array(Mode.AverageTime))
@Fork(2)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
private[util] class EnhancedStringBenchmark {

@Param(Array("short", "header", "long"))
var input = "header"

var string: String = null
var target: Array[Byte] = null

@Setup
def setup(): Unit = {
string = input match {
case "short" => "chunked"
case "header" => "Content-Type: application/json; charset=UTF-8\r\n"
case "long" => "x" * 1024
}
target = new Array[Byte](string.length + 16)
}

// the per-character loop that `getAsciiBytes` used before it switched to the bulk `String.getBytes` copy,
// kept for comparison
@tailrec private def charAtLoop(s: String, array: Array[Byte], ix: Int): Unit =
if (ix < array.length) {
array(ix) = s.charAt(ix).asInstanceOf[Byte]
charAtLoop(s, array, ix + 1)
}

@Benchmark
def baseline_charAt_asciiBytes(): Array[Byte] = {
val array = new Array[Byte](string.length)
charAtLoop(string, array, 0)
array
}

@Benchmark
def asciiBytes(): Array[Byte] = string.asciiBytes

@Benchmark
def getAsciiBytes(): Array[Byte] = {
string.getAsciiBytes(target, 8)
target
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import java.util.Locale

import org.apache.pekko.annotation.InternalApi

import scala.annotation.tailrec
import scala.annotation.{ nowarn, tailrec }
import scala.collection.immutable

/**
Expand Down Expand Up @@ -71,13 +71,13 @@ private[http] class EnhancedString(val underlying: String) extends AnyVal {
* Truncates characters to 8-bit byte value.
* If the array does not have enough space for the whole string only the portion that fits is copied.
*/
// `String.getBytes(int, int, byte[], int)` is deprecated (not for removal) but is the only JDK primitive that copies
// the low 8 bits of each char straight into an existing array: a single `System.arraycopy` for a Latin-1 coded
// string on JDK 9+, instead of a `charAt` call with its coder branch and bounds check per character.
@nowarn("cat=deprecation")
def getAsciiBytes(array: Array[Byte], offset: Int): Unit = {
@tailrec def rec(ix: Int): Unit =
if (ix < array.length) {
array(ix) = underlying.charAt(ix - offset).asInstanceOf[Byte]
rec(ix + 1)
}
rec(offset)
val len = math.min(underlying.length, array.length - offset)
if (len > 0) underlying.getBytes(0, len, array, offset)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.pekko.http.impl.util

import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

class EnhancedStringSpec extends AnyWordSpec with Matchers {

"EnhancedString.asciiBytes" should {
"map every ASCII character to the byte of the same value" in {
val ascii = new String(Array.tabulate(128)(_.toChar))
ascii.asciiBytes shouldEqual Array.tabulate(128)(_.toByte)
}

"truncate a character to its low 8 bits instead of substituting a replacement character" in {
// U+0100 is not Latin-1, so the string uses the UTF-16 coder on JDK 9+; the byte must still be the low 8 bits
// rather than the '?' that encoding with a charset would produce.
"aÿbĀcሴ".asciiBytes shouldEqual Array[Byte]('a', 0xFF.toByte, 'b', 0x00, 'c', 0x34)
}

"encode the empty string to an empty array" in {
"".asciiBytes shouldEqual Array.emptyByteArray
}
}

"EnhancedString.getAsciiBytes" should {
"copy the bytes into the target array starting at the offset" in {
val array = Array.fill[Byte](8)('.')
"abc".getAsciiBytes(array, 2)
new String(array, "ISO-8859-1") shouldEqual "..abc..."
}

"copy only the portion that fits when the array is too small" in {
val array = Array.fill[Byte](4)('.')
"abcdef".getAsciiBytes(array, 2)
new String(array, "ISO-8859-1") shouldEqual "..ab"
}

"copy nothing when the offset is at or past the end of the array" in {
val array = Array.fill[Byte](3)('.')
"abc".getAsciiBytes(array, 3)
"abc".getAsciiBytes(array, 4)
new String(array, "ISO-8859-1") shouldEqual "..."
}

"truncate a non-Latin-1 character to its low 8 bits when copying at an offset" in {
val array = new Array[Byte](3)
"Łł".getAsciiBytes(array, 1)
array shouldEqual Array[Byte](0, 0x41, 0x42)
}
}
}