"use strict";
/**
 * Three-phase search for optimal streamFlushMillis values.
 *
 * Phase 1 (Coarse): Binary search with few tasks per step to quickly find the
 * approximate threshold where noTaskData events start appearing.
 *
 * Phase 2 (Validate): Run many trials with more tasks around the coarse
 * threshold to confirm the exact minimum reliable value.
 *
 * Phase 3 (Confirm): Hammer the found minimum with many more trials to ensure
 * stability.
 *
 * The exported function returns the minimum reliable value; callers should
 * apply their own safety margin.
 *
 * @module
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.findStreamFlushMillis = findStreamFlushMillis;
const BatchCluster_1 = require("./BatchCluster");
const Logger_1 = require("./Logger");
const DEFAULT_MAX_PROCS = 4;
const DEFAULT_COARSE_TASKS = 10;
const DEFAULT_VALIDATION_TASKS = 50;
const DEFAULT_VALIDATION_TRIALS = 5;
const DEFAULT_VALIDATION_RADIUS = 3;
const DEFAULT_CONFIRMATION_TRIALS = 20;
const DEFAULT_CONFIRMATION_TASKS = 50;
// ---------------------------------------------------------------------------
// Trial runner
// ---------------------------------------------------------------------------
async function runTrial(opts, candidateMs, taskCount, timeoutMs) {
    let noTaskDataCount = 0;
    const maxProcs = opts.maxProcs ?? DEFAULT_MAX_PROCS;
    const bc = new BatchCluster_1.BatchCluster({
        streamFlushMillis: candidateMs,
        maxProcs,
        minDelayBetweenSpawnMillis: 0,
        versionCommand: opts.versionCommand,
        pass: opts.pass,
        fail: opts.fail,
        exitCommand: opts.exitCommand,
        maxTasksPerProcess: taskCount + 10,
        taskTimeoutMillis: 5000,
        spawnTimeoutMillis: 15000,
        cleanupChildProcsOnExit: false,
        processFactory: opts.processFactory,
    });
    // Resolve on first noTaskData — we short-circuit immediately.
    let resolveNoTaskData;
    const noTaskDataSignal = new Promise((resolve) => {
        resolveNoTaskData = () => resolve("noTaskData");
    });
    bc.on("noTaskData", () => {
        noTaskDataCount++;
        if (noTaskDataCount === 1)
            resolveNoTaskData();
    });
    const start = Date.now();
    let tasksCompleted = 0;
    let taskErrors = 0;
    let timedOut = false;
    // Keep the event loop alive (BatchCluster's intervals are unref'd).
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    const keepalive = setInterval(() => { }, 1000);
    try {
        const promises = [];
        for (let i = 0; i < taskCount; i++) {
            promises.push(bc.enqueueTask(opts.taskFactory(i)));
        }
        const timeout = new Promise((resolve) => {
            const t = setTimeout(() => resolve("timeout"), timeoutMs);
            t.unref();
        });
        const raceResult = await Promise.race([
            Promise.allSettled(promises),
            timeout,
            noTaskDataSignal,
        ]);
        if (raceResult === "timeout") {
            timedOut = true;
        }
        else if (raceResult !== "noTaskData") {
            for (const r of raceResult) {
                if (r.status === "fulfilled")
                    tasksCompleted++;
                else
                    taskErrors++;
            }
        }
    }
    finally {
        // Snapshot count before cleanup — shutdown can emit spurious noTaskData.
        const finalCount = noTaskDataCount;
        await bc.end(true);
        clearInterval(keepalive);
        noTaskDataCount = finalCount;
    }
    return {
        noTaskDataEvents: noTaskDataCount,
        taskErrors,
        tasksCompleted,
        durationMs: Date.now() - start,
        timedOut,
    };
}
// ---------------------------------------------------------------------------
// Phase 1: Coarse binary search
// ---------------------------------------------------------------------------
async function coarseSearch(opts, lo, hi) {
    const log = opts.logger?.() ?? Logger_1.NoLogger;
    const coarseTasks = opts.coarseTasks ?? DEFAULT_COARSE_TASKS;
    const maxProcs = opts.maxProcs ?? DEFAULT_MAX_PROCS;
    log.info(`Phase 1: Coarse binary search for streamFlushMillis in [${lo}, ${hi}]` +
        ` (${coarseTasks} tasks/step, ${maxProcs} procs)`);
    while (lo < hi) {
        const mid = Math.floor((lo + hi) / 2);
        const result = await runTrial(opts, mid, coarseTasks, 15_000);
        const label = `${mid}ms`.padEnd(6);
        const range = `[${lo}, ${hi}]`;
        if (result.timedOut) {
            log.info(`  ${label} ${range} -> TIMEOUT (${result.durationMs}ms)`);
            lo = mid + 1;
        }
        else if (result.noTaskDataEvents > 0) {
            log.info(`  ${label} ${range} -> FAIL (noTaskData, ${result.durationMs}ms)`);
            lo = mid + 1;
        }
        else {
            log.info(`  ${label} ${range} -> PASS` +
                ` (${result.tasksCompleted} ok, ${result.taskErrors} err, ${result.durationMs}ms)`);
            hi = mid;
        }
    }
    log.info(`  Coarse threshold: ${lo}ms`);
    return lo;
}
// ---------------------------------------------------------------------------
// Phase 2: Validate around the threshold
// ---------------------------------------------------------------------------
async function validateThreshold(opts, coarseThreshold, searchHi) {
    const log = opts.logger?.() ?? Logger_1.NoLogger;
    const validationTasks = opts.validationTasks ?? DEFAULT_VALIDATION_TASKS;
    const validationTrials = opts.validationTrials ?? DEFAULT_VALIDATION_TRIALS;
    const validationRadius = opts.validationRadius ?? DEFAULT_VALIDATION_RADIUS;
    const from = Math.max(0, coarseThreshold - validationRadius);
    const to = Math.min(searchHi, coarseThreshold + validationRadius);
    log.info(`Phase 2: Validating streamFlushMillis in [${from}, ${to}]` +
        ` (${validationTrials} trials x ${validationTasks} tasks)`);
    let minimumReliable;
    for (let ms = from; ms <= to; ms++) {
        let passedAll = true;
        for (let t = 0; t < validationTrials; t++) {
            const result = await runTrial(opts, ms, validationTasks, 30_000);
            if (result.noTaskDataEvents > 0 || result.timedOut) {
                const reason = result.timedOut ? "timeout" : "noTaskData";
                log.info(`  ${ms}ms: FAIL trial ${t + 1}/${validationTrials}` +
                    ` (${reason}, ${result.durationMs}ms)`);
                passedAll = false;
                break;
            }
        }
        if (passedAll) {
            log.info(`  ${ms}ms: PASS (${validationTrials}/${validationTrials} trials)`);
            minimumReliable ??= ms;
        }
    }
    if (minimumReliable == null) {
        log.warn(`  WARNING: no value in [${from}, ${to}] passed all trials — using ${to}ms`);
        minimumReliable = to;
    }
    return minimumReliable;
}
// ---------------------------------------------------------------------------
// Phase 3: Confirm the minimum with many trials
// ---------------------------------------------------------------------------
async function confirmMinimum(opts, startMs, searchHi) {
    const log = opts.logger?.() ?? Logger_1.NoLogger;
    const confirmationTrials = opts.confirmationTrials ?? DEFAULT_CONFIRMATION_TRIALS;
    const confirmationTasks = opts.confirmationTasks ?? DEFAULT_CONFIRMATION_TASKS;
    log.info(`Phase 3: Confirming streamFlushMillis minimum` +
        ` (${confirmationTrials} trials x ${confirmationTasks} tasks)`);
    for (let ms = startMs; ms <= searchHi; ms++) {
        let failed = false;
        for (let t = 0; t < confirmationTrials; t++) {
            const result = await runTrial(opts, ms, confirmationTasks, 30_000);
            if (result.noTaskDataEvents > 0 || result.timedOut) {
                const reason = result.timedOut ? "timeout" : "noTaskData";
                log.info(`  ${ms}ms: FAIL trial ${t + 1}/${confirmationTrials}` +
                    ` (${reason}, ${result.durationMs}ms)`);
                failed = true;
                break;
            }
        }
        if (!failed) {
            log.info(`  ${ms}ms: PASS (${confirmationTrials}/${confirmationTrials} trials)`);
            return ms;
        }
    }
    log.warn(`  WARNING: no value up to ${searchHi}ms passed all trials`);
    return searchHi;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Find the minimum reliable `streamFlushMillis` value for the given child
 * process on the current hardware.
 *
 * Returns the minimum `streamFlushMillis` value that produced zero
 * `noTaskData` events across all confirmation trials. Because the
 * confirmation phase already tests the worst case observed during
 * discovery, the returned value is a near-ceiling, not a mean — similar in
 * spirit to how TCP's RTO (RFC 6298) is `SRTT + 4*RTTVAR` rather than a
 * flat multiplier. Callers needing additional headroom for transient load
 * spikes should add a small additive constant (e.g. `result + 5`), since
 * OS scheduling jitter is an additive phenomenon independent of the
 * baseline value.
 *
 * @example
 * ```typescript
 * import { findStreamFlushMillis, Task } from "batch-cluster";
 *
 * const minReliable = await findStreamFlushMillis({
 *   processFactory: () => spawn("perl", [exiftoolPath, "-stay_open", "True", "-@", "-"]),
 *   versionCommand: "-ver\n-execute\n",
 *   pass: "{ready}",
 *   fail: "{ready}",
 *   exitCommand: "-stay_open\nFalse\n",
 *   taskFactory: (i) => {
 *     const cmds = ["-json\n/clean.jpg\n-execute\n", "-validate\n/warn.jpg\n-execute\n"];
 *     return new Task(cmds[i % cmds.length], (stdout) => stdout);
 *   },
 * });
 * const streamFlushMillis = minReliable + 5; // additive headroom for load spikes
 * ```
 */
async function findStreamFlushMillis(options) {
    const lo = options.lo ?? 0;
    const hi = options.hi ?? 500;
    const coarse = await coarseSearch(options, lo, hi);
    const validated = await validateThreshold(options, coarse, hi);
    return confirmMinimum(options, validated, hi);
}
//# sourceMappingURL=FindFlushThresholds.js.map