🚀 07 — Ottimizzazione e Debug

INFO

L’ottimizzazione e il debug sono competenze essenziali per sviluppare risorse FiveM performanti e stabili. La console F8, resmon e il profiler sono gli strumenti principali per diagnosticare colli di bottiglia e migliorare il codice.

ObiettivoDettagli
Cosa imparerai- Usare resmon e profiler per diagnosticare problemi di performance
- Applicare pattern di ottimizzazione (Wait condizionale, cache locale, DistanceSquared)
- Gestire errori con pcall e creare debug output efficaci
PrerequisitiTutti i file precedenti (02-06)

Console F8

La console di FiveM (F8 in gioco) è il tuo primo strumento di debug.

flowchart TD
    A[F8 in gioco] --> B{Comandi utili}
    B --> C[_devTools - Apre Chrome DevTools per NUI]
    B --> D[resmon - Mostra uso risorse in tempo reale]
    B --> E[profiler - Profiling avanzato]
    B --> F[net_graph - Mostra statistiche rete]
    B --> G[dbg_... - Vari comandi di debug]
    B --> H[reload <risorsa> - Ricarica risorsa]

Comandi Console Essenziali

ComandoDescrizione
resmonMostra il consumo CPU di ogni risorsa
profilerProfiling dettagliato di chiamate native
net_graph 1Mostra ping, pacchetti, tick rate
_devToolsApre DevTools per debugging NUI
reload mia_risorsaRicarica una risorsa senza riavviare il gioco
refreshRicarica tutte le risorse
start/stop mia_risorsaAvvia/ferma una risorsa

Identificare Colli di Bottiglia

Resmon (Resource Monitor)

F8 → resmon

Resmon mostra:

  • CPU ms — quanto tempo CPU spende ogni risorsa per frame
  • Chiamate native — quante native chiama al secondo
  • Memoria — uso memoria Lua

Target: ogni risorsa sotto 1-2ms per frame. Risorse con 5ms+ sono problematiche.

Pattern che Uccidono le Performance

WARNING

Loop senza Wait sono la causa numero 1 di lag e crash in FiveM. Un thread bloccante consuma il 100% di un core CPU e blocca l’intero runtime. Anche Wait(0) è fondamentale — cede lo scheduler al frame successivo.

-- DA EVITARE: loop senza Wait
Citizen.CreateThread(function()
    while true do
        -- Nessuna pausa → 100% CPU
        local ped = PlayerPedId()
        local coords = GetEntityCoords(ped)
        -- ...
    end
end)
 
-- CORRETTO: Wait appropriato
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(0)  -- minimo: cede 1 frame
        local ped = PlayerPedId()
        local coords = GetEntityCoords(ped)
        -- ...
    end
end)
-- DA EVITARE: polling continuo quando non serve
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(0)
        local ped = PlayerPedId()
        local veicolo = GetVehiclePedIsIn(ped, false)
        if veicolo ~= 0 then
            local speed = GetEntitySpeed(veicolo)
            -- aggiorna HUD velocità ogni frame
        end
    end
end)
 
-- CORRETTO: cambia Wait in base allo stato
Citizen.CreateThread(function()
    while true do
        local ped = PlayerPedId()
        local veicolo = GetVehiclePedIsIn(ped, false)
 
        if veicolo ~= 0 then
            local speed = GetEntitySpeed(veicolo)
            -- aggiorna HUD
            Citizen.Wait(100) -- 10 fps per HUD veicolo
        else
            Citizen.Wait(1000) -- controlla solo ogni 1s se non in veicolo
        end
    end
end)

Pattern di Thread: Wait(0) vs Wait(N)

TIP

Strategia a livelli di Wait: usa Wait(0) solo per input e UI (ogni frame), Wait(100) per HUD e controlli (10 fps), Wait(1000+) per check sporadici. Meno chiamate = migliori performance.

WaitQuando usareCosto CPU
Wait(0)Ogni frame (60fps)Alto — solo per UI o input
Wait(50)20 fpsMedio — aggiornamenti frequenti
Wait(100)10 fpsBasso — HUD, controlli
Wait(1000)1 fpsMinimo — check sporadici
Wait(5000)Ogni 5sQuasi nullo — pulizie, sync
-- Strategia a livelli
Citizen.CreateThread(function()
    while true do
        -- Ogni frame: solo input
        if IsControlJustPressed(0, 38) then
            -- azione
        end
 
        Citizen.Wait(0)
    end
end)
 
Citizen.CreateThread(function()
    while true do
        -- Ogni 200ms: check posizione
        local ped = PlayerPedId()
        local coords = GetEntityCoords(ped)
        -- ...
        Citizen.Wait(200)
    end
end)
 
Citizen.CreateThread(function()
    while true do
        -- Ogni 5s: sync dati
        TriggerServerEvent("sync:posizione")
        Citizen.Wait(5000)
    end
end)

Cache Locale vs Polling Frequente

-- DA EVITARE: chiamare native costose ripetutamente
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(100)
 
        local ped = PlayerPedId()
        local x, y, z = table.unpack(GetEntityCoords(ped))
 
        -- Calcola distanza da più punti ogni volta
        local dist1 = #(vector3(x, y, z) - punto1)
        local dist2 = #(vector3(x, y, z) - punto2)
        local dist3 = #(vector3(x, y, z) - punto3)
 
        if dist1 < 10 then
            -- ...
        end
    end
end)
 
-- OTTIMIZZATO: usa variabili locali, calcola solo serve
local puntiInteresse = {
    { x = 100, y = 200, z = 30, raggio = 10, label = "Punto A" },
    { x = 500, y = 600, z = 40, raggio = 10, label = "Punto B" },
    { x = -300, y = -400, z = 50, raggio = 10, label = "Punto C" },
}
 
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(500) -- check meno frequente
 
        local ped = PlayerPedId()
        local coords = GetEntityCoords(ped)
        local px, py, pz = coords.x, coords.y, coords.z
 
        for _, punto in ipairs(puntiInteresse) do
            local dx, dy, dz = px - punto.x, py - punto.y, pz - punto.z
            local dist = math.sqrt(dx*dx + dy*dy + dz*dz)
 
            if dist < punto.raggio then
                print("Vicino a:", punto.label)
            end
        end
    end
end)

Distance Check

-- Utility distanza veloce (senza sqrt se non serve)
local function DistanceSquared(x1, y1, z1, x2, y2, z2)
    local dx = x2 - x1
    local dy = y2 - y1
    local dz = z2 - z1
    return dx*dx + dy*dy + dz*dz
end
 
local function IsVicino(coords, target, raggio)
    return DistanceSquared(coords.x, coords.y, coords.z,
        target.x, target.y, target.z) < raggio * raggio
end
 
-- Uso: DistanceSquared evita il sqrt() costoso se non serve
Citizen.CreateThread(function()
    local centro = vector3(0, 0, 0)
    local raggio = 50
 
    while true do
        Citizen.Wait(1000)
 
        local ped = PlayerPedId()
        local coords = GetEntityCoords(ped)
 
        if IsVicino(coords, centro, raggio) then
            print("Sei nel raggio di 50m dal centro")
        end
    end
end)

Evitare String Manipulation Pesante

-- DA EVITARE: concatenazione in loop
local function GeneraTestoLungo(items)
    local risultato = ""
    for _, item in ipairs(items) do
        risultato = risultato .. item.nome .. " x" .. item.quantita .. "\n"
    end
    return risultato
end
 
-- MEGLIO: table.concat
local function GeneraTestoLungo(items)
    local parti = {}
    for _, item in ipairs(items) do
        parti[#parti + 1] = item.nome .. " x" .. item.quantita
    end
    return table.concat(parti, "\n")
end
 
-- DA EVITARE: pattern regex complessi in loop
for _, linea in ipairs(molteLinee) do
    local match = string.match(linea, "complesso%d+(%a+)%d+")
end
 
-- MEGLIO: operazioni più semplici o pre-compilate
local pattern = string.find -- pattern semplice

Debug con Print

-- client.lua — print di base
print("Questo appare nella console F8 del client")
 
-- Con colori (codici ^0-^9)
print("^2Verde")      -- colore 2 = verde
print("^1Rosso")       -- colore 1 = rosso
print("^3Giallo")      -- colore 3 = giallo
print("^4Blu")         -- colore 4 = blu
print("^5Ciano")       -- colore 5 = ciano
print("^6Viola")       -- colore 6 = viola
print("^7Bianco")      -- colore 7 = bianco (default)
print("^8Arancione")   -- colore 8 = arancione
print("^9Grigio")      -- colore 9 = grigio
-- server.lua — print sul server console
print("^2[INFO] ^7Risorsa avviata")
print("^1[ERRORE] ^7Qualcosa è andato storto")
print("^8[WARN] ^7Attenzione a questa cosa")
 
-- Debug con source
AddEventHandler("playerJoining", function()
    print(string.format(
        "^5[DEBUG] ^7Giocatore ^3%s^7 (#^6%d^7) sta entrando",
        GetPlayerName(source), source
    ))
end)
-- Lua non ha un print per tabelle built-in
local function PrintTable(t, indent)
    indent = indent or 0
    local prefix = string.rep("  ", indent)
 
    for k, v in pairs(t) do
        if type(v) == "table" then
            print(prefix .. tostring(k) .. ":")
            PrintTable(v, indent + 1)
        else
            print(prefix .. tostring(k) .. ": " .. tostring(v))
        end
    end
end
 
-- Uso
local dati = { nome = "Marco", denaro = 5000, pos = { x = 100, y = 200, z = 30 } }
PrintTable(dati)
 
-- In FiveM c'è anche json.encode per debug veloce
print(json.encode(dati)) -- converte in JSON

Debug Output e Log su File

Server log

FiveM scrive automaticamente i print() del server in logs/server_log.txt.

Log custom

-- server.lua — log su file
local function LogToFile(messaggio, livello)
    livello = livello or "INFO"
    local timestamp = os.date("%Y-%m-%d %H:%M:%S")
    local riga = string.format("[%s] [%s] %s", timestamp, livello, messaggio)
    print(riga)
 
    -- Se vuoi scrivere su file specifico (usa con cautela!)
    -- local file = io.open("resources/mia_risorsa/log.txt", "a")
    -- if file then
    --     file:write(riga .. "\n")
    --     file:close()
    -- end
end

Debug toggle

TIP

Un flag Config.Debug ti permette di attivare o disattivare tutti i debug print con una singola modifica. Ricorda di impostarlo a false prima del deploy in produzione.

-- config.lua
Config.Debug = false -- true per debug mode
 
-- client.lua
local function DebugPrint(...)
    if Config.Debug then
        local args = { ... }
        for i, v in ipairs(args) do
            args[i] = tostring(v)
        end
        print("^5[DEBUG] ^7" .. table.concat(args, " "))
    end
end
 
-- Uso
DebugPrint("Posizione giocatore:", GetEntityCoords(PlayerPedId()))
-- Non stampa niente se Config.Debug = false

Uso di pcall per Error Handling Robusto

-- Proteggi blocchi critici con pcall
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(0)
 
        local successo, errore = pcall(function()
            local ped = PlayerPedId()
            local coords = GetEntityCoords(ped)
            local veicolo = GetVehiclePedIsIn(ped, false)
 
            if veicolo ~= 0 then
                local modello = GetEntityModel(veicolo)
                local carburante = GetVehicleFuelLevel(veicolo)
                local portiere = GetVehicleDoorLockStatus(veicolo)
 
                -- Calcoli vari...
            end
        end)
 
        if not successo then
            print("^1[ERRORE CRITICO] ^7" .. tostring(errore))
            -- Non far crashare il thread
        end
    end
end)

Error Handler Centralizzato

-- error_handler/client.lua
local ErrorCount = {}
local ErrorCooldown = {}
 
local function SafeCall(fn, ...)
    local args = { ... }
    local successo, errore = pcall(fn, table.unpack(args))
 
    if not successo then
        local trace = debug.traceback()
        local chiave = tostring(errore)
 
        -- Cooldown per evitare spam di errori identici
        local now = GetGameTimer()
        if ErrorCooldown[chiave] and now - ErrorCooldown[chiave] < 5000 then
            return false
        end
        ErrorCooldown[chiave] = now
 
        ErrorCount[chiave] = (ErrorCount[chiave] or 0) + 1
 
        print(string.format(
            "^1[ERRORE #%d] ^7%s\n^5%s",
            ErrorCount[chiave], tostring(errore), trace
        ))
    end
 
    return successo
end
 
-- Uso
SafeCall(function()
    -- codice che potrebbe fallire
    error("test errore")
end)

pcall con chiamate native rischiose

-- Alcune native possono crashare se chiamate con parametri invalidi
local function SafeGetEntityCoords(entity)
    local successo, coords = pcall(GetEntityCoords, entity)
    if successo and coords then
        return coords
    end
    return vector3(0, 0, 0)
end
 
local function SafeDoesEntityExist(entity)
    local successo, esiste = pcall(DoesEntityExist, entity)
    return successo and esiste or false
end

Liste di Controllo Eventi

Resmon

F8 → resmon 1   mostra dettaglio per risorsa
F8 → resmon 0   nasconde

Cosa cercare in resmon:

  • CPU ms alti (3+) → risorsa da ottimizzare
  • Native calls alti → troppe chiamate, batch o riduci
  • Memory crescente → memory leak (tabelle globali che crescono)

Profiler

F8 → profiler
F8 → profiler record    avvia registrazione
... gioca per 30 secondi ...
F8 → profiler stop      ferma e mostra report

Il profiler mostra:

  • Numero di chiamate per ogni native
  • Tempo speso in ogni native
  • Eventuali native chiamate inutilmente

Checklist di Ottimizzazione

flowchart TD
    A[Ho un problema di performance?] --> B[Usa resmon]
    B --> C[Quale risorsa consuma più CPU?]
    C --> D{Wait è ottimale?}
    D -->|No| E[Aumenta Wait\nUsa Wait condizionale]
    D -->|Sì| F{Chiamate native eccessive?}
    F -->|Sì| G[Raggruppa native\nUsa cache locale]
    F -->|No| H{Manipolazione stringhe?}
    H -->|Sì| I[Usa table.concat\nevita pattern in loop]
    H -->|No| J{Memory leak?}
    J -->|Sì| K[Cancella tabelle globali\nRimuovi event listener inutilizzati]
    J -->|No| L[Usa profiler per approfondire]

Lista di Controllo Rapida

  • Ogni thread ha un Wait() appropriato?
  • Usi Wait(0) solo quando necessario (input, UI)?
  • I loop cambiano Wait in base allo stato?
  • Eviti chiamate native inutili?
  • Usi cache locale per dati che non cambiano spesso?
  • table.concat invece di .. per stringhe lunghe?
  • pcall attorno a codice critico?
  • Controlli DoesEntityExist prima di usare entità?
  • Usi ModelRequest + HasModelLoaded prima di creare entità?
  • Hai disattivato debug in produzione (Config.Debug = false)?
  • resmon mostra tutto sotto 1-2ms per risorsa?
  • Eventi registrati hanno AddEventHandler dopo RegisterNetEvent?
  • Rimuovi event listener temporanei con RemoveEventHandler?
  • Non fai query SQL in loop stretti?
  • DistanceSquared invece di #(vector - vector) per confronti veloci?

Strumenti Aggiuntivi

Strumento di terze parti

StrumentoDescrizione
cfx-riimTabella Cheat Engine per debug FiveM
FiveM Server BrowserVedi CPU/RAM del server
Chrome DevTools (F8 → _devTools)Debug NUI, rete, console

Aliases utili per la console F8

Puoi creare alias nei tuoi script per debug rapido:

-- client.lua — comandi debug
RegisterCommand("debugme", function()
    local ped = PlayerPedId()
    local coords = GetEntityCoords(ped)
    local veicolo = GetVehiclePedIsIn(ped, false)
 
    print("=== DEBUG INFO ===")
    print("Ped:", ped)
    print("Pos:", string.format("%.2f, %.2f, %.2f", coords.x, coords.y, coords.z))
    print("In veicolo:", veicolo ~= 0)
    print("Salute:", GetEntityHealth(ped))
    print("Armatura:", GetPedArmour(ped))
    print("==================")
end, false)
 
RegisterCommand("nui_debug", function()
    -- Forza apertura DevTools NUI
    ExecuteCommand("_devTools")
end, false)

QUESTION

Riflessione: Hai eseguito resmon sulla tua risorsa? Ogni risorsa dovrebbe stare sotto 1-2ms per frame. Se vedi picchi, controlla i loop con Wait insufficiente o chiamate native eccessive.


Torna alla: Indice Generale