matterbridge 3.3.3-dev-20251019-a73923c → 3.3.4-dev-20251020-4d2dd49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/cli.js +109 -313
- package/dist/cliHistory.js +15 -51
- package/dist/matterbridge.js +6 -78
- package/dist/utils/commandLine.js +6 -1
- package/dist/utils/inspector.js +200 -0
- package/dist/utils/tracker.js +229 -0
- package/frontend/build/assets/index.js +4 -4
- package/frontend/build/assets/vendor_mdi.js +1 -1
- package/frontend/package.json +1 -1
- package/npm-shrinkwrap.json +44 -44
- package/package.json +2 -2
package/dist/matterbridge.js
CHANGED
|
@@ -8,7 +8,7 @@ import { inspect } from 'node:util';
|
|
|
8
8
|
import { AnsiLogger, UNDERLINE, UNDERLINEOFF, db, debugStringify, BRIGHT, RESET, er, nf, rs, wr, RED, GREEN, zb, CYAN, nt, BLUE, or } from 'node-ansi-logger';
|
|
9
9
|
import { NodeStorageManager } from 'node-persist-manager';
|
|
10
10
|
import { DeviceTypeId, Endpoint, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, VendorId, StorageService, Environment, ServerNode, UINT32_MAX, UINT16_MAX, Crypto, } from '@matter/main';
|
|
11
|
-
import { FabricAction,
|
|
11
|
+
import { FabricAction, PaseClient } from '@matter/main/protocol';
|
|
12
12
|
import { AggregatorEndpoint } from '@matter/main/endpoints';
|
|
13
13
|
import { BasicInformationServer } from '@matter/main/behaviors/basic-information';
|
|
14
14
|
import { getParameter, getIntParameter, hasParameter } from './utils/commandLine.js';
|
|
@@ -130,33 +130,9 @@ export class Matterbridge extends EventEmitter {
|
|
|
130
130
|
}
|
|
131
131
|
async destroyInstance(timeout = 1000, pause = 250) {
|
|
132
132
|
this.log.info(`Destroy instance...`);
|
|
133
|
-
const servers = [];
|
|
134
|
-
if (this.bridgeMode === 'bridge') {
|
|
135
|
-
if (this.serverNode)
|
|
136
|
-
servers.push(this.serverNode);
|
|
137
|
-
}
|
|
138
|
-
if (this.bridgeMode === 'childbridge' && this.plugins !== undefined) {
|
|
139
|
-
for (const plugin of this.plugins.array()) {
|
|
140
|
-
if (plugin.serverNode)
|
|
141
|
-
servers.push(plugin.serverNode);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (this.devices !== undefined) {
|
|
145
|
-
for (const device of this.devices.array()) {
|
|
146
|
-
if (device.mode === 'server' && device.serverNode)
|
|
147
|
-
servers.push(device.serverNode);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
await Promise.resolve();
|
|
151
|
-
await wait(pause, 'destroyInstance start', true);
|
|
152
133
|
await this.cleanup('destroying instance...', false, timeout);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
await server.env.get(MdnsService)[Symbol.asyncDispose]();
|
|
156
|
-
this.log.info(`Closed ${server.id} MdnsService`);
|
|
157
|
-
}
|
|
158
|
-
await Promise.resolve();
|
|
159
|
-
await wait(pause, 'destroyInstance stop', true);
|
|
134
|
+
if (pause)
|
|
135
|
+
await wait(pause, 'destroyInstance stop', true);
|
|
160
136
|
}
|
|
161
137
|
async initialize() {
|
|
162
138
|
this.emit('initialize_started');
|
|
@@ -478,7 +454,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
478
454
|
`${hasParameter('controller') ? 'mode controller ' : ''}` +
|
|
479
455
|
`${this.restartMode !== '' ? 'restart mode ' + this.restartMode + ' ' : ''}` +
|
|
480
456
|
`running on ${this.systemInformation.osType} (v.${this.systemInformation.osRelease}) platform ${this.systemInformation.osPlatform} arch ${this.systemInformation.osArch}`);
|
|
481
|
-
const minNodeVersion =
|
|
457
|
+
const minNodeVersion = 20;
|
|
482
458
|
const nodeVersion = process.versions.node;
|
|
483
459
|
const versionMajor = parseInt(nodeVersion.split('.')[0]);
|
|
484
460
|
if (versionMajor < minNodeVersion) {
|
|
@@ -490,52 +466,6 @@ export class Matterbridge extends EventEmitter {
|
|
|
490
466
|
this.initialized = true;
|
|
491
467
|
}
|
|
492
468
|
async parseCommandLine() {
|
|
493
|
-
if (hasParameter('help')) {
|
|
494
|
-
this.log.info(`\nUsage: matterbridge [options]\n
|
|
495
|
-
Options:
|
|
496
|
-
--help: show the help
|
|
497
|
-
--bridge: start Matterbridge in bridge mode
|
|
498
|
-
--childbridge: start Matterbridge in childbridge mode
|
|
499
|
-
--port [port]: start the commissioning server on the given port (default 5540)
|
|
500
|
-
--mdnsinterface [name]: set the interface to use for the matter server mdnsInterface (default all interfaces)
|
|
501
|
-
--ipv4address [address]: set the ipv4 interface address to use for the matter listener (default all addresses)
|
|
502
|
-
--ipv6address [address]: set the ipv6 interface address to use for the matter listener (default all addresses)
|
|
503
|
-
--frontend [port]: start the frontend on the given port (default 8283)
|
|
504
|
-
--logger: set the matterbridge logger level: debug | info | notice | warn | error | fatal (default info)
|
|
505
|
-
--filelogger enable the matterbridge file logger (matterbridge.log)
|
|
506
|
-
--matterlogger: set the matter.js logger level: debug | info | notice | warn | error | fatal (default info)
|
|
507
|
-
--matterfilelogger enable the matter.js file logger (matter.log)
|
|
508
|
-
--reset: remove the commissioning for Matterbridge (bridge mode). Shutdown Matterbridge before using it!
|
|
509
|
-
--factoryreset: remove all commissioning information and reset all internal storages. Shutdown Matterbridge before using it!
|
|
510
|
-
--list: list the registered plugins
|
|
511
|
-
--loginterfaces: log the network interfaces (usefull for finding the name of the interface to use with -mdnsinterface option)
|
|
512
|
-
--logstorage: log the node storage
|
|
513
|
-
--sudo: force the use of sudo to install or update packages if the internal logic fails
|
|
514
|
-
--nosudo: force not to use sudo to install or update packages if the internal logic fails
|
|
515
|
-
--norestore: force not to automatically restore the matterbridge node storage and the matter storage from backup if it is corrupted
|
|
516
|
-
--novirtual: disable the creation of the virtual devices Restart, Update and Reboot Matterbridge
|
|
517
|
-
--ssl: enable SSL for the frontend and the WebSocketServer (the server will use the certificates and switch to https)
|
|
518
|
-
--mtls: enable mTLS for the frontend and the WebSocketServer (both server and client will use and require the certificates and switch to https)
|
|
519
|
-
--vendorId: override the default vendorId 0xfff1
|
|
520
|
-
--vendorName: override the default vendorName "Matterbridge"
|
|
521
|
-
--productId: override the default productId 0x8000
|
|
522
|
-
--productName: override the default productName "Matterbridge aggregator"
|
|
523
|
-
--service: enable the service mode (used in the systemctl configuration file)
|
|
524
|
-
--docker: enable the docker mode (used in the docker image)
|
|
525
|
-
--homedir: override the home directory (default: os.homedir())
|
|
526
|
-
--add [plugin path]: register the plugin from the given absolute or relative path
|
|
527
|
-
--add [plugin name]: register the globally installed plugin with the given name
|
|
528
|
-
--remove [plugin path]: remove the plugin from the given absolute or relative path
|
|
529
|
-
--remove [plugin name]: remove the globally installed plugin with the given name
|
|
530
|
-
--enable [plugin path]: enable the plugin from the given absolute or relative path
|
|
531
|
-
--enable [plugin name]: enable the globally installed plugin with the given name
|
|
532
|
-
--disable [plugin path]: disable the plugin from the given absolute or relative path
|
|
533
|
-
--disable [plugin name]: disable the globally installed plugin with the given name
|
|
534
|
-
--reset [plugin path]: remove the commissioning for the plugin from the given absolute or relative path (childbridge mode). Shutdown Matterbridge before using it!
|
|
535
|
-
--reset [plugin name]: remove the commissioning for the globally installed plugin (childbridge mode). Shutdown Matterbridge before using it!${rs}`);
|
|
536
|
-
this.shutdown = true;
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
469
|
if (hasParameter('list')) {
|
|
540
470
|
this.log.info(`│ Registered plugins (${this.plugins.length})`);
|
|
541
471
|
let index = 0;
|
|
@@ -606,10 +536,8 @@ export class Matterbridge extends EventEmitter {
|
|
|
606
536
|
if (this.aggregatorSerialNumber && this.aggregatorUniqueId && this.matterStorageService) {
|
|
607
537
|
const storageManager = await this.matterStorageService.open('Matterbridge');
|
|
608
538
|
const storageContext = storageManager?.createContext('persist');
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
if (this.aggregatorUniqueId)
|
|
612
|
-
await storageContext?.set('uniqueId', this.aggregatorUniqueId);
|
|
539
|
+
await storageContext?.set('serialNumber', this.aggregatorSerialNumber);
|
|
540
|
+
await storageContext?.set('uniqueId', this.aggregatorUniqueId);
|
|
613
541
|
}
|
|
614
542
|
}
|
|
615
543
|
catch (error) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isValidNumber } from './
|
|
1
|
+
import { isValidNumber } from './isvalid.js';
|
|
2
2
|
export function hasParameter(name) {
|
|
3
3
|
const commandArguments = process.argv.slice(2);
|
|
4
4
|
let markerIncluded = commandArguments.includes(`-${name}`);
|
|
@@ -6,6 +6,11 @@ export function hasParameter(name) {
|
|
|
6
6
|
markerIncluded = commandArguments.includes(`--${name}`);
|
|
7
7
|
return markerIncluded;
|
|
8
8
|
}
|
|
9
|
+
export function hasAnyParameter(...params) {
|
|
10
|
+
return params.some((param) => {
|
|
11
|
+
return hasParameter(param);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
9
14
|
export function getParameter(name) {
|
|
10
15
|
const commandArguments = process.argv.slice(2);
|
|
11
16
|
let markerIndex = commandArguments.indexOf(`-${name}`);
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
if (process.argv.includes('--loader') || process.argv.includes('-loader'))
|
|
2
|
+
console.log('\u001B[32mInspector loaded.\u001B[40;0m');
|
|
3
|
+
import EventEmitter from 'node:events';
|
|
4
|
+
import { AnsiLogger, BRIGHT, CYAN, RESET, YELLOW, db } from 'node-ansi-logger';
|
|
5
|
+
export class Inspector extends EventEmitter {
|
|
6
|
+
name;
|
|
7
|
+
debug;
|
|
8
|
+
verbose;
|
|
9
|
+
session;
|
|
10
|
+
snapshotInterval;
|
|
11
|
+
snapshotInProgress = false;
|
|
12
|
+
log;
|
|
13
|
+
constructor(name = 'Inspector', debug = false, verbose = false) {
|
|
14
|
+
super();
|
|
15
|
+
this.name = name;
|
|
16
|
+
this.debug = debug;
|
|
17
|
+
this.verbose = verbose;
|
|
18
|
+
if (process.argv.includes('--debug') || process.argv.includes('-debug')) {
|
|
19
|
+
this.debug = true;
|
|
20
|
+
}
|
|
21
|
+
if (process.argv.includes('--verbose') || process.argv.includes('-verbose')) {
|
|
22
|
+
this.verbose = true;
|
|
23
|
+
}
|
|
24
|
+
this.log = new AnsiLogger({ logName: this.name, logTimestampFormat: 4, logLevel: this.debug ? "debug" : "info" });
|
|
25
|
+
this.log.logNameColor = YELLOW;
|
|
26
|
+
this.on('start', () => {
|
|
27
|
+
this.start();
|
|
28
|
+
});
|
|
29
|
+
this.on('stop', () => {
|
|
30
|
+
this.stop();
|
|
31
|
+
});
|
|
32
|
+
this.on('snapshot', () => {
|
|
33
|
+
this.takeHeapSnapshot();
|
|
34
|
+
});
|
|
35
|
+
this.on('gc', () => {
|
|
36
|
+
this.runGarbageCollector();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async start() {
|
|
40
|
+
if (this.session) {
|
|
41
|
+
this.log.warn('Inspector session already active.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const { Session } = await import('node:inspector');
|
|
45
|
+
const { mkdirSync } = await import('node:fs');
|
|
46
|
+
this.log.debug(`Starting heap sampling...`);
|
|
47
|
+
mkdirSync('heap_profiles', { recursive: true });
|
|
48
|
+
mkdirSync('heap_snapshots', { recursive: true });
|
|
49
|
+
try {
|
|
50
|
+
this.session = new Session();
|
|
51
|
+
this.session.connect();
|
|
52
|
+
await new Promise((resolve, reject) => {
|
|
53
|
+
this.session?.post('HeapProfiler.startSampling', (err) => (err ? reject(err) : resolve()));
|
|
54
|
+
});
|
|
55
|
+
this.log.debug(`Started heap sampling`);
|
|
56
|
+
const { getIntParameter } = await import('./commandLine.js');
|
|
57
|
+
const interval = getIntParameter('snapshotinterval');
|
|
58
|
+
if (interval && interval >= 30000) {
|
|
59
|
+
this.log.debug(`Started heap snapshot interval of ${CYAN}${interval}${db} ms`);
|
|
60
|
+
clearInterval(this.snapshotInterval);
|
|
61
|
+
this.snapshotInterval = setInterval(async () => {
|
|
62
|
+
try {
|
|
63
|
+
if (this.snapshotInProgress) {
|
|
64
|
+
if (this.debug)
|
|
65
|
+
this.log.debug(`Skip heap snapshot: previous snapshot still in progress`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.log.debug(`Run heap snapshot interval`);
|
|
69
|
+
await this.takeHeapSnapshot();
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
this.log.error(`Error during scheduled heap snapshot: ${err instanceof Error ? err.message : err}`);
|
|
73
|
+
}
|
|
74
|
+
}, interval).unref();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
this.log.error(`Failed to start heap sampling: ${err instanceof Error ? err.message : err}`);
|
|
79
|
+
this.session?.disconnect();
|
|
80
|
+
this.session = undefined;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async stop() {
|
|
85
|
+
if (!this.session) {
|
|
86
|
+
this.log.warn('No active inspector session.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const { writeFileSync } = await import('node:fs');
|
|
90
|
+
const path = await import('node:path');
|
|
91
|
+
this.log.debug(`Stopping heap sampling...`);
|
|
92
|
+
if (this.snapshotInterval) {
|
|
93
|
+
this.log.debug(`Clearing heap snapshot interval...`);
|
|
94
|
+
clearInterval(this.snapshotInterval);
|
|
95
|
+
await this.takeHeapSnapshot();
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const result = await new Promise((resolve, reject) => {
|
|
99
|
+
this.session?.post('HeapProfiler.stopSampling', (err, result) => (err ? reject(err) : resolve(result)));
|
|
100
|
+
});
|
|
101
|
+
const profile = JSON.stringify(result.profile);
|
|
102
|
+
const safeTimestamp = new Date().toISOString().replace(/[<>:"/\\|?*]/g, '-');
|
|
103
|
+
const filename = path.join('heap_profiles', `${safeTimestamp}.heapprofile`);
|
|
104
|
+
writeFileSync(filename, profile);
|
|
105
|
+
this.log.debug(`Heap sampling profile saved to ${CYAN}${filename}${db}`);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
this.log.error(`Failed to stop heap sampling: ${err instanceof Error ? err.message : err}`);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
this.session.disconnect();
|
|
112
|
+
this.session = undefined;
|
|
113
|
+
this.log.debug(`Stopped heap sampling`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async takeHeapSnapshot() {
|
|
117
|
+
if (!this.session) {
|
|
118
|
+
this.log.warn('No active inspector session.');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (this.snapshotInProgress) {
|
|
122
|
+
if (this.debug)
|
|
123
|
+
this.log.debug('Heap snapshot already in progress, skipping.');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.snapshotInProgress = true;
|
|
127
|
+
const { createWriteStream } = await import('node:fs');
|
|
128
|
+
const path = await import('node:path');
|
|
129
|
+
const safeTimestamp = new Date().toISOString().replace(/[<>:"/\\|?*]/g, '-');
|
|
130
|
+
const filename = path.join('heap_snapshots', `${safeTimestamp}.heapsnapshot`);
|
|
131
|
+
this.runGarbageCollector('minor', 'async');
|
|
132
|
+
this.runGarbageCollector('major', 'async');
|
|
133
|
+
if (this.debug)
|
|
134
|
+
this.log.debug(`Taking heap snapshot to ${CYAN}${filename}${db}...`);
|
|
135
|
+
const stream = createWriteStream(filename, { flags: 'w' });
|
|
136
|
+
let streamErrored = false;
|
|
137
|
+
const onStreamError = (err) => {
|
|
138
|
+
streamErrored = true;
|
|
139
|
+
this.log.error(`Heap snapshot stream error: ${err instanceof Error ? err.message : err}`);
|
|
140
|
+
};
|
|
141
|
+
stream.once('error', onStreamError);
|
|
142
|
+
const chunksListener = (notification) => {
|
|
143
|
+
if (!stream.write(notification.params.chunk)) {
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
this.session.on('HeapProfiler.addHeapSnapshotChunk', chunksListener);
|
|
147
|
+
try {
|
|
148
|
+
await new Promise((resolve) => {
|
|
149
|
+
this.session?.post('HeapProfiler.takeHeapSnapshot', (err) => {
|
|
150
|
+
this.session?.off('HeapProfiler.addHeapSnapshotChunk', chunksListener);
|
|
151
|
+
const finalize = () => {
|
|
152
|
+
if (!err && !streamErrored) {
|
|
153
|
+
if (this.debug)
|
|
154
|
+
this.log.debug(`Heap sampling snapshot saved to ${CYAN}${filename}${db}`);
|
|
155
|
+
this.runGarbageCollector('minor', 'async');
|
|
156
|
+
this.runGarbageCollector('major', 'async');
|
|
157
|
+
this.emit('snapshot_done');
|
|
158
|
+
}
|
|
159
|
+
else if (err) {
|
|
160
|
+
this.log.error(`Failed to take heap snapshot: ${err instanceof Error ? err.message : err}`);
|
|
161
|
+
this.runGarbageCollector('minor', 'async');
|
|
162
|
+
this.runGarbageCollector('major', 'async');
|
|
163
|
+
}
|
|
164
|
+
resolve();
|
|
165
|
+
};
|
|
166
|
+
try {
|
|
167
|
+
stream.end(() => finalize());
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
this.log.error(`Error finalizing heap snapshot stream: ${e instanceof Error ? e.message : e}`);
|
|
171
|
+
finalize();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
this.snapshotInProgress = false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
runGarbageCollector(type = 'major', execution = 'async') {
|
|
181
|
+
if (global.gc && typeof global.gc === 'function') {
|
|
182
|
+
try {
|
|
183
|
+
global.gc({ type, execution });
|
|
184
|
+
if (this.debug)
|
|
185
|
+
this.log.debug(`${CYAN}${BRIGHT}Garbage collection (${type}-${execution}) triggered at ${new Date(Date.now()).toLocaleString()}.${RESET}${db}`);
|
|
186
|
+
this.emit('gc_done', type, execution);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
global.gc();
|
|
190
|
+
if (this.debug)
|
|
191
|
+
this.log.debug(`${CYAN}${BRIGHT}Garbage collection (minor-async) triggered at ${new Date(Date.now()).toLocaleString()}.${RESET}${db}`);
|
|
192
|
+
this.emit('gc_done', 'minor', 'async');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
if (this.debug)
|
|
197
|
+
this.log.debug(`${CYAN}${BRIGHT}Garbage collection not exposed. Start Node.js with --expose-gc to enable manual garbage collection.${RESET}${db}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
if (process.argv.includes('--loader') || process.argv.includes('-loader'))
|
|
2
|
+
console.log('\u001B[32mTracker loaded.\u001B[40;0m');
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import EventEmitter from 'node:events';
|
|
5
|
+
import { AnsiLogger, BRIGHT, CYAN, RESET, YELLOW, db, RED } from 'node-ansi-logger';
|
|
6
|
+
export class Tracker extends EventEmitter {
|
|
7
|
+
name;
|
|
8
|
+
debug;
|
|
9
|
+
verbose;
|
|
10
|
+
trackerInterval;
|
|
11
|
+
static historyIndex = 0;
|
|
12
|
+
static historySize = 2880;
|
|
13
|
+
static history = Array.from({ length: this.historySize }, () => ({
|
|
14
|
+
timestamp: 0,
|
|
15
|
+
freeMemory: 0,
|
|
16
|
+
peakFreeMemory: 0,
|
|
17
|
+
totalMemory: 0,
|
|
18
|
+
peakTotalMemory: 0,
|
|
19
|
+
osCpu: 0,
|
|
20
|
+
peakOsCpu: 0,
|
|
21
|
+
processCpu: 0,
|
|
22
|
+
peakProcessCpu: 0,
|
|
23
|
+
rss: 0,
|
|
24
|
+
peakRss: 0,
|
|
25
|
+
heapUsed: 0,
|
|
26
|
+
peakHeapUsed: 0,
|
|
27
|
+
heapTotal: 0,
|
|
28
|
+
peakHeapTotal: 0,
|
|
29
|
+
external: 0,
|
|
30
|
+
peakExternal: 0,
|
|
31
|
+
arrayBuffers: 0,
|
|
32
|
+
peakArrayBuffers: 0,
|
|
33
|
+
}));
|
|
34
|
+
prevCpus = os.cpus();
|
|
35
|
+
prevCpuUsage = process.cpuUsage();
|
|
36
|
+
log;
|
|
37
|
+
constructor(name = 'Tracker', debug = false, verbose = false) {
|
|
38
|
+
super();
|
|
39
|
+
this.name = name;
|
|
40
|
+
this.debug = debug;
|
|
41
|
+
this.verbose = verbose;
|
|
42
|
+
if (process.argv.includes('--debug') || process.argv.includes('-debug')) {
|
|
43
|
+
this.debug = true;
|
|
44
|
+
}
|
|
45
|
+
if (process.argv.includes('--verbose') || process.argv.includes('-verbose')) {
|
|
46
|
+
this.verbose = true;
|
|
47
|
+
}
|
|
48
|
+
this.log = new AnsiLogger({ logName: name, logTimestampFormat: 4, logLevel: this.debug ? "debug" : "info" });
|
|
49
|
+
this.log.logNameColor = YELLOW;
|
|
50
|
+
if (this.verbose) {
|
|
51
|
+
this.log.debug(`os.cpus():\n${RESET}`, os.cpus());
|
|
52
|
+
this.log.debug(`process.cpuUsage():\n${RESET}`, process.cpuUsage());
|
|
53
|
+
this.log.debug(`process.memoryUsage():\n${RESET}`, process.memoryUsage());
|
|
54
|
+
}
|
|
55
|
+
this.on('start', () => {
|
|
56
|
+
this.start();
|
|
57
|
+
});
|
|
58
|
+
this.on('stop', () => {
|
|
59
|
+
this.stop();
|
|
60
|
+
});
|
|
61
|
+
this.on('reset_peaks', () => {
|
|
62
|
+
this.resetPeaks();
|
|
63
|
+
});
|
|
64
|
+
this.on('gc', () => {
|
|
65
|
+
this.runGarbageCollector();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
formatTimeStamp(timestamp) {
|
|
69
|
+
return `${new Date(timestamp).toLocaleString()}`;
|
|
70
|
+
}
|
|
71
|
+
formatOsUpTime(seconds) {
|
|
72
|
+
if (seconds >= 86400) {
|
|
73
|
+
const days = Math.floor(seconds / 86400);
|
|
74
|
+
return `${days} day${days !== 1 ? 's' : ''}`;
|
|
75
|
+
}
|
|
76
|
+
if (seconds >= 3600) {
|
|
77
|
+
const hours = Math.floor(seconds / 3600);
|
|
78
|
+
return `${hours} hour${hours !== 1 ? 's' : ''}`;
|
|
79
|
+
}
|
|
80
|
+
if (seconds >= 60) {
|
|
81
|
+
const minutes = Math.floor(seconds / 60);
|
|
82
|
+
return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
|
|
83
|
+
}
|
|
84
|
+
return `${seconds} second${seconds !== 1 ? 's' : ''}`;
|
|
85
|
+
}
|
|
86
|
+
formatPercent(percent) {
|
|
87
|
+
return `${percent.toFixed(2)} %`;
|
|
88
|
+
}
|
|
89
|
+
formatBytes(bytes) {
|
|
90
|
+
if (bytes === 0)
|
|
91
|
+
return '0 B';
|
|
92
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
93
|
+
const idx = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
94
|
+
const value = bytes / Math.pow(1024, idx);
|
|
95
|
+
return `${value.toFixed(2)} ${units[idx]}`;
|
|
96
|
+
}
|
|
97
|
+
start(sampleIntervalMs = 10000) {
|
|
98
|
+
if (this.trackerInterval)
|
|
99
|
+
return;
|
|
100
|
+
this.log.debug(`Tracker starting...`);
|
|
101
|
+
let tryGcCount = 0;
|
|
102
|
+
this.prevCpus = os.cpus();
|
|
103
|
+
this.prevCpuUsage = process.cpuUsage();
|
|
104
|
+
this.trackerInterval = setInterval(() => {
|
|
105
|
+
tryGcCount += sampleIntervalMs / 1000;
|
|
106
|
+
if (tryGcCount > 60 * 60) {
|
|
107
|
+
this.runGarbageCollector();
|
|
108
|
+
tryGcCount = 0;
|
|
109
|
+
}
|
|
110
|
+
const entry = Tracker.history[Tracker.historyIndex];
|
|
111
|
+
const prevEntry = Tracker.history[(Tracker.historyIndex + Tracker.historySize - 1) % Tracker.historySize];
|
|
112
|
+
entry.timestamp = Date.now();
|
|
113
|
+
this.emit('uptime', os.uptime(), process.uptime());
|
|
114
|
+
const currentCpus = os.cpus();
|
|
115
|
+
const loads = currentCpus.map((cpu, idx) => {
|
|
116
|
+
const prev = this.prevCpus[idx]?.times;
|
|
117
|
+
if (!prev)
|
|
118
|
+
return 0;
|
|
119
|
+
const cur = cpu.times;
|
|
120
|
+
const idleDelta = cur.idle - prev.idle;
|
|
121
|
+
const busyDelta = cur.user - prev.user + (cur.nice - prev.nice) + (cur.sys - prev.sys) + (cur.irq - prev.irq);
|
|
122
|
+
const totalDelta = busyDelta + idleDelta;
|
|
123
|
+
if (totalDelta <= 0)
|
|
124
|
+
return 0;
|
|
125
|
+
return busyDelta / totalDelta;
|
|
126
|
+
});
|
|
127
|
+
this.prevCpus = currentCpus;
|
|
128
|
+
const avgLoad = loads.length === 0 ? 0 : loads.reduce((sum, value) => sum + value, 0) / loads.length;
|
|
129
|
+
const osCpu = Number((avgLoad * 100).toFixed(2));
|
|
130
|
+
entry.osCpu = osCpu;
|
|
131
|
+
entry.peakOsCpu = Math.max(prevEntry.peakOsCpu, osCpu);
|
|
132
|
+
const diff = process.cpuUsage(this.prevCpuUsage);
|
|
133
|
+
this.prevCpuUsage = process.cpuUsage();
|
|
134
|
+
const totalMs = (diff.user + diff.system) / 1000;
|
|
135
|
+
const processCpu = Number((((totalMs / sampleIntervalMs) * 100) / currentCpus.length).toFixed(2));
|
|
136
|
+
entry.processCpu = processCpu;
|
|
137
|
+
entry.peakProcessCpu = Math.max(prevEntry.peakProcessCpu, processCpu);
|
|
138
|
+
this.emit('cpu', entry.osCpu, entry.processCpu);
|
|
139
|
+
entry.freeMemory = os.freemem();
|
|
140
|
+
entry.peakFreeMemory = Math.max(prevEntry.peakFreeMemory, entry.freeMemory);
|
|
141
|
+
entry.totalMemory = os.totalmem();
|
|
142
|
+
entry.peakTotalMemory = Math.max(prevEntry.peakTotalMemory, entry.totalMemory);
|
|
143
|
+
const mem = process.memoryUsage();
|
|
144
|
+
entry.rss = mem.rss;
|
|
145
|
+
entry.peakRss = Math.max(prevEntry.peakRss, mem.rss);
|
|
146
|
+
entry.heapUsed = mem.heapUsed;
|
|
147
|
+
entry.peakHeapUsed = Math.max(prevEntry.peakHeapUsed, mem.heapUsed);
|
|
148
|
+
entry.heapTotal = mem.heapTotal;
|
|
149
|
+
entry.peakHeapTotal = Math.max(prevEntry.peakHeapTotal, mem.heapTotal);
|
|
150
|
+
entry.external = mem.external;
|
|
151
|
+
entry.peakExternal = Math.max(prevEntry.peakExternal, mem.external);
|
|
152
|
+
entry.arrayBuffers = mem.arrayBuffers;
|
|
153
|
+
entry.peakArrayBuffers = Math.max(prevEntry.peakArrayBuffers, mem.arrayBuffers);
|
|
154
|
+
this.emit('memory', entry.freeMemory, entry.totalMemory, entry.rss, entry.heapUsed, entry.heapTotal, entry.external, entry.arrayBuffers);
|
|
155
|
+
this.emit('snapshot', entry);
|
|
156
|
+
if (this.debug) {
|
|
157
|
+
this.log.debug(`Time: ${this.formatTimeStamp(entry.timestamp)} ` +
|
|
158
|
+
`os ${CYAN}${BRIGHT}${this.formatPercent(entry.osCpu)}${RESET}${db} (${entry.peakOsCpu > prevEntry.peakOsCpu ? RED : ''}${this.formatPercent(entry.peakOsCpu)}${db}) ` +
|
|
159
|
+
`process ${CYAN}${BRIGHT}${this.formatPercent(entry.processCpu)}${RESET}${db} (${entry.peakProcessCpu > prevEntry.peakProcessCpu ? RED : ''}${this.formatPercent(entry.peakProcessCpu)}${db}) ` +
|
|
160
|
+
`rss: ${CYAN}${BRIGHT}${this.formatBytes(entry.rss)}${RESET}${db} (${entry.peakRss > prevEntry.peakRss ? RED : ''}${this.formatBytes(entry.peakRss)}${db}) ` +
|
|
161
|
+
`heapUsed: ${CYAN}${BRIGHT}${this.formatBytes(entry.heapUsed)}${RESET}${db} (${entry.peakHeapUsed > prevEntry.peakHeapUsed ? RED : ''}${this.formatBytes(entry.peakHeapUsed)}${db}) ` +
|
|
162
|
+
`heapTotal: ${CYAN}${BRIGHT}${this.formatBytes(entry.heapTotal)}${RESET}${db} (${entry.peakHeapTotal > prevEntry.peakHeapTotal ? RED : ''}${this.formatBytes(entry.peakHeapTotal)}${db}) ` +
|
|
163
|
+
`external: ${CYAN}${BRIGHT}${this.formatBytes(entry.external)}${RESET}${db} (${entry.peakExternal > prevEntry.peakExternal ? RED : ''}${this.formatBytes(entry.peakExternal)}${db}) ` +
|
|
164
|
+
`arrayBuffers: ${CYAN}${BRIGHT}${this.formatBytes(entry.arrayBuffers)}${RESET}${db} (${entry.peakArrayBuffers > prevEntry.peakArrayBuffers ? RED : ''}${this.formatBytes(entry.peakArrayBuffers)}${db})`);
|
|
165
|
+
}
|
|
166
|
+
Tracker.historyIndex = (Tracker.historyIndex + 1) % Tracker.historySize;
|
|
167
|
+
}, sampleIntervalMs);
|
|
168
|
+
this.log.debug(`Tracker started`);
|
|
169
|
+
}
|
|
170
|
+
resetPeaks() {
|
|
171
|
+
const prevHistoryIndex = (Tracker.historyIndex + Tracker.historySize - 1) % Tracker.historySize;
|
|
172
|
+
Tracker.history[prevHistoryIndex].peakOsCpu = 0;
|
|
173
|
+
Tracker.history[prevHistoryIndex].peakProcessCpu = 0;
|
|
174
|
+
Tracker.history[prevHistoryIndex].peakRss = 0;
|
|
175
|
+
Tracker.history[prevHistoryIndex].peakHeapUsed = 0;
|
|
176
|
+
Tracker.history[prevHistoryIndex].peakHeapTotal = 0;
|
|
177
|
+
Tracker.history[prevHistoryIndex].peakExternal = 0;
|
|
178
|
+
Tracker.history[prevHistoryIndex].peakArrayBuffers = 0;
|
|
179
|
+
if (this.debug)
|
|
180
|
+
this.log.debug(`${CYAN}${BRIGHT}Peaks reset at ${new Date(Date.now()).toLocaleString()}.${RESET}${db}`);
|
|
181
|
+
this.emit('reset_peaks_done');
|
|
182
|
+
}
|
|
183
|
+
runGarbageCollector(type = 'major', execution = 'async') {
|
|
184
|
+
if (global.gc && typeof global.gc === 'function') {
|
|
185
|
+
try {
|
|
186
|
+
global.gc({ type, execution });
|
|
187
|
+
if (this.debug)
|
|
188
|
+
this.log.debug(`${CYAN}${BRIGHT}Garbage collection (${type}-${execution}) triggered at ${new Date(Date.now()).toLocaleString()}.${RESET}${db}`);
|
|
189
|
+
this.emit('gc_done', type, execution);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
global.gc();
|
|
193
|
+
if (this.debug)
|
|
194
|
+
this.log.debug(`${CYAN}${BRIGHT}Garbage collection (minor-async) triggered at ${new Date(Date.now()).toLocaleString()}.${RESET}${db}`);
|
|
195
|
+
this.emit('gc_done', 'minor', 'async');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
if (this.debug)
|
|
200
|
+
this.log.debug(`${CYAN}${BRIGHT}Garbage collection not exposed. Start Node.js with --expose-gc to enable manual garbage collection.${RESET}${db}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
stop() {
|
|
204
|
+
this.log.debug(`Tracker stopping...`);
|
|
205
|
+
if (this.trackerInterval) {
|
|
206
|
+
clearInterval(this.trackerInterval);
|
|
207
|
+
this.trackerInterval = undefined;
|
|
208
|
+
}
|
|
209
|
+
if (this.debug) {
|
|
210
|
+
this.log.debug(`Tracker history for ${YELLOW}${BRIGHT}${this.name}:${RESET}`);
|
|
211
|
+
this.log.debug('Timestamp Host cpu Process cpu Rss Heap Used Heap Total External ArrayBuffers');
|
|
212
|
+
for (let i = 0; i < Tracker.historySize; i++) {
|
|
213
|
+
const index = (Tracker.historyIndex + i) % Tracker.historySize;
|
|
214
|
+
const entry = Tracker.history[index];
|
|
215
|
+
if (entry.timestamp === 0)
|
|
216
|
+
continue;
|
|
217
|
+
this.log.debug(`${this.formatTimeStamp(entry.timestamp)} ` +
|
|
218
|
+
`${CYAN}${BRIGHT}${this.formatPercent(entry.osCpu).padStart(8)}${RESET} (${this.formatPercent(entry.peakOsCpu).padStart(8)}) ` +
|
|
219
|
+
`${CYAN}${BRIGHT}${this.formatPercent(entry.processCpu).padStart(8)}${RESET} (${this.formatPercent(entry.peakProcessCpu).padStart(8)}) ` +
|
|
220
|
+
`${CYAN}${BRIGHT}${this.formatBytes(entry.rss).padStart(9)}${RESET} (${this.formatBytes(entry.peakRss).padStart(9)}) ` +
|
|
221
|
+
`${CYAN}${BRIGHT}${this.formatBytes(entry.heapUsed).padStart(9)}${RESET} (${this.formatBytes(entry.peakHeapUsed).padStart(9)}) ` +
|
|
222
|
+
`${CYAN}${BRIGHT}${this.formatBytes(entry.heapTotal).padStart(9)}${RESET} (${this.formatBytes(entry.peakHeapTotal).padStart(9)}) ` +
|
|
223
|
+
`${CYAN}${BRIGHT}${this.formatBytes(entry.external).padStart(9)}${RESET} (${this.formatBytes(entry.peakExternal).padStart(9)}) ` +
|
|
224
|
+
`${CYAN}${BRIGHT}${this.formatBytes(entry.arrayBuffers).padStart(9)}${RESET} (${this.formatBytes(entry.peakArrayBuffers).padStart(9)})`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
this.log.debug(`Tracker stopped`);
|
|
228
|
+
}
|
|
229
|
+
}
|