Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
44 changes: 44 additions & 0 deletions Benchmarks/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// swift-tools-version:6.3
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2026 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import PackageDescription

let package = Package(
name: "swift-nio-http3-benchmarks",
platforms: [
.macOS(.v13)
],
dependencies: [
.package(path: "../"),
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-nio-quic-helpers.git", branch: "main"),
.package(url: "https://github.com/ordo-one/benchmark.git", from: "1.35.0"),
],
targets: [
.executableTarget(
name: "QPACKBenchmarks",
dependencies: [
.product(name: "NIOHTTP3", package: "swift-nio-http3"),
.product(name: "HTTPTypes", package: "swift-http-types"),
.product(name: "NIOQUICHelpers", package: "swift-nio-quic-helpers"),
.product(name: "Benchmark", package: "benchmark"),
],
path: "QPACKBenchmarks",
plugins: [
.plugin(name: "BenchmarkPlugin", package: "benchmark")
]
)
]
)
73 changes: 73 additions & 0 deletions Benchmarks/QPACKBenchmarks/QPACKBenchmarks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2026 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Benchmark
import HTTPTypes
@_spi(Benchmarks) import NIOHTTP3
import NIOQUICHelpers

/// A realistic request header set for a browser GET, exercising a mix of exact
/// static-table matches (`:method GET`), name-only matches (`:authority`) and
/// headers absent from the table (`x-custom-header`).
private let requestHeaders: [HTTPField] = [
HTTPField(name: .init(parsed: ":method")!, value: "GET"),
HTTPField(name: .init(parsed: ":scheme")!, value: "https"),
HTTPField(name: .init(parsed: ":authority")!, value: "www.example.com"),
HTTPField(name: .init(parsed: ":path")!, value: "/index.html"),
HTTPField(name: .init(parsed: "user-agent")!, value: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"),
HTTPField(name: .init(parsed: "accept")!, value: "*/*"),
HTTPField(name: .init(parsed: "accept-encoding")!, value: "gzip, deflate, br"),
HTTPField(name: .init(parsed: "accept-language")!, value: "en-US,en;q=0.9"),
HTTPField(name: .init(parsed: "cache-control")!, value: "no-cache"),
HTTPField(name: .init(parsed: "cookie")!, value: "session=abc123def456; theme=dark"),
HTTPField(name: .init(parsed: "referer")!, value: "https://www.example.com/"),
HTTPField(name: .init(parsed: "x-custom-header")!, value: "some-custom-value"),
]

let benchmarks: @Sendable () -> Void = {
// Exercises StaticHeaderTable.find on every header, with no dynamic table.
Benchmark(
"QPACKStaticEncode_BrowserGET",
configuration: .init(
metrics: [.mallocCountTotal, .instructions, .wallClock],
scalingFactor: .kilo
)
) { benchmark in
for _ in benchmark.scaledIterations {
blackHole(QPACKBenchmarks.staticEncode(headers: requestHeaders))
}
}

// Exercises the static-table lookup plus the dynamic table on every header.
Benchmark(
"QPACKDynamicEncode_BrowserGET",
configuration: .init(
metrics: [.mallocCountTotal, .instructions, .wallClock],
scalingFactor: .kilo
)
) { benchmark in
for _ in benchmark.scaledIterations {
blackHole(
QPACKBenchmarks.dynamicEncode(
headers: requestHeaders,
streamID: QUICStreamID(rawValue: 0),
dynamicTableMaxCapacity: 4096,
dynamicTableInitialCapacity: 4096,
maxBlockedStreams: 100,
targetEvictableFraction: 0.5
)
)
}
}
}
55 changes: 55 additions & 0 deletions Sources/NIOHTTP3/QPACK+SPI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2026 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

public import HTTPTypes
public import NIOQUICHelpers
private import QPACK

// QPACK isn't a library product, so add entry points for benchmarks here under SPI.
@_spi(Benchmarks)
public enum QPACKBenchmarks {
/// Encode `headers` with the static-only encoder. Returns the number of field lines
/// produced.
///
/// The return value doesn't matter, but needs to be there so the calling benchmark
/// can stop the function call from being optimised away.
@_spi(Benchmarks)
public static func staticEncode(headers: [HTTPField]) -> Int {
let encoder = StaticQPACKEncoder()
return encoder.encode(headers: headers).lines.count
}

/// Create a dynamic encoder and encode `headers` on `streamID`. Returns the number
/// of field lines produced.
///
/// The return value doesn't matter, but needs to be there so the calling benchmark
/// can stop the function call from being optimised away.
@_spi(Benchmarks)
public static func dynamicEncode(
headers: [HTTPField],
streamID: QUICStreamID,
dynamicTableMaxCapacity: Int,
dynamicTableInitialCapacity: Int,
maxBlockedStreams: Int,
targetEvictableFraction: Double
) -> Int {
var (encoder, _) = DynamicQPACKEncoder.create(
dynamicTableMaxCapacity: dynamicTableMaxCapacity,
dynamicTableInitialCapacity: dynamicTableInitialCapacity,
maxBlockedStreams: maxBlockedStreams,
targetEvictableFraction: targetEvictableFraction
)
return encoder.encode(headers: headers, forStream: streamID).fieldSection.lines.count
}
}
32 changes: 20 additions & 12 deletions Sources/QPACK/Headers/StaticHeaderTable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ package enum StaticHeaderTable {
(.init(parsed: "x-frame-options")!, "sameorigin"), // 98
]

/// A mapping of header name to the array of static table indices carrying that name.
/// Array are guaranteed to be non-empty, and their values are in ascending index order.
Comment thread
glbrntt marked this conversation as resolved.
Outdated
private static let indicesByName: [HTTPField.Name: [Int]] = {
var result = [HTTPField.Name: [Int]](minimumCapacity: Self.shared.count)
for index in Self.shared.indices {
result[Self.shared[index].0, default: []].append(index)
}
return result
}()

/// Get the element of the static table at the specific index if it exists
package static func get(at index: Int) -> (HTTPField.Name, String)? {
if self.shared.indices.contains(index) {
Expand All @@ -143,21 +153,19 @@ package enum StaticHeaderTable {
/// parameter, an indication whether that value was also found. Returns `nil`
/// if no matching header name could be located.
static func find(name: HTTPField.Name, value: String?) -> (index: Int, containsValue: Bool)? {
var nameOnlyMatchIndex: Int?
for index in Self.shared.indices {
let header = Self.shared[index]
if header.0 == name {
if header.1 == value {
guard let indices = Self.indicesByName[name] else {
return nil
}

if let value = value {
for index in indices {
if Self.shared[index].1 == value {
return (index: index, containsValue: true)
} else if nameOnlyMatchIndex == nil {
nameOnlyMatchIndex = index
}
}
}
if let nameOnlyMatchIndex {
return (index: nameOnlyMatchIndex, containsValue: false)
} else {
return nil
}

// No value (or no matching value), return the first index.
return (index: indices[0], containsValue: false)
}
}
Loading