"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.TraceProcessorSqlWorker = void 0;
exports.normalizeTraceProcessorQueryPriority = normalizeTraceProcessorQueryPriority;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const worker_threads_1 = require("worker_threads");
const config_1 = require("../config");
const traceProcessorProtobuf_1 = require("./traceProcessorProtobuf");
const traceProcessorHttpRpcClient_1 = require("./traceProcessorHttpRpcClient");
const IS_TEST_ENV = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
const PRIORITY_ORDER = ['p0', 'p1', 'p2'];
function normalizeTraceProcessorQueryPriority(value, fallback = 'p1') {
    if (typeof value !== 'string')
        return fallback;
    const normalized = value.trim().toLowerCase();
    if (['p0', 'frontend', 'interactive', 'user'].includes(normalized))
        return 'p0';
    if (['p1', 'agent', 'analysis', 'normal'].includes(normalized))
        return 'p1';
    if (['p2', 'report', 'batch', 'heavy', 'background'].includes(normalized))
        return 'p2';
    return fallback;
}
function resolveWorkerThreadPath() {
    const jsPath = path_1.default.join(__dirname, 'traceProcessorSqlWorkerThread.js');
    if (fs_1.default.existsSync(jsPath)) {
        return { filename: jsPath, execArgv: [] };
    }
    return {
        filename: path_1.default.join(__dirname, 'traceProcessorSqlWorkerThread.ts'),
        execArgv: ['--require', 'tsx/cjs'],
    };
}
class TraceProcessorSqlWorker {
    constructor(options) {
        this.queues = {
            p0: [],
            p1: [],
            p2: [],
        };
        this.pendingWorkerRequests = new Map();
        this.worker = null;
        this.running = false;
        this.destroyed = false;
        this.nextTaskId = 1;
        this.processorId = options.processorId;
        this.traceId = options.traceId;
        this.port = options.port;
        this.hostname = options.hostname || '127.0.0.1';
        this.forceInline = options.forceInline ?? IS_TEST_ENV;
        this.rawExecutor = options.rawExecutor;
    }
    get activeCount() {
        return (this.running ? 1 : 0) + this.queues.p0.length + this.queues.p1.length + this.queues.p2.length;
    }
    getStats() {
        return {
            running: this.running,
            queuedP0: this.queues.p0.length,
            queuedP1: this.queues.p1.length,
            queuedP2: this.queues.p2.length,
            usesWorkerThread: !this.forceInline && !this.rawExecutor,
        };
    }
    async query(sql, options = {}) {
        const startTime = Date.now();
        try {
            const response = await this.enqueueRaw((0, traceProcessorProtobuf_1.encodeQueryArgs)(sql), options);
            const parsed = (0, traceProcessorProtobuf_1.decodeQueryResult)(response);
            return {
                columns: parsed.columnNames,
                rows: parsed.rows,
                durationMs: Date.now() - startTime,
                ...(parsed.error ? { error: parsed.error } : {}),
            };
        }
        catch (error) {
            return {
                columns: [],
                rows: [],
                durationMs: Date.now() - startTime,
                error: error?.message || String(error),
            };
        }
    }
    enqueueRaw(body, options = {}) {
        if (this.destroyed) {
            return Promise.reject(new Error(`SQL worker for processor ${this.processorId} is destroyed`));
        }
        const priority = normalizeTraceProcessorQueryPriority(options.priority);
        const timeoutMs = options.timeoutMs ?? config_1.traceProcessorConfig.queryTimeoutMs;
        const taskId = this.nextTaskId++;
        return new Promise((resolve, reject) => {
            this.queues[priority].push({
                id: taskId,
                body,
                priority,
                timeoutMs,
                resolve,
                reject,
            });
            this.drain();
        });
    }
    destroy() {
        this.destroyed = true;
        const error = new Error(`SQL worker for processor ${this.processorId} was destroyed`);
        for (const priority of PRIORITY_ORDER) {
            const tasks = this.queues[priority].splice(0);
            for (const task of tasks)
                task.reject(error);
        }
        for (const pending of this.pendingWorkerRequests.values()) {
            pending.reject(error);
        }
        this.pendingWorkerRequests.clear();
        if (this.worker) {
            void this.worker.terminate();
            this.worker = null;
        }
    }
    drain() {
        if (this.running || this.destroyed)
            return;
        const task = this.nextTask();
        if (!task)
            return;
        this.running = true;
        void this.runTask(task)
            .then(task.resolve, task.reject)
            .finally(() => {
            this.running = false;
            this.drain();
        });
    }
    nextTask() {
        for (const priority of PRIORITY_ORDER) {
            const task = this.queues[priority].shift();
            if (task)
                return task;
        }
        return undefined;
    }
    async runTask(task) {
        const request = {
            hostname: this.hostname,
            port: this.port,
            body: task.body,
            timeoutMs: task.timeoutMs,
        };
        if (this.forceInline || this.rawExecutor) {
            return (this.rawExecutor || traceProcessorHttpRpcClient_1.executeTraceProcessorHttpRpcRaw)(request);
        }
        return this.postToWorker(task);
    }
    ensureWorker() {
        if (this.worker)
            return this.worker;
        const workerPath = resolveWorkerThreadPath();
        const worker = new worker_threads_1.Worker(workerPath.filename, {
            execArgv: workerPath.execArgv,
            env: process.env,
        });
        worker.on('message', (message) => {
            const pending = this.pendingWorkerRequests.get(message.id);
            if (!pending)
                return;
            this.pendingWorkerRequests.delete(message.id);
            if (message.ok) {
                pending.resolve(Buffer.from(message.body || []));
            }
            else {
                pending.reject(new Error(message.error || 'SQL worker query failed'));
            }
        });
        worker.on('error', error => {
            const normalized = error instanceof Error ? error : new Error(String(error));
            this.rejectPendingWorkerRequests(normalized);
            this.worker = null;
        });
        worker.on('exit', code => {
            if (code !== 0 && !this.destroyed) {
                this.rejectPendingWorkerRequests(new Error(`SQL worker exited with code ${code}`));
            }
            this.worker = null;
        });
        this.worker = worker;
        return worker;
    }
    postToWorker(task) {
        const worker = this.ensureWorker();
        return new Promise((resolve, reject) => {
            this.pendingWorkerRequests.set(task.id, { resolve, reject });
            worker.postMessage({
                id: task.id,
                hostname: this.hostname,
                port: this.port,
                body: task.body,
                timeoutMs: task.timeoutMs,
            });
        });
    }
    rejectPendingWorkerRequests(error) {
        for (const pending of this.pendingWorkerRequests.values()) {
            pending.reject(error);
        }
        this.pendingWorkerRequests.clear();
    }
}
exports.TraceProcessorSqlWorker = TraceProcessorSqlWorker;
//# sourceMappingURL=traceProcessorSqlWorker.js.map