123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577 |
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.ProfileReport = exports.extractReadableIndex = exports.formatBytes = exports.formatNumber = exports.pathString = exports.extractJSON = void 0;
- const clc = require("colorette");
- const Table = require("cli-table");
- const fs = require("fs");
- const _ = require("lodash");
- const readline = require("readline");
- const error_1 = require("./error");
- const logger_1 = require("./logger");
- const DATA_LINE_REGEX = /^data: /;
- const BANDWIDTH_NOTE = "NOTE: The numbers reported here are only estimates of the data" +
- " payloads from read operations. They are NOT a valid measure of your bandwidth bill.";
- const SPEED_NOTE = "NOTE: Speeds are reported at millisecond resolution and" +
- " are not the latencies that clients will see. Pending times" +
- " are also reported at millisecond resolution. They approximate" +
- " the interval of time between the instant a request is received" +
- " and the instant it executes.";
- const COLLAPSE_THRESHOLD = 25;
- const COLLAPSE_WILDCARD = ["$wildcard"];
- function extractJSON(line, input) {
- if (!input && !DATA_LINE_REGEX.test(line)) {
- return null;
- }
- else if (!input) {
- line = line.substring(5);
- }
- try {
- return JSON.parse(line);
- }
- catch (e) {
- return null;
- }
- }
- exports.extractJSON = extractJSON;
- function pathString(path) {
- return `/${path ? path.join("/") : ""}`;
- }
- exports.pathString = pathString;
- function formatNumber(num) {
- const parts = num.toFixed(2).split(".");
- parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
- if (+parts[1] === 0) {
- return parts[0];
- }
- return parts.join(".");
- }
- exports.formatNumber = formatNumber;
- function formatBytes(bytes) {
- const threshold = 1000;
- if (Math.round(bytes) < threshold) {
- return bytes + " B";
- }
- const units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
- let u = -1;
- let formattedBytes = bytes;
- do {
- formattedBytes /= threshold;
- u++;
- } while (Math.abs(formattedBytes) >= threshold && u < units.length - 1);
- return formatNumber(formattedBytes) + " " + units[u];
- }
- exports.formatBytes = formatBytes;
- function extractReadableIndex(query) {
- if (query.orderBy) {
- return query.orderBy;
- }
- const indexPath = _.get(query, "index.path");
- if (indexPath) {
- return pathString(indexPath);
- }
- return ".value";
- }
- exports.extractReadableIndex = extractReadableIndex;
- class ProfileReport {
- constructor(tmpFile, outStream, options = {}) {
- this.tempFile = tmpFile;
- this.output = outStream;
- this.options = options;
- this.state = {
- outband: {},
- inband: {},
- writeSpeed: {},
- broadcastSpeed: {},
- readSpeed: {},
- connectSpeed: {},
- disconnectSpeed: {},
- unlistenSpeed: {},
- unindexed: {},
- startTime: 0,
- endTime: 0,
- opCount: 0,
- };
- }
- collectUnindexed(data, path) {
- if (!data.unIndexed) {
- return;
- }
- if (!this.state.unindexed.path) {
- this.state.unindexed[path] = {};
- }
- const pathNode = this.state.unindexed[path];
- const query = data.querySet[0];
- const index = JSON.stringify(query.index);
- if (!pathNode[index]) {
- pathNode[index] = {
- times: 0,
- query: query,
- };
- }
- const indexNode = pathNode[index];
- indexNode.times += 1;
- }
- collectSpeedUnpathed(data, opStats) {
- if (Object.keys(opStats).length === 0) {
- opStats.times = 0;
- opStats.millis = 0;
- opStats.pendingCount = 0;
- opStats.pendingTime = 0;
- opStats.rejected = 0;
- }
- opStats.times += 1;
- if (data.hasOwnProperty("millis")) {
- opStats.millis += data.millis;
- }
- if (data.hasOwnProperty("pendingTime")) {
- opStats.pendingCount++;
- opStats.pendingTime += data.pendingTime;
- }
- if (data.allowed === false) {
- opStats.rejected += 1;
- }
- }
- collectSpeed(data, path, opType) {
- if (!opType[path]) {
- opType[path] = {
- times: 0,
- millis: 0,
- pendingCount: 0,
- pendingTime: 0,
- rejected: 0,
- };
- }
- const node = opType[path];
- node.times += 1;
- if (data.hasOwnProperty("millis")) {
- node.millis += data.millis;
- }
- if (data.hasOwnProperty("pendingTime")) {
- node.pendingCount++;
- node.pendingTime += data.pendingTime;
- }
- if (data.allowed === false) {
- node.rejected += 1;
- }
- }
- collectBandwidth(bytes, path, direction) {
- if (!direction[path]) {
- direction[path] = {
- times: 0,
- bytes: 0,
- };
- }
- const node = direction[path];
- node.times += 1;
- node.bytes += bytes;
- }
- collectRead(data, path, bytes) {
- this.collectSpeed(data, path, this.state.readSpeed);
- this.collectBandwidth(bytes, path, this.state.outband);
- }
- collectBroadcast(data, path, bytes) {
- this.collectSpeed(data, path, this.state.broadcastSpeed);
- this.collectBandwidth(bytes, path, this.state.outband);
- }
- collectUnlisten(data, path) {
- this.collectSpeed(data, path, this.state.unlistenSpeed);
- }
- collectConnect(data) {
- this.collectSpeedUnpathed(data, this.state.connectSpeed);
- }
- collectDisconnect(data) {
- this.collectSpeedUnpathed(data, this.state.disconnectSpeed);
- }
- collectWrite(data, path, bytes) {
- this.collectSpeed(data, path, this.state.writeSpeed);
- this.collectBandwidth(bytes, path, this.state.inband);
- }
- processOperation(data) {
- if (!this.state.startTime) {
- this.state.startTime = data.timestamp;
- }
- this.state.endTime = data.timestamp;
- const path = pathString(data.path);
- this.state.opCount++;
- switch (data.name) {
- case "concurrent-connect":
- this.collectConnect(data);
- break;
- case "concurrent-disconnect":
- this.collectDisconnect(data);
- break;
- case "realtime-read":
- this.collectRead(data, path, data.bytes);
- break;
- case "realtime-write":
- this.collectWrite(data, path, data.bytes);
- break;
- case "realtime-transaction":
- this.collectWrite(data, path, data.bytes);
- break;
- case "realtime-update":
- this.collectWrite(data, path, data.bytes);
- break;
- case "listener-listen":
- this.collectRead(data, path, data.bytes);
- this.collectUnindexed(data, path);
- break;
- case "listener-broadcast":
- this.collectBroadcast(data, path, data.bytes);
- break;
- case "listener-unlisten":
- this.collectUnlisten(data, path);
- break;
- case "rest-read":
- this.collectRead(data, path, data.bytes);
- break;
- case "rest-write":
- this.collectWrite(data, path, data.bytes);
- break;
- case "rest-update":
- this.collectWrite(data, path, data.bytes);
- break;
- default:
- break;
- }
- }
- collapsePaths(pathedObject, combiner, pathIndex = 1) {
- if (!this.options.collapse) {
- return pathedObject;
- }
- const allSegments = Object.keys(pathedObject).map((path) => {
- return path.split("/").filter((s) => {
- return s !== "";
- });
- });
- const pathSegments = allSegments.filter((segments) => {
- return segments.length > pathIndex;
- });
- const otherSegments = allSegments.filter((segments) => {
- return segments.length <= pathIndex;
- });
- if (pathSegments.length === 0) {
- return pathedObject;
- }
- const prefixes = {};
- pathSegments.forEach((segments) => {
- const prefixPath = pathString(segments.slice(0, pathIndex));
- const prefixCount = _.get(prefixes, prefixPath, new Set());
- prefixes[prefixPath] = prefixCount.add(segments[pathIndex]);
- });
- const collapsedObject = {};
- pathSegments.forEach((segments) => {
- const prefix = segments.slice(0, pathIndex);
- const prefixPath = pathString(prefix);
- const prefixCount = _.get(prefixes, prefixPath);
- const originalPath = pathString(segments);
- if (prefixCount.size >= COLLAPSE_THRESHOLD) {
- const tail = segments.slice(pathIndex + 1);
- const collapsedPath = pathString(prefix.concat(COLLAPSE_WILDCARD).concat(tail));
- const currentValue = collapsedObject[collapsedPath];
- if (currentValue) {
- collapsedObject[collapsedPath] = combiner(currentValue, pathedObject[originalPath]);
- }
- else {
- collapsedObject[collapsedPath] = pathedObject[originalPath];
- }
- }
- else {
- collapsedObject[originalPath] = pathedObject[originalPath];
- }
- });
- otherSegments.forEach((segments) => {
- const originalPath = pathString(segments);
- collapsedObject[originalPath] = pathedObject[originalPath];
- });
- return this.collapsePaths(collapsedObject, combiner, pathIndex + 1);
- }
- renderUnindexedData() {
- const table = new Table({
- head: ["Path", "Index", "Count"],
- style: {
- head: this.options.isFile ? [] : ["yellow"],
- border: this.options.isFile ? [] : ["grey"],
- },
- });
- const unindexed = this.collapsePaths(this.state.unindexed, (u1, u2) => {
- _.mergeWith(u1, u2, (p1, p2) => {
- return {
- times: p1.times + p2.times,
- query: p1.query,
- };
- });
- });
- const paths = Object.keys(unindexed);
- for (const path of paths) {
- const indices = Object.keys(unindexed[path]);
- for (const index of indices) {
- const data = unindexed[path][index];
- const row = [path, extractReadableIndex(data.query), formatNumber(data.times)];
- table.push(row);
- }
- }
- return table;
- }
- renderBandwidth(pureData) {
- const table = new Table({
- head: ["Path", "Total", "Count", "Average"],
- style: {
- head: this.options.isFile ? [] : ["yellow"],
- border: this.options.isFile ? [] : ["grey"],
- },
- });
- const data = this.collapsePaths(pureData, (b1, b2) => {
- return {
- bytes: b1.bytes + b2.bytes,
- times: b1.times + b2.times,
- };
- });
- const paths = Object.keys(data).sort((a, b) => {
- return data[b].bytes - data[a].bytes;
- });
- for (const path of paths) {
- const bandwidth = data[path];
- const row = [
- path,
- formatBytes(bandwidth.bytes),
- formatNumber(bandwidth.times),
- formatBytes(bandwidth.bytes / bandwidth.times),
- ];
- table.push(row);
- }
- return table;
- }
- renderOutgoingBandwidth() {
- return this.renderBandwidth(this.state.outband);
- }
- renderIncomingBandwidth() {
- return this.renderBandwidth(this.state.inband);
- }
- renderUnpathedOperationSpeed(speedData, hasSecurity = false) {
- const head = ["Count", "Average Execution Speed", "Average Pending Time"];
- if (hasSecurity) {
- head.push("Permission Denied");
- }
- const table = new Table({
- head: head,
- style: {
- head: this.options.isFile ? [] : ["yellow"],
- border: this.options.isFile ? [] : ["grey"],
- },
- });
- if (Object.keys(speedData).length > 0) {
- const row = [
- speedData.times,
- formatNumber(speedData.millis / speedData.times) + " ms",
- formatNumber(speedData.pendingCount === 0 ? 0 : speedData.pendingTime / speedData.pendingCount) + " ms",
- ];
- if (hasSecurity) {
- row.push(formatNumber(speedData.rejected));
- }
- table.push(row);
- }
- return table;
- }
- renderOperationSpeed(pureData, hasSecurity = false) {
- const head = ["Path", "Count", "Average Execution Speed", "Average Pending Time"];
- if (hasSecurity) {
- head.push("Permission Denied");
- }
- const table = new Table({
- head: head,
- style: {
- head: this.options.isFile ? [] : ["yellow"],
- border: this.options.isFile ? [] : ["grey"],
- },
- });
- const data = this.collapsePaths(pureData, (s1, s2) => {
- return {
- times: s1.times + s2.times,
- millis: s1.millis + s2.millis,
- pendingCount: s1.pendingCount + s2.pendingCount,
- pendingTime: s1.pendingTime + s2.pendingTime,
- rejected: s1.rejected + s2.rejected,
- };
- });
- const paths = Object.keys(data).sort((a, b) => {
- const speedA = data[a].millis / data[a].times;
- const speedB = data[b].millis / data[b].times;
- return speedB - speedA;
- });
- for (const path of paths) {
- const speed = data[path];
- const row = [
- path,
- speed.times,
- formatNumber(speed.millis / speed.times) + " ms",
- formatNumber(speed.pendingCount === 0 ? 0 : speed.pendingTime / speed.pendingCount) + " ms",
- ];
- if (hasSecurity) {
- row.push(formatNumber(speed.rejected));
- }
- table.push(row);
- }
- return table;
- }
- renderReadSpeed() {
- return this.renderOperationSpeed(this.state.readSpeed, true);
- }
- renderWriteSpeed() {
- return this.renderOperationSpeed(this.state.writeSpeed, true);
- }
- renderBroadcastSpeed() {
- return this.renderOperationSpeed(this.state.broadcastSpeed, false);
- }
- renderConnectSpeed() {
- return this.renderUnpathedOperationSpeed(this.state.connectSpeed, false);
- }
- renderDisconnectSpeed() {
- return this.renderUnpathedOperationSpeed(this.state.disconnectSpeed, false);
- }
- renderUnlistenSpeed() {
- return this.renderOperationSpeed(this.state.unlistenSpeed, false);
- }
- async parse(onLine, onClose) {
- const isFile = this.options.isFile;
- const tmpFile = this.tempFile;
- const outStream = this.output;
- const isInput = this.options.isInput;
- return new Promise((resolve, reject) => {
- const rl = readline.createInterface({
- input: fs.createReadStream(tmpFile),
- });
- let errored = false;
- rl.on("line", (line) => {
- const data = extractJSON(line, isInput);
- if (!data) {
- return;
- }
- onLine(data);
- });
- rl.on("close", () => {
- if (errored) {
- reject(new error_1.FirebaseError("There was an error creating the report."));
- }
- else {
- const result = onClose();
- if (isFile) {
- outStream.on("finish", () => {
- resolve(result);
- });
- outStream.end();
- }
- else {
- resolve(result);
- }
- }
- });
- rl.on("error", () => {
- reject();
- });
- outStream.on("error", () => {
- errored = true;
- rl.close();
- });
- });
- }
- write(data) {
- if (this.options.isFile) {
- this.output.write(data);
- }
- else {
- logger_1.logger.info(data);
- }
- }
- generate() {
- if (this.options.format === "TXT") {
- return this.generateText();
- }
- else if (this.options.format === "RAW") {
- return this.generateRaw();
- }
- else if (this.options.format === "JSON") {
- return this.generateJson();
- }
- throw new error_1.FirebaseError('Invalid report format expected "TXT", "JSON", or "RAW"');
- }
- generateRaw() {
- return this.parse(this.writeRaw.bind(this), () => {
- return null;
- });
- }
- writeRaw(data) {
- this.write(JSON.stringify(data) + "\n");
- }
- generateText() {
- return this.parse(this.processOperation.bind(this), this.outputText.bind(this));
- }
- outputText() {
- const totalTime = this.state.endTime - this.state.startTime;
- const isFile = this.options.isFile;
- const write = this.write.bind(this);
- const writeTitle = (title) => {
- if (isFile) {
- write(title + "\n");
- }
- else {
- write(clc.bold(clc.yellow(title)) + "\n");
- }
- };
- const writeTable = (title, table) => {
- writeTitle(title);
- write(table.toString() + "\n");
- };
- writeTitle(`Report operations collected from ${new Date(this.state.startTime).toISOString()} over ${totalTime} ms.`);
- writeTitle("Speed Report\n");
- write(SPEED_NOTE + "\n\n");
- writeTable("Read Speed", this.renderReadSpeed());
- writeTable("Write Speed", this.renderWriteSpeed());
- writeTable("Broadcast Speed", this.renderBroadcastSpeed());
- writeTable("Connect Speed", this.renderConnectSpeed());
- writeTable("Disconnect Speed", this.renderDisconnectSpeed());
- writeTable("Unlisten Speed", this.renderUnlistenSpeed());
- writeTitle("Bandwidth Report\n");
- write(BANDWIDTH_NOTE + "\n\n");
- writeTable("Downloaded Bytes", this.renderOutgoingBandwidth());
- writeTable("Uploaded Bytes", this.renderIncomingBandwidth());
- writeTable("Unindexed Queries", this.renderUnindexedData());
- }
- generateJson() {
- return this.parse(this.processOperation.bind(this), this.outputJson.bind(this));
- }
- outputJson() {
- const totalTime = this.state.endTime - this.state.startTime;
- const tableToJson = (table, note) => {
- const json = {
- legend: table.options.head,
- data: [],
- };
- if (note) {
- json.note = note;
- }
- table.forEach((row) => {
- json.data.push(row);
- });
- return json;
- };
- const json = {
- totalTime: totalTime,
- readSpeed: tableToJson(this.renderReadSpeed(), SPEED_NOTE),
- writeSpeed: tableToJson(this.renderWriteSpeed(), SPEED_NOTE),
- broadcastSpeed: tableToJson(this.renderBroadcastSpeed(), SPEED_NOTE),
- connectSpeed: tableToJson(this.renderConnectSpeed(), SPEED_NOTE),
- disconnectSpeed: tableToJson(this.renderDisconnectSpeed(), SPEED_NOTE),
- unlistenSpeed: tableToJson(this.renderUnlistenSpeed(), SPEED_NOTE),
- downloadedBytes: tableToJson(this.renderOutgoingBandwidth(), BANDWIDTH_NOTE),
- uploadedBytes: tableToJson(this.renderIncomingBandwidth(), BANDWIDTH_NOTE),
- unindexedQueries: tableToJson(this.renderUnindexedData()),
- };
- this.write(JSON.stringify(json, null, 2));
- if (this.options.isFile) {
- return this.output.path;
- }
- return json;
- }
- }
- exports.ProfileReport = ProfileReport;
|