tcp_coalesce_bench_test.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. package batch
  2. import (
  3. "encoding/binary"
  4. "testing"
  5. "github.com/slackhq/nebula/firewall"
  6. "github.com/slackhq/nebula/overlay/tio"
  7. "github.com/slackhq/nebula/test"
  8. )
  9. // nopTunWriter is a zero-alloc tio.GSOWriter for benchmarks. Discards
  10. // everything but satisfies the interface the coalescer detects.
  11. type nopTunWriter struct{}
  12. func (nopTunWriter) Write(p []byte) (int, error) { return len(p), nil }
  13. func (nopTunWriter) WriteGSO(hdr []byte, transportHdr []byte, pays [][]byte, _ tio.GSOProto) error {
  14. return nil
  15. }
  16. func (nopTunWriter) Capabilities() tio.Capabilities {
  17. return tio.Capabilities{TSO: true, USO: true}
  18. }
  19. // buildTCPv4BulkFlow returns a slice of N adjacent ACK-only TCP segments
  20. // on a single 5-tuple, each carrying payloadLen bytes. Seq numbers are
  21. // contiguous so every packet is coalesceable onto the previous one.
  22. func buildTCPv4BulkFlow(n, payloadLen int) [][]byte {
  23. pkts := make([][]byte, n)
  24. pay := make([]byte, payloadLen)
  25. seq := uint32(1000)
  26. for i := range n {
  27. pkts[i] = buildTCPv4(seq, tcpAck, pay)
  28. seq += uint32(payloadLen)
  29. }
  30. return pkts
  31. }
  32. // buildTCPv4Interleaved returns nFlows * perFlow packets with per-flow
  33. // seq continuity but round-robin across flows — worst case for any
  34. // "last-slot" cache.
  35. func buildTCPv4Interleaved(nFlows, perFlow, payloadLen int) [][]byte {
  36. pay := make([]byte, payloadLen)
  37. seqs := make([]uint32, nFlows)
  38. for i := range seqs {
  39. seqs[i] = uint32(1000 + i*1000000)
  40. }
  41. pkts := make([][]byte, 0, nFlows*perFlow)
  42. for range perFlow {
  43. for f := range nFlows {
  44. sport := uint16(10000 + f)
  45. pkts = append(pkts, buildTCPv4Ports(sport, 2000, seqs[f], tcpAck, pay))
  46. seqs[f] += uint32(payloadLen)
  47. }
  48. }
  49. return pkts
  50. }
  51. // buildTCPv4RunInterleaved returns nFlows*perFlow packets delivered in
  52. // runs of runLen per flow — the arrival pattern wire-side GRO actually
  53. // produces (deliverSegments splits each superdatagram into up to 64
  54. // same-flow packets back to back). Contrast with buildTCPv4Interleaved's
  55. // per-packet round-robin, the adversarial worst case for a last-slot cache.
  56. func buildTCPv4RunInterleaved(nFlows, perFlow, runLen, payloadLen int) [][]byte {
  57. pay := make([]byte, payloadLen)
  58. seqs := make([]uint32, nFlows)
  59. for i := range seqs {
  60. seqs[i] = uint32(1000 + i*1000000)
  61. }
  62. pkts := make([][]byte, 0, nFlows*perFlow)
  63. for done := 0; done < perFlow; done += runLen {
  64. for f := range nFlows {
  65. sport := uint16(10000 + f)
  66. for range runLen {
  67. pkts = append(pkts, buildTCPv4Ports(sport, 2000, seqs[f], tcpAck, pay))
  68. seqs[f] += uint32(payloadLen)
  69. }
  70. }
  71. }
  72. return pkts
  73. }
  74. // buildICMPv4 returns a minimal non-TCP packet that takes the verbatim
  75. // branch in Commit.
  76. func buildICMPv4() []byte {
  77. pkt := make([]byte, 28)
  78. pkt[0] = 0x45
  79. binary.BigEndian.PutUint16(pkt[2:4], 28)
  80. pkt[9] = 1 // ICMP
  81. copy(pkt[12:16], []byte{10, 0, 0, 1})
  82. copy(pkt[16:20], []byte{10, 0, 0, 2})
  83. return pkt
  84. }
  85. // runCommitBench drives Commit over pkts batchSize at a time, flushing
  86. // between batches, and reports per-packet cost.
  87. func runCommitBench(b *testing.B, pkts [][]byte, batchSize int) {
  88. b.Helper()
  89. c := newTestTCPCoalescer(b, nopTunWriter{})
  90. b.ReportAllocs()
  91. b.SetBytes(int64(len(pkts[0])))
  92. b.ResetTimer()
  93. for i := 0; i < b.N; i++ {
  94. pkt := pkts[i%len(pkts)]
  95. if err := c.Commit(pkt); err != nil {
  96. b.Fatal(err)
  97. }
  98. if (i+1)%batchSize == 0 {
  99. if err := c.Flush(); err != nil {
  100. b.Fatal(err)
  101. }
  102. }
  103. }
  104. // Drain any trailing partial batch so slot state doesn't leak across runs.
  105. _ = c.Flush()
  106. }
  107. // BenchmarkCommitSingleFlow is the bulk-TCP steady state: one flow,
  108. // contiguous seq, 1200-byte payloads. Every packet past the seed should
  109. // append onto the open slot. This is the case we most care about.
  110. func BenchmarkCommitSingleFlow(b *testing.B) {
  111. pkts := buildTCPv4BulkFlow(tcpCoalesceMaxSegs, 1200)
  112. runCommitBench(b, pkts, tcpCoalesceMaxSegs)
  113. }
  114. // BenchmarkCommitInterleaved4 has 4 concurrent bulk flows round-robined.
  115. // A single-entry fast-path cache will miss on every packet; an N-way
  116. // cache or map lookup carries the weight.
  117. func BenchmarkCommitInterleaved4(b *testing.B) {
  118. pkts := buildTCPv4Interleaved(4, tcpCoalesceMaxSegs, 1200)
  119. runCommitBench(b, pkts, len(pkts))
  120. }
  121. // BenchmarkCommitInterleaved16 stresses the map at higher flow counts.
  122. func BenchmarkCommitInterleaved16(b *testing.B) {
  123. pkts := buildTCPv4Interleaved(16, tcpCoalesceMaxSegs, 1200)
  124. runCommitBench(b, pkts, len(pkts))
  125. }
  126. // BenchmarkCommitRunInterleaved4 is 4 concurrent flows arriving in
  127. // GRO-burst runs of 16 — the realistic multi-flow pattern. A last-slot
  128. // cache hits for the length of each run; the per-packet round-robin
  129. // benches above are its worst case.
  130. func BenchmarkCommitRunInterleaved4(b *testing.B) {
  131. pkts := buildTCPv4RunInterleaved(4, tcpCoalesceMaxSegs, 16, 1200)
  132. runCommitBench(b, pkts, len(pkts))
  133. }
  134. // BenchmarkCommitPassthrough exercises the non-TCP branch: parseBase
  135. // bails early and addVerbatim is the only work.
  136. func BenchmarkCommitPassthrough(b *testing.B) {
  137. pkt := buildICMPv4()
  138. pkts := make([][]byte, 64)
  139. for i := range pkts {
  140. pkts[i] = pkt
  141. }
  142. runCommitBench(b, pkts, 64)
  143. }
  144. // BenchmarkCommitNonCoalesceableTCP sends SYN|ACK packets on one flow.
  145. // Each packet takes the "TCP but not admissible" branch which does a
  146. // map delete + verbatim. Measures the seal-without-slot cost.
  147. func BenchmarkCommitNonCoalesceableTCP(b *testing.B) {
  148. pay := make([]byte, 0)
  149. pkts := make([][]byte, 64)
  150. for i := range pkts {
  151. pkts[i] = buildTCPv4(uint32(1000+i), tcpSyn|tcpAck, pay)
  152. }
  153. runCommitBench(b, pkts, 64)
  154. }
  155. // runMultiCommitBench drives MultiCoalescer.Commit with in-order keys, so
  156. // it includes the staging sort's already-sorted fast path plus the
  157. // dispatch-time parse — the full steady-state cost of the batcher. The
  158. // ParsedPackets are precomputed: in production they fall out of the
  159. // firewall's newPacket, which this bench does not model.
  160. func runMultiCommitBench(b *testing.B, pkts [][]byte, batchSize int) {
  161. b.Helper()
  162. m := NewMultiCoalescer(nopTunWriter{}, test.NewLogger())
  163. pps := make([]*firewall.ParsedPacket, len(pkts))
  164. for i, p := range pkts {
  165. pps[i] = testPP(p)
  166. }
  167. b.ReportAllocs()
  168. b.SetBytes(int64(len(pkts[0])))
  169. b.ResetTimer()
  170. for i := 0; i < b.N; i++ {
  171. j := i % len(pkts)
  172. if err := m.Commit(pkts[j], SortKey{Epoch: 1, Counter: uint64(i + 1)}, pps[j]); err != nil {
  173. b.Fatal(err)
  174. }
  175. if (i+1)%batchSize == 0 {
  176. if err := m.Flush(); err != nil {
  177. b.Fatal(err)
  178. }
  179. }
  180. }
  181. _ = m.Flush()
  182. }
  183. // BenchmarkMultiCommitSingleFlow is the multi-lane analogue of
  184. // BenchmarkCommitSingleFlow — same workload but routed through the
  185. // dispatcher. The delta vs the single-lane bench measures dispatcher
  186. // overhead.
  187. func BenchmarkMultiCommitSingleFlow(b *testing.B) {
  188. pkts := buildTCPv4BulkFlow(tcpCoalesceMaxSegs, 1200)
  189. runMultiCommitBench(b, pkts, tcpCoalesceMaxSegs)
  190. }
  191. // BenchmarkMultiCommitInterleaved4 mirrors BenchmarkCommitInterleaved4
  192. // through the dispatcher.
  193. func BenchmarkMultiCommitInterleaved4(b *testing.B) {
  194. pkts := buildTCPv4Interleaved(4, tcpCoalesceMaxSegs, 1200)
  195. runMultiCommitBench(b, pkts, len(pkts))
  196. }