Skip to content
Closed
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
Expand Up @@ -72,6 +72,24 @@ protected void reset()
*/
public boolean parse(ByteBuffer buffer)
{
// Fast path: the whole 9 octets header is available, so read it
// with a single 8 octets read plus a single 1 octet read, rather
// than stepping the state machine once per octet.
if (state == State.LENGTH && cursor == 0 && buffer.remaining() >= Frame.HEADER_LENGTH)
{
int position = buffer.position();
// Octets 0..7: 3 octets of length, 1 of type, 1 of flags, 3 of the stream id.
long header = buffer.getLong(position);
length = (int)(header >>> 40) & Frame.MAX_MAX_SIZE;
type = (int)(header >>> 32) & 0xFF;
flags = (int)(header >>> 24) & 0xFF;
// Octet 8: the least significant octet of the stream id.
// The most significant bit MUST be ignored as per specification.
streamId = (((int)header << 8) | (buffer.get(position + 8) & 0xFF)) & 0x7F_FF_FF_FF;
buffer.position(position + Frame.HEADER_LENGTH);
return true;
}

while (buffer.hasRemaining())
{
switch (state)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@
public class PrefaceParser
{
private static final Logger LOG = LoggerFactory.getLogger(PrefaceParser.class);
/**
* {@code PREFACE_AS_LONGS[i]} holds the 8 preface octets starting at index {@code i},
* so that the preface can be compared 8 octets at a time from any offset.
*/
private static final long[] PREFACE_AS_LONGS = prefaceAsLongs();

private static long[] prefaceAsLongs()
{
ByteBuffer preface = ByteBuffer.wrap(PrefaceFrame.PREFACE_BYTES);
long[] longs = new long[PrefaceFrame.PREFACE_BYTES.length - Long.BYTES + 1];
for (int i = 0; i < longs.length; i++)
{
longs[i] = preface.getLong(i);
}
return longs;
}

private final Parser.Listener listener;
private int cursor;
Expand All @@ -48,15 +64,49 @@ protected void directUpgrade()

public boolean parse(ByteBuffer buffer)
{
int end = PrefaceFrame.PREFACE_BYTES.length;
int needed = end - cursor;
// Fast path: all the remaining preface octets are available in the
// buffer, so compare them 8 at a time rather than one at a time.
if (buffer.remaining() >= needed)
{
int position = buffer.position();
int index = 0;
for (; needed - index >= Long.BYTES; index += Long.BYTES)
{
if (buffer.getLong(position + index) != PREFACE_AS_LONGS[cursor + index])
return invalidPreface(buffer);
}
if (index < needed)
{
if (needed >= Long.BYTES)
{
// Compare the last 8 octets, overlapping the ones already compared.
if (buffer.getLong(position + needed - Long.BYTES) != PREFACE_AS_LONGS[end - Long.BYTES])
return invalidPreface(buffer);
}
else
{
// Fewer than 8 octets in total, for example after a direct upgrade.
for (; index < needed; index++)
{
if (buffer.get(position + index) != PrefaceFrame.PREFACE_BYTES[cursor + index])
return invalidPreface(buffer);
}
}
}
buffer.position(position + needed);
cursor = 0;
if (LOG.isDebugEnabled())
LOG.debug("Parsed preface bytes from {}", buffer);
return true;
}

while (buffer.hasRemaining())
{
int currByte = buffer.get();
if (currByte != PrefaceFrame.PREFACE_BYTES[cursor])
{
BufferUtil.clear(buffer);
notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_preface");
return false;
}
return invalidPreface(buffer);
++cursor;
if (cursor == PrefaceFrame.PREFACE_BYTES.length)
{
Expand All @@ -69,6 +119,13 @@ public boolean parse(ByteBuffer buffer)
return false;
}

private boolean invalidPreface(ByteBuffer buffer)
{
BufferUtil.clear(buffer);
notifyConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_preface");
return false;
}

protected void notifyConnectionFailure(int error, String reason)
{
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,22 @@ private boolean parse(ByteBuffer buffer, int streamId, int bodyLength)
{
if (length < 6)
return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_settings_frame");
if (buffer.remaining() >= 6)
{
// Fast path: the whole 6 octets setting is available,
// so read the identifier and the value in one go and
// stay in this state for the next setting.
settingId = buffer.getShort() & 0xFF_FF;
settingValue = buffer.getInt();
length -= 6;
if (LOG.isDebugEnabled())
LOG.debug(String.format("setting %d=%d", settingId, settingValue));
if (!onSetting(buffer, settings, settingId, settingValue))
return false;
if (length == 0)
return onSettings(buffer, settings);
break;
}
if (buffer.remaining() >= 2)
{
settingId = buffer.getShort() & 0xFF_FF;
Expand Down
5 changes: 5 additions & 0 deletions tests/jetty-jmh/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>jetty-http2-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.http2.jmh;

import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.eclipse.jetty.http.HostPortHttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.frames.PrefaceFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.generator.HeaderGenerator;
import org.eclipse.jetty.http2.generator.HeadersGenerator;
import org.eclipse.jetty.http2.generator.SettingsGenerator;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.parser.HeaderParser;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.http2.parser.PrefaceParser;
import org.eclipse.jetty.http2.parser.SettingsBodyParser;
import org.eclipse.jetty.io.ArrayByteBufferPool;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.RateControl;
import org.eclipse.jetty.io.RetainableByteBuffer;
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.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

/**
* <p>Benchmarks of the HTTP/2 frame path, one layer above
* HpackBenchmark: generating and parsing whole frames, so that the
* share of a frame that HPACK actually accounts for can be seen.</p>
* <p>Both a warmed encoder and decoder pair and a cold one are covered, as the
* dynamic table changes the balance completely.</p>
*/
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class Http2FrameBenchmark
{
private static final int STREAM_ID = 13;

private final ByteBufferPool bufferPool = new ArrayByteBufferPool();
private MetaData.Request request;
private HeaderParser warmHeaderParser;
private ByteBuffer warmHeadersFrame;
private ByteBuffer coldHeadersFrame;
private ByteBuffer prefaceFrame;
private PrefaceParser prefaceParser;
private ByteBuffer warmSettingsFrame;
private SettingsBodyParser warmSettingsBodyParser;
private ByteBuffer coldSettingsFrame;

@Setup
public void setup() throws Exception
{
HttpFields fields = HttpFields.build()
.put(HttpHeader.USER_AGENT, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
.put(HttpHeader.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.put(HttpHeader.ACCEPT_ENCODING, "gzip, deflate, br")
.put(HttpHeader.ACCEPT_LANGUAGE, "en-GB,en;q=0.9,pl;q=0.8")
.put(HttpHeader.COOKIE, "session=8f14e45fceea167a5a36dedd4bea2543; theme=dark; consent=1")
.put("X-Request-Id", "b3a1c9e2-4f6d-4a1b-9c3e-7d2f8a5b6c4d");
request = new MetaData.Request("GET", HttpScheme.HTTP.asString(), new HostPortHttpField("localhost:8080"),
"/some/resource/path?name=value", HttpVersion.HTTP_2, fields, -1);

HeadersGenerator warmHeadersGenerator = new HeadersGenerator(new HeaderGenerator(bufferPool), new HpackEncoder());

warmHeaderParser = new HeaderParser(RateControl.NO_RATE_CONTROL);

// Warm one generator and parser pair the way a connection warms them.
for (int i = 0; i < 4; i++)
{
warmHeadersFrame = generateHeaders(warmHeadersGenerator);
warmHeaderParser.parse(warmHeadersFrame.slice());
}

coldHeadersFrame = generateHeaders(new HeadersGenerator(new HeaderGenerator(bufferPool), new HpackEncoder()));

prefaceFrame = ByteBuffer.wrap(PrefaceFrame.PREFACE_BYTES);
prefaceParser = new PrefaceParser(new Parser.Listener() {});

SettingsGenerator warmSettingsGenerator = new SettingsGenerator(new HeaderGenerator(bufferPool));

warmSettingsFrame = generateSettings(warmSettingsGenerator);
warmSettingsBodyParser = new SettingsBodyParser(warmHeaderParser, new Parser.Listener() {});

coldSettingsFrame = generateSettings(new SettingsGenerator(new HeaderGenerator(bufferPool)));
}

private ByteBuffer generateSettings(SettingsGenerator generator)
{
RetainableByteBuffer.Mutable buffer = new RetainableByteBuffer.DynamicCapacity();
Map<Integer, Integer> settings = new HashMap<>();
settings.put(SettingsFrame.INITIAL_WINDOW_SIZE, 32768);
settings.put(SettingsFrame.MAX_CONCURRENT_STREAMS, 128);
settings.put(SettingsFrame.MAX_FRAME_SIZE, 8192);
settings.put(SettingsFrame.HEADER_TABLE_SIZE, 4094);
generator.generateSettings(buffer, settings, true);
return toByteBuffer(buffer);
}

private ByteBuffer generateHeaders(HeadersGenerator generator) throws Exception
{
RetainableByteBuffer.Mutable buffer = new RetainableByteBuffer.DynamicCapacity();
generator.generateHeaders(buffer, STREAM_ID, request, null, true);
return toByteBuffer(buffer);
}

private static ByteBuffer toByteBuffer(RetainableByteBuffer.Mutable buffer)
{
ByteBuffer slice = buffer.getByteBuffer();
ByteBuffer copy = ByteBuffer.allocateDirect(slice.remaining());
copy.put(slice).flip();
buffer.release();
return copy;
}

@Benchmark
public boolean parseHeadersFrameWarm()
{
return warmHeaderParser.parse(warmHeadersFrame.slice());
}

@Benchmark
public boolean parseHeadersFrameCold()
{
// A fresh parser each time, so the dynamic table is empty and every
// field arrives as a literal, as on the first message of a connection.
return new HeaderParser(RateControl.NO_RATE_CONTROL).parse(coldHeadersFrame.slice());
}

@Benchmark
public boolean parseSettingsFrameWarm()
{
return warmSettingsBodyParser.parse(warmSettingsFrame.slice());
}

@Benchmark
public boolean parseSettingsFrameCold()
{
return new SettingsBodyParser(new HeaderParser(RateControl.NO_RATE_CONTROL), new Parser.Listener() {})
.parse(coldSettingsFrame.slice());
}

@Benchmark
public boolean parsePreface()
{
return prefaceParser.parse(prefaceFrame.slice());
}


public static void main(String[] args) throws RunnerException
{
Options options = new OptionsBuilder()
.include(Http2FrameBenchmark.class.getSimpleName())
.forks(1)
.build();
new Runner(options).run();
}
}
Loading