"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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildCpuThermalPmu = buildCpuThermalPmu;
/**
 * CPU Frequency / Thermal / PMU Aggregator (Spark Plan 13)
 *
 * Folds independent attribution facets into one
 * `CpuThermalPmuContract`. Caller passes only the facets they have
 * data for; missing facets surface as `unsupportedReason` rather than
 * being zero-filled.
 */
const sparkContracts_1 = require("../types/sparkContracts");
/**
 * Compute fraction-of-window residency for each (cpu, freq) bucket.
 *
 * Codex round 7 caught that callers can pass multiple samples for the
 * same (cpu, freqHz) — common when a CPU revisits a frequency during
 * the window — so we aggregate by bucket before computing fractions
 * instead of emitting split rows that consumers have to re-sum.
 */
function buildResidency(range, samples) {
    if (!samples || samples.length === 0)
        return undefined;
    const totalDur = Math.max(1, range.endNs - range.startNs);
    const byBucket = new Map();
    for (const s of samples) {
        const key = `${s.cpu}|${s.freqHz}`;
        const existing = byBucket.get(key);
        if (existing) {
            existing.durNs += s.durNs;
        }
        else {
            byBucket.set(key, { cpu: s.cpu, freqHz: s.freqHz, durNs: s.durNs });
        }
    }
    return Array.from(byBucket.values())
        .sort((a, b) => a.cpu - b.cpu || a.freqHz - b.freqHz)
        .map(s => ({
        cpu: s.cpu,
        freqHz: s.freqHz,
        durNs: s.durNs,
        fraction: s.durNs / totalDur,
    }));
}
/**
 * Decide a thermal verdict given the hottest sample's tempMc and
 * throttleStage. Thresholds chosen to match common Android thermal HAL
 * brackets (cool < 70°C, soft 70-85, hard 85-95, shutdown >= 95).
 *
 * Codex round 4 caught that traces from thermal zones often expose only
 * temperature counters (no HAL stage), so 95°C+ samples without a
 * throttleStage need to surface as `shutdown_imminent` based on
 * temperature alone — otherwise pre-shutdown conditions are
 * under-reported as ordinary hard throttling.
 */
function decideThermal(samples) {
    if (!samples || samples.length === 0)
        return undefined;
    const hottest = samples.reduce((acc, s) => (s.tempMc > acc.tempMc ? s : acc), samples[0]);
    // Shutdown takes priority — either explicit HAL stage or pure
    // temperature evidence past the documented shutdown threshold.
    if ((hottest.throttleStage !== undefined && hottest.throttleStage >= 3)
        || hottest.tempMc >= 95000) {
        return 'shutdown_imminent';
    }
    if (hottest.throttleStage === 2 || hottest.tempMc >= 85000)
        return 'hard_throttle';
    if (hottest.throttleStage === 1 || hottest.tempMc >= 70000)
        return 'soft_throttle';
    // Codex round 6 caught that classifying 60–69°C as soft_throttle
    // contradicted the documented "cool < 70°C" bracket. Keep cool until
    // we have either a HAL stage or a temperature past 70°C.
    return 'cool';
}
/**
 * "Has data" check that rejects both undefined and []. Codex review caught
 * the same bug here that ioNetworkWakeup had: empty result arrays were
 * being marked as supported coverage instead of missing evidence.
 */
function hasRows(rows) {
    return Array.isArray(rows) && rows.length > 0;
}
function buildCpuThermalPmu(options) {
    const freqHasRows = hasRows(options.freqSamples);
    const thermalHasRows = hasRows(options.thermalSamples);
    const pmuHasRows = hasRows(options.pmuSamples);
    const cpuFreqResidency = freqHasRows ? buildResidency(options.range, options.freqSamples) : undefined;
    const thermalSamples = thermalHasRows
        ? options.thermalSamples.map(s => ({
            zone: s.zone,
            ts: s.ts,
            tempMc: s.tempMc,
            ...(s.throttleStage !== undefined ? { throttleStage: s.throttleStage } : {}),
        }))
        : undefined;
    const thermalDecision = thermalHasRows ? decideThermal(options.thermalSamples) : undefined;
    const pmuAttribution = pmuHasRows
        ? options.pmuSamples.map(s => ({
            counter: s.counter,
            ...(s.utid !== undefined ? { utid: s.utid } : {}),
            ...(s.process ? { process: s.process } : {}),
            ...(s.thread ? { thread: s.thread } : {}),
            value: s.value,
            ...(s.derived ? { derived: s.derived } : {}),
        }))
        : undefined;
    let smoothVsJank;
    if (options.smoothFraction !== undefined && options.jankFraction !== undefined) {
        smoothVsJank = {
            smoothFraction: options.smoothFraction,
            jankFraction: options.jankFraction,
            delta: options.smoothFraction - options.jankFraction,
        };
    }
    const allEmpty = !cpuFreqResidency
        && !thermalSamples
        && !pmuAttribution
        && !smoothVsJank;
    return {
        ...(0, sparkContracts_1.makeSparkProvenance)({
            source: 'cpu-thermal-pmu',
            ...(allEmpty ? { unsupportedReason: 'no CPU / thermal / PMU samples supplied' } : {}),
        }),
        range: options.range,
        ...(cpuFreqResidency ? { cpuFreqResidency } : {}),
        ...(thermalSamples ? { thermalSamples } : {}),
        ...(thermalDecision ? { thermalDecision } : {}),
        ...(pmuAttribution ? { pmuAttribution } : {}),
        ...(smoothVsJank ? { smoothVsJankComparison: smoothVsJank } : {}),
        coverage: [
            { sparkId: 8, planId: '13', status: cpuFreqResidency ? 'implemented' : 'scaffolded' },
            { sparkId: 9, planId: '13', status: thermalSamples ? 'implemented' : 'scaffolded' },
            { sparkId: 10, planId: '13', status: pmuAttribution ? 'implemented' : 'scaffolded' },
            { sparkId: 35, planId: '13', status: thermalDecision ? 'implemented' : 'scaffolded' },
        ],
    };
}
//# sourceMappingURL=cpuThermalPmu.js.map