{"version":"https://jsonfeed.org/version/1.1","title":"kmcd.dev","home_page_url":"https://kmcd.dev/","feed_url":"https://kmcd.dev/posts/","items":[{"id":"https://kmcd.dev/posts/grpc-webtransport/","url":"https://kmcd.dev/posts/grpc-webtransport/","title":"Let's Talk About Streaming Data on the Web","summary":"A practical look at streaming data in the browser, from response streams and WebSockets to WebTransport over HTTP/3.","date_published":"2026-09-01T10:00:00Z","date_modified":"2026-08-31T09:39:14+02:00","content_html":"\u003cp\u003e“Streaming data” on the web might mean consuming a response as bytes arrive, receiving server events, using a long-lived bidirectional channel, or sharing one connection between independent streams. These workloads have different requirements, but we often reduce the choice to HTTP requests or WebSockets.\u003c/p\u003e\n\u003cp\u003eI want to look at the available options and then experiment with WebTransport. I came at this from gRPC: could WebTransport make client and bidirectional streaming practical in a browser? That question deserves its own follow-up, so I am starting with the transport.\u003c/p\u003e\n\u003ch2 id=\"the-problem\"\u003eThe Problem\u003c/h2\u003e\n\u003cp\u003eThe browser handles server-to-client streaming reasonably well. Things get harder when the client and server both need to send data independently, or when we need several concurrent streams without opening several connections.\u003c/p\u003e\n\u003cp\u003egRPC is what led me here because its client and bidirectional streaming calls are not available through gRPC-Web. It is only one example. Chats, collaborative editors, games, live dashboards, media, and remote control interfaces all eventually run into some version of the same problem: we need data moving in both directions, often across several independent flows, but the browser does not expose a general-purpose TCP or QUIC socket.\u003c/p\u003e\n\u003ch2 id=\"what-we-have-today\"\u003eWhat We Have Today\u003c/h2\u003e\n\u003ch3 id=\"fetch-streams-and-server-sent-events\"\u003eFetch Streams and Server-Sent Events\u003c/h3\u003e\n\u003cp\u003eFor downloads, generated content, event feeds, and other server-to-client flows, streaming \u003ccode\u003efetch()\u003c/code\u003e or Server-Sent Events may be enough. Fetch exposes a \u003ccode\u003eReadableStream\u003c/code\u003e, while Server-Sent Events add an event-oriented channel with reconnection. Both keep ordinary HTTP semantics and remain visible in browser network tools.\u003c/p\u003e\n\u003cdiv class=\"d2-diagram-wrapper\"\u003e\n    \u003cdiv class=\"d2-diagram\"\n        style=\"display: block; width: 100%; max-width: 100%; margin-left: auto; margin-right: auto;max-height: 80vh; width:100%;\"\u003e\u003cimg src=\"/d2-diagrams/56f5f40da0c2a4ab15cc5baac2a6d68e6b73a47d659ff23373a664c21154c5e7.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;\" /\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp\u003eThey are not enough when the client also needs to stream data, or when both sides need to communicate independently over the same call.\u003c/p\u003e\n\u003ch3 id=\"webrtc\"\u003eWebRTC\u003c/h3\u003e\n\u003cp\u003eWebRTC gives us bidirectional media and data channels, but it was designed for communication between peers. Signaling, NAT traversal, and ICE make sense for calls and peer-to-peer applications but are awkward for a normal client-server API. It also sits outside ordinary HTTP infrastructure, making it harder to operate alongside a web backend.\u003c/p\u003e\n\u003ch3 id=\"websockets\"\u003eWebSockets\u003c/h3\u003e\n\u003cp\u003eWebSockets are the best general-purpose option we have today for bidirectional client-server communication in a browser. They are widely supported, well understood, and visible in browser developer tools. If an application needs one long-lived, ordered stream of messages, a WebSocket is often exactly the right answer.\u003c/p\u003e\n\u003cdiv class=\"d2-diagram-wrapper\"\u003e\n    \u003cdiv class=\"d2-diagram\"\n        style=\"display: block; width: 100%; max-width: 100%; margin-left: auto; margin-right: auto;max-height: 80vh; width:100%;\"\u003e\u003cimg src=\"/d2-diagrams/4f408fc87bd2e5ad251964ccfae5817eff0aca53cc72ef3c10fae36ba05c8898.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;\" /\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp\u003eThe problems start when we need several independent streams. We can multiplex all of them onto one WebSocket, but then we have to build a protocol with custom frames and stream IDs, plus routing, lifecycle, cancellation, backpressure, and error isolation on top of a single ordered connection. At that point, we are reimplementing a rough version of HTTP/2 in application code. It is a substantial and complex piece of infrastructure. Because every logical stream ultimately shares one ordered TCP byte stream, packet loss stalls unrelated streams until the missing data is retransmitted.\u003c/p\u003e\n\u003cp\u003eThe other option is to open a separate WebSocket for every stream or RPC. That means another connection and another WebSocket handshake each time, which wastes resources and makes short-lived calls unnecessarily slow.\u003c/p\u003e\n\u003cp\u003eSo WebSockets are still the best thing we have today, but they make us choose between a complicated application-level multiplexer and a pile of separate connections.\u003c/p\u003e\n\u003ch2 id=\"enter-webtransport\"\u003eEnter WebTransport\u003c/h2\u003e\n\u003cp\u003eWebTransport is a browser API for low-latency client-server communication over HTTP/3 and QUIC. Instead of giving us one ordered channel, it gives us a session that can carry multiple independent streams and datagrams:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eA single session can carry multiple unidirectional and bidirectional streams. Data waiting on one stream does not block the others.\u003c/li\u003e\n\u003cli\u003eStreams are reliable and ordered. They work well for chat messages, file transfers, application state, or an RPC that cannot lose data.\u003c/li\u003e\n\u003cli\u003eDatagrams are unreliable and unordered. They work well when old data has little value, such as player positions or individual frames of live audio and video.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"d2-diagram-wrapper\"\u003e\n    \u003cdiv class=\"d2-diagram\"\n        style=\"display: block; width: 100%; max-width: 100%; margin-left: auto; margin-right: auto;max-height: 80vh; width:100%;\"\u003e\u003cimg src=\"/d2-diagrams/2cc3f715981f7d438ac8401f87534c58bf363ceb467b9ef2de9b7c5f09f40a97.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;\" /\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp\u003eThis directly addresses the WebSocket tradeoff. Independent streams share one connection without sharing ordered delivery or backpressure, and we do not have to invent the multiplexing layer ourselves.\u003c/p\u003e\n\u003cp\u003eAt a high level, this is QUIC streams exposed to JavaScript. If HTTP/2 streams already make sense to you, the model should feel familiar. The important difference is that QUIC prevents packet loss on one stream from holding up data on the others.\u003c/p\u003e\n\u003cp\u003eOf course, none of these capabilities help us if browsers do not expose them. Until recently, browser support was the obvious reason not to build around WebTransport.\u003c/p\u003e\n\u003cp\u003eThat is no longer the case. \u003ca href=\"https://webkit.org/blog/17862/webkit-features-for-safari-26-4/\" rel=\"external\"\u003eSafari 26.4 enabled WebTransport\u003c/a\u003e, joining current Chrome, Edge, and Firefox releases. The version number is not a typo: Apple renumbered Safari alongside iOS, iPadOS, and macOS in 2025. The current \u003ca href=\"https://caniuse.com/webtransport\" rel=\"external\"\u003eCan I Use support report\u003c/a\u003e is green across modern major browsers. There are still old and legacy browsers without support, but browser coverage is no longer a reason to avoid testing WebTransport.\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"https://kmcd.dev/posts/grpc-webtransport/caniuse.png\" class=\"spotlight\" data-download=\"true\" aria-label=\"Source: [caniuse.com/webtransport](https://caniuse.com/webtransport)\"\u003e\n    \n    \n    \n    \n        \n            \n            \n                \n            \n            \n        \n    \n\n    \n    \n    \n\n    \n    \n        \n    \n\n    \u003cimg src=\"https://kmcd.dev/posts/grpc-webtransport/caniuse_hu_e1d95ae0faed167b.png\"\n         alt=\"Source: caniuse.com/webtransport\"/\u003e\n    \u003c/a\u003e\u003cfigcaption\u003e\n            \u003cp\u003eSource: \u003ca href=\"https://caniuse.com/webtransport\" rel=\"external\"\u003ecaniuse.com/webtransport\u003c/a\u003e\u003c/p\u003e\n        \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003ch2 id=\"browser-devtools-lack-webtransport-inspection\"\u003eBrowser DevTools Lack WebTransport Inspection\u003c/h2\u003e\n\u003cp\u003eWebTransport works. Debugging it does not. As of July 2026, none of the built-in developer tools in Chrome, Firefox, or Safari provides a WebSocket-style view of WebTransport traffic. You cannot select a session and inspect its streams or datagrams, view individual messages or frames, search their payloads, or download the exchanged application data as a response body.\u003c/p\u003e\n\u003cp\u003eThere is no single response body for DevTools to reveal after the handshake. A WebTransport session can carry multiple independent streams and datagrams for its entire lifetime. A useful inspector needs to understand those primitives, reconstruct the streams, and expose their decrypted application bytes.\u003c/p\u003e\n\u003cp\u003eChrome\u0026rsquo;s \u003ca href=\"https://chromedevtools.github.io/devtools-protocol/tot/Network/\" rel=\"external\"\u003eNetwork domain\u003c/a\u003e emits only \u003ccode\u003ewebTransportCreated\u003c/code\u003e, \u003ccode\u003ewebTransportConnectionEstablished\u003c/code\u003e, and \u003ccode\u003ewebTransportClosed\u003c/code\u003e. These notifications show that a session existed; they do not provide any visibility into what crossed it. There are no corresponding events for streams, datagrams, frames, or payload bytes. Chrome\u0026rsquo;s Network panel provides dedicated message views for \u003ca href=\"https://developer.chrome.com/docs/devtools/network/reference/\" rel=\"external\"\u003eWebSockets and event streams\u003c/a\u003e, while WebTransport has none. The \u003ca href=\"https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/\" rel=\"external\"\u003eFirefox Network Monitor\u003c/a\u003e and \u003ca href=\"https://webkit.org/web-inspector/network-tab/\" rel=\"external\"\u003eSafari Network tab\u003c/a\u003e have the same fundamental limitation: neither provides built-in inspection of WebTransport traffic.\u003c/p\u003e\n\u003cp\u003eThis is not only a concern for native gRPC. Connect and gRPC-Web are more likely starting points for browser RPC work, and they already layer envelopes, Protobuf messages, compression, status, and streaming semantics over HTTP. Moving either protocol onto WebTransport would add session and stream multiplexing beneath those layers while taking away the request and response body visibility developers currently depend on.\u003c/p\u003e\n\u003cp\u003eWithout corresponding browser tooling, diagnosing a single RPC may require correlating application messages, protocol envelopes, WebTransport streams, and server-side transport logs by hand.\u003c/p\u003e\n\u003cp\u003eWhen a connection will not establish, Chrome\u0026rsquo;s \u003ca href=\"https://new.chromium.org/for-testers/providing-network-details/\" rel=\"external\"\u003e\u003ccode\u003echrome://net-export/\u003c/code\u003e\u003c/a\u003e records browser network events. Wireshark or a QUIC-aware proxy can inspect setup when TLS keys are available. These help with connection failures, but decrypted QUIC still leaves us reconstructing streams and decoding the application protocol.\u003c/p\u003e\n\u003cp\u003eWebTransport is still in the draft specification phase, so the recommended implementation may change before it is finalized. That is a reason to be cautious about betting a business on it, but not a reason to avoid experimenting with it.\u003c/p\u003e\n\u003cp\u003eProduction applications also need to account for networks that block UDP, which is still common on corporate and managed networks. The specification includes a draft for \u003ca href=\"https://datatracker.ietf.org/doc/draft-ietf-webtrans-http2/\" rel=\"external\"\u003eWebTransport over HTTP/2\u003c/a\u003e, but browser and server support is much more limited than HTTP/3. It also cannot preserve QUIC\u0026rsquo;s unreliable delivery or stream independence. The Go example in this article only supports HTTP/3, so an application using it needs a fallback to WebSockets, streaming Fetch, or Server-Sent Events.\u003c/p\u003e\n\u003ch2 id=\"what-about-grpc\"\u003eWhat About gRPC?\u003c/h2\u003e\n\u003cp\u003eStandard gRPC supports unary, server-streaming, client-streaming, and bidirectional-streaming calls over HTTP/2. Browsers cannot speak native gRPC, so gRPC-Web translates those calls into requests the browser can make. That translation supports unary and server-streaming calls, but it cannot provide client streaming or bidirectional streaming.\u003c/p\u003e\n\u003cp\u003eThe official \u003ca href=\"https://github.com/grpc/grpc-web/blob/master/doc/streaming-roadmap.md\" rel=\"external\"\u003egRPC-Web streaming roadmap\u003c/a\u003e has discussed this for years, but the project is no longer pursuing major features. Its issue tracking \u003ca href=\"https://github.com/grpc/grpc-web/issues/24\" rel=\"external\"\u003ebidirectional streaming\u003c/a\u003e is closed as \u0026ldquo;not planned.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eConnect-ES has an open \u003ca href=\"https://github.com/connectrpc/connect-es/issues/1106\" rel=\"external\"\u003eWebTransport transport issue\u003c/a\u003e motivated by bringing client and bidirectional streaming to browsers. It has no assignee, milestone, or implementation yet, but the interest is there.\u003c/p\u003e\n\u003cp\u003eWebTransport supplies the bidirectional streams, but not gRPC message framing, metadata, status codes, deadlines, cancellation, or the mapping between RPCs and streams. Those decisions still need an implementation.\u003c/p\u003e\n\u003cp\u003eThe next experiment is to find out how much of gRPC, gRPC-Web, or Connect maps cleanly onto WebTransport without creating an incompatible browser-only protocol.\u003c/p\u003e\n\u003ch2 id=\"trying-webtransport-in-go\"\u003eTrying WebTransport in Go\u003c/h2\u003e\n\u003cp\u003eTo put this all to the test, let\u0026rsquo;s build a simple experiment: a client-server application in Go that uses WebTransport to stream messages. The server will echo whatever the client sends, and the client will send a timestamped message each second. If you\u0026rsquo;ve ever written a small WebSocket app, this will look very familiar.\u003c/p\u003e\n\u003ch3 id=\"the-server\"\u003eThe Server\u003c/h3\u003e\n\u003cp\u003eThe server sets up an HTTP/3 server and upgrades incoming requests on the \u003ccode\u003e/webtransport\u003c/code\u003e endpoint to a WebTransport session. It then enters a loop, accepting new streams and echoing back any messages it receives.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecert\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e tls\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eLoadX509KeyPair\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;localhost.pem\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;localhost-key.pem\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003ereturn\u003c/span\u003e err\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etlsConfig \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003e\u0026amp;\u003c/span\u003etls\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eConfig\u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003eCertificates\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e[]\u003c/span\u003etls\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eCertificate\u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003ecert\u003cspan style=\"color:#eceff4\"\u003e}}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eh3 \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003e\u0026amp;\u003c/span\u003ehttp3\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eServer\u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tAddr\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e            \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;:4433\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tHandler\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e         mux\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tTLSConfig\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e       tlsConfig\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tEnableDatagrams\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003etrue\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eserver \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003e\u0026amp;\u003c/span\u003ewebtransport\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eServer\u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003eH3\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e h3\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ewebtransport\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eConfigureHTTP3Server\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003eh3\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emux\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eHandleFunc\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;/webtransport\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003efunc\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003erw http\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eResponseWriter\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e r \u003cspan style=\"color:#81a1c1\"\u003e*\u003c/span\u003ehttp\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eRequest\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tsession\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e server\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eUpgrade\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003erw\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e r\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\thttp\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eError\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003erw\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eError\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(),\u003c/span\u003e http\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eStatusInternalServerError\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003ereturn\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003ego\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003efunc\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e()\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003efor\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\tstream\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e session\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eAcceptStream\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003econtext\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eBackground\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003ereturn\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003ego\u003c/span\u003e io\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eCopy\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003estream\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e stream\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Echo this stream independently.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#eceff4\"\u003e}()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// The complete sample also starts the HTTPS page server and handles shutdown.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003elog\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFatal\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003eserver\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eListenAndServe\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eNote:\u003c/strong\u003e This code requires local TLS certificates. I install the amazing \u003ca href=\"https://github.com/FiloSottile/mkcert\" rel=\"external\"\u003emkcert CLI\u003c/a\u003e and generate the certificate pair that the sample server loads:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emkcert -install\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emkcert localhost\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRun these commands from the Go sample directory. \u003ccode\u003emkcert\u003c/code\u003e creates \u003ccode\u003elocalhost.pem\u003c/code\u003e and \u003ccode\u003elocalhost-key.pem\u003c/code\u003e, which are loaded by both the Go and Rust servers.\u003c/p\u003e\n\u003ch3 id=\"testing-webtransport-in-chrome-and-firefox\"\u003eTesting WebTransport in Chrome and Firefox\u003c/h3\u003e\n\u003cp\u003eWebTransport can fail even when HTTPS works. Installing the mkcert CA makes the certificate valid for normal HTTPS, but browsers apply additional restrictions to QUIC and HTTP/3.\u003c/p\u003e\n\u003ch4 id=\"firefox\"\u003eFirefox\u003c/h4\u003e\n\u003cp\u003eFor Firefox, open \u003ccode\u003eabout:config\u003c/code\u003e, set \u003ccode\u003enetwork.http.http3.disable_when_third_party_roots_found\u003c/code\u003e to \u003ccode\u003efalse\u003c/code\u003e, and restart Firefox. Firefox otherwise disables HTTP/3 when a user-installed root CA such as mkcert is present. Use a separate development profile because this preference affects the whole profile.\u003c/p\u003e\n\u003ch4 id=\"chrome-edge-and-brave\"\u003eChrome, Edge, and Brave\u003c/h4\u003e\n\u003cp\u003eChrome uses the system trust store, where \u003ccode\u003emkcert -install\u003c/code\u003e installs the local CA. WebTransport over HTTP/3 applies an additional requirement that the certificate be issued by a publicly known root. Chromium-based browsers provide a developer mode that relaxes this additional requirement for local testing:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eOpen \u003ccode\u003echrome://flags/#webtransport-developer-mode\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eSet \u003cstrong\u003eWebTransport Developer Mode\u003c/strong\u003e to \u003cstrong\u003eEnabled\u003c/strong\u003e.\u003c/li\u003e\n\u003cli\u003eRelaunch the browser.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe browser still validates the certificate against the system trust store; this flag allows that trusted root to be a locally installed CA such as mkcert. It is intended only for development. After relaunching, open \u003ccode\u003ehttp://localhost:8080\u003c/code\u003e.\u003c/p\u003e\n\u003ch3 id=\"the-client\"\u003eThe Client\u003c/h3\u003e\n\u003cp\u003eThe Go client needs only a WebTransport session and a bidirectional stream. The complete sample wraps this in signal handling and separate read/write goroutines, but the transport-specific part is small:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003evar\u003c/span\u003e dialer webtransport\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eDialer\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e_\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e session\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e dialer\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eDial\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tctx\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;https://localhost:4433/webtransport\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003ereturn\u003c/span\u003e err\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003edefer\u003c/span\u003e session\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eCloseWithError\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#b48ead\"\u003e0\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;done\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003estream\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e session\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eOpenStreamSync\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003ectx\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003ereturn\u003c/span\u003e err\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e _\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e stream\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eWrite\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e([]\u003c/span\u003e\u003cspan style=\"color:#81a1c1\"\u003ebyte\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;Hello, WebTransport!\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e));\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003ereturn\u003c/span\u003e err\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ereply \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003emake\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e([]\u003c/span\u003e\u003cspan style=\"color:#81a1c1\"\u003ebyte\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#b48ead\"\u003e1024\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003en\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e stream\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eRead\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003ereply\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003ereturn\u003c/span\u003e err\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003elog\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003ePrintf\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;Received: %s\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e reply\u003cspan style=\"color:#eceff4\"\u003e[:\u003c/span\u003en\u003cspan style=\"color:#eceff4\"\u003e])\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe browser side follows the same shape. This example opens three bidirectional streams concurrently and sends one message over each:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003econst\u003c/span\u003e session \u003cspan style=\"color:#81a1c1\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enew\u003c/span\u003e WebTransport\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;https://localhost:4433/webtransport\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eawait\u003c/span\u003e session\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eready\u003cspan style=\"color:#eceff4\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003easync\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003efunction\u003c/span\u003e echo\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003emessage\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003econst\u003c/span\u003e stream \u003cspan style=\"color:#81a1c1\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eawait\u003c/span\u003e session\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003ecreateBidirectionalStream\u003cspan style=\"color:#eceff4\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003econst\u003c/span\u003e writer \u003cspan style=\"color:#81a1c1\"\u003e=\u003c/span\u003e stream\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003ewritable\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003egetWriter\u003cspan style=\"color:#eceff4\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003econst\u003c/span\u003e reader \u003cspan style=\"color:#81a1c1\"\u003e=\u003c/span\u003e stream\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003ereadable\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003egetReader\u003cspan style=\"color:#eceff4\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eawait\u003c/span\u003e writer\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003ewrite\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enew\u003c/span\u003e TextEncoder\u003cspan style=\"color:#eceff4\"\u003e().\u003c/span\u003eencode\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003emessage\u003cspan style=\"color:#eceff4\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003econst\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e value \u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eawait\u003c/span\u003e reader\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eread\u003cspan style=\"color:#eceff4\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enew\u003c/span\u003e TextDecoder\u003cspan style=\"color:#eceff4\"\u003e().\u003c/span\u003edecode\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003evalue\u003cspan style=\"color:#eceff4\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003econst\u003c/span\u003e replies \u003cspan style=\"color:#81a1c1\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eawait\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003ePromise\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eall\u003cspan style=\"color:#eceff4\"\u003e([\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\techo\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;first stream\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\techo\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;second stream\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\techo\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;third stream\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e]);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eEach stream is accepted independently. The sample gives every stream its own goroutine, so if work on the first stream stalls, the second and third can continue. This is the part that would require a custom multiplexer over one WebSocket.\u003c/p\u003e\n\u003cp\u003eThe JavaScript version still looks simpler, although removing every \u003ccode\u003eif err != nil {\u003c/code\u003e line from the Go example does give it a slight head start.\u003c/p\u003e\n\u003ch2 id=\"conclusion\"\u003eConclusion\u003c/h2\u003e\n\u003cp\u003eStreaming \u003ccode\u003efetch()\u003c/code\u003e and Server-Sent Events fit server-to-client data. WebSockets provide a widely deployed bidirectional channel when one ordered stream is enough. WebTransport fits applications that need independent streams, per-stream backpressure, unreliable datagrams, or isolation from cross-stream head-of-line blocking.\u003c/p\u003e\n\u003cp\u003eThe experiment worked: a browser and a Go client connected to the same Go server over HTTP/3, opened independent bidirectional streams, and exchanged data without routing everything through one ordered WebSocket channel. The programming model is surprisingly simple, and the transport provides primitives that would otherwise require a substantial application-level multiplexer.\u003c/p\u003e\n\u003cp\u003eLocal development also exposed an unexpectedly sharp edge: trusting a mkcert CA for HTTPS does not automatically make every browser accept it for WebTransport over QUIC. Firefox and Chromium both require an explicit development setting. That is fine for an experiment, but it is not something I would want to explain in a production onboarding guide.\u003c/p\u003e\n\u003cp\u003eWebTransport is the first browser API that exposes modern transport primitives directly enough to solve the independent-stream problem cleanly. Browser support is here. Tooling still lags behind: DevTools cannot inspect the traffic, local certificates need browser-specific setup, and production deployments still need fallbacks.\u003c/p\u003e\n\u003cp\u003eWebTransport deliberately stops at the transport layer. It does not define an application protocol. A follow-up can take Connect, gRPC-Web, or gRPC framing beyond the transport demo, map an RPC onto WebTransport streams, and test whether the result preserves useful semantics without creating an entirely new protocol in disguise.\u003c/p\u003e\n","tags":["streaming","webtransport","http3","grpc","go"]},{"id":"https://kmcd.dev/posts/ashburn/","url":"https://kmcd.dev/posts/ashburn/","title":"The Gravity of Ashburn, Virginia","summary":"Why a suburb in Northern Virginia rivals the world's largest internet hubs.","date_published":"2026-08-25T10:00:00Z","date_modified":"2026-08-31T09:39:14+02:00","content_html":"\u003cp\u003eWhere is the center of the internet? There is no single correct answer. You could measure submarine cables, network interconnection, cloud capacity, traffic, or the number of people online, and each would produce a different map.\u003c/p\u003e\n\u003cp\u003eBut one result from my 2026 Internet Map caught me completely off guard. When I ranked American metro areas by the amount of publicly routed IPv4 address space geolocated to them, the top result was not New York, Chicago, or Silicon Valley. It was Ashburn, Virginia, a suburb about 30 miles northwest of Washington, D.C. In this article, “Ashburn” refers to the broader Northern Virginia data-center and interconnection cluster centered there.\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"https://kmcd.dev/posts/ashburn/ashburn.jpg\" class=\"spotlight\" data-download=\"true\" aria-label=\"A data center in Ashburn, Virginia; [By Vahurzpu - Own work, CC BY-SA 4.0](https://commons.wikimedia.org/w/index.php?curid=95951368)\"\u003e\n    \n    \n    \n    \n        \n            \n            \n                \n            \n            \n        \n    \n\n    \n    \n    \n\n    \n    \n        \n    \n\n    \u003cimg src=\"https://kmcd.dev/posts/ashburn/ashburn_hu_50c3aafb24c7dfd6.jpg\"\n         alt=\"A data center in Ashburn, Virginia; By Vahurzpu - Own work, CC BY-SA 4.0\"/\u003e\n    \u003c/a\u003e\u003cfigcaption\u003e\n            \u003cp\u003eA data center in Ashburn, Virginia; \u003ca href=\"https://commons.wikimedia.org/w/index.php?curid=95951368\" rel=\"external\"\u003eBy Vahurzpu - Own work, CC BY-SA 4.0\u003c/a\u003e\n                    \u003ca href=\"ashburn.jpg\"\u003e\u003c/a\u003e\u003c/p\u003e\n        \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eRank\u003c/th\u003e\n          \u003cth style=\"text-align: left\"\u003eMetro area\u003c/th\u003e\n          \u003cth style=\"text-align: right\"\u003eIP dominance\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e1\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eAshburn\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: right\"\u003e\u003cstrong\u003e100,934,452\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e2\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eNew York\u003c/td\u003e\n          \u003ctd style=\"text-align: right\"\u003e69,322,932\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e3\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eSeattle\u003c/td\u003e\n          \u003ctd style=\"text-align: right\"\u003e30,696,072\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e4\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eChicago\u003c/td\u003e\n          \u003ctd style=\"text-align: right\"\u003e28,980,879\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e5\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eColumbus\u003c/td\u003e\n          \u003ctd style=\"text-align: right\"\u003e22,291,535\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eAshburn reached roughly 101 million IPv4 addresses, substantially more than New York and more than Seattle and Chicago combined. The ranking was not measuring population or economic activity. It was picking up the enormous concentration of internet infrastructure in Northern Virginia.\u003c/p\u003e\n\n    \n    \n        \n        \n            \n        \n        \u003cimg src=\"https://kmcd.dev/posts/ashburn/center-of-everything_hu_c60c9271e80556db.webp\"\n             alt=\"\" class=\"center\" width=\"500px\"/\u003e\n    \n\n\n\u003ch2 id=\"what-the-number-means\"\u003eWhat the Number Means\u003c/h2\u003e\n\u003cp\u003eIP dominance is the number of publicly routed IPv4 addresses geolocated to a metro area. It is not a count of users, servers, or traffic. An IP address may represent a virtual machine, load balancer, edge node, corporate gateway, or thousands of users behind carrier-grade NAT. Geolocation is also imperfect: prefixes may be registered at an office, announced from several regions, or associated with the nearest recognizable city rather than the exact location of the hardware.\u003c/p\u003e\n\u003cp\u003eIPv6 is excluded because its enormous allocation sizes make address counts mostly reflect allocation policy rather than physical infrastructure. So the exact totals should not be taken literally. But large, persistent concentrations of IPv4 space can still reveal where cloud providers, hosting companies, content networks, and other publicly reachable systems have accumulated.\u003c/p\u003e\n\u003cp\u003eAshburn also has about 32.68 Tbps of public peering capacity in my dataset, measured as the combined port capacity available through public Internet Exchange Points. That places it behind New York and Chicago, but still among the largest interconnection markets in the United States. What makes Ashburn unusual is the combination: a large interconnection market and an enormous concentration of geolocated address space.\u003c/p\u003e\n\u003ch2 id=\"why-ashburn\"\u003eWhy Ashburn?\u003c/h2\u003e\n\u003cp\u003eNorthern Virginia got an early start. MAE-East was one of the first major commercial internet exchange points in the United States. It connected networks around Washington, D.C., before expanding into facilities in Vienna, Reston, and Ashburn. Providers established points of presence nearby, carriers installed fiber, and data-center operators built around that growing network ecosystem.\u003c/p\u003e\n\u003cp\u003eMAE-East eventually declined, but the infrastructure and network presence remained. By then, the region had developed a feedback loop: a network benefits from locating near its carriers, customers, cloud providers, and peers, and every new participant makes the location more useful to the next one. Over time, Ashburn accumulated data centers, exchange fabrics, private connections, cloud regions, and long-haul fiber routes. AWS’s \u003ccode\u003eus-east-1\u003c/code\u003e is the best-known example, but it is only one part of the cluster.\u003c/p\u003e\n\u003cp\u003eVirginia reinforced that growth with available land, access to power, and tax exemptions for qualifying data-center equipment. Ashburn became important because so much of the internet already had a reason to be there.\u003c/p\u003e\n\u003ch2 id=\"the-power-problem\"\u003eThe Power Problem\u003c/h2\u003e\n\u003cp\u003eThat advantage now has a serious limit: electricity. Data centers need enormous amounts of electricity delivered to a specific site through transmission lines and substations. Adding generation elsewhere on the regional grid does not immediately solve a local shortage; the power has to reach the facility on a predictable schedule. In Northern Virginia, data-center construction has often moved faster than the grid could expand, leaving some projects facing multiyear waits for full electrical service.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.pjm.com/-/media/DotCom/library/reports-notices/load-forecast/2026-load-report.pdf\" rel=\"external\"\u003ePJM’s 2026 forecast\u003c/a\u003e shows peak load in the Dominion zone rising by more than 20 gigawatts by the late 2030s. PJM explicitly attributes much of that increase to data-center growth. Transmission corridors, substations, and generation all take years to permit and construct. Land is already expensive, and new infrastructure faces increasing political resistance.\u003c/p\u003e\n\u003cp\u003eFor the first time, Ashburn’s network gravity may be weaker than the pull of power that can actually be delivered elsewhere.\u003c/p\u003e\n\u003ch2 id=\"ai-changes-the-equation\"\u003eAI Changes the Equation\u003c/h2\u003e\n\u003cp\u003eArtificial intelligence makes the power problem more severe. Many existing data centers were designed around rack densities in the low tens of kilowatts, while NVIDIA documents roughly 120 kW of power consumption for a single \u003ca href=\"https://docs.nvidia.com/dgx/dgxgb200-user-guide/hardware.html\" rel=\"external\"\u003eGB200 NVL72 rack\u003c/a\u003e. That much power becomes heat. Conventional air cooling becomes difficult and inefficient, pushing new AI systems toward direct-to-chip liquid cooling and other designs that require different electrical, plumbing, and heat-rejection infrastructure.\u003c/p\u003e\n\u003cp\u003eSome newer Ashburn campuses can support these systems. Older facilities may require major upgrades, and not every building can be economically converted. AI workloads also change the importance of location. Customer-facing services benefit from being close to users, networks, databases, and other cloud systems, but large model-training clusters are different. They still need substantial connectivity, but metro-level latency matters less than power availability and the high-bandwidth network inside the campus.\u003c/p\u003e\n\u003cp\u003eThat gives operators more freedom to build training clusters in places with available electricity, land, and cooling capacity. The likely result is not a complete migration away from Ashburn, but a division of labor: new training clusters can be built elsewhere, latency-sensitive inference remains distributed near users and major network hubs, and Ashburn continues to host cloud services, storage, interconnection, and the systems connecting everything together.\u003c/p\u003e\n\u003ch2 id=\"ashburn-may-be-unbundled\"\u003eAshburn May Be Unbundled\u003c/h2\u003e\n\u003cp\u003eAnother city does not need to recreate Ashburn to weaken its dominance. Ashburn became powerful because connectivity was difficult to reproduce. For the next generation of data centers, power may be the harder resource to secure. Long-haul fiber can be extended to a new campus; delivering hundreds of megawatts of firm power on a predictable schedule is much harder.\u003c/p\u003e\n\u003cp\u003eAshburn’s thousands of existing interconnections cannot simply be packed into trucks and moved elsewhere. It will remain one of the world’s most important places for exchanging traffic.\u003c/p\u003e\n\u003cp\u003eBut the center of interconnection and the center of computation may begin to separate.\u003c/p\u003e\n\u003cp\u003eAshburn may not be replaced by one competing hub.\u003c/p\u003e\n\u003cp\u003eIt may be unbundled.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"https://map.kmcd.dev/?lat=39.0438\u0026amp;lng=-77.4874\u0026amp;z=7.00\u0026amp;year=2026\" rel=\"external\"\u003eExplore Ashburn on the Map »\u003c/a\u003e\u003c/strong\u003e\u003c/p\u003e\n","tags":["ashburn","virginia","data-centers","infrastructure","peering","aws","latency"]},{"id":"https://kmcd.dev/posts/protobuf-varint-vs-fixed/","url":"https://kmcd.dev/posts/protobuf-varint-vs-fixed/","title":"The CPU Cost of Protobuf Varints in Go","summary":"Do fixed-size integers serialize faster than varints? We benchmark the CPU overhead of continuation-bit parsing using Go, vtprotobuf, and hyperpb.","date_published":"2026-08-04T10:00:00Z","date_modified":"2026-08-31T09:39:14+02:00","content_html":"\u003cp\u003eWhen you define an integer field in a Protocol Buffers schema, \u003ccode\u003eint64\u003c/code\u003e is a common default. Varint encoding compresses small numbers into a byte or two, keeping network payloads lean.\u003c/p\u003e\n\u003cp\u003eHowever, that compression comes at the cost of CPU cycles. To read or write a varint, the CPU must process the value byte by byte, checking continuation bits and shifting payloads. When a field sits in a high-throughput backend service, CPU efficiency often matters far more than saving a few wire bytes.\u003c/p\u003e\n\u003cp\u003eProtobuf also provides fixed-size integers (\u003ccode\u003efixed32\u003c/code\u003e, \u003ccode\u003efixed64\u003c/code\u003e, \u003ccode\u003esfixed32\u003c/code\u003e, \u003ccode\u003esfixed64\u003c/code\u003e), which use a constant-width, little-endian format. While taking more bytes for small values, their CPU path is dramatically simpler.\u003c/p\u003e\n\u003cp\u003eIn Go benchmarks across standard \u003ccode\u003egoogle.golang.org/protobuf\u003c/code\u003e, PlanetScale \u003ccode\u003evtprotobuf\u003c/code\u003e, and \u003ccode\u003ehyperpb\u003c/code\u003e, fixed-size integers prove up to 4.5x faster to encode and decode for packed 64-bit arrays—especially when values are large or negative. Here is how wire formats, CPU overhead, and runtime implementations interact in practice.\u003c/p\u003e\n\u003ch2 id=\"how-the-wire-formats-actually-differ\"\u003eHow the Wire Formats Actually Differ\u003c/h2\u003e\n\u003cp\u003eProtobuf integer types divide into three encoding groups:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eStandard varints: \u003ccode\u003eint32\u003c/code\u003e, \u003ccode\u003eint64\u003c/code\u003e, \u003ccode\u003euint32\u003c/code\u003e, and \u003ccode\u003euint64\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eZigZag varints: \u003ccode\u003esint32\u003c/code\u003e and \u003ccode\u003esint64\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eFixed-size integers: \u003ccode\u003efixed32\u003c/code\u003e, \u003ccode\u003efixed64\u003c/code\u003e, \u003ccode\u003esfixed32\u003c/code\u003e, and \u003ccode\u003esfixed64\u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"standard-varints-int32--int64\"\u003eStandard Varints (\u003ccode\u003eint32\u003c/code\u003e / \u003ccode\u003eint64\u003c/code\u003e)\u003c/h3\u003e\n\u003cp\u003eVarints use protobuf\u0026rsquo;s \u003ca href=\"https://protobuf.dev/programming-guides/encoding/#varints\" rel=\"external\"\u003eBase 128 Varint\u003c/a\u003e format. Each byte reserves its MSB as a continuation flag, leaving 7 bits for payload:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eSmall numbers (\u003ccode\u003e\u0026lt; 128\u003c/code\u003e) fit in 1 byte.\u003c/li\u003e\n\u003cli\u003eLarger numbers require up to 10 bytes for 64-bit integers.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003efor\u003c/span\u003e v \u003cspan style=\"color:#81a1c1\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan style=\"color:#b48ead\"\u003e1\u003c/span\u003e\u003cspan style=\"color:#81a1c1\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e\u003cspan style=\"color:#b48ead\"\u003e7\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    buf\u003cspan style=\"color:#eceff4\"\u003e[\u003c/span\u003eidx\u003cspan style=\"color:#eceff4\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003ebyte\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003ev\u003cspan style=\"color:#81a1c1\"\u003e\u0026amp;\u003c/span\u003e\u003cspan style=\"color:#b48ead\"\u003e0x7f\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e|\u003c/span\u003e \u003cspan style=\"color:#b48ead\"\u003e0x80\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    v \u003cspan style=\"color:#81a1c1\"\u003e\u0026gt;\u0026gt;=\u003c/span\u003e \u003cspan style=\"color:#b48ead\"\u003e7\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    idx\u003cspan style=\"color:#81a1c1\"\u003e++\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ebuf\u003cspan style=\"color:#eceff4\"\u003e[\u003c/span\u003eidx\u003cspan style=\"color:#eceff4\"\u003e]\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003ebyte\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003ev\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe decoder reverses this bit by bit. While negligible for scalars, this loop adds noticeable overhead over millions of elements in hot paths.\u003c/p\u003e\n\u003cp\u003eNegative numbers are particularly penalizing: two\u0026rsquo;s-complement representation sets bit 63. Because Base 128 packs only 7 bits per byte, negative integers encoded as standard \u003ccode\u003eint32\u003c/code\u003e/\u003ccode\u003eint64\u003c/code\u003e force the maximum 10-byte encoding every single time—maximizing both wire size and CPU decoding cycles simultaneously.\u003c/p\u003e\n\u003ch3 id=\"zigzag-varints-sint32--sint64\"\u003eZigZag Varints (\u003ccode\u003esint32\u003c/code\u003e / \u003ccode\u003esint64\u003c/code\u003e)\u003c/h3\u003e\n\u003cp\u003eZigZag encoding solves this penalty by mapping signed integers to unsigned values (\u003ccode\u003e0 -\u0026gt; 0\u003c/code\u003e, \u003ccode\u003e-1 -\u0026gt; 1\u003c/code\u003e, \u003ccode\u003e1 -\u0026gt; 2\u003c/code\u003e, \u003ccode\u003e-2 -\u0026gt; 3\u003c/code\u003e), keeping small absolute values small on the wire.\u003c/p\u003e\n\u003cp\u003eHowever, ZigZag only solves payload bloat, not CPU cost. The parser still runs the varint continuation loop for every byte.\u003c/p\u003e\n\u003ch3 id=\"fixed-size-integers-fixed--sfixed\"\u003eFixed-Size Integers (\u003ccode\u003efixed\u003c/code\u003e / \u003ccode\u003esfixed\u003c/code\u003e)\u003c/h3\u003e\n\u003cp\u003eFixed-size integers skip small-value compression entirely:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003efixed32\u003c/code\u003e / \u003ccode\u003esfixed32\u003c/code\u003e: 4 bytes\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003efixed64\u003c/code\u003e / \u003ccode\u003esfixed64\u003c/code\u003e: 8 bytes\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eRepresented as raw little-endian values, the parser reads them directly without continuation checks or bit assembly. In Go schemas, \u003ccode\u003efixed32\u003c/code\u003e/\u003ccode\u003efixed64\u003c/code\u003e map to \u003ccode\u003euint32\u003c/code\u003e/\u003ccode\u003euint64\u003c/code\u003e, while \u003ccode\u003esfixed32\u003c/code\u003e/\u003ccode\u003esfixed64\u003c/code\u003e map to \u003ccode\u003eint32\u003c/code\u003e/\u003ccode\u003eint64\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"the-benchmark-setup\"\u003eThe Benchmark Setup\u003c/h2\u003e\n\u003cp\u003eTo measure the practical difference in Go, I set up a test module with schemas containing packed repeated integer fields. Each test message holds 1,000 elements.\u003c/p\u003e\n\u003cp\u003eI benchmarked three value distributions:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eSmall Positive\u003c/strong\u003e: integers in the range \u003ccode\u003e[0, 99]\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLarge Positive\u003c/strong\u003e: integers in the range \u003ccode\u003e[2^50, 2^50 + 999]\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNegative\u003c/strong\u003e: integers in the range \u003ccode\u003e[-100, -1]\u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe tests evaluate three Go parsing implementations:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eThe standard \u003ccode\u003egoogle.golang.org/protobuf\u003c/code\u003e runtime using \u003ccode\u003eproto.Marshal\u003c/code\u003e and \u003ccode\u003eproto.Unmarshal\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eGenerated marshal and unmarshal code from PlanetScale\u0026rsquo;s \u003ca href=\"https://github.com/planetscale/vtprotobuf\" rel=\"external\"\u003e\u003ccode\u003evtprotobuf\u003c/code\u003e\u003c/a\u003e plugin\u003c/li\u003e\n\u003cli\u003eDescriptor-based dynamic parsing using \u003ca href=\"https://github.com/bufbuild/hyperpb\" rel=\"external\"\u003e\u003ccode\u003ehyperpb\u003c/code\u003e\u003c/a\u003e with a reusable \u003ccode\u003ehyperpb.Shared\u003c/code\u003e memory arena\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eNote that \u003ccode\u003ehyperpb\u003c/code\u003e is not a direct drop-in replacement for standard struct unmarshaling. It evaluates how a specialized dynamic parser with zero-allocation memory arenas handles the wire formats, highlighting how parser architecture interacts with payload size.\u003c/p\u003e\n\u003cp\u003eAll tests ran on an Apple M1 Pro (\u003ccode\u003edarwin/arm64\u003c/code\u003e) using Go 1.26.3. Averages represent 5 independent runs of 5 seconds each:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ego \u003cspan style=\"color:#81a1c1\"\u003etest\u003c/span\u003e -bench\u003cspan style=\"color:#81a1c1\"\u003e=\u003c/span\u003e. -benchmem -benchtime\u003cspan style=\"color:#81a1c1\"\u003e=\u003c/span\u003e5s -count\u003cspan style=\"color:#81a1c1\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#b48ead\"\u003e5\u003c/span\u003e \u0026gt; results.txt\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBecause these benchmark messages use packed repeated fields, each serialized payload consists of a single field tag, a length prefix, and the concatenated binary values. This structure amortizes the tag overhead across all 1,000 elements, isolating the actual cost of the integer serialization.\u003c/p\u003e\n\u003ch3 id=\"wire-size-comparison\"\u003eWire Size Comparison\u003c/h3\u003e\n\u003cp\u003eBefore examining CPU timing, look at the serialized payload sizes for 1,000 integers:\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eInteger Type\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eSmall Positive\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eLarge Positive\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eNegative\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64\u003c/code\u003e (Varint)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1,003 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,003 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e10,003 B\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64\u003c/code\u003e (ZigZag Varint)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,363 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,003 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1,363 B\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64\u003c/code\u003e (Fixed-Size)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,003 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e8,003 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,003 B\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cdiv class=\"tip-box\"\u003e\n  ZigZag (\u003ccode\u003esint64\u003c/code\u003e) is slightly larger than plain \u003ccode\u003eint64\u003c/code\u003e for small positive numbers because the bitwise mapping shifts positive values upward. Numbers above 63 cross into 2-byte varint territory sooner. For negative numbers, however, ZigZag reduces payload size by over 86%.\n\u003c/div\u003e\n\u003cp\u003eThe size trade-off is substantial. For small positive integers, varints are an order of magnitude smaller than fixed-size integers. For large numbers, the byte-saving advantage disappears entirely since a 64-bit varint at \u003ccode\u003e2^50\u003c/code\u003e requires 8 bytes anyway. For negative numbers, plain \u003ccode\u003eint64\u003c/code\u003e expands to 10 bytes per value, making it both larger and more complex to parse than \u003ccode\u003esfixed64\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"benchmark-results\"\u003eBenchmark Results\u003c/h2\u003e\n\u003ch3 id=\"marshaling\"\u003eMarshaling\u003c/h3\u003e\n\u003cp\u003eSerialization benchmarks measure the cost of converting Go structs into protobuf wire data.\u003c/p\u003e\n\u003cdiv class=\"tabs-wrapper\" id=\"tabs-117\"\u003e\n  \u003cdiv class=\"tabs-nav\" role=\"tablist\"\u003e\u003cbutton class=\"tab-btn active\" \n              role=\"tab\" \n              aria-selected=\"true\" \n              aria-controls=\"panel-small-positive-76152\" \n              id=\"tab-btn-small-positive-76152\"\n              data-tab=\"small-positive-76152\"\u003e\n        Small Positive\n      \u003c/button\u003e\u003cbutton class=\"tab-btn\" \n              role=\"tab\" \n              aria-selected=\"false\" \n              aria-controls=\"panel-large-positive-62528\" \n              id=\"tab-btn-large-positive-62528\"\n              data-tab=\"large-positive-62528\"\u003e\n        Large Positive\n      \u003c/button\u003e\u003cbutton class=\"tab-btn\" \n              role=\"tab\" \n              aria-selected=\"false\" \n              aria-controls=\"panel-negative-95123\" \n              id=\"tab-btn-negative-95123\"\n              data-tab=\"negative-95123\"\u003e\n        Negative\n      \u003c/button\u003e\u003c/div\u003e\n  \u003cdiv class=\"tabs-panels\"\u003e\n\u003cdiv class=\"tab-panel-content\" id=\"panel-small-positive-76152\" role=\"tabpanel\" style=\"display: none;\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"5c1e356e3a340a64\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('5c1e356e3a340a64');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"sint64 (ZigZag)\",\n      \"int64 (Varint)\",\n      \"sint64 (ZigZag) + vtproto\",\n      \"int64 (Varint) + vtproto\",\n      \"sfixed64 (Fixed) + vtproto\",\n      \"sfixed64 (Fixed)\"\n    ],\n    \"datasets\": [\n      {\n        \"label\": \"ns/op\",\n        \"data\": [\n          4504,\n          3743,\n          3525,\n          2851,\n          2214,\n          1768\n        ],\n        \"backgroundColor\": [\n          \"rgba(75, 192, 192, 0.85)\",\n          \"rgba(54, 162, 235, 0.85)\",\n          \"rgba(75, 192, 192, 0.45)\",\n          \"rgba(54, 162, 235, 0.45)\",\n          \"rgba(153, 102, 255, 0.45)\",\n          \"rgba(153, 102, 255, 0.85)\"\n        ],\n        \"borderWidth\": 0\n      }\n    ]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Marshal 64-bit (Small Positive): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"display\": false\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      },\n      \"y\": {\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eBenchmark (1000 Small Positives)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1,768 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,214 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,851 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,024 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3,525 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,408 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3,743 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,024 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e4,504 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,408 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n\u003cdiv class=\"tab-panel-content\" id=\"panel-large-positive-62528\" role=\"tabpanel\" style=\"display: none;\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"9d4306e1f6e56288\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('9d4306e1f6e56288');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"sint64 (ZigZag)\",\n      \"int64 (Varint)\",\n      \"sint64 (ZigZag) + vtproto\",\n      \"int64 (Varint) + vtproto\",\n      \"sfixed64 (Fixed) + vtproto\",\n      \"sfixed64 (Fixed)\"\n    ],\n    \"datasets\": [\n      {\n        \"label\": \"ns/op\",\n        \"data\": [\n          7487,\n          7014,\n          7036,\n          6725,\n          2224,\n          1727\n        ],\n        \"backgroundColor\": [\n          \"rgba(75, 192, 192, 0.85)\",\n          \"rgba(54, 162, 235, 0.85)\",\n          \"rgba(75, 192, 192, 0.45)\",\n          \"rgba(54, 162, 235, 0.45)\",\n          \"rgba(153, 102, 255, 0.45)\",\n          \"rgba(153, 102, 255, 0.85)\"\n        ],\n        \"borderWidth\": 0\n      }\n    ]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Marshal 64-bit (Large Positive): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"display\": false\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      },\n      \"y\": {\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eBenchmark (1000 Large Positives)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1,727 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,224 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e6,725 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e7,014 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e7,036 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e7,487 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n\u003cdiv class=\"tab-panel-content\" id=\"panel-negative-95123\" role=\"tabpanel\" style=\"display: none;\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"e11ef9e1afed3054\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('e11ef9e1afed3054');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"int64 (Varint)\",\n      \"int64 (Varint) + vtproto\",\n      \"sint64 (ZigZag)\",\n      \"sint64 (ZigZag) + vtproto\",\n      \"sfixed64 (Fixed) + vtproto\",\n      \"sfixed64 (Fixed)\"\n    ],\n    \"datasets\": [\n      {\n        \"label\": \"ns/op\",\n        \"data\": [\n          7609,\n          7345,\n          4488,\n          3478,\n          2175,\n          1716\n        ],\n        \"backgroundColor\": [\n          \"rgba(54, 162, 235, 0.85)\",\n          \"rgba(54, 162, 235, 0.45)\",\n          \"rgba(75, 192, 192, 0.85)\",\n          \"rgba(75, 192, 192, 0.45)\",\n          \"rgba(153, 102, 255, 0.45)\",\n          \"rgba(153, 102, 255, 0.85)\"\n        ],\n        \"borderWidth\": 0\n      }\n    ]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Marshal 64-bit (Negative): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"display\": false\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      },\n      \"y\": {\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eBenchmark (1000 Negatives)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1,716 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,175 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3,478 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,408 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e4,488 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,408 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e7,345 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e10,240 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e7,609 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e10,240 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n\u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"unmarshaling\"\u003eUnmarshaling\u003c/h3\u003e\n\u003cp\u003eDeserialization benchmarks measure the CPU cost of parsing wire data back into allocated Go structs.\u003c/p\u003e\n\u003cdiv class=\"tabs-wrapper\" id=\"tabs-62464\"\u003e\n  \u003cdiv class=\"tabs-nav\" role=\"tablist\"\u003e\u003cbutton class=\"tab-btn active\" \n              role=\"tab\" \n              aria-selected=\"true\" \n              aria-controls=\"panel-small-positive-67741\" \n              id=\"tab-btn-small-positive-67741\"\n              data-tab=\"small-positive-67741\"\u003e\n        Small Positive\n      \u003c/button\u003e\u003cbutton class=\"tab-btn\" \n              role=\"tab\" \n              aria-selected=\"false\" \n              aria-controls=\"panel-large-positive-28262\" \n              id=\"tab-btn-large-positive-28262\"\n              data-tab=\"large-positive-28262\"\u003e\n        Large Positive\n      \u003c/button\u003e\u003cbutton class=\"tab-btn\" \n              role=\"tab\" \n              aria-selected=\"false\" \n              aria-controls=\"panel-negative-96270\" \n              id=\"tab-btn-negative-96270\"\n              data-tab=\"negative-96270\"\u003e\n        Negative\n      \u003c/button\u003e\u003c/div\u003e\n  \u003cdiv class=\"tabs-panels\"\u003e\n\u003cdiv class=\"tab-panel-content\" id=\"panel-small-positive-67741\" role=\"tabpanel\" style=\"display: none;\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"e37bb136a3ef00f2\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('e37bb136a3ef00f2');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"sint64 (ZigZag) + vtproto\",\n      \"int64 (Varint) + vtproto\",\n      \"sfixed64 (Fixed) + vtproto\",\n      \"sint64 (ZigZag)\",\n      \"int64 (Varint)\",\n      \"sfixed64 (Fixed)\",\n      \"sint64 (ZigZag) + hyperpb Shared\",\n      \"sfixed64 (Fixed) + hyperpb Shared\",\n      \"int64 (Varint) + hyperpb Shared\"\n    ],\n    \"datasets\": [\n      {\n        \"label\": \"ns/op\",\n        \"data\": [\n          3603,\n          3759,\n          3156,\n          2974,\n          2538,\n          2366,\n          1828,\n          1430,\n          478\n        ],\n        \"backgroundColor\": [\n          \"rgba(75, 192, 192, 0.45)\",\n          \"rgba(54, 162, 235, 0.45)\",\n          \"rgba(153, 102, 255, 0.45)\",\n          \"rgba(75, 192, 192, 0.85)\",\n          \"rgba(54, 162, 235, 0.85)\",\n          \"rgba(153, 102, 255, 0.85)\",\n          \"rgba(75, 192, 192, 0.30)\",\n          \"rgba(153, 102, 255, 0.30)\",\n          \"rgba(54, 162, 235, 0.30)\"\n        ],\n        \"borderWidth\": 0\n      }\n    ]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Unmarshal 64-bit (Small Positive): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"display\": false\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      },\n      \"y\": {\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eBenchmark (1000 Small Positives)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint) + hyperpb Shared\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e478 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,560 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed) + hyperpb Shared\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,430 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e10,419 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag) + hyperpb Shared\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,828 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,066 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,366 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,256 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,538 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,256 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,974 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,256 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3,156 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3,603 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3,759 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n\u003cdiv class=\"tab-panel-content\" id=\"panel-large-positive-28262\" role=\"tabpanel\" style=\"display: none;\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"5b3e82ab5c4c1e6a\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('5b3e82ab5c4c1e6a');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"int64 (Varint) + vtproto\",\n      \"sint64 (ZigZag) + vtproto\",\n      \"sint64 (ZigZag)\",\n      \"int64 (Varint)\",\n      \"sint64 (ZigZag) + hyperpb Shared\",\n      \"int64 (Varint) + hyperpb Shared\",\n      \"sfixed64 (Fixed) + vtproto\",\n      \"sfixed64 (Fixed)\",\n      \"sfixed64 (Fixed) + hyperpb Shared\"\n    ],\n    \"datasets\": [\n      {\n        \"label\": \"ns/op\",\n        \"data\": [\n          11800,\n          9201,\n          8466,\n          8383,\n          6526,\n          6249,\n          3247,\n          2505,\n          1465\n        ],\n        \"backgroundColor\": [\n          \"rgba(54, 162, 235, 0.45)\",\n          \"rgba(75, 192, 192, 0.45)\",\n          \"rgba(75, 192, 192, 0.85)\",\n          \"rgba(54, 162, 235, 0.85)\",\n          \"rgba(75, 192, 192, 0.30)\",\n          \"rgba(54, 162, 235, 0.30)\",\n          \"rgba(153, 102, 255, 0.45)\",\n          \"rgba(153, 102, 255, 0.85)\",\n          \"rgba(153, 102, 255, 0.30)\"\n        ],\n        \"borderWidth\": 0\n      }\n    ]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Unmarshal 64-bit (Large Positive): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"display\": false\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      },\n      \"y\": {\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eBenchmark (1000 Large Positives)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed) + hyperpb Shared\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1,465 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e10,416 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,505 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,256 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3,247 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint) + hyperpb Shared\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e6,249 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e10,302 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag) + hyperpb Shared\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e6,526 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e10,302 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,383 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,256 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,466 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,256 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e9,201 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e11,800 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n\u003cdiv class=\"tab-panel-content\" id=\"panel-negative-96270\" role=\"tabpanel\" style=\"display: none;\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"d03551356ea1b9c4\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('d03551356ea1b9c4');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"int64 (Varint) + vtproto\",\n      \"int64 (Varint)\",\n      \"int64 (Varint) + hyperpb Shared\",\n      \"sint64 (ZigZag) + vtproto\",\n      \"sfixed64 (Fixed) + vtproto\",\n      \"sint64 (ZigZag)\",\n      \"sfixed64 (Fixed)\",\n      \"sint64 (ZigZag) + hyperpb Shared\",\n      \"sfixed64 (Fixed) + hyperpb Shared\"\n    ],\n    \"datasets\": [\n      {\n        \"label\": \"ns/op\",\n        \"data\": [\n          14518,\n          9819,\n          7445,\n          3650,\n          3230,\n          3062,\n          2465,\n          1854,\n          1473\n        ],\n        \"backgroundColor\": [\n          \"rgba(54, 162, 235, 0.45)\",\n          \"rgba(54, 162, 235, 0.85)\",\n          \"rgba(54, 162, 235, 0.30)\",\n          \"rgba(75, 192, 192, 0.45)\",\n          \"rgba(153, 102, 255, 0.45)\",\n          \"rgba(75, 192, 192, 0.85)\",\n          \"rgba(153, 102, 255, 0.85)\",\n          \"rgba(75, 192, 192, 0.30)\",\n          \"rgba(153, 102, 255, 0.30)\"\n        ],\n        \"borderWidth\": 0\n      }\n    ]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Unmarshal 64-bit (Negative): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"display\": false\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      },\n      \"y\": {\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eBenchmark (1000 Negatives)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed) + hyperpb Shared\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1,473 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e10,419 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag) + hyperpb Shared\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,854 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,067 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,465 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,256 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3,062 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,256 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esfixed64 (Fixed) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3,230 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003esint64 (ZigZag) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3,650 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint) + hyperpb Shared\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e7,445 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e13,654 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint)\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e9,819 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,256 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003e\u003ccode\u003eint64 (Varint) + vtproto\u003c/code\u003e\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e14,518 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8,192 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n\u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch2 id=\"analyzing-the-numbers\"\u003eAnalyzing the Numbers\u003c/h2\u003e\n\u003ch3 id=\"1-value-distribution-impacts-performance\"\u003e1. Value Distribution Impacts Performance\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSmall Positive:\u003c/strong\u003e Varints shine on wire efficiency (1,003 B vs 8,003 B). Yet even with 8x larger payloads, \u003ccode\u003esfixed64\u003c/code\u003e marshals faster in Go by skipping continuation loops. Standard unmarshaling is neck-and-neck (\u003ccode\u003esfixed64\u003c/code\u003e at 2,366 ns vs \u003ccode\u003eint64\u003c/code\u003e at 2,538 ns). \u003ccode\u003ehyperpb.Shared\u003c/code\u003e leverages the compact varint payload best, reaching 478 ns via specialized arena parsing.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLarge Positive:\u003c/strong\u003e At \u003ccode\u003e2^50\u003c/code\u003e, varints take 8 bytes, which matches \u003ccode\u003esfixed64\u003c/code\u003e payload size. Without size savings, varint decoding overhead shows its cost: \u003ccode\u003eint64\u003c/code\u003e unmarshaling takes 8,383 ns in standard Go runtime vs 2,505 ns for \u003ccode\u003esfixed64\u003c/code\u003e (a 3.3x speedup).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNegative:\u003c/strong\u003e Plain \u003ccode\u003eint64\u003c/code\u003e expands to 10 bytes per value (9,819 ns unmarshal). \u003ccode\u003esint64\u003c/code\u003e (ZigZag) shrinks wire size back to 1,363 B (3,062 ns unmarshal). \u003ccode\u003esfixed64\u003c/code\u003e still beats ZigZag at 2,465 ns because flat memory copies beat bit-shifting loops.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"2-why-standard-go-runtime-beat-generated-code-on-scalar-slices\"\u003e2. Why Standard Go Runtime Beat Generated Code on Scalar Slices\u003c/h3\u003e\n\u003cp\u003ePlanetScale\u0026rsquo;s \u003ccode\u003evtprotobuf\u003c/code\u003e generated code was unexpectedly slower than standard \u003ccode\u003egoogle.golang.org/protobuf\u003c/code\u003e when unmarshaling scalar slices (e.g. \u003ccode\u003eint64 (Varint) + vtproto\u003c/code\u003e at 14,518 ns vs standard runtime at 9,819 ns for negative numbers).\u003c/p\u003e\n\u003cp\u003eWhile \u003ccode\u003evtprotobuf\u003c/code\u003e eliminates reflection overhead on struct fields, packed repeated fixed-width fields are continuous byte blocks.\u003c/p\u003e\n\u003cp\u003eThe standard Go runtime hits an optimized fast path: it reads total length, allocates the destination slice at once, and copies raw bytes into memory using \u003ccode\u003ememmove\u003c/code\u003e primitives.\u003c/p\u003e\n\u003cp\u003eIn contrast, \u003ccode\u003evtprotobuf\u003c/code\u003e generates an explicit Go loop:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003efor\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003elen\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003eb\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#b48ead\"\u003e0\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    v \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e binary\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eLittleEndian\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eUint64\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003eb\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    b \u003cspan style=\"color:#eceff4\"\u003e=\u003c/span\u003e b\u003cspan style=\"color:#eceff4\"\u003e[\u003c/span\u003e\u003cspan style=\"color:#b48ead\"\u003e8\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    list \u003cspan style=\"color:#eceff4\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003eappend\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003elist\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003eint64\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003ev\u003cspan style=\"color:#eceff4\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIn CPU-bound array parsing, an explicit Go element-by-element loop cannot compete with bulk memory block copying. Generated code does not automatically beat runtime primitives for bulk primitive data.\u003c/p\u003e\n\u003ch2 id=\"choosing-the-right-integer-type\"\u003eChoosing the Right Integer Type\u003c/h2\u003e\n\u003cp\u003eProtobuf schema decisions map directly to your data distribution and service architecture:\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eType\u003c/th\u003e\n          \u003cth style=\"text-align: left\"\u003eBest for\u003c/th\u003e\n          \u003cth style=\"text-align: left\"\u003eAvoid when\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003eint64\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eSmall non-negative values where wire size matters\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eValues may be negative or large in hot paths\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003esint64\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eSmall signed values where wire size matters\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eHot repeated fields where CPU dominates\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003ccode\u003efixed64\u003c/code\u003e / \u003ccode\u003esfixed64\u003c/code\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eHot, repeated, CPU-bound fields\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eSmall values in bandwidth-sensitive APIs\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eUse these rules to guide your schema definitions:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eUse \u003ccode\u003efixed64\u003c/code\u003e / \u003ccode\u003esfixed64\u003c/code\u003e\u003c/strong\u003e for hot, repeated, or CPU-bound fields such as database IDs, timestamps, byte offsets, coordinate arrays, or high-range metrics counters.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eUse \u003ccode\u003esint64\u003c/code\u003e\u003c/strong\u003e for signed values that frequently hover near zero, especially when network bandwidth or storage footprint is your primary constraint.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eUse plain \u003ccode\u003eint64\u003c/code\u003e\u003c/strong\u003e only when values are strictly non-negative, typically small, and not residing in a serialization hotspot.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eChanging an existing field from \u003ccode\u003eint64\u003c/code\u003e to \u003ccode\u003efixed64\u003c/code\u003e is a breaking wire-format change. Standard varints use wire type \u003ccode\u003e0\u003c/code\u003e (\u003ccode\u003eVARINT\u003c/code\u003e), whereas 64-bit fixed integers use wire type \u003ccode\u003e1\u003c/code\u003e (\u003ccode\u003eI64\u003c/code\u003e). You cannot swap integer types in place without coordinating producer and consumer migrations.\u003c/p\u003e\n\u003cp\u003eWhen you design a schema from scratch for high-throughput internal microservices, do not rely on \u003ccode\u003eint64\u003c/code\u003e out of habit. Evaluating your numerical distributions and reaching for \u003ccode\u003efixed64\u003c/code\u003e or \u003ccode\u003esfixed64\u003c/code\u003e is a straightforward way to trade away a few cheap network bytes for predictable, measurable CPU savings.\u003c/p\u003e\n","tags":["protobuf","go","performance","software-architecture"]},{"id":"https://kmcd.dev/posts/connectrpc-http-query/","url":"https://kmcd.dev/posts/connectrpc-http-query/","title":"It's Time for ConnectRPC to Adopt HTTP QUERY","summary":"","date_published":"2026-07-28T10:00:00Z","date_modified":"2026-08-31T09:39:14+02:00","content_html":"\u003cp\u003eHTTP QUERY is now standardized in \u003ca href=\"https://datatracker.ietf.org/doc/html/rfc10008\" rel=\"external\"\u003eRFC 10008\u003c/a\u003e. For backend engineers working with schema-first APIs, this provides a clean solution for a problem we have been working around for years: cacheable requests that require complex, structured input.\u003c/p\u003e\n\u003cp\u003eThat matters for a protocol like ConnectRPC. Connect already tries to map RPCs onto ordinary HTTP instead of fighting the grain of the web. It supports HTTP GET for side-effect-free unary RPCs, which is great for caching, but GET forces structured request payloads into query parameters.\u003c/p\u003e\n\u003cp\u003eConnectRPC should support QUERY as the body-carrying counterpart to GET for those same side-effect-free unary calls. Caching support across the broader web can mature over time, but the protocol shape is already useful today: structured request data belongs in the request body, not squeezed into a URL.\u003c/p\u003e\n\u003ch3 id=\"why-connect-got-it-right\"\u003eWhy Connect Got It Right\u003c/h3\u003e\n\u003cp\u003eOne of the best design decisions in the Connect protocol is that it leans into standard HTTP semantics. Unlike traditional gRPC, which demands HTTP/2 and relies heavily on trailing headers, Connect maps naturally onto the HTTP infrastructure most teams already run.\u003c/p\u003e\n\u003cp\u003eThat pays off operationally:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMeaningful HTTP Status Codes:\u003c/strong\u003e Errors map directly to standard HTTP statuses, allowing metrics to work without a specialized gRPC proxy.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eStandard Compression:\u003c/strong\u003e Traffic relies on standard \u003ccode\u003eContent-Encoding\u003c/code\u003e headers (like gzip or brotli) already built into your infrastructure.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNo Trailers Required:\u003c/strong\u003e Requests pass cleanly through standard load balancers, firewalls, and HTTP/1.1 proxies without requiring end-to-end HTTP/2.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNative Ecosystem Integration:\u003c/strong\u003e The protocol plugs directly into Go\u0026rsquo;s standard \u003ccode\u003enet/http\u003c/code\u003e stack, allowing you to reuse standard middleware, multiplexers, and observability tools.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBuilding on this foundation, Connect allows any unary RPC marked as side-effect free (\u003ccode\u003eNO_SIDE_EFFECTS\u003c/code\u003e in Protobuf) to be invoked via HTTP GET, unlocking caching at the CDN or proxy layer.\u003c/p\u003e\n\u003ch3 id=\"the-problem-with-get-and-query-parameters\"\u003eThe Problem with GET and Query Parameters\u003c/h3\u003e\n\u003cp\u003eTo make GET work with complex schema definitions, the protocol has to perform significant gymnastics. Because GET request bodies have no defined semantics and are routinely ignored or rejected by intermediate proxies, Connect is forced to cram structured payloads into the URL.\u003c/p\u003e\n\u003cp\u003eFor a simple JSON request, the client must serialize the payload, URL-encode it, and append it as a query parameter:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-http\" data-lang=\"http\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#88c0d0\"\u003eGET\u003c/span\u003e \u003cspan style=\"color:#8fbcbb\"\u003e/connectrpc.greet.v1.GreetService/Greet?connect=v1\u0026amp;encoding=json\u0026amp;message=%7B%22name%22%3A%22Buf%22%7D\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eHTTP\u003c/span\u003e\u003cspan style=\"color:#81a1c1\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#b48ead\"\u003e1.1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eHost\u003cspan style=\"color:#81a1c1\"\u003e:\u003c/span\u003e demo.connectrpc.com\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIf you use binary Protobuf or compression, the overhead increases further, requiring base64 encoding along with additional control parameters. Shoving these complex payloads into URLs creates immediate practical problems for production systems:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eBloated URLs:\u003c/strong\u003e Complex requests easily hit maximum URL length limits enforced by load balancers, reverse proxies, and older browsers.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLeaky Logs:\u003c/strong\u003e Query parameters show up in plain text in standard \u003ca href=\"https://nginx.org/\" rel=\"external\"\u003eNginx\u003c/a\u003e or \u003ca href=\"https://httpd.apache.org/\" rel=\"external\"\u003eApache\u003c/a\u003e access logs, WAF dashboards, and observability tools. If a request contains sensitive filter criteria, teams are forced to write custom masking rules to scrub their logs.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEncoding Friction:\u003c/strong\u003e Maintaining a separate serialization path just for GET requests introduces branching logic into the codebase. Clients and servers must implement special handling to treat this specific verb entirely differently than the rest of the API surface.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"enter-http-query\"\u003eEnter HTTP QUERY\u003c/h3\u003e\n\u003cp\u003eQUERY gives HTTP the method shape this use case has been missing. Semantically, it is defined as a safe, idempotent method. Mechanically, it operates like a POST, allowing a standard request body.\u003c/p\u003e\n\u003cp\u003eIf ConnectRPC adopts QUERY, the entire query parameter encoding scheme can be dropped. A QUERY request looks exactly like a POST request on the wire, keeping the payload in the HTTP body natively encoded as \u003ccode\u003eapplication/json\u003c/code\u003e or \u003ccode\u003eapplication/proto\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eHere is how a proposed JSON-based QUERY wire request looks:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-http\" data-lang=\"http\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#bf616a\"\u003eQUERY /connectrpc.greet.v1.GreetService/Greet HTTP/1.1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#bf616a\"\u003eHost: demo.connectrpc.com\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#bf616a\"\u003eContent-Type: application/json\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#bf616a\"\u003eConnect-Protocol-Version: 1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#bf616a\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#bf616a\"\u003e{\u0026#34;name\u0026#34;:\u0026#34;Buf\u0026#34;}\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAnd for binary Protobuf:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-http\" data-lang=\"http\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#bf616a\"\u003eQUERY /connectrpc.greet.v1.GreetService/Greet HTTP/1.1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#bf616a\"\u003eHost: demo.connectrpc.com\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#bf616a\"\u003eContent-Type: application/proto\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#bf616a\"\u003eConnect-Protocol-Version: 1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#bf616a\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#bf616a\"\u003e\u0026lt;binary protobuf\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is the core protocol win: \u003cstrong\u003eQUERY lets Connect reuse the normal unary POST body format instead of maintaining a specialized GET query-encoding path.\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 id=\"caching-demands-query-aware-infrastructure\"\u003eCaching Demands QUERY-Aware Infrastructure\u003c/h3\u003e\n\u003cp\u003eFrom a network caching perspective, QUERY is not simply \u0026ldquo;GET with a body.\u0026rdquo; RFC 10008 specifies that for a QUERY response to be cached, the cache key must incorporate the request body content alongside related metadata.\u003c/p\u003e\n\u003cp\u003eMost existing HTTP caching infrastructure is built strictly around request metadata such as the method, scheme, host, path, query string, and selected headers. Supporting QUERY requires intermediate proxies, gateways, and CDNs to inspect and hash request bodies. Until major CDNs document first-class support for body-aware cache keys, QUERY caching should be treated as experimental outside infrastructure you control directly.\u003c/p\u003e\n\u003ch3 id=\"the-browser-story\"\u003eThe Browser Story\u003c/h3\u003e\n\u003cp\u003eBrowser support is not a reason to avoid QUERY; it is a reason to implement it deliberately.\u003c/p\u003e\n\u003cp\u003eBecause \u003ccode\u003eQUERY\u003c/code\u003e is not a CORS-safelisted method, cross-origin browser clients require the server or gateway to allow it in preflight responses. This is a standard deployment requirement for modern APIs. Many Connect-Web deployments already require custom CORS configuration for protocol headers, content types, and credentials.\u003c/p\u003e\n\u003cp\u003eAdopting QUERY gives web clients a clean way to express search forms, filtered list views, reporting queries, and batch reads without resorting to URL hacks.\u003c/p\u003e\n\u003ch3 id=\"a-pragmatic-rollout-strategy\"\u003eA Pragmatic Rollout Strategy\u003c/h3\u003e\n\u003cp\u003eWe cannot flip a switch and expect a new HTTP verb to work across the public internet immediately. The web is built on middleboxes, load balancers, strict firewalls, and managed WAFs that are inherently suspicious of unfamiliar traffic and will likely reject QUERY requests as malformed for some time.\u003c/p\u003e\n\u003cp\u003eHowever, many modern backend servers accept arbitrary verbs without complaint. You can verify how your current stack handles unfamiliar methods right now using \u003ca href=\"https://httpbin.io\" rel=\"external\"\u003ehttpbin.io\u003c/a\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$ curl -X QUERY \u003cspan style=\"color:#81a1c1\"\u003e[\u003c/span\u003ehttps://httpbin.io/status/200\u003cspan style=\"color:#81a1c1\"\u003e](\u003c/span\u003ehttps://httpbin.io/status/200\u003cspan style=\"color:#81a1c1\"\u003e)\u003c/span\u003e -w \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;%{http_code}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#b48ead\"\u003e200\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eTo prove the server simply accepts the string token, you can test an arbitrary string:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e$ curl -X YEET \u003cspan style=\"color:#81a1c1\"\u003e[\u003c/span\u003ehttps://httpbin.io/status/200\u003cspan style=\"color:#81a1c1\"\u003e](\u003c/span\u003ehttps://httpbin.io/status/200\u003cspan style=\"color:#81a1c1\"\u003e)\u003c/span\u003e -w \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;%{http_code}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#b48ead\"\u003e200\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhile this does not prove full RFC 10008 compliance, it confirms that the initial deployment barrier at the server application layer is low. The harder problem is teaching intermediaries, caches, and client libraries what QUERY actually means.\u003c/p\u003e\n\u003cp\u003eThe most effective early deployments will target paths where engineering teams control the entire network hop: internal service-to-service traffic and browser-facing APIs behind configurable API gateways. Protocols like Connect are the ideal starting point because they can expose QUERY as an opt-in transport mechanism while the broader networking ecosystem catches up.\u003c/p\u003e\n\u003ch3 id=\"what-query-should-not-replace-yet\"\u003eWhat QUERY Should Not Replace Yet\u003c/h3\u003e\n\u003cp\u003eWhile QUERY is the cleaner protocol shape, a practical rollout requires clear boundaries:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eGET remains useful for small, URL-friendly requests:\u003c/strong\u003e For simple payloads that fit naturally in a URI, GET continues to offer mature browser integration, native CDN caching, and effortless manual debugging.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePOST remains the compatibility fallback:\u003c/strong\u003e When requests have side effects, or when traffic must route through uncooperative legacy intermediaries, POST remains the universal standard.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eQUERY starts as opt-in:\u003c/strong\u003e Support should initially be opt-in for unary RPCs explicitly marked \u003ccode\u003eNO_SIDE_EFFECTS\u003c/code\u003e. This option should be exposed to both server-side and web clients, backed by clear deployment guidance for CORS, gateways, and cache behavior.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe QUERY method solves a real, persistent networking problem. It is time to put it to work.\u003c/p\u003e\n","tags":["connectrpc","http","networking","api","protobuf","golang"]},{"id":"https://kmcd.dev/posts/benchmarking-protojsonx/","url":"https://kmcd.dev/posts/benchmarking-protojsonx/","title":"Beating Go's encoding/json with Schema-Guided ProtoJSON","summary":"I built protojsonx to measure how much of Go’s ProtoJSON overhead comes from runtime reflection.","date_published":"2026-07-21T15:20:00Z","date_modified":"2026-08-31T09:39:14+02:00","content_html":"\u003cp\u003eIn my \u003ca href=\"https://kmcd.dev/posts/hidden-cost-of-google-protobuf-value/\"\u003eprevious article\u003c/a\u003e, benchmark results showed that Go’s official \u003ccode\u003eprotojson\u003c/code\u003e package was surprisingly slow. Execution profiles consistently highlighted the same bottlenecks: reflection, pointer chasing, and descriptor traversal accounted for much of the latency and memory allocation.\u003c/p\u003e\n\u003cp\u003eProtoJSON must preserve Protobuf semantics, including custom field names, enums, presence rules, and message structure, while producing standard JSON. The official implementation uses \u003ccode\u003eprotoreflect\u003c/code\u003e to dynamically walk descriptors and inspect message structures on every call.\u003c/p\u003e\n\u003cp\u003eThat raised a concrete question: \u003cem\u003ehow much of this cost is inherent to JSON formatting, and how much comes from repeatedly resolving schema mappings at runtime?\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eTo answer this, I built \u003ca href=\"https://github.com/sudorandom/protojsonx\" rel=\"external\"\u003eprotojsonx\u003c/a\u003e. Moving schema resolution out of the hot path brings ProtoJSON marshaling performance close to binary protobuf and ahead of both Go\u0026rsquo;s standard \u003ccode\u003eencoding/json\u003c/code\u003e and \u003ccode\u003eencoding/json/v2\u003c/code\u003e in these benchmarks.\u003c/p\u003e\n\u003ca href=\"https://github.com/sudorandom/protojsonx\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"github-repo-card\"\u003e\n  \u003cdiv class=\"github-repo-card-header\"\u003e\n    \u003cdiv class=\"github-repo-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22\"\u003e\u003c/path\u003e\u003c/svg\u003e\u003c/div\u003e\n    \u003cspan class=\"github-repo-name\"\u003esudorandom/protojsonx\u003c/span\u003e\n  \u003c/div\u003e\u003cp class=\"github-repo-description\"\u003eAn experimental faster ProtoJSON encoder and decoder for Go.\u003c/p\u003e\u003c/a\u003e\n\n\u003chr\u003e\n\u003ch2 id=\"moving-schema-work-out-of-the-hot-path\"\u003eMoving Schema Work Out of the Hot Path\u003c/h2\u003e\n\u003cp\u003eThe official implementation discovers how to map each protobuf field to JSON while processing the message. \u003ccode\u003eprotojsonx\u003c/code\u003e resolves that mapping once and stores the result as a precomputed schema layout table.\u003c/p\u003e\n\u003cp\u003eIt uses that precomputed schema information in two ways:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eRuntime Table Mode:\u003c/strong\u003e Builds flat schema layout tables once at startup initialization. This allows it to marshal and unmarshal messages using direct offset arithmetic instead of per-call descriptor traversal.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eGenerated Plugin Mode:\u003c/strong\u003e Provides a \u003ccode\u003eprotoc\u003c/code\u003e plugin (\u003ccode\u003eprotoc-gen-go-protojsonx\u003c/code\u003e) that bakes type-specific marshaling and unmarshaling methods directly into generated \u003ccode\u003e.pb.go\u003c/code\u003e code, bypassing the runtime lookup layer entirely.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003ccode\u003eprotojsonx\u003c/code\u003e is designed as a mostly API-compatible experimental alternative to Go\u0026rsquo;s official \u003ccode\u003egoogle.golang.org/protobuf/encoding/protojson\u003c/code\u003e package. It handles common rules including custom \u003ccode\u003ejson_name\u003c/code\u003e, enums, field presence, unknown fields, oneofs, maps, and standard options (though experimental features like dynamic \u003ccode\u003eAny\u003c/code\u003e resolver callbacks are not yet supported).\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eWarning:\u003c/strong\u003e \u003ccode\u003eprotojsonx\u003c/code\u003e is experimental. It passes the official Protobuf conformance suite, but it has not yet seen enough production use for me to recommend it as a drop-in replacement.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"benchmark-setup\"\u003eBenchmark Setup\u003c/h2\u003e\n\u003cp\u003eThese benchmarks do not compare identical semantics. They answer a practical question: when an application needs ProtoJSON-compatible output, how expensive is that compared with Go’s general-purpose JSON packages?\u003c/p\u003e\n\u003ch3 id=\"payloads-and-sizes\"\u003ePayloads and Sizes\u003c/h3\u003e\n\u003cp\u003eI used the benchmark suite from my previous article, running on Go 1.26 on an Apple M1 Pro across three payload shapes:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSmall:\u003c/strong\u003e A flat object with 4 fields (string ID, status boolean, age integer, score float).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMedium:\u003c/strong\u003e A nested user signup event containing an actor object, string tags, and a metadata map.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLarge:\u003c/strong\u003e An array repeating the Medium object 100 times.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eSerialization output size matters, especially when comparing JSON and binary protobuf:\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003ePayload\u003c/th\u003e\n          \u003cth style=\"text-align: right\"\u003eProtoJSON bytes\u003c/th\u003e\n          \u003cth style=\"text-align: right\"\u003egeneric JSON bytes\u003c/th\u003e\n          \u003cth style=\"text-align: right\"\u003ebinary proto bytes\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003eSmall\u003c/td\u003e\n          \u003ctd style=\"text-align: right\"\u003e55 B\u003c/td\u003e\n          \u003ctd style=\"text-align: right\"\u003e55 B\u003c/td\u003e\n          \u003ctd style=\"text-align: right\"\u003e25 B\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003eMedium\u003c/td\u003e\n          \u003ctd style=\"text-align: right\"\u003e293 B\u003c/td\u003e\n          \u003ctd style=\"text-align: right\"\u003e291 B\u003c/td\u003e\n          \u003ctd style=\"text-align: right\"\u003e162 B\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003eLarge\u003c/td\u003e\n          \u003ctd style=\"text-align: right\"\u003e29,412 B\u003c/td\u003e\n          \u003ctd style=\"text-align: right\"\u003e29,201 B\u003c/td\u003e\n          \u003ctd style=\"text-align: right\"\u003e16,500 B\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eThe ProtoJSON and generic JSON sizes are nearly identical. Binary protobuf is significantly smaller, so binary numbers serve as a compact wire-format baseline rather than a direct format equivalent.\u003c/p\u003e\n\u003ch3 id=\"compared-implementations\"\u003eCompared Implementations\u003c/h3\u003e\n\u003cp\u003eAll tests measure end-to-end marshal and unmarshal execution times along with allocations.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eThe generic JSON cases (\u003ccode\u003eencoding/json\u003c/code\u003e and \u003ccode\u003eencoding/json/v2\u003c/code\u003e from \u003ccode\u003egithub.com/go-json-experiment/json\u003c/code\u003e) use native Go structs with equivalent fields.\u003c/li\u003e\n\u003cli\u003eThe Protobuf cases use standard generated messages.\u003c/li\u003e\n\u003cli\u003eI also included \u003ccode\u003ehyperpb\u003c/code\u003e (a descriptor/layout-driven dynamic protobuf parser built around read-oriented offset decoding) and \u003ccode\u003ehyperpb.Shared\u003c/code\u003e (where the benchmark reuses an arena buffer). While \u003ccode\u003ehyperpb\u003c/code\u003e is not a ProtoJSON library, its numbers provide context on raw binary parsing overhead.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow environment details and test command\u003c/b\u003e\u003c/summary\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eGo Version\u003c/strong\u003e: \u003ccode\u003ego version go1.26.3 darwin/arm64\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMachine Details\u003c/strong\u003e: Apple M1 Pro (10-core CPU, 16GB unified memory, macOS), \u003ccode\u003eGOMAXPROCS=8\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTarget Library Version\u003c/strong\u003e: \u003ccode\u003egithub.com/sudorandom/protojsonx@v0.0.6\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBenchmark Commit\u003c/strong\u003e: \u003ccode\u003eb8fff78c\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBenchmark Source\u003c/strong\u003e: Available in \u003ca href=\"https://github.com/sudorandom/kmcd.dev/tree/main/content/posts/2026/benchmarking-protojsonx/benchmarks/\" rel=\"external\"\u003ebenchmarks/\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTest Execution Command\u003c/strong\u003e:\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ego \u003cspan style=\"color:#81a1c1\"\u003etest\u003c/span\u003e -bench\u003cspan style=\"color:#81a1c1\"\u003e=\u003c/span\u003e. -benchmem -benchtime\u003cspan style=\"color:#81a1c1\"\u003e=\u003c/span\u003e5s -count\u003cspan style=\"color:#81a1c1\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#b48ead\"\u003e5\u003c/span\u003e \u0026gt; results.txt\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe tables report arithmetic means for \u003ccode\u003ens/op\u003c/code\u003e, \u003ccode\u003eB/op\u003c/code\u003e, and \u003ccode\u003eallocs/op\u003c/code\u003e computed from the raw five-run output.\u003c/p\u003e\n\u003c/details\u003e\n\u003chr\u003e\n\u003ch2 id=\"marshaling-performance\"\u003eMarshaling Performance\u003c/h2\u003e\n\u003cp\u003eMarshaling starts with an already constructed Go message, so the encoder mainly needs to walk its fields and write JSON bytes.\u003c/p\u003e\n\u003cdiv class=\"tabs-wrapper\" id=\"tabs-64726\"\u003e\n  \u003cdiv class=\"tabs-nav\" role=\"tablist\"\u003e\u003cbutton class=\"tab-btn\" \n              role=\"tab\" \n              aria-selected=\"false\" \n              aria-controls=\"panel-small-payload-33120\" \n              id=\"tab-btn-small-payload-33120\"\n              data-tab=\"small-payload-33120\"\u003e\n        Small Payload\n      \u003c/button\u003e\u003cbutton class=\"tab-btn active\" \n              role=\"tab\" \n              aria-selected=\"true\" \n              aria-controls=\"panel-medium-payload-15518\" \n              id=\"tab-btn-medium-payload-15518\"\n              data-tab=\"medium-payload-15518\"\u003e\n        Medium Payload\n      \u003c/button\u003e\u003cbutton class=\"tab-btn\" \n              role=\"tab\" \n              aria-selected=\"false\" \n              aria-controls=\"panel-large-payload-62993\" \n              id=\"tab-btn-large-payload-62993\"\n              data-tab=\"large-payload-62993\"\u003e\n        Large Payload\n      \u003c/button\u003e\u003c/div\u003e\n  \u003cdiv class=\"tabs-panels\"\u003e\n  \u003cdiv class=\"tab-panel-content\" id=\"panel-small-payload-33120\" role=\"tabpanel\" style=\"display: none;\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"a1955001fe172712\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('a1955001fe172712');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"protojson\",\n      \"encoding/json\",\n      \"encoding/json/v2\",\n      \"protojsonx (Runtime Tables)\",\n      \"protojsonx (Generated Plugin)\",\n      \"proto.Marshal\",\n      \"vtproto\",\n      \"hyperpb + Shared\"\n    ],\n    \"datasets\": [\n      {\n        \"label\": \"Small Marshal (ns/op)\",\n        \"data\": [\n          612,\n          183,\n          289,\n          142,\n          102,\n          83,\n          25,\n          296\n        ],\n        \"backgroundColor\": [\n          \"rgba(186, 85, 211, 0.75)\",\n          \"rgba(255, 165, 0, 0.75)\",\n          \"rgba(255, 130, 0, 0.75)\",\n          \"rgba(135, 206, 250, 0.75)\",\n          \"rgba(0, 191, 255, 0.75)\",\n          \"rgba(50, 205, 50, 0.75)\",\n          \"rgba(0, 250, 154, 0.75)\",\n          \"rgba(34, 139, 34, 0.75)\"\n        ],\n        \"borderColor\": [\n          \"rgba(186, 85, 211, 1)\",\n          \"rgba(255, 165, 0, 1)\",\n          \"rgba(255, 130, 0, 1)\",\n          \"rgba(135, 206, 250, 1)\",\n          \"rgba(0, 191, 255, 1)\",\n          \"rgba(50, 205, 50, 1)\",\n          \"rgba(0, 250, 154, 1)\",\n          \"rgba(34, 139, 34, 1)\"\n        ],\n        \"borderWidth\": 1\n      }\n    ]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Marshaling Performance (Small Payload): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"display\": false\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      },\n      \"y\": {\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow complete data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eFormat / Serializer\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eSpeed vs protojson\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojson\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e612 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e512 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e12\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1.0x (Baseline)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eencoding/json\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e183 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e64 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3.3x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eencoding/json/v2\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e289 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e112 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2.1x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojsonx (Runtime Tables)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e142 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e64 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e4.3x faster\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojsonx (Generated Plugin)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e102 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e64 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e6.0x faster\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eproto.Marshal\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e83 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e32 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e7.4x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003evtproto\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e25 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e32 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e24.5x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb + Shared\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e296 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e144 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e7\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2.1x faster\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n  \u003cdiv class=\"tab-panel-content\" id=\"panel-medium-payload-15518\" role=\"tabpanel\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"834e2c757c17c4f3\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('834e2c757c17c4f3');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"protojson\",\n      \"encoding/json\",\n      \"encoding/json/v2\",\n      \"protojsonx (Runtime Tables)\",\n      \"protojsonx (Generated Plugin)\",\n      \"proto.Marshal\",\n      \"vtproto\",\n      \"hyperpb + Shared\"\n    ],\n    \"datasets\": [\n      {\n        \"label\": \"Medium Marshal (ns/op)\",\n        \"data\": [\n          2227,\n          521,\n          803,\n          404,\n          318,\n          285,\n          103,\n          1062\n        ],\n        \"backgroundColor\": [\n          \"rgba(186, 85, 211, 0.75)\",\n          \"rgba(255, 165, 0, 0.75)\",\n          \"rgba(255, 130, 0, 0.75)\",\n          \"rgba(135, 206, 250, 0.75)\",\n          \"rgba(0, 191, 255, 0.75)\",\n          \"rgba(50, 205, 50, 0.75)\",\n          \"rgba(0, 250, 154, 0.75)\",\n          \"rgba(34, 139, 34, 0.75)\"\n        ],\n        \"borderColor\": [\n          \"rgba(186, 85, 211, 1)\",\n          \"rgba(255, 165, 0, 1)\",\n          \"rgba(255, 130, 0, 1)\",\n          \"rgba(135, 206, 250, 1)\",\n          \"rgba(0, 191, 255, 1)\",\n          \"rgba(50, 205, 50, 1)\",\n          \"rgba(0, 250, 154, 1)\",\n          \"rgba(34, 139, 34, 1)\"\n        ],\n        \"borderWidth\": 1\n      }\n    ]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Marshaling Performance (Medium Payload): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"display\": false\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      },\n      \"y\": {\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow complete data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eFormat / Serializer\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eSpeed vs protojson\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojson\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,227 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,722 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e34\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1.0x (Baseline)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eencoding/json\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e521 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e464 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e4.3x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eencoding/json/v2\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e803 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e608 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2.8x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojsonx (Runtime Tables)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e404 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e320 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e5.5x faster\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojsonx (Generated Plugin)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e318 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e320 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e7.0x faster\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eproto.Marshal\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e285 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e176 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e7.8x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003evtproto\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e103 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e176 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e21.6x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb + Shared\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,062 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e744 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e17\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2.1x faster\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n  \u003cdiv class=\"tab-panel-content\" id=\"panel-large-payload-62993\" role=\"tabpanel\" style=\"display: none;\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"173104d840bf5127\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('173104d840bf5127');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"protojson\",\n      \"encoding/json\",\n      \"encoding/json/v2\",\n      \"protojsonx (Runtime Tables)\",\n      \"protojsonx (Generated Plugin)\",\n      \"proto.Marshal\",\n      \"vtproto\",\n      \"hyperpb + Shared\"\n    ],\n    \"datasets\": [\n      {\n        \"label\": \"Large Marshal (ns/op)\",\n        \"data\": [\n          223838,\n          41250,\n          62036,\n          31134,\n          27601,\n          25131,\n          7575,\n          103495\n        ],\n        \"backgroundColor\": [\n          \"rgba(186, 85, 211, 0.75)\",\n          \"rgba(255, 165, 0, 0.75)\",\n          \"rgba(255, 130, 0, 0.75)\",\n          \"rgba(135, 206, 250, 0.75)\",\n          \"rgba(0, 191, 255, 0.75)\",\n          \"rgba(50, 205, 50, 0.75)\",\n          \"rgba(0, 250, 154, 0.75)\",\n          \"rgba(34, 139, 34, 0.75)\"\n        ],\n        \"borderColor\": [\n          \"rgba(186, 85, 211, 1)\",\n          \"rgba(255, 165, 0, 1)\",\n          \"rgba(255, 130, 0, 1)\",\n          \"rgba(135, 206, 250, 1)\",\n          \"rgba(0, 191, 255, 1)\",\n          \"rgba(50, 205, 50, 1)\",\n          \"rgba(0, 250, 154, 1)\",\n          \"rgba(34, 139, 34, 1)\"\n        ],\n        \"borderWidth\": 1\n      }\n    ]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Marshaling Performance (Large Payload): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"display\": false\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      },\n      \"y\": {\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow complete data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eFormat / Serializer\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eSpeed vs protojson\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojson\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e223,838 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e243,749 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2728\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1.0x (Baseline)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eencoding/json\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e41,250 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e32,823 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e5.4x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eencoding/json/v2\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e62,036 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e32,856 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3.6x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojsonx (Runtime Tables)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e31,134 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e32,797 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e7.2x faster\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojsonx (Generated Plugin)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e27,601 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e32,784 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e8.1x faster\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eproto.Marshal\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e25,131 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e18,432 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8.9x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003evtproto\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e7,575 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e18,432 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e29.5x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb + Shared\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e103,495 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e107,216 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1022\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2.2x faster\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n\u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"interpreting-marshaling-results\"\u003eInterpreting Marshaling Results\u003c/h3\u003e\n\u003cp\u003ePrecomputing schema layout tables eliminates most of official \u003ccode\u003eprotojson\u003c/code\u003e\u0026rsquo;s marshaling overhead. Both \u003ccode\u003eprotojsonx\u003c/code\u003e modes reduce marshaling to one heap allocation in these benchmarks (64 B for small, 320 B for medium, and ~32 KB for large).\u003c/p\u003e\n\u003cp\u003eAcross all payload sizes, runtime table mode gets surprisingly close to the generated plugin mode. The difference is 40 ns for the small message (142 ns vs 102 ns), and about 11% for the large case (31.1 µs vs 27.6 µs). With descriptor traversal gone, the remaining gap likely comes from interpreting the table at runtime instead of executing generated field-specific code. Projects with many generated message types will pay for that extra speed through additional generated code and a larger binary.\u003c/p\u003e\n\u003cp\u003eOne result unrelated to \u003ccode\u003eprotojsonx\u003c/code\u003e also stood out: \u003ccode\u003eencoding/json/v2\u003c/code\u003e was consistently slower than v1 during marshaling (for example, 62 µs vs 41 µs on large payloads). That may reflect additional state tracking and the costs of its more flexible implementation, though the benchmark does not isolate the cause.\u003c/p\u003e\n\u003cp\u003eOn the large payload, generated \u003ccode\u003eprotojsonx\u003c/code\u003e took 27.6 µs, compared with 25.1 µs for \u003ccode\u003eproto.Marshal\u003c/code\u003e. I did not expect JSON encoding to get that close. For this payload, once descriptor traversal was removed, writing field names and formatted values added surprisingly little time over binary encoding.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"unmarshaling-performance\"\u003eUnmarshaling Performance\u003c/h2\u003e\n\u003cp\u003eMarshaling showed that precomputed schema information eliminates most of the official implementation\u0026rsquo;s overhead. Decoding is a tougher test because avoiding reflection does not remove JSON tokenization, string parsing, or message construction.\u003c/p\u003e\n\u003cdiv class=\"tabs-wrapper\" id=\"tabs-90235\"\u003e\n  \u003cdiv class=\"tabs-nav\" role=\"tablist\"\u003e\u003cbutton class=\"tab-btn\" \n              role=\"tab\" \n              aria-selected=\"false\" \n              aria-controls=\"panel-small-payload-17852\" \n              id=\"tab-btn-small-payload-17852\"\n              data-tab=\"small-payload-17852\"\u003e\n        Small Payload\n      \u003c/button\u003e\u003cbutton class=\"tab-btn active\" \n              role=\"tab\" \n              aria-selected=\"true\" \n              aria-controls=\"panel-medium-payload-54276\" \n              id=\"tab-btn-medium-payload-54276\"\n              data-tab=\"medium-payload-54276\"\u003e\n        Medium Payload\n      \u003c/button\u003e\u003cbutton class=\"tab-btn\" \n              role=\"tab\" \n              aria-selected=\"false\" \n              aria-controls=\"panel-large-payload-47605\" \n              id=\"tab-btn-large-payload-47605\"\n              data-tab=\"large-payload-47605\"\u003e\n        Large Payload\n      \u003c/button\u003e\u003c/div\u003e\n  \u003cdiv class=\"tabs-panels\"\u003e\n  \u003cdiv class=\"tab-panel-content\" id=\"panel-small-payload-17852\" role=\"tabpanel\" style=\"display: none;\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"035726e4753790eb\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('035726e4753790eb');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"protojson\",\n      \"encoding/json\",\n      \"encoding/json/v2\",\n      \"protojsonx (Runtime Tables)\",\n      \"protojsonx (Generated Plugin)\",\n      \"proto.Unmarshal\",\n      \"vtproto\",\n      \"hyperpb\",\n      \"hyperpb + Shared\"\n    ],\n    \"datasets\": [\n      {\n        \"label\": \"Small Unmarshal (ns/op)\",\n        \"data\": [\n          934,\n          752,\n          339,\n          267,\n          136,\n          114,\n          25,\n          360,\n          125\n        ],\n        \"backgroundColor\": [\n          \"rgba(186, 85, 211, 0.75)\",\n          \"rgba(255, 165, 0, 0.75)\",\n          \"rgba(255, 130, 0, 0.75)\",\n          \"rgba(135, 206, 250, 0.75)\",\n          \"rgba(0, 191, 255, 0.75)\",\n          \"rgba(50, 205, 50, 0.75)\",\n          \"rgba(0, 250, 154, 0.75)\",\n          \"rgba(34, 139, 34, 0.75)\",\n          \"rgba(34, 139, 34, 0.45)\"\n        ],\n        \"borderColor\": [\n          \"rgba(186, 85, 211, 1)\",\n          \"rgba(255, 165, 0, 1)\",\n          \"rgba(255, 130, 0, 1)\",\n          \"rgba(135, 206, 250, 1)\",\n          \"rgba(0, 191, 255, 1)\",\n          \"rgba(50, 205, 50, 1)\",\n          \"rgba(0, 250, 154, 1)\",\n          \"rgba(34, 139, 34, 1)\",\n          \"rgba(34, 139, 34, 0.8)\"\n        ],\n        \"borderWidth\": 1\n      }\n    ]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Unmarshaling Performance (Small Payload): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"display\": false\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      },\n      \"y\": {\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow complete data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eFormat / Serializer\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eSpeed vs protojson\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojson\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e934 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e336 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e14\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1.0x (Baseline)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eencoding/json\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e752 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e280 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e6\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1.2x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eencoding/json/v2\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e339 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e48 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2.8x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojsonx (Runtime Tables)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e267 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e96 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e3\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e3.5x faster\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojsonx (Generated Plugin)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e136 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e96 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e2\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e6.9x faster\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eproto.Unmarshal\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e114 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e96 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e8.2x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003evtproto\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e25 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e16 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e37.4x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e360 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e798 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e4\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2.6x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb + Shared\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e125 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e65 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e7.5x faster\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n  \u003cdiv class=\"tab-panel-content\" id=\"panel-medium-payload-54276\" role=\"tabpanel\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"30f11b99287d0ca9\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('30f11b99287d0ca9');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"protojson\",\n      \"encoding/json\",\n      \"encoding/json/v2\",\n      \"protojsonx (Runtime Tables)\",\n      \"protojsonx (Generated Plugin)\",\n      \"proto.Unmarshal\",\n      \"vtproto\",\n      \"hyperpb\",\n      \"hyperpb + Shared\"\n    ],\n    \"datasets\": [\n      {\n        \"label\": \"Medium Unmarshal (ns/op)\",\n        \"data\": [\n          3703,\n          2937,\n          1133,\n          1108,\n          720,\n          571,\n          322,\n          638,\n          290\n        ],\n        \"backgroundColor\": [\n          \"rgba(186, 85, 211, 0.75)\",\n          \"rgba(255, 165, 0, 0.75)\",\n          \"rgba(255, 130, 0, 0.75)\",\n          \"rgba(135, 206, 250, 0.75)\",\n          \"rgba(0, 191, 255, 0.75)\",\n          \"rgba(50, 205, 50, 0.75)\",\n          \"rgba(0, 250, 154, 0.75)\",\n          \"rgba(34, 139, 34, 0.75)\",\n          \"rgba(34, 139, 34, 0.45)\"\n        ],\n        \"borderColor\": [\n          \"rgba(186, 85, 211, 1)\",\n          \"rgba(255, 165, 0, 1)\",\n          \"rgba(255, 130, 0, 1)\",\n          \"rgba(135, 206, 250, 1)\",\n          \"rgba(0, 191, 255, 1)\",\n          \"rgba(50, 205, 50, 1)\",\n          \"rgba(0, 250, 154, 1)\",\n          \"rgba(34, 139, 34, 1)\",\n          \"rgba(34, 139, 34, 0.8)\"\n        ],\n        \"borderWidth\": 1\n      }\n    ]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Unmarshaling Performance (Medium Payload): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"display\": false\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      },\n      \"y\": {\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow complete data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eFormat / Serializer\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eSpeed vs protojson\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojson\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3,703 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,304 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e58\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1.0x (Baseline)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eencoding/json\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,937 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e688 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e19\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1.3x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eencoding/json/v2\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,133 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e256 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e4\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3.3x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojsonx (Runtime Tables)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1,108 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e576 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e16\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e3.3x faster\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojsonx (Generated Plugin)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e720 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e528 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e14\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e5.1x faster\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eproto.Unmarshal\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e571 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e560 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e15\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e6.5x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003evtproto\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e322 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e432 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e14\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e11.5x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e638 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,446 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e5\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e5.8x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb + Shared\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e290 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e357 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e12.8x faster\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n  \u003cdiv class=\"tab-panel-content\" id=\"panel-large-payload-47605\" role=\"tabpanel\" style=\"display: none;\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"d2185111d4b97250\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('d2185111d4b97250');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"protojson\",\n      \"encoding/json\",\n      \"encoding/json/v2\",\n      \"protojsonx (Runtime Tables)\",\n      \"protojsonx (Generated Plugin)\",\n      \"proto.Unmarshal\",\n      \"vtproto\",\n      \"hyperpb\",\n      \"hyperpb + Shared\"\n    ],\n    \"datasets\": [\n      {\n        \"label\": \"Large Unmarshal (ns/op)\",\n        \"data\": [\n          377892,\n          272103,\n          108811,\n          111141,\n          73389,\n          54923,\n          36002,\n          24835,\n          18163\n        ],\n        \"backgroundColor\": [\n          \"rgba(186, 85, 211, 0.75)\",\n          \"rgba(255, 165, 0, 0.75)\",\n          \"rgba(255, 130, 0, 0.75)\",\n          \"rgba(135, 206, 250, 0.75)\",\n          \"rgba(0, 191, 255, 0.75)\",\n          \"rgba(50, 205, 50, 0.75)\",\n          \"rgba(0, 250, 154, 0.75)\",\n          \"rgba(34, 139, 34, 0.75)\",\n          \"rgba(34, 139, 34, 0.45)\"\n        ],\n        \"borderColor\": [\n          \"rgba(186, 85, 211, 1)\",\n          \"rgba(255, 165, 0, 1)\",\n          \"rgba(255, 130, 0, 1)\",\n          \"rgba(135, 206, 250, 1)\",\n          \"rgba(0, 191, 255, 1)\",\n          \"rgba(50, 205, 50, 1)\",\n          \"rgba(0, 250, 154, 1)\",\n          \"rgba(34, 139, 34, 1)\",\n          \"rgba(34, 139, 34, 0.8)\"\n        ],\n        \"borderWidth\": 1\n      }\n    ]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Unmarshaling Performance (Large Payload): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"display\": false\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      },\n      \"y\": {\n        \"ticks\": {\n          \"color\": \"#fff\"\n        }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow complete data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eFormat / Serializer\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eSpeed vs protojson\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojson\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e377,892 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e119,256 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e5713\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1.0x (Baseline)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eencoding/json\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e272,103 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e70,584 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1216\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1.4x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eencoding/json/v2\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e108,811 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e54,305 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e309\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e3.5x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojsonx (Runtime Tables)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e111,141 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e62,232 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1709\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e3.4x faster\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eprotojsonx (Generated Plugin)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e73,389 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e55,008 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1407\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e5.1x faster\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eproto.Unmarshal\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e54,923 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e58,232 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1509\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e6.9x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003evtproto\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e36,002 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e58,168 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1508\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e10.5x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e24,835 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e60,053 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e12\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e15.2x faster\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb + Shared\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e18,163 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e21,863 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e20.8x faster\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n\u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"interpreting-unmarshaling-results\"\u003eInterpreting Unmarshaling Results\u003c/h3\u003e\n\u003cp\u003eThe generated decoder pulls further ahead than I expected. Runtime tables are almost twice as slow on the small payload (267 ns vs 136 ns) and remain about 34% slower on the large one (111.1 µs vs 73.3 µs). When parsing JSON dynamically, runtime table mode looks up field mappings in a table for every incoming key, whereas generated code emits direct message-specific key matching and field assignments.\u003c/p\u003e\n\u003cp\u003eThe allocation count initially looks disappointing. Generated \u003ccode\u003eprotojsonx\u003c/code\u003e still performs 1,407 allocations on the large payload. In this benchmark, decoding must allocate the nested messages, slices, strings, and maps that make up the result. Many of the remaining allocations therefore belong to constructing the output message graph rather than resolving its schema.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eencoding/json/v2\u003c/code\u003e performed much better than v1 during decoding, nearly matching runtime-table \u003ccode\u003eprotojsonx\u003c/code\u003e on the large payload (108.8 µs vs 111.1 µs). Generated \u003ccode\u003eprotojsonx\u003c/code\u003e remained faster (73.3 µs), which is consistent with its ability to emit message-specific field matching and assignment code without generic reflection overhead.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"conclusion\"\u003eConclusion\u003c/h2\u003e\n\u003cp\u003eThese benchmarks suggest that much of Go’s official \u003ccode\u003eprotojson\u003c/code\u003e cost comes from resolving schemas at runtime rather than from JSON formatting alone. Precomputed tables recover most of the marshaling performance, while generated code has a larger advantage during decoding, where field matching happens for every JSON key.\u003c/p\u003e\n\u003cp\u003eThat does not make JSON equivalent to binary protobuf. Tokenization, number parsing, and constructing the destination message still cost time and allocations. But the results show that ProtoJSON can be substantially faster than Go’s current general-purpose implementation.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"what-i-need-tested-next\"\u003eWhat I Need Tested Next\u003c/h2\u003e\n\u003cp\u003eIf you are serving JSON APIs backed by Protobuf and \u003ccode\u003eprotojson\u003c/code\u003e is showing up in your profiles, run the benchmarks against your own schemas and payloads before choosing an implementation.\u003c/p\u003e\n\u003cp\u003eI’m especially interested in benchmark results from real production schemas: large repeated fields, maps, oneofs, well-known types, custom \u003ccode\u003ejson_name\u003c/code\u003e usage, and edge cases beyond standard benchmark structs.\u003c/p\u003e\n\u003cp\u003eCheck out the project on GitHub: \u003ca href=\"https://github.com/sudorandom/protojsonx\" rel=\"external\"\u003esudorandom/protojsonx\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eThe more weird schemas, the better.\u003c/p\u003e\n","tags":["protobuf","go","performance","json","protojsonx"]},{"id":"https://kmcd.dev/posts/grpc-web-should-have-fixed-grpc/","url":"https://kmcd.dev/posts/grpc-web-should-have-fixed-grpc/","title":"gRPC-Web Should Have Fixed gRPC","summary":"gRPC-Web should have been the pressure that made gRPC simpler, more inspectable, and better suited for the web.","date_published":"2026-07-14T10:00:00Z","date_modified":"2026-08-31T09:39:14+02:00","content_html":"\u003cp\u003egRPC did a lot right. It turned Protocol Buffers into an API design tool, delivering typed messages, generated clients, and strict contracts. For backend networks where you control every hop, it is a fantastic tool.\u003c/p\u003e\n\u003cp\u003eBut gRPC tied itself too tightly to its HTTP/2 transport model. Specifically, it built core application behavior, like delivering final application status codes via response trailers, around protocol features that browser JavaScript could not fully expose.\u003c/p\u003e\n\u003cp\u003eBrowsers support HTTP/2 at the network layer, but frontend JavaScript gets a much narrower API. Standard browser tools like \u003ccode\u003efetch\u003c/code\u003e and XHR do not expose HTTP trailers to application code in the way native gRPC depends on. This left gRPC in an awkward position: the browser was using HTTP/2 underneath, but JavaScript could not make a native gRPC call.\u003c/p\u003e\n\u003ch2 id=\"the-compromise-grpc-web\"\u003eThe Compromise: gRPC-Web\u003c/h2\u003e\n\u003cp\u003egRPC-Web was the official answer to this problem. It adjusted the wire format by encoding trailer-like data directly into the response body, allowing browser clients to function.\u003c/p\u003e\n\u003cp\u003eIt worked, but the deployment story usually required a proxy, which added friction and complexity. Because ordinary gRPC servers did not speak this variant natively, you usually had to run Envoy or another gRPC-Web translation layer just to bridge browser requests into your backend.\u003c/p\u003e\n\u003cp\u003eWe accepted a specialized protocol for backend-to-backend efficiency, but bringing gRPC to the web involved extra parts. gRPC-Web was treated as a weird browser variant on the side instead of a mandate to simplify gRPC itself.\u003c/p\u003e\n\u003ch2 id=\"unary-calls-should-be-boring\"\u003eUnary Calls Should Be Boring\u003c/h2\u003e\n\u003cp\u003eThe real mistake was letting the hardest engineering cases define the common case. Most RPCs are not bidirectional streams. They are ordinary request-response operations: create a thing, fetch a thing, update a thing.\u003c/p\u003e\n\u003cp\u003eFor these unary calls, the protocol should have used standard HTTP semantics.\u003c/p\u003e\n\u003cp\u003eThat means using HTTP’s existing machinery instead of recreating it inside a custom message envelope. \u003ccode\u003eContent-Type\u003c/code\u003e should say whether the body is JSON or binary protobuf. For successful unary calls, the response can use the same format as the request. Developers should be able to use \u003ccode\u003eapplication/json\u003c/code\u003e in browsers or local \u003ccode\u003ecurl\u003c/code\u003e calls for easy debugging, while production workloads can use a binary protobuf content type for better performance. \u003ccode\u003eAccept-Encoding\u003c/code\u003e should advertise supported compression formats, and \u003ccode\u003eContent-Encoding\u003c/code\u003e should describe the compression actually used. When the body size is known, \u003ccode\u003eContent-Length\u003c/code\u003e can say how large it is. When it is not known ahead of time, the underlying HTTP version already has ways to determine where the body ends.\u003c/p\u003e\n\u003cp\u003eFor unary RPCs, gRPC’s extra length prefix and compression flag were not elegant protocol design. They were streaming machinery leaking into the boring case.\u003c/p\u003e\n\u003cp\u003eYou should still define your schema in protobuf, generate your clients, and get type safety. But the network layer should look like this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl \u003cspan style=\"color:#ebcb8b\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#ebcb8b\"\u003e\u003c/span\u003e  -H \u003cspan style=\"color:#a3be8c\"\u003e\u0026#39;Content-Type: application/json\u0026#39;\u003c/span\u003e \u003cspan style=\"color:#ebcb8b\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#ebcb8b\"\u003e\u003c/span\u003e  -d \u003cspan style=\"color:#a3be8c\"\u003e\u0026#39;{\u0026#34;userId\u0026#34;:\u0026#34;123\u0026#34;}\u0026#39;\u003c/span\u003e \u003cspan style=\"color:#ebcb8b\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#ebcb8b\"\u003e\u003c/span\u003e  https://api.example.com/UserService/GetUser\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIf the operation naturally maps to an HTTP status, use it. If a user exists, return \u003ccode\u003e200\u003c/code\u003e. If they do not, return \u003ccode\u003e404\u003c/code\u003e. If the server crashes, return \u003ccode\u003e500\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eThe standard counterargument from RPC purists is that HTTP status codes are too coarse. A \u003ccode\u003e404\u003c/code\u003e could mean the URL path is missing, or it could mean the requested database resource is missing. To avoid that ambiguity, gRPC often returns \u003ccode\u003e200 OK\u003c/code\u003e for a successfully handled HTTP request and puts the RPC status in trailers.\u003c/p\u003e\n\u003cp\u003eBut treating transport errors and application errors as completely separate worlds asks too much of the web. Load balancers, API gateways, CDNs, browser tools, and monitoring dashboards already understand \u003ccode\u003e4xx\u003c/code\u003e and \u003ccode\u003e5xx\u003c/code\u003e rates. Hiding application failures behind \u003ccode\u003e200 OK\u003c/code\u003e makes that infrastructure less useful unless every layer becomes protocol-aware.\u003c/p\u003e\n\u003cp\u003eA web-native design should use standard HTTP status codes for coarse outcomes, then put richer domain-specific error details inside the JSON or binary response body.\u003c/p\u003e\n\u003cp\u003eBoring HTTP works. Browser dev tools understand it, standard middleboxes log it correctly, and tired engineers can debug it at 2 AM without specialized tools.\u003c/p\u003e\n\u003cp\u003eA practical protocol split would have been straightforward:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eUnary calls:\u003c/strong\u003e Standard HTTP request-response semantics.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eStreaming calls:\u003c/strong\u003e Framed messages over a stream-friendly transport.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"the-web-native-evolution\"\u003eThe Web-Native Evolution\u003c/h2\u003e\n\u003cp\u003eWhat I want from “the next version of gRPC” is not exotic. In fact, it already exists; it is exactly how \u003ca href=\"https://connectrpc.com/\" rel=\"external\"\u003eConnectRPC\u003c/a\u003e works. Connect preserves the protobuf service model and client generation, but treats unary calls as standard HTTP requests without the complexities of binary framing on top of HTTP. It proves that you can have type-safe contracts without forcing simple calls to cosplay as complex streams.\u003c/p\u003e\n\u003cp\u003egRPC-Web should have been the moment gRPC admitted that the web was not a weird edge case. It should have been gRPC v2: protobuf contracts and generated clients on top of boring, inspectable, web-native HTTP, with special framing saved for real streaming instead of forced onto every unary call.\u003c/p\u003e\n\u003cp\u003eInstead, it became another compatibility layer. Useful, yes. But far less ambitious than it should have been.\u003c/p\u003e\n","tags":["grpc","grpc-web","protobuf","connectrpc"]},{"id":"https://kmcd.dev/posts/dynamic-protobuf-in-go/","url":"https://kmcd.dev/posts/dynamic-protobuf-in-go/","title":"Making Dynamic Protobuf Fast in Go","summary":"Using Buf’s hyperpb to speed up FauxRPC’s runtime-loaded Protobuf request path.","date_published":"2026-07-07T10:00:00Z","date_modified":"2026-08-31T09:39:14+02:00","content_html":"\u003cp\u003eMost Go Protobuf services get to cheat: their schemas are known at build time. \u003ca href=\"https://pkg.go.dev/google.golang.org/protobuf/cmd/protoc-gen-go\" rel=\"external\"\u003e\u003ccode\u003eprotoc-gen-go\u003c/code\u003e\u003c/a\u003e turns those schemas into concrete message types, accessor methods, and runtime metadata that the protobuf runtime can use efficiently.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://fauxrpc.com\" rel=\"external\"\u003eFauxRPC\u003c/a\u003e does not get that luxury. It loads user-provided schemas at runtime so it can mock arbitrary gRPC, gRPC-Web, and Connect services without asking users to install \u003ccode\u003eprotoc\u003c/code\u003e, Buf, or a Go toolchain first. Without generated Go types for each schema, it has to parse and inspect payloads dynamically. This is also the case for tools like \u003ccode\u003ebuf\u003c/code\u003e, \u003ccode\u003egrpcurl\u003c/code\u003e, \u003ccode\u003ekreya\u003c/code\u003e, \u003ccode\u003epostman\u003c/code\u003e, and more.\u003c/p\u003e\n\u003cp\u003eThat flexibility pushed the request path toward Go\u0026rsquo;s standard \u003ca href=\"https://pkg.go.dev/google.golang.org/protobuf/types/dynamicpb\" rel=\"external\"\u003e\u003ccode\u003edynamicpb\u003c/code\u003e\u003c/a\u003e package. \u003ccode\u003edynamicpb\u003c/code\u003e is flexible, but it pays for that flexibility with extra allocations, descriptor lookups, and reflection-heavy access paths.\u003c/p\u003e\n\u003cp\u003eBuf introduced \u003ca href=\"https://github.com/bufbuild/hyperpb\" rel=\"external\"\u003e\u003ccode\u003ehyperpb\u003c/code\u003e\u003c/a\u003e, a dynamic Protobuf parser that compiles descriptors into optimized parser bytecode at runtime. I wanted to see whether it could make FauxRPC\u0026rsquo;s read path faster without giving up runtime-loaded schemas.\u003c/p\u003e\n\u003cp\u003eThe short version: yes, with caveats. \u003ccode\u003ehyperpb\u003c/code\u003e is read-only, so FauxRPC still uses \u003ccode\u003edynamicpb\u003c/code\u003e to build mock responses. But for parsing incoming requests, the performance difference was large enough to be worth the split.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"reflection-based-dynamicpb\"\u003eReflection-Based dynamicpb\u003c/h2\u003e\n\u003cp\u003eGo\u0026rsquo;s standard \u003ccode\u003edynamicpb\u003c/code\u003e package takes a \u003ccode\u003eprotoreflect.MessageDescriptor\u003c/code\u003e and constructs a dynamic message representation at runtime.\u003c/p\u003e\n\u003cp\u003eSince message layouts are only known at runtime, \u003ccode\u003edynamicpb\u003c/code\u003e must route field access through descriptors and generic message representations rather than generated message types and runtime metadata. This introduces additional indirection and dynamic dispatch. The cost mostly shows up in two places:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eIt allocates a lot.\u003c/strong\u003e Nested messages, repeated fields, map entries, and interface values turn into a pile of heap objects. This causes severe garbage collector (GC) pressure.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIt chases pointers.\u003c/strong\u003e Once the decoded message is spread across many small objects, the CPU spends more time bouncing around memory instead of reading predictable, contiguous data.\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch2 id=\"bytecode-compilation-with-hyperpb\"\u003eBytecode Compilation with hyperpb\u003c/h2\u003e\n\u003cp\u003eBuf\u0026rsquo;s \u003ccode\u003ehyperpb\u003c/code\u003e library takes a different approach. It compiles the message descriptor into dedicated, optimized table-driven parser bytecode at runtime.\u003c/p\u003e\n\u003cp\u003eThe parser avoids Go’s reflection-heavy dynamic message construction in the unmarshalling hot path, while still exposing the parsed result through the standard \u003ccode\u003eprotoreflect\u003c/code\u003e APIs. In many cases, it can parse dynamic payloads at speeds close to, or even faster than, generated Go protobuf messages.\u003c/p\u003e\n\u003ch3 id=\"pre-compiling-at-runtime\"\u003ePre-Compiling at Runtime\u003c/h3\u003e\n\u003cp\u003eBecause \u003ccode\u003ehyperpb\u003c/code\u003e uses a custom VM under the hood, it requires a compilation phase before you can parse any payloads. Similar to compiling a regular expression with Go\u0026rsquo;s \u003ccode\u003eregexp.Compile\u003c/code\u003e, you must compile the schema definition at runtime. \u003ccode\u003ehyperpb.CompileFileDescriptorSet\u003c/code\u003e compiles a specific message type out of a \u003ccode\u003eFileDescriptorSet\u003c/code\u003e, while \u003ccode\u003ehyperpb.CompileMessageDescriptor\u003c/code\u003e compiles an already-resolved message descriptor:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Done once at startup/initialization\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ehyperMsgType \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e hyperpb\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eCompileMessageDescriptor\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003emessageDesc\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis compilation cost is paid once per message type, so the compiled types should be cached and reused.\u003c/p\u003e\n\u003ch3 id=\"moving-allocation-out-of-the-hot-path\"\u003eMoving Allocation Out of the Hot Path\u003c/h3\u003e\n\u003cp\u003eThe real story is not just speed. It is allocation behavior.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003ehyperpb\u003c/code\u003e provides a reusable, pre-allocated memory arena pool through \u003ccode\u003ehyperpb.Shared\u003c/code\u003e. Pairing bytecode parsing with a reusable memory arena allows you to recycle memory buffers across multiple requests. This removes most of the per-message heap churn for read-only pipelines:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eshared \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003enew\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003ehyperpb\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eShared\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Instantiated once per goroutine/worker\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003efor\u003c/span\u003e _\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e payload \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003erange\u003c/span\u003e incoming \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Reuses the underlying pre-allocated memory arena\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    msg \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e shared\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eNewMessage\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003emType\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    _ \u003cspan style=\"color:#eceff4\"\u003e=\u003c/span\u003e proto\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eUnmarshal\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003epayload\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e msg\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#88c0d0\"\u003eroute\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003emsg\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e    \u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Note: Must be handled synchronously and complete before Free()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    shared\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFree\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e()\u003c/span\u003e \u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Recycles the arena back to the pool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"warning-box\"\u003e\n  Because the data fields in \u003ccode\u003emsg\u003c/code\u003e are backed by the pre-allocated pool\u0026rsquo;s memory arena, any references to those fields become invalid (and will read corrupted data or panic) after \u003ccode\u003eshared.Free()\u003c/code\u003e is called. The processing pipeline must handle the message completely synchronously (e.g., no asynchronous routing, lazy field reading, or passing to background goroutines) before the arena is recycled.\n\u003c/div\u003e\n\u003chr\u003e\n\u003ch2 id=\"dynamic-reflection-in-practice\"\u003eDynamic Reflection in Practice\u003c/h2\u003e\n\u003cp\u003eTo see how \u003ccode\u003edynamicpb\u003c/code\u003e and \u003ccode\u003ehyperpb\u003c/code\u003e compare in code, we can use the classic ConnectRPC/Buf \u003ca href=\"https://buf.build/connectrpc/eliza\" rel=\"external\"\u003eEliza service\u003c/a\u003e schema.\u003c/p\u003e\n\u003cp\u003eA complete set of runnable examples is available in the \u003ca href=\"https://github.com/sudorandom/kmcd.dev/tree/main/content/posts/2026/dynamic-protobuf-in-go/go\" rel=\"external\"\u003edynamic-protobuf-in-go/go\u003c/a\u003e directory. It uses \u003ccode\u003ebuf\u003c/code\u003e to compile the Protobuf definitions into a binary descriptor set, which is then loaded at runtime to perform dynamic serialization and reflection.\u003c/p\u003e\n\u003ch3 id=\"1-compiling-protobuf-descriptors-with-buf\"\u003e1. Compiling Protobuf Descriptors with Buf\u003c/h3\u003e\n\u003cp\u003eBefore using dynamic messages, we must compile the \u003ccode\u003e.proto\u003c/code\u003e schema into a \u003ccode\u003eFileDescriptorSet\u003c/code\u003e (a serialized binary image of the schemas). Using the Buf CLI, this is done with a single command:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ebuf build -o eliza.binpb\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"2-loading-descriptors-at-runtime\"\u003e2. Loading Descriptors at Runtime\u003c/h3\u003e\n\u003cp\u003eIn Go, we read this descriptor set, unmarshal it into a \u003ccode\u003edescriptorpb.FileDescriptorSet\u003c/code\u003e, and load it into a \u003ccode\u003eprotoregistry.Files\u003c/code\u003e registry:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Read compiled schema descriptors\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tdescriptorBytes\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e os\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eReadFile\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;eliza.binpb\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tlog\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFatalf\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;failed to read descriptor file (did you run \u0026#39;buf build -o eliza.binpb\u0026#39;?): %v\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003evar\u003c/span\u003e fds descriptorpb\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eFileDescriptorSet\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e proto\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eUnmarshal\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003edescriptorBytes\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003e\u0026amp;\u003c/span\u003efds\u003cspan style=\"color:#eceff4\"\u003e);\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tlog\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFatalf\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;failed to unmarshal file descriptor set: %v\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Register files\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tregistry\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e protodesc\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eNewFiles\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#81a1c1\"\u003e\u0026amp;\u003c/span\u003efds\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tlog\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFatalf\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;failed to create protodesc registry: %v\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eOnce registered, we can look up the message descriptor by its full name and locate individual fields dynamically:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Retrieve message descriptor for Eliza\u0026#39;s SayRequest\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tsayRequestName \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e protoreflect\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFullName\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;connectrpc.eliza.v1.SayRequest\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tdesc\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e registry\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFindDescriptorByName\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003esayRequestName\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tlog\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFatalf\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;failed to find descriptor for %s: %v\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e sayRequestName\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tsayRequestDesc\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e ok \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e desc\u003cspan style=\"color:#eceff4\"\u003e.(\u003c/span\u003eprotoreflect\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eMessageDescriptor\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e!\u003c/span\u003eok \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tlog\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFatalf\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;descriptor for %s is not a message descriptor\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e sayRequestName\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tsentenceField \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e sayRequestDesc\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFields\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eByName\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;sentence\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e sentenceField \u003cspan style=\"color:#81a1c1\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tlog\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFatalf\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;failed to find \u0026#39;sentence\u0026#39; field in %s\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e sayRequestName\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"3-dynamic-access-with-dynamicpb\"\u003e3. Dynamic Access with dynamicpb\u003c/h3\u003e\n\u003cp\u003eStandard \u003ccode\u003edynamicpb\u003c/code\u003e creates dynamic messages that support both reading and writing field values. The standard \u003ccode\u003eprotoreflect\u003c/code\u003e interface is used for field access:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Create request message dynamically\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tdynMsg \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e dynamicpb\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eNewMessage\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003esayRequestDesc\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Set field dynamically using the reflection interface\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tdynMsg\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eProtoReflect\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eSet\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003esentenceField\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e protoreflect\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eValueOfString\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;Hello Eliza, how are you?\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Marshal the message to binary wire format\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\twireBytes\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e proto\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eMarshal\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003edynMsg\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tlog\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFatalf\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;dynamicpb: failed to marshal message: %v\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tfmt\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003ePrintf\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;Serialized bytes: %x\\n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e wireBytes\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Unmarshal wire format back into a new dynamicpb message\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tdynMsg2 \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e dynamicpb\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eNewMessage\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003esayRequestDesc\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e proto\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eUnmarshal\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003ewireBytes\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e dynMsg2\u003cspan style=\"color:#eceff4\"\u003e);\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tlog\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFatalf\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;dynamicpb: failed to unmarshal message: %v\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Get field dynamically\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tval \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e dynMsg2\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eProtoReflect\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eGet\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003esentenceField\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tfmt\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003ePrintf\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;Decoded message: %s\\n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e val\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eString\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"4-high-performance-read-only-access-with-hyperpb\"\u003e4. High-Performance Read-Only Access with hyperpb\u003c/h3\u003e\n\u003cp\u003eBecause \u003ccode\u003ehyperpb\u003c/code\u003e is built for high-performance ingestion and routing, it only supports \u003cstrong\u003eread-only\u003c/strong\u003e access. Message descriptors must be compiled into optimized parser bytecode, and any attempt to write or mutate a message will panic:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Compile the descriptor into hyperpb optimized message type\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Note: You should compile descriptors once at startup or pool them, not per request.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\thyperMsgType \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e hyperpb\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eCompileMessageDescriptor\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003esayRequestDesc\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Instantiate hyperpb message\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\thyperMsg \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e hyperpb\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eNewMessage\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003ehyperMsgType\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Unmarshal wire bytes into it (hyperpb parses without Go reflection overhead)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e proto\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eUnmarshal\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003ewireBytes\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e hyperMsg\u003cspan style=\"color:#eceff4\"\u003e);\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\tlog\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFatalf\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;hyperpb: failed to unmarshal message: %v\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\u003cspan style=\"color:#616e87;font-style:italic\"\u003e// Get field dynamically from hyperpb message using standard protoreflect API\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\thyperVal \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e hyperMsg\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eProtoReflect\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e().\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eGet\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003esentenceField\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tfmt\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003ePrintf\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;Decoded message: %s\\n\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e hyperVal\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eString\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen running these examples (with \u003ccode\u003ego run .\u003c/code\u003e), we get the following output, verifying that both implementations decode the reflection values identically:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e--- Step 1: dynamicpb (Standard Go Reflection) ---\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eSerialized bytes: 0a1948656c6c6f20456c697a612c20686f772061726520796f753f\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eDecoded message: Hello Eliza, how are you?\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e--- Step 2: hyperpb (Table-Driven Bytecode VM) ---\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eDecoded message: Hello Eliza, how are you?\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e--- Step 3: hyperpb + Shared (Memory Reuse Arena) ---\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eDecoded message: Hello Eliza, how are you?\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eMemory arena recycled.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBy using the exact same standard \u003ccode\u003eprotoreflect\u003c/code\u003e interface, \u003ccode\u003ehyperpb\u003c/code\u003e acts as a drop-in replacement for downstream read operations while executing faster and allocating much less in these benchmarks. So I wanted to check whether this held up in my own tiny benchmark goblin cave.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"performance-evaluation\"\u003ePerformance Evaluation\u003c/h2\u003e\n\u003cp\u003eI benchmarked three dynamic parsing strategies against statically generated Go Protobuf code to measure the difference in this setup:\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eVariant\u003c/th\u003e\n          \u003cth style=\"text-align: left\"\u003eDescription\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003edynamicpb\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eEvaluates dynamic descriptor parsing and reflection-based Protobuf handling using Go\u0026rsquo;s standard \u003ca href=\"https://pkg.go.dev/google.golang.org/protobuf/types/dynamicpb\" rel=\"external\"\u003e\u003ccode\u003edynamicpb\u003c/code\u003e\u003c/a\u003e package.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eEvaluates dynamic parsing using Buf\u0026rsquo;s table-driven \u003ca href=\"https://github.com/bufbuild/hyperpb\" rel=\"external\"\u003e\u003ccode\u003ehyperpb\u003c/code\u003e\u003c/a\u003e library.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb + Shared\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eEvaluates dynamic parsing using \u003ccode\u003ehyperpb\u003c/code\u003e paired with a reusable \u003ccode\u003ehyperpb.Shared\u003c/code\u003e memory arena to recycle allocations.\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eConcrete (proto)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eStatically compiled Go Protobuf code (provided as a baseline comparison).\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eConcrete (vtproto)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003eStatically compiled, reflection-free PlanetScale \u003ca href=\"https://github.com/planetscale/vtproto\" rel=\"external\"\u003e\u003ccode\u003evtproto\u003c/code\u003e\u003c/a\u003e code (provided as a baseline comparison).\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eThe benchmarks evaluate performance across three payload scales:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSmall\u003c/strong\u003e: A flat message with 4 primitive fields (ID, status, age, score).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMedium\u003c/strong\u003e: A nested event message containing an actor object, tags, and a metadata map.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLarge\u003c/strong\u003e: An array repeating the Medium event 100 times.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe source code and setup for these benchmarks are available in the \u003ca href=\"https://github.com/sudorandom/kmcd.dev/tree/main/content/posts/2026/dynamic-protobuf-in-go/benchmarks\" rel=\"external\"\u003edynamic-protobuf-in-go/benchmarks\u003c/a\u003e directory.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eAll benchmarks were executed on an Apple M1 Pro (\u003ccode\u003edarwin/arm64\u003c/code\u003e) using Go 1.26. Descriptor compilation was excluded from timing and performed once during benchmark initialization. Measurements represent deserialization only (\u003ccode\u003eproto.Unmarshal\u003c/code\u003e) and were collected using \u003ccode\u003ego test -bench=. -benchmem\u003c/code\u003e.\u003c/em\u003e\u003c/p\u003e\n\u003ch3 id=\"benchmark-results\"\u003eBenchmark Results\u003c/h3\u003e\n\u003cdiv class=\"tabs-wrapper\" id=\"tabs-60432\"\u003e\n  \u003cdiv class=\"tabs-nav\" role=\"tablist\"\u003e\u003cbutton class=\"tab-btn\" \n              role=\"tab\" \n              aria-selected=\"false\" \n              aria-controls=\"panel-small-payload-70863\" \n              id=\"tab-btn-small-payload-70863\"\n              data-tab=\"small-payload-70863\"\u003e\n        Small Payload\n      \u003c/button\u003e\u003cbutton class=\"tab-btn active\" \n              role=\"tab\" \n              aria-selected=\"true\" \n              aria-controls=\"panel-medium-payload-94698\" \n              id=\"tab-btn-medium-payload-94698\"\n              data-tab=\"medium-payload-94698\"\u003e\n        Medium Payload\n      \u003c/button\u003e\u003cbutton class=\"tab-btn\" \n              role=\"tab\" \n              aria-selected=\"false\" \n              aria-controls=\"panel-large-payload-10842\" \n              id=\"tab-btn-large-payload-10842\"\n              data-tab=\"large-payload-10842\"\u003e\n        Large Payload\n      \u003c/button\u003e\u003c/div\u003e\n  \u003cdiv class=\"tabs-panels\"\u003e\n  \u003cdiv class=\"tab-panel-content\" id=\"panel-small-payload-70863\" role=\"tabpanel\" style=\"display: none;\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"dbd6f31a46de2b01\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('dbd6f31a46de2b01');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"dynamicpb\",\n      \"hyperpb\",\n      \"hyperpb + Shared\",\n      \"Concrete (proto)\",\n      \"Concrete (vtproto)\"\n    ],\n    \"datasets\": [{\n      \"label\": \"ns/op\",\n      \"data\": [622, 334, 120, 113, 25],\n      \"backgroundColor\": [\n        \"rgba(148, 163, 184, 0.75)\",\n        \"rgba(168, 85, 247, 0.75)\",\n        \"rgba(168, 85, 247, 0.75)\",\n        \"rgba(0, 191, 255, 0.75)\",\n        \"rgba(0, 191, 255, 0.75)\"\n      ],\n      \"borderColor\": [\n        \"rgba(148, 163, 184, 1)\",\n        \"rgba(168, 85, 247, 1)\",\n        \"rgba(168, 85, 247, 1)\",\n        \"rgba(0, 191, 255, 1)\",\n        \"rgba(0, 191, 255, 1)\"\n      ],\n      \"borderWidth\": 1\n    }]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Dynamic Parsing Performance (Small Payload): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"labels\": { \"color\": \"#fff\" },\n        \"customLegend\": [\n          { \"text\": \"dynamicpb\", \"color\": \"rgba(148, 163, 184, 0.75)\" },\n          { \"text\": \"hyperpb\", \"color\": \"rgba(168, 85, 247, 0.75)\" },\n          { \"text\": \"protobuf\", \"color\": \"rgba(0, 191, 255, 0.75)\" }\n        ]\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": { \"color\": \"#fff\" }\n      },\n      \"y\": {\n        \"ticks\": { \"color\": \"#fff\" }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eBenchmark (Small Payload)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eConcrete (vtproto)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e25 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e16 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eConcrete (proto)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e113 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e96 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb + Shared\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e120 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e65 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e334 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e798 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e4\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003edynamicpb\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e622 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e616 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e11\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n  \u003cdiv class=\"tab-panel-content\" id=\"panel-medium-payload-94698\" role=\"tabpanel\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"fbbb7ff6515790b8\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('fbbb7ff6515790b8');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"dynamicpb\",\n      \"hyperpb\",\n      \"Concrete (proto)\",\n      \"Concrete (vtproto)\",\n      \"hyperpb + Shared\"\n    ],\n    \"datasets\": [{\n      \"label\": \"ns/op\",\n      \"data\": [2368, 600, 564, 306, 286],\n      \"backgroundColor\": [\n        \"rgba(148, 163, 184, 0.75)\",\n        \"rgba(168, 85, 247, 0.75)\",\n        \"rgba(0, 191, 255, 0.75)\",\n        \"rgba(0, 191, 255, 0.75)\",\n        \"rgba(168, 85, 247, 0.75)\"\n      ],\n      \"borderColor\": [\n        \"rgba(148, 163, 184, 1)\",\n        \"rgba(168, 85, 247, 1)\",\n        \"rgba(0, 191, 255, 1)\",\n        \"rgba(0, 191, 255, 1)\",\n        \"rgba(168, 85, 247, 1)\"\n      ],\n      \"borderWidth\": 1\n    }]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Dynamic Parsing Performance (Medium Payload): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"labels\": { \"color\": \"#fff\" },\n        \"customLegend\": [\n          { \"text\": \"dynamicpb\", \"color\": \"rgba(148, 163, 184, 0.75)\" },\n          { \"text\": \"hyperpb\", \"color\": \"rgba(168, 85, 247, 0.75)\" },\n          { \"text\": \"protobuf\", \"color\": \"rgba(0, 191, 255, 0.75)\" }\n        ]\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": { \"color\": \"#fff\" }\n      },\n      \"y\": {\n        \"ticks\": { \"color\": \"#fff\" }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eBenchmark (Medium Payload)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb + Shared\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e286 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e356 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eConcrete (vtproto)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e306 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e432 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e14\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eConcrete (proto)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e564 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e560 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e15\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e600 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,444 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e5\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003edynamicpb\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,368 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e2,072 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e43\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n  \u003cdiv class=\"tab-panel-content\" id=\"panel-large-payload-10842\" role=\"tabpanel\" style=\"display: none;\"\u003e\n  \u003cdiv class=\"chart-wrapper\"\u003e\n\u003cdiv class=\"chart\"\u003e\n\u003ccanvas id=\"5e4f66804f293e93\" data-sort=\"true\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\u003c/div\u003e\n\u003cscript\u003e\n(function() {\nwindow.chartJsPromise = window.chartJsPromise || new Promise((resolve, reject) =\u003e {\nif (window.Chart) {\nresolve(window.Chart);\nreturn;\n}\nconst script = document.createElement('script');\nscript.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\nscript.async = true;\nscript.onload = () =\u003e resolve(window.Chart);\nscript.onerror = () =\u003e reject(new Error(\"Failed to load Chart.js\"));\ndocument.head.appendChild(script);\n});\nwindow.activeCharts = window.activeCharts || [];\nfunction getThemeColors() {\nreturn {\ntext: '#ffffff',\ngrid: 'rgba(255, 255, 255, 0.1)'\n};\n}\nfunction applyThemeToOptions(options, colors) {\nif (!options) return;\nif (!options.plugins) options.plugins = {};\nif (options.plugins.title) {\noptions.plugins.title.color = colors.text;\n}\nif (!options.plugins.legend) options.plugins.legend = {};\nif (!options.plugins.legend.labels) options.plugins.legend.labels = {};\noptions.plugins.legend.labels.color = colors.text;\nif (options.scales) {\nfor (const scaleId in options.scales) {\nconst scale = options.scales[scaleId];\nif (scale \u0026\u0026 typeof scale === 'object') {\nif (!scale.ticks) scale.ticks = {};\nscale.ticks.color = colors.text;\nif (!scale.grid) scale.grid = {};\nscale.grid.color = colors.grid;\n}\n}\n}\n}\nif (!window.chartThemeObserver) {\nwindow.chartThemeObserver = new MutationObserver((mutations) =\u003e {\nmutations.forEach((mutation) =\u003e {\nif (mutation.attributeName === 'data-theme') {\nconst colors = getThemeColors();\nwindow.activeCharts.forEach((chart) =\u003e {\napplyThemeToOptions(chart.options, colors);\nchart.update();\n});\n}\n});\n});\nwindow.chartThemeObserver.observe(document.documentElement, { attributes: true });\n}\nwindow.chartJsPromise.then((Chart) =\u003e {\nconst ctx = document.getElementById('5e4f66804f293e93');\nif (!ctx) return;\nconst chartConfig = \n{\n  \"type\": \"bar\",\n  \"data\": {\n    \"labels\": [\n      \"dynamicpb\",\n      \"Concrete (proto)\",\n      \"Concrete (vtproto)\",\n      \"hyperpb\",\n      \"hyperpb + Shared\"\n    ],\n    \"datasets\": [{\n      \"label\": \"ns/op\",\n      \"data\": [241009, 54078, 35350, 24664, 17967],\n      \"backgroundColor\": [\n        \"rgba(148, 163, 184, 0.75)\",\n        \"rgba(0, 191, 255, 0.75)\",\n        \"rgba(0, 191, 255, 0.75)\",\n        \"rgba(168, 85, 247, 0.75)\",\n        \"rgba(168, 85, 247, 0.75)\"\n      ],\n      \"borderColor\": [\n        \"rgba(148, 163, 184, 1)\",\n        \"rgba(0, 191, 255, 1)\",\n        \"rgba(0, 191, 255, 1)\",\n        \"rgba(168, 85, 247, 1)\",\n        \"rgba(168, 85, 247, 1)\"\n      ],\n      \"borderWidth\": 1\n    }]\n  },\n  \"options\": {\n    \"indexAxis\": \"y\",\n    \"plugins\": {\n      \"title\": {\n        \"display\": true,\n        \"text\": \"Dynamic Parsing Performance (Large Payload): lower is better\",\n        \"color\": \"#fff\"\n      },\n      \"legend\": {\n        \"labels\": { \"color\": \"#fff\" },\n        \"customLegend\": [\n          { \"text\": \"dynamicpb\", \"color\": \"rgba(148, 163, 184, 0.75)\" },\n          { \"text\": \"hyperpb\", \"color\": \"rgba(168, 85, 247, 0.75)\" },\n          { \"text\": \"protobuf\", \"color\": \"rgba(0, 191, 255, 0.75)\" }\n        ]\n      }\n    },\n    \"scales\": {\n      \"x\": {\n        \"type\": \"linear\",\n        \"min\": 0,\n        \"ticks\": { \"color\": \"#fff\" }\n      },\n      \"y\": {\n        \"ticks\": { \"color\": \"#fff\" }\n      }\n    }\n  }\n};\nconst isMobileWidth = window.innerWidth \u003e= 600 ? false : true;\nif (!chartConfig.options) chartConfig.options = {};\nif (chartConfig.options.responsive === undefined) chartConfig.options.responsive = true;\nif (chartConfig.options.maintainAspectRatio === undefined) chartConfig.options.maintainAspectRatio = false;\nif (isMobileWidth \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.labels) {\nchartConfig.data.labels = chartConfig.data.labels.map(function(label) {\nif (typeof label === 'string') {\nreturn label\n.replace('google.protobuf.Value', 'g.p.Value')\n.replace('google.protobuf.Any', 'g.p.Any')\n.replace('google.protobuf.', 'g.p.');\n}\nreturn label;\n});\n}\nif (isMobileWidth) {\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nif (!chartConfig.options.scales.x.ticks) chartConfig.options.scales.x.ticks = {};\nif (!chartConfig.options.scales.x.ticks.font) chartConfig.options.scales.x.ticks.font = {};\nchartConfig.options.scales.x.ticks.font.size = 10;\nif (!chartConfig.options.scales.y) chartConfig.options.scales.y = {};\nif (!chartConfig.options.scales.y.ticks) chartConfig.options.scales.y.ticks = {};\nif (!chartConfig.options.scales.y.ticks.font) chartConfig.options.scales.y.ticks.font = {};\nchartConfig.options.scales.y.ticks.font.size = 10;\n}\nfunction adjustHeight() {\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nconst barCount = (chartConfig.data \u0026\u0026 chartConfig.data.labels) ? chartConfig.data.labels.length : 0;\nlet chartHeight = 350;\nif (chartConfig.type === 'bar' \u0026\u0026 chartConfig.options.indexAxis === 'y') {\nconst heightPerBar = isMobile ? 35 : 30;\nconst extraPadding = isMobile ? 130 : 100;\nchartHeight = Math.max(300, (barCount * heightPerBar) + extraPadding);\n} else {\nchartHeight = isMobile ? 320 : 380;\n}\nctx.parentElement.style.height = chartHeight + 'px';\n}\nadjustHeight();\nwindow.addEventListener('resize', adjustHeight);\nif (chartConfig.options \u0026\u0026 chartConfig.options.plugins \u0026\u0026 chartConfig.options.plugins.legend \u0026\u0026 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() {}; }\nconst colors = getThemeColors();\napplyThemeToOptions(chartConfig.options, colors);\nconst customLabelPlugin = {\nid: 'customLabelPlugin',\nafterDatasetsDraw: function(chart) {\nif (chart.config.type !== 'bar') return;\nconst ctx = chart.ctx;\nconst canvasEl = chart.canvas;\nconst unit = canvasEl.getAttribute('data-unit');\nchart.data.datasets.forEach(function(dataset, datasetIndex) {\nconst meta = chart.getDatasetMeta(datasetIndex);\nmeta.data.forEach(function(bar, index) {\nconst value = dataset.data[index];\nif (value === undefined || value === null) return;\nif (bar \u0026\u0026 typeof bar.x === 'number' \u0026\u0026 chart.isDatasetVisible(datasetIndex)) {\nctx.fillStyle = '#ffffff';\nctx.textAlign = 'left';\nctx.textBaseline = 'middle';\nconst isMobile = window.innerWidth \u003e= 600 ? false : true;\nctx.font = isMobile ? 'bold 10px sans-serif' : 'bold 11px sans-serif';\nlet text = value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });\nif (unit) {\ntext += ' ' + unit;\n} else {\nconst label = dataset.label || '';\nif (label.toLowerCase().indexOf('bytes') !== -1) {\ntext += ' B';\n} else if (label.toLowerCase().indexOf('ns') !== -1) {\ntext += ' ns';\n}\n}\nctx.fillText(text, bar.x + 6, bar.y);\n}\n});\n});\n}\n};\nchartConfig.plugins = chartConfig.plugins || [];\nchartConfig.plugins.push(customLabelPlugin);\nif (!chartConfig.options.scales) chartConfig.options.scales = {};\nif (!chartConfig.options.scales.x) chartConfig.options.scales.x = {};\nchartConfig.options.scales.x.grace = isMobileWidth ? '25%' : '15%';\nconst canvasEl = ctx;\nconst sortAttr = canvasEl.getAttribute('data-sort');\nif (sortAttr !== 'false' \u0026\u0026 chartConfig.type === 'bar' \u0026\u0026 chartConfig.data \u0026\u0026 chartConfig.data.datasets \u0026\u0026 chartConfig.data.datasets[0]) {\nconst dataset = chartConfig.data.datasets[0];\nconst label = (dataset.label || '').toLowerCase();\nlet sortOrder = 'asc';\nif (label.indexOf('rps') !== -1 || label.indexOf('throughput') !== -1 || sortAttr === 'desc') {\nsortOrder = 'desc';\n}\nif (chartConfig.data.labels \u0026\u0026 dataset.data \u0026\u0026 chartConfig.data.labels.length === dataset.data.length) {\nlet items = chartConfig.data.labels.map(function(label, index) {\nreturn {\nlabel: label,\nvalue: dataset.data[index],\nbackgroundColor: Array.isArray(dataset.backgroundColor) ? dataset.backgroundColor[index] : dataset.backgroundColor,\nborderColor: Array.isArray(dataset.borderColor) ? dataset.borderColor[index] : dataset.borderColor\n};\n});\nitems.sort(function(a, b) {\nif (sortOrder === 'desc') {\nreturn b.value - a.value;\n}\nreturn a.value - b.value;\n});\nchartConfig.data.labels = items.map(function(item) { return item.label; });\ndataset.data = items.map(function(item) { return item.value; });\nif (Array.isArray(dataset.backgroundColor)) {\ndataset.backgroundColor = items.map(function(item) { return item.backgroundColor; });\n}\nif (Array.isArray(dataset.borderColor)) {\ndataset.borderColor = items.map(function(item) { return item.borderColor; });\n}\n}\n}\nconst chart = new Chart(ctx, chartConfig);\nwindow.activeCharts.push(chart);\nif (window.onChartInit) {\nwindow.onChartInit(chart);\n}\n}, (err) =\u003e {\nconsole.error(\"Chart.js loading error: \", err);\n});\n})();\n\u003c/script\u003e\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eShow data table\u003c/b\u003e\u003c/summary\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: left\"\u003eBenchmark (Large Payload)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003ens/op\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eMemory (B/op)\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003eAllocations/op\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb + Shared\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e17,967 ns\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e21,848 B\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e\u003cstrong\u003e1\u003c/strong\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003ehyperpb\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e24,664 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e60,013 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e12\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eConcrete (vtproto)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e35,350 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e58,168 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,508\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003eConcrete (proto)\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e54,078 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e58,232 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e1,509\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: left\"\u003e\u003cstrong\u003edynamicpb\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e241,009 ns\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e205,753 B\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e4,117\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003c/details\u003e\n\n\u003c/div\u003e\n\n\u003c/div\u003e\n\u003c/div\u003e\n\n\u003chr\u003e\n\u003ch2 id=\"analysis-speed-and-memory-efficiency\"\u003eAnalysis: Speed and Memory Efficiency\u003c/h2\u003e\n\u003cp\u003eThe numbers get more interesting as the payloads get larger.\u003c/p\u003e\n\u003ch3 id=\"1-execution-speedup\"\u003e1. Execution Speedup\u003c/h3\u003e\n\u003cp\u003eOn a \u003cstrong\u003eLarge Payload\u003c/strong\u003e, reflection-based \u003ccode\u003edynamicpb\u003c/code\u003e takes 241,009 ns.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eStandard \u003ccode\u003ehyperpb\u003c/code\u003e executes in 24,664 ns (a 9.7x speedup).\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ehyperpb + Shared\u003c/code\u003e executes in 17,967 ns (a 13.4x speedup).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cem\u003eNote: These benchmarks measure parsing and deserialization costs only. Work performed after unmarshalling (such as downstream data manipulation or field access) is excluded.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eInterestingly, both \u003ccode\u003ehyperpb\u003c/code\u003e configurations outperform compile-time generated static Protobuf code (\u003ccode\u003eConcrete (proto)\u003c/code\u003e at 54,078 ns and reflection-free \u003ccode\u003eConcrete (vtproto)\u003c/code\u003e at 35,350 ns). That is the part that surprised me. It does not mean \u003ccode\u003ehyperpb\u003c/code\u003e\u0026rsquo;s parser engine is inherently faster than generated Go code. Rather, the combination of bytecode parsing and arena-backed allocation reduces object creation costs for large nested payloads. Standard generated Protobuf still allocates individual heap objects for the nested sub-messages in this benchmark. The memory arena in \u003ccode\u003ehyperpb + Shared\u003c/code\u003e allocates this memory contiguously.\u003c/p\u003e\n\u003cp\u003eIn these benchmarks, this crossover point, where dynamic parsing paired with a memory arena beats statically compiled generated code, occurs even at the \u003cstrong\u003eMedium Payload\u003c/strong\u003e scale. At that size, \u003ccode\u003ehyperpb + Shared\u003c/code\u003e (286 ns) already edges out reflection-free \u003ccode\u003eConcrete (vtproto)\u003c/code\u003e (306 ns).\u003c/p\u003e\n\u003ch3 id=\"2-reducing-heap-allocations\"\u003e2. Reducing Heap Allocations\u003c/h3\u003e\n\u003cp\u003eThe allocation statistics highlight the biggest architectural advantage. On a Large Payload:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003edynamicpb\u003c/code\u003e: 4,117 heap allocations per message.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eConcrete (proto)\u003c/code\u003e: 1,509 heap allocations.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ehyperpb\u003c/code\u003e: 12 heap allocations.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ehyperpb + Shared\u003c/code\u003e: 1 heap allocation. (The remaining allocation appears to come from the top-level message pointer escaping to the heap as a \u003ccode\u003eproto.Message\u003c/code\u003e interface wrapper. Because the standard \u003ccode\u003eproto.Unmarshal\u003c/code\u003e signature requires passing an interface, this top-level escape cannot easily be avoided and prevents hitting an absolute zero allocation count.)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eFor hot-path event routing or proxying services, fewer allocations should translate into less GC pressure, which can help with CPU usage and tail latency under real load.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"when-to-use-hyperpb\"\u003eWhen to Use hyperpb\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003ehyperpb\u003c/code\u003e is built for specific use cases. It is not a universal drop-in replacement for standard Go Protobuf code.\u003c/p\u003e\n\u003ch3 id=\"ideal-use-cases\"\u003eIdeal Use Cases\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eDynamic Gateways \u0026amp; Proxies\u003c/strong\u003e: Systems receiving dynamic schemas at runtime that must inspect or forward payloads without ahead-of-time code generation.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDeveloper Tooling\u003c/strong\u003e: Tools like FauxRPC that mock interfaces, fuzz test services, or interact dynamically with user-supplied schemas.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHigh-Throughput Pipelines\u003c/strong\u003e: Pipelines with dynamic schemas where reflection overhead is a bottleneck.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"trade-offs-and-constraints\"\u003eTrade-offs and Constraints\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eExperimental API\u003c/strong\u003e: \u003ccode\u003ehyperpb\u003c/code\u003e is still pre-v1, so I would avoid wrapping it deeply into public APIs without a small compatibility layer.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePlatform Specificity\u003c/strong\u003e: \u003ccode\u003ehyperpb\u003c/code\u003e relies on specialized runtime assembly and bytecode generators tailored for 64-bit little-endian architectures. It is officially supported only on \u003ccode\u003eamd64\u003c/code\u003e and \u003ccode\u003earm64\u003c/code\u003e platforms. Compiling for other architectures requires the manual build tag \u003ccode\u003ehyperpb.unsupported\u003c/code\u003e, which compiles a slower generic parser backend.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRead-Only vs Mutable\u003c/strong\u003e: Reusing buffers via \u003ccode\u003ehyperpb.Shared\u003c/code\u003e works best for read-only access pipelines. If you need to mutate the parsed message or pass it asynchronously to other goroutines, you must copy the data or avoid using the shared arena. This increases allocations, though still resulting in fewer than standard \u003ccode\u003edynamicpb\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAOT Compilation\u003c/strong\u003e: If your schema is known at build time, compiling your Protobuf definitions remains the best approach. Static compilation (\u003ccode\u003evtproto\u003c/code\u003e or standard \u003ccode\u003eproto\u003c/code\u003e) offers strict type safety and requires no runtime bytecode compilation overhead.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"conclusion\"\u003eConclusion\u003c/h2\u003e\n\u003cp\u003eFor FauxRPC, the interesting part of \u003ccode\u003ehyperpb\u003c/code\u003e is that it offers a way to speed up the read-heavy parts of dynamic schema handling.\u003c/p\u003e\n\u003cp\u003eEven with only the request path changed, the difference was not subtle. FauxRPC still uses \u003ccode\u003edynamicpb\u003c/code\u003e where it needs to build and mutate response messages. But for request parsing, where payloads are read-only, \u003ccode\u003ehyperpb\u003c/code\u003e is a nice optimization. The benchmarks show a clear difference: fewer heap objects, less GC pressure, and faster unmarshalling on larger payloads.\u003c/p\u003e\n\u003cp\u003eIf your pipeline fits those constraints, \u003ccode\u003ehyperpb\u003c/code\u003e is worth trying.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"references--further-reading\"\u003eReferences \u0026amp; Further Reading\u003c/h2\u003e\n\u003cp\u003eFor a deeper dive into \u003ccode\u003ehyperpb\u003c/code\u003e and Go memory arenas, check out the following resources:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eOfficial hyperpb Announcement:\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://buf.build/blog/hyperpb\" rel=\"external\"\u003e\u0026ldquo;Introducing hyperpb: 10x faster dynamic Protobuf parsing that’s even 3x faster than generated code\u0026rdquo;\u003c/a\u003e on the \u003cem\u003eBuf Blog\u003c/em\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDeep Dives by Sunny (mcyoung):\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://mcyoung.xyz/2025/07/16/hyperpb/\" rel=\"external\"\u003e\u0026ldquo;Parsing Protobuf Like Never Before\u0026rdquo;\u003c/a\u003e (the internals of \u003ccode\u003ehyperpb\u003c/code\u003e\u0026rsquo;s bytecode compiler design).\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://mcyoung.xyz/2025/04/21/go-arenas/\" rel=\"external\"\u003e\u0026ldquo;Cheating the Reaper in Go\u0026rdquo;\u003c/a\u003e (a deep dive into the design and tradeoffs of memory arenas in Go).\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n","tags":["protobuf","go","performance","software-architecture"]},{"id":"https://kmcd.dev/posts/fauxrpc-proxy/","url":"https://kmcd.dev/posts/fauxrpc-proxy/","title":"Proxy, Record, and Mock gRPC APIs with FauxRPC","summary":"Stop writing mock stubs by hand. How FauxRPC uses smart proxying, reflection, and CEL to automate your API testing.","date_published":"2026-06-30T10:00:00Z","date_modified":"2026-08-31T09:39:14+02:00","content_html":"\u003cp\u003eMocking APIs is one of those tasks that starts simple and quickly turns into a chore. You begin with the best intentions, hand-crafting a few JSON fixtures for your frontend tests. But microservices evolve, payloads change, and soon you\u0026rsquo;re maintaining a massive directory of stale mock files. You find yourself trying to remember if user ID \u003ccode\u003e42\u003c/code\u003e was the one that returns a \u003ccode\u003e404\u003c/code\u003e, or the one that simulates a slow response.\u003c/p\u003e\n\u003cp\u003eWhen you\u0026rsquo;re building with gRPC or ConnectRPC, this problem gets both easier and harder. It\u0026rsquo;s easier because you have a strict schema (the Protobuf contract) to guide you. It\u0026rsquo;s harder because writing binary payloads or mock servers that conform to those schemas by hand is tedious.\u003c/p\u003e\n\u003cp\u003eIn \u003ca href=\"https://kmcd.dev/posts/fauxrpc/\"\u003eFauxRPC\u003c/a\u003e, I built a tool to generate fake data from Protobuf schemas dynamically. But I wanted to go further. I wanted to make mocks a natural byproduct of running your development environment. That is why I introduced \u003cstrong\u003eProxy Mode\u003c/strong\u003e and \u003cstrong\u003eAuto-Recording\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eBy placing FauxRPC in front of a real upstream service, it acts as a smart proxy: intercepting traffic, forwarding it to the upstream server, and writing out reusable mock stubs to disk. It even generates intelligent matching rules automatically.\u003c/p\u003e\n\u003cp\u003eHere is how it works under the hood.\u003c/p\u003e\n\u003cimg src=\"/d2-diagrams/93a139324c38c6c4a0d1ddc61c6e73be68b1af856093a706b924262a5f69d07a.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;\" /\u003e\u003ch2 id=\"the-auto-recording-workflow\"\u003eThe Auto-Recording Workflow\u003c/h2\u003e\n\u003cp\u003eRather than thinking about mocks as a separate coding chore, FauxRPC integrates mock generation directly into your existing development flow:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eStart FauxRPC\u003c/strong\u003e in proxy mode in front of a staging or local backend server.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInteract with your application\u003c/strong\u003e normally in the browser or via API clients.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCapture traffic automatically\u003c/strong\u003e as FauxRPC records every request and response into the \u003ccode\u003estubs/\u003c/code\u003e directory.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCommit the generated stubs\u003c/strong\u003e to Git along with your code changes.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRun your CI test suite\u003c/strong\u003e against FauxRPC using those recorded stubs, ensuring fast, offline, and reproducible test runs.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"zero-configuration-reflection\"\u003eZero-Configuration Reflection\u003c/h2\u003e\n\u003cp\u003eNormally, running a mock server requires you to supply the schema files. You have to pass paths to your \u003ccode\u003e.proto\u003c/code\u003e files or compiled descriptor sets (\u003ccode\u003e.binpb\u003c/code\u003e). That\u0026rsquo;s fine for a CI environment, but during local development, dragging files around and keeping them in sync is a pain.\u003c/p\u003e\n\u003cp\u003eIf you start FauxRPC in proxy mode without specifying a schema:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efauxrpc run --proxy-to\u003cspan style=\"color:#81a1c1\"\u003e=\u003c/span\u003elocalhost:8080 --record-dir\u003cspan style=\"color:#81a1c1\"\u003e=\u003c/span\u003estubs/\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFauxRPC uses \u003cstrong\u003egRPC Server Reflection\u003c/strong\u003e to discover the schema dynamically from the upstream server on startup.\u003c/p\u003e\n\u003cp\u003eBehind the scenes, FauxRPC performs a few key steps during this schema discovery phase:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eIt connects to the upstream and queries the reflection service using \u003ccode\u003eListServices\u003c/code\u003e to list all endpoints.\u003c/li\u003e\n\u003cli\u003eFor each service, it fetches the file descriptors using \u003ccode\u003eFileContainingSymbol\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eIt recursively collects the raw file descriptors (\u003ccode\u003eFileDescriptorProto\u003c/code\u003e), deduplicating common dependencies (like \u003ccode\u003egoogle/protobuf/timestamp.proto\u003c/code\u003e).\u003c/li\u003e\n\u003cli\u003eIt compiles the gathered descriptors into a \u003ccode\u003eFileDescriptorSet\u003c/code\u003e and registers them in FauxRPC\u0026rsquo;s registry.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThis means FauxRPC is ready to handle, translate, and mock any method defined on the upstream server with absolutely zero configuration.\u003c/p\u003e\n\u003cp\u003eBecause the schema is discovered directly from the running service, the proxy automatically stays in sync with upstream changes. No descriptor files need to be exported, committed, or manually refreshed—as your API evolves, FauxRPC adapts instantly.\u003c/p\u003e\n\u003ch2 id=\"the-multi-protocol-proxy-engine\"\u003eThe Multi-Protocol Proxy Engine\u003c/h2\u003e\n\u003cp\u003eFauxRPC is designed to work as a multi-protocol translator. A frontend might communicate using ConnectRPC (over JSON), while the upstream service is a pure gRPC service using binary Protobuf.\u003c/p\u003e\n\u003cp\u003eTo achieve this, FauxRPC sets up a ConnectRPC client using a custom \u003ccode\u003edynamicProtoCodec\u003c/code\u003e for proxying. This codec uses Go\u0026rsquo;s protobuf reflection (\u003ccode\u003edynamicpb.Message\u003c/code\u003e) to encode and decode payloads on the fly using the schemas retrieved during the reflection phase.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eUnary calls:\u003c/strong\u003e The proxy reads the request, forwards it using the client, intercepts the response, and writes it back.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eStreaming calls (client, server, and bidi):\u003c/strong\u003e To handle streams, FauxRPC uses \u003ccode\u003eFrameTracker\u003c/code\u003e objects. The tracker forwards frames in real time so there is no added latency, but it keeps a copy of the sequence in memory for logging and recording.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMetadata and Headers:\u003c/strong\u003e When forwarding headers via \u003ccode\u003ecopyHeaders\u003c/code\u003e, FauxRPC filters out protocol-level headers (like \u003ccode\u003econtent-type\u003c/code\u003e, \u003ccode\u003egrpc-status\u003c/code\u003e, and Connect-specific protocol headers) to let the underlying transport layer handle them cleanly. It also automatically masks sensitive headers (e.g. \u003ccode\u003eAuthorization\u003c/code\u003e, \u003ccode\u003eCookie\u003c/code\u003e, \u003ccode\u003eX-API-Key\u003c/code\u003e) with \u003ccode\u003e*****\u003c/code\u003e to avoid leaking production keys or user credentials into logs or stubs.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"graceful-fallbacks-for-parallel-teams\"\u003eGraceful Fallbacks for Parallel Teams\u003c/h2\u003e\n\u003cp\u003eIn a fast-moving team, schemas are often updated before the code is ready. A backend engineer might merge a \u003ccode\u003e.proto\u003c/code\u003e change adding a new endpoint, but the actual implementation won\u0026rsquo;t land for days. Normally, this blocks the frontend engineers who need that endpoint to build the UI.\u003c/p\u003e\n\u003cp\u003eFauxRPC\u0026rsquo;s \u003cstrong\u003eFallback Mode\u003c/strong\u003e solves this problem directly.\u003c/p\u003e\n\u003cp\u003eIf the upstream server returns an \u003ccode\u003eUnimplemented\u003c/code\u003e error code, the proxy doesn\u0026rsquo;t just pass that error to the client. Instead, it enters fallback mode:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eIt checks the local stub database to see if a mock stub matches the request.\u003c/li\u003e\n\u003cli\u003eIf no stub is found, it automatically generates realistic fake data (leveraging \u003ccode\u003eprotovalidate\u003c/code\u003e annotations if they are present in the schema).\u003c/li\u003e\n\u003cli\u003eThe fake response is returned to the client.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eTo the frontend, the endpoint looks and acts like it\u0026rsquo;s fully implemented. The frontend team keeps coding, completely unblocked by backend delays.\u003c/p\u003e\n\u003ch2 id=\"auto-recording-stubs\"\u003eAuto-Recording Stubs\u003c/h2\u003e\n\u003cp\u003eWriting mock stubs by hand is the worst part of API mocking. In proxy mode, FauxRPC can automate this entirely when you pass the \u003ccode\u003e--record-dir\u003c/code\u003e flag:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003estubs/\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e└── connectrpc.eliza.v1.ElizaService/\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ├── Say.json\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    └── Introduce.json\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAs you interact with your upstream service, FauxRPC intercepts the requests and responses, automatically writing them to disk as structured JSON or YAML files matching your service hierarchy.\u003c/p\u003e\n\u003cp\u003eFor example, a recorded stub might look like this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;id\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;c85d8869-ad10-449e-ba63-2287f7401c10\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;target\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;connectrpc.eliza.v1.ElizaService/Say\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;active_if\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;req.sentence == \\\u0026#34;hello\\\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;content\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;sentence\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;Hello! How can I help you today?\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBecause these files are saved directly in your codebase, you can commit them to Git and immediately use them as your mock suite in CI/CD pipelines or integration tests. There is no extra setup: just run your application, click around your frontend, and your API mock stubs are generated for you.\u003c/p\u003e\n\u003ch2 id=\"a-protobuf-native-dashboard-for-curation\"\u003eA Protobuf-Native Dashboard for Curation\u003c/h2\u003e\n\u003cp\u003eHaving an automated directory of stubs is a massive time saver, but you still need a way to easily curate and manage them. The developer dashboard in FauxRPC (served at \u003ccode\u003ehttp://localhost:6660/fauxrpc\u003c/code\u003e when running with \u003ccode\u003e--dashboard\u003c/code\u003e) gives you exactly that.\u003c/p\u003e\n\u003cp\u003eThe dashboard captures your live traffic history so you can view the raw JSON payload of any logged request. From there, you can copy a pre-compiled FauxRPC YAML stub, along with its generated \u003ccode\u003eactive_if\u003c/code\u003e matcher, directly to your clipboard. This gives you the ability to carefully curate your stub directory while still completely bypassing the need to hand-write them yourself.\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"https://kmcd.dev/posts/fauxrpc-proxy/fauxrpc-request-log-stub.png\" class=\"spotlight\" data-download=\"true\" aria-label=\"Viewing the FauxRPC request log history and copying a pre-generated stub\"\u003e\n    \n    \n    \n    \n        \n            \n            \n                \n            \n            \n        \n    \n\n    \n    \n    \n\n    \n    \n        \n    \n\n    \u003cimg src=\"https://kmcd.dev/posts/fauxrpc-proxy/fauxrpc-request-log-stub_hu_c351b08893238947.png\"\n         alt=\"Viewing the FauxRPC request log history and copying a pre-generated stub\"/\u003e\n    \u003c/a\u003e\n\u003c/figure\u003e\n\n\u003cp\u003eThis dashboard interface is powered by \u003cstrong\u003e\u003ca href=\"https://protodocs.dev/\" rel=\"external\"\u003eProtodocs\u003c/a\u003e\u003c/strong\u003e, which displays native protobuf schemas directly inside FauxRPC. Because it understands protobuf natively, you can view your packages, messages, and services, and use a built-in API explorer to run test calls (including unary and streaming APIs) directly from the browser without needing complex local Envoy or gRPC-Web proxy configurations.\u003c/p\u003e\n\u003ch2 id=\"generating-intelligent-cel-matchers\"\u003eGenerating Intelligent CEL Matchers\u003c/h2\u003e\n\u003cp\u003eWhen you are curating these recorded stubs, they are only useful if they match the right request parameters. If you search for user ID \u003ccode\u003e12\u003c/code\u003e, you want the stub that returns Alice. If you search for user ID \u003ccode\u003e42\u003c/code\u003e, you want the stub that returns Bob.\u003c/p\u003e\n\u003cp\u003eWhether auto-saved to disk or copied from the dashboard, FauxRPC automatically compiles request shapes into Common Expression Language (CEL) matching rules by examining the request metadata and payload:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eIt ranges over the fields set on the request message.\u003c/li\u003e\n\u003cli\u003eIt ignores complex fields (nested messages, lists, and maps) to keep the generated rules readable.\u003c/li\u003e\n\u003cli\u003eFor primitive fields (like strings, booleans, and integers), it formats the values into CEL literals.\u003c/li\u003e\n\u003cli\u003eIt joins these field checks with \u003ccode\u003e\u0026amp;\u0026amp;\u003c/code\u003e.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eFor example, if you send a request where \u003ccode\u003ename\u003c/code\u003e is \u0026ldquo;Alice\u0026rdquo; and \u003ccode\u003eage\u003c/code\u003e is 30, FauxRPC automatically generates:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1\"\u003eactive_if\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e req.name == \u0026#34;Alice\u0026#34; \u0026amp;\u0026amp; req.age == 30\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen a subsequent request comes in, FauxRPC compiles and evaluates this expression against the request payload. If it evaluates to \u003ccode\u003etrue\u003c/code\u003e, the stub is served.\u003c/p\u003e\n\u003ch2 id=\"recorded-stub-formats\"\u003eRecorded Stub Formats\u003c/h2\u003e\n\u003cp\u003eFauxRPC formats these stubs into three distinct kinds based on the call type:\u003c/p\u003e\n\u003ch3 id=\"unary--client-streaming-success\"\u003eUnary / Client-Streaming Success\u003c/h3\u003e\n\u003cp\u003eFor successful unary calls, it records the response payload:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;id\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;c85d8869-ad10-449e-ba63-2287f7401c10\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;target\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;connectrpc.eliza.v1.ElizaService/Say\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;active_if\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;req.sentence == \\\u0026#34;hello\\\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;content\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;sentence\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;Hello! How can I help you today?\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#eceff4\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;priority\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#b48ead\"\u003e10\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"unary--client-streaming-error\"\u003eUnary / Client-Streaming Error\u003c/h3\u003e\n\u003cp\u003eIf the upstream returned an error, FauxRPC records the status code and message so you can mock error states:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;id\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;18fd7d9e-108c-4a37-bcfc-fa8d7a12ad4f\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;target\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;connectrpc.eliza.v1.ElizaService/Say\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;active_if\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;req.sentence == \\\u0026#34;trigger error\\\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;error_code\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#b48ead\"\u003e3\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;error_message\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;invalid sentence structure\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;priority\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#b48ead\"\u003e10\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"server-streaming--bidirectional-streaming\"\u003eServer-Streaming / Bidirectional Streaming\u003c/h3\u003e\n\u003cp\u003eFor streaming APIs, it records the sequence of frames, mimicking latency with a default delay, and captures trailing errors:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;id\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;a97df11b-7a31-4b10-8b4b-6f81e3a1f810\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;target\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;connectrpc.eliza.v1.ElizaService/Introduce\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;active_if\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;req.name == \\\u0026#34;Bob\\\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;stream\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;items\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e[\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;content\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;sentence\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;Hi Bob!\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e},\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;delay\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;100ms\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;content\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;sentence\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;I am Eliza.\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e},\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;delay\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;100ms\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;error\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;code\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#b48ead\"\u003e5\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;message\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;session lost\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#eceff4\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#eceff4\"\u003e},\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#81a1c1\"\u003e\u0026#34;priority\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#b48ead\"\u003e10\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"summary\"\u003eSummary\u003c/h2\u003e\n\u003cp\u003eFauxRPC\u0026rsquo;s proxy and recording capabilities bridge the gap between static mock servers and real-world backend services. By discovering schemas through reflection, handling dynamic protocol translation, falling back to fake data, and generating intelligent CEL matchers automatically, it takes the busywork out of API mocking.\u003c/p\u003e\n\u003cp\u003eTraditional mocks drift because they are maintained separately from the systems they represent. By recording real traffic, discovering schemas automatically, and falling back to generated data when necessary, FauxRPC keeps mock environments aligned with production behavior while dramatically reducing the amount of manual work required. It turns mocks from a maintenance chore into a natural byproduct of running your development environment.\u003c/p\u003e\n","tags":["fauxrpc","connectrpc","grpc","protobuf","api","rpc","go","golang","http3"]},{"id":"https://kmcd.dev/posts/interactive-protobuf/","url":"https://kmcd.dev/posts/interactive-protobuf/","title":"Exploring Protocol Buffers Interactively","summary":"Announcing protobuf.kmcd.dev, an interactive explainer and playground for exploring the binary details of Protocol Buffers.","date_published":"2026-06-25T10:00:00Z","date_modified":"2026-08-31T09:39:14+02:00","content_html":"\u003cp\u003eProtocol Buffers (protobuf) are a core part of modern RPCs, schema-driven APIs, and microservices. They work well, but learning how the format operates under the hood can be dry. Most of us stick to the high-level basics (like message fields and primitive types) and rarely need to dig into the wire format, varints, ZigZag encoding, or compiler plugins.\u003c/p\u003e\n\u003cp\u003eI wanted to change that by building a visual, hands-on guide.\u003c/p\u003e\n\u003cp\u003eToday I am sharing \u003cstrong\u003e\u003ca href=\"https://protobuf.kmcd.dev\" rel=\"external\"\u003eprotobuf.kmcd.dev\u003c/a\u003e\u003c/strong\u003e. It is an interactive playground for exploring Protocol Buffers, from schema design and validation to binary wire formats and serialization efficiency.\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"https://kmcd.dev/posts/interactive-protobuf/fixed-size-diagram.png\" class=\"spotlight\" data-download=\"true\" aria-label=\"A diagram showing fixed-size fields being encoded into protobuf\"\u003e\n    \n    \n    \n    \n        \n            \n            \n                \n            \n            \n        \n    \n\n    \n    \n    \n\n    \n    \n        \n    \n\n    \u003cimg src=\"https://kmcd.dev/posts/interactive-protobuf/fixed-size-diagram_hu_8380e5299910fee0.png\"\n         alt=\"A diagram showing fixed-size fields being encoded into protobuf\" loading=\"lazy\"/\u003e\n    \u003c/a\u003e\n\u003c/figure\u003e\n\n\u003cp\u003eThis builds on a previous experiment, \u003cstrong\u003e\u003ca href=\"https://bgp.kmcd.dev\" rel=\"external\"\u003ebgp.kmcd.dev\u003c/a\u003e\u003c/strong\u003e, which I introduced in \u003ca href=\"https://kmcd.dev/posts/bgp-kmcd-dev/\"\u003eLet\u0026rsquo;s Learn About BGP\u003c/a\u003e. That project showed me how much easier it is to understand the inner workings of a protocol when you can see them update in real time. I wanted to apply that same approach here.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"a-quick-tour\"\u003eA Quick Tour\u003c/h2\u003e\n\u003cp\u003eInstead of just writing static text, I built several custom widgets and sandboxes to show what happens when you compile and serialize protobuf data.\u003c/p\u003e\n\u003ch3 id=\"deep-dive-into-the-basics\"\u003eDeep Dive into the Basics\u003c/h3\u003e\n\u003cp\u003eThe \u003ca href=\"https://protobuf.kmcd.dev/basics/\" rel=\"external\"\u003eBasics\u003c/a\u003e section covers schema design syntax and core protobuf concepts like enums, nested messages, repeated fields, maps, and oneofs. Code comparisons help explain primitive scalars, Google\u0026rsquo;s Well-Known Types, and optimal integer guidelines.\u003c/p\u003e\n\u003ch3 id=\"visualizing-the-wire-format\"\u003eVisualizing the Wire Format\u003c/h3\u003e\n\u003cp\u003eThe \u003ca href=\"https://protobuf.kmcd.dev/binary/\" rel=\"external\"\u003eBinary \u0026amp; Wire Format\u003c/a\u003e page contains the core interactive tools on the site, and it is the section I am most interested in getting your thoughts on. It explains some basics of working with binary and then it breaks down how bytes are physically laid out on the wire:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eVarint Explainer:\u003c/strong\u003e An interactive bit-level calculator. Input any integer to see how it converts from decimal to binary, splits into 7-bit groups, attaches continuation flags (MSB), and outputs final hex bytes.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eZigZag Explainer:\u003c/strong\u003e Input signed numbers and watch how the sign bit shifts to the LSB, showing the resulting bit arrangement and ZigZag value.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTag Calculator:\u003c/strong\u003e Select a field number and a wire type to see the mathematical shift (\u003ccode\u003e\u0026lt;\u0026lt; 3\u003c/code\u003e), bitwise OR operations, and the resulting hex tag byte.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInteractive Wire Type Visualizers:\u003c/strong\u003e Flowcharts that explain wire-type parsing for Varints, Fixed width (32-bit and 64-bit), and Length-delimited payloads.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBinary Explorer (Segment Inspector):\u003c/strong\u003e An interactive split layout. You write a custom protobuf schema and standard JSON data. The tool compiles the schema, encodes the JSON to raw binary bytes, and maps them to a hex grid. Hovering or clicking on individual bytes in the grid highlights them and opens an inspector pane that breaks down exactly which field number, wire type, length, or payload value those bytes represent. You can also toggle a live Protoscope view to see the binary translate into diagnostic text.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cfigure\u003e\u003ca href=\"https://kmcd.dev/posts/interactive-protobuf/binary-explorer.png\" class=\"spotlight\" data-download=\"true\" aria-label=\"The Binary Explorer split layout showing a schema compiling to a raw hex grid\"\u003e\n    \n    \n    \n    \n        \n            \n            \n                \n            \n            \n        \n    \n\n    \n    \n    \n\n    \n    \n        \n    \n\n    \u003cimg src=\"https://kmcd.dev/posts/interactive-protobuf/binary-explorer_hu_3cc7be83abbe0dee.png\"\n         alt=\"The Binary Explorer split layout showing a schema compiling to a raw hex grid\" loading=\"lazy\"/\u003e\n    \u003c/a\u003e\n\u003c/figure\u003e\n\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eProtoscope Lab:\u003c/strong\u003e A diagnostic sandbox where you can write raw Protoscope text on the left and see it compile into hex bytes on the right.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cem\u003eI am particularly interested in whether this section hits the mark. Are these visualizations actually helpful for understanding the low-level encoding, or do they feel too contrived?\u003c/em\u003e\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"https://kmcd.dev/posts/interactive-protobuf/varint-calculator.png\" class=\"spotlight\" data-download=\"true\" aria-label=\"The Varint Explainer widget showing a live bit-level decimal to binary conversion\"\u003e\n    \n    \n    \n    \n        \n            \n            \n                \n            \n            \n        \n    \n\n    \n    \n    \n\n    \n    \n        \n    \n\n    \u003cimg src=\"https://kmcd.dev/posts/interactive-protobuf/varint-calculator_hu_e24c34b464040124.png\"\n         alt=\"The Varint Explainer widget showing a live bit-level decimal to binary conversion\" loading=\"lazy\"/\u003e\n    \u003c/a\u003e\n\u003c/figure\u003e\n\n\u003ch3 id=\"real-time-benchmarking--efficiency\"\u003eReal-Time Benchmarking \u0026amp; Efficiency\u003c/h3\u003e\n\u003cp\u003eThe \u003ca href=\"https://protobuf.kmcd.dev/efficiency/\" rel=\"external\"\u003eEfficiency \u0026amp; Performance\u003c/a\u003e page features a simulator with a JSON editor on the left and live metrics on the right.\u003c/p\u003e\n\u003cp\u003eYou can write your own JSON structure or load preset profiles (Basic User, Nested Data, Packed List). The app compiles the schema, encodes the JSON into protobuf binary, and runs both payloads through browser-native GZIP compression. It renders bar charts comparing minified/gzipped JSON versus protobuf binary sizes, showing the payload reduction ratio. It also includes alerts for things like Gzip overhead if your payload is too small to benefit from compression.\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"https://kmcd.dev/posts/interactive-protobuf/efficiency.png\" class=\"spotlight\" data-download=\"true\" aria-label=\"Real-time dashboard rendering animated bar charts that compare JSON and Protobuf payload sizes\"\u003e\n    \n    \n    \n    \n        \n            \n            \n                \n            \n            \n        \n    \n\n    \n    \n    \n\n    \n    \n        \n    \n\n    \u003cimg src=\"https://kmcd.dev/posts/interactive-protobuf/efficiency_hu_940270c68dfa7d7c.png\"\n         alt=\"Real-time dashboard rendering animated bar charts that compare JSON and Protobuf payload sizes\" loading=\"lazy\"/\u003e\n    \u003c/a\u003e\n\u003c/figure\u003e\n\n\u003ch3 id=\"options-reflection-and-validation\"\u003eOptions, Reflection, and Validation\u003c/h3\u003e\n\u003cp\u003eThe \u003cstrong\u003e\u003ca href=\"https://protobuf.kmcd.dev/tooling/\" rel=\"external\"\u003eTooling \u0026amp; Reflection\u003c/a\u003e\u003c/strong\u003e and \u003cstrong\u003e\u003ca href=\"https://protobuf.kmcd.dev/advanced/\" rel=\"external\"\u003eAdvanced\u003c/a\u003e\u003c/strong\u003e pages contain two developer sandboxes:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eDescriptor Playground:\u003c/strong\u003e Write protobuf schemas and inspect the compiled \u003ccode\u003eFileDescriptorSet\u003c/code\u003e JSON metadata structure in real time. You can download the output directly as a raw binary descriptor (\u003ccode\u003edescriptor.bin\u003c/code\u003e) or a JSON file.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eValidation Lab:\u003c/strong\u003e An interactive sandbox illustrating the \u003ccode\u003eprotovalidate\u003c/code\u003e specification. You can write schemas containing CEL (Common Expression Language) validation rules (like \u003ccode\u003ethis.age \u0026gt;= 18\u003c/code\u003e) and test them against JSON payloads. If violations occur, the page displays a list of validation errors, naming the violating fields and the failed constraint rules.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"the-protobuf-ecosystem\"\u003eThe Protobuf Ecosystem\u003c/h3\u003e\n\u003cp\u003eThis page contains a searchable index of protobuf-related projects, including compilers, linters, formatters, validators, RPC frameworks, and language-specific tooling. Whether you\u0026rsquo;re looking for a code generator, validation framework, or language-specific library, the explorer makes it easier to discover tools across the broader protobuf ecosystem.\u003c/p\u003e\n\u003cp\u003eExplore the complete list at \u003cstrong\u003e\u003ca href=\"https://protobuf.kmcd.dev/ecosystem/\" rel=\"external\"\u003eprotobuf.kmcd.dev/ecosystem\u003c/a\u003e\u003c/strong\u003e.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"powered-by-webassembly-wasm\"\u003ePowered by WebAssembly (WASM)\u003c/h2\u003e\n\u003cp\u003eBuilding interactive compilers in a web browser usually requires a round-trip to a backend server or rewriting everything in TypeScript. To avoid the server costs of passing custom schemas to a backend or the development time of porting several different libraries to TypeScript, this site runs Go\u0026rsquo;s compiler tooling directly inside the browser using WebAssembly.\u003c/p\u003e\n\u003cp\u003eAs I noted in \u003ca href=\"https://kmcd.dev/posts/wasm-demos/\"\u003eZero-Friction Demos with WASM\u003c/a\u003e, compiling Go libraries into WASM works really well for client-side tools. The WASM binary handles the schema compilation, JSON-to-binary serialization, and Protoscope compilation natively on the client. It is fast, private, and works offline.\u003c/p\u003e\n\u003ch2 id=\"give-it-a-spin\"\u003eGive it a Spin\u003c/h2\u003e\n\u003cp\u003eIf you want to look at the lower-level mechanics of Protocol Buffers, or if you just need a zero-install scratchpad to inspect wire formats and CEL validations, check out the site starting from the \u003cstrong\u003e\u003ca href=\"https://protobuf.kmcd.dev/intro/\" rel=\"external\"\u003eIntroduction\u003c/a\u003e\u003c/strong\u003e:\u003c/p\u003e\n\u003cp\u003e👉 \u003cstrong\u003e\u003ca href=\"https://protobuf.kmcd.dev\" rel=\"external\"\u003eprotobuf.kmcd.dev\u003c/a\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eThe source code and widgets are open source and available on GitHub at \u003cstrong\u003e\u003ca href=\"https://github.com/sudorandom/protobuf.kmcd.dev\" rel=\"external\"\u003esudorandom/protobuf.kmcd.dev\u003c/a\u003e\u003c/strong\u003e. Please give the site a try and let me know your thoughts (especially on the binary section), bug reports, or suggestions!\u003c/p\u003e\n","tags":["protobuf","wasm","go","webassembly","dataviz"]},{"id":"https://kmcd.dev/posts/introducing-protodocs/","url":"https://kmcd.dev/posts/introducing-protodocs/","title":"Introducing ProtoDocs","summary":"A protobuf-first documentation browser for APIs that need clearer generated docs.","date_published":"2026-06-23T10:00:00Z","date_modified":"2026-08-31T09:39:14+02:00","content_html":"\u003cp\u003e\u003cstrong\u003eProtocol Buffers are fantastic for defining API contracts.\u003c/strong\u003e You get a mature tooling ecosystem, strong compatibility habits, and schemas that are precise enough to build around. The documentation story is not nearly as good. If you have ever tried to hand generated protobuf docs to an API consumer, you have probably seen the gap.\u003c/p\u003e\n\u003cp\u003eMost tools force an awkward compromise. Some produce complete output that is hard to read. Others look polished only after custom templates, glue code, and project-specific maintenance. The fallback is to share the \u003ccode\u003e.proto\u003c/code\u003e files directly, which works for people already deep in protobuf but is rough for everyone else.\u003c/p\u003e\n\u003cp\u003eThat frustration is why I originally built \u003ca href=\"https://github.com/sudorandom/protoc-gen-connect-openapi\" rel=\"external\"\u003eprotoc-gen-connect-openapi\u003c/a\u003e. The OpenAPI ecosystem has tools like Redocly and Swagger UI, and they make good-looking, browsable API sites easy to publish. Translating \u003ca href=\"https://connectrpc.com\" rel=\"external\"\u003eConnect APIs\u003c/a\u003e into OpenAPI made sense at the time, and it still works well. But it also means starting with protobuf, converting to another schema format, and then documenting that result.\u003c/p\u003e\n\u003cp\u003eI wanted the shorter path: start from protobuf, keep the schema visible, and still end up with something polished enough to publish.\u003c/p\u003e\n\u003cp\u003eSo I made \u003cstrong\u003e\u003ca href=\"https://protodocs.dev/\" rel=\"external\"\u003eProtoDocs\u003c/a\u003e\u003c/strong\u003e.\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"https://kmcd.dev/posts/introducing-protodocs/eliza-sshot.png\" class=\"spotlight\" data-download=\"true\" aria-label=\"A screenshot of the ProtoDocs interface showing protobuf files, services, messages, and RPC calls.\"\u003e\n    \n    \n    \n    \n        \n            \n            \n                \n            \n            \n        \n    \n\n    \n    \n    \n\n    \n    \n        \n    \n\n    \u003cimg src=\"https://kmcd.dev/posts/introducing-protodocs/eliza-sshot_hu_e3e07748f039b0cb.png\"\n         alt=\"A screenshot of the ProtoDocs interface showing protobuf files, services, messages, and RPC calls.\"/\u003e\n    \u003c/a\u003e\u003cfigcaption\u003e\n            \u003cp\u003eA screenshot of the ProtoDocs interface showing protobuf files, services, messages, and RPC calls.\u003c/p\u003e\n        \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003ch2 id=\"protobuf-first-documentation\"\u003eProtobuf-first documentation\u003c/h2\u003e\n\u003cp\u003eProtoDocs is a web-based documentation browser for Protocol Buffer definitions. Its model follows protobuf directly: files, packages, services, RPCs, messages, enums, fields, oneofs, options, and comments.\u003c/p\u003e\n\u003cp\u003eWhat does \u0026ldquo;protobuf-first\u0026rdquo; actually mean? It means the tool treats the source as the primary interface. I think protobuf schemas are clean, expressive, and readable, especially compared with the nested JSON or YAML you often end up reading in OpenAPI. ProtoDocs renders the actual protobuf syntax, adds highlighting, and layers interactive tooltips over keywords, field names, type references, and custom options. You can read the source and explore it at the same time.\u003c/p\u003e\n\u003cp\u003eProtoDocs also defines its own configuration as a Protocol Buffer schema, so the project can document its own config format. You can browse the official configuration options directly on \u003ca href=\"https://protodocs.dev/#/files/protodocs/v1/config.proto\" rel=\"external\"\u003eprotodocs.dev\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eThe goal is simple: make protobuf documentation readable, navigable, and useful without first turning it into a different API description format.\u003c/p\u003e\n\u003ch2 id=\"what-it-does\"\u003eWhat it does\u003c/h2\u003e\n\u003cp\u003eThe UI lets you explore an API from a few different angles:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eBrowse files and packages\u003c/strong\u003e in a directory hierarchy that merges common path prefixes, which makes larger proto trees easier to scan.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eInspect services and RPC methods\u003c/strong\u003e, including unary and streaming signatures, alongside custom method options.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDrill into messages\u003c/strong\u003e, enums, custom options, oneof declarations, nested types, comments, and linked type references.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eJump to definitions\u003c/strong\u003e and find references across the loaded files. Pinned type tooltips let you move from a field to its type declaration, or see where a type is used, without losing your place.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSend live requests\u003c/strong\u003e from an interactive \u0026ldquo;Try it out\u0026rdquo; panel using Connect, \u003ca href=\"https://github.com/grpc/grpc-web\" rel=\"external\"\u003egRPC-Web\u003c/a\u003e, or proxied \u003ca href=\"https://grpc.io\" rel=\"external\"\u003egRPC\u003c/a\u003e, including streaming calls.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTunnel browser traffic\u003c/strong\u003e through a WebSocket-based proxy to avoid CORS restrictions and connect directly to services.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRead highlighted protobuf source\u003c/strong\u003e with keyword tooltips that explain the underlying syntax.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLoad schemas several ways\u003c/strong\u003e, including pre-built descriptor sets from \u003ccode\u003ebuf\u003c/code\u003e or \u003ccode\u003eprotoc\u003c/code\u003e, live server reflection, or an in-memory Go registry for embedded usage.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAs you edit request parameters in the client panel, ProtoDocs generates command snippets for Connect, \u003ccode\u003egrpcurl\u003c/code\u003e, and \u003ccode\u003ebuf curl\u003c/code\u003e. The browser interaction becomes a reproducible terminal command instead of a one-off experiment.\u003c/p\u003e\n\u003ch2 id=\"built-for-connect-but-not-only-connect\"\u003eBuilt for Connect, but not only Connect\u003c/h2\u003e\n\u003cp\u003eProtoDocs is designed with ConnectRPC servers in mind.\u003c/p\u003e\n\u003cp\u003eA Connect server can expose Connect, gRPC, and gRPC-Web from the same API surface, which makes it a natural fit for browser-based exploration. ProtoDocs uses that shape to keep the interactive client close to the real RPC contract.\u003c/p\u003e\n\u003cp\u003eThe interactive client supports:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eConnect\u003c/li\u003e\n\u003cli\u003egRPC-Web\u003c/li\u003e\n\u003cli\u003egRPC through proxy mode\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe documentation side only needs protobuf descriptors, so schema browsing works beyond Connect. The richer interactive path is where ConnectRPC servers shine.\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"https://kmcd.dev/posts/introducing-protodocs/eliza-tryitout-sshot.png\" class=\"spotlight\" data-download=\"true\" aria-label=\"A screenshot of the ProtoDocs request panel for trying RPC calls from the browser.\"\u003e\n    \n    \n    \n    \n        \n            \n            \n                \n            \n            \n        \n    \n\n    \n    \n    \n\n    \n    \n        \n    \n\n    \u003cimg src=\"https://kmcd.dev/posts/introducing-protodocs/eliza-tryitout-sshot_hu_4682a155a0087a77.png\"\n         alt=\"A screenshot of the ProtoDocs request panel for trying RPC calls from the browser.\"/\u003e\n    \u003c/a\u003e\u003cfigcaption\u003e\n            \u003cp\u003eA screenshot of the ProtoDocs request panel for trying RPC calls from the browser.\u003c/p\u003e\n        \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003ch2 id=\"three-ways-to-run-it\"\u003eThree ways to run it\u003c/h2\u003e\n\u003cp\u003eThere are three primary deployment modes:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eStatic website\u003c/li\u003e\n\u003cli\u003eWebsite with a backend\u003c/li\u003e\n\u003cli\u003eEmbedded Go application\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eEach one has a different tradeoff.\u003c/p\u003e\n\u003ch3 id=\"static-website\"\u003eStatic website\u003c/h3\u003e\n\u003cp\u003eFor the static path, put the built frontend on Nginx, S3, Netlify, GitHub Pages, or any other static host, then point it at one or more descriptor files. That is enough for browsable protobuf documentation.\u003c/p\u003e\n\u003cp\u003eThis is the simplest deployment. For interactive requests, the browser talks directly to the target API server, so that server needs CORS configured if you want the \u0026ldquo;Try it out!\u0026rdquo; panel to work.\u003c/p\u003e\n\u003cdiv class=\"d2-diagram-wrapper\"\u003e\n    \u003cdiv class=\"d2-diagram\"\n        style=\"display: block; width: 100%; max-width: 100%; margin-left: auto; margin-right: auto; max-height:70vh; width:100%;\"\u003e\u003cimg src=\"/d2-diagrams/aa9d335ef5502f1a4d9ac6c99a7cb0d76e3e2191d112456ac551678f1a47731e.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;\" /\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003ch3 id=\"website-with-a-small-backend\"\u003eWebsite with a small backend\u003c/h3\u003e\n\u003cp\u003eProtoDocs can also run behind a small backend proxy.\u003c/p\u003e\n\u003cp\u003eHere, browser requests and streaming calls tunnel through the proxy. That avoids CORS problems and lets the backend forward requests to gRPC, gRPC-Web, or Connect services.\u003c/p\u003e\n\u003cdiv class=\"d2-diagram-wrapper\"\u003e\n    \u003cdiv class=\"d2-diagram\"\n        style=\"display: block; width: 100%; max-width: 100%; margin-left: auto; margin-right: auto; max-height:70vh; width:100%;\"\u003e\u003cimg src=\"/d2-diagrams/529635df2c14bfe1e1c07278a29ede62129e7e906b9a83a9f476d228e6ecc92f.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;\" /\u003e\u003c/div\u003e\n\u003c/div\u003e\n\u003cp\u003eI expect this mode to be the best fit for internal tools and developer portals where \u0026ldquo;open the docs and call the API\u0026rdquo; needs to work out of the box.\u003c/p\u003e\n\u003ch3 id=\"embedded-go-handler\"\u003eEmbedded Go handler\u003c/h3\u003e\n\u003cp\u003eThe Go package can also embed ProtoDocs directly into an HTTP server.\u003c/p\u003e\n\u003cp\u003eThat gives you the documentation UI, descriptor loading from the Go registry, and the proxy path from the same application. If you already have a Go service exposing Connect or gRPC handlers, mounting ProtoDocs alongside it makes the service self-documenting without a separate docs deployment.\u003c/p\u003e\n\u003cp\u003eAt a high level, it looks like this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ehandler\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e:=\u003c/span\u003e protodocs\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eNewHandler\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003eprotodocs\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eConfig\u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Title\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e    \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;My Service Documentation\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    LogoText\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;My Service\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Registry\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e protoregistry\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003eGlobalFiles\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Prefix\u003cspan style=\"color:#eceff4\"\u003e:\u003c/span\u003e   \u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;/docs/\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e})\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003eif\u003c/span\u003e err \u003cspan style=\"color:#81a1c1\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#81a1c1;font-weight:bold\"\u003enil\u003c/span\u003e \u003cspan style=\"color:#eceff4\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    log\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eFatal\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003eerr\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#eceff4\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ehttp\u003cspan style=\"color:#eceff4\"\u003e.\u003c/span\u003e\u003cspan style=\"color:#88c0d0\"\u003eHandle\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e(\u003c/span\u003e\u003cspan style=\"color:#a3be8c\"\u003e\u0026#34;/docs/\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#eceff4\"\u003e,\u003c/span\u003e handler\u003cspan style=\"color:#eceff4\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"themes-source-and-sharp-edges\"\u003eThemes, source, and sharp edges\u003c/h2\u003e\n\u003cp\u003eProtoDocs includes light, dark, and cyberpunk themes. They can follow system preferences or be toggled manually.\u003c/p\u003e\n\u003cp\u003eIt renders source views, markdown comments, structured options, package trees, service lists, search results, and linked type details. The point is to make the schema readable without asking people to clone a repo, find the right proto directory, and mentally resolve every import by hand.\u003c/p\u003e\n\u003cp\u003eIt is still early.\u003c/p\u003e\n\u003cp\u003eThere are bugs, and parts of the UI and runtime behavior will change as the project gets more real-world use.\u003c/p\u003e\n\u003cp\u003eBut it is far enough along that I want people to try it, point out what is confusing, and show me the protobuf schemas that break it. If you run into sharp edges, have feedback on the layout, or want to contribute a fix, please open an issue or send a PR on GitHub.\u003c/p\u003e\n\u003ch2 id=\"try-it\"\u003eTry it\u003c/h2\u003e\n\u003cp\u003eYou can try the live demo at \u003cstrong\u003e\u003ca href=\"https://protodocs.dev/\" rel=\"external\"\u003eprotodocs.dev\u003c/a\u003e\u003c/strong\u003e.\u003c/p\u003e\n\u003ca href=\"https://github.com/sudorandom/protodocs\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"github-repo-card\"\u003e\n  \u003cdiv class=\"github-repo-card-header\"\u003e\n    \u003cdiv class=\"github-repo-icon\"\u003e\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\u003e\u003cpath d=\"M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22\"\u003e\u003c/path\u003e\u003c/svg\u003e\u003c/div\u003e\n    \u003cspan class=\"github-repo-name\"\u003esudorandom/protodocs\u003c/span\u003e\n  \u003c/div\u003e\u003cp class=\"github-repo-description\"\u003eA protobuf-first documentation browser for APIs that need clearer generated docs.\u003c/p\u003e\u003c/a\u003e\n\n\u003cp\u003eIf you have a ConnectRPC, gRPC-Web, or descriptor-heavy protobuf project, point ProtoDocs at it and see where it falls over.\u003c/p\u003e\n","tags":["protobuf","grpc","connectrpc","documentation","go","rpc"]}]}