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.
@@ -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, MdnsService, PaseClient } from '@matter/main/protocol';
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
- this.log.info(`Dispose ${servers.length} MdnsService...`);
154
- for (const server of servers) {
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 = 18;
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
- if (this.aggregatorSerialNumber)
610
- await storageContext?.set('serialNumber', this.aggregatorSerialNumber);
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 './export.js';
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
+ }