"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.EntityStore = void 0;
exports.createEntityStore = createEntityStore;
const CURRENT_SNAPSHOT_VERSION = 1;
// =============================================================================
// EntityStore Class
// =============================================================================
/**
 * Session-scoped entity cache for frames, sessions, and other performance entities.
 *
 * Usage pattern:
 * 1. After strategy executor runs, capture entities from responses
 * 2. Upsert captured entities into store
 * 3. On drill-down follow-up, resolve from store (cache hit = no enrichment SQL)
 * 4. Mark analyzed entities to support "extend" (analyze more) scenarios
 *
 * Supported entity types (Phase 3):
 * - frame: Rendering frames (jank analysis)
 * - session: Scroll sessions
 * - cpu_slice: CPU scheduling slices
 * - binder: IPC/Binder transactions
 * - gc: Garbage collection events
 * - memory: Memory allocations and events
 * - generic: Extensible for any other entity type
 */
// P2-B4: Per-type entity caps to prevent unbounded growth in long sessions
const ENTITY_CAPS = {
    frame: 200,
    session: 50,
    cpu_slice: 100,
    binder: 100,
    gc: 50,
    memory: 50,
    generic: 100,
};
class EntityStore {
    constructor() {
        this.framesById = new Map();
        this.sessionsById = new Map();
        // New entity type storage (Phase 3)
        this.cpuSlicesById = new Map();
        this.bindersById = new Map();
        this.gcsById = new Map();
        this.memoriesById = new Map();
        this.genericsById = new Map();
        this.analyzedFrameIds = new Set();
        this.analyzedSessionIds = new Set();
        this.lastCandidateFrameIds = [];
        this.lastCandidateSessionIds = [];
        // Generalized analyzed tracking (Phase 3)
        this.analyzedEntityIds = new Map();
    }
    // P2-B4: Evict oldest entries when a map exceeds its cap
    enforceCapLRU(map, cap) {
        if (map.size <= cap)
            return;
        // Sort by updated_at ascending, evict oldest
        const entries = Array.from(map.entries())
            .sort((a, b) => (a[1].updated_at || 0) - (b[1].updated_at || 0));
        const toRemove = entries.slice(0, map.size - cap);
        for (const [key] of toRemove) {
            map.delete(key);
        }
    }
    // ==========================================================================
    // Frame Operations
    // ==========================================================================
    /**
     * Upsert a frame entity. Merges with existing if present.
     * Newer data (by updated_at) wins for non-null fields.
     */
    upsertFrame(entity) {
        const id = normalizeId(entity.frame_id);
        if (!id)
            return;
        const existing = this.framesById.get(id);
        if (existing) {
            const merged = mergeEntity(existing, { ...entity, frame_id: id });
            this.framesById.set(id, merged);
        }
        else {
            this.framesById.set(id, { ...entity, frame_id: id, updated_at: Date.now() });
            this.enforceCapLRU(this.framesById, ENTITY_CAPS.frame);
        }
    }
    /**
     * Get a frame entity by ID.
     * Tries both original ID and normalized string version.
     */
    getFrame(id) {
        const normalized = normalizeId(id);
        return this.framesById.get(normalized);
    }
    /**
     * Get all frame entities.
     */
    getAllFrames() {
        return Array.from(this.framesById.values());
    }
    /**
     * Mark a frame as analyzed (for extend support).
     */
    markFrameAnalyzed(id) {
        this.analyzedFrameIds.add(normalizeId(id));
        // P2-5: Cap analyzed sets to prevent unbounded growth in long sessions
        if (this.analyzedFrameIds.size > EntityStore.ANALYZED_SET_CAP) {
            const iter = this.analyzedFrameIds.values();
            this.analyzedFrameIds.delete(iter.next().value);
        }
    }
    /**
     * Check if a frame was already analyzed in a previous drill-down.
     */
    wasFrameAnalyzed(id) {
        return this.analyzedFrameIds.has(normalizeId(id));
    }
    /**
     * Get all analyzed frame IDs.
     * Used by IncrementalAnalyzer to build PreviousAnalysisState.
     */
    getAnalyzedFrameIds() {
        return Array.from(this.analyzedFrameIds);
    }
    /**
     * Set the candidate frame list from last discovery stage.
     * Used by extend to know which frames can be analyzed next.
     */
    setLastCandidateFrames(ids) {
        this.lastCandidateFrameIds = ids.map(normalizeId);
    }
    /**
     * Get candidate frames that haven't been analyzed yet.
     */
    getUnanalyzedCandidateFrames() {
        return this.lastCandidateFrameIds.filter(id => !this.analyzedFrameIds.has(id));
    }
    /**
     * Get all candidate frame IDs from last discovery.
     */
    getLastCandidateFrames() {
        return [...this.lastCandidateFrameIds];
    }
    // ==========================================================================
    // Session Operations
    // ==========================================================================
    /**
     * Upsert a session entity. Merges with existing if present.
     */
    upsertSession(entity) {
        const id = normalizeId(entity.session_id);
        if (!id)
            return;
        const existing = this.sessionsById.get(id);
        if (existing) {
            const merged = mergeEntity(existing, { ...entity, session_id: id });
            this.sessionsById.set(id, merged);
        }
        else {
            this.sessionsById.set(id, { ...entity, session_id: id, updated_at: Date.now() });
            this.enforceCapLRU(this.sessionsById, ENTITY_CAPS.session);
        }
    }
    /**
     * Get a session entity by ID.
     */
    getSession(id) {
        const normalized = normalizeId(id);
        return this.sessionsById.get(normalized);
    }
    /**
     * Get all session entities.
     */
    getAllSessions() {
        return Array.from(this.sessionsById.values());
    }
    /**
     * Mark a session as analyzed.
     */
    markSessionAnalyzed(id) {
        this.analyzedSessionIds.add(normalizeId(id));
        if (this.analyzedSessionIds.size > EntityStore.ANALYZED_SET_CAP) {
            const iter = this.analyzedSessionIds.values();
            this.analyzedSessionIds.delete(iter.next().value);
        }
    }
    /**
     * Check if a session was already analyzed.
     */
    wasSessionAnalyzed(id) {
        return this.analyzedSessionIds.has(normalizeId(id));
    }
    /**
     * Get all analyzed session IDs.
     * Used by IncrementalAnalyzer to build PreviousAnalysisState.
     */
    getAnalyzedSessionIds() {
        return Array.from(this.analyzedSessionIds);
    }
    /**
     * Set the candidate session list from last discovery stage.
     */
    setLastCandidateSessions(ids) {
        this.lastCandidateSessionIds = ids.map(normalizeId);
    }
    /**
     * Get candidate sessions that haven't been analyzed yet.
     */
    getUnanalyzedCandidateSessions() {
        return this.lastCandidateSessionIds.filter(id => !this.analyzedSessionIds.has(id));
    }
    /**
     * Get all candidate session IDs from last discovery.
     */
    getLastCandidateSessions() {
        return [...this.lastCandidateSessionIds];
    }
    // ==========================================================================
    // CPU Slice Operations (Phase 3)
    // ==========================================================================
    /**
     * Upsert a CPU slice entity.
     */
    upsertCpuSlice(entity) {
        const id = normalizeId(entity.slice_id);
        if (!id)
            return;
        const existing = this.cpuSlicesById.get(id);
        if (existing) {
            const merged = mergeEntity(existing, { ...entity, slice_id: id });
            this.cpuSlicesById.set(id, merged);
        }
        else {
            this.cpuSlicesById.set(id, { ...entity, slice_id: id, updated_at: Date.now() });
            this.enforceCapLRU(this.cpuSlicesById, ENTITY_CAPS.cpu_slice);
        }
    }
    getCpuSlice(id) {
        return this.cpuSlicesById.get(normalizeId(id));
    }
    getAllCpuSlices() {
        return Array.from(this.cpuSlicesById.values());
    }
    // ==========================================================================
    // Binder Operations (Phase 3)
    // ==========================================================================
    /**
     * Upsert a Binder transaction entity.
     */
    upsertBinder(entity) {
        const id = normalizeId(entity.transaction_id);
        if (!id)
            return;
        const existing = this.bindersById.get(id);
        if (existing) {
            const merged = mergeEntity(existing, { ...entity, transaction_id: id });
            this.bindersById.set(id, merged);
        }
        else {
            this.bindersById.set(id, { ...entity, transaction_id: id, updated_at: Date.now() });
            this.enforceCapLRU(this.bindersById, ENTITY_CAPS.binder);
        }
    }
    getBinder(id) {
        return this.bindersById.get(normalizeId(id));
    }
    getAllBinders() {
        return Array.from(this.bindersById.values());
    }
    // ==========================================================================
    // GC Operations (Phase 3)
    // ==========================================================================
    /**
     * Upsert a GC event entity.
     */
    upsertGc(entity) {
        const id = normalizeId(entity.gc_id);
        if (!id)
            return;
        const existing = this.gcsById.get(id);
        if (existing) {
            const merged = mergeEntity(existing, { ...entity, gc_id: id });
            this.gcsById.set(id, merged);
        }
        else {
            this.gcsById.set(id, { ...entity, gc_id: id, updated_at: Date.now() });
            this.enforceCapLRU(this.gcsById, ENTITY_CAPS.gc);
        }
    }
    getGc(id) {
        return this.gcsById.get(normalizeId(id));
    }
    getAllGcs() {
        return Array.from(this.gcsById.values());
    }
    // ==========================================================================
    // Memory Operations (Phase 3)
    // ==========================================================================
    /**
     * Upsert a memory event entity.
     */
    upsertMemory(entity) {
        const id = normalizeId(entity.memory_id);
        if (!id)
            return;
        const existing = this.memoriesById.get(id);
        if (existing) {
            const merged = mergeEntity(existing, { ...entity, memory_id: id });
            this.memoriesById.set(id, merged);
        }
        else {
            this.memoriesById.set(id, { ...entity, memory_id: id, updated_at: Date.now() });
            this.enforceCapLRU(this.memoriesById, ENTITY_CAPS.memory);
        }
    }
    getMemory(id) {
        return this.memoriesById.get(normalizeId(id));
    }
    getAllMemories() {
        return Array.from(this.memoriesById.values());
    }
    // ==========================================================================
    // Generic Entity Operations (Phase 3)
    // ==========================================================================
    /**
     * Upsert a generic entity (extensible for any entity type).
     */
    upsertGeneric(entity) {
        const id = normalizeId(entity.entity_id);
        if (!id)
            return;
        const existing = this.genericsById.get(id);
        if (existing) {
            const merged = mergeEntity(existing, { ...entity, entity_id: id });
            this.genericsById.set(id, merged);
        }
        else {
            this.genericsById.set(id, { ...entity, entity_id: id, updated_at: Date.now() });
            this.enforceCapLRU(this.genericsById, ENTITY_CAPS.generic);
        }
    }
    getGeneric(id) {
        return this.genericsById.get(normalizeId(id));
    }
    getAllGenerics() {
        return Array.from(this.genericsById.values());
    }
    /**
     * Get all generic entities of a specific type.
     */
    getGenericsByType(entityType) {
        return this.getAllGenerics().filter(e => e.entity_type === entityType);
    }
    // ==========================================================================
    // Generalized Analysis Tracking (Phase 3)
    // ==========================================================================
    /**
     * Mark any entity type as analyzed.
     * @param entityType - The type of entity (e.g., 'cpu_slice', 'binder')
     * @param id - The entity ID
     */
    markEntityAnalyzed(entityType, id) {
        if (!this.analyzedEntityIds.has(entityType)) {
            this.analyzedEntityIds.set(entityType, new Set());
        }
        const set = this.analyzedEntityIds.get(entityType);
        set.add(normalizeId(id));
        if (set.size > EntityStore.ANALYZED_SET_CAP) {
            const iter = set.values();
            set.delete(iter.next().value);
        }
    }
    /**
     * Check if any entity type was analyzed.
     */
    wasEntityAnalyzed(entityType, id) {
        const set = this.analyzedEntityIds.get(entityType);
        return set ? set.has(normalizeId(id)) : false;
    }
    /**
     * Get all analyzed entity IDs for a type.
     */
    getAnalyzedEntityIds(entityType) {
        const set = this.analyzedEntityIds.get(entityType);
        return set ? Array.from(set) : [];
    }
    // ==========================================================================
    // Bulk Operations
    // ==========================================================================
    /**
     * Upsert multiple frames at once.
     */
    upsertFrames(entities) {
        for (const entity of entities) {
            this.upsertFrame(entity);
        }
    }
    /**
     * Upsert multiple sessions at once.
     */
    upsertSessions(entities) {
        for (const entity of entities) {
            this.upsertSession(entity);
        }
    }
    /**
     * Upsert multiple CPU slices at once.
     */
    upsertCpuSlices(entities) {
        for (const entity of entities) {
            this.upsertCpuSlice(entity);
        }
    }
    /**
     * Upsert multiple Binder transactions at once.
     */
    upsertBinders(entities) {
        for (const entity of entities) {
            this.upsertBinder(entity);
        }
    }
    /**
     * Upsert multiple GC events at once.
     */
    upsertGcs(entities) {
        for (const entity of entities) {
            this.upsertGc(entity);
        }
    }
    /**
     * Upsert multiple memory events at once.
     */
    upsertMemories(entities) {
        for (const entity of entities) {
            this.upsertMemory(entity);
        }
    }
    /**
     * Upsert multiple generic entities at once.
     */
    upsertGenerics(entities) {
        for (const entity of entities) {
            this.upsertGeneric(entity);
        }
    }
    // ==========================================================================
    // Statistics
    // ==========================================================================
    /**
     * Get store statistics for debugging/logging.
     */
    getStats() {
        const totalEntityCount = this.framesById.size +
            this.sessionsById.size +
            this.cpuSlicesById.size +
            this.bindersById.size +
            this.gcsById.size +
            this.memoriesById.size +
            this.genericsById.size;
        return {
            frameCount: this.framesById.size,
            sessionCount: this.sessionsById.size,
            cpuSliceCount: this.cpuSlicesById.size,
            binderCount: this.bindersById.size,
            gcCount: this.gcsById.size,
            memoryCount: this.memoriesById.size,
            genericCount: this.genericsById.size,
            analyzedFrameCount: this.analyzedFrameIds.size,
            analyzedSessionCount: this.analyzedSessionIds.size,
            candidateFrameCount: this.lastCandidateFrameIds.length,
            candidateSessionCount: this.lastCandidateSessionIds.length,
            totalEntityCount,
        };
    }
    // ==========================================================================
    // Serialization
    // ==========================================================================
    /**
     * Serialize to a JSON-safe snapshot for persistence.
     */
    serialize() {
        // Convert analyzedEntityIds Map to Record
        const analyzedEntityIdsRecord = {};
        for (const [type, ids] of this.analyzedEntityIds.entries()) {
            analyzedEntityIdsRecord[type] = Array.from(ids);
        }
        return {
            version: CURRENT_SNAPSHOT_VERSION,
            framesById: Array.from(this.framesById.entries()),
            sessionsById: Array.from(this.sessionsById.entries()),
            // New entity types (Phase 3)
            cpuSlicesById: Array.from(this.cpuSlicesById.entries()),
            bindersById: Array.from(this.bindersById.entries()),
            gcsById: Array.from(this.gcsById.entries()),
            memoriesById: Array.from(this.memoriesById.entries()),
            genericsById: Array.from(this.genericsById.entries()),
            // Incremental execution support
            analyzedFrameIds: Array.from(this.analyzedFrameIds),
            analyzedSessionIds: Array.from(this.analyzedSessionIds),
            lastCandidateFrameIds: this.lastCandidateFrameIds,
            lastCandidateSessionIds: this.lastCandidateSessionIds,
            // Generalized analyzed tracking
            analyzedEntityIds: analyzedEntityIdsRecord,
        };
    }
    /**
     * Deserialize from a snapshot.
     */
    static deserialize(snapshot) {
        const store = new EntityStore();
        // Version migration (for future compatibility)
        if (!snapshot.version || snapshot.version < CURRENT_SNAPSHOT_VERSION) {
            // Handle older versions - v1 snapshots are compatible
        }
        // Restore frame and session maps (always present)
        if (snapshot.framesById) {
            store.framesById = new Map(snapshot.framesById);
        }
        if (snapshot.sessionsById) {
            store.sessionsById = new Map(snapshot.sessionsById);
        }
        // Restore new entity type maps (Phase 3 - may be absent in older snapshots)
        if (snapshot.cpuSlicesById) {
            store.cpuSlicesById = new Map(snapshot.cpuSlicesById);
        }
        if (snapshot.bindersById) {
            store.bindersById = new Map(snapshot.bindersById);
        }
        if (snapshot.gcsById) {
            store.gcsById = new Map(snapshot.gcsById);
        }
        if (snapshot.memoriesById) {
            store.memoriesById = new Map(snapshot.memoriesById);
        }
        if (snapshot.genericsById) {
            store.genericsById = new Map(snapshot.genericsById);
        }
        // Restore tracking sets
        if (snapshot.analyzedFrameIds) {
            store.analyzedFrameIds = new Set(snapshot.analyzedFrameIds);
        }
        if (snapshot.analyzedSessionIds) {
            store.analyzedSessionIds = new Set(snapshot.analyzedSessionIds);
        }
        // Restore candidate lists
        if (snapshot.lastCandidateFrameIds) {
            store.lastCandidateFrameIds = snapshot.lastCandidateFrameIds;
        }
        if (snapshot.lastCandidateSessionIds) {
            store.lastCandidateSessionIds = snapshot.lastCandidateSessionIds;
        }
        // Restore generalized analyzed tracking (Phase 3)
        if (snapshot.analyzedEntityIds) {
            for (const [type, ids] of Object.entries(snapshot.analyzedEntityIds)) {
                store.analyzedEntityIds.set(type, new Set(ids));
            }
        }
        return store;
    }
    /**
     * Clear all data (for testing or reset).
     */
    clear() {
        this.framesById.clear();
        this.sessionsById.clear();
        this.cpuSlicesById.clear();
        this.bindersById.clear();
        this.gcsById.clear();
        this.memoriesById.clear();
        this.genericsById.clear();
        this.analyzedFrameIds.clear();
        this.analyzedSessionIds.clear();
        this.analyzedEntityIds.clear();
        this.lastCandidateFrameIds = [];
        this.lastCandidateSessionIds = [];
    }
}
exports.EntityStore = EntityStore;
// Incremental analysis tracking (P2-5: capped to prevent unbounded growth)
EntityStore.ANALYZED_SET_CAP = 500;
// =============================================================================
// Utilities
// =============================================================================
/**
 * Normalize any ID to string for consistent Map keys.
 * Handles number, string, BigInt, undefined/null.
 */
function normalizeId(id) {
    if (id === null || id === undefined)
        return '';
    if (typeof id === 'bigint')
        return id.toString();
    return String(id);
}
/**
 * Merge two entities, preferring newer non-null values.
 */
function mergeEntity(existing, incoming) {
    const result = { ...existing };
    const now = Date.now();
    for (const [key, value] of Object.entries(incoming)) {
        if (value !== null && value !== undefined) {
            result[key] = value;
        }
    }
    result.updated_at = now;
    return result;
}
// =============================================================================
// Factory
// =============================================================================
/**
 * Create a new EntityStore instance.
 */
function createEntityStore() {
    return new EntityStore();
}
//# sourceMappingURL=entityStore.js.map