Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid CoW while processing the next state when HTTP2FrameDecoder decodes #438

Merged
merged 5 commits into from
Apr 17, 2024

Conversation

stefanadranca
Copy link
Contributor

@stefanadranca stefanadranca commented Apr 11, 2024

Motivation:

CoWs appear when switching over the current state of the parser and holding it while also modifying it - inside processNExtState(). Following append(bytes: ByteBuffer)'s pattern, we should use a function that sets the state to an intermediary one with no associated data, before making the transformations.

Modifications:

  • updated the avoidParserCoW() helper function to take into consideration throwing functions
  • used it in the switch cases over the parser state inside processNextState()

Result:

CoW will be avoided when changing the state of the HTTP2FrameDecoder while decoding, so we won't have unnecessary heap allocations.

Motivation:

CoWs appear when switching over the current state of the parser and holding it while also modifying it - inside `processNExtState()`. Following `append(bytes: ByteBuffer)`'s pattern, we should use a function that sets the state to an intermediary one with no associated data, before making the transformations.

Modifications:
- created the `avoidParserCoW()` helper function for the throwing functions
- used it in the switch cases inside `processNextState()`

Result:

CoW will be avoided when changing the state of the HTTP2FrameDecoder when decoding, so we won't have unnecessary heap allocations.
Comment on lines 1397 to 1403
/// This is the implementation for a throwing function. If the state doesn't get updated because an error
/// was thrown, this function sets the parser state to the initial one.
///
/// The `body` function must assign a value to the parser state, otherwise it will be the intermediary
/// `appending` state, which is not wanted and it results in the this function's assertion failure.
@inline(__always)
private mutating func avoidingParserCoW<ReturnType>(_ body: (inout ParserState) throws -> ReturnType) throws -> ReturnType {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than adding a complete new function can we just modify the existing one to accept a throwing closing and use rethrows to propagate the throwing-ness of the closure?

Comment on lines 796 to 799
return ParseResult.needMoreData
}
newState = .accumulatingFrameHeader(processResult)
return ParseResult.continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to use type inference here (and elsewhere in this PR)

Suggested change
return ParseResult.needMoreData
}
newState = .accumulatingFrameHeader(processResult)
return ParseResult.continue
return .needMoreData
}
newState = .accumulatingFrameHeader(processResult)
return .continue

case .initialized:
// no bytes, no frame
return .needMoreData

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: trailing whitespace

Suggested change

@glbrntt
Copy link
Contributor

glbrntt commented Apr 11, 2024

@swift-server-bot add to allowlist

@glbrntt glbrntt added the 🔨 semver/patch No public API change. label Apr 11, 2024
@stefanadranca stefanadranca requested a review from glbrntt April 11, 2024 15:25
@inline(__always)
private mutating func avoidingParserCoW<ReturnType>(_ body: (inout ParserState) -> ReturnType) -> ReturnType {
private mutating func avoidingParserCoW<ReturnType>(_ body: (inout ParserState) throws -> ReturnType) rethrows -> ReturnType {
let initialState = self.state
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I didn't catch this before, but I think by doing this we're undoing the effectiveness of this function. The reason is that the closure will be capturing the associated payload held by self.state, now we're grabbing it here again so we'll CoW if we mutate the one we captured in the closure.

I don't think restoring the state in case of an error is actually necessary either: the previous code didn't do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, I'm trying to think how to solve this. The problem is that in the case of an error that assertion that checks the final state fails - the previous implementation didn't receive a throwing function as a parameter so the state was always updated to a valid one (in the body of that function). In the append() function where avoidingParserCoW was already used there are no throwing function calls.

Copy link
Contributor Author

@stefanadranca stefanadranca Apr 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So when we are not updating it to the initial value the tests in which the process function throws fail - as the assertion from avoidingParserCoW fails. For example:

func testContinuationFrameStreamZero() throws {
      var buf = byteBuffer(withBytes: frameBytes, extraCapacity: 10)
      let continuationFrameBytes: [UInt8] = [
                  0x00, 0x00, 0x07,           // 3-byte payload length (7 bytes)
                  0x09,                       // 1-byte frame type (CONTINUATION)
                  0x04,                       // 1-byte flags (END_HEADERS)
                  0x00, 0x00, 0x00, 0x00,     // 4-byte stream identifier, stream 0
              ]
        buf.writeBytes(continuationFrameBytes)
        buf.writeBuffer(&headers2)

        // This should fail
        decoder.append(bytes: buf)
        XCTAssertThrowsError(try decoder.nextFrame()) { error in
            XCTAssertEqual(error as? InternalError, .codecError(code: .protocolError))
        }
}

Copy link
Contributor Author

@stefanadranca stefanadranca Apr 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we get rid of that assertion if an error is thrown?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you have to enforce that the closure updates the state if it throws.

You can police this in code by making this function non-throwing. Each closure which wants to throw can then catch the error in the closure and return the error in a Result type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we get rid of that assertion if an error is thrown?

No the assertion is there for a reason: we must never leave state in the appending state, otherwise the next call which switches over the state won't have any idea what it can do!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@stefanadranca stefanadranca requested a review from glbrntt April 15, 2024 15:07
Comment on lines 793 to 809
return try self.avoidingParserCoW { newState in
let result = Result<ParseResult, Error> {
guard let processResult = try state.process() else {
newState = .awaitingClientMagic(state)
return .needMoreData
}
newState = .accumulatingFrameHeader(processResult)
return .continue
}

self.state = .accumulatingFrameHeader(newState)
return .continue

switch result {
case let .success(parseResult):
return parseResult
case let .failure(error):
newState = .awaitingClientMagic(state)
throw error
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you fix the indentation here?

@stefanadranca stefanadranca requested a review from glbrntt April 15, 2024 16:35
@glbrntt glbrntt merged commit c6afe04 into apple:main Apr 17, 2024
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🔨 semver/patch No public API change.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants