Skip to content

Commit cffa7f0

Browse files
committed
Add XOR benchmarks
1 parent 9603c3b commit cffa7f0

File tree

5 files changed

+158
-116
lines changed

5 files changed

+158
-116
lines changed

bench_test.go

Lines changed: 0 additions & 112 deletions
This file was deleted.

websocket.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ func (c *Conn) handleControl(h header) {
228228
}
229229

230230
if h.masked {
231-
xor(h.maskKey, 0, b)
231+
fastXOR(h.maskKey, 0, b)
232232
}
233233

234234
switch h.opcode {
@@ -322,7 +322,7 @@ func (c *Conn) dataReadLoop(h header) (err error) {
322322
left -= int64(len(b))
323323

324324
if h.masked {
325-
maskPos = xor(h.maskKey, maskPos, b)
325+
maskPos = fastXOR(h.maskKey, maskPos, b)
326326
}
327327

328328
// Must set this before we signal the read is done.

websocket_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,3 +701,100 @@ func checkWSTestIndex(t *testing.T, path string) {
701701
}
702702
}
703703
}
704+
705+
func benchConn(b *testing.B, echo, stream bool) {
706+
name := "buffered"
707+
if stream {
708+
name = "stream"
709+
}
710+
711+
b.Run(name, func(b *testing.B) {
712+
s, closeFn := testServer(b, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
713+
c, err := websocket.Accept(w, r, websocket.AcceptOptions{})
714+
if err != nil {
715+
b.Logf("server handshake failed: %+v", err)
716+
return
717+
}
718+
if echo {
719+
echoLoop(r.Context(), c)
720+
} else {
721+
discardLoop(r.Context(), c)
722+
}
723+
}))
724+
defer closeFn()
725+
726+
wsURL := strings.Replace(s.URL, "http", "ws", 1)
727+
728+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
729+
defer cancel()
730+
731+
c, _, err := websocket.Dial(ctx, wsURL, websocket.DialOptions{})
732+
if err != nil {
733+
b.Fatalf("failed to dial: %v", err)
734+
}
735+
defer c.Close(websocket.StatusInternalError, "")
736+
737+
sizes := []int{
738+
2,
739+
512,
740+
4096,
741+
16384,
742+
}
743+
744+
for _, size := range sizes {
745+
msg := []byte(strings.Repeat("2", size))
746+
buf := make([]byte, len(msg))
747+
b.Run(strconv.Itoa(size), func(b *testing.B) {
748+
b.SetBytes(int64(len(msg)))
749+
b.ReportAllocs()
750+
for i := 0; i < b.N; i++ {
751+
if stream {
752+
w, err := c.Writer(ctx, websocket.MessageText)
753+
if err != nil {
754+
b.Fatal(err)
755+
}
756+
757+
_, err = w.Write(msg)
758+
if err != nil {
759+
b.Fatal(err)
760+
}
761+
762+
err = w.Close()
763+
if err != nil {
764+
b.Fatal(err)
765+
}
766+
} else {
767+
err = c.Write(ctx, websocket.MessageText, msg)
768+
if err != nil {
769+
b.Fatal(err)
770+
}
771+
}
772+
773+
if echo {
774+
_, r, err := c.Reader(ctx)
775+
if err != nil {
776+
b.Fatal(err)
777+
}
778+
779+
_, err = io.ReadFull(r, buf)
780+
if err != nil {
781+
b.Fatal(err)
782+
}
783+
}
784+
}
785+
})
786+
}
787+
788+
c.Close(websocket.StatusNormalClosure, "")
789+
})
790+
}
791+
792+
func BenchmarkConn(b *testing.B) {
793+
b.Run("write", func(b *testing.B) {
794+
benchConn(b, false, false)
795+
benchConn(b, false, true)
796+
})
797+
b.Run("echo", func(b *testing.B) {
798+
benchConn(b, true, true)
799+
})
800+
}

xor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
// The returned value is the position of the next byte
1313
// to be used for masking in the key. This is so that
1414
// unmasking can be performed without the entire frame.
15-
func xor(key [4]byte, keyPos int, b []byte) int {
15+
func fastXOR(key [4]byte, keyPos int, b []byte) int {
1616
// If the payload is greater than 16 bytes, then it's worth
1717
// masking 8 bytes at a time.
1818
// Optimization from https://github.com/golang/go/issues/31586#issuecomment-485530859

xor_test.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package websocket
22

33
import (
4+
"crypto/rand"
5+
"strconv"
46
"testing"
57

68
"github.com/google/go-cmp/cmp"
@@ -12,7 +14,7 @@ func Test_xor(t *testing.T) {
1214
key := [4]byte{0xa, 0xb, 0xc, 0xff}
1315
p := []byte{0xa, 0xb, 0xc, 0xf2, 0xc}
1416
pos := 0
15-
pos = xor(key, pos, p)
17+
pos = fastXOR(key, pos, p)
1618

1719
if exp := []byte{0, 0, 0, 0x0d, 0x6}; !cmp.Equal(exp, p) {
1820
t.Fatalf("unexpected mask: %v", cmp.Diff(exp, p))
@@ -22,3 +24,58 @@ func Test_xor(t *testing.T) {
2224
t.Fatalf("unexpected mask pos: %v", cmp.Diff(exp, pos))
2325
}
2426
}
27+
28+
func basixXOR(maskKey [4]byte, pos int, b []byte) int {
29+
for i := range b {
30+
b[i] ^= maskKey[pos&3]
31+
pos++
32+
}
33+
return pos & 3
34+
}
35+
36+
func BenchmarkXOR(b *testing.B) {
37+
sizes := []int{
38+
2,
39+
32,
40+
512,
41+
4096,
42+
16384,
43+
}
44+
45+
fns := []struct {
46+
name string
47+
fn func([4]byte, int, []byte) int
48+
}{
49+
{
50+
"basic",
51+
basixXOR,
52+
},
53+
{
54+
"fast",
55+
fastXOR,
56+
},
57+
}
58+
59+
var maskKey [4]byte
60+
_, err := rand.Read(maskKey[:])
61+
if err != nil {
62+
b.Fatalf("failed to populate mask key: %v", err)
63+
}
64+
65+
for _, size := range sizes {
66+
data := make([]byte, size)
67+
68+
b.Run(strconv.Itoa(size), func(b *testing.B) {
69+
for _, fn := range fns {
70+
b.Run(fn.name, func(b *testing.B) {
71+
b.ReportAllocs()
72+
b.SetBytes(int64(size))
73+
74+
for i := 0; i < b.N; i++ {
75+
fn.fn(maskKey, 0, data)
76+
}
77+
})
78+
}
79+
})
80+
}
81+
}

0 commit comments

Comments
 (0)