Hello world!
When doing work that takes a significant amount of time to compute, the usual solution is to yield ever so often to avoid causing lag. However, doing so leaves performance on the table
When yielding, the script is resumed on the next frame. Since most users run at a capped fps of, usually 60 fps, the engine just sleeps in between the time all the work for the frame was done, and the point where 16.6ms is reached (1/0.0166 = 60 fps)
Thus, implementing a yield into your code makes the computation take longer to compute, compared to no yield. Though not implementing a yield is bad practice due to the lag spikes it could create
Moreover, fine tuning the point where yielding happens, to ensure the least amount of wasted time, doesn’t work for two reasons. Some devices will do the computation faster than others, and, other scripts in your game already take up some of the 16.6ms, and that amount is likely to be variable
How ComputeModule fixes that issue
ComputeModule keeps track of when the frame started, and when it is supposed to end. With that information, it is able to cram the computation within that area, yielding right before the frame is supposed to end
By default, it detects the fps automatically on the client
Copy the code
local Signal = ComputationModule:BindFunction(function()
local n = 0
for i = 1, 10000 do
for i = 1, 1000 do
n += 1
end
ComputationModule:EvaluateYield()
end
end)
Signal:Wait() -- Computation was completed
– Methods –
To use ComputeModule, require it, then use :BindFunction(Function) and pass a function to it
Once you do some work, call :EvaluateYield(). This method evaluates if there is still time available to keep doing the computation, or if it should yield to the next frame. It also allows other binded functions to run
:EvaluateYield() should be used at appropriate intervals, maybe around 0.2ms to 0.01ms. Doesn’t have to be precise though
When calling :BindFunction(Function), a custom signal is returned (signal only has :Wait()
and :Once()
). That signal is fired when the function did all the work, or had an error. No arguments are passed by this signal
You can also use :UpdateTargetFPS(TargetFPS) to set the target fps the module will follow. Default is 60 on the server, and Automatic on the client
Since the module calculates the fps for the automatic setting, there is a :GetFPS() method, free of charge :D
– Settings –
DEBUG_ENABLED - Prints information into the output
1 - The amount of time spent not doing actual work (the overhead), in milliseconds, and the % of the total time it represents
2 - The amount of threads that were resumed. More thread resumed = more overhead. The same threads are resumed many times per frame
3 - The amount of active threads at the end of the frame
TIME_BUFFER - How much time to reserve, as a buffer. Since there are no events right at the beginning and right at the end of a frame, some stuff still runs after the compute module (a task named Thread, idk what it does, and Data Sender, on some frames)
Might change this setting to not be a fixed time
MINIMUM_WORK_TIME - The minimum amount of time the module will reserve to let binded functions do work. This is to ensure the work gets done, even if that means causing lag
MINIMUM_CYCLES - Serves a similar purpose to MINIMUM_WORK_TIME, but instead of being the time, it is the minimum cycles it will do. A cycle is every function running once (1 call to :EvaluateYield())
TARGET_FPS - Fps the module will follow. Default is 60 on the server, and Automatic on the client. Can be modified at runtime with :UpdateTargetFPS(TargetFps)
– Files –
Creator Hub
Compute Module.rbxl (122.4 KB) includes the code I have used to test it
Updates
([DD/MM/YYYY], screw you America)
[05/07/2024] - Added a custom signal implementation, fixed grammatical mistakes and renamed stuff from ComputationModule to ComputeModule
The module goes anywhere you want
If you have some code binded to RunService.PostSimulation, you might want to require it from a client script and/or a server script (with RunContext set to server) from ReplicatedFirst. This will ensure that the connection to RunService.PostSimulation in the module is fired after the connections in your own code
If anyone has questions about how it works or whatever, I’ll gladly answer them
If the module doesn’t work properly on your device (such as not being able to get high fps when TARGET_FPS is automatic, or something else), please tell me
Stupid things
There is no event at the beginning of a frame and at the end of a frame, which causes some issues with the module. It’s stupid
Cannot get the fps setting of the menu. Because of that, I had to calculate the fps manually, and set the TargetFps to be a bit higher than the fps, to prevent the module from restricting the fps. If the frametime is higher than expected, it will instead tank the fps down to 60