← Volver al listado de tecnologías

Debugging y Profiling Profesional

Por: Artiko
defolddebuggingprofilingoptimizaciontesting

Debugging y Profiling Profesional

El debugging y profiling son habilidades esenciales para crear juegos de calidad profesional. En esta lección aprenderás técnicas avanzadas para identificar, diagnosticar y resolver problemas de rendimiento y bugs complejos.

🔍 Sistema de Debugging Avanzado

Logger Personalizado

-- debug_logger.script
local Logger = {}

-- Niveles de log
local LOG_LEVELS = {
    TRACE = 1,
    DEBUG = 2,
    INFO = 3,
    WARN = 4,
    ERROR = 5,
    FATAL = 6
}

local LOG_COLORS = {
    TRACE = "#808080",
    DEBUG = "#00FF00",
    INFO = "#0080FF",
    WARN = "#FFFF00",
    ERROR = "#FF8000",
    FATAL = "#FF0000"
}

function init(self)
    self.log_level = LOG_LEVELS.DEBUG
    self.log_file = nil
    self.log_buffer = {}
    self.max_buffer_size = 1000
    self.session_id = os.time()

    -- Configurar archivo de log
    if sys.get_sys_info().system_name ~= "HTML5" then
        local log_path = sys.get_save_file("defold_game", "debug_" .. self.session_id .. ".log")
        self.log_file = io.open(log_path, "w")
    end

    self:info("Logger initialized - Session: " .. self.session_id)
end

local function format_message(level, category, message, context)
    local timestamp = os.date("%Y-%m-%d %H:%M:%S")
    local formatted = string.format("[%s] [%s] [%s] %s",
                                   timestamp, level, category or "GENERAL", message)

    if context then
        formatted = formatted .. " | Context: " .. pprint.pformat(context)
    end

    return formatted
end

local function add_to_buffer(self, level, category, message, context)
    local log_entry = {
        timestamp = os.time(),
        level = level,
        category = category,
        message = message,
        context = context,
        stack_trace = debug.traceback()
    }

    table.insert(self.log_buffer, log_entry)

    -- Mantener buffer limitado
    if #self.log_buffer > self.max_buffer_size then
        table.remove(self.log_buffer, 1)
    end
end

local function write_log(self, level, category, message, context)
    if LOG_LEVELS[level] < self.log_level then
        return
    end

    local formatted = format_message(level, category, message, context)

    -- Escribir a consola con color
    local color = LOG_COLORS[level] or "#FFFFFF"
    print(formatted)

    -- Escribir a archivo si está disponible
    if self.log_file then
        self.log_file:write(formatted .. "\n")
        self.log_file:flush()
    end

    -- Añadir a buffer
    add_to_buffer(self, level, category, message, context)

    -- Enviar a sistema de analytics si es error
    if level == "ERROR" or level == "FATAL" then
        self:send_error_analytics(level, category, message, context)
    end
end

function Logger.trace(self, category, message, context)
    write_log(self, "TRACE", category, message, context)
end

function Logger.debug(self, category, message, context)
    write_log(self, "DEBUG", category, message, context)
end

function Logger.info(self, category, message, context)
    write_log(self, "INFO", category, message, context)
end

function Logger.warn(self, category, message, context)
    write_log(self, "WARN", category, message, context)
end

function Logger.error(self, category, message, context)
    write_log(self, "ERROR", category, message, context)
end

function Logger.fatal(self, category, message, context)
    write_log(self, "FATAL", category, message, context)
end

-- Función para capturar stack trace detallado
local function capture_stack_trace(self, skip_levels)
    skip_levels = skip_levels or 2
    local stack = {}

    for level = skip_levels, 10 do
        local info = debug.getinfo(level, "Sln")
        if not info then break end

        table.insert(stack, {
            source = info.source,
            line = info.currentline,
            name = info.name or "unknown",
            func_type = info.namewhat
        })
    end

    return stack
end

-- Sistema de breakpoints condicionales
local function create_conditional_breakpoint(self, condition_func, action_func)
    return function(...)
        if condition_func(...) then
            local context = {
                args = {...},
                stack = capture_stack_trace(self),
                timestamp = os.time()
            }

            self:debug("BREAKPOINT", "Conditional breakpoint triggered", context)

            if action_func then
                action_func(context)
            end
        end
    end
end

return Logger

Crash Reporter

-- crash_reporter.script
local CrashReporter = {}

function init(self)
    self.crash_data = {}
    self.auto_report = true
    self.report_endpoint = "https://api.example.com/crashes"

    -- Configurar manejo de errores globales
    self:setup_error_handler()
end

local function setup_error_handler(self)
    -- Capturar errores Lua
    local original_error = _G.error

    _G.error = function(message, level)
        level = level or 1

        local crash_info = {
            type = "lua_error",
            message = tostring(message),
            stack_trace = debug.traceback("", level + 1),
            timestamp = os.time(),
            game_state = self:capture_game_state(),
            device_info = self:capture_device_info(),
            memory_info = self:capture_memory_info()
        }

        self:record_crash(crash_info)
        original_error(message, level + 1)
    end

    -- Capturar assertions fallidas
    local original_assert = _G.assert

    _G.assert = function(condition, message)
        if not condition then
            local crash_info = {
                type = "assertion_failed",
                message = message or "Assertion failed",
                stack_trace = debug.traceback(),
                timestamp = os.time(),
                game_state = self:capture_game_state()
            }

            self:record_crash(crash_info)
        end

        return original_assert(condition, message)
    end
end

local function capture_game_state(self)
    return {
        scene = msg.url().path,
        fps = sys.get_engine_info().frame_count,
        uptime = socket.gettime(),
        input_state = self:get_current_input_state(),
        audio_state = self:get_audio_state(),
        network_state = self:get_network_state()
    }
end

local function capture_device_info(self)
    local sys_info = sys.get_sys_info()
    return {
        platform = sys_info.system_name,
        device_model = sys_info.device_model,
        language = sys_info.language,
        territory = sys_info.territory,
        gmt_offset = sys_info.gmt_offset,
        device_ident = sys_info.device_ident,
        user_agent = sys_info.user_agent
    }
end

local function capture_memory_info(self)
    return {
        lua_memory = collectgarbage("count"),
        texture_memory = profiler.get_memory_usage().texture_memory,
        total_memory = sys.get_engine_info().heap_size
    }
end

local function record_crash(self, crash_info)
    -- Guardar crash localmente
    local crash_id = "crash_" .. os.time() .. "_" .. math.random(1000, 9999)
    crash_info.crash_id = crash_id

    table.insert(self.crash_data, crash_info)

    -- Guardar en archivo
    local crash_file = sys.get_save_file("defold_game", crash_id .. ".json")
    local file = io.open(crash_file, "w")
    if file then
        file:write(json.encode(crash_info))
        file:close()
    end

    -- Enviar automáticamente si está habilitado
    if self.auto_report then
        self:send_crash_report(crash_info)
    end

    print("CRASH RECORDED:", crash_id)
end

local function send_crash_report(self, crash_info)
    local data = json.encode(crash_info)

    http.request(self.report_endpoint, "POST", function(self, id, response)
        if response.status == 200 then
            print("Crash report sent successfully:", crash_info.crash_id)
        else
            print("Failed to send crash report:", response.status)
        end
    end, nil, nil, {["Content-Type"] = "application/json"}, data)
end

return CrashReporter

📊 Sistema de Profiling Detallado

Performance Profiler

-- performance_profiler.script
local Profiler = {}

local PROFILER_CATEGORIES = {
    RENDER = "render",
    PHYSICS = "physics",
    SCRIPTS = "scripts",
    AUDIO = "audio",
    NETWORK = "network",
    INPUT = "input"
}

function init(self)
    self.enabled = true
    self.frame_data = {}
    self.category_timers = {}
    self.memory_samples = {}
    self.fps_history = {}
    self.profiling_overhead = 0

    -- Configurar sampling
    self.sample_interval = 1/60 -- 60 FPS
    self.max_samples = 3600 -- 1 minuto de datos

    for category, _ in pairs(PROFILER_CATEGORIES) do
        self.category_timers[category] = {
            start_time = 0,
            total_time = 0,
            call_count = 0,
            min_time = math.huge,
            max_time = 0
        }
    end
end

-- Función para medir tiempo de ejecución
local function start_timer(self, category)
    if not self.enabled then return end

    local timer = self.category_timers[category]
    if timer then
        timer.start_time = socket.gettime()
    end
end

local function end_timer(self, category)
    if not self.enabled then return end

    local current_time = socket.gettime()
    local timer = self.category_timers[category]

    if timer and timer.start_time > 0 then
        local elapsed = current_time - timer.start_time
        timer.total_time = timer.total_time + elapsed
        timer.call_count = timer.call_count + 1
        timer.min_time = math.min(timer.min_time, elapsed)
        timer.max_time = math.max(timer.max_time, elapsed)
        timer.start_time = 0
    end
end

-- Decorator para funciones
local function profile_function(self, category, func)
    return function(...)
        start_timer(self, category)
        local results = {func(...)}
        end_timer(self, category)
        return unpack(results)
    end
end

-- Muestreo de memoria
local function sample_memory(self)
    local memory_info = {
        timestamp = socket.gettime(),
        lua_memory = collectgarbage("count") * 1024,
        texture_memory = render.get_render_stats().texture_memory or 0,
        total_objects = game_object_count() or 0
    }

    table.insert(self.memory_samples, memory_info)

    if #self.memory_samples > self.max_samples then
        table.remove(self.memory_samples, 1)
    end
end

-- Análisis de hotspots
local function analyze_hotspots(self)
    local hotspots = {}

    for category, timer in pairs(self.category_timers) do
        if timer.call_count > 0 then
            hotspots[category] = {
                total_time = timer.total_time,
                avg_time = timer.total_time / timer.call_count,
                min_time = timer.min_time,
                max_time = timer.max_time,
                call_count = timer.call_count,
                percentage = 0 -- Se calculará después
            }
        end
    end

    -- Calcular porcentajes
    local total_time = 0
    for _, data in pairs(hotspots) do
        total_time = total_time + data.total_time
    end

    for category, data in pairs(hotspots) do
        data.percentage = (data.total_time / total_time) * 100
    end

    return hotspots
end

-- Detección automática de problemas
local function detect_performance_issues(self)
    local issues = {}
    local hotspots = analyze_hotspots(self)

    -- Frame rate bajo
    local avg_fps = self:calculate_average_fps()
    if avg_fps < 50 then
        table.insert(issues, {
            type = "LOW_FPS",
            severity = "HIGH",
            description = string.format("Average FPS: %.1f", avg_fps)
        })
    end

    -- Hotspots problemáticos
    for category, data in pairs(hotspots) do
        if data.percentage > 30 then
            table.insert(issues, {
                type = "HOTSPOT",
                severity = "MEDIUM",
                category = category,
                description = string.format("%s consuming %.1f%% of frame time",
                                          category, data.percentage)
            })
        end
    end

    -- Memoria alta
    local current_memory = collectgarbage("count") * 1024
    if current_memory > 100 * 1024 * 1024 then -- 100MB
        table.insert(issues, {
            type = "HIGH_MEMORY",
            severity = "MEDIUM",
            description = string.format("Memory usage: %.1fMB", current_memory / (1024*1024))
        })
    end

    return issues
end

function update(self, dt)
    if not self.enabled then return end

    -- Muestrear FPS
    local fps = 1.0 / dt
    table.insert(self.fps_history, fps)

    if #self.fps_history > self.max_samples then
        table.remove(self.fps_history, 1)
    end

    -- Muestrear memoria cada segundo
    if #self.memory_samples == 0 or
       socket.gettime() - self.memory_samples[#self.memory_samples].timestamp >= 1.0 then
        sample_memory(self)
    end
end

-- Generar reporte
local function generate_report(self)
    local hotspots = analyze_hotspots(self)
    local issues = detect_performance_issues(self)

    local report = {
        timestamp = os.date("%Y-%m-%d %H:%M:%S"),
        session_duration = socket.gettime(),
        average_fps = self:calculate_average_fps(),
        memory_peak = self:calculate_peak_memory(),
        hotspots = hotspots,
        issues = issues,
        frame_samples = #self.fps_history,
        memory_samples = #self.memory_samples
    }

    return report
end

Profiler.start_timer = start_timer
Profiler.end_timer = end_timer
Profiler.profile_function = profile_function
Profiler.generate_report = generate_report

return Profiler

GPU Profiler

-- gpu_profiler.script
local GPUProfiler = {}

function init(self)
    self.render_stats = {}
    self.draw_call_history = {}
    self.texture_usage = {}
    self.shader_stats = {}

    -- Configurar hooks de render
    self:setup_render_hooks()
end

local function setup_render_hooks(self)
    -- Hook para interceptar draw calls
    local original_render_draw = render.draw

    render.draw = function(predicate, options)
        local start_time = socket.gettime()

        -- Obtener estadísticas antes del draw
        local stats_before = render.get_render_stats()

        -- Ejecutar draw call original
        local result = original_render_draw(predicate, options)

        -- Obtener estadísticas después del draw
        local stats_after = render.get_render_stats()
        local end_time = socket.gettime()

        -- Registrar estadísticas
        self:record_draw_call({
            predicate = tostring(predicate),
            duration = end_time - start_time,
            triangles_before = stats_before.triangles or 0,
            triangles_after = stats_after.triangles or 0,
            draw_calls_before = stats_before.draw_calls or 0,
            draw_calls_after = stats_after.draw_calls or 0
        })

        return result
    end
end

local function record_draw_call(self, call_data)
    call_data.timestamp = socket.gettime()
    call_data.triangles_drawn = call_data.triangles_after - call_data.triangles_before
    call_data.new_draw_calls = call_data.draw_calls_after - call_data.draw_calls_before

    table.insert(self.draw_call_history, call_data)

    -- Mantener historial limitado
    if #self.draw_call_history > 1000 then
        table.remove(self.draw_call_history, 1)
    end
end

local function analyze_render_performance(self)
    local total_draw_calls = 0
    local total_triangles = 0
    local total_time = 0
    local predicate_stats = {}

    for _, call in ipairs(self.draw_call_history) do
        total_draw_calls = total_draw_calls + call.new_draw_calls
        total_triangles = total_triangles + call.triangles_drawn
        total_time = total_time + call.duration

        -- Estadísticas por predicado
        if not predicate_stats[call.predicate] then
            predicate_stats[call.predicate] = {
                draw_calls = 0,
                triangles = 0,
                time = 0
            }
        end

        local pred_stat = predicate_stats[call.predicate]
        pred_stat.draw_calls = pred_stat.draw_calls + call.new_draw_calls
        pred_stat.triangles = pred_stat.triangles + call.triangles_drawn
        pred_stat.time = pred_stat.time + call.duration
    end

    return {
        total_draw_calls = total_draw_calls,
        total_triangles = total_triangles,
        total_render_time = total_time,
        average_triangles_per_call = total_triangles / math.max(total_draw_calls, 1),
        predicate_breakdown = predicate_stats
    }
end

-- Análisis de uso de texturas
local function analyze_texture_usage(self)
    local render_stats = render.get_render_stats()

    local texture_analysis = {
        current_memory = render_stats.texture_memory or 0,
        estimated_textures = render_stats.texture_count or 0,
        average_texture_size = 0
    }

    if texture_analysis.estimated_textures > 0 then
        texture_analysis.average_texture_size =
            texture_analysis.current_memory / texture_analysis.estimated_textures
    end

    return texture_analysis
end

-- Recomendaciones de optimización
local function generate_optimization_suggestions(self)
    local render_analysis = analyze_render_performance(self)
    local suggestions = {}

    -- Demasiados draw calls
    if render_analysis.total_draw_calls > 500 then
        table.insert(suggestions, {
            type = "BATCHING",
            priority = "HIGH",
            description = string.format("Consider batching: %d draw calls detected",
                                       render_analysis.total_draw_calls)
        })
    end

    -- Pocos triángulos por draw call
    if render_analysis.average_triangles_per_call < 100 then
        table.insert(suggestions, {
            type = "MESH_OPTIMIZATION",
            priority = "MEDIUM",
            description = string.format("Low triangle density: %.1f triangles per draw call",
                                       render_analysis.average_triangles_per_call)
        })
    end

    -- Análisis por predicado
    for predicate, stats in pairs(render_analysis.predicate_breakdown) do
        if stats.time > 0.005 then -- Más de 5ms
            table.insert(suggestions, {
                type = "SHADER_OPTIMIZATION",
                priority = "MEDIUM",
                predicate = predicate,
                description = string.format("Predicate '%s' consuming %.2fms per frame",
                                           predicate, stats.time * 1000)
            })
        end
    end

    return suggestions
end

return GPUProfiler

🧠 Memory Profiling

Garbage Collection Analyzer

-- gc_analyzer.script
local GCAnalyzer = {}

function init(self)
    self.gc_events = {}
    self.memory_snapshots = {}
    self.allocation_tracking = {}

    -- Configurar hooks de GC
    self:setup_gc_hooks()

    -- Configurar tracking de allocaciones
    self:setup_allocation_tracking()
end

local function setup_gc_hooks(self)
    -- Hook para collectgarbage
    local original_collectgarbage = _G.collectgarbage

    _G.collectgarbage = function(opt, arg)
        local start_time = socket.gettime()
        local memory_before = original_collectgarbage("count")

        local result = original_collectgarbage(opt, arg)

        local end_time = socket.gettime()
        local memory_after = original_collectgarbage("count")

        if opt == "collect" or opt == nil then
            self:record_gc_event({
                type = "manual_collect",
                duration = end_time - start_time,
                memory_before = memory_before,
                memory_after = memory_after,
                memory_freed = memory_before - memory_after
            })
        end

        return result
    end

    -- Monitoring automático de GC
    timer.delay(1.0, true, function()
        self:check_automatic_gc()
    end)
end

local function setup_allocation_tracking(self)
    -- Tracking de creación de tables
    local original_setmetatable = _G.setmetatable

    _G.setmetatable = function(table, metatable)
        if self.allocation_tracking.enabled then
            local info = debug.getinfo(2, "Sl")
            local location = string.format("%s:%d", info.source or "unknown", info.currentline or 0)

            if not self.allocation_tracking[location] then
                self.allocation_tracking[location] = 0
            end
            self.allocation_tracking[location] = self.allocation_tracking[location] + 1
        end

        return original_setmetatable(table, metatable)
    end
end

local function record_gc_event(self, event_data)
    event_data.timestamp = socket.gettime()
    table.insert(self.gc_events, event_data)

    -- Mantener historial limitado
    if #self.gc_events > 100 then
        table.remove(self.gc_events, 1)
    end

    print(string.format("GC Event: %s, Duration: %.2fms, Freed: %.1fKB",
                       event_data.type,
                       event_data.duration * 1000,
                       event_data.memory_freed))
end

local function check_automatic_gc(self)
    local current_memory = collectgarbage("count")
    local last_snapshot = self.memory_snapshots[#self.memory_snapshots]

    if last_snapshot and current_memory < last_snapshot.memory * 0.8 then
        -- Posible GC automático detectado
        self:record_gc_event({
            type = "automatic_collect",
            memory_before = last_snapshot.memory,
            memory_after = current_memory,
            memory_freed = last_snapshot.memory - current_memory,
            duration = 0 -- No podemos medir duración de GC automático
        })
    end

    -- Tomar snapshot de memoria
    table.insert(self.memory_snapshots, {
        timestamp = socket.gettime(),
        memory = current_memory
    })

    if #self.memory_snapshots > 60 then -- 1 minuto de snapshots
        table.remove(self.memory_snapshots, 1)
    end
end

-- Análisis de patrones de memoria
local function analyze_memory_patterns(self)
    if #self.memory_snapshots < 10 then
        return {error = "Insufficient data for analysis"}
    end

    local memory_values = {}
    for _, snapshot in ipairs(self.memory_snapshots) do
        table.insert(memory_values, snapshot.memory)
    end

    -- Calcular estadísticas
    local min_memory = math.min(unpack(memory_values))
    local max_memory = math.max(unpack(memory_values))
    local avg_memory = 0
    for _, value in ipairs(memory_values) do
        avg_memory = avg_memory + value
    end
    avg_memory = avg_memory / #memory_values

    -- Detectar memory leaks (tendencia creciente)
    local growth_rate = (memory_values[#memory_values] - memory_values[1]) /
                       (#memory_values - 1)

    -- Detectar fragmentación (variabilidad alta)
    local variance = 0
    for _, value in ipairs(memory_values) do
        variance = variance + (value - avg_memory) ^ 2
    end
    variance = variance / #memory_values
    local fragmentation_index = math.sqrt(variance) / avg_memory

    return {
        min_memory = min_memory,
        max_memory = max_memory,
        avg_memory = avg_memory,
        memory_range = max_memory - min_memory,
        growth_rate = growth_rate,
        fragmentation_index = fragmentation_index,
        potential_leak = growth_rate > 1.0, -- Más de 1KB por segundo
        high_fragmentation = fragmentation_index > 0.2
    }
end

-- Recomendaciones de optimización de memoria
local function generate_memory_recommendations(self)
    local analysis = analyze_memory_patterns(self)
    local recommendations = {}

    if analysis.potential_leak then
        table.insert(recommendations, {
            type = "MEMORY_LEAK",
            priority = "HIGH",
            description = string.format("Potential memory leak detected: %.1fKB/s growth rate",
                                       analysis.growth_rate)
        })
    end

    if analysis.high_fragmentation then
        table.insert(recommendations, {
            type = "FRAGMENTATION",
            priority = "MEDIUM",
            description = string.format("High memory fragmentation: %.1f%% variability",
                                       analysis.fragmentation_index * 100)
        })
    end

    -- Análisis de GC frecuencia
    local gc_frequency = #self.gc_events / (socket.gettime() / 60) -- GCs por minuto
    if gc_frequency > 10 then
        table.insert(recommendations, {
            type = "GC_FREQUENCY",
            priority = "MEDIUM",
            description = string.format("High GC frequency: %.1f collections per minute",
                                       gc_frequency)
        })
    end

    return recommendations
end

return GCAnalyzer

🎮 Debug UI en Runtime

Console de Debug

-- debug_console.script
local DebugConsole = {}

function init(self)
    self.visible = false
    self.commands = {}
    self.command_history = {}
    self.output_buffer = {}
    self.input_text = ""
    self.cursor_pos = 0

    -- Registrar comandos por defecto
    self:register_default_commands()

    -- Configurar GUI
    self:setup_gui()
end

local function register_default_commands(self)
    -- Comando help
    self:register_command("help", function(args)
        self:print("Available commands:")
        for cmd_name, cmd_info in pairs(self.commands) do
            self:print(string.format("  %s - %s", cmd_name, cmd_info.description))
        end
    end, "Show available commands")

    -- Comando clear
    self:register_command("clear", function(args)
        self.output_buffer = {}
    end, "Clear console output")

    -- Comando memory
    self:register_command("memory", function(args)
        local memory_kb = collectgarbage("count")
        self:print(string.format("Lua memory: %.1f KB", memory_kb))

        if args[1] == "collect" then
            collectgarbage("collect")
            local after_gc = collectgarbage("count")
            self:print(string.format("After GC: %.1f KB (freed %.1f KB)",
                                   after_gc, memory_kb - after_gc))
        end
    end, "Show memory usage (use 'memory collect' to force GC)")

    -- Comando fps
    self:register_command("fps", function(args)
        local enable = args[1] ~= "off"
        msg.post("debug_manager", "toggle_fps_display", {enabled = enable})
        self:print("FPS display " .. (enable and "enabled" or "disabled"))
    end, "Toggle FPS display")

    -- Comando goto
    self:register_command("goto", function(args)
        if not args[1] then
            self:print("Usage: goto <x> <y>")
            return
        end

        local x = tonumber(args[1]) or 0
        local y = tonumber(args[2]) or 0
        local player = game.get_player()

        if player then
            go.set_position(vmath.vector3(x, y, 0), player)
            self:print(string.format("Teleported to (%.1f, %.1f)", x, y))
        else
            self:print("Player not found")
        end
    end, "Teleport to position (x, y)")

    -- Comando spawn
    self:register_command("spawn", function(args)
        if not args[1] then
            self:print("Usage: spawn <factory_name>")
            return
        end

        local factory_url = "#" .. args[1] .. "_factory"
        local success, obj = pcall(factory.create, factory_url)

        if success then
            self:print("Spawned: " .. tostring(obj))
        else
            self:print("Failed to spawn: " .. args[1])
        end
    end, "Spawn object from factory")

    -- Comando set
    self:register_command("set", function(args)
        if #args < 3 then
            self:print("Usage: set <object_url> <property> <value>")
            return
        end

        local obj_url = args[1]
        local property = args[2]
        local value = args[3]

        -- Intentar convertir value al tipo apropiado
        local converted_value = tonumber(value) or value
        if value == "true" then converted_value = true
        elseif value == "false" then converted_value = false
        end

        local success, err = pcall(go.set, obj_url, property, converted_value)
        if success then
            self:print(string.format("Set %s.%s = %s", obj_url, property, tostring(converted_value)))
        else
            self:print("Error: " .. tostring(err))
        end
    end, "Set object property")
end

local function register_command(self, name, func, description)
    self.commands[name] = {
        func = func,
        description = description or "No description"
    }
end

local function execute_command(self, command_line)
    if command_line == "" then return end

    -- Añadir al historial
    table.insert(self.command_history, command_line)
    if #self.command_history > 50 then
        table.remove(self.command_history, 1)
    end

    -- Parsear comando
    local parts = {}
    for part in command_line:gmatch("%S+") do
        table.insert(parts, part)
    end

    if #parts == 0 then return end

    local command_name = parts[1]
    local args = {}
    for i = 2, #parts do
        table.insert(args, parts[i])
    end

    -- Mostrar comando ejecutado
    self:print("> " .. command_line)

    -- Ejecutar comando
    local command = self.commands[command_name]
    if command then
        local success, err = pcall(command.func, args)
        if not success then
            self:print("Error executing command: " .. tostring(err))
        end
    else
        self:print("Unknown command: " .. command_name .. " (type 'help' for available commands)")
    end
end

local function print(self, text)
    table.insert(self.output_buffer, {
        text = tostring(text),
        timestamp = os.time()
    })

    -- Mantener buffer limitado
    if #self.output_buffer > 100 then
        table.remove(self.output_buffer, 1)
    end

    -- Actualizar GUI
    self:update_console_display()
end

function on_input(self, action_id, action)
    if action_id == hash("debug_console") and action.pressed then
        self.visible = not self.visible
        gui.set_enabled(gui.get_node("console_root"), self.visible)

        if self.visible then
            msg.post(".", "acquire_input_focus")
        else
            msg.post(".", "release_input_focus")
        end
    end

    if not self.visible then return end

    if action_id == hash("enter") and action.pressed then
        execute_command(self, self.input_text)
        self.input_text = ""
        self.cursor_pos = 0
        self:update_input_display()

    elseif action_id == hash("backspace") and action.pressed then
        if #self.input_text > 0 and self.cursor_pos > 0 then
            self.input_text = self.input_text:sub(1, self.cursor_pos - 1) ..
                             self.input_text:sub(self.cursor_pos + 1)
            self.cursor_pos = self.cursor_pos - 1
            self:update_input_display()
        end

    elseif action_id == hash("text") then
        self.input_text = self.input_text:sub(1, self.cursor_pos) ..
                         action.text ..
                         self.input_text:sub(self.cursor_pos + 1)
        self.cursor_pos = self.cursor_pos + #action.text
        self:update_input_display()
    end
end

DebugConsole.register_command = register_command
DebugConsole.execute_command = execute_command
DebugConsole.print = print

return DebugConsole

📚 Recursos y Referencias

Herramientas de Debugging

Profiling Tools

🎯 Ejercicios Propuestos

  1. Sistema de Métricas: Crea un sistema completo de métricas que envíe datos a un servidor de analytics.

  2. Memory Leak Detector: Implementa un detector automático de memory leaks que alerte en tiempo real.

  3. Performance Budget: Crea un sistema de “presupuesto de rendimiento” que alerte cuando se superen límites.

  4. Visual Profiler: Desarrolla un profiler visual que muestre gráficos de rendimiento en tiempo real.

  5. Automated Testing: Implementa un framework de testing automatizado para detectar regresiones.

El debugging y profiling profesional son habilidades que separan a los desarrolladores amateur de los profesionales. Dominar estas técnicas te permitirá crear juegos estables, optimizados y de calidad profesional.