"use strict";
//
//  Copyright (c) Microsoft Corporation. All rights reserved.
//
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionContour = void 0;
const errors_1 = require("../errors");
const queue_1 = require("../util/queue");
const semaphore_1 = require("../util/semaphore");
/**
 * Collects session metrics over time, producing an outline of the timing, speed,
 * and quantity of bytes sent/received during the session.
 *
 * Metrics are recorded across a number of equal time intervals. As the session time
 * increases, intervals are expanded to keep the number of intervals under the configured
 * maximum. Each expansion doubles the length of all intervals, while combining the metrics
 * within each pair of combined intervals. Therefore, a longer session has longer intervals
 * and less-granular metrics. In this way, the memory usage (and serialized size) of the
 * session contour remains roughly constant regardless of the length of the session.
 *
 * Metrics exposed via the collection properties on this class may be momentarily
 * inconsistent (but will not throw exceptions) if continued session operation causes
 * intervals to be expanded while the data is being read concurrently. To avoid any
 * inconsistency, hold a lock on the <see cref="SessionContour" /> instance while reading
 * data. (Or wait until the session ends.)
 *
 * A session contour can be exported in a compact form suitable for logging or telemetry.
 * Use the code in `SessionContour.kql` to chart a session contour in Azure Data Explorer.
 */
class SessionContour {
    /**
     * Creates a new instance of the `SessionContour` class.
     *
     * @param maxIntervals Maximum number of metric intervals to record,
     * defaults to 256. Must be a power of two.
     */
    constructor(maxIntervals = 256) {
        /** Current size of the metrics interval, in milliseconds. */
        this.intervalMs = SessionContour.initialInterval;
        /** Number of intervals for which metrics have been recorded. */
        this.count = 0;
        this.updateQueue = new queue_1.Queue();
        this.updateSemaphore = new semaphore_1.Semaphore(0);
        this.disposed = false;
        if (maxIntervals < 2 || (maxIntervals & (maxIntervals - 1)) !== 0) {
            throw new Error('Contour intervals must be a power of two.');
        }
        this.maxIntervals = maxIntervals;
        this.intervalBytesSent = new Array(maxIntervals);
        this.intervalBytesReceived = new Array(maxIntervals);
        this.intervalLatencyMin = new Array(maxIntervals);
        this.intervalLatencyMax = new Array(maxIntervals);
        this.intervalLatencySum = new Array(maxIntervals);
        this.intervalLatencyCount = new Array(maxIntervals);
        this.intervalLatencyAvg = new Array(maxIntervals);
        this.intervalBytesSent.fill(0);
        this.intervalBytesReceived.fill(0);
        this.intervalLatencyMin.fill(0);
        this.intervalLatencyMax.fill(0);
        this.intervalLatencySum.fill(0);
        this.intervalLatencyCount.fill(0);
        this.intervalLatencyAvg.fill(0);
    }
    /**
     * Gets the current number of contour intervals with recorded metrics. This is always
     * less than `maxIntervals`.
     */
    get intervalCount() {
        return this.count;
    }
    /**
     * Gets the current time span of each contour interval, in milliseconds. This interval time
     * span is doubled as necesary such that the entire duration of the session is always covered
     * by fewer intervals than the maximum.
     */
    get interval() {
        return this.intervalMs;
    }
    /**
     * Gets the total number of bytes sent for each interval during the session,
     * including all channels and non-channel protocol messages, and including message
     * framing, padding, and MAC bytes. The number of values is equal to `intervalCount`.
     */
    get bytesSent() {
        return this.intervalBytesSent.slice(0, this.count);
    }
    /**
     * Gets the total number of bytes received for each interval during the session,
     * including all channels and non-channel protocol messages, and including message
     * framing, padding, and MAC bytes. The number of values is equal to `intervalCount`.
     */
    get bytesReceived() {
        return this.intervalBytesReceived.slice(0, this.count);
    }
    /**
     * Gets the minimum recorded round-trip connection latency between client and server for
     * each interval during the session. The number of values is equal to `intervalCount`.
     */
    get latencyMinMs() {
        return this.intervalLatencyMin.slice(0, this.count);
    }
    /**
     * Gets the maximum recorded round-trip connection latency between client and server for
     * each interval during the session. The number of values is equal to `intervalCount`.
     */
    get latencyMaxMs() {
        return this.intervalLatencyMax.slice(0, this.count);
    }
    /**
     * Gets the average recorded round-trip connection latency between client and server for
     * each interval during the session. The number of values is equal to `intervalCount`.
     */
    get latencyAverageMs() {
        return this.intervalLatencyAvg.slice(0, this.count);
    }
    onMessageSent(e) {
        this.updateQueue.enqueue({
            time: e.time,
            bytesSent: e.size,
        });
        this.updateSemaphore.tryRelease();
    }
    onMessageReceived(e) {
        this.updateQueue.enqueue({
            time: e.time,
            bytesReceived: e.size,
        });
        this.updateSemaphore.tryRelease();
    }
    onLatencyUpdated(e) {
        this.updateQueue.enqueue(e);
        this.updateSemaphore.tryRelease();
    }
    onSessionClosed() {
        this.updateSemaphore.tryRelease();
    }
    /**
     * Starts collecting session metrics, and processes the metrics in a backgroud loop until
     * cancelled or until the session is closed or the `SessionContour` instance is disposed.
     */
    async collectMetrics(sessionMetrics, cancellation) {
        if (!sessionMetrics)
            throw new TypeError('A session metrics object is required.');
        if (this.disposed)
            throw new errors_1.ObjectDisposedError(this);
        const eventRegistrations = [];
        eventRegistrations.push(sessionMetrics.onMessageSent(this.onMessageSent.bind(this)));
        eventRegistrations.push(sessionMetrics.onMessageReceived(this.onMessageReceived.bind(this)));
        eventRegistrations.push(sessionMetrics.onLatencyUpdated(this.onLatencyUpdated.bind(this)));
        eventRegistrations.push(sessionMetrics.onSessionClosed(this.onSessionClosed.bind(this)));
        try {
            while (!(cancellation === null || cancellation === void 0 ? void 0 : cancellation.isCancellationRequested)) {
                try {
                    await this.updateSemaphore.wait(cancellation);
                }
                catch (e) {
                    // The semaphore was disposed.
                    break;
                }
                const update = this.updateQueue.dequeue();
                if (!update) {
                    // The semaphore was released without enqueueing an update item.
                    break;
                }
                const intervalIndex = this.updateInterval(update.time);
                if (update.bytesSent) {
                    this.intervalBytesSent[intervalIndex] += update.bytesSent;
                }
                if (update.bytesReceived) {
                    this.intervalBytesReceived[intervalIndex] += update.bytesReceived;
                }
                const latency = update.latency;
                if (latency) {
                    if (!this.intervalLatencyMin[intervalIndex] ||
                        latency < this.intervalLatencyMin[intervalIndex]) {
                        this.intervalLatencyMin[intervalIndex] = latency;
                    }
                    if (!this.intervalLatencyMax[intervalIndex] ||
                        latency > this.intervalLatencyMax[intervalIndex]) {
                        this.intervalLatencyMax[intervalIndex] = latency;
                    }
                    this.intervalLatencySum[intervalIndex] += latency;
                    this.intervalLatencyCount[intervalIndex]++;
                    this.intervalLatencyAvg[intervalIndex] =
                        this.intervalLatencySum[intervalIndex] / this.intervalLatencyCount[intervalIndex];
                }
            }
            if (this.disposed) {
                this.updateSemaphore.dispose();
            }
        }
        finally {
            for (const eventRegistration of eventRegistrations) {
                eventRegistration.dispose();
            }
        }
    }
    updateInterval(time) {
        let intervalIndex = Math.floor(time / this.intervalMs);
        if (intervalIndex >= this.intervalCount) {
            // Expand as needed to accomodate the current time interval.
            while (intervalIndex >= this.maxIntervals) {
                this.expandIntervals();
                intervalIndex = Math.floor(time / this.intervalMs);
            }
            this.count = intervalIndex + 1;
        }
        return intervalIndex;
    }
    expandIntervals() {
        const combineLatency = (a, b, f) => a === 0 ? b : b === 0 ? a : f(a, b);
        const halfMaxIntervals = this.maxIntervals / 2;
        for (let i = 0; i < halfMaxIntervals; i++) {
            const iA = 2 * i;
            const iB = 2 * i + 1;
            this.intervalBytesSent[i] = this.intervalBytesSent[iA] + this.intervalBytesSent[iB];
            this.intervalBytesReceived[i] =
                this.intervalBytesReceived[iA] + this.intervalBytesReceived[iB];
            this.intervalLatencyMin[i] = combineLatency(this.intervalLatencyMin[iA], this.intervalLatencyMin[iB], Math.min);
            this.intervalLatencyMax[i] = combineLatency(this.intervalLatencyMax[iA], this.intervalLatencyMax[iB], Math.max);
            this.intervalLatencySum[i] = this.intervalLatencySum[iA] + this.intervalLatencySum[iB];
            const countSum = this.intervalLatencyCount[iA] + this.intervalLatencyCount[iB];
            this.intervalLatencyCount[i] = countSum;
            this.intervalLatencyAvg[i] =
                countSum === 0 ? 0 : this.intervalLatencySum[i] / this.intervalLatencyCount[i];
        }
        this.intervalBytesSent.fill(0, halfMaxIntervals, this.maxIntervals);
        this.intervalBytesReceived.fill(0, halfMaxIntervals, this.maxIntervals);
        this.intervalLatencyMin.fill(0, halfMaxIntervals, this.maxIntervals);
        this.intervalLatencyMax.fill(0, halfMaxIntervals, this.maxIntervals);
        this.intervalLatencySum.fill(0, halfMaxIntervals, this.maxIntervals);
        this.intervalLatencyCount.fill(0, halfMaxIntervals, this.maxIntervals);
        this.intervalLatencyAvg.fill(0, halfMaxIntervals, this.maxIntervals);
        this.intervalMs *= 2;
    }
    dispose() {
        this.disposed = true;
        // The semaphore will be disposed after all remaining updates have been processed.
        this.updateSemaphore.tryRelease();
    }
    /**
     * Serializes the session contour into a compact form suitable for recording in
     * logs or telemetry.
     *
     * This compact serialization format uses one byte per metric per interval, so there is
     * some loss of precision, but generally not so much that it affects a visualization. A
     * scale factor for each metric is automatically determined and included in the serialized
     * header. The size of the serialized encoded data will be a little under 7 bytes per
     * interval. With the default interval maximum (256), that comes out to less than 1.75 KB.
     *
     * Use the code in `SessionContour.kql` to decode and chart this output in
     * Azure Data Explorer.
     */
    export() {
        // Time and value scales are in log2 form, determined based on the maximum
        // value in each series. This allows for a reasonable range of precision for each
        // value (with byte values ranging from 0-255). For example a max latency in the
        // 500ms range will get a scale factor of 1 (because ceil(log2(500/255)) = 1), so
        // each serialized value (0-255) is half the actual value (0-510).
        const getScale = (values) => Math.max(0, Math.ceil(Math.log2(Math.max(...values) / 255)));
        const applyReverseScale = (value, scale) => Math.round(value / Math.pow(2, scale));
        const bytes = Buffer.alloc(3 + (2 + this.intervalCount) * 5);
        const version = 1;
        const timeScale = Math.log2(this.interval / SessionContour.initialInterval);
        bytes[0] = version;
        bytes[1] = 5; // Number of metrics per interval
        bytes[2] = timeScale;
        bytes[3] = getScale(this.latencyMinMs);
        bytes[4] = getScale(this.latencyMaxMs);
        bytes[5] = getScale(this.latencyAverageMs);
        bytes[6] = getScale(this.bytesSent);
        bytes[7] = getScale(this.bytesReceived);
        bytes[8] = SessionMetric.latencyMin;
        bytes[9] = SessionMetric.latencyMax;
        bytes[10] = SessionMetric.latencyAverage;
        bytes[11] = SessionMetric.bytesSent;
        bytes[12] = SessionMetric.bytesReceived;
        for (let i = 0; i < this.intervalCount; i++) {
            const offset = 13 + 5 * i;
            bytes[offset + 0] = applyReverseScale(this.intervalLatencyMin[i], bytes[3]);
            bytes[offset + 1] = applyReverseScale(this.intervalLatencyMax[i], bytes[4]);
            bytes[offset + 2] = applyReverseScale(this.intervalLatencyAvg[i], bytes[5]);
            bytes[offset + 3] = applyReverseScale(this.intervalBytesSent[i], bytes[6]);
            bytes[offset + 4] = applyReverseScale(this.intervalBytesReceived[i], bytes[7]);
        }
        return bytes.toString('base64');
    }
    /**
     * Deserializes a session contour that was previously exported.
     *
     * Due to loss in precision, some values in the deserialized contour will not exactly match
     * the original, but they will be close.
     */
    static import(contourBase64) {
        const bytes = Buffer.from(contourBase64, 'base64');
        if (bytes.length < 3) {
            throw new Error('Invalid session contour string.');
        }
        const version = bytes[0];
        const metricsPerInterval = bytes[1];
        const timeScale = bytes[2];
        if (version !== 1) {
            throw new Error(`Unsupported session contour version: ${version}`);
        }
        const intervalCount = (bytes.length - 3) / metricsPerInterval - 2;
        if (intervalCount < 1 || bytes.length !== 3 + metricsPerInterval * (intervalCount + 2)) {
            throw new Error('Incomplete session contour string.');
        }
        const maxIntervals = Math.pow(2, Math.ceil(Math.log2(intervalCount)));
        const sessionContour = new SessionContour(maxIntervals);
        sessionContour.intervalMs = Math.pow(2, timeScale) * SessionContour.initialInterval;
        sessionContour.count = intervalCount;
        const scales = new Array(metricsPerInterval);
        for (let m = 0; m < metricsPerInterval; m++) {
            scales[m] = Math.pow(2, bytes[3 + m]);
        }
        const ids = new Array(metricsPerInterval);
        for (let m = 0; m < metricsPerInterval; m++) {
            ids[m] = bytes[3 + metricsPerInterval + m];
        }
        for (let i = 0; i < intervalCount; i++) {
            const offset = 3 + (2 + i) * metricsPerInterval;
            for (let m = 0; m < metricsPerInterval; m++) {
                switch (ids[m]) {
                    case SessionMetric.latencyMin:
                        sessionContour.intervalLatencyMin[i] = bytes[offset + m] * scales[m];
                        break;
                    case SessionMetric.latencyMax:
                        sessionContour.intervalLatencyMax[i] = bytes[offset + m] * scales[m];
                        break;
                    case SessionMetric.latencyAverage:
                        sessionContour.intervalLatencyAvg[i] = sessionContour.intervalLatencySum[i] =
                            bytes[offset + m] * scales[m];
                        sessionContour.intervalLatencyCount[i] = bytes[offset + m] === 0 ? 0 : 1;
                        break;
                    case SessionMetric.bytesSent:
                        sessionContour.intervalBytesSent[i] = bytes[offset + m] * scales[m];
                        break;
                    case SessionMetric.bytesReceived:
                        sessionContour.intervalBytesReceived[i] = bytes[offset + m] * scales[m];
                        break;
                    default:
                        // Ignore any unknown metrics
                        break;
                }
            }
        }
        return sessionContour;
    }
}
exports.SessionContour = SessionContour;
SessionContour.initialInterval = 1000; // 1 second (in milliseconds)
var SessionMetric;
(function (SessionMetric) {
    SessionMetric[SessionMetric["none"] = 0] = "none";
    SessionMetric[SessionMetric["latencyMin"] = 1] = "latencyMin";
    SessionMetric[SessionMetric["latencyMax"] = 2] = "latencyMax";
    SessionMetric[SessionMetric["latencyAverage"] = 3] = "latencyAverage";
    SessionMetric[SessionMetric["bytesSent"] = 11] = "bytesSent";
    SessionMetric[SessionMetric["bytesReceived"] = 12] = "bytesReceived";
})(SessionMetric || (SessionMetric = {}));
//# sourceMappingURL=https://main.vscode-cdn.net/sourcemaps/8761a5560cfd65fdd19ce7e2bd18dab5c0a4d84e/node_modules/@microsoft/dev-tunnels-ssh/metrics/sessionContour.js.map