@memlab/core
Version:
memlab core libraries
480 lines (479 loc) • 15.7 kB
JavaScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @oncall memory_lab
*/
'use strict';
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const chalk_1 = __importDefault(require("chalk"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const readline_1 = __importDefault(require("readline"));
const string_width_1 = __importDefault(require("string-width"));
const Config_1 = require("./Config");
const TABLE_MAX_WIDTH = 50;
const LOG_BUFFER_LENGTH = 100;
const prevLine = '\x1b[F';
const eraseLine = '\x1b[K';
const barComplete = chalk_1.default.green('\u2588');
const barIncomplete = chalk_1.default.grey('\u2591');
function formatTableArg(arg) {
if (!Array.isArray(arg)) {
return arg;
}
arg.forEach(obj => {
if (typeof obj !== 'object') {
return;
}
for (const key of Object.keys(obj)) {
const value = obj[key];
if (typeof value !== 'string') {
continue;
}
if (value.length <= TABLE_MAX_WIDTH) {
continue;
}
obj[key] = value.substring(0, TABLE_MAX_WIDTH) + '...';
}
});
}
function registerExitCleanup(inst, exitHandler) {
const p = process;
// normal exit
p.on('exit', exitHandler.bind(null, { cleanup: true }));
// ctrl + c event
p.on('SIGINT', exitHandler.bind(null, { exit: true }));
// kill pid
p.on('SIGUSR1', exitHandler.bind(null, { exit: true }));
p.on('SIGUSR2', exitHandler.bind(null, { exit: true }));
}
class MemLabConsole {
constructor() {
this.config = {};
this.log = [];
this.logFileSet = new Set();
this.styles = {
top: (msg) => msg,
high: chalk_1.default.dim.bind(chalk_1.default),
mid: chalk_1.default.yellow.bind(chalk_1.default),
low: chalk_1.default.grey.bind(chalk_1.default),
success: chalk_1.default.green.bind(chalk_1.default),
error: chalk_1.default.red.bind(chalk_1.default),
warning: chalk_1.default.yellow.bind(chalk_1.default),
};
this.annotations = {
STACK_TRACE: 'stack-trace',
};
this.sections = {
dict: Object.create(null),
arr: [{ name: 'header', msgs: [] }],
};
this.init();
}
static getInstance() {
if (MemLabConsole.singleton) {
return MemLabConsole.singleton;
}
const inst = new MemLabConsole();
MemLabConsole.singleton = inst;
const exitHandler = (
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_options,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_exitCode) => {
inst.flushLog({ sync: true });
inst.clearPrevOverwriteMsg();
};
registerExitCleanup(inst, exitHandler);
return inst;
}
get isTextOutput() {
return this.config.outputFormat === Config_1.OutputFormat.Text;
}
get outStream() {
return this.isTextOutput ? process.stdout : process.stderr;
}
style(msg, name) {
if (Object.prototype.hasOwnProperty.call(this.styles, name)) {
return this.styles[name](msg);
}
return this.styles.low(msg);
}
init() {
this.beginSection('main-section-DO-NOT-USE');
}
getLastSection() {
const list = this.sections.arr;
return list[list.length - 1];
}
getLastMsg() {
const lastSection = this.getLastSection();
if (!lastSection || !lastSection.msgs) {
return null;
}
const msgs = lastSection.msgs;
return msgs.length === 0 ? null : msgs[msgs.length - 1];
}
logMsg(msg) {
if (typeof msg !== 'string') {
return;
}
// remove control characters
const lines = msg.split('\n').map(line => line
// eslint-disable-next-line no-control-regex
.replace(/[\u0000-\u001F\u007F-\u009F]/g, '')
.replace(/\[\d{1,3}m/g, ''));
this.log.push(...lines);
this.tryFlush();
}
tryFlush() {
if (this.log.length > LOG_BUFFER_LENGTH) {
this.flushLog({ sync: true });
}
}
flushLog(options = {}) {
const str = this.log.join('\n');
this.log = [];
if (str.length === 0) {
return;
}
// synchronous logging
if (options.sync) {
for (const logFile of this.logFileSet) {
try {
fs_1.default.appendFileSync(logFile, str + '\n', 'UTF-8');
}
catch (_a) {
// fail silently
}
}
}
else {
// async logging
const emptyCallback = () => {
// no op
};
for (const logFile of this.logFileSet) {
try {
fs_1.default.appendFile(logFile, str + '\n', 'UTF-8', emptyCallback);
}
catch (_b) {
// fail silently
}
}
}
}
pushMsg(msg, options = {}) {
if (this.sections.arr.length === 0) {
return;
}
// calculate each line's visible width
const lines = msg.split('\n').map(line => (0, string_width_1.default)(line));
const section = this.getLastSection();
section === null || section === void 0 ? void 0 : section.msgs.push({ lines, options });
}
clearPrevMsgInLastSection() {
const lastSection = this.getLastSection();
this.clearPrevMsgInSection(lastSection);
}
clearPrevMsgInSection(section) {
var _a;
if (this.config.isContinuousTest) {
return;
}
if (!section || section.msgs.length === 0) {
return;
}
if (!this.config.muteConsole) {
this.outStream.write(eraseLine);
}
const msg = section.msgs.pop();
if (!msg) {
return;
}
const lines = msg.lines;
while (lines.length > 0) {
const line = (_a = lines.pop()) !== null && _a !== void 0 ? _a : 0;
const width = this.outStream.columns;
let n = line === 0 ? 1 : Math.ceil(line / width);
if (!this.config.muteConsole && !this.config.isTest) {
while (n-- > 0) {
this.outStream.write(prevLine + eraseLine);
}
}
}
}
clearPrevSection() {
var _a;
if (this.config.isContinuousTest) {
return;
}
const lastSection = this.getLastSection();
if (!lastSection) {
return;
}
while (((_a = lastSection === null || lastSection === void 0 ? void 0 : lastSection.msgs) === null || _a === void 0 ? void 0 : _a.length) > 0) {
this.clearPrevMsgInSection(lastSection);
}
this.sections.arr.pop();
delete this.sections.dict[lastSection.name];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
shouldBeConcise(_msgType) {
if (!this.config || !this.config.runningMode) {
return false;
}
return this.config.runningMode.shouldUseConciseConsole();
}
clearPrevOverwriteMsg() {
var _a;
if (this.config.isContinuousTest) {
return;
}
const lastMsg = this.getLastMsg();
if (!lastMsg || !((_a = lastMsg.options) === null || _a === void 0 ? void 0 : _a.isOverwrite)) {
return;
}
this.clearPrevMsgInLastSection();
}
printStr(msg, options = {}) {
this.pushMsg(msg, options);
if (this.config.isTest) {
return;
}
if (this.config.isContinuousTest || !this.config.muteConsole) {
if (this.isTextOutput) {
console.log(msg);
}
else {
this.outStream.write(msg);
this.outStream.write('\n');
}
}
}
writeOutput(output) {
var _a;
this.log.push(output);
if ((_a = this.config.muteConfig) === null || _a === void 0 ? void 0 : _a.muteOutput) {
return;
}
process.stdout.write(output);
this.tryFlush();
}
registerLogFile(logFile) {
this.flushLog({ sync: true });
this.logFileSet.add(path_1.default.resolve(logFile));
}
unregisterLogFile(logFile) {
this.flushLog({ sync: true });
this.logFileSet.delete(path_1.default.resolve(logFile));
}
beginSection(name) {
if (this.config.isContinuousTest) {
return;
}
this.clearPrevOverwriteMsg();
if (this.sections.dict[name]) {
this.endSection(name);
}
const section = { name, msgs: [] };
this.sections.dict[name] = section;
this.sections.arr.push(section);
}
endSection(name) {
if (this.config.isContinuousTest || this.sections.arr.length === 0) {
return;
}
if (!this.sections.dict[name]) {
return;
}
let section;
do {
section = this.getLastSection();
this.clearPrevSection();
} while ((section === null || section === void 0 ? void 0 : section.name) !== name);
}
setConfig(config) {
this.config = config;
}
table(...args) {
var _a;
if (this.shouldBeConcise('table') ||
this.config.isTest ||
this.config.muteConsole ||
((_a = this.config.muteConfig) === null || _a === void 0 ? void 0 : _a.muteTable)) {
return;
}
this.clearPrevOverwriteMsg();
// make sure the values are not too big
formatTableArg(args[0]);
if (args[0].length === 0) {
return;
}
console.table(...args);
}
trace() {
var _a;
if (this.config.isTest ||
this.config.muteConsole ||
((_a = this.config.muteConfig) === null || _a === void 0 ? void 0 : _a.muteTrace)) {
return;
}
console.trace();
}
topLevel(msg) {
var _a;
if (this.shouldBeConcise('topLevel')) {
return this.overwrite(msg);
}
this.logMsg(msg);
if ((_a = this.config.muteConfig) === null || _a === void 0 ? void 0 : _a.muteTopLevel) {
return;
}
this.clearPrevOverwriteMsg();
this.printStr(this.style(msg, 'top'));
}
highLevel(msg) {
var _a;
if (this.shouldBeConcise('highLevel')) {
return this.overwrite(msg);
}
this.logMsg(msg);
if ((_a = this.config.muteConfig) === null || _a === void 0 ? void 0 : _a.muteHighLevel) {
return;
}
this.clearPrevOverwriteMsg();
this.printStr(this.style(msg, 'high'));
}
midLevel(msg) {
var _a;
if (this.shouldBeConcise('midLevel')) {
return this.overwrite(msg);
}
this.logMsg(msg);
if ((_a = this.config.muteConfig) === null || _a === void 0 ? void 0 : _a.muteMidLevel) {
return;
}
this.clearPrevOverwriteMsg();
this.printStr(this.style(msg, 'mid'));
}
lowLevel(msg, options = {}) {
var _a, _b;
if (this.shouldBeConcise('lowLevel')) {
return this.overwrite(msg);
}
this.logMsg(msg);
if ((_a = this.config.muteConfig) === null || _a === void 0 ? void 0 : _a.muteLowLevel) {
if (options.annotation !== this.annotations.STACK_TRACE ||
((_b = this.config.muteConfig) === null || _b === void 0 ? void 0 : _b.muteError)) {
return;
}
}
this.clearPrevOverwriteMsg();
this.printStr(this.style(msg, 'low'));
}
success(msg) {
var _a;
if (this.shouldBeConcise('success')) {
return this.overwrite(msg);
}
this.logMsg(msg);
if ((_a = this.config.muteConfig) === null || _a === void 0 ? void 0 : _a.muteSuccess) {
return;
}
this.clearPrevOverwriteMsg();
this.printStr(this.style(msg, 'success'));
}
// top level error, only used before halting the execution
criticalError(msg) {
this.clearPrevOverwriteMsg();
this.logMsg(msg);
console.error(msg);
}
error(msg) {
var _a;
this.logMsg(msg);
if ((_a = this.config.muteConfig) === null || _a === void 0 ? void 0 : _a.muteError) {
return;
}
this.clearPrevOverwriteMsg();
this.printStr(this.style(msg, 'error'));
}
warning(msg) {
var _a;
this.logMsg(msg);
if ((_a = this.config.muteConfig) === null || _a === void 0 ? void 0 : _a.muteWarning) {
return;
}
this.clearPrevOverwriteMsg();
this.printStr(this.style(msg, 'warning'));
}
nextLine() {
if (this.shouldBeConcise('nextLine')) {
return this.overwrite('');
}
this.logMsg('');
this.clearPrevOverwriteMsg();
this.printStr('');
}
overwrite(msg, options = {}) {
var _a;
this.logMsg(msg);
if ((_a = this.config.muteConfig) === null || _a === void 0 ? void 0 : _a.muteLowLevel) {
return;
}
const str = this.style(msg, options.level || 'low');
if (this.config.isContinuousTest) {
this.printStr(msg, { isOverwrite: false });
return;
}
if (this.config.isTest || this.config.muteConsole) {
this.printStr(str, { isOverwrite: true });
return;
}
this.clearPrevOverwriteMsg();
this.printStr(str, { isOverwrite: true });
}
waitForConsole(query) {
const rl = readline_1.default.createInterface({
input: process.stdin,
output: this.outStream,
});
this.pushMsg(query);
this.logMsg(query);
return new Promise(resolve => rl.question(query, ans => {
rl.close();
resolve(ans);
}));
}
progress(cur, total, options = {}) {
let width = Math.floor(this.outStream.columns * 0.8);
width = Math.min(width, 80);
const messageMaxWidth = Math.floor(width * 0.3);
let message = options.message || '';
const messageWidth = Math.min(message.length, messageMaxWidth);
message = message.substring(0, messageWidth);
// calculate progress bar
const barWidth = width - messageWidth - 1;
const barBodyWidth = barWidth - 2;
const changeIndex = Math.floor((cur / total) * barBodyWidth);
let bar = '';
for (let i = 0; i < barBodyWidth; ++i) {
bar += i < changeIndex ? barComplete : barIncomplete;
}
const percent = Math.floor((cur / total) * 100);
const progress = `${message}: |${bar}| ${percent}/100`;
this.overwrite(progress, { level: 'top' });
}
flush() {
this.clearPrevOverwriteMsg();
}
}
exports.default = MemLabConsole.getInstance();