mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
reduce allocs in getSlotQueueKey (#778)
this somewhat minimally comes up in profiling, but it was an itch i needed to scratch. this does 10x less allocations and is 3x faster (with 3x less bytes), and they're the small painful kind of allocation. we're only reading these strings so the uses of unsafe are fine (I think audit me). the byte array we're casting to a string at the end is also heap allocated and does escape. I only count 2 allocations, but there's 3 (`hash.Sum` and `make([]string)`), using a pool of sha1 hash.Hash shaves 120 byte and an alloc off so seems worth it (it's minimal). if we set a max size of config vals with a constant we could avoid that allocation and we could probably find a checksum package that doesn't use the `hash.Hash` that would speed things up a little (no dynamic dispatch, doesn't allocate in Sum) but there's not one I know of in stdlib. master: ``` ✗: go test -run=yodawg -bench . -benchmem -benchtime 1s -cpuprofile cpu.out goos: linux goarch: amd64 pkg: github.com/fnproject/fn/api/agent BenchmarkSlotKey 200000 6068 ns/op 696 B/op 31 allocs/op PASS ok github.com/fnproject/fn/api/agent 1.454s ``` now: ``` ✗: go test -run=yodawg -bench . -benchmem -benchtime 1s -cpuprofile cpu.out goos: linux goarch: amd64 pkg: github.com/fnproject/fn/api/agent BenchmarkSlotKey 1000000 1901 ns/op 168 B/op 3 allocs/op PASS ok github.com/fnproject/fn/api/agent 2.092s ``` once we have versioned apps/routes we don't need to build a sha or sort configs so this will get a lot faster. anyway, mostly funsies here... my life is that sad now.
This commit is contained in:
@@ -3,10 +3,12 @@ package agent
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"encoding/binary"
|
||||
"hash"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//
|
||||
@@ -270,31 +272,70 @@ func (a *slotQueueMgr) deleteSlotQueue(slots *slotQueue) bool {
|
||||
return isDeleted
|
||||
}
|
||||
|
||||
var shapool = &sync.Pool{New: func() interface{} { return sha1.New() }}
|
||||
|
||||
// TODO do better; once we have app+route versions this function
|
||||
// can be simply app+route names & version
|
||||
func getSlotQueueKey(call *call) string {
|
||||
// return a sha1 hash of a (hopefully) unique string of all the config
|
||||
// values, to make map lookups quicker [than the giant unique string]
|
||||
|
||||
hash := sha1.New()
|
||||
fmt.Fprint(hash, call.AppName, "\x00")
|
||||
fmt.Fprint(hash, call.Path, "\x00")
|
||||
fmt.Fprint(hash, call.Image, "\x00")
|
||||
fmt.Fprint(hash, call.Timeout, "\x00")
|
||||
fmt.Fprint(hash, call.IdleTimeout, "\x00")
|
||||
fmt.Fprint(hash, call.Memory, "\x00")
|
||||
fmt.Fprint(hash, call.CPUs, "\x00")
|
||||
fmt.Fprint(hash, call.Format, "\x00")
|
||||
hash := shapool.Get().(hash.Hash)
|
||||
hash.Reset()
|
||||
defer shapool.Put(hash)
|
||||
|
||||
// we have to sort these before printing, yay. TODO do better
|
||||
hash.Write(unsafeBytes(call.AppName))
|
||||
hash.Write(unsafeBytes("\x00"))
|
||||
hash.Write(unsafeBytes(call.Path))
|
||||
hash.Write(unsafeBytes("\x00"))
|
||||
hash.Write(unsafeBytes(call.Image))
|
||||
hash.Write(unsafeBytes("\x00"))
|
||||
hash.Write(unsafeBytes(call.Format))
|
||||
hash.Write(unsafeBytes("\x00"))
|
||||
|
||||
// these are all static in size we only need to delimit the whole block of them
|
||||
var byt [8]byte
|
||||
binary.LittleEndian.PutUint32(byt[:4], uint32(call.Timeout))
|
||||
hash.Write(byt[:4])
|
||||
|
||||
binary.LittleEndian.PutUint32(byt[:4], uint32(call.IdleTimeout))
|
||||
hash.Write(byt[:4])
|
||||
|
||||
binary.LittleEndian.PutUint64(byt[:], call.Memory)
|
||||
hash.Write(byt[:])
|
||||
|
||||
binary.LittleEndian.PutUint64(byt[:], uint64(call.CPUs))
|
||||
hash.Write(byt[:])
|
||||
hash.Write(unsafeBytes("\x00"))
|
||||
|
||||
// we have to sort these before printing, yay.
|
||||
// TODO if we had a max size for config const we could avoid this!
|
||||
keys := make([]string, 0, len(call.Config))
|
||||
for k := range call.Config {
|
||||
keys = append(keys, k)
|
||||
i := sort.SearchStrings(keys, k)
|
||||
keys = append(keys, "")
|
||||
copy(keys[i+1:], keys[i:])
|
||||
keys[i] = k
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
fmt.Fprint(hash, k, "\x00", call.Config[k], "\x00")
|
||||
hash.Write(unsafeBytes(k))
|
||||
hash.Write(unsafeBytes("\x00"))
|
||||
hash.Write(unsafeBytes(call.Config[k]))
|
||||
hash.Write(unsafeBytes("\x00"))
|
||||
}
|
||||
|
||||
var buf [sha1.Size]byte
|
||||
return string(hash.Sum(buf[:0]))
|
||||
byts := hash.Sum(buf[:0])
|
||||
return unsafeString(byts)
|
||||
}
|
||||
|
||||
// WARN: this is read only
|
||||
func unsafeBytes(a string) []byte {
|
||||
return *(*[]byte)(unsafe.Pointer(&a))
|
||||
}
|
||||
|
||||
// WARN: this is read only
|
||||
func unsafeString(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@ package agent
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fnproject/fn/api/models"
|
||||
)
|
||||
|
||||
type testSlot struct {
|
||||
@@ -260,3 +263,50 @@ func TestSlotQueueBasic3(t *testing.T) {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSlotKey(b *testing.B) {
|
||||
appName := "myapp"
|
||||
path := "/"
|
||||
image := "fnproject/fn-test-utils"
|
||||
const timeout = 1
|
||||
const idleTimeout = 20
|
||||
const memory = 256
|
||||
CPUs := models.MilliCPUs(1000)
|
||||
method := "GET"
|
||||
url := "http://127.0.0.1:8080/r/" + appName + path
|
||||
payload := "payload"
|
||||
typ := "sync"
|
||||
format := "default"
|
||||
cfg := models.Config{
|
||||
"FN_FORMAT": format,
|
||||
"FN_APP_NAME": appName,
|
||||
"FN_PATH": path,
|
||||
"FN_MEMORY": strconv.Itoa(memory),
|
||||
"FN_CPUS": CPUs.String(),
|
||||
"FN_TYPE": typ,
|
||||
"APP_VAR": "FOO",
|
||||
"ROUTE_VAR": "BAR",
|
||||
}
|
||||
|
||||
cm := &models.Call{
|
||||
Config: cfg,
|
||||
AppName: appName,
|
||||
Path: path,
|
||||
Image: image,
|
||||
Type: typ,
|
||||
Format: format,
|
||||
Timeout: timeout,
|
||||
IdleTimeout: idleTimeout,
|
||||
Memory: memory,
|
||||
CPUs: CPUs,
|
||||
Payload: payload,
|
||||
URL: url,
|
||||
Method: method,
|
||||
}
|
||||
|
||||
call := &call{Call: cm}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = getSlotQueueKey(call)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user