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,74 @@
/*
* 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.shaded.com.twitter.hpack

import java.util.concurrent.TimeUnit

import org.openjdk.jmh.annotations._

import org.apache.pekko.http.impl.util.ByteStringOutputStream

@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Array(Mode.AverageTime))
@Fork(2)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 8, time = 2)
class HpackEncoderBenchmark {

/** `default` lets the encoder pick Huffman when it is shorter, the other two force one string literal form. */
@Param(Array("default", "huffman", "raw"))
var mode = "default"

// indexing is disabled so that every call encodes the values as string literals instead of table references
var encoder: Encoder = null
val out = new ByteStringOutputStream(128)

// a typical browser request; every value goes through HuffmanEncoder.getEncodedLength and most through encode
val headers: Array[(String, String)] = Array(
":method" -> "GET",
":scheme" -> "https",
":authority" -> "www.example.com",
":path" -> "/api/v1/users/12345/orders?page=2&sort=created_at&direction=desc",
"user-agent" ->
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"accept-language" -> "en-US,en;q=0.9",
"accept-encoding" -> "gzip, deflate, br",
"cookie" -> "session=ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12; theme=dark; consent=1",
"cache-control" -> "no-cache")

@Setup
def setup(): Unit =
encoder = mode match {
case "default" => new Encoder(4096, false, false, false)
case "huffman" => new Encoder(4096, false, true, false)
case "raw" => new Encoder(4096, false, false, true)
}

@Benchmark
def encodeHeaders(): Int = {
var i = 0
while (i < headers.length) {
val (name, value) = headers(i)
encoder.encodeHeader(out, name, value, false)
i += 1
}
out.takeByteString().length
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@

package org.apache.pekko.http.shaded.com.twitter.hpack;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import org.apache.pekko.http.impl.util.ByteStringOutputStream;
import org.apache.pekko.http.impl.util.StringTools;
import org.apache.pekko.http.shaded.com.twitter.hpack.HpackUtil.IndexType;

Expand Down Expand Up @@ -75,8 +74,8 @@ public Encoder(int maxHeaderTableSize) {
}

/** Encode the header field into the header block. */
public void encodeHeader(OutputStream out, String name, String value, boolean sensitive)
throws IOException {
public void encodeHeader(
ByteStringOutputStream out, String name, String value, boolean sensitive) {

// If the header value is sensitive then it must never be indexed
if (sensitive) {
Expand Down Expand Up @@ -130,7 +129,7 @@ public void encodeHeader(OutputStream out, String name, String value, boolean se
}

/** Set the maximum table size. */
public void setMaxHeaderTableSize(OutputStream out, int maxHeaderTableSize) throws IOException {
public void setMaxHeaderTableSize(ByteStringOutputStream out, int maxHeaderTableSize) {
if (maxHeaderTableSize < 0) {
throw new IllegalArgumentException("Illegal Capacity: " + maxHeaderTableSize);
}
Expand All @@ -148,7 +147,7 @@ public int getMaxHeaderTableSize() {
}

/** Encode integer according to Section 5.1. */
private static void encodeInteger(OutputStream out, int mask, int n, int i) throws IOException {
private static void encodeInteger(ByteStringOutputStream out, int mask, int n, int i) {
if (n < 0 || n > 8) {
throw new IllegalArgumentException("N: " + n);
}
Expand All @@ -171,23 +170,25 @@ private static void encodeInteger(OutputStream out, int mask, int n, int i) thro
}

/** Encode string literal according to Section 5.2. */
private void encodeStringLiteral(OutputStream out, String string) throws IOException {
int length = string.length();
int huffmanLength = Huffman.ENCODER.getEncodedLength(string);
private void encodeStringLiteral(ByteStringOutputStream out, String string) {
// convert once up front: the length computation, the Huffman coder and the raw literal all work
// on the octets
byte[] stringBytes = StringTools.asciiStringBytes(string);
int length = stringBytes.length;
int huffmanLength = Huffman.ENCODER.getEncodedLength(stringBytes);
if ((huffmanLength < length && !forceHuffmanOff) || forceHuffmanOn) {
encodeInteger(out, 0x80, 7, huffmanLength);
Huffman.ENCODER.encode(out, string);
int position = out.reserve(huffmanLength);
Huffman.ENCODER.encode(stringBytes, out.array(), position, huffmanLength);
} else {
byte[] stringBytes = StringTools.asciiStringBytes(string);
encodeInteger(out, 0x00, 7, length);
out.write(stringBytes, 0, stringBytes.length);
out.write(stringBytes, 0, length);
}
}

/** Encode literal header field according to Section 6.2. */
private void encodeLiteral(
OutputStream out, String name, String value, IndexType indexType, int nameIndex)
throws IOException {
ByteStringOutputStream out, String name, String value, IndexType indexType, int nameIndex) {
int mask;
int prefixBits;
switch (indexType) {
Expand Down Expand Up @@ -228,7 +229,7 @@ private int getNameIndex(String name) {
* Ensure that the dynamic table has enough room to hold 'headerSize' more bytes. Removes the
* oldest entry from the dynamic table until sufficient space is available.
*/
private void ensureCapacity(int headerSize) throws IOException {
private void ensureCapacity(int headerSize) {
while (size + headerSize > capacity) {
int index = length();
if (index == 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@

package org.apache.pekko.http.shaded.com.twitter.hpack;

import java.io.IOException;
import java.io.OutputStream;

final class HuffmanEncoder {

private final int[] codes;
Expand All @@ -51,25 +48,37 @@ final class HuffmanEncoder {
}

/**
* Compresses the input string literal using the Huffman coding.
* Compresses the input string literal using the Huffman coding, writing the result directly into
* <code>dst</code> instead of byte by byte to an <code>OutputStream</code>.
*
* @param out the output stream for the compressed data
* @throws IOException if an I/O error occurs. In particular, an <code>IOException</code> may be
* thrown if the output stream has been closed.
* @param data the string literal to be Huffman encoded
* @param dst the array to write the Huffman coded string literal to
* @param dstOffset the position in <code>dst</code> to start writing at
* @param encodedLength the value of {@link #getEncodedLength(byte[])} for <code>data</code>,
* which the caller has typically already computed to decide whether to use Huffman coding and
* to reserve room in <code>dst</code>
*/
public void encode(OutputStream out, String string) throws IOException {
if (out == null) {
throw new NullPointerException("out");
} else if (string == null) {
throw new NullPointerException("string");
public void encode(byte[] data, byte[] dst, int dstOffset, int encodedLength) {
if (data == null) {
throw new NullPointerException("data");
} else if (dst == null) {
throw new NullPointerException("dst");
} else if (dstOffset < 0 || encodedLength < 0 || dstOffset + encodedLength > dst.length) {
throw new IndexOutOfBoundsException(
"dstOffset "
+ dstOffset
+ ", encodedLength "
+ encodedLength
+ ", dst.length "
+ dst.length);
}

int pos = dstOffset;
long current = 0;
int n = 0;
int len = string.length();

for (int i = 0; i < len; i++) {
int b = string.charAt(i) & 0xFF;
for (byte value : data) {
int b = value & 0xFF;
int code = codes[b];
int nbits = lengths[b];

Expand All @@ -79,14 +88,22 @@ public void encode(OutputStream out, String string) throws IOException {

while (n >= 8) {
n -= 8;
out.write(((int) (current >> n)));
dst[pos++] = (byte) (current >> n);
}
}

if (n > 0) {
current <<= (8 - n);
current |= (0xFF >>> n); // this should be EOS symbol
out.write((int) current);
dst[pos++] = (byte) current;
}

if (pos - dstOffset != encodedLength) {
throw new IllegalArgumentException(
"encodedLength "
+ encodedLength
+ " does not match the Huffman encoded length "
+ (pos - dstOffset));
}
}

Expand All @@ -96,13 +113,13 @@ public void encode(OutputStream out, String string) throws IOException {
* @param data the string literal to be Huffman encoded
* @return the number of bytes required to Huffman encode <code>data</code>
*/
public int getEncodedLength(String data) {
public int getEncodedLength(byte[] data) {
if (data == null) {
throw new NullPointerException("data");
}
long len = 0;
for (int i = 0; i < data.length(); i++) {
len += lengths[data.charAt(i) & 0xFF];
for (byte b : data) {
len += lengths[b & 0xFF];
}
return (int) ((len + 7) >> 3);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 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.

# The shaded HPACK Encoder writes into the internal ByteStringOutputStream instead of any java.io.OutputStream,
# and the package-private HuffmanEncoder codes byte arrays instead of Strings, straight into that stream's array
ProblemFilters.exclude[IncompatibleMethTypeProblem]("org.apache.pekko.http.shaded.com.twitter.hpack.Encoder.encodeHeader")
ProblemFilters.exclude[IncompatibleMethTypeProblem]("org.apache.pekko.http.shaded.com.twitter.hpack.Encoder.setMaxHeaderTableSize")
ProblemFilters.exclude[IncompatibleMethTypeProblem]("org.apache.pekko.http.shaded.com.twitter.hpack.HuffmanEncoder.encode")
ProblemFilters.exclude[IncompatibleMethTypeProblem]("org.apache.pekko.http.shaded.com.twitter.hpack.HuffmanEncoder.getEncodedLength")
ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.http.shaded.com.twitter.hpack.HuffmanEncoder.encode")
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@

package org.apache.pekko.http.impl.engine.http2.hpack

import java.io.ByteArrayOutputStream
import org.apache.pekko
import pekko.annotation.InternalApi
import pekko.http.impl.engine.http2.Http2Protocol.SettingIdentifier
import pekko.http.impl.engine.http2._
import pekko.http.impl.util.ByteStringOutputStream
import pekko.stream.{ Attributes, FlowShape, Inlet, Outlet }
import pekko.stream.stage.{ GraphStage, GraphStageLogic, InHandler, OutHandler, StageLogging }
import pekko.util.ByteString
Expand All @@ -44,7 +44,7 @@ private[http2] object HeaderCompression extends GraphStage[FlowShape[FrameEvent,
private val currentMaxFrameSize = Http2Protocol.InitialMaxFrameSize

val encoder = new pekko.http.shaded.com.twitter.hpack.Encoder(Http2Protocol.InitialMaxHeaderTableSize)
val os = new ByteArrayOutputStream(128)
val os = new ByteStringOutputStream(128)

def onPull(): Unit = pull(eventsIn)
def onPush(): Unit = grab(eventsIn) match {
Expand All @@ -71,8 +71,7 @@ private[http2] object HeaderCompression extends GraphStage[FlowShape[FrameEvent,
throw new IllegalStateException(
s"Didn't expect key-value-pair [$key] -> [$value](${value.getClass}) here.")
}
val result = ByteString.fromArrayUnsafe(os.toByteArray) // BAOS.toByteArray always creates a copy
os.reset()
val result = os.takeByteString() // hands the array over without copying and starts a new block
if (result.size <= currentMaxFrameSize)
push(eventsOut, HeadersFrame(streamId, endStream, endHeaders = true, result, prioInfo))
else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ private[http] object PerMessageDeflate {
output.write(buffer, 0, count)
count = inflater.inflate(buffer)
}
output.toByteStringUnsafe
output.takeByteString()
} catch {
case ex: DataFormatException =>
throw new ProtocolException(s"Invalid WebSocket compressed message: ${ex.getMessage}")
Expand Down Expand Up @@ -351,7 +351,7 @@ private[http] object PerMessageDeflate {
output.write(buffer, 0, count)
count = deflater.deflate(buffer, 0, buffer.length, Deflater.SYNC_FLUSH)
}
val bytes = output.toByteStringUnsafe
val bytes = output.takeByteString()
if (removeTail && bytes.endsWith(EmptyStoredBlock)) bytes.dropRight(EmptyStoredBlock.length) else bytes
}

Expand Down
Loading