Skip to content

Commit 57666e5

Browse files
authored
ts database:profile and use apiv2 (#2804)
* tsify profile, use apiv2 * a little better setup and handling * cleanup some vars * remove async keyword * handle res error
1 parent d305caf commit 57666e5

File tree

5 files changed

+127
-136
lines changed

5 files changed

+127
-136
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"dependencies": {
7676
"@google-cloud/pubsub": "^1.7.0",
7777
"JSONStream": "^1.2.1",
78+
"abort-controller": "^3.0.0",
7879
"archiver": "^3.0.0",
7980
"body-parser": "^1.19.0",
8081
"chokidar": "^3.0.2",

src/apiv2.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import fetch, { Response, RequestInit } from "node-fetch";
2+
import { AbortSignal } from "abort-controller";
23
import { Readable } from "stream";
34

45
import { FirebaseError } from "./error";
@@ -14,6 +15,7 @@ interface RequestOptions<T> extends VerbOptions<T> {
1415
path: string;
1516
json?: T;
1617
responseType?: "json" | "stream";
18+
signal?: AbortSignal;
1719
}
1820

1921
interface VerbOptions<T> {
@@ -243,6 +245,7 @@ export class Client {
243245
const fetchOptions: RequestInit = {
244246
headers: options.headers,
245247
method: options.method,
248+
signal: options.signal,
246249
};
247250

248251
if (options.json !== undefined) {
@@ -305,7 +308,6 @@ export class Client {
305308
private logResponse(res: Response, body: unknown, options: ClientRequestOptions<unknown>): void {
306309
const logURL = this.requestURL(options);
307310
logger.debug(`<<< [apiv2][status] ${options.method} ${logURL} ${res.status}`);
308-
309311
let logBody = "[omitted]";
310312
if (!options.skipLog?.resBody) {
311313
if (body instanceof Readable) {

src/commands/database-profile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ var requireInstance = require("../requireInstance");
77
var { populateInstanceDetails } = require("../management/database");
88
var { requirePermissions } = require("../requirePermissions");
99
var utils = require("../utils");
10-
var profiler = require("../profiler");
10+
var { profiler } = require("../profiler");
1111
var { Emulators } = require("../emulator/types");
1212
var { warnEmulatorNotSupported } = require("../emulator/commandUtils");
1313

src/profiler.js

Lines changed: 0 additions & 134 deletions
This file was deleted.

src/profiler.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import * as fs from "fs";
2+
import * as ora from "ora";
3+
import * as readline from "readline";
4+
import * as tmp from "tmp";
5+
import AbortController from "abort-controller";
6+
7+
import { Client } from "./apiv2";
8+
import { realtimeOriginOrEmulatorOrCustomUrl } from "./database/api";
9+
import * as logger from "./logger";
10+
import * as ProfileReport from "./profileReport";
11+
import * as responseToError from "./responseToError";
12+
import * as utils from "./utils";
13+
14+
tmp.setGracefulCleanup();
15+
16+
/**
17+
* Profiles a database.
18+
* @param options the CLI options object.
19+
*/
20+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
21+
export async function profiler(options: any): Promise<unknown> {
22+
const origin = realtimeOriginOrEmulatorOrCustomUrl(options);
23+
const url = new URL(utils.getDatabaseUrl(origin, options.instance, "/.settings/profile.json?"));
24+
const rl = readline.createInterface({ input: process.stdin });
25+
26+
const fileOut = !!options.output;
27+
const tmpFile = tmp.tmpNameSync();
28+
const tmpStream = fs.createWriteStream(tmpFile);
29+
const outStream = fileOut ? fs.createWriteStream(options.output) : process.stdout;
30+
const spinner = ora({
31+
text: "0 operations recorded. Press [enter] to stop",
32+
color: "yellow",
33+
});
34+
const outputFormat = options.raw ? "RAW" : options.parent.json ? "JSON" : "TXT";
35+
36+
// Controller is used to stop the request stream when the user stops the
37+
// command or the duration passes.
38+
const controller = new AbortController();
39+
40+
const generateReport = (): Promise<void> => {
41+
rl.close();
42+
spinner.stop();
43+
controller.abort();
44+
const dataFile = options.input || tmpFile;
45+
const reportOptions = {
46+
format: outputFormat,
47+
isFile: fileOut,
48+
isInput: !!options.input,
49+
collapse: options.collapse,
50+
};
51+
const report = new ProfileReport(dataFile, outStream, reportOptions);
52+
return report.generate();
53+
};
54+
55+
if (options.input) {
56+
// If there is input, don't contact the server.
57+
return generateReport();
58+
}
59+
60+
const c = new Client({ urlPrefix: url.origin, auth: true });
61+
const res = await c.request<unknown, NodeJS.ReadableStream>({
62+
method: "GET",
63+
path: url.pathname,
64+
responseType: "stream",
65+
resolveOnHTTPError: true,
66+
headers: {
67+
Accept: "text/event-stream",
68+
},
69+
signal: controller.signal,
70+
});
71+
72+
if (res.response.status >= 400) {
73+
throw responseToError(res.response, await res.response.text());
74+
}
75+
76+
if (!options.duration) {
77+
spinner.start();
78+
}
79+
80+
let counter = 0;
81+
res.body.on("data", (chunk: Buffer) => {
82+
if (chunk.toString().includes("event: log")) {
83+
counter++;
84+
spinner.text = `${counter} operations recorded. Press [enter] to stop`;
85+
}
86+
});
87+
// If the response stream is closed, this handler is called (not
88+
// necessarially an error condition).
89+
res.body.on("end", () => {
90+
spinner.text = counter + " operations recorded.\n";
91+
});
92+
// If the duration passes or another exception happens, this handler is
93+
// called.
94+
let resError: Error | undefined;
95+
res.body.on("error", (e) => {
96+
if (e.type !== "aborted") {
97+
resError = e;
98+
logger.error("Unexpected error from response stream:", e);
99+
}
100+
});
101+
102+
const p = new Promise((resolve, reject) => {
103+
const fn = (): void => {
104+
// Use the signal to stop the ongoing request.
105+
controller.abort();
106+
if (resError) {
107+
return reject(resError);
108+
}
109+
resolve(generateReport());
110+
};
111+
if (options.duration) {
112+
setTimeout(fn, options.duration * 1000);
113+
} else {
114+
// On newline, generate the report.
115+
rl.question("", fn);
116+
}
117+
});
118+
119+
// With everything set, start the stream and return the promise.
120+
res.body.pipe(tmpStream);
121+
return p;
122+
}

0 commit comments

Comments
 (0)