Skip to content

Pool

Pool reuses Instances for things you spawn all the time — bullets, VFX, damage numbers — turning Instance.new/Destroy churn into table operations. It’s one half of the kernel’s pay-instance-costs-once pair; the other half, Preload, warms assets ahead of time instead of recycling live instances.

Instance.new + Destroy churn fragments memory and stutters frames — every projectile tracer created and destroyed is allocation the GC must sweep (GcWatch shows this as churn: stable heap, busy cycles). A pool keeps retired instances in an idle list and vends them back out on the next acquire.

Internally a pool is three structures:

  • Idle — an array used as a stack; acquire pops the most recently released instance (warm in cache), release pushes.
  • IdleSet — a set mirror of Idle, so a double release is an O(1) check instead of a scan.
  • LiveSet + LiveCount — every instance currently checked out. Releasing something the pool never vended is an error, and destroy() can reach checked-out instances too.

There is no cap and no shrinking: an empty idle list means acquire calls your Create factory, so the pool grows to your peak concurrent demand and holds there. That is the point — peak demand is what you would have allocated anyway, and holding it means never allocating it again.

Pool manages lifetime only. It never resets properties. release does exactly one thing to the instance — sets Parent = nil (which stops rendering, physics, and touch events) — and acquire hands it back exactly as it was released. Resetting visual state (position, transparency, particle emitters, attributes) is the acquirer’s job, because only the caller knows which properties its effect dirties.

-- src/Server/Bootstrap.luau (works identically on the client)
local Pool = require(game:GetService("ReplicatedStorage").ChloeKernel.Pool)
return function(kernel)
local Casings = Pool.new({
Create = function()
local Casing = Instance.new("Part")
Casing.Size = Vector3.new(0.2, 0.2, 0.5)
Casing.Material = Enum.Material.Metal
Casing.CanCollide = false
Casing.CanQuery = false
return Casing
end,
InitialSize = 16, -- prewarmed at boot, not mid-firefight
})
kernel.Bus:subscribe("Weapon.Fired", function(_, muzzleCframe)
local Casing = Casings:acquire() :: BasePart
Casing.CFrame = muzzleCframe -- reset state on acquire, not on release
Casing.Parent = workspace
task.delay(2, function()
Casings:release(Casing)
end)
end)
end
Member Description
Pool.new({ Create, InitialSize? }) → pool Create: () -> Instance is the factory. InitialSize (default 0) prewarms that many instances immediately, parented to nil.
pool:acquire() → Instance Pops an idle instance, or calls Create when the idle list is empty. The instance is tracked as live.
pool:release(instance) Unparents the instance and returns it to the idle list. Errors on a double release ("instance was already released to this pool") and on foreign instances ("instance was not acquired from this pool").
pool:idleCount() → number Instances waiting in the idle list.
pool.LiveCount Instances currently checked out. idleCount() + LiveCount is the pool’s total footprint.
pool:destroy() Destroys every idle and every live checked-out instance, then clears all tracking.
  • ProjectilesProjectileClient keeps one pool per visual definition (prewarmed 4 each) plus a shared tracer pool (prewarmed 8) for the minimal fallback visuals rendered when the full-visual cap is hit. Sixty projectiles a second costs zero instance churn.
  • Dissolve — one pool per voxel template (prewarmed 16); a dissolve effect’s hundreds of drifting voxels are all reused parts.
  • Simulation — the load harness’s projectile visuals are pooled, so the stress test measures kernel cost, not instance churn.
  • Fixed-size effects with a natural lifetime (casings, hit sparks, damage numbers) suit task.delay-driven release. Effects with variable lifetimes should release from the code that owns their end condition — the pool has no timeout of its own.
  • Pools are plain Luau with no kernel dependency; they work in any script, server or client, including inside processes.
  • Pool recycles Instances. For recycling plain Lua tables (scratch lists, query results), see TablePool — same idle-stack idea, different resource: TablePool unparents nothing and reparents nothing, it only clears and reuses a bare table.