This is an automated email from the ASF dual-hosted git repository.

pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-grpc.git


The following commit(s) were added to refs/heads/main by this push:
     new 6c8dc2ba Encode response data frames through GrpcProtocolWriter with 
deferred writes (#909)
6c8dc2ba is described below

commit 6c8dc2baf9d9c689c92e8d907e8af9489134e856
Author: PJ Fanning <[email protected]>
AuthorDate: Tue Oct 20 17:19:26 2026 +0100

    Encode response data frames through GrpcProtocolWriter with deferred writes 
(#909)
    
    * Generalise unary response optimisation.
    
    Encapsulate optimisation of unary responses into a 
GrpcProtocol/GrpcProtocolWriter concern, allowing it to be applied to other 
variants.
    This removes the need for the frame encoder, as this is just a map with 
encodeFrame.
    
    * Keep the frame serializer types internal.
    
    Motivation:
    The deferred data frame change made ProtobufFrameSerializer a public
    @ApiMayChange trait, which would expose the internal DeferredDataWriter
    to anyone extending the public serializers.
    
    Modification:
    - Keep ProtobufFrameSerializer private[grpc], as it is on main.
    - Make DeferredDataWriter's methods, and the ScalaPB and Google
      serializers' implementations of them, private[grpc].
    - Add the MiMa filters for the Scala 3 positional accessors of
      GrpcProtocolReader and GrpcProtocolWriter, and rename the filter file
      after the change.
    - Remove debug println output from the serializer specs.
    
    Result:
    No new public API. Binary compatibility passes on Scala 2.13 and 3.
    
    Tests:
    - sbt runtime/test: 180 passed
    - sbt +runtime/mimaReportBinaryIssues: passed
    
    References:
    Refs #905
    
    * Add a benchmark for encoding response data frames.
    
    Motivation:
    The deferred data frame change affects how response messages are
    written into gRPC frames, but no benchmark covered gRPC-Web responses.
    
    Modification:
    Add FrameWriteBenchmark, which marshals unary and streamed responses
    for the native and gRPC-Web protocols without compression, with 16 B,
    1 KiB and 64 KiB bytes payloads.
    
    Result:
    Frame encoding can be compared with main.
    
    Tests:
    - sbt benchmarks/Jmh/compile: passed
    
    References:
    Refs #905
    
    * Fix FrameWriteBenchmark formatting and Scala 3.9 warnings.
    
    Replace the deprecated = _ initializers, which fail the next build under
    -Werror, and apply scalafmt.
    
    * Use the pekko import prefix for DeferredDataFrame imports.
    
    * Fix the frane parameter name typo in GrpcProtocolWeb.
    
    * Use OutboundFrame in GrpcProtocolWeb encodeFrame and encodeFrameToBytes, 
matching GrpcProtocolNative.
    
    ---------
    
    Co-authored-by: Tim Whittington <[email protected]>
    Co-authored-by: PJ Fanning <[email protected]>
---
 .../apache/pekko/grpc/FrameWriteBenchmark.scala    | 79 ++++++++++++++++++++++
 .../deferred-data-frame.excludes                   | 43 ++++++++++++
 .../scala/org/apache/pekko/grpc/GrpcProtocol.scala | 52 +++++++++++---
 .../org/apache/pekko/grpc/ProtobufSerializer.scala | 12 +++-
 .../pekko/grpc/internal/AbstractGrpcProtocol.scala | 37 ++++++----
 .../pekko/grpc/internal/GrpcEntityHelpers.scala    | 27 +++-----
 .../pekko/grpc/internal/GrpcProtocolNative.scala   | 15 ++--
 .../pekko/grpc/internal/GrpcProtocolWeb.scala      | 19 ++++--
 .../pekko/grpc/internal/GrpcResponseHelpers.scala  | 38 +----------
 .../grpc/javadsl/GoogleProtobufSerializer.scala    | 14 ++--
 .../grpc/scaladsl/ScalapbProtobufSerializer.scala  | 18 +++--
 .../javadsl/GoogleProtobufSerializerSpec.scala     | 14 ++++
 .../scaladsl/ScalapbProtobufSerializerSpec.scala   | 15 ++++
 13 files changed, 276 insertions(+), 107 deletions(-)

diff --git 
a/benchmarks/src/main/scala/org/apache/pekko/grpc/FrameWriteBenchmark.scala 
b/benchmarks/src/main/scala/org/apache/pekko/grpc/FrameWriteBenchmark.scala
new file mode 100644
index 00000000..e939f196
--- /dev/null
+++ b/benchmarks/src/main/scala/org/apache/pekko/grpc/FrameWriteBenchmark.scala
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pekko.grpc
+
+import scala.concurrent.Await
+import scala.concurrent.duration.Duration
+
+import org.apache.pekko
+import pekko.actor.ActorSystem
+import pekko.grpc.internal.{ GrpcProtocolNative, GrpcProtocolWeb, Identity }
+import pekko.grpc.scaladsl.{ GrpcMarshalling, ScalapbProtobufSerializer }
+import pekko.http.scaladsl.model.HttpEntity
+import pekko.stream.{ Materializer, SystemMaterializer }
+import pekko.stream.scaladsl.{ Sink, Source }
+import pekko.util.ByteString
+import com.google.protobuf.{ ByteString => ProtobufByteString }
+import com.google.protobuf.any.{ Any => ScalapbAny }
+import org.openjdk.jmh.annotations._
+import org.openjdk.jmh.infra.Blackhole
+
+// Encoding of response data frames, for the native and gRPC-Web protocols 
without compression.
+class FrameWriteBenchmark extends CommonBenchmark {
+  @Param(Array("native", "web"))
+  var protocol: String = "native"
+
+  @Param(Array("16", "1024", "65536"))
+  var size: Int = 0
+
+  implicit val system: ActorSystem = ActorSystem("bench")
+  implicit val mat: Materializer = SystemMaterializer(system).materializer
+  implicit val serializer: ScalapbProtobufSerializer[ScalapbAny] = new 
ScalapbProtobufSerializer(ScalapbAny)
+
+  implicit var writer: GrpcProtocol.GrpcProtocolWriter = 
GrpcProtocolNative.newWriter(Identity)
+  var message: ScalapbAny = ScalapbAny.defaultInstance
+
+  @Setup
+  def setup(): Unit = {
+    writer = protocol match {
+      case "native" => GrpcProtocolNative.newWriter(Identity)
+      case "web"    => GrpcProtocolWeb.newWriter(Identity)
+    }
+    message = ScalapbAny("benchmark", ProtobufByteString.copyFrom(new 
Array[Byte](size)))
+  }
+
+  @TearDown
+  def tearDown(): Unit = system.terminate()
+
+  @Benchmark
+  def unary(blackhole: Blackhole): Unit =
+    consume(GrpcMarshalling.marshal(message).entity, blackhole)
+
+  @Benchmark
+  @OperationsPerInvocation(100)
+  def stream(blackhole: Blackhole): Unit =
+    
consume(GrpcMarshalling.marshalStream(Source.repeat(message).take(100)).entity, 
blackhole)
+
+  private def consume(entity: HttpEntity, blackhole: Blackhole): Unit =
+    entity match {
+      case HttpEntity.Strict(_, data) => blackhole.consume(data)
+      case _                          =>
+        blackhole.consume(
+          Await.result(entity.dataBytes.runWith(Sink.fold(0)((n, b: 
ByteString) => n + b.length)), Duration.Inf))
+    }
+}
diff --git 
a/runtime/src/main/mima-filters/2.0.x.backwards.excludes/deferred-data-frame.excludes
 
b/runtime/src/main/mima-filters/2.0.x.backwards.excludes/deferred-data-frame.excludes
new file mode 100644
index 00000000..40a6dd58
--- /dev/null
+++ 
b/runtime/src/main/mima-filters/2.0.x.backwards.excludes/deferred-data-frame.excludes
@@ -0,0 +1,43 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# encode data frames through GrpcProtocolWriter, with deferred writes into the 
frame buffer
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolReader.frameDecoder")
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolReader.copy")
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolReader.copy$default$3")
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolReader.this")
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolReader.apply")
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolReader.unapply")
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolWriter.encodeFrame")
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolWriter.encodeDataToResponse")
+ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolWriter.frameEncoder")
+ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolWriter.copy")
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolWriter.copy$default$3")
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolWriter.copy$default$4")
+ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolWriter.copy$default$5")
+ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolWriter.this")
+ProblemFilters.exclude[MissingTypesProblem]("org.apache.pekko.grpc.GrpcProtocol$GrpcProtocolWriter$")
+ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolWriter.apply")
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolWriter.unapply")
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.internal.AbstractGrpcProtocol.writer")
+ProblemFilters.exclude[IncompatibleClassSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol$GrpcProtocolReader$")
+ProblemFilters.exclude[IncompatibleClassSignatureProblem]("org.apache.pekko.grpc.internal.AbstractGrpcProtocol$GrpcFramingDecoderStage")
+# Scala 3 positional accessors of the changed GrpcProtocolReader and 
GrpcProtocolWriter fields
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolReader._3")
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolWriter._3")
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolWriter._4")
+ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.grpc.GrpcProtocol#GrpcProtocolWriter._5")
diff --git a/runtime/src/main/scala/org/apache/pekko/grpc/GrpcProtocol.scala 
b/runtime/src/main/scala/org/apache/pekko/grpc/GrpcProtocol.scala
index 22c7823e..b3ea8b1b 100644
--- a/runtime/src/main/scala/org/apache/pekko/grpc/GrpcProtocol.scala
+++ b/runtime/src/main/scala/org/apache/pekko/grpc/GrpcProtocol.scala
@@ -18,6 +18,7 @@ import pekko.NotUsed
 import pekko.annotation.InternalApi
 import pekko.annotation.InternalStableApi
 import pekko.grpc.GrpcProtocol.{ GrpcProtocolReader, GrpcProtocolWriter }
+import pekko.grpc.GrpcProtocol.DeferredDataFrame.DeferredDataWriter
 import pekko.grpc.internal.{
   AbstractGrpcProtocol,
   Codec,
@@ -87,11 +88,48 @@ object GrpcProtocol {
   /** A frame in a logical gRPC protocol stream */
   sealed trait Frame
 
-  /** A data (or message) frame in a gRPC protocol stream. */
-  case class DataFrame(data: ByteString) extends Frame
+  /** An outbound data frame to be encoded in a gRPC protocol stream */
+  sealed trait OutboundFrame extends Frame
+
+  /** A frame received from a peer system. */
+  sealed trait InboundFrame extends Frame
+
+  /** A data (or message) frame in a gRPC protocol stream */
+  case class DataFrame(data: ByteString) extends InboundFrame with 
OutboundFrame
 
   /** A trailer (status headers) frame in a gRPC protocol stream */
-  case class TrailerFrame(trailers: List[HttpHeader]) extends Frame
+  case class TrailerFrame(trailers: List[HttpHeader]) extends InboundFrame 
with OutboundFrame
+
+  /** An outbound write of a data frame, deferred to allow optimized write 
into a pre-allocated buffer */
+  case class DeferredDataFrame[T](element: T, writer: DeferredDataWriter[T]) 
extends OutboundFrame {
+    type Element = T
+
+    /** Fallback to DataFrame style write, when optimized framed write is not 
possible. */
+    def data: ByteString = {
+      val data = new Array[Byte](writer.serializedSize(element))
+      writer.serializeTo(element, data, 0)
+      ByteString.fromArrayUnsafe(data)
+    }
+
+  }
+
+  object DeferredDataFrame {
+    trait DeferredDataWriter[T] {
+
+      /**
+       * Compute the size of the serialized form of the given element.
+       */
+      private[grpc] def serializedSize(t: T): Int
+
+      /**
+       * Serialize the given element into the given frame, starting at the 
given offset.
+       * @param t the element to serialize.
+       * @param frame a preallocated frame buffer, which will be at least of 
size offset + serializedSize(t)
+       * @param offset the offset to place the serialized data of the element 
at.
+       */
+      private[grpc] def serializeTo(t: T, frame: Array[Byte], offset: Int): 
Unit
+    }
+  }
 
   /**
    * Implements the encoding of a stream of gRPC Frames into a 
physical/transport layer.
@@ -104,11 +142,9 @@ object GrpcProtocol {
       /** The compression codec to be used for data frame bodies */
       messageEncoding: Codec,
       /** Encodes a frame as a part in a chunk stream. */
-      encodeFrame: Frame => ChunkStreamPart,
+      encodeFrame: OutboundFrame => ChunkStreamPart,
       /** A shortcut to encode a data frame directly into a Response */
-      encodeDataToResponse: (ByteString, Seq[HttpHeader], Trailer) => 
HttpResponse,
-      /** A Flow over a stream of Frame using this frame encoding */
-      frameEncoder: Flow[Frame, ChunkStreamPart, NotUsed])
+      encodeDataToResponse: (OutboundFrame, Seq[HttpHeader], Trailer) => 
HttpResponse)
 
   /**
    * Implements the decoding of the gRPC framing from a physical/transport 
layer.
@@ -118,7 +154,7 @@ object GrpcProtocol {
       messageEncoding: Codec,
       decodeSingleFrame: ByteString => ByteString,
       /** A Flow of Frames over a stream of messages encoded in gRPC framing. 
*/
-      frameDecoder: Flow[ByteString, Frame, NotUsed]) {
+      frameDecoder: Flow[ByteString, InboundFrame, NotUsed]) {
 
     /**
      * A Flow of Frames over a stream of messages encoded in gRPC framing that 
only
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/ProtobufSerializer.scala 
b/runtime/src/main/scala/org/apache/pekko/grpc/ProtobufSerializer.scala
index e5392960..688b698d 100644
--- a/runtime/src/main/scala/org/apache/pekko/grpc/ProtobufSerializer.scala
+++ b/runtime/src/main/scala/org/apache/pekko/grpc/ProtobufSerializer.scala
@@ -14,6 +14,7 @@
 package org.apache.pekko.grpc
 
 import org.apache.pekko
+import pekko.grpc.GrpcProtocol.DeferredDataFrame
 import pekko.grpc.internal.ByteStringUtils
 import pekko.util.ByteString
 
@@ -21,10 +22,15 @@ import java.io.InputStream
 
 trait ProtobufSerializer[T] {
   def serialize(t: T): ByteString
+
   def deserialize(bytes: ByteString): T
+
   def deserialize(stream: InputStream): T = 
deserialize(ByteStringUtils.fromInputStream(stream))
 }
 
-private[grpc] trait ProtobufFrameSerializer[T] extends ProtobufSerializer[T] {
-  private[grpc] def serializeDataFrame(t: T): ByteString
-}
+/**
+ * Optional optimisation of ProtobufSerializer, which allows for more 
efficient serialization into a frame where the
+ * serialized size of the encoded element can be determined in advance.
+ */
+private[grpc] trait ProtobufFrameSerializer[T] extends ProtobufSerializer[T]
+    with DeferredDataFrame.DeferredDataWriter[T]
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/AbstractGrpcProtocol.scala
 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/AbstractGrpcProtocol.scala
index 7aa57f88..0b79158b 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/AbstractGrpcProtocol.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/AbstractGrpcProtocol.scala
@@ -15,7 +15,8 @@ package org.apache.pekko.grpc.internal
 import org.apache.pekko
 import pekko.NotUsed
 import pekko.grpc.GrpcProtocol
-import pekko.grpc.GrpcProtocol.{ Frame, GrpcProtocolReader, GrpcProtocolWriter 
}
+import pekko.grpc.GrpcProtocol.{ GrpcProtocolReader, GrpcProtocolWriter, 
InboundFrame, OutboundFrame }
+import pekko.grpc.GrpcProtocol.DeferredDataFrame.DeferredDataWriter
 import pekko.http.javadsl.{ model => jmodel }
 import pekko.http.scaladsl.model.HttpEntity.ChunkStreamPart
 import pekko.http.scaladsl.model.{ ContentType, HttpHeader, HttpResponse, 
MediaType, Trailer }
@@ -113,6 +114,15 @@ object AbstractGrpcProtocol {
       })
       .toContentType
 
+  def encodeFrameData[T](element: T, elementDataWriter: DeferredDataWriter[T], 
isCompressed: Boolean,
+      isTrailer: Boolean): ByteString = {
+    val dataLength = elementDataWriter.serializedSize(element)
+    val frame = new Array[Byte](AbstractGrpcProtocol.FrameHeaderSize + 
dataLength)
+    writeFrameHeader(frame, 0, dataLength, isCompressed, isTrailer)
+    elementDataWriter.serializeTo(element, frame, 
AbstractGrpcProtocol.FrameHeaderSize)
+    ByteString.fromArrayUnsafe(frame)
+  }
+
   def encodeFrameData(data: ByteString, isCompressed: Boolean, isTrailer: 
Boolean): ByteString = {
     val header = new Array[Byte](FrameHeaderSize)
     writeFrameHeader(header, 0, data.length, isCompressed, isTrailer)
@@ -122,14 +132,13 @@ object AbstractGrpcProtocol {
   def writer(
       protocol: GrpcProtocol,
       codec: Codec,
-      encodeFrame: Frame => ChunkStreamPart,
-      encodeDataToResponse: (ByteString, Seq[HttpHeader], Trailer) => 
HttpResponse): GrpcProtocolWriter =
+      encodeFrame: OutboundFrame => ChunkStreamPart,
+      encodeDataToResponse: (OutboundFrame, Seq[HttpHeader], Trailer) => 
HttpResponse): GrpcProtocolWriter =
     GrpcProtocolWriter(
       adjustCompressibility(protocol.contentType, codec),
       codec,
       encodeFrame,
-      encodeDataToResponse,
-      Flow[Frame].map(encodeFrame))
+      encodeDataToResponse)
 
   /**
    * The default maximum inbound message size (4 MiB), matching grpc-java's 
default.
@@ -138,12 +147,12 @@ object AbstractGrpcProtocol {
 
   def reader(
       codec: Codec,
-      decodeFrame: (Int, ByteString) => Frame,
+      decodeFrame: (Int, ByteString) => InboundFrame,
       preDecodeStrict: ByteString => ByteString = null,
       preDecodeFlow: Flow[ByteString, ByteString, NotUsed] = null,
       maxInboundMessageSize: Int = DefaultMaxInboundMessageSize): 
GrpcProtocolReader = {
     val strictAdapter: ByteString => ByteString = if (preDecodeStrict eq null) 
identity else preDecodeStrict
-    val adapter: Flow[ByteString, Frame, NotUsed] => Flow[ByteString, Frame, 
NotUsed] =
+    val adapter: Flow[ByteString, InboundFrame, NotUsed] => Flow[ByteString, 
InboundFrame, NotUsed] =
       if (preDecodeFlow eq null) identity
       else x => Flow[ByteString].via(preDecodeFlow).via(x)
 
@@ -171,23 +180,23 @@ object AbstractGrpcProtocol {
       adapter(Flow.fromGraph(new GrpcFramingDecoderStage(codec, decodeFrame, 
maxInboundMessageSize))))
   }
 
-  class GrpcFramingDecoderStage(codec: Codec, deframe: (Int, ByteString) => 
Frame, maxInboundMessageSize: Int)
-      extends ByteStringParser[Frame] {
+  class GrpcFramingDecoderStage(codec: Codec, deframe: (Int, ByteString) => 
InboundFrame, maxInboundMessageSize: Int)
+      extends ByteStringParser[InboundFrame] {
     override def createLogic(inheritedAttributes: Attributes): GraphStageLogic 
=
       new ParsingLogic {
         startWith(ReadFrameHeader)
 
-        trait Step extends ParseStep[Frame]
+        trait Step extends ParseStep[InboundFrame]
 
         // handle explicitly to avoid noisy log: a peer sending oversized or 
malformed frame
         // headers should not cost us a stack trace per attempt
-        private def failWith(status: Status): ParseResult[Frame] = {
+        private def failWith(status: Status): ParseResult[InboundFrame] = {
           failStage(new StatusException(status))
           ParseResult(None, Failed)
         }
 
         object ReadFrameHeader extends Step {
-          override def parse(reader: ByteReader): ParseResult[Frame] = {
+          override def parse(reader: ByteReader): ParseResult[InboundFrame] = {
             val frameType = reader.readByte()
             val length = reader.readIntBE()
 
@@ -205,7 +214,7 @@ object AbstractGrpcProtocol {
         sealed case class ReadFrame(frameType: Int, length: Int) extends Step {
           private val compression = (frameType & 0x01) == 1
 
-          override def parse(reader: ByteReader): ParseResult[Frame] =
+          override def parse(reader: ByteReader): ParseResult[InboundFrame] =
             try ParseResult(
                 Some(deframe(frameType, codec.uncompress(compression, 
reader.take(length), maxInboundMessageSize))),
                 ReadFrameHeader)
@@ -217,7 +226,7 @@ object AbstractGrpcProtocol {
         }
 
         case object Failed extends Step {
-          override def parse(reader: ByteReader): ParseResult[Frame] = 
ParseResult(None, Failed)
+          override def parse(reader: ByteReader): ParseResult[InboundFrame] = 
ParseResult(None, Failed)
         }
       }
   }
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcEntityHelpers.scala 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcEntityHelpers.scala
index d39f0364..d3aa213a 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcEntityHelpers.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcEntityHelpers.scala
@@ -18,9 +18,9 @@ import pekko.NotUsed
 import pekko.actor.{ ActorSystem, ClassicActorSystemProvider }
 import pekko.annotation.InternalApi
 import pekko.grpc.{ GrpcServiceException, ProtobufFrameSerializer, 
ProtobufSerializer, Trailers }
-import pekko.grpc.GrpcProtocol.{ DataFrame, Frame, GrpcProtocolWriter, 
TrailerFrame }
+import pekko.grpc.GrpcProtocol.{ DataFrame, DeferredDataFrame, 
GrpcProtocolWriter, OutboundFrame, TrailerFrame }
 import pekko.grpc.scaladsl.{ headers, BytesEntry, Metadata, MetadataEntry, 
StringEntry }
-import pekko.http.scaladsl.model.HttpEntity.{ Chunk, ChunkStreamPart }
+import pekko.http.scaladsl.model.HttpEntity.ChunkStreamPart
 import pekko.http.scaladsl.model.HttpHeader
 import pekko.http.scaladsl.model.headers.RawHeader
 import pekko.stream.scaladsl.Source
@@ -65,24 +65,17 @@ object GrpcEntityHelpers {
       FlowShape(merge.in(0), merge.out)
     }
 
-  private def chunks[T](e: Source[T, NotUsed], trail: Source[Frame, NotUsed])(
+  private def chunks[T](e: Source[T, NotUsed], trail: Source[OutboundFrame, 
NotUsed])(
       implicit m: ProtobufSerializer[T],
       writer: GrpcProtocolWriter): Source[ChunkStreamPart, NotUsed] =
-    if ((writer.messageEncoding eq Identity) && writer.contentType == 
GrpcProtocolNative.contentType) {
-      m match {
-        case frameSerializer: ProtobufFrameSerializer[T @unchecked] =>
-          e.map { msg => Chunk(frameSerializer.serializeDataFrame(msg)): 
ChunkStreamPart }
-            .via(concatCheap(trail.map(writer.encodeFrame)))
-        case _ => frameEncodedChunks(e, trail)
-      }
-    } else {
-      frameEncodedChunks(e, trail)
-    }
+    e.map(msg => 
writer.encodeFrame(outboundDataFrame(msg))).via(concatCheap(trail.map(writer.encodeFrame)))
 
-  private def frameEncodedChunks[T](e: Source[T, NotUsed], trail: 
Source[Frame, NotUsed])(
-      implicit m: ProtobufSerializer[T],
-      writer: GrpcProtocolWriter): Source[ChunkStreamPart, NotUsed] =
-    e.map { msg => DataFrame(m.serialize(msg)) 
}.via(concatCheap(trail)).via(writer.frameEncoder)
+  def outboundDataFrame[T](t: T)(implicit m: ProtobufSerializer[T]): 
OutboundFrame = {
+    m match {
+      case fm: ProtobufFrameSerializer[T] => DeferredDataFrame(t, fm)
+      case _                              => DataFrame(m.serialize(t))
+    }
+  }
 
   def trailer(status: Status): TrailerFrame =
     TrailerFrame(trailers = statusHeaders(status))
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcProtocolNative.scala
 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcProtocolNative.scala
index 97d3f16f..1ad3166f 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcProtocolNative.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcProtocolNative.scala
@@ -49,21 +49,26 @@ object GrpcProtocolNative extends 
AbstractGrpcProtocol("grpc") {
   private def decodeFrame(@nowarn("msg=is never used") frameType: Int, data: 
ByteString) = DataFrame(data)
 
   @inline
-  private def encodeFrame(codec: Codec, frame: Frame): ChunkStreamPart =
+  private def encodeFrame(codec: Codec, frame: OutboundFrame): ChunkStreamPart 
=
     frame match {
+      case ef @ DeferredDataFrame(element, dataWriter) =>
+        if (codec eq Identity)
+          Chunk(AbstractGrpcProtocol.encodeFrameData(element, dataWriter, 
codec.isCompressed, isTrailer = false))
+        else
+          Chunk(AbstractGrpcProtocol.encodeFrameData(codec.compress(ef.data), 
codec.isCompressed, isTrailer = false))
       case DataFrame(data) =>
         Chunk(AbstractGrpcProtocol.encodeFrameData(codec.compress(data), 
codec.isCompressed, isTrailer = false))
       case TrailerFrame(headers) => LastChunk(trailer = headers)
     }
+
+  @inline
   private def encodeDataToResponse(
-      codec: Codec)(data: ByteString, headers: Seq[HttpHeader], trailer: 
Trailer): HttpResponse =
+      codec: Codec)(frame: OutboundFrame, headers: Seq[HttpHeader], trailer: 
Trailer): HttpResponse =
     new HttpResponse(
       status = StatusCodes.OK,
       headers = headers,
-      entity = HttpEntity(contentType, encodeDataToFrameBytes(codec, data)),
+      entity = HttpEntity(contentType, encodeFrame(codec, frame).data),
       protocol = HttpProtocols.`HTTP/1.1`,
       attributes = Map.empty[AttributeKey[?], 
Any].updated(AttributeKeys.trailer, trailer))
 
-  private def encodeDataToFrameBytes(codec: Codec, data: ByteString): 
ByteString =
-    AbstractGrpcProtocol.encodeFrameData(codec.compress(data), 
codec.isCompressed, isTrailer = false)
 }
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcProtocolWeb.scala 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcProtocolWeb.scala
index a0f0a758..64d29d1a 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcProtocolWeb.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcProtocolWeb.scala
@@ -34,26 +34,31 @@ abstract class GrpcProtocolWebBase(subType: String) extends 
AbstractGrpcProtocol
   override protected def reader(codec: Codec, maxInboundMessageSize: Int): 
GrpcProtocolReader =
     AbstractGrpcProtocol.reader(codec, decodeFrame, preDecodeStrict, 
preDecodeFlow, maxInboundMessageSize)
 
-  private def encodeFrame(codec: Codec, frame: Frame): ChunkStreamPart =
+  private def encodeFrame(codec: Codec, frame: OutboundFrame): ChunkStreamPart 
=
     Chunk(postEncode(encodeFrameToBytes(codec, frame)))
 
   private def encodeDataToResponse(
-      codec: Codec)(data: ByteString, headers: Seq[HttpHeader], trailer: 
Trailer): HttpResponse =
+      codec: Codec)(frame: OutboundFrame, headers: Seq[HttpHeader], trailer: 
Trailer): HttpResponse =
     HttpResponse(
       status = StatusCodes.OK,
       headers = headers,
-      entity = HttpEntity(contentType, encodeDataToFrameBytes(codec, data, 
trailer)),
+      entity = HttpEntity(contentType, encodeDataToFrameBytes(codec, frame, 
trailer)),
       protocol = HttpProtocols.`HTTP/1.1`)
 
-  private def encodeDataToFrameBytes(codec: Codec, data: ByteString, trailer: 
Trailer): ByteString = {
+  private def encodeDataToFrameBytes(codec: Codec, frame: OutboundFrame, 
trailer: Trailer): ByteString = {
     val trailerData = encodeTrailerHeaders(trailer.headers.iterator)
     val trailerFrame =
       AbstractGrpcProtocol.encodeFrameData(codec.compress(trailerData), 
codec.isCompressed, isTrailer = true)
-    postEncode(encodeFrameToBytes(codec, DataFrame(data)) ++ trailerFrame)
+    postEncode(encodeFrameToBytes(codec, frame) ++ trailerFrame)
   }
 
-  private def encodeFrameToBytes(codec: Codec, frame: Frame): ByteString =
+  private def encodeFrameToBytes(codec: Codec, frame: OutboundFrame): 
ByteString =
     frame match {
+      case ef @ DeferredDataFrame(element, dataWriter) =>
+        if (codec eq Identity)
+          AbstractGrpcProtocol.encodeFrameData(element, dataWriter, 
codec.isCompressed, isTrailer = false)
+        else
+          AbstractGrpcProtocol.encodeFrameData(codec.compress(ef.data), 
codec.isCompressed, isTrailer = false)
       case DataFrame(data) =>
         AbstractGrpcProtocol.encodeFrameData(codec.compress(data), 
codec.isCompressed, isTrailer = false)
       case TrailerFrame(trailer) =>
@@ -63,7 +68,7 @@ abstract class GrpcProtocolWebBase(subType: String) extends 
AbstractGrpcProtocol
           isTrailer = true)
     }
 
-  private final def decodeFrame(frameHeader: Int, data: ByteString): Frame = {
+  private final def decodeFrame(frameHeader: Int, data: ByteString): 
InboundFrame = {
     (frameHeader & 0x80) match {
       case 0    => DataFrame(data)
       case 0x80 => TrailerFrame(decodeTrailer(data))
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcResponseHelpers.scala
 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcResponseHelpers.scala
index 0c5bfd4e..9da29529 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcResponseHelpers.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/internal/GrpcResponseHelpers.scala
@@ -19,22 +19,12 @@ import pekko.actor.{ ActorSystem, 
ClassicActorSystemProvider }
 import pekko.annotation.InternalApi
 import pekko.grpc.GrpcProtocol.{ GrpcProtocolWriter, TrailerFrame }
 import pekko.grpc.scaladsl.{ headers, GrpcExceptionHandler }
-import pekko.grpc.{ ProtobufFrameSerializer, ProtobufSerializer, Trailers }
+import pekko.grpc.{ ProtobufSerializer, Trailers }
 import pekko.http.scaladsl.model.headers.{ Trailer => TrailerHeader }
 import pekko.http.scaladsl.model.HttpEntity.ChunkStreamPart
-import pekko.http.scaladsl.model.{
-  AttributeKey,
-  AttributeKeys,
-  HttpEntity,
-  HttpHeader,
-  HttpProtocols,
-  HttpResponse,
-  StatusCodes,
-  Trailer
-}
+import pekko.http.scaladsl.model.{ HttpEntity, HttpHeader, HttpResponse, 
Trailer }
 import pekko.stream.Materializer
 import pekko.stream.scaladsl.Source
-import pekko.util.ByteString
 import io.grpc.Status
 
 import scala.concurrent.{ ExecutionContext, Future }
@@ -49,8 +39,6 @@ import scala.util.control.NonFatal
 object GrpcResponseHelpers {
   private val TrailerOk = GrpcEntityHelpers.trailer(Status.OK)
   private val TrailerOkAttribute = Trailer(TrailerOk.trailers)
-  private val TrailerOkAttributes =
-    Map.empty[AttributeKey[?], Any].updated(AttributeKeys.trailer, 
TrailerOkAttribute)
   private val IdentityResponseHeaders: Seq[HttpHeader] =
     headers.`Message-Encoding`(Identity.name) :: Nil
 
@@ -72,16 +60,7 @@ object GrpcResponseHelpers {
       system: ClassicActorSystemProvider): HttpResponse = {
     val responseHeaders = responseHeadersFor(writer)
     try {
-      if ((writer.messageEncoding eq Identity) && writer.contentType == 
GrpcProtocolNative.contentType) {
-        m match {
-          case frameSerializer: ProtobufFrameSerializer[T @unchecked] =>
-            nativeResponse(writer, frameSerializer.serializeDataFrame(e), 
responseHeaders)
-          case _ =>
-            writer.encodeDataToResponse(m.serialize(e), responseHeaders, 
TrailerOkAttribute)
-        }
-      } else {
-        writer.encodeDataToResponse(m.serialize(e), responseHeaders, 
TrailerOkAttribute)
-      }
+      writer.encodeDataToResponse(GrpcEntityHelpers.outboundDataFrame(e), 
responseHeaders, TrailerOkAttribute)
     } catch {
       case NonFatal(ex) => status(GrpcEntityHelpers.handleException(ex, 
eHandler))
     }
@@ -91,17 +70,6 @@ object GrpcResponseHelpers {
     if (writer.messageEncoding eq Identity) IdentityResponseHeaders
     else headers.`Message-Encoding`(writer.messageEncoding.name) :: Nil
 
-  private def nativeResponse(
-      writer: GrpcProtocolWriter,
-      encodedData: ByteString,
-      responseHeaders: Seq[HttpHeader]): HttpResponse =
-    new HttpResponse(
-      status = StatusCodes.OK,
-      headers = responseHeaders,
-      entity = HttpEntity(writer.contentType, encodedData),
-      protocol = HttpProtocols.`HTTP/1.1`,
-      attributes = TrailerOkAttributes)
-
   def apply[T](e: Source[T, NotUsed], status: Future[Status])(
       implicit m: ProtobufSerializer[T],
       mat: Materializer,
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/javadsl/GoogleProtobufSerializer.scala
 
b/runtime/src/main/scala/org/apache/pekko/grpc/javadsl/GoogleProtobufSerializer.scala
index c71b6ed1..60b46b90 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/javadsl/GoogleProtobufSerializer.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/javadsl/GoogleProtobufSerializer.scala
@@ -16,7 +16,6 @@ package org.apache.pekko.grpc.javadsl
 import org.apache.pekko
 import pekko.annotation.ApiMayChange
 import pekko.grpc.ProtobufFrameSerializer
-import pekko.grpc.internal.AbstractGrpcProtocol
 import pekko.util.ByteString
 import com.google.protobuf.CodedOutputStream
 import com.google.protobuf.Parser
@@ -28,17 +27,16 @@ class GoogleProtobufSerializer[T <: 
com.google.protobuf.Message](parser: Parser[
 
   override def serialize(t: T): ByteString =
     ByteString.fromArrayUnsafe(t.toByteArray)
-  override private[grpc] def serializeDataFrame(t: T): ByteString = {
-    val dataLength = t.getSerializedSize
-    val frame = new Array[Byte](AbstractGrpcProtocol.FrameHeaderSize + 
dataLength)
-    AbstractGrpcProtocol.writeFrameHeader(frame, 0, dataLength, isCompressed = 
false, isTrailer = false)
 
-    val output = CodedOutputStream.newInstance(frame, 
AbstractGrpcProtocol.FrameHeaderSize, dataLength)
+  override private[grpc] def serializedSize(t: T): Int = t.getSerializedSize
+
+  override private[grpc] def serializeTo(t: T, frame: Array[Byte], offset: 
Int): Unit = {
+    val dataLength = t.getSerializedSize
+    val output = CodedOutputStream.newInstance(frame, offset, dataLength)
     t.writeTo(output)
     output.checkNoSpaceLeft()
-
-    ByteString.fromArrayUnsafe(frame)
   }
+
   override def deserialize(bytes: ByteString): T = {
     val inputStream = bytes.asInputStream
     try parser.parseFrom(inputStream)
diff --git 
a/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/ScalapbProtobufSerializer.scala
 
b/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/ScalapbProtobufSerializer.scala
index a9884852..142f1ca8 100644
--- 
a/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/ScalapbProtobufSerializer.scala
+++ 
b/runtime/src/main/scala/org/apache/pekko/grpc/scaladsl/ScalapbProtobufSerializer.scala
@@ -16,7 +16,6 @@ package org.apache.pekko.grpc.scaladsl
 import org.apache.pekko
 import pekko.annotation.ApiMayChange
 import pekko.grpc.ProtobufFrameSerializer
-import pekko.grpc.internal.AbstractGrpcProtocol
 import pekko.util.ByteString
 import com.google.protobuf.CodedOutputStream
 import com.google.protobuf.CodedInputStream
@@ -25,21 +24,20 @@ import scalapb.{ GeneratedMessage, 
GeneratedMessageCompanion }
 import java.io.InputStream
 
 @ApiMayChange
-class ScalapbProtobufSerializer[T <: GeneratedMessage](companion: 
GeneratedMessageCompanion[T])
-    extends ProtobufFrameSerializer[T] {
+class ScalapbProtobufSerializer[T <: GeneratedMessage](
+    companion: GeneratedMessageCompanion[T]) extends 
ProtobufFrameSerializer[T] {
   override def serialize(t: T): ByteString =
     ByteString.fromArrayUnsafe(t.toByteArray)
-  override private[grpc] def serializeDataFrame(t: T): ByteString = {
-    val dataLength = t.serializedSize
-    val frame = new Array[Byte](AbstractGrpcProtocol.FrameHeaderSize + 
dataLength)
-    AbstractGrpcProtocol.writeFrameHeader(frame, 0, dataLength, isCompressed = 
false, isTrailer = false)
 
-    val output = CodedOutputStream.newInstance(frame, 
AbstractGrpcProtocol.FrameHeaderSize, dataLength)
+  override private[grpc] def serializedSize(t: T): Int = t.serializedSize
+
+  override private[grpc] def serializeTo(t: T, frame: Array[Byte], offset: 
Int): Unit = {
+    val dataLength = t.serializedSize
+    val output = CodedOutputStream.newInstance(frame, offset, dataLength)
     t.writeTo(output)
     output.checkNoSpaceLeft()
-
-    ByteString.fromArrayUnsafe(frame)
   }
+
   override def deserialize(bytes: ByteString): T =
     companion.parseFrom(CodedInputStream.newInstance(bytes.asByteBuffer))
   override def deserialize(data: InputStream): T =
diff --git 
a/runtime/src/test/scala/org/apache/pekko/grpc/javadsl/GoogleProtobufSerializerSpec.scala
 
b/runtime/src/test/scala/org/apache/pekko/grpc/javadsl/GoogleProtobufSerializerSpec.scala
index 212c0824..f5c9386b 100644
--- 
a/runtime/src/test/scala/org/apache/pekko/grpc/javadsl/GoogleProtobufSerializerSpec.scala
+++ 
b/runtime/src/test/scala/org/apache/pekko/grpc/javadsl/GoogleProtobufSerializerSpec.scala
@@ -27,5 +27,19 @@ class GoogleProtobufSerializerSpec extends AnyWordSpec with 
Matchers {
       val deserialized = anySerializer.deserialize(serialized)
       deserialized should be(obj)
     }
+
+    "serialize into buffer at offset" in {
+      val anySerializer = new GoogleProtobufSerializer(ProtobufAny.parser())
+
+      val obj = 
ProtobufAny.newBuilder().setTypeUrl("asdf").setValue(ByteString.copyFromUtf8("ASDF")).build()
+      val serialized = anySerializer.serialize(obj).toByteBuffer.array()
+
+      val prefixLength = 27
+      val frame = new Array[Byte](serialized.length + prefixLength)
+      anySerializer.serializeTo(obj, frame, prefixLength)
+
+      (0 until prefixLength).foreach(frame(_) shouldBe 0)
+      (prefixLength until frame.length).foreach(i => frame(i) shouldBe 
serialized(i - prefixLength))
+    }
   }
 }
diff --git 
a/runtime/src/test/scala/org/apache/pekko/grpc/scaladsl/ScalapbProtobufSerializerSpec.scala
 
b/runtime/src/test/scala/org/apache/pekko/grpc/scaladsl/ScalapbProtobufSerializerSpec.scala
index 95150340..acf4c74b 100644
--- 
a/runtime/src/test/scala/org/apache/pekko/grpc/scaladsl/ScalapbProtobufSerializerSpec.scala
+++ 
b/runtime/src/test/scala/org/apache/pekko/grpc/scaladsl/ScalapbProtobufSerializerSpec.scala
@@ -28,5 +28,20 @@ class ScalapbProtobufSerializerSpec extends AnyWordSpec with 
Matchers {
       val deserialized = anySerializer.deserialize(serialized)
       deserialized should be(obj)
     }
+
+    "serialize into buffer at offset" in {
+      val anySerializer = new ScalapbProtobufSerializer(ScalapbAny)
+
+      val obj = ScalapbAny("asdf", ByteString.copyFromUtf8("ASDF"))
+      val serialized = anySerializer.serialize(obj).toByteBuffer.array()
+
+      val prefixLength = 27
+      val frame = new Array[Byte](serialized.length + prefixLength)
+      anySerializer.serializeTo(obj, frame, prefixLength)
+
+      (0 until prefixLength).foreach(frame(_) shouldBe 0)
+      (prefixLength until frame.length).foreach(i => frame(i) shouldBe 
serialized(i - prefixLength))
+    }
+
   }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to