설명 없음
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

profileReport.js 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.ProfileReport = exports.extractReadableIndex = exports.formatBytes = exports.formatNumber = exports.pathString = exports.extractJSON = void 0;
  4. const clc = require("colorette");
  5. const Table = require("cli-table");
  6. const fs = require("fs");
  7. const _ = require("lodash");
  8. const readline = require("readline");
  9. const error_1 = require("./error");
  10. const logger_1 = require("./logger");
  11. const DATA_LINE_REGEX = /^data: /;
  12. const BANDWIDTH_NOTE = "NOTE: The numbers reported here are only estimates of the data" +
  13. " payloads from read operations. They are NOT a valid measure of your bandwidth bill.";
  14. const SPEED_NOTE = "NOTE: Speeds are reported at millisecond resolution and" +
  15. " are not the latencies that clients will see. Pending times" +
  16. " are also reported at millisecond resolution. They approximate" +
  17. " the interval of time between the instant a request is received" +
  18. " and the instant it executes.";
  19. const COLLAPSE_THRESHOLD = 25;
  20. const COLLAPSE_WILDCARD = ["$wildcard"];
  21. function extractJSON(line, input) {
  22. if (!input && !DATA_LINE_REGEX.test(line)) {
  23. return null;
  24. }
  25. else if (!input) {
  26. line = line.substring(5);
  27. }
  28. try {
  29. return JSON.parse(line);
  30. }
  31. catch (e) {
  32. return null;
  33. }
  34. }
  35. exports.extractJSON = extractJSON;
  36. function pathString(path) {
  37. return `/${path ? path.join("/") : ""}`;
  38. }
  39. exports.pathString = pathString;
  40. function formatNumber(num) {
  41. const parts = num.toFixed(2).split(".");
  42. parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  43. if (+parts[1] === 0) {
  44. return parts[0];
  45. }
  46. return parts.join(".");
  47. }
  48. exports.formatNumber = formatNumber;
  49. function formatBytes(bytes) {
  50. const threshold = 1000;
  51. if (Math.round(bytes) < threshold) {
  52. return bytes + " B";
  53. }
  54. const units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
  55. let u = -1;
  56. let formattedBytes = bytes;
  57. do {
  58. formattedBytes /= threshold;
  59. u++;
  60. } while (Math.abs(formattedBytes) >= threshold && u < units.length - 1);
  61. return formatNumber(formattedBytes) + " " + units[u];
  62. }
  63. exports.formatBytes = formatBytes;
  64. function extractReadableIndex(query) {
  65. if (query.orderBy) {
  66. return query.orderBy;
  67. }
  68. const indexPath = _.get(query, "index.path");
  69. if (indexPath) {
  70. return pathString(indexPath);
  71. }
  72. return ".value";
  73. }
  74. exports.extractReadableIndex = extractReadableIndex;
  75. class ProfileReport {
  76. constructor(tmpFile, outStream, options = {}) {
  77. this.tempFile = tmpFile;
  78. this.output = outStream;
  79. this.options = options;
  80. this.state = {
  81. outband: {},
  82. inband: {},
  83. writeSpeed: {},
  84. broadcastSpeed: {},
  85. readSpeed: {},
  86. connectSpeed: {},
  87. disconnectSpeed: {},
  88. unlistenSpeed: {},
  89. unindexed: {},
  90. startTime: 0,
  91. endTime: 0,
  92. opCount: 0,
  93. };
  94. }
  95. collectUnindexed(data, path) {
  96. if (!data.unIndexed) {
  97. return;
  98. }
  99. if (!this.state.unindexed.path) {
  100. this.state.unindexed[path] = {};
  101. }
  102. const pathNode = this.state.unindexed[path];
  103. const query = data.querySet[0];
  104. const index = JSON.stringify(query.index);
  105. if (!pathNode[index]) {
  106. pathNode[index] = {
  107. times: 0,
  108. query: query,
  109. };
  110. }
  111. const indexNode = pathNode[index];
  112. indexNode.times += 1;
  113. }
  114. collectSpeedUnpathed(data, opStats) {
  115. if (Object.keys(opStats).length === 0) {
  116. opStats.times = 0;
  117. opStats.millis = 0;
  118. opStats.pendingCount = 0;
  119. opStats.pendingTime = 0;
  120. opStats.rejected = 0;
  121. }
  122. opStats.times += 1;
  123. if (data.hasOwnProperty("millis")) {
  124. opStats.millis += data.millis;
  125. }
  126. if (data.hasOwnProperty("pendingTime")) {
  127. opStats.pendingCount++;
  128. opStats.pendingTime += data.pendingTime;
  129. }
  130. if (data.allowed === false) {
  131. opStats.rejected += 1;
  132. }
  133. }
  134. collectSpeed(data, path, opType) {
  135. if (!opType[path]) {
  136. opType[path] = {
  137. times: 0,
  138. millis: 0,
  139. pendingCount: 0,
  140. pendingTime: 0,
  141. rejected: 0,
  142. };
  143. }
  144. const node = opType[path];
  145. node.times += 1;
  146. if (data.hasOwnProperty("millis")) {
  147. node.millis += data.millis;
  148. }
  149. if (data.hasOwnProperty("pendingTime")) {
  150. node.pendingCount++;
  151. node.pendingTime += data.pendingTime;
  152. }
  153. if (data.allowed === false) {
  154. node.rejected += 1;
  155. }
  156. }
  157. collectBandwidth(bytes, path, direction) {
  158. if (!direction[path]) {
  159. direction[path] = {
  160. times: 0,
  161. bytes: 0,
  162. };
  163. }
  164. const node = direction[path];
  165. node.times += 1;
  166. node.bytes += bytes;
  167. }
  168. collectRead(data, path, bytes) {
  169. this.collectSpeed(data, path, this.state.readSpeed);
  170. this.collectBandwidth(bytes, path, this.state.outband);
  171. }
  172. collectBroadcast(data, path, bytes) {
  173. this.collectSpeed(data, path, this.state.broadcastSpeed);
  174. this.collectBandwidth(bytes, path, this.state.outband);
  175. }
  176. collectUnlisten(data, path) {
  177. this.collectSpeed(data, path, this.state.unlistenSpeed);
  178. }
  179. collectConnect(data) {
  180. this.collectSpeedUnpathed(data, this.state.connectSpeed);
  181. }
  182. collectDisconnect(data) {
  183. this.collectSpeedUnpathed(data, this.state.disconnectSpeed);
  184. }
  185. collectWrite(data, path, bytes) {
  186. this.collectSpeed(data, path, this.state.writeSpeed);
  187. this.collectBandwidth(bytes, path, this.state.inband);
  188. }
  189. processOperation(data) {
  190. if (!this.state.startTime) {
  191. this.state.startTime = data.timestamp;
  192. }
  193. this.state.endTime = data.timestamp;
  194. const path = pathString(data.path);
  195. this.state.opCount++;
  196. switch (data.name) {
  197. case "concurrent-connect":
  198. this.collectConnect(data);
  199. break;
  200. case "concurrent-disconnect":
  201. this.collectDisconnect(data);
  202. break;
  203. case "realtime-read":
  204. this.collectRead(data, path, data.bytes);
  205. break;
  206. case "realtime-write":
  207. this.collectWrite(data, path, data.bytes);
  208. break;
  209. case "realtime-transaction":
  210. this.collectWrite(data, path, data.bytes);
  211. break;
  212. case "realtime-update":
  213. this.collectWrite(data, path, data.bytes);
  214. break;
  215. case "listener-listen":
  216. this.collectRead(data, path, data.bytes);
  217. this.collectUnindexed(data, path);
  218. break;
  219. case "listener-broadcast":
  220. this.collectBroadcast(data, path, data.bytes);
  221. break;
  222. case "listener-unlisten":
  223. this.collectUnlisten(data, path);
  224. break;
  225. case "rest-read":
  226. this.collectRead(data, path, data.bytes);
  227. break;
  228. case "rest-write":
  229. this.collectWrite(data, path, data.bytes);
  230. break;
  231. case "rest-update":
  232. this.collectWrite(data, path, data.bytes);
  233. break;
  234. default:
  235. break;
  236. }
  237. }
  238. collapsePaths(pathedObject, combiner, pathIndex = 1) {
  239. if (!this.options.collapse) {
  240. return pathedObject;
  241. }
  242. const allSegments = Object.keys(pathedObject).map((path) => {
  243. return path.split("/").filter((s) => {
  244. return s !== "";
  245. });
  246. });
  247. const pathSegments = allSegments.filter((segments) => {
  248. return segments.length > pathIndex;
  249. });
  250. const otherSegments = allSegments.filter((segments) => {
  251. return segments.length <= pathIndex;
  252. });
  253. if (pathSegments.length === 0) {
  254. return pathedObject;
  255. }
  256. const prefixes = {};
  257. pathSegments.forEach((segments) => {
  258. const prefixPath = pathString(segments.slice(0, pathIndex));
  259. const prefixCount = _.get(prefixes, prefixPath, new Set());
  260. prefixes[prefixPath] = prefixCount.add(segments[pathIndex]);
  261. });
  262. const collapsedObject = {};
  263. pathSegments.forEach((segments) => {
  264. const prefix = segments.slice(0, pathIndex);
  265. const prefixPath = pathString(prefix);
  266. const prefixCount = _.get(prefixes, prefixPath);
  267. const originalPath = pathString(segments);
  268. if (prefixCount.size >= COLLAPSE_THRESHOLD) {
  269. const tail = segments.slice(pathIndex + 1);
  270. const collapsedPath = pathString(prefix.concat(COLLAPSE_WILDCARD).concat(tail));
  271. const currentValue = collapsedObject[collapsedPath];
  272. if (currentValue) {
  273. collapsedObject[collapsedPath] = combiner(currentValue, pathedObject[originalPath]);
  274. }
  275. else {
  276. collapsedObject[collapsedPath] = pathedObject[originalPath];
  277. }
  278. }
  279. else {
  280. collapsedObject[originalPath] = pathedObject[originalPath];
  281. }
  282. });
  283. otherSegments.forEach((segments) => {
  284. const originalPath = pathString(segments);
  285. collapsedObject[originalPath] = pathedObject[originalPath];
  286. });
  287. return this.collapsePaths(collapsedObject, combiner, pathIndex + 1);
  288. }
  289. renderUnindexedData() {
  290. const table = new Table({
  291. head: ["Path", "Index", "Count"],
  292. style: {
  293. head: this.options.isFile ? [] : ["yellow"],
  294. border: this.options.isFile ? [] : ["grey"],
  295. },
  296. });
  297. const unindexed = this.collapsePaths(this.state.unindexed, (u1, u2) => {
  298. _.mergeWith(u1, u2, (p1, p2) => {
  299. return {
  300. times: p1.times + p2.times,
  301. query: p1.query,
  302. };
  303. });
  304. });
  305. const paths = Object.keys(unindexed);
  306. for (const path of paths) {
  307. const indices = Object.keys(unindexed[path]);
  308. for (const index of indices) {
  309. const data = unindexed[path][index];
  310. const row = [path, extractReadableIndex(data.query), formatNumber(data.times)];
  311. table.push(row);
  312. }
  313. }
  314. return table;
  315. }
  316. renderBandwidth(pureData) {
  317. const table = new Table({
  318. head: ["Path", "Total", "Count", "Average"],
  319. style: {
  320. head: this.options.isFile ? [] : ["yellow"],
  321. border: this.options.isFile ? [] : ["grey"],
  322. },
  323. });
  324. const data = this.collapsePaths(pureData, (b1, b2) => {
  325. return {
  326. bytes: b1.bytes + b2.bytes,
  327. times: b1.times + b2.times,
  328. };
  329. });
  330. const paths = Object.keys(data).sort((a, b) => {
  331. return data[b].bytes - data[a].bytes;
  332. });
  333. for (const path of paths) {
  334. const bandwidth = data[path];
  335. const row = [
  336. path,
  337. formatBytes(bandwidth.bytes),
  338. formatNumber(bandwidth.times),
  339. formatBytes(bandwidth.bytes / bandwidth.times),
  340. ];
  341. table.push(row);
  342. }
  343. return table;
  344. }
  345. renderOutgoingBandwidth() {
  346. return this.renderBandwidth(this.state.outband);
  347. }
  348. renderIncomingBandwidth() {
  349. return this.renderBandwidth(this.state.inband);
  350. }
  351. renderUnpathedOperationSpeed(speedData, hasSecurity = false) {
  352. const head = ["Count", "Average Execution Speed", "Average Pending Time"];
  353. if (hasSecurity) {
  354. head.push("Permission Denied");
  355. }
  356. const table = new Table({
  357. head: head,
  358. style: {
  359. head: this.options.isFile ? [] : ["yellow"],
  360. border: this.options.isFile ? [] : ["grey"],
  361. },
  362. });
  363. if (Object.keys(speedData).length > 0) {
  364. const row = [
  365. speedData.times,
  366. formatNumber(speedData.millis / speedData.times) + " ms",
  367. formatNumber(speedData.pendingCount === 0 ? 0 : speedData.pendingTime / speedData.pendingCount) + " ms",
  368. ];
  369. if (hasSecurity) {
  370. row.push(formatNumber(speedData.rejected));
  371. }
  372. table.push(row);
  373. }
  374. return table;
  375. }
  376. renderOperationSpeed(pureData, hasSecurity = false) {
  377. const head = ["Path", "Count", "Average Execution Speed", "Average Pending Time"];
  378. if (hasSecurity) {
  379. head.push("Permission Denied");
  380. }
  381. const table = new Table({
  382. head: head,
  383. style: {
  384. head: this.options.isFile ? [] : ["yellow"],
  385. border: this.options.isFile ? [] : ["grey"],
  386. },
  387. });
  388. const data = this.collapsePaths(pureData, (s1, s2) => {
  389. return {
  390. times: s1.times + s2.times,
  391. millis: s1.millis + s2.millis,
  392. pendingCount: s1.pendingCount + s2.pendingCount,
  393. pendingTime: s1.pendingTime + s2.pendingTime,
  394. rejected: s1.rejected + s2.rejected,
  395. };
  396. });
  397. const paths = Object.keys(data).sort((a, b) => {
  398. const speedA = data[a].millis / data[a].times;
  399. const speedB = data[b].millis / data[b].times;
  400. return speedB - speedA;
  401. });
  402. for (const path of paths) {
  403. const speed = data[path];
  404. const row = [
  405. path,
  406. speed.times,
  407. formatNumber(speed.millis / speed.times) + " ms",
  408. formatNumber(speed.pendingCount === 0 ? 0 : speed.pendingTime / speed.pendingCount) + " ms",
  409. ];
  410. if (hasSecurity) {
  411. row.push(formatNumber(speed.rejected));
  412. }
  413. table.push(row);
  414. }
  415. return table;
  416. }
  417. renderReadSpeed() {
  418. return this.renderOperationSpeed(this.state.readSpeed, true);
  419. }
  420. renderWriteSpeed() {
  421. return this.renderOperationSpeed(this.state.writeSpeed, true);
  422. }
  423. renderBroadcastSpeed() {
  424. return this.renderOperationSpeed(this.state.broadcastSpeed, false);
  425. }
  426. renderConnectSpeed() {
  427. return this.renderUnpathedOperationSpeed(this.state.connectSpeed, false);
  428. }
  429. renderDisconnectSpeed() {
  430. return this.renderUnpathedOperationSpeed(this.state.disconnectSpeed, false);
  431. }
  432. renderUnlistenSpeed() {
  433. return this.renderOperationSpeed(this.state.unlistenSpeed, false);
  434. }
  435. async parse(onLine, onClose) {
  436. const isFile = this.options.isFile;
  437. const tmpFile = this.tempFile;
  438. const outStream = this.output;
  439. const isInput = this.options.isInput;
  440. return new Promise((resolve, reject) => {
  441. const rl = readline.createInterface({
  442. input: fs.createReadStream(tmpFile),
  443. });
  444. let errored = false;
  445. rl.on("line", (line) => {
  446. const data = extractJSON(line, isInput);
  447. if (!data) {
  448. return;
  449. }
  450. onLine(data);
  451. });
  452. rl.on("close", () => {
  453. if (errored) {
  454. reject(new error_1.FirebaseError("There was an error creating the report."));
  455. }
  456. else {
  457. const result = onClose();
  458. if (isFile) {
  459. outStream.on("finish", () => {
  460. resolve(result);
  461. });
  462. outStream.end();
  463. }
  464. else {
  465. resolve(result);
  466. }
  467. }
  468. });
  469. rl.on("error", () => {
  470. reject();
  471. });
  472. outStream.on("error", () => {
  473. errored = true;
  474. rl.close();
  475. });
  476. });
  477. }
  478. write(data) {
  479. if (this.options.isFile) {
  480. this.output.write(data);
  481. }
  482. else {
  483. logger_1.logger.info(data);
  484. }
  485. }
  486. generate() {
  487. if (this.options.format === "TXT") {
  488. return this.generateText();
  489. }
  490. else if (this.options.format === "RAW") {
  491. return this.generateRaw();
  492. }
  493. else if (this.options.format === "JSON") {
  494. return this.generateJson();
  495. }
  496. throw new error_1.FirebaseError('Invalid report format expected "TXT", "JSON", or "RAW"');
  497. }
  498. generateRaw() {
  499. return this.parse(this.writeRaw.bind(this), () => {
  500. return null;
  501. });
  502. }
  503. writeRaw(data) {
  504. this.write(JSON.stringify(data) + "\n");
  505. }
  506. generateText() {
  507. return this.parse(this.processOperation.bind(this), this.outputText.bind(this));
  508. }
  509. outputText() {
  510. const totalTime = this.state.endTime - this.state.startTime;
  511. const isFile = this.options.isFile;
  512. const write = this.write.bind(this);
  513. const writeTitle = (title) => {
  514. if (isFile) {
  515. write(title + "\n");
  516. }
  517. else {
  518. write(clc.bold(clc.yellow(title)) + "\n");
  519. }
  520. };
  521. const writeTable = (title, table) => {
  522. writeTitle(title);
  523. write(table.toString() + "\n");
  524. };
  525. writeTitle(`Report operations collected from ${new Date(this.state.startTime).toISOString()} over ${totalTime} ms.`);
  526. writeTitle("Speed Report\n");
  527. write(SPEED_NOTE + "\n\n");
  528. writeTable("Read Speed", this.renderReadSpeed());
  529. writeTable("Write Speed", this.renderWriteSpeed());
  530. writeTable("Broadcast Speed", this.renderBroadcastSpeed());
  531. writeTable("Connect Speed", this.renderConnectSpeed());
  532. writeTable("Disconnect Speed", this.renderDisconnectSpeed());
  533. writeTable("Unlisten Speed", this.renderUnlistenSpeed());
  534. writeTitle("Bandwidth Report\n");
  535. write(BANDWIDTH_NOTE + "\n\n");
  536. writeTable("Downloaded Bytes", this.renderOutgoingBandwidth());
  537. writeTable("Uploaded Bytes", this.renderIncomingBandwidth());
  538. writeTable("Unindexed Queries", this.renderUnindexedData());
  539. }
  540. generateJson() {
  541. return this.parse(this.processOperation.bind(this), this.outputJson.bind(this));
  542. }
  543. outputJson() {
  544. const totalTime = this.state.endTime - this.state.startTime;
  545. const tableToJson = (table, note) => {
  546. const json = {
  547. legend: table.options.head,
  548. data: [],
  549. };
  550. if (note) {
  551. json.note = note;
  552. }
  553. table.forEach((row) => {
  554. json.data.push(row);
  555. });
  556. return json;
  557. };
  558. const json = {
  559. totalTime: totalTime,
  560. readSpeed: tableToJson(this.renderReadSpeed(), SPEED_NOTE),
  561. writeSpeed: tableToJson(this.renderWriteSpeed(), SPEED_NOTE),
  562. broadcastSpeed: tableToJson(this.renderBroadcastSpeed(), SPEED_NOTE),
  563. connectSpeed: tableToJson(this.renderConnectSpeed(), SPEED_NOTE),
  564. disconnectSpeed: tableToJson(this.renderDisconnectSpeed(), SPEED_NOTE),
  565. unlistenSpeed: tableToJson(this.renderUnlistenSpeed(), SPEED_NOTE),
  566. downloadedBytes: tableToJson(this.renderOutgoingBandwidth(), BANDWIDTH_NOTE),
  567. uploadedBytes: tableToJson(this.renderIncomingBandwidth(), BANDWIDTH_NOTE),
  568. unindexedQueries: tableToJson(this.renderUnindexedData()),
  569. };
  570. this.write(JSON.stringify(json, null, 2));
  571. if (this.options.isFile) {
  572. return this.output.path;
  573. }
  574. return json;
  575. }
  576. }
  577. exports.ProfileReport = ProfileReport;