// Package messages benchmarks compare three serialization strategies: // // 1. msgpack map[string]any — the old approach (dynamic, no types) // 2. msgpack typed struct — the new approach (compile-time safe, short keys) // 3. protobuf — optional future migration // // Run with: // // go test -bench=. -benchmem -count=5 ./messages/... | tee bench.txt // # optional: go install golang.org/x/perf/cmd/benchstat@latest && benchstat bench.txt package messages import ( "testing" "time" "github.com/vmihailenco/msgpack/v5" "google.golang.org/protobuf/proto" pb "git.daviestechlabs.io/daviestechlabs/handler-base/messages/proto" ) // ──────────────────────────────────────────────────────────────────────────── // Test fixtures — equivalent data across all three encodings // ──────────────────────────────────────────────────────────────────────────── // chatRequestMap is the legacy map[string]any representation. func chatRequestMap() map[string]any { return map[string]any{ "request_id": "req-abc-123", "user_id": "user-42", "message": "What is the capital of France?", "query": "", "premium": true, "enable_rag": true, "enable_reranker": true, "enable_streaming": false, "top_k": 10, "collection": "documents", "enable_tts": false, "system_prompt": "You are a helpful assistant.", "response_subject": "ai.chat.response.req-abc-123", } } // chatRequestStruct is the typed struct representation. func chatRequestStruct() ChatRequest { return ChatRequest{ RequestID: "req-abc-123", UserID: "user-42", Message: "What is the capital of France?", Premium: true, EnableRAG: true, EnableReranker: true, TopK: 10, Collection: "documents", SystemPrompt: "You are a helpful assistant.", ResponseSubject: "ai.chat.response.req-abc-123", } } // chatRequestProto is the protobuf representation. func chatRequestProto() *pb.ChatRequest { return &pb.ChatRequest{ RequestId: "req-abc-123", UserId: "user-42", Message: "What is the capital of France?", Premium: true, EnableRag: true, EnableReranker: true, TopK: 10, Collection: "documents", SystemPrompt: "You are a helpful assistant.", ResponseSubject: "ai.chat.response.req-abc-123", } } // voiceResponseMap is a voice response with a 16 KB audio payload. func voiceResponseMap() map[string]any { return map[string]any{ "request_id": "vr-001", "response": "The capital of France is Paris.", "audio": make([]byte, 16384), "transcription": "What is the capital of France?", } } func voiceResponseStruct() VoiceResponse { return VoiceResponse{ RequestID: "vr-001", Response: "The capital of France is Paris.", Audio: make([]byte, 16384), Transcription: "What is the capital of France?", } } func voiceResponseProto() *pb.VoiceResponse { return &pb.VoiceResponse{ RequestId: "vr-001", Response: "The capital of France is Paris.", Audio: make([]byte, 16384), Transcription: "What is the capital of France?", } } // ttsChunkMap simulates a streaming audio chunk (~32 KB). func ttsChunkMap() map[string]any { return map[string]any{ "session_id": "tts-sess-99", "chunk_index": 3, "total_chunks": 12, "audio_b64": string(make([]byte, 32768)), // old: base64 string "is_last": false, "timestamp": time.Now().Unix(), "sample_rate": 24000, } } func ttsChunkStruct() TTSAudioChunk { return TTSAudioChunk{ SessionID: "tts-sess-99", ChunkIndex: 3, TotalChunks: 12, Audio: make([]byte, 32768), // new: raw bytes IsLast: false, Timestamp: time.Now().Unix(), SampleRate: 24000, } } func ttsChunkProto() *pb.TTSAudioChunk { return &pb.TTSAudioChunk{ SessionId: "tts-sess-99", ChunkIndex: 3, TotalChunks: 12, Audio: make([]byte, 32768), IsLast: false, Timestamp: time.Now().Unix(), SampleRate: 24000, } } // ──────────────────────────────────────────────────────────────────────────── // Wire-size comparison (run once, printed by TestWireSize) // ──────────────────────────────────────────────────────────────────────────── func TestWireSize(t *testing.T) { tests := []struct { name string mapData any structVal any protoMsg proto.Message }{ {"ChatRequest", chatRequestMap(), chatRequestStruct(), chatRequestProto()}, {"VoiceResponse", voiceResponseMap(), voiceResponseStruct(), voiceResponseProto()}, {"TTSAudioChunk", ttsChunkMap(), ttsChunkStruct(), ttsChunkProto()}, } for _, tt := range tests { mapBytes, _ := msgpack.Marshal(tt.mapData) structBytes, _ := msgpack.Marshal(tt.structVal) protoBytes, _ := proto.Marshal(tt.protoMsg) t.Logf("%-16s map=%5d B struct=%5d B proto=%5d B (struct saves %.0f%%, proto saves %.0f%%)", tt.name, len(mapBytes), len(structBytes), len(protoBytes), 100*(1-float64(len(structBytes))/float64(len(mapBytes))), 100*(1-float64(len(protoBytes))/float64(len(mapBytes))), ) } } // ──────────────────────────────────────────────────────────────────────────── // Encode benchmarks // ──────────────────────────────────────────────────────────────────────────── func BenchmarkEncode_ChatRequest_MsgpackMap(b *testing.B) { data := chatRequestMap() b.ResetTimer() for b.Loop() { _, _ = msgpack.Marshal(data) } } func BenchmarkEncode_ChatRequest_MsgpackStruct(b *testing.B) { data := chatRequestStruct() b.ResetTimer() for b.Loop() { _, _ = msgpack.Marshal(data) } } func BenchmarkEncode_ChatRequest_Protobuf(b *testing.B) { data := chatRequestProto() b.ResetTimer() for b.Loop() { _, _ = proto.Marshal(data) } } func BenchmarkEncode_VoiceResponse_MsgpackMap(b *testing.B) { data := voiceResponseMap() b.ResetTimer() for b.Loop() { _, _ = msgpack.Marshal(data) } } func BenchmarkEncode_VoiceResponse_MsgpackStruct(b *testing.B) { data := voiceResponseStruct() b.ResetTimer() for b.Loop() { _, _ = msgpack.Marshal(data) } } func BenchmarkEncode_VoiceResponse_Protobuf(b *testing.B) { data := voiceResponseProto() b.ResetTimer() for b.Loop() { _, _ = proto.Marshal(data) } } func BenchmarkEncode_TTSChunk_MsgpackMap(b *testing.B) { data := ttsChunkMap() b.ResetTimer() for b.Loop() { _, _ = msgpack.Marshal(data) } } func BenchmarkEncode_TTSChunk_MsgpackStruct(b *testing.B) { data := ttsChunkStruct() b.ResetTimer() for b.Loop() { _, _ = msgpack.Marshal(data) } } func BenchmarkEncode_TTSChunk_Protobuf(b *testing.B) { data := ttsChunkProto() b.ResetTimer() for b.Loop() { _, _ = proto.Marshal(data) } } // ──────────────────────────────────────────────────────────────────────────── // Decode benchmarks // ──────────────────────────────────────────────────────────────────────────── func BenchmarkDecode_ChatRequest_MsgpackMap(b *testing.B) { encoded, _ := msgpack.Marshal(chatRequestMap()) b.ResetTimer() for b.Loop() { var m map[string]any _ = msgpack.Unmarshal(encoded, &m) } } func BenchmarkDecode_ChatRequest_MsgpackStruct(b *testing.B) { encoded, _ := msgpack.Marshal(chatRequestStruct()) b.ResetTimer() for b.Loop() { var m ChatRequest _ = msgpack.Unmarshal(encoded, &m) } } func BenchmarkDecode_ChatRequest_Protobuf(b *testing.B) { encoded, _ := proto.Marshal(chatRequestProto()) b.ResetTimer() for b.Loop() { var m pb.ChatRequest _ = proto.Unmarshal(encoded, &m) } } func BenchmarkDecode_VoiceResponse_MsgpackMap(b *testing.B) { encoded, _ := msgpack.Marshal(voiceResponseMap()) b.ResetTimer() for b.Loop() { var m map[string]any _ = msgpack.Unmarshal(encoded, &m) } } func BenchmarkDecode_VoiceResponse_MsgpackStruct(b *testing.B) { encoded, _ := msgpack.Marshal(voiceResponseStruct()) b.ResetTimer() for b.Loop() { var m VoiceResponse _ = msgpack.Unmarshal(encoded, &m) } } func BenchmarkDecode_VoiceResponse_Protobuf(b *testing.B) { encoded, _ := proto.Marshal(voiceResponseProto()) b.ResetTimer() for b.Loop() { var m pb.VoiceResponse _ = proto.Unmarshal(encoded, &m) } } func BenchmarkDecode_TTSChunk_MsgpackMap(b *testing.B) { encoded, _ := msgpack.Marshal(ttsChunkMap()) b.ResetTimer() for b.Loop() { var m map[string]any _ = msgpack.Unmarshal(encoded, &m) } } func BenchmarkDecode_TTSChunk_MsgpackStruct(b *testing.B) { encoded, _ := msgpack.Marshal(ttsChunkStruct()) b.ResetTimer() for b.Loop() { var m TTSAudioChunk _ = msgpack.Unmarshal(encoded, &m) } } func BenchmarkDecode_TTSChunk_Protobuf(b *testing.B) { encoded, _ := proto.Marshal(ttsChunkProto()) b.ResetTimer() for b.Loop() { var m pb.TTSAudioChunk _ = proto.Unmarshal(encoded, &m) } } // ──────────────────────────────────────────────────────────────────────────── // Roundtrip benchmarks (encode + decode) // ──────────────────────────────────────────────────────────────────────────── func BenchmarkRoundtrip_ChatRequest_MsgpackMap(b *testing.B) { data := chatRequestMap() b.ResetTimer() for b.Loop() { enc, _ := msgpack.Marshal(data) var dec map[string]any _ = msgpack.Unmarshal(enc, &dec) } } func BenchmarkRoundtrip_ChatRequest_MsgpackStruct(b *testing.B) { data := chatRequestStruct() b.ResetTimer() for b.Loop() { enc, _ := msgpack.Marshal(data) var dec ChatRequest _ = msgpack.Unmarshal(enc, &dec) } } func BenchmarkRoundtrip_ChatRequest_Protobuf(b *testing.B) { data := chatRequestProto() b.ResetTimer() for b.Loop() { enc, _ := proto.Marshal(data) var dec pb.ChatRequest _ = proto.Unmarshal(enc, &dec) } } // ──────────────────────────────────────────────────────────────────────────── // Typed struct unit tests — verify roundtrip correctness // ──────────────────────────────────────────────────────────────────────────── func TestRoundtrip_ChatRequest(t *testing.T) { orig := chatRequestStruct() data, err := msgpack.Marshal(orig) if err != nil { t.Fatal(err) } var dec ChatRequest if err := msgpack.Unmarshal(data, &dec); err != nil { t.Fatal(err) } if dec.RequestID != orig.RequestID { t.Errorf("RequestID = %q, want %q", dec.RequestID, orig.RequestID) } if dec.Message != orig.Message { t.Errorf("Message = %q, want %q", dec.Message, orig.Message) } if dec.TopK != orig.TopK { t.Errorf("TopK = %d, want %d", dec.TopK, orig.TopK) } if dec.Premium != orig.Premium { t.Errorf("Premium = %v, want %v", dec.Premium, orig.Premium) } if dec.EffectiveQuery() != orig.Message { t.Errorf("EffectiveQuery() = %q, want %q", dec.EffectiveQuery(), orig.Message) } } func TestRoundtrip_VoiceResponse(t *testing.T) { orig := voiceResponseStruct() data, err := msgpack.Marshal(orig) if err != nil { t.Fatal(err) } var dec VoiceResponse if err := msgpack.Unmarshal(data, &dec); err != nil { t.Fatal(err) } if dec.RequestID != orig.RequestID { t.Errorf("RequestID mismatch") } if len(dec.Audio) != len(orig.Audio) { t.Errorf("Audio len = %d, want %d", len(dec.Audio), len(orig.Audio)) } if dec.Transcription != orig.Transcription { t.Errorf("Transcription mismatch") } } func TestRoundtrip_TTSAudioChunk(t *testing.T) { orig := ttsChunkStruct() data, err := msgpack.Marshal(orig) if err != nil { t.Fatal(err) } var dec TTSAudioChunk if err := msgpack.Unmarshal(data, &dec); err != nil { t.Fatal(err) } if dec.SessionID != orig.SessionID { t.Errorf("SessionID mismatch") } if dec.ChunkIndex != orig.ChunkIndex { t.Errorf("ChunkIndex = %d, want %d", dec.ChunkIndex, orig.ChunkIndex) } if len(dec.Audio) != len(orig.Audio) { t.Errorf("Audio len = %d, want %d", len(dec.Audio), len(orig.Audio)) } if dec.SampleRate != orig.SampleRate { t.Errorf("SampleRate = %d, want %d", dec.SampleRate, orig.SampleRate) } } func TestRoundtrip_PipelineTrigger(t *testing.T) { orig := PipelineTrigger{ RequestID: "pip-001", Pipeline: "document-ingestion", Parameters: map[string]any{"source": "s3://bucket/data"}, } data, err := msgpack.Marshal(orig) if err != nil { t.Fatal(err) } var dec PipelineTrigger if err := msgpack.Unmarshal(data, &dec); err != nil { t.Fatal(err) } if dec.Pipeline != orig.Pipeline { t.Errorf("Pipeline = %q, want %q", dec.Pipeline, orig.Pipeline) } if dec.Parameters["source"] != orig.Parameters["source"] { t.Errorf("Parameters[source] mismatch") } } func TestRoundtrip_STTTranscription(t *testing.T) { orig := STTTranscription{ SessionID: "stt-001", Transcript: "hello world", Sequence: 5, IsPartial: false, IsFinal: true, Timestamp: time.Now().Unix(), SpeakerID: "speaker-1", HasVoiceActivity: true, State: "listening", } data, err := msgpack.Marshal(orig) if err != nil { t.Fatal(err) } var dec STTTranscription if err := msgpack.Unmarshal(data, &dec); err != nil { t.Fatal(err) } if dec.Transcript != orig.Transcript { t.Errorf("Transcript = %q, want %q", dec.Transcript, orig.Transcript) } if dec.IsFinal != orig.IsFinal { t.Error("IsFinal mismatch") } } func TestRoundtrip_ErrorResponse(t *testing.T) { orig := ErrorResponse{Error: true, Message: "something broke", Type: "InternalError"} data, err := msgpack.Marshal(orig) if err != nil { t.Fatal(err) } var dec ErrorResponse if err := msgpack.Unmarshal(data, &dec); err != nil { t.Fatal(err) } if !dec.Error || dec.Message != "something broke" || dec.Type != "InternalError" { t.Errorf("ErrorResponse roundtrip mismatch: %+v", dec) } } func TestTimestamp(t *testing.T) { ts := Timestamp() now := time.Now().Unix() if ts < now-1 || ts > now+1 { t.Errorf("Timestamp() = %d, expected ~%d", ts, now) } }