<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Software-Architecture on kmcd.dev</title><link>https://kmcd.dev/tags/software-architecture/</link><description>Recent content in Software-Architecture on kmcd.dev</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>All Rights Reserved</copyright><lastBuildDate>Tue, 16 Jun 2026 10:00:00 +0000</lastBuildDate><atom:link href="https://kmcd.dev/tags/software-architecture/index.xml" rel="self" type="application/rss+xml"/><item><title>The Hidden Cost of google.protobuf.Value</title><link>https://kmcd.dev/posts/hidden-cost-of-google-protobuf-value/</link><pubDate>Tue, 16 Jun 2026 10:00:00 +0000</pubDate><guid>https://kmcd.dev/posts/hidden-cost-of-google-protobuf-value/</guid><description> 
                &lt;p> &lt;img hspace="5" src="https://kmcd.dev/posts/hidden-cost-of-google-protobuf-value/cover.svg" /> &lt;/p>
                
                The hidden performance cost of dynamic Protobuf in Go.
                </description><content:encoded><![CDATA[<p>Migrating legacy JSON APIs to gRPC frequently stumbles over a common anti-pattern: unstructured, dynamic JSON fields (such as <code>metadata</code> or <code>extra_properties</code>) mapped directly into Protobuf using <a href="https://protobuf.dev/reference/protobuf/google.protobuf/#value" rel="external"><code>google.protobuf.Value</code></a> or <a href="https://protobuf.dev/reference/protobuf/google.protobuf/#struct" rel="external"><code>google.protobuf.Struct</code></a>.</p>
<p>These belong to Protobuf&rsquo;s <strong>Well-Known Types (WKTs)</strong>, a library of standardized, common message schemas defined by Google (such as <code>Timestamp</code>, <code>Duration</code>, and <code>Any</code>) that ship out-of-the-box with the Protobuf compiler to provide consistent representation for reusable data structures across different languages.</p>
<p>But what actually are these specific dynamic types under the hood, and how are they used?</p>
<h3 id="the-value-message">The <code>Value</code> Message</h3>
<p>At the core of dynamic Protobuf is <a href="https://protobuf.dev/reference/protobuf/google.protobuf/#value" rel="external"><code>google.protobuf.Value</code></a>. If we look at its official definition, it is defined as a <code>oneof</code> containing the possible JSON-compatible data types:</p>
<div class="highlight"><pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;"><code class="language-protobuf" data-lang="protobuf"><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">message</span> <span style="color:#8fbcbb">Value</span> <span style="color:#eceff4">{</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>  <span style="color:#81a1c1;font-weight:bold">oneof</span> kind <span style="color:#eceff4">{</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>    NullValue null_value <span style="color:#81a1c1">=</span> <span style="color:#b48ead">1</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>    <span style="color:#81a1c1">double</span> number_value <span style="color:#81a1c1">=</span> <span style="color:#b48ead">2</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>    <span style="color:#81a1c1">string</span> string_value <span style="color:#81a1c1">=</span> <span style="color:#b48ead">3</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>    <span style="color:#81a1c1">bool</span> bool_value <span style="color:#81a1c1">=</span> <span style="color:#b48ead">4</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>    Struct struct_value <span style="color:#81a1c1">=</span> <span style="color:#b48ead">5</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>    ListValue list_value <span style="color:#81a1c1">=</span> <span style="color:#b48ead">6</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>  <span style="color:#eceff4">}</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span><span style="color:#eceff4">}</span><span style="color:#bf616a">
</span></span></span></code></pre></div><p><code>google.protobuf.Value</code> represents a dynamically typed wrapper that can hold any single JSON-compatible value: null, a number (encoded as a double-precision float), a string, a boolean, a nested <code>Struct</code> (representing a JSON object), or a list of values (<code>ListValue</code>, representing a JSON array).</p>
<h4 id="how-value-is-used">How <code>Value</code> is Used</h4>
<p><code>Value</code> is used to represent a single dynamic field whose specific type is not known at compile-time. Instead of declaring a rigid type like <code>string</code> or <code>int32</code>, defining a field as <code>Value</code> allows it to wrap any scalar, nested object, or list at runtime. In languages like Go, this compiles to a struct where the type of the value is inspected or retrieved via type assertions or helper getters on the underlying <code>oneof</code> interface.</p>
<hr>
<h3 id="the-struct-message">The <code>Struct</code> Message</h3>
<p>To represent a dynamic object structure (a collection of key-value pairs), Protobuf provides <a href="https://protobuf.dev/reference/protobuf/google.protobuf/#struct" rel="external"><code>google.protobuf.Struct</code></a>. Under the hood, it maps string keys to <code>Value</code> messages:</p>
<div class="highlight"><pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;"><code class="language-protobuf" data-lang="protobuf"><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">message</span> <span style="color:#8fbcbb">Struct</span> <span style="color:#eceff4">{</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>  map<span style="color:#eceff4">&lt;</span><span style="color:#81a1c1">string</span><span style="color:#eceff4">,</span> Value<span style="color:#eceff4">&gt;</span> fields <span style="color:#81a1c1">=</span> <span style="color:#b48ead">1</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span><span style="color:#eceff4">}</span><span style="color:#bf616a">
</span></span></span></code></pre></div><p>In practice, <code>Struct</code> behaves like a JSON object. In Go, this translates directly to a key-value map (<code>map[string]any</code>) or a raw JavaScript object where the keys are strings and the values are dynamically typed.</p>
<h4 id="how-struct-is-used">How <code>Struct</code> is Used</h4>
<p><code>Struct</code> is used to carry arbitrary structured dictionaries, and can be utilized in two primary ways:</p>
<ol>
<li><strong>Directly</strong>: As a top-level field in your message to represent unstructured configuration or metadata (e.g., <code>google.protobuf.Struct metadata = 1;</code>).</li>
<li><strong>Via <code>Value</code></strong>: Since <code>Struct</code> is one of the options inside <code>Value</code>&rsquo;s <code>oneof kind</code> (<code>Struct struct_value = 5;</code>), a <code>Struct</code> contains a map of keys to <code>Value</code>s, which can themselves be <code>Struct</code>s. This recursive relationship allows <code>Struct</code> and <code>Value</code> to together represent complex, deeply nested JSON objects containing nested lists and primitive values.</li>
</ol>
<p>Together, they allow Protobuf messages to carry arbitrary, unstructured JSON-like payloads without declaring a strict schema beforehand. This is a common architectural pattern, and it successfully solves a real developer pain point: handling highly dynamic data. Since Protobuf is famous for being fast and compact, one would intuitively think that wrapping dynamic payloads in these well-known types would still be more efficient than standard JSON.</p>
<p>I decided to test that assumption, and the results were <strong>the complete opposite</strong> of what I expected. Most of Protobuf&rsquo;s performance advantage comes from ahead-of-time schema knowledge. Statically compiled schemas are the primary optimization lever that allows Protocol Buffers to achieve speed and compactness. The moment you remove schema information and adopt unstructured types like <code>google.protobuf.Value</code> or <code>google.protobuf.Struct</code>, you give up many of the optimizations that make Protobuf fast and compact.</p>
<h2 id="the-structural-cost-of-dynamic-data">The Structural Cost of Dynamic Data</h2>
<p>Before looking at the benchmark numbers, it helps to understand why <code>google.protobuf.Value</code> is structurally expensive on the wire compared to compact JSON. When representing arbitrary object structures, <code>google.protobuf.Struct</code> is defined under the hood as <code>map&lt;string, Value&gt; fields = 1;</code>.</p>
<p>In the Protobuf wire format, maps are not native primitives. Instead, they are represented as a repeated list of auto-generated key-value message entries. The map field is equivalent to:</p>
<div class="highlight"><pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;"><code class="language-protobuf" data-lang="protobuf"><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">message</span> <span style="color:#8fbcbb">MapEntry</span> <span style="color:#eceff4">{</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>  <span style="color:#81a1c1">string</span> key <span style="color:#81a1c1">=</span> <span style="color:#b48ead">1</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>  google.protobuf.Value value <span style="color:#81a1c1">=</span> <span style="color:#b48ead">2</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span><span style="color:#eceff4">}</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span><span style="color:#616e87;font-style:italic">// Inside google.protobuf.Struct:
</span></span></span><span style="display:flex;"><span><span style="color:#616e87;font-style:italic"></span><span style="color:#81a1c1;font-weight:bold">repeated</span> MapEntry fields <span style="color:#81a1c1">=</span> <span style="color:#b48ead">1</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span></code></pre></div><p>This explains why every key-value entry is serialized as a nested sub-message (<code>MapEntry</code>) containing two inner fields (<code>key</code> and <code>value</code>), each with its own tag and length overhead.</p>
<p>For tiny dynamic payloads, the structural metadata overhead of <code>Struct</code> can exceed the payload itself. While in practice payloads are enclosed in full objects and messages, comparing the raw serialization layout of a single dynamic key-value entry like <code>&quot;age&quot;: 30</code> illustrates this overhead:</p>
<h3 id="wire-format-layout-comparison">Wire Format Layout Comparison</h3>
<h4 id="1-compact-json-10-bytes">1. Compact JSON (10 bytes)</h4>
<p>For this conceptual representation of JSON, containing a single key/value pair for age wrapped in enclosing object braces, we have:</p>
<div class="highlight"><pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;"><code class="language-json" data-lang="json"><span style="display:flex;"><span><span style="color:#eceff4">{</span><span style="color:#81a1c1">&#34;age&#34;</span><span style="color:#eceff4">:</span><span style="color:#b48ead">30</span><span style="color:#eceff4">}</span>
</span></span></code></pre></div><p>This payload is exactly <strong>10 bytes</strong> in size:</p>
<ul>
<li>2 bytes for the enclosing object braces <code>{}</code></li>
<li>5 bytes for the key <code>&quot;age&quot;</code> (including quotes)</li>
<li>1 byte for the colon separator <code>:</code></li>
<li>2 bytes for the numeric characters <code>&quot;30&quot;</code></li>
</ul>
<h4 id="2-dynamic-protobuf-protoscope-representation">2. Dynamic Protobuf (Protoscope Representation)</h4>
<p>If we describe the dynamic Protobuf binary payload using <a href="https://github.com/protocolbuffers/protoscope" rel="external">Protoscope</a> (the language for representing raw Protobuf wire formats) to visualize the structure and tags:</p>
<div class="highlight">
<pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;"><code class="language-protoscope" data-lang="protoscope">1: {           # Struct.fields map entry header        -&gt; 2 Bytes
  1: &#34;age&#34;     # MapEntry.key (tag/len &#43; &#34;age&#34; string) -&gt; 5 Bytes
  2: {         # MapEntry.value Value wrapper header   -&gt; 2 Bytes
    2: 30.0    # Value.number_value double field       -&gt; 9 Bytes
  }
}</code></pre>
</div>
<ul>
<li>Each set of braces <code>{}</code> represents a length-delimited sub-message, which compiles to a field tag followed by a length prefix byte on the wire.</li>
<li>The prefix tags <code>1:</code> and <code>2:</code> represent the field numbers.</li>
<li><code>30.0</code> compiles to field tag 2 (wire type 1, 64-bit) followed by its fixed 8-byte float value.</li>
</ul>
<p>Summing these up (2B + 5B + 2B + 9B) results in a total payload size of exactly 18 bytes for this specific encoding.</p>
<p>So with this context, we can now say <em>why</em> <code>google.protobuf.Value</code> is inefficient:</p>
<ol>
<li>
<p><strong>Double Nesting and Header Overhead:</strong> In compact JSON, the valid payload <code>{&quot;age&quot;:30}</code> consumes 10 bytes total. In dynamic Protobuf, the nested schema-less structure requires 18 bytes. The nested header metadata alone (the field tags and length prefixes for each layer) consumes <strong>7 bytes</strong>, which is nearly as large as the entire JSON payload before any key or value content is even written.</p>
</li>
<li>
<p><strong>No Field Name Compression</strong>: One of Protobuf&rsquo;s largest size advantages usually comes from discarding human-readable field names (like <code>&quot;age&quot;</code>) and replacing them with compact, 1-byte numeric tags. However, because <code>google.protobuf.Struct</code> is unstructured, it must serialize the actual field name string <code>&quot;age&quot;</code> on the wire. This completely forfeits the field-name compression benefit that makes static Protobuf so compact.</p>
</li>
<li>
<p><strong>No Varint Compression for Numbers</strong>: Instead of benefiting from Protobuf&rsquo;s specialized integer encodings, all numeric values are represented through a double-precision float field. Small integers suffer most because Protobuf normally compresses them using varints. Large values may see less dramatic differences because JSON must serialize every digit as text. To visualize the wire layout comparison for representing the small number <code>30</code>:</p>
</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Compact JSON (&#34;30&#34;):
</span></span><span style="display:flex;"><span>00110011 00110000 -&gt; 2 bytes
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Static Protobuf (Varint 30):
</span></span><span style="display:flex;"><span>00011110 -&gt; 1 byte
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Dynamic Protobuf (30.0 double-precision float):
</span></span><span style="display:flex;"><span>00000000 00000000 00000000 00000000 
</span></span><span style="display:flex;"><span>00000000 00000000 00111110 01000000 -&gt; 8 bytes (little-endian)
</span></span></code></pre></div><p>As a result of this deeply nested structure and fixed-size floats, dynamic Protobuf payloads often end up larger on the wire than compact JSON. However, this result is heavily workload-dependent. The biggest penalties come from having many fields, many keys, small numeric values, and deeply nested structures. A payload consisting mostly of a single massive string will not exhibit the same structural overhead.</p>
<h3 id="wire-inefficiency-vs-runtime-inefficiency">Wire Inefficiency vs. Runtime Inefficiency</h3>
<p>Dynamic Protobuf hurts in two completely different ways that affect different engineering decisions:</p>
<ol>
<li><strong>Wire Inefficiency:</strong> The serialized payload becomes larger than many developers expect. This is caused by human-readable field names being serialized repeatedly, nested map entry encoding, double-precision float storage for all numbers, and the loss of varint integer compression. Bandwidth-sensitive systems, databases, or event brokers care heavily about this.</li>
<li><strong>Runtime Inefficiency:</strong> The runtime representation in Go becomes allocation-heavy and expensive to parse. This is caused by Go&rsquo;s allocation behavior, interface-heavy and pointer-heavy structures in the standard <code>structpb</code> package, Go&rsquo;s reflection model, and tree-shaped decoding. CPU-bound services that deserialize payloads frequently care heavily about this.</li>
</ol>
<p>To see exactly how these wire and runtime inefficiencies compound, I built a benchmark suite to measure the real-world performance differences.</p>
<h2 id="the-benchmark-setup">The Benchmark Setup</h2>
<p>I built a Go benchmark comparing standard JSON against various Protobuf strategies across three payload sizes. The complete benchmark suite and Go test code are available in the <a href="https://github.com/sudorandom/kmcd.dev/tree/main/content/posts/2026/google-protobuf-value-considered-harmful/benchmarks" rel="external">sudorandom/kmcd.dev</a> repository on GitHub.</p>
<p>The payload configurations are:</p>
<ul>
<li><strong>Small:</strong> A flat object with 4 fields (string ID, status boolean, age integer, score float).</li>
<li><strong>Medium:</strong> A nested user signup event containing an actor object, string tags, and a metadata map.</li>
<li><strong>Large:</strong> An array repeating the Medium object 100 times.</li>
</ul>
<p>This benchmark focuses specifically on the tradeoff between schema-less Protobuf WKTs and common Go JSON implementations rather than surveying all binary serialization formats (like MessagePack, CBOR, BSON, Avro, or FlatBuffers).</p>
<h3 id="benchmark-variants">Benchmark Variants</h3>
<p>To evaluate performance across different serialization models, I compared the following variants:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Variant</th>
          <th style="text-align: center">Format</th>
          <th style="text-align: left">Description</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSON)</strong></td>
          <td style="text-align: center">JSON</td>
          <td style="text-align: left">Serializes a standard concrete Go struct using Go&rsquo;s standard <a href="https://pkg.go.dev/encoding/json" rel="external"><code>encoding/json</code></a> library (JSON v1).</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONv2)</strong></td>
          <td style="text-align: center">JSON</td>
          <td style="text-align: left">Serializes a standard concrete Go struct using the experimental, higher-performance <a href="https://pkg.go.dev/github.com/go-json-experiment/json" rel="external"><code>github.com/go-json-experiment/json</code></a> library (JSON v2).</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Map (JSON)</strong></td>
          <td style="text-align: center">JSON</td>
          <td style="text-align: left">Serializes a generic, schema-less Go map (<code>map[string]any</code>) using the standard <a href="https://pkg.go.dev/encoding/json" rel="external"><code>encoding/json</code></a> library.</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Map (JSONv2)</strong></td>
          <td style="text-align: center">JSON</td>
          <td style="text-align: left">Serializes a generic, schema-less Go map (<code>map[string]any</code>) using the experimental <a href="https://pkg.go.dev/github.com/go-json-experiment/json" rel="external"><code>github.com/go-json-experiment/json</code></a> library.</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (proto)</strong></td>
          <td style="text-align: center">Protobuf</td>
          <td style="text-align: left">Serializes statically generated Protobuf messages using Go&rsquo;s official <a href="https://pkg.go.dev/google.golang.org/protobuf/proto" rel="external"><code>google.golang.org/protobuf/proto</code></a> library.</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (vtproto)</strong></td>
          <td style="text-align: center">Protobuf</td>
          <td style="text-align: left">Serializes statically generated Protobuf messages using PlanetScale&rsquo;s optimized, reflection-free <a href="https://github.com/planetscale/vtproto" rel="external"><code>vtproto</code></a> generator.</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Any (proto)</strong></td>
          <td style="text-align: center">Protobuf</td>
          <td style="text-align: left">Serializes static Protobuf messages wrapped in a dynamic, polymorphic Well-Known Type <a href="https://protobuf.dev/reference/protobuf/google.protobuf/#any" rel="external"><code>google.protobuf.Any</code></a>.</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (proto)</strong></td>
          <td style="text-align: center">Protobuf</td>
          <td style="text-align: left">Serializes schema-less dynamic payloads using the Well-Known Type <a href="https://protobuf.dev/reference/protobuf/google.protobuf/#value" rel="external"><code>google.protobuf.Value</code></a> (<a href="https://pkg.go.dev/google.golang.org/protobuf/types/known/structpb" rel="external"><code>structpb</code></a>).</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Protobuf + JSON</strong></td>
          <td style="text-align: center">Protobuf</td>
          <td style="text-align: left">Bypasses the dynamic WKT wrapper by storing the raw serialized JSON string directly inside an opaque Protobuf string/bytes field (Opaque JSON Packaging).</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONProto)</strong></td>
          <td style="text-align: center">JSON</td>
          <td style="text-align: left">Serializes statically generated Protobuf messages into JSON format using Go&rsquo;s official <a href="https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson" rel="external"><code>protojson</code></a> encoder.</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (JSONProto)</strong></td>
          <td style="text-align: center">JSON</td>
          <td style="text-align: left">Serializes dynamic <a href="https://protobuf.dev/reference/protobuf/google.protobuf/#value" rel="external"><code>google.protobuf.Value</code></a> payloads into JSON format using Go&rsquo;s official <a href="https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson" rel="external"><code>protojson</code></a> encoder.</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Any (JSONProto)</strong></td>
          <td style="text-align: center">JSON</td>
          <td style="text-align: left">Serializes polymorphic <a href="https://protobuf.dev/reference/protobuf/google.protobuf/#any" rel="external"><code>google.protobuf.Any</code></a> wrappers into JSON format using Go&rsquo;s official <a href="https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson" rel="external"><code>protojson</code></a> encoder.</td>
      </tr>
  </tbody>
</table>
<p><strong>A Note on <code>Any</code> vs <code>Value</code>:</strong>
<code>Any</code> and <code>Value</code> solve fundamentally different problems. <code>Any</code> is not a schema-less alternative; it is a schema-dispatch mechanism. <code>Any</code> assumes a schema exists and the consumer knows it, while <code>Value</code> assumes no schema exists at all. The comparison is useful because teams often reach for <code>Value</code> when their actual requirement is polymorphism rather than truly schema-less data.</p>
<p><strong>A Note on Payload Decoding in Benchmarks:</strong>
To ensure a fair, apples-to-apples performance comparison, the unmarshaling benchmarks for all variants fully deserialize both the outer envelope and the inner dynamic/polymorphic payloads:</p>
<ul>
<li>In the <strong><code>google.protobuf.Any</code></strong> benchmark, the payload is not left as raw bytes; it is fully unpacked into a concrete statically-compiled Go struct using <code>anypb.UnmarshalTo()</code>.</li>
<li>In the <strong><code>Protobuf + JSON</code></strong> benchmark, the inner JSON string is not left unparsed; it is fully deserialized into a concrete Go struct using standard <code>json.Unmarshal()</code>.</li>
<li>In the <strong><code>google.protobuf.Value</code></strong> benchmark, the payload is fully parsed into a tree of Go objects representing the JSON-like data.</li>
</ul>
<p>Importantly, in real-world applications, both <code>google.protobuf.Any</code> and opaque <code>Protobuf + JSON</code> packaging allow you to bypass this inner parsing entirely on intermediate routing nodes (deferred/lazy parsing). This is a significant architectural advantage if intermediate services only need to forward or store the payload without inspecting it. However, to maintain a level playing field and measure the actual parsing cost, these benchmarks force full decoding.</p>
<p>To handle arbitrary data, the dynamic Protobuf configurations rely on standard <code>structpb</code> definitions:</p>
<div class="highlight"><pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;"><code class="language-protobuf" data-lang="protobuf"><span style="display:flex;"><span>syntax <span style="color:#81a1c1">=</span> <span style="color:#a3be8c">&#34;proto3&#34;</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span><span style="color:#81a1c1;font-weight:bold">package</span> <span style="color:#8fbcbb">event</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span><span style="color:#81a1c1;font-weight:bold">import</span> <span style="color:#a3be8c">&#34;google/protobuf/struct.proto&#34;</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span><span style="color:#81a1c1;font-weight:bold">message</span> <span style="color:#8fbcbb">EventEnvelope</span> <span style="color:#eceff4">{</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>  <span style="color:#81a1c1">string</span> id <span style="color:#81a1c1">=</span> <span style="color:#b48ead">1</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>  <span style="color:#81a1c1">int64</span> timestamp <span style="color:#81a1c1">=</span> <span style="color:#b48ead">2</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>  google.protobuf.Value payload <span style="color:#81a1c1">=</span> <span style="color:#b48ead">3</span><span style="color:#eceff4">;</span> <span style="color:#616e87;font-style:italic">// Dynamic payload field
</span></span></span><span style="display:flex;"><span><span style="color:#616e87;font-style:italic"></span><span style="color:#eceff4">}</span><span style="color:#bf616a">
</span></span></span></code></pre></div><h3 id="benchmark-disclaimer-and-workload-caveats">Benchmark Disclaimer and Workload Caveats</h3>
<p>As with all performance testing, microbenchmarks should be taken with a grain of salt. Actual performance will vary depending on your specific hardware, operating system, compiler version, garbage collection settings, and payload structure. Workload shape and configuration matter enormously. Map-heavy workloads are particularly pathological for <code>google.protobuf.Struct</code> due to Go&rsquo;s map lookup and insertion overhead. Deeply nested objects increase allocation and traversal costs substantially, whereas flat structures suffer less. Furthermore, implementation details like hot-path struct reuse or memory pooling (such as a thread-local arena pool solution using dynamic parser bytecode) can dramatically change outcomes in production, shifting the bottleneck back toward wire serialization and parsing logic.</p>
<p>Microbenchmarks represent synthetic workloads and may not perfectly translate to the performance profile of a complex, production system. You should always run benchmarks under your own representative workloads before making significant architectural decisions.</p>
<p>This article focuses specifically on Go&rsquo;s protobuf implementation and the <code>structpb</code> runtime model. Other languages may exhibit different allocation and parsing characteristics, though the wire-format overhead discussed here remains universal.</p>
<p>Additionally, dynamic Protobuf is not always a poor choice. For admin panels, configuration APIs, low-volume integrations, or systems where schema flexibility matters more than throughput, <code>google.protobuf.Value</code> remains a perfectly reasonable choice. It is only when these structures sit directly on high-throughput hot paths that performance problems emerge.</p>
<hr>
<h2 id="benchmark-results">Benchmark Results</h2>
<p>The benchmarks were executed under Go 1.26 on an Apple M1 Pro.</p>
<h3 id="wire-size">Wire Size</h3>
<p>First, I compared the serialized payload size of each configuration. Because Protobuf is famous for being compact, developers often assume even unstructured dynamic payloads using <code>google.protobuf.Value</code> will be smaller than standard JSON. Measuring serialized byte sizes is a straightforward test that yields definitive, objective results.</p>
<p><em>Note: These measurements reflect raw serialized payload size before transport-level compression. Systems using gzip or zstd may observe different relative wire sizes. Repeated JSON keys compress extremely well, though Protobuf map entries and repeated keys also benefit heavily from transport compression.</em></p>
<div class="rss-tabs-container">
  
  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Small Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="4f4d600c93fe3008" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('4f4d600c93fe3008');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "Concrete (proto)",
      "google.protobuf.Value (proto)",
      "google.protobuf.Any (proto)",
      "Protobuf + JSON",
      "Concrete (JSON)",
      "Concrete (JSONProto)",
      "google.protobuf.Value (JSONProto)",
      "google.protobuf.Any (JSONProto)"
    ],
    "datasets": [
      {
        "label": "Small Payload (Bytes)",
        "data": [25, 74, 74, 57, 55, 55, 55, 111],
        "backgroundColor": [
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(186, 85, 211, 0.75)",
          "rgba(186, 85, 211, 0.75)",
          "rgba(186, 85, 211, 0.75)"
        ],
        "borderColor": [
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(186, 85, 211, 1)",
          "rgba(186, 85, 211, 1)",
          "rgba(186, 85, 211, 1)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Serialized Data Size (Small Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "labels": { "color": "#fff" },
        "customLegend": [
          { "text": "proto", "color": "rgba(0, 191, 255, 0.75)" },
          { "text": "json", "color": "rgba(255, 165, 0, 0.75)" },
          { "text": "jsonproto", "color": "rgba(186, 85, 211, 0.75)" }
        ]
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": { "color": "#fff" }
      },
      "y": {
        "ticks": { "color": "#fff" }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Format / Config (Small Payload)</th>
          <th style="text-align: center">Serialized Size</th>
          <th style="text-align: center">% of JSON (lower is better)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSON)</strong></td>
          <td style="text-align: center">55 B</td>
          <td style="text-align: center">100.0% (Baseline)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (proto)</strong> / <strong>Concrete (vtproto)</strong></td>
          <td style="text-align: center">25 B</td>
          <td style="text-align: center"><strong>45.5%</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Protobuf + JSON</strong></td>
          <td style="text-align: center">57 B</td>
          <td style="text-align: center">103.6%</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Any (proto)</strong></td>
          <td style="text-align: center">74 B</td>
          <td style="text-align: center">134.5%</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (proto)</strong></td>
          <td style="text-align: center">74 B</td>
          <td style="text-align: center">134.5%</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONProto)</strong></td>
          <td style="text-align: center">55 B</td>
          <td style="text-align: center">100.0%</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (JSONProto)</strong></td>
          <td style="text-align: center">55 B</td>
          <td style="text-align: center">100.0%</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Any (JSONProto)</strong></td>
          <td style="text-align: center">111 B</td>
          <td style="text-align: center">201.8%</td>
      </tr>
  </tbody>
</table>
</details>

</div>

  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Medium Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="f3448acb4b785d27" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('f3448acb4b785d27');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "Concrete (proto)",
      "google.protobuf.Value (proto)",
      "google.protobuf.Any (proto)",
      "Protobuf + JSON",
      "Concrete (JSON)",
      "Concrete (JSONProto)",
      "google.protobuf.Value (JSONProto)",
      "google.protobuf.Any (JSONProto)"
    ],
    "datasets": [
      {
        "label": "Medium Payload (Bytes)",
        "data": [162, 328, 212, 294, 291, 293, 291, 349],
        "backgroundColor": [
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(186, 85, 211, 0.75)",
          "rgba(186, 85, 211, 0.75)",
          "rgba(186, 85, 211, 0.75)"
        ],
        "borderColor": [
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(186, 85, 211, 1)",
          "rgba(186, 85, 211, 1)",
          "rgba(186, 85, 211, 1)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Serialized Data Size (Medium Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "labels": { "color": "#fff" },
        "customLegend": [
          { "text": "proto", "color": "rgba(0, 191, 255, 0.75)" },
          { "text": "json", "color": "rgba(255, 165, 0, 0.75)" },
          { "text": "jsonproto", "color": "rgba(186, 85, 211, 0.75)" }
        ]
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": { "color": "#fff" }
      },
      "y": {
        "ticks": { "color": "#fff" }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Format / Config (Medium Payload)</th>
          <th style="text-align: center">Serialized Size</th>
          <th style="text-align: center">% of JSON (lower is better)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSON)</strong></td>
          <td style="text-align: center">291 B</td>
          <td style="text-align: center">100.0% (Baseline)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (proto)</strong> / <strong>Concrete (vtproto)</strong></td>
          <td style="text-align: center">162 B</td>
          <td style="text-align: center"><strong>55.7%</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Any (proto)</strong></td>
          <td style="text-align: center">212 B</td>
          <td style="text-align: center"><strong>72.9%</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Protobuf + JSON</strong></td>
          <td style="text-align: center">294 B</td>
          <td style="text-align: center">101.0%</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (proto)</strong></td>
          <td style="text-align: center">328 B</td>
          <td style="text-align: center">112.7%</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONProto)</strong></td>
          <td style="text-align: center">293 B</td>
          <td style="text-align: center">100.7%</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (JSONProto)</strong></td>
          <td style="text-align: center">291 B</td>
          <td style="text-align: center">100.0%</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Any (JSONProto)</strong></td>
          <td style="text-align: center">349 B</td>
          <td style="text-align: center">119.9%</td>
      </tr>
  </tbody>
</table>
</details>

</div>

  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Large Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="693a1f996cf24ed1" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('693a1f996cf24ed1');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "Concrete (proto)",
      "google.protobuf.Value (proto)",
      "google.protobuf.Any (proto)",
      "Protobuf + JSON",
      "Concrete (JSON)",
      "Concrete (JSONProto)",
      "google.protobuf.Value (JSONProto)",
      "google.protobuf.Any (JSONProto)"
    ],
    "datasets": [
      {
        "label": "Large Payload (Bytes)",
        "data": [16500, 33104, 21200, 29205, 29201, 29412, 29201, 34900],
        "backgroundColor": [
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(186, 85, 211, 0.75)",
          "rgba(186, 85, 211, 0.75)",
          "rgba(186, 85, 211, 0.75)"
        ],
        "borderColor": [
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(186, 85, 211, 1)",
          "rgba(186, 85, 211, 1)",
          "rgba(186, 85, 211, 1)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Serialized Data Size (Large Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "labels": { "color": "#fff" },
        "customLegend": [
          { "text": "proto", "color": "rgba(0, 191, 255, 0.75)" },
          { "text": "json", "color": "rgba(255, 165, 0, 0.75)" },
          { "text": "jsonproto", "color": "rgba(186, 85, 211, 0.75)" }
        ]
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": { "color": "#fff" }
      },
      "y": {
        "ticks": { "color": "#fff" }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Format / Config (Large Payload)</th>
          <th style="text-align: center">Serialized Size</th>
          <th style="text-align: center">% of JSON (lower is better)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSON)</strong></td>
          <td style="text-align: center">29,201 B</td>
          <td style="text-align: center">100.0% (Baseline)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (proto)</strong> / <strong>Concrete (vtproto)</strong></td>
          <td style="text-align: center">16,500 B</td>
          <td style="text-align: center"><strong>56.5%</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Any (proto)</strong></td>
          <td style="text-align: center">21200 B</td>
          <td style="text-align: center"><strong>72.6%</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Protobuf + JSON</strong></td>
          <td style="text-align: center">29,205 B</td>
          <td style="text-align: center">100.0%</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (proto)</strong></td>
          <td style="text-align: center">33,104 B</td>
          <td style="text-align: center">113.4%</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONProto)</strong></td>
          <td style="text-align: center">29,412 B</td>
          <td style="text-align: center">100.7%</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (JSONProto)</strong></td>
          <td style="text-align: center">29,201 B</td>
          <td style="text-align: center">100.0%</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Any (JSONProto)</strong></td>
          <td style="text-align: center">34,900 B</td>
          <td style="text-align: center">119.5%</td>
      </tr>
  </tbody>
</table>
</details>

</div>


</div>

<h3 id="processing-throughput">Processing Throughput</h3>
<p>Wire size is only half the battle; parsing these dynamic structures introduces significant CPU and memory costs. Building and parsing schema-less Protobuf trees involves significant pointer-wrapping overhead, resulting in higher CPU usage and frequent heap allocations. Standard concrete Protobuf marshals almost instantly, and PlanetScale&rsquo;s reflection-free generator <code>Concrete (vtproto)</code> is the absolute fastest. Much of <code>vtproto</code>&rsquo;s advantage comes from eliminating reflection and generating specialized straight-line serialization code ahead of time.</p>
<div class="rss-tabs-container">
  
  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Small Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="90a775425300d052" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('90a775425300d052');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "Concrete (vtproto)",
      "Concrete (proto)",
      "Concrete (JSON)",
      "google.protobuf.Any (proto)",
      "Protobuf + JSON",
      "Concrete (JSONv2)",
      "Map (JSON)",
      "Concrete (JSONProto)",
      "Map (JSONv2)",
      "google.protobuf.Value (proto)",
      "google.protobuf.Value (JSONProto)"
    ],
    "datasets": [
      {
        "label": "Small Payload (ns/op)",
        "data": [31, 104, 210, 287, 365, 365, 706, 761, 950, 2163, 3017],
        "backgroundColor": [
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(186, 85, 211, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(186, 85, 211, 0.75)"
        ],
        "borderColor": [
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(186, 85, 211, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(186, 85, 211, 1)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Marshaling Performance (Small Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "labels": { "color": "#fff" },
        "customLegend": [
          { "text": "proto", "color": "rgba(0, 191, 255, 0.75)" },
          { "text": "json", "color": "rgba(255, 165, 0, 0.75)" },
          { "text": "jsonproto", "color": "rgba(186, 85, 211, 0.75)" }
        ]
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": { "color": "#fff" }
      },
      "y": {
        "ticks": { "color": "#fff" }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Benchmark (Small Payload)</th>
          <th style="text-align: center">ns/op</th>
          <th style="text-align: center">Memory (B/op)</th>
          <th style="text-align: center">Allocations/op</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Concrete (vtproto)</strong></td>
          <td style="text-align: center"><strong>31 ns</strong></td>
          <td style="text-align: center"><strong>32 B</strong></td>
          <td style="text-align: center"><strong>1</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Any (proto)</strong></td>
          <td style="text-align: center">97 ns</td>
          <td style="text-align: center">80 B</td>
          <td style="text-align: center">1</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (proto)</strong></td>
          <td style="text-align: center">104 ns</td>
          <td style="text-align: center">32 B</td>
          <td style="text-align: center">1</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSON)</strong></td>
          <td style="text-align: center">212 ns</td>
          <td style="text-align: center">64 B</td>
          <td style="text-align: center">1</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Protobuf + JSON</strong></td>
          <td style="text-align: center">361 ns</td>
          <td style="text-align: center">256 B</td>
          <td style="text-align: center">4</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONv2)</strong></td>
          <td style="text-align: center">372 ns</td>
          <td style="text-align: center">112 B</td>
          <td style="text-align: center">2</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Map (JSON)</strong></td>
          <td style="text-align: center">707 ns</td>
          <td style="text-align: center">352 B</td>
          <td style="text-align: center">10</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONProto)</strong></td>
          <td style="text-align: center">774 ns</td>
          <td style="text-align: center">512 B</td>
          <td style="text-align: center">12</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Map (JSONv2)</strong></td>
          <td style="text-align: center">949 ns</td>
          <td style="text-align: center">151 B</td>
          <td style="text-align: center">9</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (proto)</strong></td>
          <td style="text-align: center">1,598 ns</td>
          <td style="text-align: center">208 B</td>
          <td style="text-align: center">9</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (JSONProto)</strong></td>
          <td style="text-align: center">2,461 ns</td>
          <td style="text-align: center">704 B</td>
          <td style="text-align: center">23</td>
      </tr>
  </tbody>
</table>
</details>

</div>

  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Medium Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="55fc8aaabaf10e67" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('55fc8aaabaf10e67');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "Concrete (vtproto)",
      "Concrete (proto)",
      "google.protobuf.Any (proto)",
      "Concrete (JSON)",
      "Protobuf + JSON",
      "Concrete (JSONv2)",
      "Map (JSONv2)",
      "Map (JSON)",
      "Concrete (JSONProto)",
      "google.protobuf.Value (proto)",
      "google.protobuf.Value (JSONProto)"
    ],
    "datasets": [
      {
        "label": "Medium Payload (ns/op)",
        "data": [129, 366, 597, 656, 855, 1002, 1852, 2273, 2739, 6854, 10177],
        "backgroundColor": [
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(186, 85, 211, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(186, 85, 211, 0.75)"
        ],
        "borderColor": [
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(186, 85, 211, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(186, 85, 211, 1)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Marshaling Performance (Medium Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "labels": { "color": "#fff" },
        "customLegend": [
          { "text": "proto", "color": "rgba(0, 191, 255, 0.75)" },
          { "text": "json", "color": "rgba(255, 165, 0, 0.75)" },
          { "text": "jsonproto", "color": "rgba(186, 85, 211, 0.75)" }
        ]
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": { "color": "#fff" }
      },
      "y": {
        "ticks": { "color": "#fff" }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Benchmark (Medium Payload)</th>
          <th style="text-align: center">ns/op</th>
          <th style="text-align: center">Memory (B/op)</th>
          <th style="text-align: center">Allocations/op</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Concrete (vtproto)</strong></td>
          <td style="text-align: center"><strong>123 ns</strong></td>
          <td style="text-align: center"><strong>176 B</strong></td>
          <td style="text-align: center"><strong>1</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Any (proto)</strong></td>
          <td style="text-align: center">127 ns</td>
          <td style="text-align: center">224 B</td>
          <td style="text-align: center">1</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (proto)</strong></td>
          <td style="text-align: center">356 ns</td>
          <td style="text-align: center">176 B</td>
          <td style="text-align: center">1</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSON)</strong></td>
          <td style="text-align: center">634 ns</td>
          <td style="text-align: center">464 B</td>
          <td style="text-align: center">2</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Protobuf + JSON</strong></td>
          <td style="text-align: center">844 ns</td>
          <td style="text-align: center">1,024 B</td>
          <td style="text-align: center">4</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONv2)</strong></td>
          <td style="text-align: center">985 ns</td>
          <td style="text-align: center">608 B</td>
          <td style="text-align: center">3</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Map (JSONv2)</strong></td>
          <td style="text-align: center">1,835 ns</td>
          <td style="text-align: center">456 B</td>
          <td style="text-align: center">12</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Map (JSON)</strong></td>
          <td style="text-align: center">2,346 ns</td>
          <td style="text-align: center">1,200 B</td>
          <td style="text-align: center">28</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONProto)</strong></td>
          <td style="text-align: center">2,797 ns</td>
          <td style="text-align: center">1,722 B</td>
          <td style="text-align: center">34</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (proto)</strong></td>
          <td style="text-align: center">4,872 ns</td>
          <td style="text-align: center">736 B</td>
          <td style="text-align: center">25</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (JSONProto)</strong></td>
          <td style="text-align: center">7,828 ns</td>
          <td style="text-align: center">2,750 B</td>
          <td style="text-align: center">70</td>
      </tr>
  </tbody>
</table>
</details>

</div>

  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Large Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="4dd8e90e17576f31" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('4dd8e90e17576f31');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "Concrete (vtproto)",
      "Concrete (proto)",
      "Concrete (JSON)",
      "google.protobuf.Any (proto)",
      "Protobuf + JSON",
      "Concrete (JSONv2)",
      "Map (JSONv2)",
      "Map (JSON)",
      "Concrete (JSONProto)",
      "google.protobuf.Value (proto)",
      "google.protobuf.Value (JSONProto)"
    ],
    "datasets": [
      {
        "label": "Large Payload (ns/op)",
        "data": [9065, 31060, 50746, 59813, 61323, 76945, 107436, 236591, 279812, 680700, 988728],
        "backgroundColor": [
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(186, 85, 211, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(186, 85, 211, 0.75)"
        ],
        "borderColor": [
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(186, 85, 211, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(186, 85, 211, 1)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Marshaling Performance (Large Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "labels": { "color": "#fff" },
        "customLegend": [
          { "text": "proto", "color": "rgba(0, 191, 255, 0.75)" },
          { "text": "json", "color": "rgba(255, 165, 0, 0.75)" },
          { "text": "jsonproto", "color": "rgba(186, 85, 211, 0.75)" }
        ]
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": { "color": "#fff" }
      },
      "y": {
        "ticks": { "color": "#fff" }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Benchmark (Large Payload)</th>
          <th style="text-align: center">ns/op</th>
          <th style="text-align: center">Memory (B/op)</th>
          <th style="text-align: center">Allocations/op</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Concrete (vtproto)</strong></td>
          <td style="text-align: center"><strong>8,818 ns</strong></td>
          <td style="text-align: center"><strong>18,432 B</strong></td>
          <td style="text-align: center"><strong>1</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Any (proto)</strong></td>
          <td style="text-align: center">13,042 ns</td>
          <td style="text-align: center">22,400 B</td>
          <td style="text-align: center">100</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (proto)</strong></td>
          <td style="text-align: center">30,748 ns</td>
          <td style="text-align: center">18,432 B</td>
          <td style="text-align: center">1</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSON)</strong></td>
          <td style="text-align: center">50,543 ns</td>
          <td style="text-align: center">32,823 B</td>
          <td style="text-align: center">2</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Protobuf + JSON</strong></td>
          <td style="text-align: center">60,580 ns</td>
          <td style="text-align: center">98,877 B</td>
          <td style="text-align: center">4</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONv2)</strong></td>
          <td style="text-align: center">76,952 ns</td>
          <td style="text-align: center">32,837 B</td>
          <td style="text-align: center">3</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Map (JSONv2)</strong></td>
          <td style="text-align: center">106,291 ns</td>
          <td style="text-align: center">35,259 B</td>
          <td style="text-align: center">303</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Map (JSON)</strong></td>
          <td style="text-align: center">226,032 ns</td>
          <td style="text-align: center">120,907 B</td>
          <td style="text-align: center">2,702</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONProto)</strong></td>
          <td style="text-align: center">284,717 ns</td>
          <td style="text-align: center">243,744 B</td>
          <td style="text-align: center">2,728</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (proto)</strong></td>
          <td style="text-align: center">486,336 ns</td>
          <td style="text-align: center">79,360 B</td>
          <td style="text-align: center">2,401</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (JSONProto)</strong></td>
          <td style="text-align: center">811,368 ns</td>
          <td style="text-align: center">320,172 B</td>
          <td style="text-align: center">6,260</td>
      </tr>
  </tbody>
</table>
</details>

</div>


</div>

<p>The most surprising finding here is not that <code>Value</code> is slower than static Protobuf. Everyone expects that. The headline-worthy result is this: in these benchmarks, dynamic Protobuf frequently loses to plain JSON as well.</p>
<p>For a medium payload, standard static Protobuf is 13x faster than dynamic binary <code>Value</code> serialization, but standard JSON is over 7x faster than <code>Value</code>. When evaluating unmarshaling, the gap widens further:</p>
<div class="rss-tabs-container">
  
  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Small Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="f677bbc334ed7e88" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('f677bbc334ed7e88');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "Concrete (vtproto)",
      "Concrete (proto)",
      "google.protobuf.Any (proto)",
      "Concrete (JSONv2)",
      "Concrete (JSON)",
      "Map (JSONv2)",
      "Protobuf + JSON",
      "Concrete (JSONProto)",
      "Map (JSON)",
      "google.protobuf.Value (proto)",
      "google.protobuf.Value (JSONProto)"
    ],
    "datasets": [
      {
        "label": "Small Payload (ns/op)",
        "data": [32, 141, 315, 430, 936, 964, 1130, 1161, 1345, 1715, 3220],
        "backgroundColor": [
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(186, 85, 211, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(186, 85, 211, 0.75)"
        ],
        "borderColor": [
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(186, 85, 211, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(186, 85, 211, 1)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Unmarshaling Performance (Small Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "labels": { "color": "#fff" },
        "customLegend": [
          { "text": "proto", "color": "rgba(0, 191, 255, 0.75)" },
          { "text": "json", "color": "rgba(255, 165, 0, 0.75)" },
          { "text": "jsonproto", "color": "rgba(186, 85, 211, 0.75)" }
        ]
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": { "color": "#fff" }
      },
      "y": {
        "ticks": { "color": "#fff" }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Benchmark (Small Payload)</th>
          <th style="text-align: center">ns/op</th>
          <th style="text-align: center">Memory (B/op)</th>
          <th style="text-align: center">Allocations/op</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Concrete (vtproto)</strong></td>
          <td style="text-align: center"><strong>32 ns</strong></td>
          <td style="text-align: center"><strong>16 B</strong></td>
          <td style="text-align: center"><strong>1</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (proto)</strong></td>
          <td style="text-align: center">141 ns</td>
          <td style="text-align: center">96 B</td>
          <td style="text-align: center">2</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Any (proto)</strong></td>
          <td style="text-align: center">315 ns</td>
          <td style="text-align: center">256 B</td>
          <td style="text-align: center">5</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONv2)</strong></td>
          <td style="text-align: center">430 ns</td>
          <td style="text-align: center">48 B</td>
          <td style="text-align: center">1</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSON)</strong></td>
          <td style="text-align: center">936 ns</td>
          <td style="text-align: center">280 B</td>
          <td style="text-align: center">6</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Map (JSONv2)</strong></td>
          <td style="text-align: center">964 ns</td>
          <td style="text-align: center">408 B</td>
          <td style="text-align: center">8</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Protobuf + JSON</strong></td>
          <td style="text-align: center">1,130 ns</td>
          <td style="text-align: center">472 B</td>
          <td style="text-align: center">9</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONProto)</strong></td>
          <td style="text-align: center">1,161 ns</td>
          <td style="text-align: center">336 B</td>
          <td style="text-align: center">14</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Map (JSON)</strong></td>
          <td style="text-align: center">1,345 ns</td>
          <td style="text-align: center">648 B</td>
          <td style="text-align: center">20</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (proto)</strong></td>
          <td style="text-align: center">1,715 ns</td>
          <td style="text-align: center">832 B</td>
          <td style="text-align: center">26</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (JSONProto)</strong></td>
          <td style="text-align: center">3,220 ns</td>
          <td style="text-align: center">1,256 B</td>
          <td style="text-align: center">43</td>
      </tr>
  </tbody>
</table>
</details>

</div>

  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Medium Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="08c42069dbc1acc3" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('08c42069dbc1acc3');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "Concrete (vtproto)",
      "Concrete (proto)",
      "google.protobuf.Any (proto)",
      "Concrete (JSONv2)",
      "Map (JSONv2)",
      "Concrete (JSON)",
      "Protobuf + JSON",
      "Map (JSON)",
      "Concrete (JSONProto)",
      "google.protobuf.Value (proto)",
      "google.protobuf.Value (JSONProto)"
    ],
    "datasets": [
      {
        "label": "Medium Payload (ns/op)",
        "data": [383, 690, 906, 1410, 2595, 3659, 3970, 4177, 4659, 5686, 10819],
        "backgroundColor": [
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(186, 85, 211, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(186, 85, 211, 0.75)"
        ],
        "borderColor": [
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(186, 85, 211, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(186, 85, 211, 1)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Unmarshaling Performance (Medium Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "labels": { "color": "#fff" },
        "customLegend": [
          { "text": "proto", "color": "rgba(0, 191, 255, 0.75)" },
          { "text": "json", "color": "rgba(255, 165, 0, 0.75)" },
          { "text": "jsonproto", "color": "rgba(186, 85, 211, 0.75)" }
        ]
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": { "color": "#fff" }
      },
      "y": {
        "ticks": { "color": "#fff" }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Benchmark (Medium Payload)</th>
          <th style="text-align: center">ns/op</th>
          <th style="text-align: center">Memory (B/op)</th>
          <th style="text-align: center">Allocations/op</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Concrete (vtproto)</strong></td>
          <td style="text-align: center"><strong>383 ns</strong></td>
          <td style="text-align: center"><strong>432 B</strong></td>
          <td style="text-align: center"><strong>14</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (proto)</strong></td>
          <td style="text-align: center">690 ns</td>
          <td style="text-align: center">560 B</td>
          <td style="text-align: center">15</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Any (proto)</strong></td>
          <td style="text-align: center">906 ns</td>
          <td style="text-align: center">864 B</td>
          <td style="text-align: center">18</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONv2)</strong></td>
          <td style="text-align: center">1,410 ns</td>
          <td style="text-align: center">256 B</td>
          <td style="text-align: center">4</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Map (JSONv2)</strong></td>
          <td style="text-align: center">2,595 ns</td>
          <td style="text-align: center">1,392 B</td>
          <td style="text-align: center">30</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSON)</strong></td>
          <td style="text-align: center">3,659 ns</td>
          <td style="text-align: center">688 B</td>
          <td style="text-align: center">19</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Protobuf + JSON</strong></td>
          <td style="text-align: center">3,970 ns</td>
          <td style="text-align: center">1,392 B</td>
          <td style="text-align: center">22</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Map (JSON)</strong></td>
          <td style="text-align: center">4,177 ns</td>
          <td style="text-align: center">1,856 B</td>
          <td style="text-align: center">54</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONProto)</strong></td>
          <td style="text-align: center">4,659 ns</td>
          <td style="text-align: center">1,304 B</td>
          <td style="text-align: center">58</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (proto)</strong></td>
          <td style="text-align: center">5,686 ns</td>
          <td style="text-align: center">2,888 B</td>
          <td style="text-align: center">90</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (JSONProto)</strong></td>
          <td style="text-align: center">10,819 ns</td>
          <td style="text-align: center">4,080 B</td>
          <td style="text-align: center">145</td>
      </tr>
  </tbody>
</table>
</details>

</div>

  <div class="rss-tab-section" style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 10px;">
  <h3>Large Payload</h3>
  <div class="chart-wrapper">
<div class="chart">
<canvas id="ac2ecb211192577e" data-sort="true"></canvas>
</div>
</div>
<script>
(function() {
window.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) => {
if (window.Chart) {
resolve(window.Chart);
return;
}
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.async = true;
script.onload = () => resolve(window.Chart);
script.onerror = () => reject(new Error("Failed to load Chart.js"));
document.head.appendChild(script);
});
window.activeCharts = window.activeCharts || [];
function getThemeColors() {
return {
text: '#ffffff',
grid: 'rgba(255, 255, 255, 0.1)'
};
}
function applyThemeToOptions(options, colors) {
if (!options) return;
if (!options.plugins) options.plugins = {};
if (options.plugins.title) {
options.plugins.title.color = colors.text;
}
if (!options.plugins.legend) options.plugins.legend = {};
if (!options.plugins.legend.labels) options.plugins.legend.labels = {};
options.plugins.legend.labels.color = colors.text;
if (options.scales) {
for (const scaleId in options.scales) {
const scale = options.scales[scaleId];
if (scale && typeof scale === 'object') {
if (!scale.ticks) scale.ticks = {};
scale.ticks.color = colors.text;
if (!scale.grid) scale.grid = {};
scale.grid.color = colors.grid;
}
}
}
}
if (!window.chartThemeObserver) {
window.chartThemeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
const colors = getThemeColors();
window.activeCharts.forEach((chart) => {
applyThemeToOptions(chart.options, colors);
chart.update();
});
}
});
});
window.chartThemeObserver.observe(document.documentElement, { attributes: true });
}
window.chartJsPromise.then((Chart) => {
const ctx = document.getElementById('ac2ecb211192577e');
if (!ctx) return;
const chartConfig = 
{
  "type": "bar",
  "data": {
    "labels": [
      "Concrete (vtproto)",
      "Concrete (proto)",
      "google.protobuf.Any (proto)",
      "Concrete (JSONv2)",
      "Map (JSONv2)",
      "Map (JSON)",
      "Concrete (JSON)",
      "Protobuf + JSON",
      "Concrete (JSONProto)",
      "google.protobuf.Value (proto)",
      "google.protobuf.Value (JSONProto)"
    ],
    "datasets": [
      {
        "label": "Large Payload (ns/op)",
        "data": [43925, 67402, 90635, 137095, 219971, 337811, 345927, 358199, 473163, 585077, 1150365],
        "backgroundColor": [
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(255, 165, 0, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(186, 85, 211, 0.75)",
          "rgba(0, 191, 255, 0.75)",
          "rgba(186, 85, 211, 0.75)"
        ],
        "borderColor": [
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(255, 165, 0, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(186, 85, 211, 1)",
          "rgba(0, 191, 255, 1)",
          "rgba(186, 85, 211, 1)"
        ],
        "borderWidth": 1
      }
    ]
  },
  "options": {
    "indexAxis": "y",
    "plugins": {
      "title": {
        "display": true,
        "text": "Unmarshaling Performance (Large Payload): lower is better",
        "color": "#fff"
      },
      "legend": {
        "labels": { "color": "#fff" },
        "customLegend": [
          { "text": "proto", "color": "rgba(0, 191, 255, 0.75)" },
          { "text": "json", "color": "rgba(255, 165, 0, 0.75)" },
          { "text": "jsonproto", "color": "rgba(186, 85, 211, 0.75)" }
        ]
      }
    },
    "scales": {
      "x": {
        "type": "linear",
        "min": 0,
        "ticks": { "color": "#fff" }
      },
      "y": {
        "ticks": { "color": "#fff" }
      }
    }
  }
};
const isMobileWidth = window.innerWidth >= 600 ? false : true;
if (!chartConfig.options) chartConfig.options = {};
if (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;
if (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;
if (isMobileWidth && chartConfig.data && chartConfig.data.labels) {
chartConfig.data.labels = chartConfig.data.labels.map(function(label) {
if (typeof label === 'string') {
return label
.replace('google.protobuf.Value', 'g.p.Value')
.replace('google.protobuf.Any', 'g.p.Any')
.replace('google.protobuf.', 'g.p.');
}
return label;
});
}
if (isMobileWidth) {
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
if (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};
if (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};
chartConfig.options.scales.x.ticks.font.size = 10;
if (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};
if (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};
if (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};
chartConfig.options.scales.y.ticks.font.size = 10;
}
function adjustHeight() {
const isMobile = window.innerWidth >= 600 ? false : true;
const barCount = (chartConfig.data && chartConfig.data.labels) ? chartConfig.data.labels.length : 0;
let chartHeight = 350;
if (chartConfig.type === 'bar' && chartConfig.options.indexAxis === 'y') {
const heightPerBar = isMobile ? 35 : 30;
const extraPadding = isMobile ? 130 : 100;
chartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);
} else {
chartHeight = isMobile ? 320 : 380;
}
ctx.parentElement.style.height = chartHeight + 'px';
}
adjustHeight();
window.addEventListener('resize', adjustHeight);
if (chartConfig.options && chartConfig.options.plugins && chartConfig.options.plugins.legend && chartConfig.options.plugins.legend.customLegend) { const customItems = chartConfig.options.plugins.legend.customLegend; chartConfig.options.plugins.legend.labels = chartConfig.options.plugins.legend.labels || {}; chartConfig.options.plugins.legend.labels.generateLabels = function(chart) { return customItems.map(function(item) { return { text: item.text, fillStyle: item.color, strokeStyle: item.color, lineWidth: 1, hidden: false, fontColor: '#ffffff', color: '#ffffff' }; }); }; chartConfig.options.plugins.legend.onClick = function() {}; }
const colors = getThemeColors();
applyThemeToOptions(chartConfig.options, colors);
const customLabelPlugin = {
id: 'customLabelPlugin',
afterDatasetsDraw: function(chart) {
if (chart.config.type !== 'bar') return;
const ctx = chart.ctx;
const canvasEl = chart.canvas;
const unit = canvasEl.getAttribute('data-unit');
chart.data.datasets.forEach(function(dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex);
meta.data.forEach(function(bar, index) {
const value = dataset.data[index];
if (value === undefined || value === null) return;
if (bar && typeof bar.x === 'number' && chart.isDatasetVisible(datasetIndex)) {
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const isMobile = window.innerWidth >= 600 ? false : true;
ctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';
let text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
if (unit) {
text += ' ' + unit;
} else {
const label = dataset.label || '';
if (label.toLowerCase().indexOf('bytes') !== -1) {
text += ' B';
} else if (label.toLowerCase().indexOf('ns') !== -1) {
text += ' ns';
}
}
ctx.fillText(text, bar.x + 6, bar.y);
}
});
});
}
};
chartConfig.plugins = chartConfig.plugins || [];
chartConfig.plugins.push(customLabelPlugin);
if (!chartConfig.options.scales) chartConfig.options.scales = {};
if (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};
chartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';
const canvasEl = ctx;
const sortAttr = canvasEl.getAttribute('data-sort');
if (sortAttr !== 'false' && chartConfig.type === 'bar' && chartConfig.data && chartConfig.data.datasets && chartConfig.data.datasets[0]) {
const dataset = chartConfig.data.datasets[0];
const label = (dataset.label || '').toLowerCase();
let sortOrder = 'asc';
if (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {
sortOrder = 'desc';
}
if (chartConfig.data.labels && dataset.data && chartConfig.data.labels.length === dataset.data.length) {
let items = chartConfig.data.labels.map(function(label, index) {
return {
label: label,
value: dataset.data[index],
backgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,
borderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor
};
});
items.sort(function(a, b) {
if (sortOrder === 'desc') {
return b.value - a.value;
}
return a.value - b.value;
});
chartConfig.data.labels = items.map(function(item) { return item.label; });
dataset.data = items.map(function(item) { return item.value; });
if (Array.isArray(dataset.backgroundColor)) {
dataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });
}
if (Array.isArray(dataset.borderColor)) {
dataset.borderColor = items.map(function(item) { return item.borderColor; });
}
}
}
const chart = new Chart(ctx, chartConfig);
window.activeCharts.push(chart);
if (window.onChartInit) {
window.onChartInit(chart);
}
}, (err) => {
console.error("Chart.js loading error: ", err);
});
})();
</script>
<details>
<summary><b>Show data table</b></summary>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Benchmark (Large Payload)</th>
          <th style="text-align: center">ns/op</th>
          <th style="text-align: center">Memory (B/op)</th>
          <th style="text-align: center">Allocations/op</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Concrete (vtproto)</strong></td>
          <td style="text-align: center"><strong>43,925 ns</strong></td>
          <td style="text-align: center"><strong>58,168 B</strong></td>
          <td style="text-align: center"><strong>1,508</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (proto)</strong></td>
          <td style="text-align: center">67,402 ns</td>
          <td style="text-align: center">58,232 B</td>
          <td style="text-align: center">1,509</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Any (proto)</strong></td>
          <td style="text-align: center">90,635 ns</td>
          <td style="text-align: center">86,400 B</td>
          <td style="text-align: center">1,800</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONv2)</strong></td>
          <td style="text-align: center">137,095 ns</td>
          <td style="text-align: center">54,303 B</td>
          <td style="text-align: center">309</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Map (JSONv2)</strong></td>
          <td style="text-align: center">219,971 ns</td>
          <td style="text-align: center">144,699 B</td>
          <td style="text-align: center">3,309</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Map (JSON)</strong></td>
          <td style="text-align: center">337,811 ns</td>
          <td style="text-align: center">162,297 B</td>
          <td style="text-align: center">4,313</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSON)</strong></td>
          <td style="text-align: center">345,927 ns</td>
          <td style="text-align: center">70,584 B</td>
          <td style="text-align: center">1,216</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Protobuf + JSON</strong></td>
          <td style="text-align: center">358,199 ns</td>
          <td style="text-align: center">136,184 B</td>
          <td style="text-align: center">1,219</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Concrete (JSONProto)</strong></td>
          <td style="text-align: center">473,163 ns</td>
          <td style="text-align: center">119,256 B</td>
          <td style="text-align: center">5,713</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (proto)</strong></td>
          <td style="text-align: center">585,077 ns</td>
          <td style="text-align: center">291,106 B</td>
          <td style="text-align: center">9,011</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>google.protobuf.Value (JSONProto)</strong></td>
          <td style="text-align: center">1,150,365 ns</td>
          <td style="text-align: center">395,331 B</td>
          <td style="text-align: center">14,414</td>
      </tr>
  </tbody>
</table>
</details>

</div>


</div>

<p>Dynamic binary parsing takes <strong>5,772 ns</strong> and requires <strong>90 allocations</strong>, compared to just <strong>674 ns</strong> and <strong>15 allocations</strong> for standard static Protobuf.</p>
<h3 id="appendix-the-hidden-cost-of-construction">Appendix: The Hidden Cost of Construction</h3>
<p>To keep comparisons apples-to-apples, the main benchmarks above isolate the pure marshaling (serialization) and unmarshaling (deserialization) phases using pre-constructed message structures.</p>
<p>However, in real-world systems, using dynamic types introduces a secondary runtime overhead: translating native Go data structures (like <code>map[string]any</code>) into <code>structpb.Value</code> (construction) before serialization, and converting them back (using <code>.AsInterface()</code>) after deserialization.</p>
<p>The table below shows the cost of these construction and conversion steps across our three payload sizes:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Payload</th>
          <th style="text-align: left">Phase</th>
          <th style="text-align: center">Time (ns/op)</th>
          <th style="text-align: center">Memory (B/op)</th>
          <th style="text-align: center">Allocations/op</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Small</strong></td>
          <td style="text-align: left">Construction (<code>NewValue</code>)</td>
          <td style="text-align: center">504 ns</td>
          <td style="text-align: center">671 B</td>
          <td style="text-align: center">13</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left">Conversion (<code>AsInterface</code>)</td>
          <td style="text-align: center">300 ns</td>
          <td style="text-align: center">368 B</td>
          <td style="text-align: center">5</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Medium</strong></td>
          <td style="text-align: left">Construction (<code>NewValue</code>)</td>
          <td style="text-align: center">2,076 ns</td>
          <td style="text-align: center">2,223 B</td>
          <td style="text-align: center">43</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left">Conversion (<code>AsInterface</code>)</td>
          <td style="text-align: center">1,127 ns</td>
          <td style="text-align: center">1,240 B</td>
          <td style="text-align: center">19</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Large</strong></td>
          <td style="text-align: left">Construction (<code>NewValue</code>)</td>
          <td style="text-align: center">200,092 ns</td>
          <td style="text-align: center">223,407 B</td>
          <td style="text-align: center">4,305</td>
      </tr>
      <tr>
          <td style="text-align: left"></td>
          <td style="text-align: left">Conversion (<code>AsInterface</code>)</td>
          <td style="text-align: center">129,140 ns</td>
          <td style="text-align: center">125,818 B</td>
          <td style="text-align: center">1,902</td>
      </tr>
  </tbody>
</table>
<p>For a medium payload, constructing the <code>structpb.Value</code> and converting it back adds an extra <strong>3.2 microseconds</strong> and <strong>62 heap allocations</strong> to the overall lifecycle. When combined with the marshaling and unmarshaling costs, these conversion steps represent a significant hidden performance penalty in high-throughput Go services.</p>
<h2 id="the-root-cause-allocations-and-pointer-chasing">The Root Cause: Allocations and Pointer Chasing</h2>
<p>With the wire and processing numbers in hand, we can look at the mechanical reasons behind the overhead.</p>
<ol>
<li><strong>Go Runtime Allocations:</strong> Because Go is statically typed, representing a polymorphic JSON-like tree requires nesting interfaces and pointers. Deserializing a dynamic <code>Value</code> payload requires Go&rsquo;s runtime to allocate a unique <code>*structpb.Value</code> pointer for every single map key, list item, and value in the tree. Every node in the tree is represented as a separate Protobuf message (<code>Value</code>, <code>Struct</code>, or <code>ListValue</code>), requiring recursive traversal during serialization and deserialization. On a large payload, this creates over 9,000 individual heap allocations, putting immense pressure on Go&rsquo;s garbage collector and memory allocator. Furthermore, <code>structpb</code> relies heavily on interface boxing. Pushing values through these abstraction layers prevents the compiler from applying the aggressive optimizations it normally uses for generated static structs.</li>
<li><strong>Cache Locality and Memory Layout:</strong> At the systems level, statically generated Protobuf messages compile to flat, cache-friendly Go structs representing contiguous (or near-contiguous) memory blocks. In contrast, <code>structpb.Value</code> constructs a highly fragmented graph of heap-allocated objects connected by pointers. Traversing this tree-shaped structure causes frequent pointer chasing, which hurts cache locality, increases L1/L2 cache misses, and hinders branch prediction. Because the pointer graph fragments memory, the CPU prefetcher cannot effectively predict the next memory address, leading to those cache misses.</li>
</ol>
<h2 id="are-you-actually-solving-a-dynamic-data-problem">Are You Actually Solving a Dynamic Data Problem?</h2>
<p>In practice, many <code>google.protobuf.Value</code> fields are not truly dynamic. They are often legacy JSON blobs carried forward during API migrations. Before reaching for <code>Value</code>, ask whether the payload has a finite, documented structure. If it does, a normal Protobuf message is usually the better long-term design. A dynamic field is often a temporary migration artifact rather than a genuine domain requirement. If the field&rsquo;s shape is stable enough to document, it is usually stable enough to model as a statically typed Protobuf message.</p>
<h2 id="high-performance-alternatives">High-Performance Alternatives</h2>
<p>Importantly, this does not mean runtime Protobuf parsing itself is inherently slow. Rather, the real bottleneck is schema-less, JSON-style polymorphism layered onto Protobuf through <code>Struct</code> and <code>Value</code>.</p>
<p>If your system requires runtime schema flexibility, avoid <code>google.protobuf.Struct</code> for high-throughput paths and leverage these specific optimizations depending on your runtime requirements:</p>
<h3 id="polymorphism-use-googleprotobufany">Polymorphism: Use <code>google.protobuf.Any</code></h3>
<p>When data conforms to a known set of pre-compiled schemas, wrap the fields in an <code>Any</code> message. It records a clean <code>type_url</code> string alongside raw compiled binary bytes.</p>
<ul>
<li><strong>Pros:</strong> Highly compact (212 bytes for a medium payload) and fast. Marshaling is over 35x faster than using generic values.</li>
<li><strong>Cons:</strong> Requires compile-time schema awareness for all incoming types. Crucially, the consuming service must have the exact generated Go types compiled and registered in its binary to cleanly unpack the message (e.g., via <code>anypb.UnmarshalTo()</code>). If the global protobuf registry lacks the specific type matching the incoming <code>type_url</code>, unmarshaling will fail. This strict coupling highlights why <code>Any</code> is a schema-dispatch mechanism, not a drop-in replacement for fully unstructured JSON. Additionally, <code>Any</code> carries the wire overhead of serializing the <code>type_url</code> string (e.g., <code>type.googleapis.com/package.Message</code>), which adds a few dozen bytes depending on your package name length. This explains the size increases for <code>Any</code> visible in the benchmark charts compared to native static Protobuf.</li>
</ul>
<h2 id="recommendations">Recommendations</h2>
<p>To help choose the right design pattern for your dynamic payloads, you can follow this decision flow:</p>
<div class="d2-diagram-wrapper">
    <div class="d2-diagram"
        style="display: block; width: 100%; max-width: 100%; margin-left: auto; margin-right: auto; max-height:100%;"><img src="/d2-diagrams/ba576583e492cd812de6dbf4f21531c460567239868a2062fed9806350708186.svg" alt="D2 Diagram" loading="lazy" style="max-width: 100%; max-height: inherit; width: 100%; height: auto; object-fit: contain; display: block; margin: 0 auto;" /></div>
</div>
<h3 id="model-your-actual-data-in-protobuf">Model your actual data in Protobuf</h3>
<p>Statically defining your schemas is always the ideal path. It yields the best performance, full compile-time type safety, and clear API contracts. Commit to first-class, statically typed fields whenever possible. It&rsquo;s worth it.</p>
<h3 id="use-a-native-mapstring-string-for-flat-attributes">Use a native <code>map&lt;string, string&gt;</code> for flat attributes</h3>
<p>If your metadata is strictly flat key-value strings (like HTTP headers or tags), use a native <code>map&lt;string, string&gt;</code>. It converts cleanly to a native Go map without pointer wrapping or parsing overhead.</p>
<h3 id="use-googleprotobufany-for-middle-layer-opacity">Use <code>google.protobuf.Any</code> for middle-layer opacity</h3>
<p>If you have opaque data that you don&rsquo;t want intermediate routing nodes to parse, wrap the payload in a <code>google.protobuf.Any</code> message. This allows middle layers to forward or store packets without deserialization, while downstream consumers can decode the payload cleanly using pre-compiled schemas.</p>
<h3 id="pack-raw-json-into-strings-opaque-json-packaging">Pack raw JSON into strings (Opaque JSON Packaging)</h3>
<p>If you must support unstructured data on a high-throughput, latency-sensitive hot path, storing the dynamic data as raw JSON directly inside a standard <code>string</code> or <code>bytes</code> Protobuf field is often the best choice.</p>
<p>Let&rsquo;s be honest: this feels gross. Wrapping raw, stringified JSON inside a Protobuf binary envelope violates schema purity, makes API documentation messy, and is generally a dirty hack. Specifically, it breaks schema-first workflows: API tooling (like OpenAPI generators, Buf, or gRPC gateways) that relies on strict contracts cannot introspect the structure of the JSON payload. This forces downstream consumers to rely on out-of-band documentation rather than compile-time safety and automatic code generation, making it a last-resort optimization.</p>
<p>But pragmatism sometimes has to beat purity in high-throughput systems. If you are on a high-throughput, latency-sensitive hot path, the numbers don&rsquo;t care about architectural aesthetics. Bypassing dynamic WKT parsing in favor of opaque JSON packaging saves a lot of CPU cycles and heap allocations. Among the approaches evaluated here, opaque JSON packaging provides the best performance/flexibility tradeoff for fully unstructured payloads. <strong>You may not like it, but this might be what peak performance looks like</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;"><code class="language-protobuf" data-lang="protobuf"><span style="display:flex;"><span><span style="color:#81a1c1;font-weight:bold">message</span> <span style="color:#8fbcbb">EventEnvelope</span> <span style="color:#eceff4">{</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>  <span style="color:#81a1c1">string</span> event_json <span style="color:#81a1c1">=</span> <span style="color:#b48ead">1</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span>  <span style="color:#81a1c1">int64</span> timestamp <span style="color:#81a1c1">=</span> <span style="color:#b48ead">2</span><span style="color:#eceff4">;</span><span style="color:#bf616a">
</span></span></span><span style="display:flex;"><span><span style="color:#bf616a"></span><span style="color:#eceff4">}</span><span style="color:#bf616a">
</span></span></span></code></pre></div><h3 id="use-googleprotobufvalue-for-low-throughput-dynamic-data">Use <code>google.protobuf.Value</code> for low-throughput dynamic data</h3>
<p>If you just want a quick, standardized way to represent arbitrary JSON-like structures in Protobuf and your throughput/latency budgets aren&rsquo;t tight, using the built-in Well-Known Types is completely fine and requires the least custom logic.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Static Protobuf delivers its benefits because the schema is known ahead of time. <code>google.protobuf.Value</code> intentionally gives up that information in exchange for flexibility. In Go, that tradeoff can be surprisingly expensive in both wire size and runtime cost. If your payload has a schema, model it. If it doesn&rsquo;t and performance matters, opaque JSON may outperform dynamic Protobuf despite being less elegant.</p>
]]></content:encoded></item></channel></rss>