Skip to content

Commit

Permalink
Minor optimisations (#454)
Browse files Browse the repository at this point in the history
* Don't create ByteBuffer before we know its length

* Minor JSON change for performance test

* Add content-length to response generator body init

* Use HTTPFields.contains

Doesn't construct http field contents

* Use NIO helpers

* Combined with PR #458

Co-authored-by: Joannis Orlandos <joannis@orlandos.nl>

---------

Co-authored-by: Joannis Orlandos <joannis@orlandos.nl>
  • Loading branch information
adam-fowler and Joannis committed May 20, 2024
1 parent 3f9d6b3 commit 2c80d7b
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 27 deletions.
8 changes: 5 additions & 3 deletions Sources/Hummingbird/Codable/JSON/JSONCoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ extension JSONEncoder: ResponseEncoder {
/// - value: Value to encode
/// - request: Request used to generate response
public func encode(_ value: some Encodable, from request: Request, context: some BaseRequestContext) throws -> Response {
var buffer = context.allocator.buffer(capacity: 0)
let data = try self.encode(value)
buffer.writeBytes(data)
let buffer = context.allocator.buffer(data: data)
return Response(
status: .ok,
headers: [.contentType: "application/json; charset=utf-8"],
headers: [
.contentType: "application/json; charset=utf-8",
.contentLength: data.count.description,
],
body: .init(byteBuffer: buffer)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ extension URLEncodedFormEncoder: ResponseEncoder {
/// - value: Value to encode
/// - request: Request used to generate response
public func encode(_ value: some Encodable, from request: Request, context: some BaseRequestContext) throws -> Response {
var buffer = context.allocator.buffer(capacity: 0)
let string = try self.encode(value)
buffer.writeString(string)
let buffer = context.allocator.buffer(string: string)
return Response(
status: .ok,
headers: [.contentType: "application/x-www-form-urlencoded"],
headers: [
.contentType: "application/x-www-form-urlencoded",
.contentLength: buffer.readableBytes.description,
],
body: .init(byteBuffer: buffer)
)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Hummingbird/Middleware/CORSMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public struct CORSMiddleware<Context: BaseRequestContext>: RouterMiddleware {
/// apply CORS middleware
public func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
// if no origin header then don't apply CORS
guard request.headers[.origin] != nil else {
guard request.headers.contains(.origin) else {
return try await next(request, context)
}

Expand Down
29 changes: 25 additions & 4 deletions Sources/Hummingbird/Router/ResponseGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ extension String: ResponseGenerator {
/// Generate response holding string
public func response(from request: Request, context: some BaseRequestContext) -> Response {
let buffer = context.allocator.buffer(string: self)
return Response(status: .ok, headers: [.contentType: "text/plain; charset=utf-8"], body: .init(byteBuffer: buffer))
return Response(
status: .ok,
headers: [
.contentType: "text/plain; charset=utf-8",
.contentLength: buffer.readableBytes.description,
],
body: .init(byteBuffer: buffer)
)
}
}

Expand All @@ -42,15 +49,29 @@ extension Substring: ResponseGenerator {
/// Generate response holding string
public func response(from request: Request, context: some BaseRequestContext) -> Response {
let buffer = context.allocator.buffer(substring: self)
return Response(status: .ok, headers: [.contentType: "text/plain; charset=utf-8"], body: .init(byteBuffer: buffer))
return Response(
status: .ok,
headers: [
.contentType: "text/plain; charset=utf-8",
.contentLength: buffer.readableBytes.description,
],
body: .init(byteBuffer: buffer)
)
}
}

/// Extend ByteBuffer to conform to ResponseGenerator
extension ByteBuffer: ResponseGenerator {
/// Generate response holding bytebuffer
public func response(from request: Request, context: some BaseRequestContext) -> Response {
Response(status: .ok, headers: [.contentType: "application/octet-stream"], body: .init(byteBuffer: self))
Response(
status: .ok,
headers: [
.contentType: "application/octet-stream",
.contentLength: self.readableBytes.description,
],
body: .init(byteBuffer: self)
)
}
}

Expand Down Expand Up @@ -98,7 +119,7 @@ public struct EditedResponse<Generator: ResponseGenerator>: ResponseGenerator {
// only add headers from generated response if they don't exist in override headers
var headers = self.headers
for header in response.headers {
if headers[header.name] == nil {
if !headers.contains(header.name) {
headers.append(header)
}
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/HummingbirdCore/Request/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ public struct Request: Sendable {
/// Body of HTTP request
public var body: RequestBody
/// Request HTTP method
@inlinable
public var method: HTTPRequest.Method { self.head.method }
/// Request HTTP headers
@inlinable
public var headers: HTTPFields { self.head.headerFields }

// MARK: Initialization
Expand Down
37 changes: 22 additions & 15 deletions Sources/HummingbirdCore/Response/Response.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,40 @@ import HTTPTypes

/// Holds all the required to generate a HTTP Response
public struct Response: Sendable {
public var head: HTTPResponse
/// Response status
public var status: HTTPResponse.Status
/// Response headers
public var headers: HTTPFields
/// Response head constructed from status and headers
@inlinable
public var head: HTTPResponse {
get { HTTPResponse(status: self.status, headerFields: self.headers) }
set {
self.status = newValue.status
self.headers = newValue.headerFields
}
}

/// Response body
public var body: ResponseBody {
didSet {
if let contentLength = body.contentLength {
self.head.headerFields[.contentLength] = String(describing: contentLength)
self.headers[.contentLength] = String(describing: contentLength)
}
}
}

/// Initialize Response
@inlinable
public init(status: HTTPResponse.Status, headers: HTTPFields = .init(), body: ResponseBody = .init()) {
self.head = .init(status: status, headerFields: headers)
self.status = status
self.headers = headers
self.body = body
if let contentLength = body.contentLength, headers[.contentLength] == nil {
self.head.headerFields[.contentLength] = String(describing: contentLength)
if let contentLength = body.contentLength, !self.headers.contains(.contentLength) {
self.headers[.contentLength] = String(describing: contentLength)
}
}

public var status: HTTPResponse.Status {
get { self.head.status }
set { self.head.status = newValue }
}

public var headers: HTTPFields {
get { self.head.headerFields }
set { self.head.headerFields = newValue }
}

/// Return HEAD response based off this response
public func createHeadResponse() -> Response {
.init(status: self.status, headers: self.headers, body: .init())
Expand Down
6 changes: 5 additions & 1 deletion Sources/PerformanceTest/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,14 @@ router.post { request, _ in
return Response(status: .ok, body: .init(asyncSequence: request.body))
}

struct Object: ResponseEncodable {
let message: String
}

// return JSON
// ./wrk -c 128 -d 15s -t 8 http://localhost:8080/json
router.get("json") { _, _ in
return ["message": "Hello, world"]
return Object(message: "Hello, world")
}

// return JSON
Expand Down

0 comments on commit 2c80d7b

Please sign in to comment.