diff --git a/browsers/pools/overview.mdx b/browsers/pools/overview.mdx index d3ffd1c..6491771 100644 --- a/browsers/pools/overview.mdx +++ b/browsers/pools/overview.mdx @@ -461,3 +461,35 @@ func main() { ## API reference For more details on all available endpoints and parameters, see the [Browser Pools API reference](https://kernel.sh/docs/api-reference/browser-pools/list-browser-pools). + +## Pool sizing calculator + +import { PoolSizingCalculator } from '/snippets/pool-sizing-calculator.jsx'; + +Use the calculator below to estimate a pool size for your workload. It assumes `reuse: false` on release, so every acquisition ends in destruction and triggers a refill. + + + +### How the calculation works + +Two constraints have to be satisfied at the same time, so the recommended size is the larger of the two. + +**Concurrency floor.** With a peak acquisition rate `λ` (per minute) and an average acquired duration `d` (minutes), the number of browsers held simultaneously trends toward `λ × d`. We multiply by a 1.25× safety factor to keep ~10–20% of the pool available during normal load (see the [pool sizing guidance in the FAQ](/browsers/pools/faq#how-do-i-know-if-my-pool-is-too-small-or-too-large)). + +``` +concurrency_floor = ceil(λ × d × 1.25) +``` + +**Refill floor.** The [`fill_rate_per_minute`](https://kernel.sh/docs/api-reference/browser-pools/create-a-browser-pool#body-fill-rate-per-minute) is a percentage of pool size and is capped at 25. With `reuse: false`, browsers are destroyed at the acquisition rate, so the refill rate must keep up: + +``` +refill_floor = ceil(100 × λ / fill_rate) +``` + +**Recommended pool size.** + +``` +N = max(concurrency_floor, refill_floor) +``` + +At the 25% fill ceiling, the two constraints meet at `d ≈ 3.2` minutes. Below that, the refill ceiling sets the floor; above it, concurrency does. diff --git a/snippets/pool-sizing-calculator.jsx b/snippets/pool-sizing-calculator.jsx new file mode 100644 index 0000000..a5d4c83 --- /dev/null +++ b/snippets/pool-sizing-calculator.jsx @@ -0,0 +1,105 @@ +const { useState, useEffect, useRef } = React; +const { Card, Columns } = MintlifyComponents; + +export const PoolSizingCalculator = () => { + const defaults = { acquisitionRate: 10, sessionDurationMinutes: 5, fillRate: 25 }; + + const [acquisitionRate, setAcquisitionRate] = useState(defaults.acquisitionRate); + const [sessionDurationMinutes, setSessionDurationMinutes] = useState(defaults.sessionDurationMinutes); + const [fillRate, setFillRate] = useState(defaults.fillRate); + const [flash, setFlash] = useState(false); + const prevResultRef = useRef(null); + const hasInteracted = useRef(false); + + useEffect(() => { + if (!hasInteracted.current) return; + var url = new URL(window.location); + url.searchParams.set('acquisitionRate', acquisitionRate); + url.searchParams.set('sessionDuration', sessionDurationMinutes); + url.searchParams.set('fillRate', fillRate); + url.hash = 'pool-sizing-calculator'; + window.history.replaceState(null, '', url); + }, [acquisitionRate, sessionDurationMinutes, fillRate]); + + const safety = 1.25; + const lambda = Number.isFinite(acquisitionRate) && acquisitionRate > 0 ? acquisitionRate : 0; + const duration = Number.isFinite(sessionDurationMinutes) && sessionDurationMinutes > 0 ? sessionDurationMinutes : 0; + const rate = Number.isFinite(fillRate) && fillRate > 0 ? Math.min(fillRate, 25) : 1; + + const refillFloor = Math.ceil((100 * lambda) / rate); + const concurrencyFloor = Math.ceil(lambda * duration * safety); + const poolSize = Math.max(refillFloor, concurrencyFloor); + const bindingConstraint = concurrencyFloor >= refillFloor ? 'concurrency' : 'refill'; + + useEffect(() => { + var prev = prevResultRef.current; + if (prev !== null && prev.poolSize !== poolSize) { + setFlash(true); + var t = setTimeout(() => setFlash(false), 300); + return () => clearTimeout(t); + } + prevResultRef.current = { poolSize }; + }, [poolSize]); + + const labelStyle = { fontWeight: 600, fontSize: '0.875rem', minWidth: '12rem', flexShrink: 0, maxWidth: '12rem' }; + const rowStyle = { display: 'flex', alignItems: 'center', gap: '0.5rem', minHeight: '2.25rem' }; + const inputStyle = { minWidth: 0, flex: 1, maxWidth: '100%', boxSizing: 'border-box', background: 'transparent' }; + const numberInputStyle = { borderBottom: '1px solid #81b300', textAlign: 'right' }; + const flashStyle = { background: flash ? '#81b300' : 'transparent', transition: 'background 0.5s ease', marginLeft: 'auto' }; + + const setRate = (v) => { + hasInteracted.current = true; + const n = parseInt(v); + if (Number.isNaN(n)) { setFillRate(0); return; } + setFillRate(Math.max(1, Math.min(25, n))); + }; + + return ( + + +
+ + { hasInteracted.current = true; setAcquisitionRate(parseFloat(e.target.value)); }} /> +
+
+ + { hasInteracted.current = true; setSessionDurationMinutes(parseFloat(e.target.value)); }} /> +
+
+ + setRate(e.target.value)} /> +
+
+ + Assumes reuse: false on release (every acquisition triggers a refill). Safety factor 1.25× covers the recommended 10–20% headroom. + +
+
+ +
+ Concurrency floor: + {concurrencyFloor} +
+
+ Refill floor: + {refillFloor} +
+
+ Pool size: + {poolSize} +
+
+ + Binding constraint: {bindingConstraint}. + {bindingConstraint === 'refill' + ? ' Shorter sessions or higher acquisition rates push refill above concurrency — the 25% fill ceiling sets the floor.' + : ' Longer-held browsers dominate — pool size scales with acquisitions × duration.'} + +
+
+
+ ); +};