Debugging & Testing
Complete guide to debugging, testing, and optimizing FiveM resources.
Effective debugging and testing are crucial for developing reliable FiveM resources. This guide covers tools, techniques, and best practices for identifying and fixing issues.
Debugging Tools
Console Logging
-- Basic logging
print('Debug message')
print('Player data:', json.encode(playerData))
-- Conditional logging
local DEBUG_MODE = true
function DebugLog(message, ...)
if DEBUG_MODE then
print(string.format('[DEBUG] ' .. message, ...))
end
end
-- Usage
DebugLog('Player %s joined with money: %d', playerName, playerMoney)
Citizen.Trace
-- More detailed logging
Citizen.Trace('Resource started successfully\n')
-- Trace with formatting
function TraceLog(level, message, ...)
local formatted = string.format(message, ...)
Citizen.Trace(string.format('[%s] %s\n', level, formatted))
end
-- Usage
TraceLog('INFO', 'Player connected: %s', playerName)
TraceLog('ERROR', 'Database connection failed: %s', error)
Resource Monitor (resmon)
-- In-game command to check resource performance
-- Type 'resmon' in F8 console to see:
-- - CPU usage per resource
-- - Memory usage
-- - Tick time
-- - Thread count
-- Optimize based on resmon data
Citizen.CreateThread(function()
while true do
-- Bad: Runs every frame (high CPU usage)
Citizen.Wait(0)
DoExpensiveOperation()
end
end)
-- Good: Runs every 100ms
Citizen.CreateThread(function()
while true do
Citizen.Wait(100)
DoExpensiveOperation()
end
end)
Profiler
-- Built-in profiler for detailed performance analysis
-- Type 'profiler' in F8 console
-- Profile specific functions
function ExpensiveFunction()
profiler.startTimer('ExpensiveFunction')
-- Your code here
for i = 1, 1000 do
DoSomething()
end
profiler.stopTimer('ExpensiveFunction')
end
Client-Side Debugging
Drawing Debug Information
local DEBUG_MODE = true
function DrawDebugInfo()
if not DEBUG_MODE then return end
local playerPed = PlayerPedId()
local coords = GetEntityCoords(playerPed)
local heading = GetEntityHeading(playerPed)
local vehicle = GetVehiclePedIsIn(playerPed, false)
-- Draw coordinates
DrawText2D(0.01, 0.01, string.format('Coords: %.2f, %.2f, %.2f', coords.x, coords.y, coords.z))
DrawText2D(0.01, 0.04, string.format('Heading: %.2f', heading))
-- Draw vehicle info if in vehicle
if vehicle ~= 0 then
local speed = GetEntitySpeed(vehicle) * 2.236936 -- Convert to MPH
local model = GetEntityModel(vehicle)
DrawText2D(0.01, 0.07, string.format('Vehicle: %s', GetDisplayNameFromVehicleModel(model)))
DrawText2D(0.01, 0.10, string.format('Speed: %.2f MPH', speed))
end
end
function DrawText2D(x, y, text)
SetTextFont(0)
SetTextProportional(1)
SetTextScale(0.25, 0.25)
SetTextColour(255, 255, 255, 255)
SetTextDropshadow(0, 0, 0, 0, 255)
SetTextEdge(2, 0, 0, 0, 150)
SetTextDropShadow()
SetTextOutline()
SetTextEntry("STRING")
AddTextComponentString(text)
DrawText(x, y)
end
-- Main debug loop
Citizen.CreateThread(function()
while true do
Citizen.Wait(0)
DrawDebugInfo()
end
end)
Debug Commands
-- Create debug commands for testing
if DEBUG_MODE then
RegisterCommand('debugtp', function(source, args)
if #args >= 3 then
local x, y, z = tonumber(args[1]), tonumber(args[2]), tonumber(args[3])
SetEntityCoords(PlayerPedId(), x, y, z)
print('Teleported to:', x, y, z)
end
end)
RegisterCommand('debugveh', function(source, args)
local model = args[1] or 'adder'
local hash = GetHashKey(model)
RequestModel(hash)
while not HasModelLoaded(hash) do
Citizen.Wait(1)
end
local coords = GetEntityCoords(PlayerPedId())
local vehicle = CreateVehicle(hash, coords.x, coords.y, coords.z, 0.0, true, false)
TaskWarpPedIntoVehicle(PlayerPedId(), vehicle, -1)
SetModelAsNoLongerNeeded(hash)
print('Spawned vehicle:', model)
end)
RegisterCommand('debugmoney', function(source, args)
local amount = tonumber(args[1]) or 1000
TriggerServerEvent('debug:addMoney', amount)
print('Added money:', amount)
end)
end
Visual Debug Helpers
local debugMarkers = {}
function AddDebugMarker(coords, color, duration)
table.insert(debugMarkers, {
coords = coords,
color = color or {255, 0, 0, 200},
endTime = GetGameTimer() + (duration or 5000)
})
end
function DrawDebugMarkers()
for i = #debugMarkers, 1, -1 do
local marker = debugMarkers[i]
if GetGameTimer() > marker.endTime then
table.remove(debugMarkers, i)
else
DrawMarker(1, marker.coords.x, marker.coords.y, marker.coords.z,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
1.0, 1.0, 1.0,
marker.color[1], marker.color[2], marker.color[3], marker.color[4],
false, true, 2, false, false, false, false)
end
end
end
-- Usage
AddDebugMarker(GetEntityCoords(PlayerPedId()), {0, 255, 0, 200}, 10000)
Citizen.CreateThread(function()
while true do
Citizen.Wait(0)
if DEBUG_MODE then
DrawDebugMarkers()
end
end
end)
Server-Side Debugging
Error Handling
function SafeExecute(func, ...)
local success, result = pcall(func, ...)
if not success then
print('^1[ERROR]^7 Function execution failed:', result)
print('^3[DEBUG]^7 Stack trace:', debug.traceback())
return false, result
end
return true, result
end
-- Usage
local success, data = SafeExecute(function()
return json.decode(jsonString)
end)
if not success then
print('Failed to decode JSON:', data)
return
end
Database Debugging
-- MySQL debugging
function ExecuteQueryWithDebug(query, parameters, callback)
local startTime = GetGameTimer()
MySQL.Async.fetchAll(query, parameters, function(result)
local endTime = GetGameTimer()
local executionTime = endTime - startTime
print(string.format('[DB] Query executed in %dms: %s', executionTime, query))
if executionTime > 1000 then
print('^3[WARNING]^7 Slow query detected!')
end
if callback then
callback(result)
end
end)
end
-- Usage
ExecuteQueryWithDebug('SELECT * FROM users WHERE identifier = ?', {identifier}, function(result)
print('User data:', json.encode(result))
end)
Player State Debugging
local DEBUG_PLAYERS = {}
function StartPlayerDebugging(src)
DEBUG_PLAYERS[src] = {
events = {},
lastSeen = GetGameTimer(),
totalEvents = 0
}
end
function LogPlayerEvent(src, eventName, data)
if DEBUG_PLAYERS[src] then
table.insert(DEBUG_PLAYERS[src].events, {
event = eventName,
data = data,
timestamp = GetGameTimer()
})
DEBUG_PLAYERS[src].totalEvents = DEBUG_PLAYERS[src].totalEvents + 1
DEBUG_PLAYERS[src].lastSeen = GetGameTimer()
-- Keep only last 50 events
if #DEBUG_PLAYERS[src].events > 50 then
table.remove(DEBUG_PLAYERS[src].events, 1)
end
end
end
function GetPlayerDebugInfo(src)
return DEBUG_PLAYERS[src]
end
-- Debug command
RegisterCommand('playerdebug', function(source, args)
if source == 0 then -- Console only
local playerId = tonumber(args[1])
if playerId and DEBUG_PLAYERS[playerId] then
local info = DEBUG_PLAYERS[playerId]
print(string.format('Player %d - Total Events: %d, Last Seen: %d ms ago',
playerId, info.totalEvents, GetGameTimer() - info.lastSeen))
print('Recent events:')
for _, event in ipairs(info.events) do
print(string.format(' %s: %s (%d ms ago)',
event.event, json.encode(event.data),
GetGameTimer() - event.timestamp))
end
end
end
end, true)
Testing Strategies
Unit Testing
-- Simple unit testing framework
local Tests = {}
function Tests.assertEqual(actual, expected, message)
if actual ~= expected then
error(string.format('Assertion failed: %s. Expected %s, got %s',
message or 'values should be equal', tostring(expected), tostring(actual)))
end
end
function Tests.assertTrue(condition, message)
if not condition then
error(string.format('Assertion failed: %s', message or 'condition should be true'))
end
end
function Tests.runTest(name, testFunc)
print(string.format('Running test: %s', name))
local success, error = pcall(testFunc)
if success then
print(string.format('✓ Test passed: %s', name))
else
print(string.format('✗ Test failed: %s - %s', name, error))
end
end
-- Example tests
Tests.runTest('Money calculation', function()
local player = {money = 1000}
local result = AddMoney(player, 500)
Tests.assertEqual(result.money, 1500, 'Money should be added correctly')
end)
Tests.runTest('Distance calculation', function()
local distance = GetDistance({x=0, y=0, z=0}, {x=3, y=4, z=0})
Tests.assertEqual(distance, 5.0, 'Distance should be calculated correctly')
end)
Integration Testing
-- Test player joining process
function TestPlayerJoin()
local testIdentifier = 'test:player123'
local testName = 'TestPlayer'
-- Simulate player join
local success = SafeExecute(function()
local playerData = CreateNewPlayer(testIdentifier, testName)
Tests.assertTrue(playerData ~= nil, 'Player data should be created')
Tests.assertEqual(playerData.name, testName, 'Player name should match')
Tests.assertEqual(playerData.money, 5000, 'Default money should be 5000')
-- Test data persistence
SavePlayerData(playerData.id, playerData)
local loaded = LoadPlayerData(testIdentifier)
Tests.assertEqual(loaded.name, testName, 'Loaded data should match saved data')
-- Cleanup test data
DeleteTestPlayer(testIdentifier)
end)
if not success then
print('Player join test failed')
else
print('Player join test passed')
end
end
Load Testing
-- Simulate multiple players for performance testing
function SimulatePlayerLoad(playerCount)
print(string.format('Simulating load for %d players...', playerCount))
local startTime = GetGameTimer()
for i = 1, playerCount do
local fakeId = 9000 + i
local identifier = 'test:loadtest' .. i
-- Simulate player data operations
CreateTestPlayer(fakeId, identifier)
UpdatePlayerMoney(fakeId, math.random(1000, 10000))
SavePlayerData(fakeId)
end
local endTime = GetGameTimer()
local totalTime = endTime - startTime
print(string.format('Load test completed in %d ms (%.2f ms per player)',
totalTime, totalTime / playerCount))
-- Cleanup
for i = 1, playerCount do
local fakeId = 9000 + i
DeleteTestPlayer(fakeId)
end
end
Performance Monitoring
Resource Performance Tracking
local PerformanceTracker = {
metrics = {},
startTimes = {}
}
function PerformanceTracker.start(name)
PerformanceTracker.startTimes[name] = GetGameTimer()
end
function PerformanceTracker.stop(name)
local startTime = PerformanceTracker.startTimes[name]
if not startTime then return end
local duration = GetGameTimer() - startTime
if not PerformanceTracker.metrics[name] then
PerformanceTracker.metrics[name] = {
totalTime = 0,
calls = 0,
maxTime = 0,
minTime = math.huge
}
end
local metric = PerformanceTracker.metrics[name]
metric.totalTime = metric.totalTime + duration
metric.calls = metric.calls + 1
metric.maxTime = math.max(metric.maxTime, duration)
metric.minTime = math.min(metric.minTime, duration)
PerformanceTracker.startTimes[name] = nil
end
function PerformanceTracker.getReport()
for name, metric in pairs(PerformanceTracker.metrics) do
local avgTime = metric.totalTime / metric.calls
print(string.format('%s: %d calls, avg: %.2fms, max: %.2fms, min: %.2fms',
name, metric.calls, avgTime, metric.maxTime, metric.minTime))
end
end
-- Usage
function ExpensiveFunction()
PerformanceTracker.start('ExpensiveFunction')
-- Your code here
Citizen.Wait(10)
PerformanceTracker.stop('ExpensiveFunction')
end
-- Generate report command
RegisterCommand('perfreport', function()
PerformanceTracker.getReport()
end, true)
Memory Monitoring
local MemoryMonitor = {
lastCheck = 0,
checkInterval = 30000, -- 30 seconds
threshold = 100 -- MB
}
function MemoryMonitor.check()
local now = GetGameTimer()
if now - MemoryMonitor.lastCheck < MemoryMonitor.checkInterval then
return
end
local memUsage = collectgarbage('count') / 1024 -- Convert to MB
if memUsage > MemoryMonitor.threshold then
print(string.format('^3[WARNING]^7 High memory usage: %.2f MB', memUsage))
-- Force garbage collection
collectgarbage('collect')
local afterGC = collectgarbage('count') / 1024
print(string.format('^2[INFO]^7 Memory after GC: %.2f MB (freed %.2f MB)',
afterGC, memUsage - afterGC))
end
MemoryMonitor.lastCheck = now
end
-- Monitor memory in main thread
Citizen.CreateThread(function()
while true do
Citizen.Wait(1000)
MemoryMonitor.check()
end
end)
Common Issues and Solutions
Event Handling Issues
-- Problem: Event not firing
-- Solution: Check event registration
-- Wrong
AddEventHandler('myevent', function() end) -- Missing RegisterNetEvent
-- Correct
RegisterNetEvent('myevent')
AddEventHandler('myevent', function() end)
Threading Issues
-- Problem: Blocking main thread
-- Solution: Use proper waits
-- Wrong
Citizen.CreateThread(function()
while true do
-- No wait - blocks main thread
DoSomething()
end
end)
-- Correct
Citizen.CreateThread(function()
while true do
Citizen.Wait(100) -- Proper wait
DoSomething()
end
end)
Memory Leaks
-- Problem: Not cleaning up references
local players = {}
AddEventHandler('playerDropped', function()
local src = source
-- Clean up player data
players[src] = nil
end)
-- Problem: Infinite table growth
local eventLog = {}
RegisterNetEvent('logevent')
AddEventHandler('logevent', function(data)
table.insert(eventLog, data)
-- Solution: Limit table size
if #eventLog > 1000 then
table.remove(eventLog, 1)
end
end)
Database Connection Issues
-- Retry mechanism for database operations
function RetryDatabaseOperation(operation, maxRetries, delay)
maxRetries = maxRetries or 3
delay = delay or 1000
local function attempt(retryCount)
local success, result = pcall(operation)
if success then
return result
else
print(string.format('Database operation failed (attempt %d/%d): %s',
retryCount, maxRetries, result))
if retryCount < maxRetries then
Citizen.Wait(delay)
return attempt(retryCount + 1)
else
error('Database operation failed after ' .. maxRetries .. ' attempts')
end
end
end
return attempt(1)
end
-- Usage
RetryDatabaseOperation(function()
MySQL.Sync.execute('INSERT INTO users VALUES (?, ?)', {name, identifier})
end, 3, 2000)