"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExternalRpcProcessor = exports.TraceProcessorFactory = exports.WorkingTraceProcessor = void 0;
exports.getBundledTraceProcessorPath = getBundledTraceProcessorPath;
exports.getPrebuiltTraceProcessorPlatformKey = getPrebuiltTraceProcessorPlatformKey;
exports.getPrebuiltTraceProcessorPath = getPrebuiltTraceProcessorPath;
exports.getBackendBinTraceProcessorPath = getBackendBinTraceProcessorPath;
exports.getUserTraceProcessorPath = getUserTraceProcessorPath;
exports.isTraceProcessorPathPlaceholder = isTraceProcessorPathPlaceholder;
exports.getTraceProcessorPath = getTraceProcessorPath;
exports.supportsTraceProcessorCorsOriginsFlag = supportsTraceProcessorCorsOriginsFlag;
exports.isTraceProcessorReadyMessage = isTraceProcessorReadyMessage;
exports.isFatalTraceProcessorListenFailure = isFatalTraceProcessorListenFailure;
exports.killOrphanProcessors = killOrphanProcessors;
const events_1 = require("events");
const child_process_1 = require("child_process");
const net_1 = __importDefault(require("net"));
const fs_1 = __importDefault(require("fs"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const uuid_1 = require("../utils/uuid");
const traceProcessorProtobuf_1 = require("./traceProcessorProtobuf");
const traceProcessorHttpRpcClient_1 = require("./traceProcessorHttpRpcClient");
const portPool_1 = require("./portPool");
const config_1 = require("../config");
const logger_1 = __importDefault(require("../utils/logger"));
const perfettoStdlibScanner_1 = require("./perfettoStdlibScanner");
const processRss_1 = require("./processRss");
const traceProcessorSqlWorker_1 = require("./traceProcessorSqlWorker");
const traceProcessorRamBudget_1 = require("./traceProcessorRamBudget");
const IS_TEST_ENV = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
// Path to the trace_processor_shell binary.
// Use the locally built version from perfetto/out/ui which has the viz stdlib modules.
// Can be overridden via TRACE_PROCESSOR_PATH environment variable.
// Keep this as a runtime getter: CLI loads .env after module imports, so a module-level
// process.env read would miss TRACE_PROCESSOR_PATH from backend/.env / --env-file.
// Path: backend/src/services/ -> ../../../ -> perfetto/out/ui/
function getBundledTraceProcessorPath() {
    const executableName = process.platform === 'win32' ? 'trace_processor_shell.exe' : 'trace_processor_shell';
    return path_1.default.resolve(__dirname, '../../../perfetto/out/ui', executableName);
}
function getPrebuiltTraceProcessorPlatformKey() {
    if (process.platform === 'linux' && process.arch === 'x64')
        return 'linux-x64';
    if (process.platform === 'darwin' && process.arch === 'arm64')
        return 'darwin-arm64';
    if (process.platform === 'win32' && process.arch === 'x64')
        return 'win32-x64';
    return undefined;
}
function getPrebuiltTraceProcessorPath() {
    const platformKey = getPrebuiltTraceProcessorPlatformKey();
    if (!platformKey)
        return undefined;
    const executableName = process.platform === 'win32' ? 'trace_processor_shell.exe' : 'trace_processor_shell';
    return path_1.default.resolve(__dirname, '../../prebuilts/trace_processor', platformKey, executableName);
}
function getBackendBinTraceProcessorPath() {
    const executableName = process.platform === 'win32' ? 'trace_processor_shell.exe' : 'trace_processor_shell';
    return path_1.default.resolve(__dirname, '../../bin', executableName);
}
function getUserTraceProcessorPath() {
    const home = process.env.SMARTPERFETTO_HOME && process.env.SMARTPERFETTO_HOME.trim()
        ? path_1.default.resolve(process.env.SMARTPERFETTO_HOME)
        : path_1.default.join(os_1.default.homedir(), '.smartperfetto');
    const executableName = process.platform === 'win32' ? 'trace_processor_shell.exe' : 'trace_processor_shell';
    return path_1.default.join(home, 'bin', executableName);
}
function isTraceProcessorPathPlaceholder(value) {
    const normalized = value.trim().replace(/\\/g, '/');
    return normalized === '/path/to/trace_processor_shell' ||
        normalized === '/path/to/trace_processor_shell.exe' ||
        normalized === 'path/to/trace_processor_shell' ||
        normalized === 'path/to/trace_processor_shell.exe' ||
        normalized === '/absolute/path/to/trace_processor_shell' ||
        normalized === '/absolute/path/to/trace_processor_shell.exe' ||
        /^[A-Za-z]:\/path\/to\/trace_processor_shell(?:\.exe)?$/.test(normalized);
}
let warnedTraceProcessorPathPlaceholder = false;
function resolveEnvTraceProcessorPath() {
    const configured = process.env.TRACE_PROCESSOR_PATH?.trim();
    if (!configured)
        return undefined;
    if (isTraceProcessorPathPlaceholder(configured)) {
        if (!warnedTraceProcessorPathPlaceholder) {
            warnedTraceProcessorPathPlaceholder = true;
            console.warn('[TraceProcessor] Ignoring placeholder TRACE_PROCESSOR_PATH. ' +
                'Comment it out or set it to an absolute trace_processor_shell path.');
        }
        return undefined;
    }
    return path_1.default.resolve(configured);
}
function getTraceProcessorPath() {
    const envPath = resolveEnvTraceProcessorPath();
    if (envPath)
        return envPath;
    const prebuiltPath = getPrebuiltTraceProcessorPath();
    if (prebuiltPath && fs_1.default.existsSync(prebuiltPath))
        return prebuiltPath;
    const bundledPath = getBundledTraceProcessorPath();
    if (fs_1.default.existsSync(bundledPath))
        return bundledPath;
    const backendBinPath = getBackendBinTraceProcessorPath();
    if (fs_1.default.existsSync(backendBinPath))
        return backendBinPath;
    return getUserTraceProcessorPath();
}
const traceProcessorCorsFlagSupportCache = new Map();
function supportsTraceProcessorCorsOriginsFlag(binaryPath = getTraceProcessorPath()) {
    const cached = traceProcessorCorsFlagSupportCache.get(binaryPath);
    if (cached !== undefined)
        return cached;
    const result = (0, child_process_1.spawnSync)(binaryPath, ['--help'], {
        encoding: 'utf-8',
        timeout: 5000,
    });
    const help = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
    const supported = help.includes('--http-additional-cors-origins') || help.includes('additional-cors');
    traceProcessorCorsFlagSupportCache.set(binaryPath, supported);
    return supported;
}
function isTraceProcessorReadyMessage(text) {
    return text.includes('Starting HTTP server') ||
        text.includes('Starting RPC server');
}
// Tier 0: absolute minimum stdlib modules needed for any analysis to start.
// Only the 3 most heavily referenced modules (19-32 skill/TS usages each).
// All other modules load on-demand via skill YAML prerequisites or explicit
// INCLUDE PERFETTO MODULE in SQL queries — no need to preload them here.
const CRITICAL_STDLIB_MODULES = [
    'android.frames.timeline', // 19 skills, 19 TS refs — frame/jank analysis foundation
    'android.startup.startups', // 16 skills, 32 TS refs — startup analysis foundation
    'android.binder', // 22 skills,  6 TS refs — IPC/blocking analysis foundation
];
function isFatalTraceProcessorListenFailure(text) {
    if (!(text.includes('Failed to listen') || text.includes('Address already in use'))) {
        return false;
    }
    return text
        .split(/\r?\n/)
        .filter(line => line.includes('Failed to listen') || line.includes('Address already in use'))
        .some(line => {
        // Docker/Linux containers commonly have IPv6 loopback disabled. Perfetto can
        // still serve backend queries on 127.0.0.1, so this warning is not fatal.
        if (line.includes('IPv6 socket') || line.includes('[::')) {
            return false;
        }
        if (line.includes('errno: 0, No error')) {
            return false;
        }
        return true;
    });
}
/**
 * Kill all orphan trace_processor_shell processes.
 * This should be called at startup to clean up processes from previous runs.
 */
function killOrphanProcessors() {
    if (!IS_TEST_ENV) {
        console.log('[TraceProcessor] Checking for orphan trace_processor_shell processes...');
    }
    try {
        // Find all trace_processor_shell processes
        const result = (0, child_process_1.execSync)('pgrep -f trace_processor_shell 2>/dev/null || true', { encoding: 'utf-8' });
        const pids = result.trim().split('\n').filter(pid => pid.length > 0);
        if (pids.length === 0) {
            if (!IS_TEST_ENV) {
                console.log('[TraceProcessor] No orphan processes found');
            }
            return 0;
        }
        if (!IS_TEST_ENV) {
            console.log(`[TraceProcessor] Found ${pids.length} orphan process(es): ${pids.join(', ')}`);
        }
        // Kill each process
        let killed = 0;
        for (const pid of pids) {
            try {
                (0, child_process_1.execSync)(`kill ${pid} 2>/dev/null || true`);
                killed++;
                if (!IS_TEST_ENV) {
                    console.log(`[TraceProcessor] Killed orphan process ${pid}`);
                }
            }
            catch (e) {
                // Process may already be dead
            }
        }
        if (!IS_TEST_ENV) {
            console.log(`[TraceProcessor] Killed ${killed} orphan process(es)`);
        }
        return killed;
    }
    catch (error) {
        if (!IS_TEST_ENV) {
            console.log('[TraceProcessor] Error checking for orphan processes:', error.message);
        }
        return 0;
    }
}
function probeOk(startTime, detail) {
    return {
        ok: true,
        durationMs: Date.now() - startTime,
        ...(detail ? { detail } : {}),
    };
}
function probeError(startTime, error, detail) {
    const message = error instanceof Error ? error.message : String(error);
    return {
        ok: false,
        durationMs: Date.now() - startTime,
        error: message,
        ...(detail ? { detail } : {}),
    };
}
function probeTcpAccept(port, timeoutMs, hostname = '127.0.0.1') {
    const startTime = Date.now();
    return new Promise((resolve) => {
        let settled = false;
        const socket = net_1.default.createConnection({ host: hostname, port });
        const finish = (result) => {
            if (settled)
                return;
            settled = true;
            clearTimeout(timer);
            socket.destroy();
            resolve(result);
        };
        const timer = setTimeout(() => {
            finish(probeError(startTime, new Error('RPC accept timeout')));
        }, timeoutMs);
        if (typeof timer.unref === 'function') {
            timer.unref();
        }
        socket.once('connect', () => finish(probeOk(startTime)));
        socket.once('error', error => finish(probeError(startTime, error)));
    });
}
async function probeDedicatedHealthQuery(port, timeoutMs) {
    const startTime = Date.now();
    try {
        const result = await (0, traceProcessorHttpRpcClient_1.executeTraceProcessorHttpRpcSql)({
            port,
            sql: 'SELECT 1',
            timeoutMs,
        });
        if (result.error) {
            return probeError(startTime, new Error(result.error));
        }
        return probeOk(startTime);
    }
    catch (error) {
        return probeError(startTime, error);
    }
}
/**
 * A working Trace Processor that uses trace_processor_shell in HTTP mode.
 *
 * This implementation:
 * 1. Starts trace_processor_shell with --httpd flag
 * 2. Loads the trace file once at initialization
 * 3. Executes queries via HTTP requests (fast, no reload)
 * 4. Properly cleans up the process on destroy
 */
class WorkingTraceProcessor extends events_1.EventEmitter {
    /** Get the HTTP port this processor is listening on */
    get httpPort() {
        return this._httpPort;
    }
    /** Number of in-flight queries. Factory uses this to avoid evicting busy processors. */
    get activeQueries() {
        return this._activeQueries;
    }
    getRuntimeStats() {
        const rssSample = this.sampleRss();
        return {
            kind: 'owned_process',
            processorId: this.id,
            processorKey: this.processorKey,
            traceId: this.traceId,
            ...(this.leaseId ? { leaseId: this.leaseId } : {}),
            leaseMode: this.leaseMode,
            status: this.status,
            activeQueries: this.activeQueries,
            httpPort: this.httpPort,
            ...(this.process?.pid ? { pid: this.process.pid } : {}),
            rssBytes: rssSample.rssBytes,
            startupRssBytes: this.startupRssBytes,
            peakRssBytes: this.peakRssBytes,
            lastRssSampleAt: this.lastRssSampleAt,
            rssSampleSource: rssSample.source,
            ...(rssSample.error ? { rssSampleError: rssSample.error } : {}),
            sqlWorker: this.sqlWorker.getStats(),
        };
    }
    sampleRss() {
        const pid = this.process?.pid;
        const rssSample = pid ? (0, processRss_1.readProcessRssBytes)(pid) : null;
        this.lastRssBytes = rssSample?.rssBytes ?? null;
        this.lastRssSampleSource = rssSample?.source ?? 'unavailable';
        this.lastRssSampleError = rssSample?.error;
        this.lastRssSampleAt = Date.now();
        if (this.lastRssBytes !== null) {
            if (this.startupRssBytes === null)
                this.startupRssBytes = this.lastRssBytes;
            this.peakRssBytes = Math.max(this.peakRssBytes ?? 0, this.lastRssBytes);
        }
        return {
            rssBytes: this.lastRssBytes,
            source: this.lastRssSampleSource,
            ...(this.lastRssSampleError ? { error: this.lastRssSampleError } : {}),
        };
    }
    constructor(traceId, tracePath, options = {}) {
        super();
        this.status = 'initializing';
        this.process = null;
        this.isDestroyed = false;
        this.serverReady = false;
        this._activeQueries = 0;
        this.startupRssBytes = null;
        this.peakRssBytes = null;
        this.lastRssBytes = null;
        this.lastRssSampleAt = null;
        this.lastRssSampleSource = 'unavailable';
        this._criticalModulesLoaded = false;
        this._criticalModulesLoadPromise = null;
        this._criticalModulesLoadFailures = 0;
        this.id = (0, uuid_1.uuidv4)();
        this.traceId = traceId;
        this.tracePath = tracePath;
        this.processorKey = options.processorKey ?? traceId;
        this.leaseId = options.leaseId;
        this.leaseMode = options.leaseMode ?? 'shared';
        // Allocate port from pool
        this._httpPort = (0, portPool_1.getPortPool)().allocate(this.processorKey);
        this.sqlWorker = new traceProcessorSqlWorker_1.TraceProcessorSqlWorker({
            processorId: this.id,
            traceId: this.traceId,
            port: this._httpPort,
        });
    }
    async initialize() {
        console.log(`[TraceProcessor] Initializing HTTP mode for trace: ${this.tracePath}`);
        console.log(`[TraceProcessor] Using port: ${this.httpPort}`);
        // Check if trace file exists
        if (!fs_1.default.existsSync(this.tracePath)) {
            throw new Error(`Trace file not found: ${this.tracePath}`);
        }
        // Check if trace_processor_shell exists
        const traceProcessorPath = getTraceProcessorPath();
        if (!fs_1.default.existsSync(traceProcessorPath)) {
            throw new Error(`trace_processor_shell not found at: ${traceProcessorPath}`);
        }
        if (this.isDestroyed) {
            throw new Error('Processor destroyed during initialization');
        }
        // Start trace_processor_shell in HTTP mode
        try {
            await this.startHttpServer();
            // Verify server is working with a test query
            console.log(`[TraceProcessor] Verifying server with test query...`);
            const testResult = await this.executeHttpQuery('SELECT 1 as test', { priority: 'p1' });
            if (testResult.error) {
                throw new Error(`Server verification failed: ${testResult.error}`);
            }
            this.sampleRss();
            this.status = 'ready';
            console.log(`[TraceProcessor] Processor ${this.id} ready (HTTP mode) for trace ${this.traceId}`);
            this.emit('ready');
            // Stdlib modules are loaded on demand:
            // - Skills handle their own prerequisites via INCLUDE PERFETTO MODULE
            //   (skillExecutor.buildSqlWithModuleIncludes)
            // - Raw `execute_sql` / `execute_sql_on` calls go through
            //   sqlIncludeInjector (backend/src/agentv3/sqlIncludeInjector.ts),
            //   which scans the SQL and prepends required INCLUDEs.
            // Background preload was removed because it competes with Agent queries
            // for the single-threaded trace_processor_shell, causing socket hang ups
            // on large traces (200MB+).
        }
        catch (error) {
            console.error(`[TraceProcessor] Initialization failed:`, error.message);
            this.status = 'error';
            this.destroy();
            throw error;
        }
    }
    /**
     * Start trace_processor_shell HTTP server
     */
    async startHttpServer() {
        return new Promise((resolve, reject) => {
            // Build CORS origins string from config
            const corsOrigins = `${config_1.traceProcessorConfig.perfettoUiOrigin},${config_1.traceProcessorConfig.perfettoUiOrigin.replace('localhost', '127.0.0.1')}`;
            const args = [
                '--httpd',
                '--http-port', String(this.httpPort),
                // Only pass --http-additional-cors-origins when supported (added after v47);
                // older binaries (≤v47) don't enforce CORS and exit with code 1 on unknown flags.
                ...(supportsTraceProcessorCorsOriginsFlag() ? ['--http-additional-cors-origins', corsOrigins] : []),
                this.tracePath
            ];
            const traceProcessorPath = getTraceProcessorPath();
            console.log(`[TraceProcessor] Starting: ${traceProcessorPath} ${args.join(' ')}`);
            this.process = (0, child_process_1.spawn)(traceProcessorPath, args, {
                stdio: ['ignore', 'pipe', 'pipe'],
                detached: false,
            });
            let stdout = '';
            let stderr = '';
            let resolved = false;
            // Timeout for server startup
            const timeout = setTimeout(() => {
                if (!resolved) {
                    resolved = true;
                    reject(new Error(`Server startup timeout. stdout: ${stdout}, stderr: ${stderr}`));
                }
            }, config_1.traceProcessorConfig.startupTimeoutMs);
            this.process.stdout?.on('data', (data) => {
                const text = data.toString();
                stdout += text;
                console.log(`[TraceProcessor] stdout: ${text.trim()}`);
                // Check if server is ready
                if (isTraceProcessorReadyMessage(text)) {
                    // Wait a bit for server to be fully ready
                    setTimeout(() => {
                        if (!resolved) {
                            resolved = true;
                            clearTimeout(timeout);
                            this.serverReady = true;
                            resolve();
                        }
                    }, 500);
                }
            });
            this.process.stderr?.on('data', (data) => {
                const text = data.toString();
                stderr += text;
                if (!IS_TEST_ENV) {
                    console.log(`[TraceProcessor] stderr: ${text.trim()}`);
                }
                // Also check stderr for server ready message
                if (isTraceProcessorReadyMessage(text)) {
                    setTimeout(() => {
                        if (!resolved) {
                            resolved = true;
                            clearTimeout(timeout);
                            this.serverReady = true;
                            resolve();
                        }
                    }, 500);
                }
                // Check for errors
                if (text.includes('Could not open') || text.includes('Could not read')) {
                    if (!resolved) {
                        resolved = true;
                        clearTimeout(timeout);
                        reject(new Error(`Failed to load trace: ${text}`));
                    }
                }
                // Check for port in use error
                if (isFatalTraceProcessorListenFailure(text)) {
                    if (!resolved) {
                        resolved = true;
                        clearTimeout(timeout);
                        reject(new Error(`PORT_IN_USE:${this.httpPort}`));
                    }
                }
            });
            this.process.on('error', (error) => {
                if (!resolved) {
                    resolved = true;
                    clearTimeout(timeout);
                    reject(error);
                }
            });
            this.process.on('close', (code) => {
                if (!IS_TEST_ENV) {
                    console.log(`[TraceProcessor] Process exited with code ${code}`);
                }
                this.serverReady = false;
                this.status = 'error';
                if (!resolved) {
                    resolved = true;
                    clearTimeout(timeout);
                    reject(new Error(`Process exited unexpectedly with code ${code}`));
                }
            });
        });
    }
    async query(sql, options = {}) {
        if (this.status !== 'ready') {
            throw new Error(`Trace processor not ready (status: ${this.status})`);
        }
        if (!this.serverReady) {
            throw new Error('HTTP server not ready');
        }
        // Sequential stdlib loading on first query.
        // Each module gets its own HTTP request (bounded by the configured query timeout),
        // unlike the previous bulk approach that concatenated all 22 INCLUDEs into one
        // request which timed out on large (200MB+) traces.
        if (!this._criticalModulesLoaded
            && this._criticalModulesLoadFailures < WorkingTraceProcessor.MAX_STDLIB_LOAD_RETRIES
            && !this._criticalModulesLoadPromise) {
            this._criticalModulesLoadPromise = this._loadCriticalModulesSequentially();
        }
        if (this._criticalModulesLoadPromise) {
            await this._criticalModulesLoadPromise;
        }
        logger_1.default.debug('TraceProcessor', `Executing HTTP query: ${sql.substring(0, 100)}...`);
        return this.enqueueHttpQuery(sql, options);
    }
    async queryRaw(body, options = {}) {
        if (this.status !== 'ready') {
            throw new Error(`Trace processor not ready (status: ${this.status})`);
        }
        if (!this.serverReady) {
            throw new Error('HTTP server not ready');
        }
        this._activeQueries++;
        try {
            return await this.sqlWorker.enqueueRaw(body, options);
        }
        finally {
            this._activeQueries--;
        }
    }
    async queryHealth(timeoutMs = config_1.traceProcessorConfig.healthQueryTimeoutMs) {
        return probeDedicatedHealthQuery(this.httpPort, timeoutMs);
    }
    async checkHealth(options = {}) {
        const liveness = this.checkProcessLiveness();
        const rpcAccept = await probeTcpAccept(this.httpPort, options.rpcAcceptTimeoutMs ?? config_1.traceProcessorConfig.healthRpcAcceptTimeoutMs);
        const queryResponsive = await this.queryHealth(options.queryTimeoutMs ?? config_1.traceProcessorConfig.healthQueryTimeoutMs);
        return {
            ok: liveness.ok && rpcAccept.ok && queryResponsive.ok,
            checkedAt: new Date().toISOString(),
            processorId: this.id,
            traceId: this.traceId,
            httpPort: this.httpPort,
            liveness,
            rpcAccept,
            queryResponsive,
        };
    }
    checkProcessLiveness() {
        const startTime = Date.now();
        const pid = this.process?.pid;
        if (!pid) {
            return probeError(startTime, new Error('No trace_processor pid'));
        }
        try {
            process.kill(pid, 0);
            return probeOk(startTime);
        }
        catch (error) {
            return probeError(startTime, error);
        }
    }
    async enqueueHttpQuery(sql, options) {
        this._activeQueries++;
        try {
            return await this.executeHttpQuery(sql, options);
        }
        finally {
            this._activeQueries--;
        }
    }
    /**
     * Load critical stdlib modules one at a time in the background.
     * With only 3 Tier-0 modules this completes in a few seconds,
     * well within the configured query timeout per module.
     */
    async _loadCriticalModulesSequentially() {
        this._activeQueries++;
        const startTime = Date.now();
        let loaded = 0;
        let failed = 0;
        try {
            for (const m of CRITICAL_STDLIB_MODULES) {
                if (this.isDestroyed)
                    break;
                const result = await this.enqueueHttpQuery(`INCLUDE PERFETTO MODULE ${m};`, { priority: 'p1' });
                if (result.error) {
                    failed++;
                    if (!result.error.includes('not found') && !result.error.includes('no such')) {
                        console.warn(`[TraceProcessor] Failed to load stdlib module ${m}: ${result.error}`);
                    }
                }
                else {
                    loaded++;
                }
            }
            // Only mark as loaded if the loop ran to completion (not interrupted by destroy).
            // Partial module failures are expected (e.g., GPU modules on traces without GPU data).
            if (!this.isDestroyed) {
                this._criticalModulesLoaded = true;
            }
            const elapsed = Date.now() - startTime;
            console.log(`[TraceProcessor] Critical stdlib modules loaded for trace ${this.traceId}: ${loaded}/${CRITICAL_STDLIB_MODULES.length} in ${elapsed}ms (${failed} failed)`);
        }
        catch (err) {
            this._criticalModulesLoadFailures++;
            console.warn(`[TraceProcessor] Stdlib sequential load attempt ${this._criticalModulesLoadFailures}/${WorkingTraceProcessor.MAX_STDLIB_LOAD_RETRIES} failed for trace ${this.traceId}:`, err);
        }
        finally {
            this._activeQueries--;
            this._criticalModulesLoadPromise = null;
        }
    }
    /**
     * Execute SQL query via HTTP
     */
    async executeHttpQuery(sql, options = {}) {
        const result = await this.sqlWorker.query(sql, {
            ...options,
            timeoutMs: options.timeoutMs ?? config_1.traceProcessorConfig.queryTimeoutMs,
        });
        if (result.error === 'Query timeout') {
            logger_1.default.warn('TraceProcessor', `Query exceeded ${config_1.traceProcessorConfig.queryTimeoutMs}ms; aborting request for processor ${this.id}`);
        }
        else if (result.error) {
            logger_1.default.warn('TraceProcessor', `Query error: ${result.error}`);
        }
        else {
            logger_1.default.debug('TraceProcessor', `Query returned ${result.rows.length} rows in ${result.durationMs}ms`);
        }
        return result;
    }
    async preloadModules(modules, label) {
        const loaded = [];
        const failed = [];
        if (modules.length === 0) {
            console.warn(`[TraceProcessor] No ${label} stdlib modules found to preload`);
            return { loaded, failed };
        }
        const startTime = Date.now();
        if (label === 'all') {
            const namespaceGroups = (0, perfettoStdlibScanner_1.groupModulesByNamespace)(modules);
            console.log(`[TraceProcessor] Preloading ${modules.length} ${label} stdlib modules:`, namespaceGroups);
        }
        else {
            console.log(`[TraceProcessor] Preloading ${modules.length} ${label} stdlib modules: ${modules.join(', ')}`);
        }
        // Load modules sequentially to avoid overwhelming the single-threaded
        // trace_processor_shell. Parallel batches cause "socket hang up" on large traces
        // because queued HTTP connections time out while TP processes earlier queries.
        for (const moduleName of modules) {
            let result;
            try {
                const queryResult = await this.executeHttpQuery(`INCLUDE PERFETTO MODULE ${moduleName};`, { priority: 'p2' });
                if (queryResult.error) {
                    result = { status: 'rejected', reason: new Error(queryResult.error) };
                }
                else {
                    result = { status: 'fulfilled', value: moduleName };
                }
            }
            catch (err) {
                result = { status: 'rejected', reason: err };
            }
            {
                // Classify result as loaded or failed
                if (result.status === 'fulfilled') {
                    loaded.push(moduleName);
                }
                else {
                    failed.push(moduleName);
                    // Only log errors for non-trivial failures (not "module not found" which is expected
                    // when trace doesn't have the required data)
                    const errorMsg = result.reason?.message || String(result.reason);
                    if (!errorMsg.includes('not found') && !errorMsg.includes('no such')) {
                        logger_1.default.debug('TraceProcessor', `Failed to load module ${moduleName}: ${errorMsg}`);
                    }
                }
            }
        }
        const elapsed = Date.now() - startTime;
        console.log(`[TraceProcessor] Preloaded ${loaded.length}/${modules.length} ${label} stdlib modules in ${elapsed}ms ` +
            `(${failed.length} failed)`);
        return { loaded, failed };
    }
    /**
     * Preload critical modules used by frequently hit analysis paths.
     */
    async preloadCriticalPerfettoModules() {
        return this.preloadModules(CRITICAL_STDLIB_MODULES, 'critical');
    }
    /**
     * Preload all Perfetto stdlib modules to make additional views/tables available.
     */
    async preloadAllPerfettoModules() {
        return this.preloadModules((0, perfettoStdlibScanner_1.getPerfettoStdlibModules)(), 'all');
    }
    destroy() {
        if (!IS_TEST_ENV) {
            console.log(`[TraceProcessor] Destroying processor ${this.id} for trace ${this.traceId}`);
        }
        this.isDestroyed = true;
        this.serverReady = false;
        this.status = 'error';
        this.sqlWorker.destroy();
        if (this.process) {
            try {
                const processorKey = this.processorKey;
                const proc = this.process;
                let released = false;
                const releasePortOnce = () => {
                    if (released)
                        return;
                    released = true;
                    (0, portPool_1.getPortPool)().release(processorKey);
                };
                // Force kill after timeout (fallback).
                const killTimer = setTimeout(() => {
                    if (!proc.killed) {
                        try {
                            proc.kill('SIGKILL');
                        }
                        catch {
                            // ignore
                        }
                    }
                    // Ensure port is eventually released even if close event is missed.
                    releasePortOnce();
                }, config_1.traceProcessorConfig.killTimeoutMs);
                // Release the port as soon as the process actually exits.
                // Also clears the kill timer to avoid late callbacks/noisy logs in tests.
                proc.once('close', () => {
                    clearTimeout(killTimer);
                    releasePortOnce();
                });
                // In Jest, don't let this timer keep the event loop alive.
                if (IS_TEST_ENV && typeof killTimer.unref === 'function') {
                    killTimer.unref();
                }
                // Try graceful shutdown last (after handlers are registered)
                proc.kill('SIGTERM');
            }
            catch (e) {
                // Process may already be dead, still release port
                (0, portPool_1.getPortPool)().release(this.processorKey);
            }
            this.process = null;
        }
        else {
            // No process, but still release port
            (0, portPool_1.getPortPool)().release(this.processorKey);
        }
        this.removeAllListeners();
    }
}
exports.WorkingTraceProcessor = WorkingTraceProcessor;
WorkingTraceProcessor.MAX_STDLIB_LOAD_RETRIES = 3;
class TraceProcessorFactory {
    static async create(traceId, tracePath, options = {}) {
        const processorKey = options.processorKey ?? traceId;
        // Check if processor already exists and is ready
        const existing = this.processors.get(processorKey);
        if (existing instanceof WorkingTraceProcessor && existing.status === 'ready') {
            console.log(`[TraceProcessorFactory] Reusing existing processor for trace ${traceId} (${processorKey})`);
            return existing;
        }
        // Clean up failed processor if exists
        if (existing) {
            console.log(`[TraceProcessorFactory] Cleaning up existing processor for trace ${traceId} (${processorKey})`);
            this.remove(processorKey);
        }
        // Clean up oldest owned idle processors if too many (skip busy/external ones).
        while (this.processors.size >= this.maxProcessors) {
            const idle = Array.from(this.processors.entries())
                .find(([, p]) => p instanceof WorkingTraceProcessor && p.activeQueries === 0);
            if (idle) {
                console.log(`[TraceProcessorFactory] Cleaning up idle processor: ${idle[0]}`);
                idle[1].destroy();
                this.processors.delete(idle[0]);
            }
            else {
                // All processors are busy — allow temporary over-limit rather than killing active work
                console.warn(`[TraceProcessorFactory] All ${this.processors.size} processors are busy, allowing temporary over-limit`);
                break;
            }
        }
        const traceSizeBytes = fs_1.default.statSync(tracePath).size;
        (0, traceProcessorRamBudget_1.assertTraceProcessorAdmission)({
            traceId,
            traceSizeBytes,
            processors: Array.from(this.processors.values()).map(processor => processor.getRuntimeStats()),
        });
        const maxAttempts = 8;
        let lastError;
        for (let attempt = 1; attempt <= maxAttempts; attempt++) {
            // Create new processor (allocates a port from the pool)
            console.log(`[TraceProcessorFactory] Creating new HTTP-mode processor for trace ${traceId} (${processorKey}) (attempt ${attempt}/${maxAttempts})`);
            const processor = new WorkingTraceProcessor(traceId, tracePath, {
                ...options,
                processorKey,
            });
            processor.on('error', () => {
                this.processors.delete(processorKey);
            });
            this.processors.set(processorKey, processor);
            try {
                await processor.initialize();
                return processor;
            }
            catch (error) {
                lastError = error;
                // Ensure we don't keep a failed processor around.
                try {
                    processor.destroy();
                }
                catch {
                    // ignore
                }
                this.processors.delete(processorKey);
                // Retry with a different port if the chosen port is already in use by another process.
                const msg = String(error?.message || '');
                if (msg.startsWith('PORT_IN_USE:')) {
                    const portStr = msg.split(':')[1];
                    const port = Number(portStr);
                    if (Number.isFinite(port)) {
                        (0, portPool_1.getPortPool)().blockPort(port);
                    }
                    else {
                        // Fallback: release any allocation for this processor key so next attempt can allocate again.
                        (0, portPool_1.getPortPool)().release(processorKey);
                    }
                    continue;
                }
                throw error;
            }
        }
        throw lastError || new Error('Failed to create trace processor');
    }
    static get(processorKey) {
        return this.processors.get(processorKey);
    }
    static remove(processorKey) {
        const processor = this.processors.get(processorKey);
        if (processor) {
            console.log(`[TraceProcessorFactory] Removing processor ${processorKey}`);
            this.processors.delete(processorKey);
            if (processor instanceof ExternalRpcProcessor) {
                const stillReferenced = Array.from(this.processors.values()).some(p => p === processor);
                if (!stillReferenced) {
                    processor.destroy();
                    this.externalProcessorsByPort.delete(processor.httpPort);
                }
            }
            else {
                processor.destroy();
            }
            return true;
        }
        return false;
    }
    static cleanup() {
        console.log(`[TraceProcessorFactory] Cleaning up all processors`);
        for (const processor of new Set(this.processors.values())) {
            processor.destroy();
        }
        this.processors.clear();
        this.externalProcessorsByPort.clear();
    }
    static getStats() {
        const processors = Array.from(this.processors.values()).map(processor => processor.getRuntimeStats());
        return {
            count: this.processors.size,
            traceIds: Array.from(new Set(processors.map(processor => processor.traceId))),
            processorKeys: Array.from(this.processors.keys()),
            processors,
            ramBudget: (0, traceProcessorRamBudget_1.getTraceProcessorRamBudgetStats)(processors),
        };
    }
    /**
     * Create a processor that connects to an existing external HTTP RPC endpoint.
     * This is used when the frontend is already connected to a trace_processor via HTTP RPC.
     * We don't start a new process, we just create a wrapper that queries the existing one.
     */
    static async createFromExternalRpc(traceId, port) {
        const current = this.processors.get(traceId);
        if (current instanceof ExternalRpcProcessor && current.httpPort === port && current.status === 'ready') {
            console.log(`[TraceProcessorFactory] Reusing external RPC processor for trace ${traceId} on port ${port}`);
            return current;
        }
        if (current) {
            console.log(`[TraceProcessorFactory] Remapping trace ${traceId} to external RPC port ${port}`);
            this.remove(traceId);
        }
        const existing = this.externalProcessorsByPort.get(port);
        if (existing && existing.status === 'ready') {
            console.log(`[TraceProcessorFactory] Reusing external RPC processor for port ${port}`);
            this.processors.set(traceId, existing);
            return existing;
        }
        if (existing) {
            this.externalProcessorsByPort.delete(port);
        }
        console.log(`[TraceProcessorFactory] Creating external RPC processor for port ${port}`);
        const processor = new ExternalRpcProcessor(traceId, port);
        // Verify connection with a dedicated health query — avoid triggering lazy
        // stdlib load or the main SQL worker queue at registration time.
        try {
            const health = await processor.queryHealth(config_1.traceProcessorConfig.healthQueryTimeoutMs);
            if (!health.ok) {
                throw new Error(health.error || 'Health query failed');
            }
            console.log(`[TraceProcessorFactory] External RPC connection verified on port ${port}`);
        }
        catch (error) {
            console.error(`[TraceProcessorFactory] Failed to verify external RPC connection:`, error);
            throw new Error(`Cannot connect to external trace_processor on port ${port}`);
        }
        this.processors.set(traceId, processor);
        this.externalProcessorsByPort.set(port, processor);
        return processor;
    }
}
exports.TraceProcessorFactory = TraceProcessorFactory;
TraceProcessorFactory.processors = new Map();
TraceProcessorFactory.externalProcessorsByPort = new Map();
TraceProcessorFactory.maxProcessors = 5;
/**
 * A lightweight processor that connects to an external trace_processor HTTP RPC endpoint.
 * Unlike WorkingTraceProcessor, this doesn't start a new process.
 */
class ExternalRpcProcessor extends events_1.EventEmitter {
    get httpPort() {
        return this._httpPort;
    }
    /** Number of in-flight or queued queries. Factory uses this to avoid unsafe eviction. */
    get activeQueries() {
        return this._activeQueries;
    }
    getRuntimeStats() {
        return {
            kind: 'external_rpc',
            processorId: this.id,
            traceId: this.traceId,
            status: this.status,
            activeQueries: this.activeQueries,
            httpPort: this.httpPort,
            rssBytes: null,
            rssSampleSource: 'external',
            sqlWorker: this.sqlWorker.getStats(),
        };
    }
    constructor(traceId, port) {
        super();
        this.status = 'ready';
        this._activeQueries = 0;
        this._criticalModulesLoaded = false;
        this._criticalModulesLoadPromise = null;
        this._criticalModulesLoadFailures = 0;
        this.id = `external-${port}`;
        this.traceId = traceId;
        this._httpPort = port;
        this.sqlWorker = new traceProcessorSqlWorker_1.TraceProcessorSqlWorker({
            processorId: this.id,
            traceId: this.traceId,
            port: this._httpPort,
            forceInline: IS_TEST_ENV,
            rawExecutor: IS_TEST_ENV
                ? async (request) => {
                    const sql = (0, traceProcessorProtobuf_1.decodeQueryArgsSql)(request.body);
                    const result = await this._execRaw(sql);
                    return (0, traceProcessorProtobuf_1.encodeQueryResult)({
                        columnNames: result.columns,
                        rows: result.rows,
                        ...(result.error ? { error: result.error } : {}),
                    });
                }
                : undefined,
        });
        console.log(`[ExternalRpcProcessor] Created for trace ${traceId} on port ${port}`);
    }
    async query(sql, options = {}) {
        // Sequential stdlib loading on first query (same pattern as WorkingTraceProcessor).
        if (!this._criticalModulesLoaded
            && this._criticalModulesLoadFailures < ExternalRpcProcessor.MAX_STDLIB_LOAD_RETRIES
            && !this._criticalModulesLoadPromise) {
            this._criticalModulesLoadPromise = this._loadCriticalModulesSequentially();
        }
        if (this._criticalModulesLoadPromise) {
            await this._criticalModulesLoadPromise;
        }
        return this.enqueueRawQuery(sql, options);
    }
    async queryRaw(body, options = {}) {
        this._activeQueries++;
        try {
            return await this.sqlWorker.enqueueRaw(body, options);
        }
        finally {
            this._activeQueries--;
        }
    }
    async queryHealth(timeoutMs = config_1.traceProcessorConfig.healthQueryTimeoutMs) {
        return probeDedicatedHealthQuery(this.httpPort, timeoutMs);
    }
    async checkHealth(options = {}) {
        const startTime = Date.now();
        const liveness = probeOk(startTime, 'external_rpc_no_owned_process');
        const rpcAccept = await probeTcpAccept(this.httpPort, options.rpcAcceptTimeoutMs ?? config_1.traceProcessorConfig.healthRpcAcceptTimeoutMs);
        const queryResponsive = await this.queryHealth(options.queryTimeoutMs ?? config_1.traceProcessorConfig.healthQueryTimeoutMs);
        return {
            ok: liveness.ok && rpcAccept.ok && queryResponsive.ok,
            checkedAt: new Date().toISOString(),
            processorId: this.id,
            traceId: this.traceId,
            httpPort: this.httpPort,
            liveness,
            rpcAccept,
            queryResponsive,
        };
    }
    /**
     * Load critical stdlib modules one at a time in the background.
     * With only 3 Tier-0 modules this completes in a few seconds.
     */
    async _loadCriticalModulesSequentially() {
        this._activeQueries++;
        const startTime = Date.now();
        let loaded = 0;
        let failed = 0;
        try {
            for (const m of CRITICAL_STDLIB_MODULES) {
                const result = await this.enqueueRawQuery(`INCLUDE PERFETTO MODULE ${m};`, { priority: 'p1' });
                if (result.error) {
                    failed++;
                    if (!result.error.includes('not found') && !result.error.includes('no such')) {
                        console.warn(`[ExternalRpcProcessor] Failed to load stdlib module ${m}: ${result.error}`);
                    }
                }
                else {
                    loaded++;
                }
            }
            this._criticalModulesLoaded = true;
            const elapsed = Date.now() - startTime;
            console.log(`[ExternalRpcProcessor] Critical stdlib modules loaded for trace ${this.traceId}: ${loaded}/${CRITICAL_STDLIB_MODULES.length} in ${elapsed}ms (${failed} failed)`);
        }
        catch (err) {
            this._criticalModulesLoadFailures++;
            console.warn(`[ExternalRpcProcessor] Stdlib sequential load attempt ${this._criticalModulesLoadFailures}/${ExternalRpcProcessor.MAX_STDLIB_LOAD_RETRIES} failed:`, err);
        }
        finally {
            this._activeQueries--;
            this._criticalModulesLoadPromise = null;
        }
    }
    async enqueueRawQuery(sql, options = {}) {
        this._activeQueries++;
        try {
            return await this.sqlWorker.query(sql, {
                ...options,
                timeoutMs: options.timeoutMs ?? config_1.traceProcessorConfig.queryTimeoutMs,
            });
        }
        finally {
            this._activeQueries--;
        }
    }
    /** Low-level query without stdlib lazy-load. Used by factory for connectivity checks. */
    _execRaw(sql, options = {}) {
        return (0, traceProcessorHttpRpcClient_1.executeTraceProcessorHttpRpcSql)({
            port: this._httpPort,
            sql,
            timeoutMs: options.timeoutMs ?? config_1.traceProcessorConfig.queryTimeoutMs,
        });
    }
    destroy() {
        // External RPC processor doesn't own the process, so nothing to clean up
        console.log(`[ExternalRpcProcessor] Destroyed (trace ${this.traceId})`);
        this.status = 'error';
        this.sqlWorker.destroy();
        this.emit('destroyed');
    }
}
exports.ExternalRpcProcessor = ExternalRpcProcessor;
// TODO: If external TP restarts on the same port, _criticalModulesLoaded stays true
// but modules are gone. Detect connection errors and reset flag to force reload.
ExternalRpcProcessor.MAX_STDLIB_LOAD_RETRIES = 3;
//# sourceMappingURL=workingTraceProcessor.js.map