Add memory optimization improvements for reduced GC pressure (v0.36.16)
Some checks failed
Go / build-and-release (push) Has been cancelled
Some checks failed
Go / build-and-release (push) Has been cancelled
- Add buffer pool (pkg/database/bufpool) with SmallPool (64B) and MediumPool (1KB) for reusing bytes.Buffer instances on hot paths - Fix escape analysis in index types (uint40, letter, word) by using fixed-size arrays instead of make() calls that escape to heap - Add handler concurrency limiter (ORLY_MAX_HANDLERS_PER_CONN, default 100) to prevent unbounded goroutine growth under WebSocket load - Add pre-allocation hints to Uint40s.Union/Intersection/Difference methods - Update compact_event.go, save-event.go, serial_cache.go, and get-indexes-for-event.go to use pooled buffers Files modified: - app/config/config.go: Add MaxHandlersPerConnection config - app/handle-websocket.go: Initialize handler semaphore - app/listener.go: Add semaphore acquire/release in messageProcessor - pkg/database/bufpool/pool.go: New buffer pool package - pkg/database/compact_event.go: Use buffer pool, fix escape analysis - pkg/database/get-indexes-for-event.go: Reuse single buffer for all indexes - pkg/database/indexes/types/letter.go: Fixed array in UnmarshalRead - pkg/database/indexes/types/uint40.go: Fixed arrays, pre-allocation hints - pkg/database/indexes/types/word.go: Fixed array in UnmarshalRead - pkg/database/save-event.go: Use buffer pool for key encoding - pkg/database/serial_cache.go: Use buffer pool for lookups 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -106,6 +106,9 @@ type C struct {
|
|||||||
SerialCachePubkeys int `env:"ORLY_SERIAL_CACHE_PUBKEYS" default:"100000" usage:"max pubkeys to cache for compact event storage (default: 100000, ~3.2MB memory)"`
|
SerialCachePubkeys int `env:"ORLY_SERIAL_CACHE_PUBKEYS" default:"100000" usage:"max pubkeys to cache for compact event storage (default: 100000, ~3.2MB memory)"`
|
||||||
SerialCacheEventIds int `env:"ORLY_SERIAL_CACHE_EVENT_IDS" default:"500000" usage:"max event IDs to cache for compact event storage (default: 500000, ~16MB memory)"`
|
SerialCacheEventIds int `env:"ORLY_SERIAL_CACHE_EVENT_IDS" default:"500000" usage:"max event IDs to cache for compact event storage (default: 500000, ~16MB memory)"`
|
||||||
|
|
||||||
|
// Connection concurrency control
|
||||||
|
MaxHandlersPerConnection int `env:"ORLY_MAX_HANDLERS_PER_CONN" default:"100" usage:"max concurrent message handlers per WebSocket connection (limits goroutine growth under load)"`
|
||||||
|
|
||||||
// Adaptive rate limiting (PID-controlled)
|
// Adaptive rate limiting (PID-controlled)
|
||||||
RateLimitEnabled bool `env:"ORLY_RATE_LIMIT_ENABLED" default:"true" usage:"enable adaptive PID-controlled rate limiting for database operations"`
|
RateLimitEnabled bool `env:"ORLY_RATE_LIMIT_ENABLED" default:"true" usage:"enable adaptive PID-controlled rate limiting for database operations"`
|
||||||
RateLimitTargetMB int `env:"ORLY_RATE_LIMIT_TARGET_MB" default:"0" usage:"target memory limit in MB (0=auto-detect: 66% of available, min 500MB)"`
|
RateLimitTargetMB int `env:"ORLY_RATE_LIMIT_TARGET_MB" default:"0" usage:"target memory limit in MB (0=auto-detect: 66% of available, min 500MB)"`
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ whitelist:
|
|||||||
})
|
})
|
||||||
|
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
// Determine handler semaphore size from config
|
||||||
|
handlerSemSize := s.Config.MaxHandlersPerConnection
|
||||||
|
if handlerSemSize <= 0 {
|
||||||
|
handlerSemSize = 100 // Default if not configured
|
||||||
|
}
|
||||||
|
|
||||||
listener := &Listener{
|
listener := &Listener{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
@@ -98,6 +104,7 @@ whitelist:
|
|||||||
writeDone: make(chan struct{}),
|
writeDone: make(chan struct{}),
|
||||||
messageQueue: make(chan messageRequest, 100), // Buffered channel for message processing
|
messageQueue: make(chan messageRequest, 100), // Buffered channel for message processing
|
||||||
processingDone: make(chan struct{}),
|
processingDone: make(chan struct{}),
|
||||||
|
handlerSem: make(chan struct{}, handlerSemSize), // Limits concurrent handlers
|
||||||
subscriptions: make(map[string]context.CancelFunc),
|
subscriptions: make(map[string]context.CancelFunc),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type Listener struct {
|
|||||||
messageQueue chan messageRequest // Buffered channel for message processing
|
messageQueue chan messageRequest // Buffered channel for message processing
|
||||||
processingDone chan struct{} // Closed when message processor exits
|
processingDone chan struct{} // Closed when message processor exits
|
||||||
handlerWg sync.WaitGroup // Tracks spawned message handler goroutines
|
handlerWg sync.WaitGroup // Tracks spawned message handler goroutines
|
||||||
|
handlerSem chan struct{} // Limits concurrent message handlers per connection
|
||||||
authProcessing sync.RWMutex // Ensures AUTH completes before other messages check authentication
|
authProcessing sync.RWMutex // Ensures AUTH completes before other messages check authentication
|
||||||
// Flow control counters (atomic for concurrent access)
|
// Flow control counters (atomic for concurrent access)
|
||||||
droppedMessages atomic.Int64 // Messages dropped due to full queue
|
droppedMessages atomic.Int64 // Messages dropped due to full queue
|
||||||
@@ -240,9 +241,20 @@ func (l *Listener) messageProcessor() {
|
|||||||
// Not AUTH - unlock immediately and process concurrently
|
// Not AUTH - unlock immediately and process concurrently
|
||||||
// The next message can now be dequeued (possibly another non-AUTH to process concurrently)
|
// The next message can now be dequeued (possibly another non-AUTH to process concurrently)
|
||||||
l.authProcessing.Unlock()
|
l.authProcessing.Unlock()
|
||||||
|
|
||||||
|
// Acquire semaphore to limit concurrent handlers (blocking with context awareness)
|
||||||
|
select {
|
||||||
|
case l.handlerSem <- struct{}{}:
|
||||||
|
// Semaphore acquired
|
||||||
|
case <-l.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
l.handlerWg.Add(1)
|
l.handlerWg.Add(1)
|
||||||
go func(data []byte, remote string) {
|
go func(data []byte, remote string) {
|
||||||
defer l.handlerWg.Done()
|
defer func() {
|
||||||
|
<-l.handlerSem // Release semaphore
|
||||||
|
l.handlerWg.Done()
|
||||||
|
}()
|
||||||
l.HandleMessage(data, remote)
|
l.HandleMessage(data, remote)
|
||||||
}(req.data, req.remote)
|
}(req.data, req.remote)
|
||||||
}
|
}
|
||||||
|
|||||||
94
pkg/database/bufpool/pool.go
Normal file
94
pkg/database/bufpool/pool.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
//go:build !(js && wasm)
|
||||||
|
|
||||||
|
// Package bufpool provides buffer pools for reducing GC pressure in hot paths.
|
||||||
|
//
|
||||||
|
// Two pool sizes are provided:
|
||||||
|
// - SmallPool (64 bytes): For index keys, serial encoding, short buffers
|
||||||
|
// - MediumPool (1KB): For event encoding, larger serialization buffers
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// buf := bufpool.GetSmall()
|
||||||
|
// defer bufpool.PutSmall(buf)
|
||||||
|
// // Use buf...
|
||||||
|
// // IMPORTANT: Copy buf.Bytes() before Put if data is needed after
|
||||||
|
package bufpool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SmallBufferSize for index keys (8-64 bytes typical)
|
||||||
|
SmallBufferSize = 64
|
||||||
|
|
||||||
|
// MediumBufferSize for event encoding (300-1000 bytes typical)
|
||||||
|
MediumBufferSize = 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// smallPool for index keys and short encodings
|
||||||
|
smallPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return bytes.NewBuffer(make([]byte, 0, SmallBufferSize))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// mediumPool for event encoding and larger buffers
|
||||||
|
mediumPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return bytes.NewBuffer(make([]byte, 0, MediumBufferSize))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetSmall returns a small buffer (64 bytes) from the pool.
|
||||||
|
// Call PutSmall when done to return it to the pool.
|
||||||
|
//
|
||||||
|
// WARNING: Copy buf.Bytes() before calling PutSmall if the data
|
||||||
|
// is needed after the buffer is returned to the pool.
|
||||||
|
func GetSmall() *bytes.Buffer {
|
||||||
|
return smallPool.Get().(*bytes.Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutSmall returns a small buffer to the pool.
|
||||||
|
// The buffer is reset before being returned.
|
||||||
|
func PutSmall(buf *bytes.Buffer) {
|
||||||
|
if buf == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf.Reset()
|
||||||
|
smallPool.Put(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMedium returns a medium buffer (1KB) from the pool.
|
||||||
|
// Call PutMedium when done to return it to the pool.
|
||||||
|
//
|
||||||
|
// WARNING: Copy buf.Bytes() before calling PutMedium if the data
|
||||||
|
// is needed after the buffer is returned to the pool.
|
||||||
|
func GetMedium() *bytes.Buffer {
|
||||||
|
return mediumPool.Get().(*bytes.Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutMedium returns a medium buffer to the pool.
|
||||||
|
// The buffer is reset before being returned.
|
||||||
|
func PutMedium(buf *bytes.Buffer) {
|
||||||
|
if buf == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf.Reset()
|
||||||
|
mediumPool.Put(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyBytes copies the buffer contents to a new slice.
|
||||||
|
// Use this before returning the buffer to the pool if the
|
||||||
|
// data needs to persist.
|
||||||
|
func CopyBytes(buf *bytes.Buffer) []byte {
|
||||||
|
if buf == nil || buf.Len() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]byte, buf.Len())
|
||||||
|
copy(result, buf.Bytes())
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"git.mleku.dev/mleku/nostr/encoders/tag"
|
"git.mleku.dev/mleku/nostr/encoders/tag"
|
||||||
"git.mleku.dev/mleku/nostr/encoders/varint"
|
"git.mleku.dev/mleku/nostr/encoders/varint"
|
||||||
"lol.mleku.dev/chk"
|
"lol.mleku.dev/chk"
|
||||||
|
"next.orly.dev/pkg/database/bufpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CompactEventFormat defines the binary format for compact event storage.
|
// CompactEventFormat defines the binary format for compact event storage.
|
||||||
@@ -72,7 +73,8 @@ type SerialResolver interface {
|
|||||||
// MarshalCompactEvent encodes an event using compact serial references.
|
// MarshalCompactEvent encodes an event using compact serial references.
|
||||||
// The resolver is used to look up/create serial mappings for pubkeys and event IDs.
|
// The resolver is used to look up/create serial mappings for pubkeys and event IDs.
|
||||||
func MarshalCompactEvent(ev *event.E, resolver SerialResolver) (data []byte, err error) {
|
func MarshalCompactEvent(ev *event.E, resolver SerialResolver) (data []byte, err error) {
|
||||||
buf := new(bytes.Buffer)
|
buf := bufpool.GetMedium()
|
||||||
|
defer bufpool.PutMedium(buf)
|
||||||
|
|
||||||
// Version byte
|
// Version byte
|
||||||
buf.WriteByte(CompactFormatVersion)
|
buf.WriteByte(CompactFormatVersion)
|
||||||
@@ -109,7 +111,8 @@ func MarshalCompactEvent(ev *event.E, resolver SerialResolver) (data []byte, err
|
|||||||
// Signature (64 bytes)
|
// Signature (64 bytes)
|
||||||
buf.Write(ev.Sig)
|
buf.Write(ev.Sig)
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
// Copy bytes before returning buffer to pool
|
||||||
|
return bufpool.CopyBytes(buf), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodeCompactTag encodes a single tag with serial references for e/p tags.
|
// encodeCompactTag encodes a single tag with serial references for e/p tags.
|
||||||
@@ -221,8 +224,8 @@ func writeUint40(w io.Writer, value uint64) {
|
|||||||
|
|
||||||
// readUint40 reads a 5-byte big-endian unsigned integer.
|
// readUint40 reads a 5-byte big-endian unsigned integer.
|
||||||
func readUint40(r io.Reader) (value uint64, err error) {
|
func readUint40(r io.Reader) (value uint64, err error) {
|
||||||
buf := make([]byte, 5)
|
var buf [5]byte // Fixed array avoids heap escape
|
||||||
if _, err = io.ReadFull(r, buf); err != nil {
|
if _, err = io.ReadFull(r, buf[:]); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
value = (uint64(buf[0]) << 32) |
|
value = (uint64(buf[0]) << 32) |
|
||||||
@@ -331,9 +334,9 @@ func decodeCompactTag(r io.Reader, resolver SerialResolver) (t *tag.T, err error
|
|||||||
|
|
||||||
// decodeTagElement decodes a single tag element from compact format.
|
// decodeTagElement decodes a single tag element from compact format.
|
||||||
func decodeTagElement(r io.Reader, resolver SerialResolver) (elem []byte, err error) {
|
func decodeTagElement(r io.Reader, resolver SerialResolver) (elem []byte, err error) {
|
||||||
// Read type flag
|
// Read type flag (fixed array avoids heap escape)
|
||||||
typeBuf := make([]byte, 1)
|
var typeBuf [1]byte
|
||||||
if _, err = io.ReadFull(r, typeBuf); err != nil {
|
if _, err = io.ReadFull(r, typeBuf[:]); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
typeFlag := typeBuf[0]
|
typeFlag := typeBuf[0]
|
||||||
|
|||||||
@@ -4,21 +4,22 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
|
|
||||||
"lol.mleku.dev/chk"
|
"lol.mleku.dev/chk"
|
||||||
|
"next.orly.dev/pkg/database/bufpool"
|
||||||
"next.orly.dev/pkg/database/indexes"
|
"next.orly.dev/pkg/database/indexes"
|
||||||
. "next.orly.dev/pkg/database/indexes/types"
|
. "next.orly.dev/pkg/database/indexes/types"
|
||||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||||
)
|
)
|
||||||
|
|
||||||
// appendIndexBytes marshals an index to a byte slice and appends it to the idxs slice
|
// appendIndexBytes marshals an index to a byte slice and appends it to the idxs slice.
|
||||||
func appendIndexBytes(idxs *[][]byte, idx *indexes.T) (err error) {
|
// It reuses the provided buffer (resetting it first) to avoid allocations.
|
||||||
buf := new(bytes.Buffer)
|
func appendIndexBytes(idxs *[][]byte, idx *indexes.T, buf *bytes.Buffer) (err error) {
|
||||||
|
buf.Reset()
|
||||||
// Marshal the index to the buffer
|
// Marshal the index to the buffer
|
||||||
if err = idx.MarshalWrite(buf); chk.E(err) {
|
if err = idx.MarshalWrite(buf); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Copy the buffer's bytes to a new byte slice
|
// Copy the buffer's bytes to a new byte slice and append
|
||||||
// Append the byte slice to the idxs slice
|
*idxs = append(*idxs, bufpool.CopyBytes(buf))
|
||||||
*idxs = append(*idxs, buf.Bytes())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +29,10 @@ func appendIndexBytes(idxs *[][]byte, idx *indexes.T) (err error) {
|
|||||||
func GetIndexesForEvent(ev *event.E, serial uint64) (
|
func GetIndexesForEvent(ev *event.E, serial uint64) (
|
||||||
idxs [][]byte, err error,
|
idxs [][]byte, err error,
|
||||||
) {
|
) {
|
||||||
|
// Get a reusable buffer for all index serializations
|
||||||
|
buf := bufpool.GetSmall()
|
||||||
|
defer bufpool.PutSmall(buf)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if chk.E(err) {
|
if chk.E(err) {
|
||||||
idxs = nil
|
idxs = nil
|
||||||
@@ -44,7 +49,7 @@ func GetIndexesForEvent(ev *event.E, serial uint64) (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
idIndex := indexes.IdEnc(idHash, ser)
|
idIndex := indexes.IdEnc(idHash, ser)
|
||||||
if err = appendIndexBytes(&idxs, idIndex); chk.E(err) {
|
if err = appendIndexBytes(&idxs, idIndex, buf); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// FullIdPubkey index
|
// FullIdPubkey index
|
||||||
@@ -61,17 +66,17 @@ func GetIndexesForEvent(ev *event.E, serial uint64) (
|
|||||||
idPubkeyIndex := indexes.FullIdPubkeyEnc(
|
idPubkeyIndex := indexes.FullIdPubkeyEnc(
|
||||||
ser, fullID, pubHash, createdAt,
|
ser, fullID, pubHash, createdAt,
|
||||||
)
|
)
|
||||||
if err = appendIndexBytes(&idxs, idPubkeyIndex); chk.E(err) {
|
if err = appendIndexBytes(&idxs, idPubkeyIndex, buf); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// CreatedAt index
|
// CreatedAt index
|
||||||
createdAtIndex := indexes.CreatedAtEnc(createdAt, ser)
|
createdAtIndex := indexes.CreatedAtEnc(createdAt, ser)
|
||||||
if err = appendIndexBytes(&idxs, createdAtIndex); chk.E(err) {
|
if err = appendIndexBytes(&idxs, createdAtIndex, buf); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// PubkeyCreatedAt index
|
// PubkeyCreatedAt index
|
||||||
pubkeyIndex := indexes.PubkeyEnc(pubHash, createdAt, ser)
|
pubkeyIndex := indexes.PubkeyEnc(pubHash, createdAt, ser)
|
||||||
if err = appendIndexBytes(&idxs, pubkeyIndex); chk.E(err) {
|
if err = appendIndexBytes(&idxs, pubkeyIndex, buf); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Process tags for tag-related indexes
|
// Process tags for tag-related indexes
|
||||||
@@ -101,7 +106,7 @@ func GetIndexesForEvent(ev *event.E, serial uint64) (
|
|||||||
key, valueHash, pubHash, createdAt, ser,
|
key, valueHash, pubHash, createdAt, ser,
|
||||||
)
|
)
|
||||||
if err = appendIndexBytes(
|
if err = appendIndexBytes(
|
||||||
&idxs, pubkeyTagIndex,
|
&idxs, pubkeyTagIndex, buf,
|
||||||
); chk.E(err) {
|
); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -110,7 +115,7 @@ func GetIndexesForEvent(ev *event.E, serial uint64) (
|
|||||||
key, valueHash, createdAt, ser,
|
key, valueHash, createdAt, ser,
|
||||||
)
|
)
|
||||||
if err = appendIndexBytes(
|
if err = appendIndexBytes(
|
||||||
&idxs, tagIndex,
|
&idxs, tagIndex, buf,
|
||||||
); chk.E(err) {
|
); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -122,7 +127,7 @@ func GetIndexesForEvent(ev *event.E, serial uint64) (
|
|||||||
key, valueHash, kind, createdAt, ser,
|
key, valueHash, kind, createdAt, ser,
|
||||||
)
|
)
|
||||||
if err = appendIndexBytes(
|
if err = appendIndexBytes(
|
||||||
&idxs, kindTagIndex,
|
&idxs, kindTagIndex, buf,
|
||||||
); chk.E(err) {
|
); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -131,7 +136,7 @@ func GetIndexesForEvent(ev *event.E, serial uint64) (
|
|||||||
key, valueHash, kind, pubHash, createdAt, ser,
|
key, valueHash, kind, pubHash, createdAt, ser,
|
||||||
)
|
)
|
||||||
if err = appendIndexBytes(
|
if err = appendIndexBytes(
|
||||||
&idxs, kindPubkeyTagIndex,
|
&idxs, kindPubkeyTagIndex, buf,
|
||||||
); chk.E(err) {
|
); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -142,7 +147,7 @@ func GetIndexesForEvent(ev *event.E, serial uint64) (
|
|||||||
kind.Set(uint16(ev.Kind))
|
kind.Set(uint16(ev.Kind))
|
||||||
// Kind index
|
// Kind index
|
||||||
kindIndex := indexes.KindEnc(kind, createdAt, ser)
|
kindIndex := indexes.KindEnc(kind, createdAt, ser)
|
||||||
if err = appendIndexBytes(&idxs, kindIndex); chk.E(err) {
|
if err = appendIndexBytes(&idxs, kindIndex, buf); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// KindPubkey index
|
// KindPubkey index
|
||||||
@@ -150,7 +155,7 @@ func GetIndexesForEvent(ev *event.E, serial uint64) (
|
|||||||
kindPubkeyIndex := indexes.KindPubkeyEnc(
|
kindPubkeyIndex := indexes.KindPubkeyEnc(
|
||||||
kind, pubHash, createdAt, ser,
|
kind, pubHash, createdAt, ser,
|
||||||
)
|
)
|
||||||
if err = appendIndexBytes(&idxs, kindPubkeyIndex); chk.E(err) {
|
if err = appendIndexBytes(&idxs, kindPubkeyIndex, buf); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +165,7 @@ func GetIndexesForEvent(ev *event.E, serial uint64) (
|
|||||||
w := new(Word)
|
w := new(Word)
|
||||||
w.FromWord(h) // 8-byte truncated hash
|
w.FromWord(h) // 8-byte truncated hash
|
||||||
wIdx := indexes.WordEnc(w, ser)
|
wIdx := indexes.WordEnc(w, ser)
|
||||||
if err = appendIndexBytes(&idxs, wIdx); chk.E(err) {
|
if err = appendIndexBytes(&idxs, wIdx, buf); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,7 +181,7 @@ func GetIndexesForEvent(ev *event.E, serial uint64) (
|
|||||||
w := new(Word)
|
w := new(Word)
|
||||||
w.FromWord(h)
|
w.FromWord(h)
|
||||||
wIdx := indexes.WordEnc(w, ser)
|
wIdx := indexes.WordEnc(w, ser)
|
||||||
if err = appendIndexBytes(&idxs, wIdx); chk.E(err) {
|
if err = appendIndexBytes(&idxs, wIdx, buf); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ func (p *Letter) MarshalWrite(w io.Writer) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Letter) UnmarshalRead(r io.Reader) (err error) {
|
func (p *Letter) UnmarshalRead(r io.Reader) (err error) {
|
||||||
val := make([]byte, 1)
|
var val [1]byte // Fixed array avoids heap escape
|
||||||
if _, err = r.Read(val); chk.E(err) {
|
if _, err = r.Read(val[:]); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.val = val[0]
|
p.val = val[0]
|
||||||
|
|||||||
@@ -46,23 +46,23 @@ func (c *Uint40) MarshalWrite(w io.Writer) (err error) {
|
|||||||
if c.value > MaxUint40 {
|
if c.value > MaxUint40 {
|
||||||
return errors.New("value exceeds 40-bit range")
|
return errors.New("value exceeds 40-bit range")
|
||||||
}
|
}
|
||||||
// Buffer for the 5 bytes
|
// Fixed array avoids heap escape
|
||||||
buf := make([]byte, 5)
|
var buf [5]byte
|
||||||
// Write the upper 5 bytes (ignoring the most significant 3 bytes of uint64)
|
// Write the upper 5 bytes (ignoring the most significant 3 bytes of uint64)
|
||||||
buf[0] = byte((c.value >> 32) & 0xFF) // Most significant byte
|
buf[0] = byte((c.value >> 32) & 0xFF) // Most significant byte
|
||||||
buf[1] = byte((c.value >> 24) & 0xFF)
|
buf[1] = byte((c.value >> 24) & 0xFF)
|
||||||
buf[2] = byte((c.value >> 16) & 0xFF)
|
buf[2] = byte((c.value >> 16) & 0xFF)
|
||||||
buf[3] = byte((c.value >> 8) & 0xFF)
|
buf[3] = byte((c.value >> 8) & 0xFF)
|
||||||
buf[4] = byte(c.value & 0xFF) // Least significant byte
|
buf[4] = byte(c.value & 0xFF) // Least significant byte
|
||||||
_, err = w.Write(buf)
|
_, err = w.Write(buf[:])
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalRead reads 5 bytes from the provided reader and decodes it into a 40-bit unsigned integer.
|
// UnmarshalRead reads 5 bytes from the provided reader and decodes it into a 40-bit unsigned integer.
|
||||||
func (c *Uint40) UnmarshalRead(r io.Reader) (err error) {
|
func (c *Uint40) UnmarshalRead(r io.Reader) (err error) {
|
||||||
// Buffer for the 5 bytes
|
// Fixed array avoids heap escape
|
||||||
buf := make([]byte, 5)
|
var buf [5]byte
|
||||||
_, err = r.Read(buf)
|
_, err = r.Read(buf[:])
|
||||||
if chk.E(err) {
|
if chk.E(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -81,8 +81,9 @@ type Uint40s []*Uint40
|
|||||||
// Union computes the union of the current Uint40s slice with another Uint40s slice. The result
|
// Union computes the union of the current Uint40s slice with another Uint40s slice. The result
|
||||||
// contains all unique elements from both slices.
|
// contains all unique elements from both slices.
|
||||||
func (s Uint40s) Union(other Uint40s) Uint40s {
|
func (s Uint40s) Union(other Uint40s) Uint40s {
|
||||||
valueMap := make(map[uint64]bool)
|
totalCap := len(s) + len(other)
|
||||||
var result Uint40s
|
valueMap := make(map[uint64]bool, totalCap)
|
||||||
|
result := make(Uint40s, 0, totalCap) // Pre-allocate for worst case
|
||||||
|
|
||||||
// Add elements from the current Uint40s slice to the result
|
// Add elements from the current Uint40s slice to the result
|
||||||
for _, item := range s {
|
for _, item := range s {
|
||||||
@@ -108,8 +109,13 @@ func (s Uint40s) Union(other Uint40s) Uint40s {
|
|||||||
// Intersection computes the intersection of the current Uint40s slice with another Uint40s
|
// Intersection computes the intersection of the current Uint40s slice with another Uint40s
|
||||||
// slice. The result contains only the elements that exist in both slices.
|
// slice. The result contains only the elements that exist in both slices.
|
||||||
func (s Uint40s) Intersection(other Uint40s) Uint40s {
|
func (s Uint40s) Intersection(other Uint40s) Uint40s {
|
||||||
valueMap := make(map[uint64]bool)
|
// Result can be at most the size of the smaller slice
|
||||||
var result Uint40s
|
smallerLen := len(s)
|
||||||
|
if len(other) < smallerLen {
|
||||||
|
smallerLen = len(other)
|
||||||
|
}
|
||||||
|
valueMap := make(map[uint64]bool, len(other))
|
||||||
|
result := make(Uint40s, 0, smallerLen) // Pre-allocate for worst case
|
||||||
|
|
||||||
// Add all elements from the other Uint40s slice to the map
|
// Add all elements from the other Uint40s slice to the map
|
||||||
for _, item := range other {
|
for _, item := range other {
|
||||||
@@ -131,8 +137,8 @@ func (s Uint40s) Intersection(other Uint40s) Uint40s {
|
|||||||
// The result contains only the elements that are in the current slice but not in the other
|
// The result contains only the elements that are in the current slice but not in the other
|
||||||
// slice.
|
// slice.
|
||||||
func (s Uint40s) Difference(other Uint40s) Uint40s {
|
func (s Uint40s) Difference(other Uint40s) Uint40s {
|
||||||
valueMap := make(map[uint64]bool)
|
valueMap := make(map[uint64]bool, len(other))
|
||||||
var result Uint40s
|
result := make(Uint40s, 0, len(s)) // Pre-allocate for worst case (no overlap)
|
||||||
|
|
||||||
// Mark all elements in the other Uint40s slice
|
// Mark all elements in the other Uint40s slice
|
||||||
for _, item := range other {
|
for _, item := range other {
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ func (w *Word) MarshalWrite(wr io.Writer) (err error) {
|
|||||||
// UnmarshalRead reads the word from the reader, stopping at the zero-byte marker
|
// UnmarshalRead reads the word from the reader, stopping at the zero-byte marker
|
||||||
func (w *Word) UnmarshalRead(r io.Reader) error {
|
func (w *Word) UnmarshalRead(r io.Reader) error {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
tmp := make([]byte, 1)
|
var tmp [1]byte // Fixed array avoids heap escape
|
||||||
foundEndMarker := false
|
foundEndMarker := false
|
||||||
|
|
||||||
// Read bytes until the zero byte is encountered
|
// Read bytes until the zero byte is encountered
|
||||||
for {
|
for {
|
||||||
n, err := r.Read(tmp)
|
n, err := r.Read(tmp[:])
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
if tmp[0] == 0x00 { // Stop on encountering the zero-byte marker
|
if tmp[0] == 0x00 { // Stop on encountering the zero-byte marker
|
||||||
foundEndMarker = true
|
foundEndMarker = true
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -12,6 +11,7 @@ import (
|
|||||||
"github.com/dgraph-io/badger/v4"
|
"github.com/dgraph-io/badger/v4"
|
||||||
"lol.mleku.dev/chk"
|
"lol.mleku.dev/chk"
|
||||||
"lol.mleku.dev/log"
|
"lol.mleku.dev/log"
|
||||||
|
"next.orly.dev/pkg/database/bufpool"
|
||||||
"next.orly.dev/pkg/database/indexes"
|
"next.orly.dev/pkg/database/indexes"
|
||||||
"next.orly.dev/pkg/database/indexes/types"
|
"next.orly.dev/pkg/database/indexes/types"
|
||||||
"next.orly.dev/pkg/mode"
|
"next.orly.dev/pkg/mode"
|
||||||
@@ -277,14 +277,15 @@ func (d *D) SaveEvent(c context.Context, ev *event.E) (
|
|||||||
|
|
||||||
// Calculate legacy size for comparison (for metrics tracking)
|
// Calculate legacy size for comparison (for metrics tracking)
|
||||||
// We marshal to get accurate size comparison
|
// We marshal to get accurate size comparison
|
||||||
legacyBuf := new(bytes.Buffer)
|
legacyBuf := bufpool.GetMedium()
|
||||||
|
defer bufpool.PutMedium(legacyBuf)
|
||||||
ev.MarshalBinary(legacyBuf)
|
ev.MarshalBinary(legacyBuf)
|
||||||
legacySize := legacyBuf.Len()
|
legacySize := legacyBuf.Len()
|
||||||
|
|
||||||
if compactErr != nil {
|
if compactErr != nil {
|
||||||
// Fall back to legacy format if compact encoding fails
|
// Fall back to legacy format if compact encoding fails
|
||||||
log.W.F("SaveEvent: compact encoding failed, using legacy format: %v", compactErr)
|
log.W.F("SaveEvent: compact encoding failed, using legacy format: %v", compactErr)
|
||||||
compactData = legacyBuf.Bytes()
|
compactData = bufpool.CopyBytes(legacyBuf)
|
||||||
} else {
|
} else {
|
||||||
// Track storage savings
|
// Track storage savings
|
||||||
TrackCompactSaving(legacySize, len(compactData))
|
TrackCompactSaving(legacySize, len(compactData))
|
||||||
@@ -322,13 +323,16 @@ func (d *D) SaveEvent(c context.Context, ev *event.E) (
|
|||||||
// Format: cmp|serial|compact_event_data
|
// Format: cmp|serial|compact_event_data
|
||||||
// This is the only storage format - legacy evt/sev/aev/rev prefixes
|
// This is the only storage format - legacy evt/sev/aev/rev prefixes
|
||||||
// are handled by migration and no longer written for new events
|
// are handled by migration and no longer written for new events
|
||||||
cmpKeyBuf := new(bytes.Buffer)
|
cmpKeyBuf := bufpool.GetSmall()
|
||||||
if err = indexes.CompactEventEnc(ser).MarshalWrite(cmpKeyBuf); chk.E(err) {
|
if err = indexes.CompactEventEnc(ser).MarshalWrite(cmpKeyBuf); chk.E(err) {
|
||||||
|
bufpool.PutSmall(cmpKeyBuf)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = txn.Set(cmpKeyBuf.Bytes(), compactData); chk.E(err) {
|
if err = txn.Set(bufpool.CopyBytes(cmpKeyBuf), compactData); chk.E(err) {
|
||||||
|
bufpool.PutSmall(cmpKeyBuf)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
bufpool.PutSmall(cmpKeyBuf)
|
||||||
|
|
||||||
// Create graph edges between event and all related pubkeys
|
// Create graph edges between event and all related pubkeys
|
||||||
// This creates bidirectional edges: event->pubkey and pubkey->event
|
// This creates bidirectional edges: event->pubkey and pubkey->event
|
||||||
@@ -336,6 +340,10 @@ func (d *D) SaveEvent(c context.Context, ev *event.E) (
|
|||||||
eventKind := new(types.Uint16)
|
eventKind := new(types.Uint16)
|
||||||
eventKind.Set(ev.Kind)
|
eventKind.Set(ev.Kind)
|
||||||
|
|
||||||
|
// Reuse a single buffer for graph edge keys (reset between uses)
|
||||||
|
graphKeyBuf := bufpool.GetSmall()
|
||||||
|
defer bufpool.PutSmall(graphKeyBuf)
|
||||||
|
|
||||||
for _, pkInfo := range pubkeysForGraph {
|
for _, pkInfo := range pubkeysForGraph {
|
||||||
// Determine direction for forward edge (event -> pubkey perspective)
|
// Determine direction for forward edge (event -> pubkey perspective)
|
||||||
directionForward := new(types.Letter)
|
directionForward := new(types.Letter)
|
||||||
@@ -353,23 +361,20 @@ func (d *D) SaveEvent(c context.Context, ev *event.E) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create event -> pubkey edge (with kind and direction)
|
// Create event -> pubkey edge (with kind and direction)
|
||||||
epgKeyBuf := new(bytes.Buffer)
|
graphKeyBuf.Reset()
|
||||||
if err = indexes.EventPubkeyGraphEnc(ser, pkInfo.serial, eventKind, directionForward).MarshalWrite(epgKeyBuf); chk.E(err) {
|
if err = indexes.EventPubkeyGraphEnc(ser, pkInfo.serial, eventKind, directionForward).MarshalWrite(graphKeyBuf); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Make a copy of the key bytes to avoid buffer reuse issues in txn
|
if err = txn.Set(bufpool.CopyBytes(graphKeyBuf), nil); chk.E(err) {
|
||||||
epgKey := make([]byte, epgKeyBuf.Len())
|
|
||||||
copy(epgKey, epgKeyBuf.Bytes())
|
|
||||||
if err = txn.Set(epgKey, nil); chk.E(err) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create pubkey -> event edge (reverse, with kind and direction for filtering)
|
// Create pubkey -> event edge (reverse, with kind and direction for filtering)
|
||||||
pegKeyBuf := new(bytes.Buffer)
|
graphKeyBuf.Reset()
|
||||||
if err = indexes.PubkeyEventGraphEnc(pkInfo.serial, eventKind, directionReverse, ser).MarshalWrite(pegKeyBuf); chk.E(err) {
|
if err = indexes.PubkeyEventGraphEnc(pkInfo.serial, eventKind, directionReverse, ser).MarshalWrite(graphKeyBuf); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = txn.Set(pegKeyBuf.Bytes(), nil); chk.E(err) {
|
if err = txn.Set(bufpool.CopyBytes(graphKeyBuf), nil); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,25 +402,22 @@ func (d *D) SaveEvent(c context.Context, ev *event.E) (
|
|||||||
// Create forward edge: source event -> target event (outbound e-tag)
|
// Create forward edge: source event -> target event (outbound e-tag)
|
||||||
directionOut := new(types.Letter)
|
directionOut := new(types.Letter)
|
||||||
directionOut.Set(types.EdgeDirectionETagOut)
|
directionOut.Set(types.EdgeDirectionETagOut)
|
||||||
eegKeyBuf := new(bytes.Buffer)
|
graphKeyBuf.Reset()
|
||||||
if err = indexes.EventEventGraphEnc(ser, targetSerial, eventKind, directionOut).MarshalWrite(eegKeyBuf); chk.E(err) {
|
if err = indexes.EventEventGraphEnc(ser, targetSerial, eventKind, directionOut).MarshalWrite(graphKeyBuf); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Make a copy of the key bytes to avoid buffer reuse issues in txn
|
if err = txn.Set(bufpool.CopyBytes(graphKeyBuf), nil); chk.E(err) {
|
||||||
eegKey := make([]byte, eegKeyBuf.Len())
|
|
||||||
copy(eegKey, eegKeyBuf.Bytes())
|
|
||||||
if err = txn.Set(eegKey, nil); chk.E(err) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create reverse edge: target event -> source event (inbound e-tag)
|
// Create reverse edge: target event -> source event (inbound e-tag)
|
||||||
directionIn := new(types.Letter)
|
directionIn := new(types.Letter)
|
||||||
directionIn.Set(types.EdgeDirectionETagIn)
|
directionIn.Set(types.EdgeDirectionETagIn)
|
||||||
geeKeyBuf := new(bytes.Buffer)
|
graphKeyBuf.Reset()
|
||||||
if err = indexes.GraphEventEventEnc(targetSerial, eventKind, directionIn, ser).MarshalWrite(geeKeyBuf); chk.E(err) {
|
if err = indexes.GraphEventEventEnc(targetSerial, eventKind, directionIn, ser).MarshalWrite(graphKeyBuf); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = txn.Set(geeKeyBuf.Bytes(), nil); chk.E(err) {
|
if err = txn.Set(bufpool.CopyBytes(graphKeyBuf), nil); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v4"
|
"github.com/dgraph-io/badger/v4"
|
||||||
"lol.mleku.dev/chk"
|
"lol.mleku.dev/chk"
|
||||||
|
"next.orly.dev/pkg/database/bufpool"
|
||||||
"next.orly.dev/pkg/database/indexes"
|
"next.orly.dev/pkg/database/indexes"
|
||||||
"next.orly.dev/pkg/database/indexes/types"
|
"next.orly.dev/pkg/database/indexes/types"
|
||||||
)
|
)
|
||||||
@@ -281,7 +281,8 @@ func (r *DatabaseSerialResolver) GetEventIdBySerial(serial uint64) (eventId []by
|
|||||||
// GetEventIdBySerial looks up an event ID by its serial number.
|
// GetEventIdBySerial looks up an event ID by its serial number.
|
||||||
// Uses the SerialEventId index (sei prefix).
|
// Uses the SerialEventId index (sei prefix).
|
||||||
func (d *D) GetEventIdBySerial(ser *types.Uint40) (eventId []byte, err error) {
|
func (d *D) GetEventIdBySerial(ser *types.Uint40) (eventId []byte, err error) {
|
||||||
keyBuf := new(bytes.Buffer)
|
keyBuf := bufpool.GetSmall()
|
||||||
|
defer bufpool.PutSmall(keyBuf)
|
||||||
if err = indexes.SerialEventIdEnc(ser).MarshalWrite(keyBuf); chk.E(err) {
|
if err = indexes.SerialEventIdEnc(ser).MarshalWrite(keyBuf); chk.E(err) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -318,12 +319,13 @@ func (d *D) StoreEventIdSerial(txn *badger.Txn, serial uint64, eventId []byte) e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
keyBuf := new(bytes.Buffer)
|
keyBuf := bufpool.GetSmall()
|
||||||
|
defer bufpool.PutSmall(keyBuf)
|
||||||
if err := indexes.SerialEventIdEnc(ser).MarshalWrite(keyBuf); chk.E(err) {
|
if err := indexes.SerialEventIdEnc(ser).MarshalWrite(keyBuf); chk.E(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return txn.Set(keyBuf.Bytes(), eventId)
|
return txn.Set(bufpool.CopyBytes(keyBuf), eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SerialCacheStats holds statistics about the serial cache.
|
// SerialCacheStats holds statistics about the serial cache.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v0.36.15
|
v0.36.16
|
||||||
|
|||||||
Reference in New Issue
Block a user