Files
lneto/http/httphi/routermem.go
T
Pat Whittingslow d05cd14018 httphi router refactor (#176)
* httphi: RouterConfig refactor to enable DefaultRouterConfig

* RequestHeader not case sensitive anymore

* httphi: use DefaultRouterConfig in examples

* httphi: remove gated stage complexity

Misusing Stage methods by calling them once header has been written is totally harmless as far as I can tell. We simplify the codebase on this occasion by removing the headerWritten check for all stage methods

* httphi: improve APIs

* httphi: remove status
2026-08-03 15:33:43 -07:00

123 lines
6.1 KiB
Go

package httphi
import (
"math"
"unsafe"
"github.com/soypat/lneto/http/httpraw"
)
const (
// minRequestHeaderBuffer is the smallest request buffer [httpraw.Header]
// accepts with buffer growth disabled, which is how exchanges are configured.
minRequestHeaderBuffer = 32
// minResponseHeaderBuffer is the room [Exchange.FlushHeader] needs for the
// CRLF closing the header block, written even when no field was staged.
minResponseHeaderBuffer = len("\r\n")
// maxExchangeBuffer bounds an exchange's whole buffer: [Exchange] indexes it
// with uint16 offsets, so a larger one would be addressed truncated.
maxExchangeBuffer = math.MaxUint16
// sizeofExchange is the fixed cost of an exchange, which a Router pays per
// connection it can serve concurrently on top of the buffers it hands it.
// It dwarfs a small request buffer, so budgets must account for it.
sizeofExchange = int(unsafe.Sizeof(Exchange{}))
// sizeofPathValue is the per-wildcard cost of the path value table.
sizeofPathValue = int(unsafe.Sizeof(PathValue{}))
// sizeofJob is an exchange's slot in the queue connections wait on for a
// worker goroutine, sized to the goroutine count in worker mode.
sizeofJob = int(unsafe.Sizeof(job{}))
// bytesPerHeaderField is the request buffer [DefaultRouterConfig] budgets per
// parseable header field. Real fields run a little longer than this
// ("Accept-Encoding: gzip, deflate, br\r\n" is 35 bytes), so a request fills
// the buffer before it exhausts the field table, which is the cheaper of the
// two limits to hit: growing the table costs [httpraw.SizeKV] per field on top
// of the bytes the field already occupies.
bytesPerHeaderField = 32
// defaultResponseHeaderBuffer is the response header room
// [DefaultRouterConfig] reserves when the budget can afford it: enough for a
// Content-Type, a Content-Length and a Connection field with room to spare.
// It does not scale with the request buffer because what a response header
// costs depends on the fields a handler stages, not on the request's size.
defaultResponseHeaderBuffer = 128
)
// MemoryUsagePerConnection returns the heap bytes a [Router] configured with cfg
// reserves for each connection it can serve concurrently, maxPathValues being
// the [Mux.MaxPathValues] of the mux it is configured with. Goroutine stacks are
// not counted: those are the runtime's to size, not the router's.
//
// In worker mode this is exact and fixed, so a router's whole heap footprint is
// this times FixedNumGoroutines, plus the runtime's own header for the job
// queue. With FixedNumGoroutines -1 the router allocates one of these per
// connection in flight instead, so the total grows with peak concurrency.
//
// It is the inverse of [DefaultRouterConfig] and useful to check a hand written
// configuration against a memory budget.
func (cfg RouterConfig) MemoryUsagePerConnection(maxPathValues int) int {
if maxPathValues < 0 {
maxPathValues = 0 // Mux with no routes registered yet, see [Mux.MaxPathValues].
}
n := sizeofExchange + // Exchange itself, an element of the router's exchange store.
cfg.RequestHeaderBufferSize + cfg.ResponseHeaderMinBufferSize + // Its window into the raw buffer.
cfg.RequestNumHeaderKVCap*httpraw.SizeKV + // The request header's field table.
maxPathValues*sizeofPathValue // Its window into the path value store.
if cfg.workerMode() {
n += sizeofJob // Slot in the queue connections wait on for a worker.
}
return n
}
// DefaultRouterConfig is a general purpose configuration creator
// for small, medium, large, performant or embedded projects.
//
// Generalness is achieved with parameters that let the Configuration
// determine allocation buffer sizes based on typical usage for that
// number of goroutines and heap allocation on a per-connection basis.
func DefaultRouterConfig(numGoroutines, memoryPerConnectionBytes, maxPathValues int) RouterConfig {
// The budget is spent on the request header buffer first, since that is what
// decides which requests are answered at all, then on a field table sized to
// match it and a small response header reserve. A budget too small to fund the
// minimum viable exchange yields the minimum instead, so the returned config is
// always one [Router.Configure] accepts but may exceed a budget under roughly
// sizeofExchange + 200 bytes. Check it with MemoryUsagePerConnection when the
// bound has to hold.
if numGoroutines <= 0 {
numGoroutines = -1 // Unbounded mode, the only non-positive value Validate accepts.
}
// Everything the exchange costs before any buffer is sized: subtract it first
// so the buffers below divide up what is actually left to spend.
fixed := sizeofExchange + maxPathValues*sizeofPathValue
if numGoroutines > 0 {
fixed += sizeofJob
}
// A budget past what the buffers may grow to is only spendable up to the cap
// below, so clamp before the products: on a 32 bit target an unclamped
// multiply would overflow and wrap a generous budget into a tiny buffer.
const maxSpendable = (maxExchangeBuffer + defaultResponseHeaderBuffer) *
(bytesPerHeaderField + httpraw.SizeKV) / bytesPerHeaderField
avail := min(memoryPerConnectionBytes-fixed, maxSpendable)
// The response reserve is a floor rather than a share of the budget, but a
// budget this small cannot afford the full one without starving the request.
respBuf := min(defaultResponseHeaderBuffer, avail/4)
// Solve avail-respBuf = reqBuf + reqBuf/bytesPerHeaderField*httpraw.SizeKV for
// reqBuf, the field table growing with the buffer it parses.
reqBuf := (avail - respBuf) * bytesPerHeaderField / (bytesPerHeaderField + httpraw.SizeKV)
// Clamp to what [RouterConfig.Validate] accepts. Truncating division above
// keeps the result under budget; these floors are what can push it over.
respBuf = max(respBuf, minResponseHeaderBuffer)
reqBuf = max(reqBuf, minRequestHeaderBuffer)
reqBuf = min(reqBuf, maxExchangeBuffer-respBuf)
return RouterConfig{
FixedNumGoroutines: numGoroutines,
RequestHeaderBufferSize: reqBuf,
ResponseHeaderMinBufferSize: respBuf,
RequestNumHeaderKVCap: max(reqBuf/bytesPerHeaderField, 1),
NormalizeOutgoingKeys: false,
}
}