var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { SQL } from "../Utils/AnalyticsDatabase/Statement";
import { getQuerySettings } from "../Utils/AnalyticsDatabase/QuerySettingsHelper";
import ProfileSampleDatabaseService from "./ProfileSampleService";
import ProfileDatabaseService from "./ProfileService";
import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
import Includes from "../../Types/BaseDatabase/Includes";
import AnalyticsTableName from "../../Types/AnalyticsDatabase/AnalyticsTableName";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
// --- Service ---
export class ProfileAggregationService {
    /**
     * Build a flamegraph tree from ProfileSample records.
     *
     * Each sample has a `stacktrace` array where each element follows the
     * format "functionName@fileName:lineNumber".  The array is ordered
     * LEAF-FIRST (index 0 = leaf, last index = root) — the pprof
     * Sample.location_id and OTLP Stack.location_indices conventions both
     * store the innermost frame first, and ingestion preserves wire order.
     *
     * Samples are pre-aggregated in ClickHouse by stack identity, then
     * stacks sharing common prefixes are merged into a tree of
     * ProfileFlamegraphNode objects.
     */
    static async getFlamegraph(request) {
        const statement = ProfileAggregationService.buildGroupedStackQuery(request);
        const dbResult = await ProfileSampleDatabaseService.executeQuery(statement);
        const response = await dbResult.json();
        const rows = response.data || [];
        const truncated = rows.length >= ProfileAggregationService.MAX_STACK_FETCH;
        // Build the tree from the pre-aggregated stacks
        const root = {
            functionName: "(root)",
            fileName: "",
            lineNumber: 0,
            selfValue: 0,
            totalValue: 0,
            children: [],
            frameType: "",
        };
        /*
         * Per-node child index so frame lookup is O(1) instead of a linear
         * children.find() — wide fan-out (thousands of siblings under root)
         * made the scan quadratic. Kept outside the nodes so the serialized
         * tree shape is unchanged.
         */
        const childIndex = new WeakMap();
        for (const row of rows) {
            const stacktrace = row["stacktrace"] || [];
            const frameTypes = row["frameTypes"] || [];
            const value = Number(row["totalValue"] || 0);
            if (stacktrace.length === 0) {
                continue;
            }
            /*
             * Walk down the tree, creating nodes as needed. Stored stacks are
             * leaf-first, so iterate from the END of the array (root) toward
             * index 0 (leaf) to build the tree root-to-leaf.
             */
            let currentNode = root;
            currentNode.totalValue += value;
            for (let i = stacktrace.length - 1; i >= 0; i--) {
                const frame = ProfileAggregationService.parseFrame(stacktrace[i]);
                const frameType = frameTypes[i] || "";
                const frameKey = `${frame.functionName}@${frame.fileName}:${frame.lineNumber}`;
                let index = childIndex.get(currentNode);
                if (!index) {
                    index = new Map();
                    childIndex.set(currentNode, index);
                }
                let childNode = index.get(frameKey);
                if (!childNode) {
                    childNode = {
                        functionName: frame.functionName,
                        fileName: frame.fileName,
                        lineNumber: frame.lineNumber,
                        selfValue: 0,
                        totalValue: 0,
                        children: [],
                        frameType: frameType,
                    };
                    currentNode.children.push(childNode);
                    index.set(frameKey, childNode);
                }
                childNode.totalValue += value;
                // Index 0 is the leaf frame in leaf-first storage — it gets selfValue
                if (i === 0) {
                    childNode.selfValue += value;
                }
                currentNode = childNode;
            }
        }
        return { flamegraph: root, truncated };
    }
    /**
     * Return the top functions aggregated across samples, sorted by the
     * requested metric (selfValue, totalValue, or sampleCount), along with
     * the pre-limit window total so callers can show "% of total" figures.
     */
    static async getFunctionList(request) {
        var _a, _b;
        const filters = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ projectId: request.projectId }, (request.startTime !== undefined && { startTime: request.startTime })), (request.endTime !== undefined && { endTime: request.endTime })), (request.profileId !== undefined && { profileId: request.profileId })), (request.serviceIds !== undefined && {
            serviceIds: request.serviceIds,
        })), (request.profileType !== undefined && {
            profileType: request.profileType,
        })), (request.profileTypes !== undefined && {
            profileTypes: request.profileTypes,
        }));
        const statement = ProfileAggregationService.buildGroupedStackQuery(filters);
        const windowTotalStatement = ProfileAggregationService.buildWindowTotalQuery(filters);
        /*
         * The window total must be computed over ALL matching rows (pre-limit),
         * so it cannot be derived from the capped grouped result. It is a
         * single cheap aggregate, so run both queries concurrently.
         */
        const [dbResult, windowTotalDbResult] = await Promise.all([
            ProfileSampleDatabaseService.executeQuery(statement),
            ProfileSampleDatabaseService.executeQuery(windowTotalStatement),
        ]);
        const response = await dbResult.json();
        const windowTotalResponse = await windowTotalDbResult.json();
        const rows = response.data || [];
        const truncated = rows.length >= ProfileAggregationService.MAX_STACK_FETCH;
        const windowTotalRows = windowTotalResponse.data || [];
        const windowTotal = Number(((_a = windowTotalRows[0]) === null || _a === void 0 ? void 0 : _a["windowTotal"]) || 0);
        // Aggregate per-function stats from the pre-aggregated stacks
        const functionMap = new Map();
        for (const row of rows) {
            const stacktrace = row["stacktrace"] || [];
            const frameTypes = row["frameTypes"] || [];
            const value = Number(row["totalValue"] || 0);
            /*
             * Each grouped row stands in for `sampleCount` raw samples sharing
             * one stack, so per-function sample counts add the group size, not 1.
             */
            const groupSampleCount = Number(row["sampleCount"] || 0);
            if (stacktrace.length === 0) {
                continue;
            }
            const seenInThisSample = new Set();
            for (let i = 0; i < stacktrace.length; i++) {
                const frame = ProfileAggregationService.parseFrame(stacktrace[i]);
                const frameType = frameTypes[i] || "";
                const key = `${frame.functionName}@${frame.fileName}:${frame.lineNumber}`;
                // Stacks are stored leaf-first, so index 0 is the leaf frame.
                const isLeaf = i === 0;
                let entry = functionMap.get(key);
                if (!entry) {
                    entry = {
                        functionName: frame.functionName,
                        fileName: frame.fileName,
                        selfValue: 0,
                        totalValue: 0,
                        sampleCount: 0,
                        frameType: frameType,
                    };
                    functionMap.set(key, entry);
                }
                // totalValue: count the value once per unique function per stack
                if (!seenInThisSample.has(key)) {
                    entry.totalValue += value;
                    entry.sampleCount += groupSampleCount;
                    seenInThisSample.add(key);
                }
                // selfValue: only the leaf frame
                if (isLeaf) {
                    entry.selfValue += value;
                }
            }
        }
        // Sort
        const sortBy = request.sortBy || "selfValue";
        const items = Array.from(functionMap.values());
        items.sort((a, b) => {
            if (sortBy === "totalValue") {
                return b.totalValue - a.totalValue;
            }
            if (sortBy === "sampleCount") {
                return b.sampleCount - a.sampleCount;
            }
            return b.selfValue - a.selfValue;
        });
        const limit = (_b = request.limit) !== null && _b !== void 0 ? _b : ProfileAggregationService.DEFAULT_FUNCTION_LIST_LIMIT;
        return {
            functions: items.slice(0, limit),
            windowTotal,
            truncated,
        };
    }
    /**
     * "Sandwich" aggregation for one function: every matching stack is
     * split at the focused function, the frames toward the root become the
     * callers tree and the frames toward the leaf become the callees tree.
     * Both trees are rooted at the focused function itself so the UI can
     * render them stacked around a single center row.
     *
     * Frames match on functionName + fileName only — line numbers shift on
     * every deploy (and with inlining), so including them would split one
     * logical function into many.
     */
    static async getFunctionFocus(request) {
        var _a;
        const filters = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ projectId: request.projectId }, (request.profileId !== undefined && { profileId: request.profileId })), (request.startTime !== undefined && { startTime: request.startTime })), (request.endTime !== undefined && { endTime: request.endTime })), (request.serviceIds !== undefined && {
            serviceIds: request.serviceIds,
        })), (request.profileType !== undefined && {
            profileType: request.profileType,
        })), (request.profileTypes !== undefined && {
            profileTypes: request.profileTypes,
        }));
        const statement = ProfileAggregationService.buildFunctionStackQuery(filters, request.functionName, request.fileName);
        const windowTotalStatement = ProfileAggregationService.buildWindowTotalQuery(filters);
        /*
         * windowTotal deliberately ignores the function prefilter — it is the
         * denominator for "% of window" figures, so it must cover every row
         * matching the non-function filters. Both reads are independent, so
         * run them concurrently.
         */
        const [dbResult, windowTotalDbResult] = await Promise.all([
            ProfileSampleDatabaseService.executeQuery(statement),
            ProfileSampleDatabaseService.executeQuery(windowTotalStatement),
        ]);
        const response = await dbResult.json();
        const windowTotalResponse = await windowTotalDbResult.json();
        const rows = response.data || [];
        const truncated = rows.length >= ProfileAggregationService.MAX_STACK_FETCH;
        const windowTotalRows = windowTotalResponse.data || [];
        const windowTotal = Number(((_a = windowTotalRows[0]) === null || _a === void 0 ? void 0 : _a["windowTotal"]) || 0);
        /*
         * Both trees are rooted at the focused function. lineNumber is 0
         * because the view aggregates across line numbers by design.
         */
        const callers = {
            functionName: request.functionName,
            fileName: request.fileName,
            lineNumber: 0,
            selfValue: 0,
            totalValue: 0,
            children: [],
            frameType: "",
        };
        const callees = {
            functionName: request.functionName,
            fileName: request.fileName,
            lineNumber: 0,
            selfValue: 0,
            totalValue: 0,
            children: [],
            frameType: "",
        };
        /*
         * Per-node child index for O(1) frame lookup, mirroring getFlamegraph.
         * One map serves both trees since it is keyed by parent node identity.
         */
        const childIndex = new WeakMap();
        let totalValue = 0;
        let selfValue = 0;
        let sampleCount = 0;
        for (const row of rows) {
            const stacktrace = row["stacktrace"] || [];
            const frameTypes = row["frameTypes"] || [];
            const value = Number(row["totalValue"] || 0);
            const groupSampleCount = Number(row["sampleCount"] || 0);
            if (stacktrace.length === 0) {
                continue;
            }
            /*
             * Stacks are leaf-first, so scanning from the END finds the
             * occurrence CLOSEST TO THE ROOT first. For recursive functions
             * this attributes the whole recursive subtree (including nested
             * occurrences) to one split point, so each stack's value is
             * counted exactly once.
             */
            let occurrenceIndex = -1;
            for (let i = stacktrace.length - 1; i >= 0; i--) {
                const frame = ProfileAggregationService.parseFrame(stacktrace[i]);
                if (frame.functionName === request.functionName &&
                    frame.fileName === request.fileName) {
                    occurrenceIndex = i;
                    break;
                }
            }
            /*
             * The ClickHouse prefilter is a prefix match and can overmatch
             * when a fileName containing colons shares a prefix with another
             * (parseFrame splits on the LAST colon) — drop those rows here.
             */
            if (occurrenceIndex === -1) {
                continue;
            }
            totalValue += value;
            sampleCount += groupSampleCount;
            /*
             * Self time counts whenever the function is the EXECUTING (leaf)
             * frame — checked against index 0 directly rather than the split
             * point, because a recursive stack splits at the root-most
             * occurrence while the leaf may still be the same function
             * (standard sandwich-view semantics: self = "was on CPU").
             */
            const leafFrame = ProfileAggregationService.parseFrame(stacktrace[0]);
            if (leafFrame.functionName === request.functionName &&
                leafFrame.fileName === request.fileName) {
                selfValue += value;
            }
            const occurrenceFrameType = frameTypes[occurrenceIndex] || "";
            if (!callers.frameType) {
                callers.frameType = occurrenceFrameType;
            }
            if (!callees.frameType) {
                callees.frameType = occurrenceFrameType;
            }
            callers.totalValue += value;
            callees.totalValue += value;
            /*
             * Caller chain: frames ABOVE the occurrence (toward the root),
             * direct caller first so depth in the tree equals caller distance.
             */
            const callerIndices = [];
            for (let i = occurrenceIndex + 1; i < stacktrace.length; i++) {
                callerIndices.push(i);
            }
            /*
             * Callee chain: frames BELOW the occurrence (toward the leaf),
             * direct callee first.
             */
            const calleeIndices = [];
            for (let i = occurrenceIndex - 1; i >= 0; i--) {
                calleeIndices.push(i);
            }
            ProfileAggregationService.appendFocusChain({
                root: callers,
                stacktrace,
                frameTypes,
                frameIndices: callerIndices,
                value,
                childIndex,
            });
            ProfileAggregationService.appendFocusChain({
                root: callees,
                stacktrace,
                frameTypes,
                frameIndices: calleeIndices,
                value,
                childIndex,
            });
        }
        return {
            functionName: request.functionName,
            fileName: request.fileName,
            totalValue,
            selfValue,
            sampleCount,
            windowTotal,
            callers,
            callees,
            truncated,
        };
    }
    /**
     * Walk one split half of a stack into a focus tree, merging frames on
     * functionName + fileName (line numbers aggregated away — same
     * deploy-stability rationale as the focused-function match itself).
     * The terminal node of the chain absorbs selfValue: in the callers
     * tree that is the stack root ("the chain starts here"), in the
     * callees tree the leaf ("the time bottoms out here"). An empty chain
     * terminates at the focused root itself.
     */
    static appendFocusChain(data) {
        let currentNode = data.root;
        for (const frameIndex of data.frameIndices) {
            const frame = ProfileAggregationService.parseFrame(data.stacktrace[frameIndex]);
            const frameType = data.frameTypes[frameIndex] || "";
            const frameKey = `${frame.functionName}@${frame.fileName}`;
            let index = data.childIndex.get(currentNode);
            if (!index) {
                index = new Map();
                data.childIndex.set(currentNode, index);
            }
            let childNode = index.get(frameKey);
            if (!childNode) {
                childNode = {
                    functionName: frame.functionName,
                    fileName: frame.fileName,
                    lineNumber: 0,
                    selfValue: 0,
                    totalValue: 0,
                    children: [],
                    frameType: frameType,
                };
                currentNode.children.push(childNode);
                index.set(frameKey, childNode);
            }
            childNode.totalValue += data.value;
            currentNode = childNode;
        }
        currentNode.selfValue += data.value;
    }
    /**
     * Build a diff flamegraph comparing two time ranges.
     * Returns a tree where each node has baseline/comparison values and
     * deltas, plus a truncation flag covering both source windows.
     */
    static async getDiffFlamegraph(request) {
        /*
         * The two windows are independent reads, so fetch them concurrently —
         * sequential awaits doubled the latency of every diff request.
         */
        const [baselineResult, comparisonResult] = await Promise.all([
            ProfileAggregationService.getFlamegraph(Object.assign(Object.assign(Object.assign({ projectId: request.projectId, startTime: request.baselineStartTime, endTime: request.baselineEndTime }, (request.serviceIds !== undefined && {
                serviceIds: request.serviceIds,
            })), (request.profileType !== undefined && {
                profileType: request.profileType,
            })), (request.profileTypes !== undefined && {
                profileTypes: request.profileTypes,
            }))),
            ProfileAggregationService.getFlamegraph(Object.assign(Object.assign(Object.assign({ projectId: request.projectId, startTime: request.comparisonStartTime, endTime: request.comparisonEndTime }, (request.serviceIds !== undefined && {
                serviceIds: request.serviceIds,
            })), (request.profileType !== undefined && {
                profileType: request.profileType,
            })), (request.profileTypes !== undefined && {
                profileTypes: request.profileTypes,
            }))),
        ]);
        return {
            diffFlamegraph: ProfileAggregationService.mergeDiffTrees(baselineResult.flamegraph, comparisonResult.flamegraph),
            truncated: baselineResult.truncated || comparisonResult.truncated,
        };
    }
    static mergeDiffTrees(baseline, comparison) {
        const baselineValue = (baseline === null || baseline === void 0 ? void 0 : baseline.totalValue) || 0;
        const comparisonValue = (comparison === null || comparison === void 0 ? void 0 : comparison.totalValue) || 0;
        const delta = comparisonValue - baselineValue;
        const deltaPercent = baselineValue > 0
            ? (delta / baselineValue) * 100
            : comparisonValue > 0
                ? 100
                : 0;
        const node = {
            functionName: (baseline === null || baseline === void 0 ? void 0 : baseline.functionName) || (comparison === null || comparison === void 0 ? void 0 : comparison.functionName) || "(root)",
            fileName: (baseline === null || baseline === void 0 ? void 0 : baseline.fileName) || (comparison === null || comparison === void 0 ? void 0 : comparison.fileName) || "",
            lineNumber: (baseline === null || baseline === void 0 ? void 0 : baseline.lineNumber) || (comparison === null || comparison === void 0 ? void 0 : comparison.lineNumber) || 0,
            baselineValue,
            comparisonValue,
            delta,
            deltaPercent,
            selfBaselineValue: (baseline === null || baseline === void 0 ? void 0 : baseline.selfValue) || 0,
            selfComparisonValue: (comparison === null || comparison === void 0 ? void 0 : comparison.selfValue) || 0,
            selfDelta: ((comparison === null || comparison === void 0 ? void 0 : comparison.selfValue) || 0) - ((baseline === null || baseline === void 0 ? void 0 : baseline.selfValue) || 0),
            children: [],
            frameType: (baseline === null || baseline === void 0 ? void 0 : baseline.frameType) || (comparison === null || comparison === void 0 ? void 0 : comparison.frameType) || "",
        };
        // Merge children by matching on functionName + fileName + lineNumber
        const baselineChildren = new Map();
        const comparisonChildren = new Map();
        if (baseline) {
            for (const child of baseline.children) {
                const key = `${child.functionName}@${child.fileName}:${child.lineNumber}`;
                baselineChildren.set(key, child);
            }
        }
        if (comparison) {
            for (const child of comparison.children) {
                const key = `${child.functionName}@${child.fileName}:${child.lineNumber}`;
                comparisonChildren.set(key, child);
            }
        }
        // All unique child keys
        const allKeys = new Set([
            ...baselineChildren.keys(),
            ...comparisonChildren.keys(),
        ]);
        for (const key of allKeys) {
            const baselineChild = baselineChildren.get(key) || null;
            const comparisonChild = comparisonChildren.get(key) || null;
            node.children.push(ProfileAggregationService.mergeDiffTrees(baselineChild, comparisonChild));
        }
        // Sort children by comparison value descending
        node.children.sort((a, b) => {
            return b.comparisonValue - a.comparisonValue;
        });
        return node;
    }
    /**
     * Aggregate sample / profile counts per primaryEntityId for a time window.
     * Drives the "loudest services first" sort on the Profiles dashboard
     * so a developer opening the page lands on the workloads that are
     * actually doing work rather than scrolling past kernel-thread noise.
     *
     * Reads the small Profile table (one row per ingested profile, with a
     * denormalized sampleCount) instead of scanning every row of the huge
     * ProfileSample table for what is just a per-service ranking.
     */
    static async getServiceActivity(request) {
        const statement = ProfileAggregationService.buildServiceActivityQuery(request);
        const dbResult = await ProfileDatabaseService.executeQuery(statement);
        const response = await dbResult.json();
        const rows = response.data || [];
        const out = [];
        for (const row of rows) {
            const primaryEntityId = String(row["primaryEntityId"] || "");
            if (!primaryEntityId) {
                continue;
            }
            out.push({
                primaryEntityId,
                sampleCount: Number(row["totalSampleCount"] || 0),
                profileCount: Number(row["profileCount"] || 0),
                /*
                 * Profile rows do not carry a summed sample value, and no client
                 * reads this field — it is kept at 0 purely so the response shape
                 * stays stable for existing consumers.
                 */
                totalValue: 0,
            });
        }
        return out;
    }
    /**
     * Group profiling volume by a dimension ("service" = primaryEntityId,
     * anything else = a Profile attribute key) over a time window.
     *
     * Reads the small Profile table (one row per ingested profile with a
     * denormalized sampleCount) — same reasoning as getServiceActivity: a
     * share-of-volume ranking never needs the huge ProfileSample table.
     *
     * totalSampleCount sums over ALL fetched groups before the limit is
     * applied, so item shares stay percentages of the real total rather
     * than of the visible top-N.
     */
    static async getBreakdown(request) {
        var _a;
        const statement = ProfileAggregationService.buildBreakdownQuery(request);
        const dbResult = await ProfileDatabaseService.executeQuery(statement);
        const response = await dbResult.json();
        const rows = response.data || [];
        // Rows arrive sorted by summed sampleCount descending (query ORDER BY).
        const allItems = [];
        let totalSampleCount = 0;
        for (const row of rows) {
            const value = String(row["breakdownValue"] || "");
            if (!value) {
                continue;
            }
            const groupSampleCount = Number(row["totalSampleCount"] || 0);
            totalSampleCount += groupSampleCount;
            allItems.push({
                value,
                sampleCount: groupSampleCount,
                profileCount: Number(row["profileCount"] || 0),
                share: 0,
            });
        }
        const limit = (_a = request.limit) !== null && _a !== void 0 ? _a : ProfileAggregationService.DEFAULT_BREAKDOWN_LIMIT;
        const items = allItems.slice(0, limit);
        for (const item of items) {
            item.share =
                totalSampleCount > 0 ? (item.sampleCount / totalSampleCount) * 100 : 0;
        }
        return { items, totalSampleCount };
    }
    // --- Query builders ---
    /**
     * Fetch samples pre-aggregated by stack identity. Collapsing identical
     * (stacktrace, frameTypes) pairs in ClickHouse shrinks tens of thousands
     * of raw sample rows to a few hundred unique stacks before they cross
     * the wire, and the ORDER BY makes the LIMIT drop the smallest stacks
     * first instead of an arbitrary subset.
     *
     * `value` is stored as Int128 in ClickHouse, so the sum is wrapped in
     * toFloat64 to keep the JSON output numeric (toFloat64OrZero would fail
     * with "Illegal type Int128").
     */
    static buildGroupedStackQuery(request) {
        const statement = SQL `
      SELECT
        stacktrace,
        frameTypes,
        toFloat64(sum(value)) AS totalValue,
        count() AS sampleCount
      FROM ${ProfileAggregationService.TABLE_NAME}
      WHERE projectId = ${{
            type: TableColumnType.ObjectID,
            value: request.projectId,
        }}
    `;
        ProfileAggregationService.appendSampleFilters(statement, request);
        statement.append(SQL ` GROUP BY stacktrace, frameTypes
           ORDER BY totalValue DESC
           LIMIT ${{
            type: TableColumnType.Number,
            value: ProfileAggregationService.MAX_STACK_FETCH,
        }}`);
        /*
         * Cap runtime below the client's 58s request_timeout; 'break' yields
         * a partial (top stacks) result rather than holding a pool connection.
         */
        statement.append(getQuerySettings({
            maxExecutionTimeInSeconds: 45,
            timeoutOverflowMode: "break",
        }));
        return statement;
    }
    /**
     * Grouped-stack query restricted to stacks containing one function, so
     * a sandwich aggregation never pulls the full window's stacks over the
     * wire just to discard most of them.
     *
     * Stored frame strings are "functionName" + ("@fileName" when fileName
     * is non-empty) + (":lineNumber" when lineNumber > 0) — see
     * OtelProfilesIngestService.resolveStackFrames. Line numbers shift
     * across deploys, so the predicate matches the functionName+fileName
     * identity only:
     *   - non-empty fileName: exact "fn@file" (no line recorded) OR prefix
     *     "fn@file:" (line recorded);
     *   - empty fileName: the exact bare frame (folded uploads and
     *     address-only frames store just the name — parseFrame treats any
     *     '@'-less frame as all functionName, so equality is the full
     *     match).
     * The prefix form can overmatch when one colon-bearing fileName is a
     * prefix of another (parseFrame splits on the LAST colon), so callers
     * must re-check each frame in memory via parseFrame.
     */
    static buildFunctionStackQuery(request, functionName, fileName) {
        const statement = SQL `
      SELECT
        stacktrace,
        frameTypes,
        toFloat64(sum(value)) AS totalValue,
        count() AS sampleCount
      FROM ${ProfileAggregationService.TABLE_NAME}
      WHERE projectId = ${{
            type: TableColumnType.ObjectID,
            value: request.projectId,
        }}
    `;
        ProfileAggregationService.appendSampleFilters(statement, request);
        if (fileName) {
            statement.append(SQL ` AND arrayExists(x -> (x = ${{
                type: TableColumnType.Text,
                value: `${functionName}@${fileName}`,
            }} OR startsWith(x, ${{
                type: TableColumnType.Text,
                value: `${functionName}@${fileName}:`,
            }})), stacktrace)`);
        }
        else {
            statement.append(SQL ` AND has(stacktrace, ${{
                type: TableColumnType.Text,
                value: functionName,
            }})`);
        }
        statement.append(SQL ` GROUP BY stacktrace, frameTypes
           ORDER BY totalValue DESC
           LIMIT ${{
            type: TableColumnType.Number,
            value: ProfileAggregationService.MAX_STACK_FETCH,
        }}`);
        /*
         * Cap runtime below the client's 58s request_timeout; 'break' yields
         * a partial (top stacks) result rather than holding a pool connection.
         */
        statement.append(getQuerySettings({
            maxExecutionTimeInSeconds: 45,
            timeoutOverflowMode: "break",
        }));
        return statement;
    }
    /**
     * Total of `value` across every sample row matching the filters — no
     * GROUP BY and no LIMIT, so it stays correct even when the grouped
     * stack query truncates.
     */
    static buildWindowTotalQuery(request) {
        const statement = SQL `
      SELECT
        toFloat64(sum(value)) AS windowTotal
      FROM ${ProfileAggregationService.TABLE_NAME}
      WHERE projectId = ${{
            type: TableColumnType.ObjectID,
            value: request.projectId,
        }}
    `;
        ProfileAggregationService.appendSampleFilters(statement, request);
        statement.append(getQuerySettings({
            maxExecutionTimeInSeconds: 45,
            timeoutOverflowMode: "break",
        }));
        return statement;
    }
    static buildServiceActivityQuery(request) {
        /*
         * One Profile row per ingested profile with a denormalized sampleCount,
         * so the per-service ranking never has to touch the (orders of
         * magnitude larger) ProfileSample table. uniqExact tolerates duplicate
         * rows for the same profileId from re-ingestion.
         */
        const statement = SQL `
      SELECT
        toString(primaryEntityId) AS primaryEntityId,
        toFloat64(sum(sampleCount)) AS totalSampleCount,
        uniqExact(profileId) AS profileCount
      FROM ${ProfileAggregationService.PROFILE_TABLE_NAME}
      WHERE projectId = ${{
            type: TableColumnType.ObjectID,
            value: request.projectId,
        }}
        AND startTime >= ${{
            type: TableColumnType.Date,
            value: request.startTime,
        }}
        AND startTime <= ${{
            type: TableColumnType.Date,
            value: request.endTime,
        }}
    `;
        statement.append(" AND retentionDate >= now()");
        /*
         * profileTypes (array) wins over profileType (single) so the UI
         * can OR together every raw type string in a category.
         */
        if (request.profileTypes && request.profileTypes.length > 0) {
            statement.append(SQL ` AND profileType IN (${{
                type: TableColumnType.Text,
                value: new Includes(request.profileTypes),
            }})`);
        }
        else if (request.profileType) {
            statement.append(SQL ` AND profileType = ${{
                type: TableColumnType.Text,
                value: request.profileType,
            }}`);
        }
        statement.append(SQL ` GROUP BY primaryEntityId
           ORDER BY totalSampleCount DESC
           LIMIT 10000`);
        /*
         * Cap runtime below the client's 58s request_timeout; 'break' yields
         * partial service activity rather than holding a pool connection.
         */
        statement.append(getQuerySettings({
            maxExecutionTimeInSeconds: 45,
            timeoutOverflowMode: "break",
        }));
        return statement;
    }
    /**
     * Per-group volume aggregate over the Profile table for getBreakdown.
     *
     * The "service" dimension groups by primaryEntityId (mirroring
     * buildServiceActivityQuery); any other dimension is a Profile
     * attribute key, read via the parameter-bound Map subscript
     * `attributes[key]` — the same O(1) fast path the analytics query
     * generator uses for map equality. The subscript yields '' for missing
     * keys, so the != '' guard drops profiles without the attribute.
     */
    static buildBreakdownQuery(request) {
        let statement;
        if (request.breakdownBy === "service") {
            statement = SQL `
        SELECT
          toString(primaryEntityId) AS breakdownValue,
          toFloat64(sum(sampleCount)) AS totalSampleCount,
          uniqExact(profileId) AS profileCount
        FROM ${ProfileAggregationService.PROFILE_TABLE_NAME}
        WHERE projectId = ${{
                type: TableColumnType.ObjectID,
                value: request.projectId,
            }}
      `;
        }
        else {
            statement = SQL `
        SELECT
          attributes[${{
                type: TableColumnType.Text,
                value: request.breakdownBy,
            }}] AS breakdownValue,
          toFloat64(sum(sampleCount)) AS totalSampleCount,
          uniqExact(profileId) AS profileCount
        FROM ${ProfileAggregationService.PROFILE_TABLE_NAME}
        WHERE projectId = ${{
                type: TableColumnType.ObjectID,
                value: request.projectId,
            }}
          AND attributes[${{
                type: TableColumnType.Text,
                value: request.breakdownBy,
            }}] != ''
      `;
        }
        statement.append(SQL ` AND startTime >= ${{
            type: TableColumnType.Date,
            value: request.startTime,
        }} AND startTime <= ${{
            type: TableColumnType.Date,
            value: request.endTime,
        }}`);
        /*
         * Read-side retention filter: rows past their per-service retention
         * stay in their part until the whole part drops (ttl_only_drop_parts).
         */
        statement.append(" AND retentionDate >= now()");
        ProfileAggregationService.appendCommonFilters(statement, request);
        statement.append(SQL ` GROUP BY breakdownValue
           ORDER BY totalSampleCount DESC
           LIMIT ${{
            type: TableColumnType.Number,
            value: ProfileAggregationService.MAX_BREAKDOWN_GROUP_FETCH,
        }}`);
        /*
         * Cap runtime below the client's 58s request_timeout; 'break' yields
         * a partial (top groups) result rather than holding a pool connection.
         */
        statement.append(getQuerySettings({
            maxExecutionTimeInSeconds: 45,
            timeoutOverflowMode: "break",
        }));
        return statement;
    }
    /**
     * Shared WHERE-clause tail for ProfileSample reads: optional profileId /
     * time-window filters, the read-side retention filter, and the common
     * service / profile-type filters.
     */
    static appendSampleFilters(statement, request) {
        if (request.profileId) {
            statement.append(SQL ` AND profileId = ${{
                type: TableColumnType.Text,
                value: request.profileId,
            }}`);
        }
        if (request.startTime) {
            statement.append(SQL ` AND time >= ${{
                type: TableColumnType.Date,
                value: request.startTime,
            }}`);
        }
        if (request.endTime) {
            statement.append(SQL ` AND time <= ${{
                type: TableColumnType.Date,
                value: request.endTime,
            }}`);
        }
        /*
         * Read-side retention filter: rows past their per-service retention
         * stay in their part until the whole part drops (ttl_only_drop_parts).
         */
        statement.append(" AND retentionDate >= now()");
        ProfileAggregationService.appendCommonFilters(statement, request);
    }
    static appendCommonFilters(statement, request) {
        if (request.serviceIds && request.serviceIds.length > 0) {
            statement.append(SQL ` AND primaryEntityId IN (${{
                type: TableColumnType.ObjectID,
                value: new Includes(request.serviceIds.map((id) => {
                    return id.toString();
                })),
            }})`);
        }
        /*
         * profileTypes (array) wins over profileType (single) so the UI can
         * OR together every raw type string in a category.
         */
        if (request.profileTypes && request.profileTypes.length > 0) {
            statement.append(SQL ` AND profileType IN (${{
                type: TableColumnType.Text,
                value: new Includes(request.profileTypes),
            }})`);
        }
        else if (request.profileType) {
            statement.append(SQL ` AND profileType = ${{
                type: TableColumnType.Text,
                value: request.profileType,
            }}`);
        }
    }
    /**
     * Parse a frame string in the format "functionName@fileName:lineNumber".
     * Falls back gracefully if the format is unexpected.
     */
    static parseFrame(frame) {
        // Expected format: "functionName@fileName:lineNumber"
        const atIndex = frame.indexOf("@");
        if (atIndex === -1) {
            return {
                functionName: frame,
                fileName: "",
                lineNumber: 0,
            };
        }
        const functionName = frame.substring(0, atIndex);
        const rest = frame.substring(atIndex + 1);
        const lastColonIndex = rest.lastIndexOf(":");
        if (lastColonIndex === -1) {
            return {
                functionName,
                fileName: rest,
                lineNumber: 0,
            };
        }
        const fileName = rest.substring(0, lastColonIndex);
        const lineNumberStr = rest.substring(lastColonIndex + 1);
        const lineNumber = parseInt(lineNumberStr, 10) || 0;
        return {
            functionName,
            fileName,
            lineNumber,
        };
    }
}
ProfileAggregationService.TABLE_NAME = AnalyticsTableName.ProfileSample;
ProfileAggregationService.PROFILE_TABLE_NAME = AnalyticsTableName.Profile;
ProfileAggregationService.DEFAULT_FUNCTION_LIST_LIMIT = 50;
ProfileAggregationService.DEFAULT_BREAKDOWN_LIMIT = 10;
/**
 * Cap on unique groups fetched by a breakdown query. Groups are ordered
 * by summed sampleCount, so hitting the cap drops the quietest groups
 * first — totals over the fetched groups stay representative even for
 * pathological high-cardinality attribute keys.
 */
ProfileAggregationService.MAX_BREAKDOWN_GROUP_FETCH = 10000;
/**
 * Cap on unique (stacktrace, frameTypes) groups fetched per query.
 * Sample reads GROUP BY stack identity and ORDER BY summed value, so
 * hitting this cap drops the *smallest* stacks first — truncation is
 * deterministic and barely visible in a flamegraph, unlike a LIMIT over
 * raw unordered sample rows.
 */
ProfileAggregationService.MAX_STACK_FETCH = 10000;
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], ProfileAggregationService, "getFlamegraph", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], ProfileAggregationService, "getFunctionList", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], ProfileAggregationService, "getFunctionFocus", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], ProfileAggregationService, "getDiffFlamegraph", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], ProfileAggregationService, "getServiceActivity", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], ProfileAggregationService, "getBreakdown", null);
export default ProfileAggregationService;
//# sourceMappingURL=ProfileAggregationService.js.map