Skip to content

Commit cc22f25

Browse files
Merge branch 'master' into hooks-remote-config-addition
2 parents 6174cbf + a465031 commit cc22f25

File tree

3 files changed

+68
-94
lines changed

3 files changed

+68
-94
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
## Version 24.05.XX
22
Features:
33
- [hooks] Added remote config changes to internal actions
4+
- [system-utility] New endpoint: /take-heap-snapshot.
5+
- [system-utility] Using nodejs fs to write profiler files instead of gridfs.
46

57
## Version 24.05.22
68
Features:

plugins/system-utility/api/api.js

+19-24
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,7 @@ function handleMessage(msg) {
3131
if (msg.cmd === "stopProfiler" || msg.cmd === "startProfiler") {
3232
args = [processName];
3333
}
34-
35-
systemUtility[msg.cmd](...args).catch(err => {
36-
log.e(err);
37-
console.error(err);
38-
});
34+
systemUtility[msg.cmd](...args).catch(err => log.e(err));
3935
}
4036
else if (typeof msg === "object" && msg.cmd === "setNumberOfWorkers") {
4137
numberOfWorkers = msg.params.numberOfWorkers;
@@ -178,29 +174,29 @@ function stopWithTimeout(type, fromTimeout = false) {
178174
});
179175
return true;
180176

181-
case 'list-files':
182-
validate(params, () => {
183-
systemUtility.listProfilerFiles()
184-
.then(res => common.returnMessage(params, 200, res))
185-
.catch(err => {
186-
log.e(err);
187-
common.returnMessage(params, 404, "Profiler files not found");
177+
case "take-heap-snapshot":
178+
validate(params, async() => {
179+
try {
180+
params.res.writeHead(200, {
181+
"Content-Type": "plain/text; charset=utf-8",
182+
"Content-Disposition": "attachment; filename=heap.heapsnapshot"
188183
});
184+
systemUtility.takeHeapSnapshot(params.res);
185+
}
186+
catch (err) {
187+
log.e(err);
188+
common.returnMessage(params, 500, err.toString());
189+
}
189190
});
190191
return true;
191192

192-
case 'download':
193+
case 'list-files':
193194
validate(params, () => {
194-
systemUtility.downloadProfilerFile(params.qstring.filename)
195-
.then(({ data, filename }) => {
196-
common.returnRaw(params, 200, data, {
197-
'Content-Type': 'plain/text; charset=utf-8',
198-
'Content-disposition': 'attachment; filename=' + filename
199-
});
200-
})
195+
systemUtility.listProfilerFiles()
196+
.then(res => common.returnMessage(params, 200, res))
201197
.catch(err => {
202198
log.e(err);
203-
common.returnMessage(params, 404, "File not found");
199+
common.returnMessage(params, 404, "Profiler files couldn't be found");
204200
});
205201
});
206202
return true;
@@ -210,7 +206,7 @@ function stopWithTimeout(type, fromTimeout = false) {
210206
try {
211207
const tarStream = await systemUtility.profilerFilesTarStream();
212208
if (tarStream === null) {
213-
common.returnMessage(params, 404, "Profiler files not found");
209+
common.returnMessage(params, 404, "Profiler files couldn't be found");
214210
}
215211
else {
216212
params.res.writeHead(200, {
@@ -223,8 +219,7 @@ function stopWithTimeout(type, fromTimeout = false) {
223219
}
224220
catch (err) {
225221
log.e(err);
226-
console.error(err);
227-
common.returnMessage(params, 500, "Server error");
222+
common.returnMessage(params, 500, err.toString());
228223
}
229224
});
230225
return true;

plugins/system-utility/api/system.utility.js

+47-70
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
/**
2+
* @typedef {import('stream').Writable} Writable
3+
*/
14
var common = require('../../../api/utils/common.js');
25
const inspector = require('inspector');
3-
const countlyFs = require('../../../api/utils/countlyFs.js');
46
var exec = require('child_process').exec;
57
const tar = require("tar-stream");
68
const session = new inspector.Session();
9+
const path = require("path");
10+
const fs = require("fs/promises");
711

8-
const PROFILER_DIR = "nodeprofile";
12+
const PROFILER_DIR = path.join(__dirname, "../../../log/nodeprofile");
913

1014
var _id = null;
1115

@@ -424,23 +428,19 @@ function sessionPost(cmd) {
424428
}
425429

426430
/**
427-
* Saves the result to gridfs
428-
* @param {string} filename file name with extension
429-
* @param {object} result result object returned by the profiler
430-
* @returns {Promise<string>} filename
431+
* Takes a snapshot and writes its content to the passed stream.
432+
* IMPORTANT: it will call the "end" for the passed stream object when it's done
433+
* @param {Writable} stream Writable stream to write snapshot content to
431434
*/
432-
function saveProfilerResult(filename, result) {
433-
return new Promise((res, rej) => {
434-
countlyFs.gridfs.saveData(
435-
PROFILER_DIR, filename, JSON.stringify(result),
436-
{ writeMode: "overwrite" },
437-
function(err) {
438-
if (err) {
439-
return rej(err);
440-
}
441-
res(filename);
442-
}
443-
);
435+
function takeHeapSnapshot(stream) {
436+
session.connect();
437+
session.on("HeapProfiler.addHeapSnapshotChunk", m => stream.write(m.params.chunk));
438+
session.post("HeapProfiler.takeHeapSnapshot", null, (err) => {
439+
session.disconnect();
440+
stream.end();
441+
if (err) {
442+
console.error(err);
443+
}
444444
});
445445
}
446446

@@ -470,30 +470,22 @@ async function startProfiler() {
470470
async function stopProfiler(processName) {
471471
const errors = [];
472472

473-
// clear old files
474473
try {
475-
await new Promise(
476-
(res, rej) => countlyFs.gridfs.deleteAll(
477-
PROFILER_DIR,
478-
null,
479-
err => err ? rej(err) : res()
480-
)
481-
);
474+
await fs.rm(PROFILER_DIR, { force: true, recursive: true }); // clear old files
475+
await fs.mkdir(PROFILER_DIR, { recursive: true });
482476
}
483477
catch (err) {
484-
if (err.code === 26) { // NamespaceNotFound: thrown when there's no collection initially
485-
// do nothing...
486-
}
487-
else {
488-
throw err;
489-
}
478+
errors.push(err);
490479
}
491480

492481
// coverage
493482
try {
494483
const coverage = await sessionPost("Profiler.takePreciseCoverage");
495-
await saveProfilerResult(processName + ".coverage", coverage?.result);
496484
await sessionPost("Profiler.stopPreciseCoverage");
485+
await fs.writeFile(
486+
path.join(PROFILER_DIR, processName + ".coverage"),
487+
JSON.stringify(coverage?.result)
488+
);
497489
}
498490
catch (err) {
499491
errors.push(err);
@@ -502,8 +494,11 @@ async function stopProfiler(processName) {
502494
// cpu profiler
503495
try {
504496
const cpuProfile = await sessionPost("Profiler.stop");
505-
await saveProfilerResult(processName + ".cpuprofile", cpuProfile?.profile);
506497
await sessionPost("Profiler.disable");
498+
await fs.writeFile(
499+
path.join(PROFILER_DIR, processName + ".cpuprofile"),
500+
JSON.stringify(cpuProfile?.profile)
501+
);
507502
}
508503
catch (err) {
509504
errors.push(err);
@@ -512,8 +507,11 @@ async function stopProfiler(processName) {
512507
// heap profiler
513508
try {
514509
const heapProfile = await sessionPost("HeapProfiler.stopSampling");
515-
await saveProfilerResult(processName + ".heapprofile", heapProfile?.profile);
516510
await sessionPost("HeapProfiler.disable");
511+
await fs.writeFile(
512+
path.join(PROFILER_DIR, processName + ".heapprofile"),
513+
JSON.stringify(heapProfile?.profile)
514+
);
517515
}
518516
catch (err) {
519517
errors.push(err);
@@ -526,39 +524,24 @@ async function stopProfiler(processName) {
526524
}
527525
}
528526

529-
/**
530-
* Returns the data of a file in PROFILER_DIR collection
531-
* @param {string} filename file name with extension
532-
* @returns {Promise<{data:string, filename:string}>} file object with name and content
533-
*/
534-
function downloadProfilerFile(filename) {
535-
return new Promise((resolve, reject) => {
536-
countlyFs.gridfs.getData(PROFILER_DIR, filename, {}, (err, data) => {
537-
if (err) {
538-
return reject("File not found");
539-
}
540-
resolve({ data, filename });
541-
});
542-
});
543-
}
544527
/**
545528
* Returns the names, creation dates and size of all files in the PROFILER_DIR collection
546-
* @returns {Promise<Array<{createdOn: Date, filename: string, size: number }>>} file info
529+
* @returns {Promise<Array<{createdOn: Date, filename: string, size: number, path: string }>>} file info
547530
*/
548-
function listProfilerFiles() {
549-
return new Promise((resolve, reject) => {
550-
countlyFs.gridfs.listFiles(PROFILER_DIR, (err, files) => {
551-
if (err) {
552-
return reject(err);
553-
}
554-
resolve(files);
555-
});
556-
});
531+
async function listProfilerFiles() {
532+
const files = await fs.readdir(PROFILER_DIR);
533+
return Promise.all(
534+
files.map(async(filename) => {
535+
const fullPath = path.join(PROFILER_DIR, filename);
536+
const { birthtime, size } = await fs.stat(fullPath);
537+
return { createdOn: birthtime, size, filename, path: fullPath };
538+
})
539+
);
557540
}
558541

559542
/**
560543
* Returns the tarball read stream for all profiler files
561-
* @returns {tar.Pack} tar stream
544+
* @returns {Promise<tar.Pack>} tar stream
562545
*/
563546
async function profilerFilesTarStream() {
564547
const files = await listProfilerFiles();
@@ -569,14 +552,8 @@ async function profilerFilesTarStream() {
569552
const pack = tar.pack();
570553
for (let i = 0; i < files.length; i++) {
571554
const entry = pack.entry({ name: files[i].filename, size: files[i].size });
572-
const stream = await new Promise((res, rej) => {
573-
countlyFs.gridfs.getStream(
574-
PROFILER_DIR,
575-
files[i].filename,
576-
{},
577-
(err, fileStream) => err ? rej(err) : res(fileStream)
578-
);
579-
});
555+
const handle = await fs.open(files[i].path);
556+
const stream = handle.createReadStream();
580557
stream.pipe(entry);
581558
stream.on("end", () => {
582559
entry.end();
@@ -622,9 +599,9 @@ function stopInspector() {
622599
});
623600
}
624601

602+
exports.takeHeapSnapshot = takeHeapSnapshot;
625603
exports.startProfiler = startProfiler;
626604
exports.stopProfiler = stopProfiler;
627-
exports.downloadProfilerFile = downloadProfilerFile;
628605
exports.listProfilerFiles = listProfilerFiles;
629606
exports.startInspector = startInspector;
630607
exports.stopInspector = stopInspector;

0 commit comments

Comments
 (0)