Write an openapi spec for the worker API and use `deepmap/oapi-codegen`
to generate scaffolding for the server-side using the `labstack/echo`
server.
Incidentally, echo by default returns the errors in the same format that
worker API always has:
{ "message": "..." }
The API itself is unchanged to make this change easier to understand. It
will be changed to better suit our needs in future commits.
151 lines
2.9 KiB
Go
151 lines
2.9 KiB
Go
package bytebufferpool
|
|
|
|
import (
|
|
"sort"
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
const (
|
|
minBitSize = 6 // 2**6=64 is a CPU cache line size
|
|
steps = 20
|
|
|
|
minSize = 1 << minBitSize
|
|
maxSize = 1 << (minBitSize + steps - 1)
|
|
|
|
calibrateCallsThreshold = 42000
|
|
maxPercentile = 0.95
|
|
)
|
|
|
|
// Pool represents byte buffer pool.
|
|
//
|
|
// Distinct pools may be used for distinct types of byte buffers.
|
|
// Properly determined byte buffer types with their own pools may help reducing
|
|
// memory waste.
|
|
type Pool struct {
|
|
calls [steps]uint64
|
|
calibrating uint64
|
|
|
|
defaultSize uint64
|
|
maxSize uint64
|
|
|
|
pool sync.Pool
|
|
}
|
|
|
|
var defaultPool Pool
|
|
|
|
// Get returns an empty byte buffer from the pool.
|
|
//
|
|
// Got byte buffer may be returned to the pool via Put call.
|
|
// This reduces the number of memory allocations required for byte buffer
|
|
// management.
|
|
func Get() *ByteBuffer { return defaultPool.Get() }
|
|
|
|
// Get returns new byte buffer with zero length.
|
|
//
|
|
// The byte buffer may be returned to the pool via Put after the use
|
|
// in order to minimize GC overhead.
|
|
func (p *Pool) Get() *ByteBuffer {
|
|
v := p.pool.Get()
|
|
if v != nil {
|
|
return v.(*ByteBuffer)
|
|
}
|
|
return &ByteBuffer{
|
|
B: make([]byte, 0, atomic.LoadUint64(&p.defaultSize)),
|
|
}
|
|
}
|
|
|
|
// Put returns byte buffer to the pool.
|
|
//
|
|
// ByteBuffer.B mustn't be touched after returning it to the pool.
|
|
// Otherwise data races will occur.
|
|
func Put(b *ByteBuffer) { defaultPool.Put(b) }
|
|
|
|
// Put releases byte buffer obtained via Get to the pool.
|
|
//
|
|
// The buffer mustn't be accessed after returning to the pool.
|
|
func (p *Pool) Put(b *ByteBuffer) {
|
|
idx := index(len(b.B))
|
|
|
|
if atomic.AddUint64(&p.calls[idx], 1) > calibrateCallsThreshold {
|
|
p.calibrate()
|
|
}
|
|
|
|
maxSize := int(atomic.LoadUint64(&p.maxSize))
|
|
if maxSize == 0 || cap(b.B) <= maxSize {
|
|
b.Reset()
|
|
p.pool.Put(b)
|
|
}
|
|
}
|
|
|
|
func (p *Pool) calibrate() {
|
|
if !atomic.CompareAndSwapUint64(&p.calibrating, 0, 1) {
|
|
return
|
|
}
|
|
|
|
a := make(callSizes, 0, steps)
|
|
var callsSum uint64
|
|
for i := uint64(0); i < steps; i++ {
|
|
calls := atomic.SwapUint64(&p.calls[i], 0)
|
|
callsSum += calls
|
|
a = append(a, callSize{
|
|
calls: calls,
|
|
size: minSize << i,
|
|
})
|
|
}
|
|
sort.Sort(a)
|
|
|
|
defaultSize := a[0].size
|
|
maxSize := defaultSize
|
|
|
|
maxSum := uint64(float64(callsSum) * maxPercentile)
|
|
callsSum = 0
|
|
for i := 0; i < steps; i++ {
|
|
if callsSum > maxSum {
|
|
break
|
|
}
|
|
callsSum += a[i].calls
|
|
size := a[i].size
|
|
if size > maxSize {
|
|
maxSize = size
|
|
}
|
|
}
|
|
|
|
atomic.StoreUint64(&p.defaultSize, defaultSize)
|
|
atomic.StoreUint64(&p.maxSize, maxSize)
|
|
|
|
atomic.StoreUint64(&p.calibrating, 0)
|
|
}
|
|
|
|
type callSize struct {
|
|
calls uint64
|
|
size uint64
|
|
}
|
|
|
|
type callSizes []callSize
|
|
|
|
func (ci callSizes) Len() int {
|
|
return len(ci)
|
|
}
|
|
|
|
func (ci callSizes) Less(i, j int) bool {
|
|
return ci[i].calls > ci[j].calls
|
|
}
|
|
|
|
func (ci callSizes) Swap(i, j int) {
|
|
ci[i], ci[j] = ci[j], ci[i]
|
|
}
|
|
|
|
func index(n int) int {
|
|
n--
|
|
n >>= minBitSize
|
|
idx := 0
|
|
for n > 0 {
|
|
n >>= 1
|
|
idx++
|
|
}
|
|
if idx >= steps {
|
|
idx = steps - 1
|
|
}
|
|
return idx
|
|
}
|