Skip to content

Best way to get bytes sent / bytes downloaded from gRPC #2216

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

Open
liuliu opened this issue Apr 3, 2025 · 3 comments
Open

Best way to get bytes sent / bytes downloaded from gRPC #2216

liuliu opened this issue Apr 3, 2025 · 3 comments
Labels
status/triage Collecting information required to triage the issue.

Comments

@liuliu
Copy link

liuliu commented Apr 3, 2025

Is your feature request related to a problem? Please describe it.

My use case for gPRC is on client - server connection. As such, there are cases client is on unstable connection and customers would benefit from getting better understanding of how long they need to wait for a certain call to complete. As such, we would like to provide feedback on the progress bar for how many bytes sent / how many bytes downloaded on per-frame level. This currently is not possible.

Describe the solution you'd like

There are several ways to implement this. It seems require a bit more thinking. On macOS / iOS platform in particular, it seem implementing custom NWFramerImplementation and insert it into NWParameters would be the most straightforward, but that requires change to SwiftNIOTransportService, a library upstream to grpc-swift.

I also explored some other ways, but it all pointing to changes required in SwiftNIO upstream library.

Describe alternatives you've considered

Alternatively, we can use interceptor, but that is inaccurate and won't reflect how much bytes sent / how much bytes downloaded while a call is still in progress.

Another alternative is to initialize bidirectional streaming call, and support chunking there. But that requires some complications on our gRPC schema itself.

@glbrntt
Copy link
Collaborator

glbrntt commented Apr 7, 2025

Do you want to trace the bytes sent/received across a whole connection or per RPC? And are you using v1 or v2?

@glbrntt glbrntt added the status/triage Collecting information required to triage the issue. label Apr 7, 2025
@liuliu
Copy link
Author

liuliu commented Apr 7, 2025

Do you want to trace the bytes sent/received across a whole connection or per RPC? And are you using v1 or v2?

Whole connection is OK given that it would be difficult to split and on client, we only have 1 or 2 RPC call at the same time (likely 1). Currently on v1 but can upgrade to v2 if calls for it.

@glbrntt
Copy link
Collaborator

glbrntt commented Apr 8, 2025

Okay great, we can work with that. I think the best option here is to create a NIO ChannelHandler which records bytes sent and received. We can install it between the TLS handler and the HTTP/2 handler using the debugChannelInitializer on the client.

Here's a rough sketch of how it could look:

final class ByteRecordingHandler: ChannelDuplexHandler {
  typealias InboundIn = ByteBuffer
  typealias InboundOut = ByteBuffer
  typealias OutboundIn = ByteBuffer
  typealias OutboundOut = ByteBuffer

  func channelRead(context: ChannelHandlerContext, data: NIOAny) {
    // Unwrap the NIOAny to get a ByteBuffer
    let buffer = Self.unwrapInboundIn(data)
    let byteCount = buffer.readableBytes

    // ... record bytes received
     
    // Forward the bytes we read
    context.fireChannelRead(data)
  }

  func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
    // Unwrap the NIOAny to get a ByteBuffer
    let buffer = Self.unwrapOutboundIn(data)
    let byteCount = buffer.readableBytes
    
    // ... record bytes sent

    // Forward the bytes
    context.write(data, promise: promise)
  }
}

Then you'll need to plug this into your client:

let channel = try GRPCChannelPool.with(
  target: .host("localhost", port: 1234),
  transportSecurity: .plaintext,
  eventLoopGroup: group
) {
  $0.debugChannelInitializer = { channel in
    channel.eventLoop.makeCompletedFuture {
      let sync = channel.pipeline.syncOperations
      let http2Handler = try sync.handler(type: NIOHTTP2Handler.self)
      // Note: this closure is called for every new connection, so you should
      // emit any events to a shared `Sendable` object held by the `ByteRecordingHandler`.
      // That object will be the bridge between the connection and your application.
      let recorder = ByteRecordingHandler(...)
      try sync.addHandler(recorder, position: .before(http2Handler))
    }
  }
}

Hopefully this'll set you off on the right path, let me know if you need more help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status/triage Collecting information required to triage the issue.
Projects
None yet
Development

No branches or pull requests

2 participants