pool

package
v1.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: MIT Imports: 1 Imported by: 0

Documentation

Overview

Package pool provides object pooling for NornicDB to reduce allocations.

Object pooling reuses allocated objects instead of creating new ones, reducing GC pressure and improving throughput for high-frequency operations.

Pooled objects: - Query results (rows, columns) - Node/Edge slices - String builders - Byte buffers

Usage:

// Get a slice from pool
rows := pool.GetRowSlice()
defer pool.PutRowSlice(rows)

// Use the slice...
rows = append(rows, newRow)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Configure

func Configure(config PoolConfig)

Configure sets global pool configuration.

This function should be called once during application initialization, before any pooled objects are allocated. Calling it multiple times will reinitialize all pools, which may cause temporary allocation spikes.

Parameters:

  • config: Pool configuration with Enabled and MaxSize settings

Example 1 - Production Setup:

func main() {
	// Configure pooling early
	pool.Configure(pool.PoolConfig{
		Enabled: true,
		MaxSize: 1000,
	})

	// Now start your application
	server.Start()
}

Example 2 - Disable for Debugging:

// Disable pooling to detect memory leaks
pool.Configure(pool.PoolConfig{
	Enabled: false, // All Get/Put operations become no-ops
})

Example 3 - Memory-Constrained Environment:

// Reduce pool size for low-memory systems
pool.Configure(pool.PoolConfig{
	Enabled: true,
	MaxSize: 100, // Keep fewer objects
})

Performance Impact:

  • Enabling pooling: 50-70% reduction in allocations
  • Disabling pooling: Easier debugging, higher GC pressure
  • MaxSize too low: More allocations (pool fills up)
  • MaxSize too high: More memory usage

Thread Safety:

Not thread-safe. Call only during initialization.

func GetByteBuffer

func GetByteBuffer() []byte

GetByteBuffer returns a byte buffer from the pool.

The returned buffer has length 0 but pre-allocated capacity (typically 1KB). Use this for temporary byte operations like encoding, serialization, or building binary data. Always call PutByteBuffer when done.

Returns:

  • Empty byte slice with capacity 1024

Example 1 - JSON Encoding:

func encodeJSON(data interface{}) ([]byte, error) {
	buf := pool.GetByteBuffer()
	defer pool.PutByteBuffer(buf)

	encoder := json.NewEncoder(bytes.NewBuffer(buf))
	if err := encoder.Encode(data); err != nil {
		return nil, err
	}

	// Return a copy (original goes back to pool)
	result := make([]byte, len(buf))
	copy(result, buf)
	return result, nil
}

Example 2 - Binary Protocol:

func encodeMessage(msgType byte, payload []byte) []byte {
	buf := pool.GetByteBuffer()
	defer pool.PutByteBuffer(buf)

	// Write header
	buf = append(buf, msgType)
	buf = binary.BigEndian.AppendUint32(buf, uint32(len(payload)))

	// Write payload
	buf = append(buf, payload...)

	return append([]byte(nil), buf...) // Return copy
}

Example 3 - Hash Computation:

func computeHash(data string) []byte {
	buf := pool.GetByteBuffer()
	defer pool.PutByteBuffer(buf)

	h := sha256.New()
	h.Write([]byte(data))
	return h.Sum(buf[:0]) // Use buf as backing array
}

Performance:

  • Eliminates allocation for temporary buffers
  • Pre-allocated 1KB handles most use cases
  • Grows automatically if needed

func GetInterfaceSlice

func GetInterfaceSlice() []interface{}

GetInterfaceSlice returns an interface slice from the pool.

The returned slice has length 0 but pre-allocated capacity (typically 16). Use this for building lists of mixed-type values like query result rows. Always call PutInterfaceSlice when done.

Returns:

  • Empty interface slice with capacity 16

Example 1 - Build Result Row:

func buildRow(id int, name string, active bool) []interface{} {
	row := pool.GetInterfaceSlice()
	defer pool.PutInterfaceSlice(row)

	row = append(row, id, name, active)

	// Return a copy
	return append([]interface{}(nil), row...)
}

Example 2 - Collect Values:

func extractValues(props map[string]interface{}, keys []string) []interface{} {
	values := pool.GetInterfaceSlice()
	defer pool.PutInterfaceSlice(values)

	for _, key := range keys {
		if val, ok := props[key]; ok {
			values = append(values, val)
		}
	}

	return append([]interface{}(nil), values...)
}

Example 3 - Function Arguments:

func callFunction(name string, args ...interface{}) interface{} {
	argList := pool.GetInterfaceSlice()
	defer pool.PutInterfaceSlice(argList)

	argList = append(argList, args...)

	return functions[name](argList...)
}

Performance:

  • Eliminates allocation for slice header
  • Pre-allocated capacity 16 handles most rows
  • Typical savings: 1 allocation per call

func GetMap

func GetMap() map[string]interface{}

GetMap returns a string-to-interface map from the pool.

The returned map is empty and ready to use. Maps are commonly used for query parameters, node properties, and JSON-like data structures. Always call PutMap when done to return it to the pool.

Returns:

  • Empty map with capacity 8

Example 1 - Query Parameters:

func buildParams(userID string, filters map[string]string) map[string]interface{} {
	params := pool.GetMap()
	defer pool.PutMap(params)

	params["userID"] = userID
	for k, v := range filters {
		params[k] = v
	}

	// Return a copy
	result := make(map[string]interface{}, len(params))
	for k, v := range params {
		result[k] = v
	}
	return result
}

Example 2 - Node Properties:

func createNode(id, name string, age int) *Node {
	props := pool.GetMap()
	defer pool.PutMap(props)

	props["id"] = id
	props["name"] = name
	props["age"] = age
	props["created_at"] = time.Now()

	return &Node{Properties: copyMap(props)}
}

Example 3 - JSON Response:

func buildResponse(status string, data interface{}) map[string]interface{} {
	resp := pool.GetMap()
	defer pool.PutMap(resp)

	resp["status"] = status
	resp["data"] = data
	resp["timestamp"] = time.Now().Unix()

	return copyMap(resp)
}

Performance:

  • Eliminates allocation for map header and initial buckets
  • Pre-allocated capacity 8 handles most use cases
  • Typical savings: 1 allocation per call

func GetRowSlice

func GetRowSlice() [][]interface{}

GetRowSlice returns a row slice from the pool for query results.

The returned slice has length 0 but may have pre-allocated capacity (typically 64). This reduces allocations when building query result sets. Always call PutRowSlice when done to return the slice to the pool.

Returns:

  • Empty slice with pre-allocated capacity if pooling is enabled
  • Fresh slice with capacity 64 if pooling is disabled

Example 1 - Basic Usage:

rows := pool.GetRowSlice()
defer pool.PutRowSlice(rows)

// Build result set
for _, record := range records {
	row := []interface{}{record.ID, record.Name, record.Value}
	rows = append(rows, row)
}

return rows

Example 2 - Query Execution:

func executeQuery(query string) ([][]interface{}, error) {
	rows := pool.GetRowSlice()
	defer pool.PutRowSlice(rows)

	// Execute query and populate rows
	results := db.Query(query)
	for results.Next() {
		var id, name, value interface{}
		results.Scan(&id, &name, &value)
		rows = append(rows, []interface{}{id, name, value})
	}

	// Make a copy to return (original goes back to pool)
	result := make([][]interface{}, len(rows))
	copy(result, rows)
	return result, nil
}

Example 3 - Batch Processing:

// Process 10,000 records with minimal allocations
for batch := range batches {
	rows := pool.GetRowSlice()

	for _, record := range batch {
		rows = append(rows, record.ToRow())
	}

	processBatch(rows)
	pool.PutRowSlice(rows) // Reuse for next batch
}

Performance:

  • Eliminates allocation for slice header and backing array
  • Typical savings: 2-3 allocations per query
  • For 1000 QPS: ~2000-3000 fewer allocations/sec

Memory Safety:

  • Returned slice is empty (len=0) but has capacity
  • Safe to append without initial allocation
  • Don't modify after calling PutRowSlice

ELI12:

Imagine you're doing homework and need scratch paper:

  • GetRowSlice: Grab a blank sheet from the recycling bin (pool)
  • Use it: Write your work on the paper (append rows)
  • PutRowSlice: Erase it and put it back in the bin for next time
  • This is faster than getting new paper from the store every time!

func GetStringSlice

func GetStringSlice() []string

GetStringSlice returns a string slice from the pool.

The returned slice has length 0 but pre-allocated capacity (typically 16). Use this for building lists of strings like labels, column names, or tags. Always call PutStringSlice when done.

Returns:

  • Empty string slice with capacity 16

Example 1 - Collect Labels:

func getNodeLabels(node *Node) []string {
	labels := pool.GetStringSlice()
	defer pool.PutStringSlice(labels)

	for _, label := range node.Labels {
		if isActive(label) {
			labels = append(labels, label)
		}
	}

	return append([]string(nil), labels...) // Return copy
}

Example 2 - Build Column Names:

func getColumns(fields []Field) []string {
	cols := pool.GetStringSlice()
	defer pool.PutStringSlice(cols)

	for _, field := range fields {
		cols = append(cols, field.Name)
	}

	return append([]string(nil), cols...)
}

Example 3 - Parse Tags:

func parseTags(input string) []string {
	tags := pool.GetStringSlice()
	defer pool.PutStringSlice(tags)

	for _, tag := range strings.Split(input, ",") {
		tag = strings.TrimSpace(tag)
		if tag != "" {
			tags = append(tags, tag)
		}
	}

	return append([]string(nil), tags...)
}

Performance:

  • Eliminates allocation for slice header
  • Pre-allocated capacity avoids growth for small lists
  • Typical savings: 1 allocation per call

func IsEnabled

func IsEnabled() bool

IsEnabled returns whether pooling is enabled.

This function can be used to check if pooling is active, which is useful for conditional logic or debugging output.

Returns:

  • true if pooling is enabled (objects are reused)
  • false if pooling is disabled (objects are allocated fresh)

Example:

if pool.IsEnabled() {
	fmt.Println("Object pooling is active")
} else {
	fmt.Println("Object pooling is disabled (debug mode)")
}

Use Case:

// Conditional metrics collection
if pool.IsEnabled() {
	metrics.RecordPoolHit()
} else {
	metrics.RecordAllocation()
}

func PutByteBuffer

func PutByteBuffer(buf []byte)

PutByteBuffer returns a byte buffer to the pool for reuse.

The buffer is cleared (length reset to 0) before being pooled. Very large buffers (capacity > 1MB) are not pooled to prevent memory leaks.

Parameters:

  • buf: The buffer to return to the pool (will be cleared)

Example:

buf := pool.GetByteBuffer()
defer pool.PutByteBuffer(buf)

// Use buffer...
buf = append(buf, []byte("data")...)

Memory Safety:

  • Buffer is cleared (length set to 0)
  • Capacity is preserved for reuse
  • Don't use buffer after calling PutByteBuffer
  • Buffers >1MB are discarded, not pooled

func PutInterfaceSlice

func PutInterfaceSlice(s []interface{})

PutInterfaceSlice returns an interface slice to the pool for reuse.

The slice is cleared (all references set to nil, length reset to 0) before being pooled. Slices with capacity > MaxSize are not pooled to prevent memory leaks.

Parameters:

  • s: The slice to return to the pool (will be cleared)

Example:

row := pool.GetInterfaceSlice()
defer pool.PutInterfaceSlice(row)

row = append(row, 1, "test", true)
// Use row...

Memory Safety:

  • All references are set to nil
  • Allows GC to collect slice contents
  • Don't use slice after calling PutInterfaceSlice

func PutMap

func PutMap(m map[string]interface{})

PutMap returns a map to the pool for reuse.

The map is cleared (all entries deleted) before being pooled. Very large maps (len > MaxSize) are not pooled to prevent memory leaks.

Parameters:

  • m: The map to return to the pool (will be cleared)

Example:

params := pool.GetMap()
defer pool.PutMap(params)

params["key"] = "value"
// Use params...

Memory Safety:

  • All entries are deleted before pooling
  • Allows GC to collect map values
  • Don't use map after calling PutMap
  • Maps with >MaxSize entries are discarded

func PutNodeSlice

func PutNodeSlice(nodes []*PooledNode)

PutNodeSlice returns a node slice to the pool for reuse.

The slice is cleared (all pointers set to nil) before being pooled to allow garbage collection of the node objects. Slices with capacity > MaxSize are not pooled to prevent memory leaks.

Parameters:

  • nodes: The slice to return to the pool (will be cleared)

Example:

nodes := pool.GetNodeSlice()
defer pool.PutNodeSlice(nodes)

// Build node list...
nodes = append(nodes, &pool.PooledNode{ID: "node-1"})

Memory Safety:

  • All node pointers are set to nil
  • Allows GC to collect node objects
  • Don't access nodes after calling PutNodeSlice

ELI12:

Like returning a tray of cups to the cafeteria:

  • Empty all the cups first (clear pointers)
  • Put the tray back on the stack (return to pool)
  • If the tray is too big (cap > MaxSize), throw it away

func PutRowSlice

func PutRowSlice(rows [][]interface{})

PutRowSlice returns a row slice to the pool for reuse.

The slice is cleared (all references set to nil) before being pooled to allow garbage collection of the row contents. Very large slices (capacity > MaxSize) are not pooled to prevent memory leaks.

Parameters:

  • rows: The slice to return to the pool (will be cleared)

Example:

rows := pool.GetRowSlice()
defer pool.PutRowSlice(rows) // Always return to pool

// Use rows...
rows = append(rows, []interface{}{1, "test"})

Memory Safety:

  • All row references are cleared (set to nil)
  • Allows GC to collect row contents
  • Don't use the slice after calling PutRowSlice

Size Limits:

  • Slices with cap > MaxSize are not pooled (prevents memory leaks)
  • Typical MaxSize: 1000 rows
  • For larger result sets, consider streaming

ELI12:

Like returning a whiteboard to the classroom:

  • First, erase everything on it (clear references)
  • Then put it back on the rack (return to pool)
  • If it's too big to fit on the rack (cap > MaxSize), throw it away
  • Don't try to use the whiteboard after you've returned it!

func PutStringBuilder

func PutStringBuilder(b *PooledStringBuilder)

PutStringBuilder returns a string builder to the pool for reuse.

The builder is automatically reset before being pooled. Very large builders (capacity > 64KB) are not pooled to prevent memory leaks.

Parameters:

  • b: The builder to return to the pool (will be reset)

Example:

builder := pool.GetStringBuilder()
defer pool.PutStringBuilder(builder) // Always return to pool

builder.WriteString("temporary string")
return builder.String()

Memory Safety:

  • Builder is automatically reset (cleared)
  • Don't use the builder after calling PutStringBuilder
  • Large builders (>64KB) are discarded, not pooled

func PutStringSlice

func PutStringSlice(s []string)

PutStringSlice returns a string slice to the pool for reuse.

The slice is cleared (length reset to 0) before being pooled. Slices with capacity > MaxSize are not pooled to prevent memory leaks.

Parameters:

  • s: The slice to return to the pool (will be cleared)

Example:

labels := pool.GetStringSlice()
defer pool.PutStringSlice(labels)

labels = append(labels, "User", "Active")
// Use labels...

Memory Safety:

  • Slice is cleared (length set to 0)
  • Capacity is preserved for reuse
  • Don't use slice after calling PutStringSlice

Types

type PoolConfig

type PoolConfig struct {
	// Enabled controls whether pooling is active
	Enabled bool

	// MaxSize limits maximum objects kept in each pool
	MaxSize int
}

PoolConfig configures object pooling behavior.

Object pooling reduces memory allocations by reusing objects instead of creating new ones. This is especially beneficial for high-frequency operations like query execution where thousands of temporary objects are created per second.

Fields:

  • Enabled: Controls whether pooling is active (disable for debugging)
  • MaxSize: Maximum capacity for pooled objects (prevents memory leaks)

Example:

config := pool.PoolConfig{
	Enabled: true,
	MaxSize: 1000, // Keep up to 1000 objects per pool
}
pool.Configure(config)

ELI12:

Think of object pooling like a library's book return system:

  • Instead of buying new books every time (allocating memory), you return books to the library (pool) when done
  • The next person can check out the same book (reuse object)
  • The library only keeps 1000 books max (MaxSize limit)
  • This saves money (reduces garbage collection) and is faster!

type PooledNode

type PooledNode struct {
	ID         string
	Labels     []string
	Properties map[string]interface{}
}

PooledNode is a minimal node representation for pooling.

This lightweight structure is used for temporary node storage during query execution. It contains only the essential fields needed for most operations, reducing memory overhead.

Fields:

  • ID: Unique node identifier
  • Labels: Node labels (e.g., ["Person", "Employee"])
  • Properties: Node properties as key-value pairs

Example:

node := &pool.PooledNode{
	ID:     "node-123",
	Labels: []string{"User"},
	Properties: map[string]interface{}{
		"name": "Alice",
		"age":  30,
	},
}

Use with Pool:

nodes := pool.GetNodeSlice()
defer pool.PutNodeSlice(nodes)

nodes = append(nodes, &pool.PooledNode{
	ID:     "node-1",
	Labels: []string{"Person"},
})

ELI12:

Think of PooledNode like a sticky note with just the important info:

  • ID: The note's unique number
  • Labels: Tags like "Homework" or "Reminder"
  • Properties: The actual information written on the note
  • It's simpler than a full notebook page, so it's faster to work with!

func GetNodeSlice

func GetNodeSlice() []*PooledNode

GetNodeSlice returns a node slice from the pool.

The returned slice has length 0 but pre-allocated capacity (typically 64 nodes). Use this when building lists of nodes during query execution. Always call PutNodeSlice when done to return the slice to the pool.

Returns:

  • Empty slice with capacity 64 if pooling is enabled
  • Fresh slice if pooling is disabled

Example 1 - Query Results:

func findUsers(db *DB) []*pool.PooledNode {
	nodes := pool.GetNodeSlice()
	defer pool.PutNodeSlice(nodes)

	for _, user := range db.QueryUsers() {
		nodes = append(nodes, &pool.PooledNode{
			ID:     user.ID,
			Labels: []string{"User"},
			Properties: map[string]interface{}{
				"name":  user.Name,
				"email": user.Email,
			},
		})
	}

	// Return a copy (original goes back to pool)
	result := make([]*pool.PooledNode, len(nodes))
	copy(result, nodes)
	return result
}

Example 2 - Graph Traversal:

func traverse(start string, depth int) []*pool.PooledNode {
	visited := pool.GetNodeSlice()
	defer pool.PutNodeSlice(visited)

	queue := []string{start}
	for len(queue) > 0 && depth > 0 {
		current := queue[0]
		queue = queue[1:]

		node := db.GetNode(current)
		visited = append(visited, node)

		for _, neighbor := range db.GetNeighbors(current) {
			queue = append(queue, neighbor)
		}
		depth--
	}

	return copyNodes(visited)
}

Performance:

  • Saves 1-2 allocations per query
  • Reduces GC pressure for graph operations
  • Especially beneficial for traversals with many intermediate nodes

type PooledStringBuilder

type PooledStringBuilder struct {
	// contains filtered or unexported fields
}

PooledStringBuilder is a poolable string builder for efficient string concatenation.

This builder uses a byte slice internally for zero-allocation string building. It's more efficient than using += for string concatenation, especially when building large strings or concatenating in loops.

Methods:

  • WriteString: Append a string
  • WriteByte: Append a single byte
  • String: Get the final string
  • Len: Get current length
  • Reset: Clear for reuse

Example 1 - Build Cypher Query:

builder := pool.GetStringBuilder()
defer pool.PutStringBuilder(builder)

builder.WriteString("MATCH (n:User) WHERE ")
for i, filter := range filters {
	if i > 0 {
		builder.WriteString(" AND ")
	}
	builder.WriteString(filter)
}
builder.WriteString(" RETURN n")

query := builder.String()

Example 2 - Build JSON:

builder := pool.GetStringBuilder()
defer pool.PutStringBuilder(builder)

builder.WriteByte('{')
builder.WriteString(`"name":"`)
builder.WriteString(name)
builder.WriteString(`","age":`)
builder.WriteString(strconv.Itoa(age))
builder.WriteByte('}')

json := builder.String()

Performance vs strings.Builder:

  • Similar performance for single-threaded use
  • Pooled version reduces allocations by ~90%
  • Best for frequently-called functions

Performance vs += concatenation:

  • 10-100x faster for loops
  • O(n) vs O(n²) complexity
  • Use builder for >3 concatenations

ELI12:

Think of PooledStringBuilder like a reusable notepad:

  • WriteString: Write words on the notepad
  • WriteByte: Write a single letter
  • String: Read what you wrote
  • Reset: Erase the notepad to start over
  • Much faster than using a new piece of paper for each word!

func GetStringBuilder

func GetStringBuilder() *PooledStringBuilder

GetStringBuilder returns a string builder from the pool.

The returned builder is empty and ready to use. Always call PutStringBuilder when done to return it to the pool. The builder has pre-allocated capacity (typically 256 bytes) to avoid initial allocations.

Returns:

  • Empty PooledStringBuilder with pre-allocated capacity

Example 1 - Build SQL Query:

builder := pool.GetStringBuilder()
defer pool.PutStringBuilder(builder)

builder.WriteString("SELECT * FROM users WHERE ")
for i, condition := range conditions {
	if i > 0 {
		builder.WriteString(" AND ")
	}
	builder.WriteString(condition)
}

return builder.String()

Example 2 - Build CSV Line:

func buildCSVLine(fields []string) string {
	builder := pool.GetStringBuilder()
	defer pool.PutStringBuilder(builder)

	for i, field := range fields {
		if i > 0 {
			builder.WriteByte(',')
		}
		builder.WriteByte('"')
		builder.WriteString(field)
		builder.WriteByte('"')
	}

	return builder.String()
}

Example 3 - Build Log Message:

builder := pool.GetStringBuilder()
defer pool.PutStringBuilder(builder)

builder.WriteString("[")
builder.WriteString(level)
builder.WriteString("] ")
builder.WriteString(time.Now().Format(time.RFC3339))
builder.WriteString(": ")
builder.WriteString(message)

logger.Print(builder.String())

Performance:

  • Eliminates allocation for string builder
  • Pre-allocated capacity avoids growth for small strings
  • Typical savings: 1-2 allocations per call

func (*PooledStringBuilder) Len

func (b *PooledStringBuilder) Len() int

Len returns the current length of the built string in bytes.

Returns:

  • Number of bytes currently in the buffer

Example:

builder := pool.GetStringBuilder()
builder.WriteString("Hello")
fmt.Println(builder.Len()) // 5

func (*PooledStringBuilder) Reset

func (b *PooledStringBuilder) Reset()

Reset clears the builder for reuse.

This method resets the length to 0 while keeping the underlying capacity, allowing the buffer to be reused without reallocation.

Example:

builder := pool.GetStringBuilder()
builder.WriteString("first")
fmt.Println(builder.String()) // "first"

builder.Reset()
builder.WriteString("second")
fmt.Println(builder.String()) // "second"

func (*PooledStringBuilder) String

func (b *PooledStringBuilder) String() string

String returns the built string.

This method converts the internal byte buffer to a string. The conversion creates a copy, so the builder can be safely reused after calling String.

Returns:

  • The concatenated string

Example:

builder := pool.GetStringBuilder()
builder.WriteString("test")
result := builder.String()
fmt.Println(result) // "test"

func (*PooledStringBuilder) WriteByte

func (b *PooledStringBuilder) WriteByte(c byte)

WriteByte appends a single byte to the builder.

Use this for single characters or delimiters. More efficient than WriteString for single bytes.

Parameters:

  • c: Byte to append

Example:

builder := pool.GetStringBuilder()
builder.WriteByte('[')
builder.WriteString("items")
builder.WriteByte(']')
fmt.Println(builder.String()) // "[items]"

func (*PooledStringBuilder) WriteString

func (b *PooledStringBuilder) WriteString(s string)

WriteString appends a string to the builder.

This method efficiently appends a string to the internal buffer without creating intermediate string copies.

Parameters:

  • s: String to append

Example:

builder := pool.GetStringBuilder()
builder.WriteString("Hello")
builder.WriteString(" ")
builder.WriteString("World")
fmt.Println(builder.String()) // "Hello World"

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL