Naar de inhoud
Measure before you optimise

Measure before you optimise

How to find the code that actually costs you: resmon, the FiveM profiler, tick times, and why the shape of a loop matters more than any single line.

Deze docs zijn voorlopig in het Engels.

Last updated

Every performance argument in FiveM ends the same way: someone measures. Until then it is opinion. A resource that “feels heavy” is usually fine, and the one nobody suspects is the one waking up 60 times a second to loop over a config table.

Measure first, then use the rest of this section on whatever the numbers point at.

The two budgets

Side Budget What eats it
Client About 16 ms per frame at 60 FPS, shared by GTA and every resource Per frame loops, drawing, distance checks, NUI
Server One tick, shared by every resource and by sync Blocking calls, slow queries, big loops, heavy event handlers

Neither side has a spare thread waiting for you. Client Lua runs inside the game’s frame, and server Lua runs on the server’s main loop. Anything slow delays everything else.

resmon: which resource

Press F8 in game and run:

Text
resmon 1

You get one row per client resource with CPU time per frame and memory. Roughly:

Time per frame Reading
0.00 to 0.02 ms Idle. Nothing to do here.
0.02 to 0.10 ms Normal for a resource doing something.
0.10 to 0.50 ms Look at it, especially if it stays there while nothing is happening.
Over 0.50 ms, constantly Something runs every frame that should not.

Two things people get wrong with resmon:

  • Test in the right place. A shop resource costs nothing across the map and costs real time standing in the shop. Both numbers are interesting, and only one of them is the bug.
  • Watch the idle number. A HUD that costs 0.30 ms while drawing is doing its job. A fishing script that costs 0.30 ms while you stand in the desert is not.

On the server, txAdmin’s performance chart shows tick times, and the console prints hitch warnings when a tick runs long.

The profiler: which function

When resmon has named the resource, the built in profiler names the code. It works in the client console and the server console:

Text
profiler record 500
profiler status
profiler view
profiler saveJSON myprofile.json

profiler record 500 records 500 frames or ticks, profiler view opens the result as a flame chart, and saveJSON writes it to a file in the server or game folder.

If you would rather not read flame charts, drop the saved JSON into the Profiler Analyzer. It ranks resources and threads by the time they used and points at the usual causes.

Read the shape, not the line

When you have the numbers, look at the structure before you look at the syntax. In rough order of what is worth fixing:

client.lua (the expensive shape)
CreateThread(function()
    while true do
        Wait(0)                                   -- 60 passes a second, forever
        local ped = PlayerPedId()                 -- a native call, every frame
        for i = 1, #Config.Shops do               -- 40 shops, every frame
            local shop = Config.Shops[i]
            if #(GetEntityCoords(ped) - shop.coords) < 20.0 then
                drawShop(shop)
            end
        end
    end
end)

The problem is not #(a - b) and it is not the for loop. The problem is that the whole block runs every frame whether or not a shop is anywhere near you, and that GetEntityCoords is called once per shop instead of once per pass.

client.lua (the cheap shape)
CreateThread(function()
    local shops = Config.Shops
    while true do
        local sleep = 1000
        local pos = GetEntityCoords(PlayerPedId())  -- once per pass

        for i = 1, #shops do
            local shop = shops[i]
            if #(pos - shop.coords) < 20.0 then
                sleep = 0                            -- only now go per frame
                drawShop(shop)
            end
        end

        Wait(sleep)
    end
end)

Same natives, same loop, same maths. Away from every shop it now runs once a second instead of 60 times, and it calls GetEntityCoords once instead of 40 times. That is the kind of change that shows up in resmon. Nothing else on these pages will beat it.

Threads and performance goes further with dynamic waits, ox_lib points and zones, and reacting to events instead of polling.

Where the small stuff belongs

The rest of this section has plenty of “this form is cheaper than that form”. They are real, and they are small. Use them like this:

  • Writing new code: pick the cheap form. It costs you nothing to type t[#t + 1] = v instead of table.insert(t, v).
  • Inside a hot loop: worth changing. A thousand iterations a frame turns a tiny difference into a visible one.
  • In code that runs once at startup, or on a command: leave it alone. Readability wins.

Warning

Do not trust benchmark numbers from forum posts, including old ones about FiveM. The Lua runtime changed, the VM changed, and a lot of the numbers still going around were measured on versions nobody runs any more. If a claim matters to your resource, measure it on your resource.

Assets count too

A perfectly written resource still stutters if it streams 4K textures onto every car. Oversized .ytd files cause texture loss and memory pressure, and no amount of Lua tuning fixes that. See Streaming custom assets and the YTD Optimizer.

Next: Scope, files and layout.