Runtime
ComputeService
Run heavy work in parallel on Roblox Actors with worker pools, parallel for-loops, dependency graphs and shared tables, all returning Promises.
Getting it
local ComputeService = Engine:GetService("ComputeService")Overview
Roblox can run Luau on several CPU cores at once through Actors, but setting that up by hand means managing actor scripts, messages and results. ComputeService does it for you: it keeps a pool of worker actors, sends them work, and gives you a Promise with the result.
Work is written as a task module: a ModuleScript that returns one function. That function runs in parallel, so it must follow Roblox's parallel rules (it can read the DataModel and do maths, but cannot change most instances).
local Noise = require(path.to.your.noise) -- anything that is safe in parallel
return function(index: number, size: number)
local x = (index - 1) % size
local z = (index - 1) // size
return math.noise(x / 50, z / 50) * 40
endlocal ComputeService = Engine:GetService("ComputeService")
local SIZE = 512
ComputeService:ParallelForAsync(SIZE * SIZE, ReplicatedStorage.Tasks.HeightAt, SIZE)
:andThen(function(heights)
buildTerrain(heights) -- back in serial, safe to change instances
end)How many workers
When first used, ComputeService runs a short capacity probe: it measures how many actors this machine can run at the same time before they stop getting faster, and sizes pools to match. Until the probe finishes, DefaultMaxWorkers is used. On servers it re-checks when players join (at most once per CapacityRecheckCooldownSeconds).
Workers live in a folder named Engine_ComputeService_Workers under ServerScriptService (server) or the player's PlayerScripts (client).
Parallel for
ParallelForAsync
ComputeService:ParallelForAsync(count: number, jobModule: ModuleScript, shared: any?): Promise<{ any }>Calls the job function as fn(i, shared) for every i from 1 to count, split into one chunk per worker. Resolves with the results in order. shared is sent to every worker; for large data pass a shared table's Raw (see Shared tables) instead of copying a big table to each worker.
Worker pools
CreatePool
ComputeService:CreatePool(name: string, config: WorkerPoolConfig?): WorkerPoolA named pool for your own tasks. Names starting with __ are reserved.
| Config | Description |
|---|---|
MaxWorkers | Most workers at once. Default: the detected capacity. |
IdleTimeout | Seconds an idle worker lives before it is destroyed. |
IdleSweepInterval | Seconds between idle checks. |
Prewarm | Workers to create up front. Default: MaxWorkers. |
GetPool / DestroyPool
ComputeService:GetPool(name: string): WorkerPool?
ComputeService:DestroyPool(name: string)WorkerPool
| Member | Description |
|---|---|
:RegisterTask(name, moduleScript) | Makes a task module available to the pool's workers. |
:RunAsync(taskName, ...): Promise | Runs the task once with the arguments on a free worker and resolves with its return values. Waits in a queue when every worker is busy. |
:RunRangeAsync(taskName, count, extra?): Promise<{ any }> | Like ParallelForAsync on this pool: fn(i, extra) for 1 to count. |
:RunAllAsync(taskName, argsList): Promise<{ any }> | One RunAsync per entry of argsList (each an array of arguments), resolved together. |
:Prewarm(count): number | Creates workers ahead of time and returns how many were made. |
:Start(), :Stop() | Start idle sweeping; stop and destroy all workers. Running a task starts the pool by itself. |
:Destroy() | Stops the pool for good. |
:GetStats(): PoolStats | WorkerCount, BusyCount, IdleCount, QueueLength, TotalTasksRun, TotalTaskErrors. |
MaxWorkers, IdleTimeout | Settable. |
Name, WorkerCount, BusyCount, IdleCount, QueueLength, Destroyed | Read-only. |
WorkerCreated, WorkerDestroyed, TaskFailed | Signals. |
local pool = ComputeService:CreatePool("Pathing", { MaxWorkers = 4 })
pool:RegisterTask("FindPath", ReplicatedStorage.Tasks.FindPath)
pool:RunAsync("FindPath", startCell, goalCell):andThen(function(path)
npc:Follow(path)
end)An error inside a task rejects that task's Promise; the worker survives and takes the next task.
Task graphs
CreateGraph
ComputeService:CreateGraph(): TaskGraphA set of tasks with dependencies. Tasks whose dependencies are done run in parallel; each task receives the results of the tasks it depends on as arguments, in the order you listed them.
local graph = ComputeService:CreateGraph()
:AddTask("Heights", Tasks.Heights)
:AddTask("Moisture", Tasks.Moisture)
:AddTask("Biomes", Tasks.Biomes, { "Heights", "Moisture" })
graph:RunAsync():andThen(function(results)
paintBiomes(results.Biomes)
end)| Method | Description |
|---|---|
:AddTask(name, moduleScript, dependsOn?) | Adds a task. Returns the graph, so calls chain. |
:HasTask(name), :GetTaskNames() | |
:RunAsync(pool?): Promise<{ [name]: result }> | Runs everything and resolves with every result by name. Rejects with the failing task's name and error. A graph with a cycle or a missing dependency rejects before anything runs. |
:Describe(): string | The graph as Graphviz DOT. |
Shared tables
CreateShared / GetShared
ComputeService:CreateShared(name: string, initial: { [any]: any }?): SharedHandle
ComputeService:GetShared(name: string): SharedHandleA named SharedTable that every actor can read without copying. Inside a task module, read it with SharedTableRegistry:GetSharedTable("ComputeService:" .. name), or pass the handle's Raw as an argument.
| Member | Description |
|---|---|
:Get(key), :Set(key, value) | Read and write. |
:Update(key, fn) | Atomic update with fn(old). |
:Increment(key, delta?): number | Atomic add, returns the new value. |
:Snapshot(deep?) | A normal Luau table copy. |
:Clear() | |
:Freeze(deep?) | Replaces it with a frozen copy, which is faster to read from many actors. |
Name, Size, Frozen, Raw | Read-only. |
Diagnostics
| Method | Description |
|---|---|
:GetStats() | { PoolCount, TotalWorkers, TotalBusy, TotalQueued } over every pool. |
:GetActorCount() | Workers alive right now. |
:GetMaxActorCount() | The detected capacity (or the default until detection finishes). |
:RecheckCapacityAsync(): Promise<number> | Runs the capacity probe again. |
Configuration
DefaultConfiguration/Services/ComputeService7 settings| Setting | Default | What it does |
|---|---|---|
| DefaultMaxWorkers | 16 | Bootstrap worker ceiling used until the capacity probe has measured the real one. |
| DefaultIdleTimeout | 45 | Seconds a worker can sit idle before it is destroyed. |
| IdleSweepInterval | 15 | Seconds between idle-worker sweeps. |
| CapacityIterationsPerActor | 250000 | Trig iterations each actor runs per capacity-probe trial. |
| CapacityMaxProbe | 64 | Largest actor count the capacity probe will ever try. |
| CapacityTolerance | 1.6 | How far a trial's wall time may drift above the N=1 baseline before that N is rejected. |
| CapacityRecheckCooldownSeconds | 60 | Minimum seconds between two PlayerAdded-triggered capacity rechecks (server only). |
Types
export type WorkerPool
export type WorkerPoolConfig = { MaxWorkers: number?, IdleTimeout: number?, IdleSweepInterval: number?, Prewarm: number? }
export type PoolStats
export type TaskGraph
export type SharedHandle
export type Stats = { PoolCount: number, TotalWorkers: number, TotalBusy: number, TotalQueued: number }