From 92efc7fddd5c283d7ab70322f7b0f40f173e7ff3 Mon Sep 17 00:00:00 2001 From: Tyson Andre Date: Mon, 20 Nov 2017 21:21:12 -0800 Subject: [PATCH 1/2] Speed up scanning memcache responses, increase throughput. The Sscanf was revealed as being CPU intensive by the -cpuprofile option (It has to use reflection to know what to parse?) ``` export GOMAXPROCS=4; go test -run='^$' -bench SetGet -benchtime 5s ``` Before this change: BenchmarkSetGet-4 200000 50299 ns/op After this change: BenchmarkSetGet-4 200000 45301 ns/op --- memcache/memcache.go | 48 ++++++++++++++++++++++++++++++++++ memcache/memcache_test.go | 55 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/memcache/memcache.go b/memcache/memcache.go index b98a7653..00b3ca53 100644 --- a/memcache/memcache.go +++ b/memcache/memcache.go @@ -113,6 +113,8 @@ var ( resultTouched = []byte("TOUCHED\r\n") resultClientErrorPrefix = []byte("CLIENT_ERROR ") + + valuePrefix = []byte("VALUE ") ) // New returns a memcache client using the provided server(s) @@ -495,6 +497,51 @@ func parseGetResponse(r *bufio.Reader, cb func(*Item)) error { // scanGetResponseLine populates it and returns the declared size of the item. // It does not read the bytes of the item. +// This should be equivalent to the commented out code, which was more time consuming due to reflection in Sscanf. +func scanGetResponseLine(line []byte, it *Item) (size int, err error) { + if len(line) < 13 { + // "VALUE x 0 1\r\n" is the shortest possible message, and that is 13 bytes long. + return -1, fmt.Errorf("Line is too short: %q", line) + } + if !bytes.Equal(line[:6], valuePrefix) { + return -1, fmt.Errorf("Expected line to begin with \"VALUE \": %q", line) + } + if !bytes.Equal(line[len(line)-2:], crlf) { + return -1, fmt.Errorf("Expected line to end with \\r\\n: %q", line) + } + line = line[6 : len(line)-2] + parts := bytes.Split(line, space) + // pattern := "VALUE %s %d %d %d\r\n" + // dest := []interface{}{&it.Key, &it.Flags, &size, &it.casid} + partsCount := len(parts) + if partsCount < 3 || partsCount > 4 { + return -1, fmt.Errorf("Expected line to match %s %s %d [%d]: got %q", line) + } + // "%s %d %d\n" + it.Key = string(parts[0]) + if it.Key == "" { + return -1, fmt.Errorf("memcache: unexpected empty key in %q: %v", line, err) + } + flagsRaw, err := strconv.ParseUint(string(parts[1]), 10, 32) + it.Flags = uint32(flagsRaw) + if err != nil { + return -1, fmt.Errorf("memcache: unexpected flags in %q: %v", line, err) + } + sizeRaw, err := strconv.ParseInt(string(parts[2]), 10, 0) + if err != nil { + return -1, fmt.Errorf("memcache: unexpected size in %q: %v", line, err) + } + if partsCount >= 4 { + // "%s %d %d %d\n" + it.casid, err = strconv.ParseUint(string(parts[3]), 10, 64) + if err != nil { + return -1, fmt.Errorf("memcache: unexpected casid in %q: %v", line, err) + } + } + return int(sizeRaw), nil +} + +/* func scanGetResponseLine(line []byte, it *Item) (size int, err error) { pattern := "VALUE %s %d %d %d\r\n" dest := []interface{}{&it.Key, &it.Flags, &size, &it.casid} @@ -508,6 +555,7 @@ func scanGetResponseLine(line []byte, it *Item) (size int, err error) { } return size, nil } +*/ // Set writes the given item, unconditionally. func (c *Client) Set(item *Item) error { diff --git a/memcache/memcache_test.go b/memcache/memcache_test.go index 4b52a911..a9608b6f 100644 --- a/memcache/memcache_test.go +++ b/memcache/memcache_test.go @@ -26,13 +26,18 @@ import ( "os" "os/exec" "strings" + "sync" "testing" "time" ) const testServer = "localhost:11211" -func setup(t *testing.T) bool { +type skippable interface { + Skipf(format string, args ...interface{}) +} + +func setup(t skippable) bool { c, err := net.Dial("tcp", testServer) if err != nil { t.Skipf("skipping test; no server running at %s", testServer) @@ -287,3 +292,51 @@ func BenchmarkOnItem(b *testing.B) { c.onItem(&item, dummyFn) } } + +func BenchmarkSetGet(b *testing.B) { + if !setup(b) { + return + } + // Don't call flush_all + c := New(testServer) + + checkErr := func(err error, format string, args ...interface{}) { + if err != nil { + b.Fatalf(format, args...) + } + } + + benchmarkWorkerCount := 4 + N := b.N + wg := sync.WaitGroup{} + wg.Add(benchmarkWorkerCount) + for j := 0; j < benchmarkWorkerCount; j++ { + j := j + go func() { + defer wg.Done() + expectedKey := fmt.Sprintf("foo%d", j+100) + for i := 0; i < N; i++ { + expectedValue := fmt.Sprintf("foo%d", i+10000000) + + // Set + foo := &Item{Key: expectedKey, Value: []byte(expectedValue), Flags: 123} + err := c.Set(foo) + checkErr(err, "first set(%s): %v", expectedKey, err) + + // Get + it, err := c.Get(expectedKey) + checkErr(err, "get(%s): %v", expectedKey, err) + if it.Key != expectedKey { + b.Errorf("get(%s) Key = %q, want %s", expectedKey, it.Key, expectedKey) + } + if string(it.Value) != expectedValue { + b.Errorf("get(%s) Value = %q, want %s", expectedKey, string(it.Value), expectedValue) + } + if it.Flags != 123 { + b.Errorf("get(foo) Flags = %v, want 123", it.Flags) + } + } + }() + } + wg.Wait() +} From 10bcdf348b9e253765354da7aafa156be886f943 Mon Sep 17 00:00:00 2001 From: Tyson Andre Date: Wed, 22 Nov 2017 11:30:54 -0800 Subject: [PATCH 2/2] Fix an incorrect printf format string --- memcache/memcache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/memcache/memcache.go b/memcache/memcache.go index 00b3ca53..72686032 100644 --- a/memcache/memcache.go +++ b/memcache/memcache.go @@ -515,7 +515,7 @@ func scanGetResponseLine(line []byte, it *Item) (size int, err error) { // dest := []interface{}{&it.Key, &it.Flags, &size, &it.casid} partsCount := len(parts) if partsCount < 3 || partsCount > 4 { - return -1, fmt.Errorf("Expected line to match %s %s %d [%d]: got %q", line) + return -1, fmt.Errorf("Expected line to match %%s %%s %%d [%%d]: got %q", line) } // "%s %d %d\n" it.Key = string(parts[0])