QUIC Connection Group Server Sending Pace

We have an implementation in which we use QUIC via a connection group, server are client are on Swift using the Network framework.

Our use case is, the server should send data buffers to the client as fast and as much as possible, now the pace to call the send method from the server should be carefully done, because if we send too much data of course the client is not gonna be able to receive it.

The question would be, is there a way to query the congestion window so we know on the server side, how much data we should be able to send at some point? Asking because we are not getting all the data we are sending from the server on our client side...

We are using these settings:

        let options = NWProtocolQUIC.Options(alpn: ["h3"])
        options.direction = .bidirectional
        //
        options.idleTimeout = 86_400_000
        options.maxUDPPayloadSize = Int.max
        options.initialMaxData = Int.max
        options.initialMaxStreamDataBidirectionalLocal = Int.max
        options.initialMaxStreamDataBidirectionalRemote = Int.max
        options.initialMaxStreamDataUnidirectional = Int.max
        options.initialMaxStreamsBidirectional = 400
        options.initialMaxStreamsUnidirectional = 400  

Questions:

1.- Can we get a little more detail in above options, specifically on their impact to the actual connection?

2.- IsinitialMaxData the actual congestion window value

3.- Are we missing something or making incorrect assumptions?

Thanks in advance.

Answered by palfonso in 794240022

Sure np.

After isolating the logic to only the Network framework implementation, I'm observing exactly what you described:

1.- CPU is steady in 34%

2.- Memory in under pressure, because I'm not waiting for the completion handler to send the next one (I can find a balance in this scenario, not big deal).

In my implementation I should have some specific logic that is causing the cpu to spike like that, so I'm glad is not a bug in the framework.

Really appreciate you help.

Thanks

How are you sending this data? Down a single stream? Creating a new stream for each message? Or something else?

Also:

server are client are on Swift using the Network framework.

Did you mean “server and client”?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

1.- How are you sending this data?

We have a pool of NWConnection objects which were initialized on the server using the group object, of course all of those connections are already in "ready" state. Then when a data buffer is ready/prepared on the server, we pull a connection from the pool, we divide the buffer in chunks (64K each) and we call the send method for every chunk, when the last chunk is sent we wait for the completion: .contentProcessed to be called (we wait only for that last chunk to be processed) and then we repeat the process again with another buffer and a different connection from the pool. As you can see, this happens pretty fast, there's not too much pacing that we are doing here, because we do not have an understanding of how many bytes can be in flight over the wire at a particular time which will probably dictate how much data we can send.

So short answer is, yes we are using an existing nwconnection for each message.

2.- Did you mean “server and client”?

Yes, that's what I meant.

Happy to clarify any other behavior...

Thanks.

we divide the buffer in chunks (64K each) and we call the send method for every chunk

Why do you chunk your data?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Our thought is we want to be respectful of the client's bandwidth, but you are probably asking because that's something that's being handled for us? For example, if our messages were 100MB (instead of 2.5 MB which is the case now) should we just send the entire 100MB buffer and Network framework will just take care of that? Interesting...

Then, our client's receive method will process whatever comes until we get the entire file?

func receive() {
connection.batch {
           connection.receive(minimumIncompleteLength: 1, maximumLength: Int.max) { [weak self] content, contentContext, isComplete, error in
               .....
               .....
               self.receive()
            }
        }
}

Questions:

  • Should we just send the entire buffer and let the receive method to get all the chunks?
  • If previous question is true, should we even care about implementing some kind of pacing mechanism in order to understand the amount of data we need to send to the client?
  • For feedback/back pressure sake, if we wanted to implement that, is there a way to query/get the number of bytes that can be sent over the wire in a particular time?

Thanks in advance.

you are probably asking because that's something that's being handled for us?

Right. QUIC implements flow control on both each individual stream and on the tunnel as a whole. In the NWConnection API that flow control is based on the completion handler you pass to to the send(…) API. So, I see two sensible paths forward:

  • If you want to reduce the amount of memory consumed in the sender, issue a send and then, when it completes, issue the next send.

  • If not, just issue one big send.

Right now you’re adding extra complexity without actually reducing your memory usage )-:

Should we just send the entire buffer and let the receive method to get all the chunks?

This is a decision you make on the send side, based on how much memory you want to consume there. No matter what you do on the send side, your sends won’t overwhelm the receiver because of QUIC’s flow control.

For feedback/back pressure sake, if we wanted to implement that, is there a way to query/get the number of bytes that can be sent over the wire in a particular time?

No, because that’s not how flow control is expressed in NWConnection. Rather, it’s based on the completion handler, as explained above.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

What I'm doing right now is, since we are working with 2.5MB size buffers I send the entire buffer using only one call to the send method and when I get the completion for that I send the next buffer and I repeat that again and again.

The reason I do that is because, if I just call send sequentially for let's say 10 or 20 buffers WITHOUT waiting for the completion of each to get triggered, it doesn't seem to send data to the client and the CPU goes to 300-400%.

Are we missing something?

it doesn't seem to send data to the client and the CPU goes to 300-400%

Hmmm, that’s weird. I would expect this to have a memory impact — the more data you send without waiting for the send completion handler, the more data the network stack has to buffer — but I wouldn’t expect it to have such a catastrophic CPU impact. That seems like a bug to me.

I’d appreciate you filing a bug about it. Make sure to include a sysdiagnose from the sending machine while the CPU usage is so high. Please post your bug number, just for the record.

Also, you’re using Network framework at both ends, right? If so, I’d be great if you could attach a small test project that reproduces this.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

What you are describing makes total sense to me.

Now, before filing a bug let me give you a little more context:

1.- I'm using a macOS app as the QUIC server using Network framework (macOS Sonoma 14.5)

2.- Client app is on visionOS 2 Beta 2 and I also use Network framework.

macOS app is the one sending the streams to the client and showing the weird CPU usage.

Client app on visionOS seems stable in terms of CPU and memory. I'm using Xcode 16 Beta btw.

Thanks for the extra details.

None of that seems problematic in any way.

When you see the high CPU usage, does a sample show any of your code running? Or is it entirely within the OS?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Sure np.

After isolating the logic to only the Network framework implementation, I'm observing exactly what you described:

1.- CPU is steady in 34%

2.- Memory in under pressure, because I'm not waiting for the completion handler to send the next one (I can find a balance in this scenario, not big deal).

In my implementation I should have some specific logic that is causing the cpu to spike like that, so I'm glad is not a bug in the framework.

Really appreciate you help.

Thanks

QUIC Connection Group Server Sending Pace
 
 
Q