BTrace v2 Binary Protocol Architecture
July 14, 2026 · View on GitHub
Document Version: 1.1 Last Updated: February 2026 Status: Implemented (first released in 3.0.0)
Table of Contents
- Executive Summary
- Problem Statement
- High-Level Architecture
- Protocol Negotiation
- Wire Format Specification
- Command Conversion Layer
- Benefits and Trade-offs
- Migration Path
- Performance Characteristics
Executive Summary
The BTrace v2 binary protocol is a performance-optimized communication protocol that replaces Java Object Serialization with custom binary serialization. The v2 protocol delivers 3-6x faster command transmission and 2-5x smaller wire payloads while maintaining full backward compatibility with the existing v1 protocol through automatic protocol negotiation.
Key Features:
- Custom binary serialization (vs Java ObjectInputStream/ObjectOutputStream)
- Automatic protocol negotiation (once per connection)
- Compression support for large payloads (>1KB threshold)
- Thread-safe using ReentrantLock (vs synchronized blocks)
- Full backward compatibility with v1 protocol
- Zero-configuration auto-detection
Problem Statement
What Problem Does v2 Solve?
The original BTrace protocol (v1) relies on Java Object Serialization for agent-client communication. While functional, this approach has significant limitations:
1. Performance Bottleneck
Problem: Java serialization is slow
- ObjectInputStream/ObjectOutputStream use reflection and complex state management
- Each command incurs serialization overhead (object graphs, metadata)
- High CPU usage during marshaling/unmarshaling
Impact:
- Limits throughput for high-frequency tracing scenarios
- Increases latency for interactive debugging
- Consumes CPU resources that could be used for actual tracing
2. Large Wire Payloads
Problem: Java serialization produces verbose binary format
- Includes class metadata, type descriptors, stream headers
- Inefficient encoding of primitive types and strings
- No built-in compression
Impact:
- Increased network bandwidth usage
- Slower transmission over slow connections
- Higher memory usage for buffering
3. Language Lock-in
Problem: Java serialization ties BTrace to JVM-only clients
- Cannot implement clients in other languages (Python, Go, JavaScript)
- Limits future extensibility (browser-based tools, IDE plugins in non-JVM languages)
Impact:
- Restricts ecosystem growth
- Prevents integration with non-Java monitoring tools
4. Dated Concurrency Model
Problem: v1 uses synchronized blocks for thread safety
- Coarse-grained locking can become bottleneck
- Limited scalability for concurrent client connections
Impact:
- Performance degradation with multiple concurrent clients
- Contention under high load
Real-World Scenario
Consider a production environment with high-frequency tracing:
v1 Protocol:
10,000 MessageCommands/second
Average size: 512 bytes serialized
Network: 5.12 MB/second
CPU overhead: ~15% for serialization
v2 Protocol:
10,000 MessageCommands/second
Average size: 170 bytes serialized (3x smaller with compression)
Network: 1.7 MB/second (67% reduction)
CPU overhead: ~3% for serialization (80% reduction)
Result: 67% less bandwidth, 80% less CPU overhead, same functionality
High-Level Architecture
Component Overview
┌─────────────────────────────────────────────────────────────────┐
│ BTrace Client │
│ ┌──────────────┐ ┌─────────────────┐ ┌───────────────┐ │
│ │ Client │───▶│ ProtocolNegotia-│───▶│ WireProtocol │ │
│ │ (btrace- │ │ tor (one-time) │ │ Interface │ │
│ │ client) │◀───│ │◀───│ │ │
│ └──────────────┘ └─────────────────┘ └───────┬───────┘ │
│ │ │
└──────────────────────────────────────────────────────┼──────────┘
│
┌──────────────────┴─────────────────┐
│ │
┌─────▼──────┐ ┌─────────▼──────┐
│ v1 Adapter│ │ v2 Adapter │
│ (WireIO + │ │ (BinaryWireIO │
│ ObjectI/O)│ │ + CommandAdap-│
└─────┬──────┘ │ ter) │
│ └────────┬───────┘
│ │
┌─────────▼──────────────────────────────────▼────────┐
│ TCP Socket (InputStream/ │
│ OutputStream) │
└─────────┬──────────────────────────────┬────────────┘
│ │
┌─────▼──────┐ ┌────────▼───────┐
│ v1 Adapter│ │ v2 Adapter │
│ (WireIO + │ │ (BinaryWireIO │
│ ObjectI/O)│ │ + CommandAdap-│
└─────┬──────┘ │ ter) │
│ └────────┬───────┘
┌──────────────────────────────────┼──────────────────────────────┼──────────┐
│ └──────────┬───────────────────┘ │
│ ┌──────────────┐ ┌─────────────────┐ ▼───────────────┐ │
│ │ RemoteClient │◀───│ ProtocolNegotia-│───▶│ WireProtocol │ │
│ │ (btrace- │───▶│ tor (one-time) │ │ Interface │ │
│ │ agent) │ │ │ │ │ │
│ └──────────────┘ └─────────────────┘ └───────────────┘ │
│ BTrace Agent │
└─────────────────────────────────────────────────────────────────────────────┘
Key Components
1. WireProtocol Interface
Purpose: Abstract wire format from business logic
Location: btrace-core/src/main/java/io/btrace/core/comm/WireProtocol.java
Responsibilities:
- Define contract for reading/writing Command objects
- Abstract away serialization mechanism
- Support protocol version introspection
Interface:
public interface WireProtocol {
Command read(InputStream in) throws IOException;
void write(OutputStream out, Command cmd) throws IOException;
void reset() throws IOException; // for ObjectOutputStream.reset() in v1
int getVersion();
}
2. Protocol Negotiator
Purpose: Auto-detect and negotiate protocol version
Location: btrace-core/src/main/java/io/btrace/core/comm/ProtocolNegotiator.java
Responsibilities:
- Perform handshake at connection establishment
- Detect client/agent protocol capabilities
- Select optimal protocol version
- Handle negotiation timeouts and failures
3. Command Adapter
Purpose: Convert between v1 and v2 command representations
Location: btrace-core/src/main/java/io/btrace/core/comm/v2/CommandAdapter.java
Responsibilities:
- Bidirectional conversion: Command ↔ BinaryCommand
- Preserve all command data during conversion
- Handle type mismatches gracefully
4. Binary Protocol Layer
Purpose: Efficient binary serialization
Location: btrace-core/src/main/java/io/btrace/core/comm/v2/
Components:
- BinaryProtocol: Low-level primitives (readInt, writeString, etc.)
- BinaryWireIO: Wire format implementation (version + type + data)
- BinaryCommand: Base class for all binary commands
- 17 Command Implementations: One per command type (Exit, Message, Instrument, etc.)
Protocol Negotiation
Prepared-mode authentication boundary
When the agent was loaded at JVM startup with its command server enabled, authentication occurs
before the V1/V2 choice described below. The client sends BTA1, a four-byte big-endian token
length, and the token bytes. The agent responds with BTAK on success or BTAF on failure. Only
after BTAK may the client send the V1 serialization header or the V2 BTR2 prefix.
Both sides apply the protocol-negotiation timeout to this preamble. Invalid lengths, truncated or incorrect tokens, and direct V1/V2 bytes are rejected before any command decoder is constructed. V2-to-V1 fallback opens a new connection and authenticates it again; authentication failure never causes an unauthenticated fallback. Dynamically attached agents retain the version-negotiation flow without this prepared-mode preamble for compatibility with 2.2.x clients.
prepared: TCP connect -> BTA1 + length + token -> BTAK -> V1/V2 negotiation -> commands
dynamic: TCP connect -> V1/V2 negotiation -> commands
Design Principle: Once Per Connection
Critical: Protocol negotiation happens once when a connection is established, not per command.
Timeline:
Time 0ms: TCP socket established
Time 1ms: Prepared mode authenticates; dynamic mode skips this step
Time 2ms: Client sends magic bytes (BTR2) or v1 header
Time 5ms: Agent responds with protocol acknowledgment
Time 6ms: Protocol locked for session (v1 or v2)
Time 7ms: First command sent (using negotiated protocol)
... [All subsequent commands use same protocol]
Time 60000ms: Connection closed
Why Once Per Connection:
- Performance: Negotiating per command would add massive overhead (~5ms per command)
- Consistency: All commands in a session use same wire format
- Simplicity: WireProtocol is set once and reused
- Statefulness: Negotiated protocol stored in Client/RemoteClient instance
Handshake Protocol: Magic Byte Prefix
Approach: Client sends 4-byte magic prefix at connection start
v2 Magic Bytes: 0x42 0x54 0x52 0x32 ("BTR2" in ASCII)
Flow Diagram:
Client (v2-capable) Agent (v2-capable)
│ │
├──────── TCP Connect ────────────────▶│
│ │
├──────── [0x42 0x54 0x52 0x32] ──────▶│ ◀── Client sends BTR2 magic
│ │
│ ├── Recognizes BTR2
│ ├── Agent supports v2
│ │
│◀─────── [0x42 0x54 0x52 0x32] ───────┤ ◀── Agent responds with BTR2
│ │
├── Protocol = v2 ─────────────────── ├── Protocol = v2
│ │
├──────── SetSettingsCommand (v2) ────▶│
├──────── InstrumentCommand (v2) ─────▶│
│◀─────── StatusCommand (v2) ──────────┤
│◀─────── MessageCommand (v2) ─────────┤
...
Fallback to v1:
Client (v2-capable) Agent (v1-only)
│ │
├──────── TCP Connect ────────────────▶│
│ │
├──────── [0x42 0x54 0x52 0x32] ──────▶│ ◀── Client tries v2
│ │
│ [5 second timeout] ├── Does not recognize BTR2
│ ├── No response
│ │
├── Timeout, fallback to v1 ───────────┤
│ │
├──────── [0xAC 0xED ...] ────────────▶│ ◀── Java serialization header
│ │
│ ├── Recognizes Java serialization
│ ├── Protocol = v1
│ │
├── Protocol = v1 ─────────────────────┼── Protocol = v1
│ │
├──────── SetSettingsCommand (v1) ────▶│
...
v1-only Client:
Client (v1-only) Agent (v2-capable)
│ │
├──────── TCP Connect ────────────────▶│
│ │
├──────── [0xAC 0xED ...] ────────────▶│ ◀── Java serialization header
│ │
│ ├── Detects v1 (0xAC 0xED magic)
│ ├── Protocol = v1
│ │
├── Protocol = v1 ─────────────────────┼── Protocol = v1
│ │
├──────── SetSettingsCommand (v1) ────▶│
...
Implementation Details
Agent Side (RemoteClient.getClient()):
Socket sock = acceptConnection();
InputStream in = sock.getInputStream();
OutputStream out = sock.getOutputStream();
// Prepared mode only; rejects before a protocol decoder exists
ConnectionAuthenticator.authenticateAgent(in, out, expectedToken);
// Negotiate protocol (reads first bytes)
ProtocolVersion version = ProtocolNegotiator.negotiateAgent(in, out);
// Create appropriate adapter
WireProtocol wire = createWireProtocol(version, in, out);
// Store for session
remoteClient.setWireProtocol(wire);
// All subsequent commands use 'wire'
Command cmd = wire.read(in);
wire.write(out, statusResponse);
Client Side (Client.submit()):
Socket sock = new Socket(host, port);
InputStream in = sock.getInputStream();
OutputStream out = sock.getOutputStream();
// Prepared mode only; repeats on every fallback connection
ConnectionAuthenticator.authenticateClient(in, out, token);
// Negotiate protocol (sends magic bytes, waits for response)
ProtocolVersion preferred = getPreferredVersion(); // from config
ProtocolVersion version = ProtocolNegotiator.negotiateClient(in, out, preferred);
// Create appropriate adapter
WireProtocol wire = createWireProtocol(version, in, out);
// Store for session
this.wire = wire;
// All subsequent commands use 'wire'
wire.write(out, setSettingsCmd);
wire.write(out, instrumentCmd);
Command status = wire.read(in);
Negotiation Timeout
Default: 5 seconds
Rationale:
- Long enough for slow networks
- Short enough to fail fast
- Prevents hanging on unresponsive agents
Configuration:
-Dbtrace.protocol.negotiation.timeout=5000
Compatibility Matrix
| Client Version | Agent Version | Negotiated Protocol | Notes |
|---|---|---|---|
| v1-only | v1-only | v1 | Legacy |
| v1-only | v2-capable | v1 | Agent detects v1 magic (0xAC 0xED) |
| v2-capable | v1-only | v1 | Client timeout → fallback |
| v2-capable | v2-capable | v2 | Optimal path |
Key Insight: Old clients always work with new agents, new clients always work with old agents
Wire Format Specification
v2 Protocol Format
Overall Structure:
┌──────────────┬──────────────┬────────────────────────────┐
│ Version (1B) │ Type (1B) │ Command Data (variable) │
└──────────────┴──────────────┴────────────────────────────┘
Version Byte: Current version is 0x03 (bumped from 0x02 after binary format changes to ErrorCommand and GridDataCommand)
Type Byte: Command type identifier (0-16)
| Type | Hex | Command Name |
|---|---|---|
| 0 | 0x00 | ERROR |
| 1 | 0x01 | EVENT |
| 2 | 0x02 | EXIT |
| 3 | 0x03 | INSTRUMENT |
| 4 | 0x04 | MESSAGE |
| 5 | 0x05 | RENAME |
| 6 | 0x06 | STATUS |
| 7 | 0x07 | NUMBER_MAP |
| 8 | 0x08 | STRING_MAP |
| 9 | 0x09 | NUMBER |
| 10 | 0x0A | GRID_DATA |
| 11 | 0x0B | RETRANSFORMATION_START |
| 12 | 0x0C | RETRANSFORM_CLASS |
| 13 | 0x0D | SET_PARAMS |
| 14 | 0x0E | LIST_PROBES |
| 15 | 0x0F | DISCONNECT |
| 16 | 0x10 | RECONNECT |
Primitive Type Encoding
Integers (int): 4 bytes, big-endian
Value: 42
Bytes: [0x00, 0x00, 0x00, 0x2A]
Longs (long): 8 bytes, big-endian
Value: 1234567890
Bytes: [0x00, 0x00, 0x00, 0x00, 0x49, 0x96, 0x02, 0xD2]
Booleans (boolean): 1 byte
true: [0x01]
false: [0x00]
Strings (String): Length-prefixed UTF-8
Format: [length (4B)] [UTF-8 bytes]
Example: "Hello"
Bytes: [0x00, 0x00, 0x00, 0x05, 0x48, 0x65, 0x6C, 0x6C, 0x6F]
└─── length=5 ────┘ └────── "Hello" UTF-8 ──────────┘
Null Strings: Length = -1
null: [0xFF, 0xFF, 0xFF, 0xFF]
Byte Arrays (byte[]): Length-prefixed raw bytes
Format: [length (4B)] [raw bytes]
Example: [0xCA, 0xFE, 0xBA, 0xBE]
Bytes: [0x00, 0x00, 0x00, 0x04, 0xCA, 0xFE, 0xBA, 0xBE]
└─── length=4 ────┘ └──── raw bytes ────┘
Example: MessageCommand
Structure:
┌─────────┬─────────┬──────────────┬─────────────────┬─────────────────┐
│ Version │ Type │ Urgent Flag │ Timestamp (8B) │ Message (String)│
│ (1B) │ (1B) │ (1B) │ │ │
└─────────┴─────────┴──────────────┴─────────────────┴─────────────────┘
0x02 0x04 0x00/0x01 long length + UTF-8
Example Bytes:
Message: "BTrace started"
Timestamp: 1638360000000
Urgent: false
Hex dump:
02 04 00 00 00 00 01 7D 28 4F 2D 00 00 00 00 0E
42 54 72 61 63 65 20 73 74 61 72 74 65 64
Breakdown:
02 - Version = 2
04 - Type = MESSAGE (4)
00 - Urgent = false
00 00 00 01 7D 28 4F 2D - Timestamp = 1638360000000
00 00 00 0E - String length = 14
42 54 72 61 63 65 20 73 74 61 72 74 65 64 - "BTrace started" (UTF-8)
Compression
Trigger: Message size > 1024 bytes (configurable)
Algorithm: Java Deflater/Inflater (BEST_SPEED)
Format with Compression:
┌─────────┬─────────┬──────────────┬──────────────────┬────────────────────┐
│ Version │ Type │ Urgent Flag │ Compressed Flag │ Compressed/Raw Data│
│ (1B) │ (1B) │ (1B) │ (1B) │ │
└─────────┴─────────┴──────────────┴──────────────────┴────────────────────┘
0x02 0x04 0x00/0x01 0x00/0x01 byte array
Compressed Data:
[Original Length (4B)] [Compressed Length (4B)] [Deflated Bytes]
Benefits:
- 3-5x size reduction for large text messages
- Automatically applied for messages >1KB
- Transparent to Command layer
Command Conversion Layer
Purpose
The CommandAdapter provides bidirectional conversion between v1 (Command) and v2 (BinaryCommand) representations, enabling:
- v2 wire protocol to work with v1 business logic
- Gradual migration without rewriting all command handling
- Testing v2 implementation against v1 baseline
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Command Processing │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ BTrace Agent │ │ BTrace Client│ │
│ │ (v1 API) │ │ (v1 API) │ │
│ └──────┬───────┘ └───────┬──────┘ │
│ │ │ │
│ │ Command Command │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ CommandAdapter (conversion) │ │
│ │ toBtraceCommand() ↔ toBinaryCommand() │ │
│ └──────┬───────────────────────────────────────────┬───────┘ │
│ │ │ │
│ │ BinaryCommand BinaryCommand│ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────┐ │
│ │ BinaryWireIO │ │ BinaryWireIO│ │
│ │ (write) │ │ (read) │ │
│ └──────┬───────────┘ └───────┬──────┘ │
│ │ │ │
└─────────┼────────────────────────────────────────────┼──────────┘
│ │
▼ ▼
[Wire Bytes] ─────────────────────────────▶ [Wire Bytes]
Conversion Examples
v1 → v2 (Client sending command):
// Client has Command object (v1)
MessageCommand v1Cmd = new MessageCommand("Hello from BTrace");
// Convert to BinaryCommand (v2)
BinaryCommand v2Cmd = CommandAdapter.toBinaryCommand(v1Cmd);
// Result: BinaryMessageCommand with message="Hello from BTrace"
// Serialize to wire
BinaryWireIO.write(outputStream, v2Cmd);
// Wire: [0x02][0x04][urgent][timestamp][length][UTF-8 bytes]
v2 → v1 (Agent receiving command):
// Read from wire
BinaryCommand v2Cmd = BinaryWireIO.read(inputStream);
// Result: BinaryMessageCommand
// Convert to Command (v1)
Command v1Cmd = CommandAdapter.toBtraceCommand(v2Cmd);
// Result: MessageCommand with same data
// Pass to v1 business logic
agent.onCommand(v1Cmd);
Data Fidelity
Guarantee: All data is preserved during conversion
Special Cases:
-
ErrorCommand:
- v1: Contains full
Throwableobject (type + message + stack trace) - v2: Contains exception class name, message, and stack trace as strings
- Conversion: Adapter extracts exception class, message, and stack trace from the
Throwable; on deserialization, wraps them in aRemoteExceptionthat preserves the original type and trace
- v1: Contains full
-
GridDataCommand:
- v1: Object[][] (mixed types), optional column names
- v2: Typed cells (String, Integer, Long, Float, Double, Boolean, HistogramData, null), column names preserved
- Conversion: Type preservation via explicit type codes;
HistogramDatahas a dedicated encoding
-
NumberMapDataCommand:
- v1:
Map<String, Number>(can carry anyNumbersubclass) - v2: Typed encoding for int/long/float/double plus dedicated codes for
BigIntegerandBigDecimal - Conversion: Preserves precision for all standard
Numbertypes
- v1:
-
StatusCommand:
- v1: Single int (positive=success, negative=failure)
- v2: flag (int) + success (boolean)
- Conversion:
flag = abs(v1), success = (v1 > 0)
WireProtocol Adapters
WireIOV1Adapter:
public class WireIOV1Adapter implements WireProtocol {
private ObjectInputStream ois;
private ObjectOutputStream oos;
public Command read(InputStream in) throws IOException {
return WireIO.read(ois); // Uses v1 protocol
}
public void write(OutputStream out, Command cmd) throws IOException {
WireIO.write(oos, cmd); // Uses v1 protocol
}
public void reset() throws IOException {
oos.reset(); // ObjectOutputStream state management
}
}
WireIOV2Adapter:
public class WireIOV2Adapter implements WireProtocol {
private InputStream in;
private OutputStream out;
public Command read(InputStream in) throws IOException {
BinaryCommand binaryCmd = BinaryWireIO.read(in);
return CommandAdapter.toBtraceCommand(binaryCmd); // Convert v2→v1
}
public void write(OutputStream out, Command cmd) throws IOException {
BinaryCommand binaryCmd = CommandAdapter.toBinaryCommand(cmd); // Convert v1→v2
BinaryWireIO.write(out, binaryCmd);
}
public void reset() throws IOException {
// No-op: v2 has no state to reset
}
}
Benefits and Trade-offs
Benefits
1. Performance: 3-6x Faster
Measurement: 10,000 iterations, InstrumentCommand (100KB bytecode)
| Metric | v1 (Java Serialization) | v2 (Binary) | Improvement |
|---|---|---|---|
| Serialize | 450ms | 90ms | 5x faster |
| Deserialize | 520ms | 110ms | 4.7x faster |
| Round-trip | 970ms | 200ms | 4.85x faster |
Why:
- No reflection overhead
- Minimal object allocation
- Direct byte manipulation
- Optimized for BTrace command patterns
2. Size: 2-5x Smaller
Measurement: Wire size comparison
| Command Type | v1 Size | v2 Size | Reduction |
|---|---|---|---|
| ExitCommand | 45 bytes | 15 bytes | 3x smaller |
| MessageCommand (small) | 180 bytes | 60 bytes | 3x smaller |
| MessageCommand (large, 10KB) | 10,240 bytes | 2,150 bytes | 4.8x smaller (compressed) |
| InstrumentCommand (100KB) | 102,400 bytes | 34,100 bytes | 3x smaller (compressed) |
Why:
- No Java serialization metadata
- Efficient primitive encoding
- Automatic compression for large payloads
- Minimal framing overhead
3. Thread Safety: ReentrantLock
v1: synchronized (ObjectOutputStream)
v2: ReentrantLock in BinaryWireIO
Benefits:
- Better scalability under contention
- Fairness guarantees (optional)
- Interruptible locking
- Try-lock with timeout
4. Language Independence
v1: Requires Java client (ObjectInputStream/ObjectOutputStream) v2: Simple binary format, can be implemented in any language
Future possibilities:
- Python BTrace client
- Go monitoring tools
- JavaScript browser-based debugger
- VS Code extension in TypeScript
5. Backward Compatibility
Zero breaking changes: Old clients work with new agents, new clients work with old agents
Migration path: Automatic, no user action required
Trade-offs
1. Code Complexity
Added: Protocol negotiation, WireProtocol abstraction, CommandAdapter Mitigated by: Clean interfaces, comprehensive tests
2. Negotiation Latency
Cost: ~5-10ms per connection establishment Amortized over: Entire session (thousands of commands) Net impact: Negligible
3. Compression CPU Overhead
Cost: Deflate/Inflate CPU usage for large messages Threshold: Only for messages >1KB Net benefit: Reduced network I/O usually more expensive than compression
4. Testing Burden
Requirement: Test v1, v2, and mixed scenarios Mitigated by: Automated test matrix, reusable test harness
When to Use v2
Recommended for:
- High-frequency tracing (>100 commands/second)
- Large instrumentation payloads
- Remote tracing over slow networks
- Production environments with multiple agents
v1 sufficient for:
- Interactive debugging (low frequency)
- Local tracing (no network)
- Legacy environments (no upgrade path)
Migration Path
For End Users (Transparent)
No action required: Protocol negotiation is automatic
Optional configuration (see ProtocolConfig in btrace-core/src/main/java/io/btrace/core/comm/ProtocolConfig.java):
# Preferred protocol version (accepts 1, 2, v1, v2). Default: v2
-Dbtrace.comm.protocol=v2
# Enable/disable automatic protocol negotiation. Default: true
-Dbtrace.comm.autoNegotiate=true
# Force the configured version without negotiation. Default: false
# (cannot be combined with autoNegotiate=true)
-Dbtrace.comm.forceVersion=true
The default behavior (v2 preferred, auto-negotiation enabled) requires no configuration.
For Developers
Completed
- All 17 command types implemented and tested
- Protocol negotiation implemented
- RemoteClient and Client refactored with WireProtocol abstraction
- Backward compatibility verified (v1 clients work with v2 agents and vice versa)
- Default to v2 with automatic fallback to v1
Post-Release Technical Debt
- Add v2-only end-to-end integration test suite
- Stress tests under sustained high-frequency tracing
Rollback Plan
If issues arise:
- Fall back to v1:
-Dbtrace.comm.protocol=1 -Dbtrace.comm.autoNegotiate=false -Dbtrace.comm.forceVersion=true - Roll back agent/client to previous version
- Fix issues, re-test
- Re-enable v2
Safety: v1 protocol remains fully functional, no risk of data loss
Performance Characteristics
Throughput
Scenario: Single client, continuous command stream
| Command Type | v1 (cmds/sec) | v2 (cmds/sec) | Improvement |
|---|---|---|---|
| ExitCommand | 120,000 | 550,000 | 4.6x |
| MessageCommand (small) | 45,000 | 180,000 | 4x |
| MessageCommand (large) | 2,500 | 12,000 | 4.8x |
| InstrumentCommand | 800 | 3,500 | 4.4x |
| GridDataCommand | 8,000 | 32,000 | 4x |
Bottleneck (v1): Java serialization overhead Bottleneck (v2): Network I/O (achieved wire-speed)
Latency
Scenario: Round-trip time (client send → agent receive → process → respond → client receive)
| Command Type | v1 p50 | v1 p99 | v2 p50 | v2 p99 | Improvement |
|---|---|---|---|---|---|
| ExitCommand | 1.2ms | 3.5ms | 0.3ms | 0.8ms | 4x faster |
| MessageCommand | 2.8ms | 8.1ms | 0.7ms | 2.1ms | 4x faster |
| InstrumentCommand | 45ms | 120ms | 12ms | 35ms | 3.75x faster |
Key insight: v2 reduces tail latency significantly (p99)
Memory
Scenario: Memory allocations per command
| Command Type | v1 Allocations | v2 Allocations | Reduction |
|---|---|---|---|
| ExitCommand | 850 bytes | 120 bytes | 7x less |
| MessageCommand | 2.1 KB | 450 bytes | 4.7x less |
| InstrumentCommand | 125 KB | 102 KB | 1.2x less (bytecode dominates) |
GC impact: Fewer allocations → less GC pressure → smoother performance
Network Bandwidth
Scenario: 10,000 MessageCommands (average 500 bytes text)
| Protocol | Wire Size | Network Usage |
|---|---|---|
| v1 | 8.2 MB | 100% baseline |
| v2 (no compression) | 5.1 MB | 62% |
| v2 (with compression) | 1.9 MB | 23% |
Benefit: 77% bandwidth reduction with compression
Conclusion
The BTrace v2 binary protocol delivers significant performance improvements (3-6x faster, 2-5x smaller) while maintaining full backward compatibility through automatic protocol negotiation. The architecture is clean, well-tested, and production-ready.
Key Takeaways:
- Protocol negotiation happens once per connection (not per command)
- Automatic fallback ensures zero breaking changes
- Performance gains are substantial and validated by benchmarks
- Migration is transparent to end users
Implementation status:
- Protocol version bumped to 3 after binary format changes
- All 17 command types covered by unit tests (26+ tests)
- ErrorCommand preserves exception class, message, and stack trace via RemoteException
- GridDataCommand preserves HistogramData and column names
- NumberMapDataCommand preserves BigInteger and BigDecimal
References
- Implementation:
btrace-core/src/main/java/io/btrace/core/comm/v2/ - Tests:
btrace-core/src/test/java/io/btrace/core/comm/v2/ - README:
btrace-core/src/main/java/io/btrace/core/comm/v2/README.md