@lakutata/monitor 3.0.0-beta.0
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/LICENSE +23 -0
- package/README.md +114 -0
- package/dist/cjs/exports/Monitor.d.ts +9 -0
- package/dist/cjs/exports/Monitor.js +28 -0
- package/dist/cjs/monitor/CpuMonitor.d.ts +60 -0
- package/dist/cjs/monitor/CpuMonitor.js +96 -0
- package/dist/cjs/monitor/EventLoopMonitor.d.ts +65 -0
- package/dist/cjs/monitor/EventLoopMonitor.js +92 -0
- package/dist/cjs/monitor/HttpRequestMonitor.d.ts +61 -0
- package/dist/cjs/monitor/HttpRequestMonitor.js +92 -0
- package/dist/cjs/monitor/MemoryMonitor.d.ts +61 -0
- package/dist/cjs/monitor/MemoryMonitor.js +152 -0
- package/dist/cjs/monitor/interfaces/ICpuMonitorStatistics.d.ts +40 -0
- package/dist/cjs/monitor/interfaces/ICpuMonitorStatistics.js +2 -0
- package/dist/cjs/monitor/interfaces/IEventLoopMonitorStatistics.d.ts +45 -0
- package/dist/cjs/monitor/interfaces/IEventLoopMonitorStatistics.js +2 -0
- package/dist/cjs/monitor/interfaces/IHttpRequestMonitorStatistics.d.ts +43 -0
- package/dist/cjs/monitor/interfaces/IHttpRequestMonitorStatistics.js +2 -0
- package/dist/cjs/monitor/interfaces/IMemoryMonitorStatistics.d.ts +124 -0
- package/dist/cjs/monitor/interfaces/IMemoryMonitorStatistics.js +2 -0
- package/dist/cjs/monitor/interfaces/IMonitor.d.ts +10 -0
- package/dist/cjs/monitor/interfaces/IMonitor.js +2 -0
- package/dist/cjs/monitor/lib/Samples.d.ts +57 -0
- package/dist/cjs/monitor/lib/Samples.js +119 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/exports/Monitor.js +9 -0
- package/dist/esm/monitor/CpuMonitor.js +92 -0
- package/dist/esm/monitor/EventLoopMonitor.js +88 -0
- package/dist/esm/monitor/HttpRequestMonitor.js +88 -0
- package/dist/esm/monitor/MemoryMonitor.js +115 -0
- package/dist/esm/monitor/interfaces/ICpuMonitorStatistics.js +1 -0
- package/dist/esm/monitor/interfaces/IEventLoopMonitorStatistics.js +1 -0
- package/dist/esm/monitor/interfaces/IHttpRequestMonitorStatistics.js +1 -0
- package/dist/esm/monitor/interfaces/IMemoryMonitorStatistics.js +1 -0
- package/dist/esm/monitor/interfaces/IMonitor.js +1 -0
- package/dist/esm/monitor/lib/Samples.js +114 -0
- package/dist/types/exports/Monitor.d.ts +9 -0
- package/dist/types/monitor/CpuMonitor.d.ts +60 -0
- package/dist/types/monitor/EventLoopMonitor.d.ts +65 -0
- package/dist/types/monitor/HttpRequestMonitor.d.ts +61 -0
- package/dist/types/monitor/MemoryMonitor.d.ts +61 -0
- package/dist/types/monitor/interfaces/ICpuMonitorStatistics.d.ts +40 -0
- package/dist/types/monitor/interfaces/IEventLoopMonitorStatistics.d.ts +45 -0
- package/dist/types/monitor/interfaces/IHttpRequestMonitorStatistics.d.ts +43 -0
- package/dist/types/monitor/interfaces/IMemoryMonitorStatistics.d.ts +124 -0
- package/dist/types/monitor/interfaces/IMonitor.d.ts +10 -0
- package/dist/types/monitor/lib/Samples.d.ts +57 -0
- package/package.json +44 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from '../monitor/CpuMonitor.js';
|
|
2
|
+
export * from '../monitor/EventLoopMonitor.js';
|
|
3
|
+
export * from '../monitor/HttpRequestMonitor.js';
|
|
4
|
+
export * from '../monitor/MemoryMonitor.js';
|
|
5
|
+
export * from '../monitor/interfaces/IMonitor.js';
|
|
6
|
+
export * from '../monitor/interfaces/ICpuMonitorStatistics.js';
|
|
7
|
+
export * from '../monitor/interfaces/IEventLoopMonitorStatistics.js';
|
|
8
|
+
export * from '../monitor/interfaces/IHttpRequestMonitorStatistics.js';
|
|
9
|
+
export * from '../monitor/interfaces/IMemoryMonitorStatistics.js';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Component } from '@lakutata/core';
|
|
2
|
+
import { round, Samples } from './lib/Samples.js';
|
|
3
|
+
/**
|
|
4
|
+
* The CPU monitor: measures the CPU time the process uses (user plus system, all its threads), as a percentage of one
|
|
5
|
+
* core: 100 is one core fully busy, and a process using several cores (worker threads, the libuv thread pool) goes
|
|
6
|
+
* above 100. It takes a sample every second, each the average usage over that second, and keeps the statistics of
|
|
7
|
+
* every sample since it was created or last {@link CpuMonitor.reset}: the latest, the minimum, the maximum, the mean
|
|
8
|
+
* and percentiles, in constant memory.
|
|
9
|
+
*
|
|
10
|
+
* Declare it in the `components` of the application (`{class: CpuMonitor}`) and get it with `@Inject` or
|
|
11
|
+
* `getObject()`; it is a singleton. It starts sampling when it is created: list it in `bootstrap` to measure from the
|
|
12
|
+
* launch. The statistics are all 0 until the first sample, one second after its creation. Its timer does not keep the
|
|
13
|
+
* process alive, and stops when the component is destroyed.
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import {Application} from 'lakutata'
|
|
17
|
+
* import {CpuMonitor, type ICpuMonitorStatistics} from 'lakutata/com/monitor'
|
|
18
|
+
*
|
|
19
|
+
* Application
|
|
20
|
+
* .run(() => ({
|
|
21
|
+
* id: 'cpu.app',
|
|
22
|
+
* name: 'Cpu',
|
|
23
|
+
* components: {
|
|
24
|
+
* cpu: {class: CpuMonitor}
|
|
25
|
+
* },
|
|
26
|
+
* //Created at the launch, so that it samples from the start
|
|
27
|
+
* bootstrap: ['cpu']
|
|
28
|
+
* }))
|
|
29
|
+
* .onLaunched(async (app: Application): Promise<void> => {
|
|
30
|
+
* const cpu: CpuMonitor = await app.getObject('cpu')
|
|
31
|
+
* setInterval((): void => {
|
|
32
|
+
* const statistics: ICpuMonitorStatistics = cpu.statistics
|
|
33
|
+
* console.log(`CPU ${statistics.usage}% (p95 ${statistics.usageP95}%, max ${statistics.usageMax}%)`)
|
|
34
|
+
* }, 60000)
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export class CpuMonitor extends Component {
|
|
39
|
+
#fractionDigits = 5;
|
|
40
|
+
#intervalDelay = 1000;
|
|
41
|
+
#samples = new Samples(1e5);
|
|
42
|
+
#sampleInterval;
|
|
43
|
+
#previousUsage = process.cpuUsage();
|
|
44
|
+
#previousTime = process.hrtime.bigint();
|
|
45
|
+
/**
|
|
46
|
+
* The statistics of the CPU samples since the monitor was created or reset, computed at each read: the latest
|
|
47
|
+
* usage and the minimum, maximum, mean and percentiles (P50 to P99) of the samples, in percent of one core, rounded
|
|
48
|
+
* to 5 decimals. The percentiles are approximate (3 significant digits). All 0 before the first sample.
|
|
49
|
+
*/
|
|
50
|
+
get statistics() {
|
|
51
|
+
return {
|
|
52
|
+
usage: round(this.#samples.latest, this.#fractionDigits),
|
|
53
|
+
usageMin: round(this.#samples.min, this.#fractionDigits),
|
|
54
|
+
usageMax: round(this.#samples.max, this.#fractionDigits),
|
|
55
|
+
usageAvg: round(this.#samples.mean, this.#fractionDigits),
|
|
56
|
+
usageP50: round(this.#samples.percentile(50), this.#fractionDigits),
|
|
57
|
+
usageP90: round(this.#samples.percentile(90), this.#fractionDigits),
|
|
58
|
+
usageP95: round(this.#samples.percentile(95), this.#fractionDigits),
|
|
59
|
+
usageP99: round(this.#samples.percentile(99), this.#fractionDigits)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Take a sample: the CPU usage since the previous sample (user plus system time over the elapsed time, in percent of
|
|
64
|
+
* one core). Called every second.
|
|
65
|
+
* @protected
|
|
66
|
+
*/
|
|
67
|
+
sampleCpuUsage() {
|
|
68
|
+
const now = process.hrtime.bigint();
|
|
69
|
+
const usage = process.cpuUsage();
|
|
70
|
+
const cpuMicroseconds = (usage.user - this.#previousUsage.user) + (usage.system - this.#previousUsage.system);
|
|
71
|
+
const elapsedMicroseconds = Number(now - this.#previousTime) / 1000;
|
|
72
|
+
this.#previousUsage = usage;
|
|
73
|
+
this.#previousTime = now;
|
|
74
|
+
if (elapsedMicroseconds > 0)
|
|
75
|
+
this.#samples.record(cpuMicroseconds / elapsedMicroseconds * 100);
|
|
76
|
+
}
|
|
77
|
+
async init() {
|
|
78
|
+
this.#previousUsage = process.cpuUsage();
|
|
79
|
+
this.#previousTime = process.hrtime.bigint();
|
|
80
|
+
this.#sampleInterval = setInterval(() => this.sampleCpuUsage(), this.#intervalDelay).unref();
|
|
81
|
+
}
|
|
82
|
+
async destroy() {
|
|
83
|
+
clearInterval(this.#sampleInterval);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Forget the samples: the statistics are 0 until the next sample, then cover the samples taken since. Use it to
|
|
87
|
+
* measure a period, such as between two reports.
|
|
88
|
+
*/
|
|
89
|
+
reset() {
|
|
90
|
+
this.#samples.reset();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
|
|
2
|
+
import { Component } from '@lakutata/core';
|
|
3
|
+
/**
|
|
4
|
+
* The event loop monitor: measures how late the event loop of the main thread runs its timers (the event loop delay,
|
|
5
|
+
* in milliseconds, with Node's `monitorEventLoopDelay` at a 1 ms resolution) and how busy it is (its utilization, in
|
|
6
|
+
* percent of the time). A growing delay means synchronous work (CPU-bound code, synchronous I/O, large JSON) blocks the
|
|
7
|
+
* other requests; a utilization near 100 means the loop has no idle time left.
|
|
8
|
+
*
|
|
9
|
+
* Both cover the time since the monitor was created or last {@link EventLoopMonitor.reset}: the delays are a
|
|
10
|
+
* histogram of every measure, the utilization is the share of that whole time the loop was busy. Declare it in the
|
|
11
|
+
* `components` of the application (`{class: EventLoopMonitor}`) and get it with `@Inject` or `getObject()`; it is a
|
|
12
|
+
* singleton. It starts measuring when it is created: list it in `bootstrap` to measure from the launch; it stops when
|
|
13
|
+
* the component is destroyed.
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import {Application} from 'lakutata'
|
|
17
|
+
* import {EventLoopMonitor, type IEventLoopMonitorStatistics} from 'lakutata/com/monitor'
|
|
18
|
+
*
|
|
19
|
+
* Application
|
|
20
|
+
* .run(() => ({
|
|
21
|
+
* id: 'loop.app',
|
|
22
|
+
* name: 'Loop',
|
|
23
|
+
* components: {
|
|
24
|
+
* eventLoop: {class: EventLoopMonitor}
|
|
25
|
+
* },
|
|
26
|
+
* bootstrap: ['eventLoop']
|
|
27
|
+
* }))
|
|
28
|
+
* .onLaunched(async (app: Application): Promise<void> => {
|
|
29
|
+
* const eventLoop: EventLoopMonitor = await app.getObject('eventLoop')
|
|
30
|
+
* setInterval((): void => {
|
|
31
|
+
* const statistics: IEventLoopMonitorStatistics = eventLoop.statistics
|
|
32
|
+
* if (statistics.p99 > 100) console.log(`The event loop lags: p99 ${statistics.p99} ms, ${statistics.utilRate}% busy`)
|
|
33
|
+
* //Each report covers the last minute
|
|
34
|
+
* eventLoop.reset()
|
|
35
|
+
* }, 60000)
|
|
36
|
+
* })
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class EventLoopMonitor extends Component {
|
|
40
|
+
constructor() {
|
|
41
|
+
super(...arguments);
|
|
42
|
+
/**
|
|
43
|
+
* The histogram of the event loop delays, in nanoseconds, sampled every millisecond while enabled (from the
|
|
44
|
+
* monitor's initialization to its destruction).
|
|
45
|
+
*/
|
|
46
|
+
this.histogram = monitorEventLoopDelay({ resolution: 1 });
|
|
47
|
+
/**
|
|
48
|
+
* The event loop utilization when the monitor was created or reset: the start of the period that
|
|
49
|
+
* {@link IEventLoopMonitorStatistics.utilRate} covers.
|
|
50
|
+
*/
|
|
51
|
+
this.initUtil = performance.eventLoopUtilization();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The event loop statistics since the monitor was created or reset, computed at each read: the minimum, maximum,
|
|
55
|
+
* mean, standard deviation and percentiles of the delays in milliseconds (not rounded; all 0 before the first
|
|
56
|
+
* measure), and the utilization in percent.
|
|
57
|
+
*/
|
|
58
|
+
get statistics() {
|
|
59
|
+
//Without sample yet, the histogram gives no meaningful value
|
|
60
|
+
const sampled = this.histogram.count > 0;
|
|
61
|
+
const delay = (nanoseconds) => sampled ? nanoseconds / 1e6 : 0;
|
|
62
|
+
return {
|
|
63
|
+
min: delay(this.histogram.min),
|
|
64
|
+
max: delay(this.histogram.max),
|
|
65
|
+
avg: delay(this.histogram.mean),
|
|
66
|
+
stdDev: delay(this.histogram.stddev),
|
|
67
|
+
p50: delay(this.histogram.percentile(50)),
|
|
68
|
+
p90: delay(this.histogram.percentile(90)),
|
|
69
|
+
p95: delay(this.histogram.percentile(95)),
|
|
70
|
+
p99: delay(this.histogram.percentile(99)),
|
|
71
|
+
utilRate: performance.eventLoopUtilization(this.initUtil).utilization * 100
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async init() {
|
|
75
|
+
this.histogram.enable();
|
|
76
|
+
}
|
|
77
|
+
async destroy() {
|
|
78
|
+
this.histogram.disable();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Forget the delays and restart the utilization: the statistics then cover the time since this call. Use it to
|
|
82
|
+
* measure a period, such as between two reports.
|
|
83
|
+
*/
|
|
84
|
+
reset() {
|
|
85
|
+
this.histogram.reset();
|
|
86
|
+
this.initUtil = performance.eventLoopUtilization();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { PerformanceObserver } from 'node:perf_hooks';
|
|
2
|
+
import { Component } from '@lakutata/core';
|
|
3
|
+
import { round, Samples } from './lib/Samples.js';
|
|
4
|
+
/**
|
|
5
|
+
* The HTTP request monitor: measures how long the HTTP requests received by the process take, in milliseconds, from
|
|
6
|
+
* their arrival to the end of their response, through the performance entries of `node:http` (so every server built on
|
|
7
|
+
* it is counted: the Express and Fastify adapters, and any other server of the process). The requests the process
|
|
8
|
+
* sends are not counted, nor are HTTP/2 requests.
|
|
9
|
+
*
|
|
10
|
+
* It keeps the count and the statistics of the durations since it was created or last {@link HttpRequestMonitor.reset},
|
|
11
|
+
* in constant memory. The entries reach it in batches, shortly after the responses end, so a request just answered may
|
|
12
|
+
* not be counted yet. Declare it in the `components` of the application (`{class: HttpRequestMonitor}`) and get it with
|
|
13
|
+
* `@Inject` or `getObject()`; it is a singleton. It only sees the requests ending after its creation: list it in
|
|
14
|
+
* `bootstrap` to measure from the launch; it stops when the component is destroyed.
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import {Application} from 'lakutata'
|
|
18
|
+
* import {HttpRequestMonitor, type IHttpRequestMonitorStatistics} from 'lakutata/com/monitor'
|
|
19
|
+
*
|
|
20
|
+
* Application
|
|
21
|
+
* .run(() => ({
|
|
22
|
+
* id: 'http.app',
|
|
23
|
+
* name: 'Http',
|
|
24
|
+
* components: {
|
|
25
|
+
* httpRequests: {class: HttpRequestMonitor}
|
|
26
|
+
* },
|
|
27
|
+
* bootstrap: ['httpRequests']
|
|
28
|
+
* }))
|
|
29
|
+
* .onLaunched(async (app: Application): Promise<void> => {
|
|
30
|
+
* const httpRequests: HttpRequestMonitor = await app.getObject('httpRequests')
|
|
31
|
+
* setInterval((): void => {
|
|
32
|
+
* const statistics: IHttpRequestMonitorStatistics = httpRequests.statistics
|
|
33
|
+
* console.log(`${statistics.count} requests, avg ${statistics.avg} ms, p99 ${statistics.p99} ms`)
|
|
34
|
+
* httpRequests.reset()
|
|
35
|
+
* }, 60000)
|
|
36
|
+
* })
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class HttpRequestMonitor extends Component {
|
|
40
|
+
constructor() {
|
|
41
|
+
super(...arguments);
|
|
42
|
+
this.#fractionDigits = 2;
|
|
43
|
+
this.#durations = new Samples(100);
|
|
44
|
+
/**
|
|
45
|
+
* The observer of the `http` performance entries, recording the duration of each received request (the
|
|
46
|
+
* `HttpRequest` entries); observing from the monitor's initialization to its destruction.
|
|
47
|
+
*/
|
|
48
|
+
this.observer = new PerformanceObserver((list) => {
|
|
49
|
+
//Every request of the batch, the received ones only (HttpClient entries are the requests sent)
|
|
50
|
+
for (const entry of list.getEntriesByName('HttpRequest'))
|
|
51
|
+
this.#durations.record(entry.duration);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
#fractionDigits;
|
|
55
|
+
#durations;
|
|
56
|
+
/**
|
|
57
|
+
* The statistics of the request durations since the monitor was created or reset, computed at each read: the
|
|
58
|
+
* number of requests, and the minimum, maximum, mean, standard deviation and percentiles of their durations in
|
|
59
|
+
* milliseconds, rounded to 2 decimals. The percentiles are approximate (3 significant digits). All 0 before the first
|
|
60
|
+
* request.
|
|
61
|
+
*/
|
|
62
|
+
get statistics() {
|
|
63
|
+
return {
|
|
64
|
+
count: this.#durations.count,
|
|
65
|
+
min: round(this.#durations.min, this.#fractionDigits),
|
|
66
|
+
max: round(this.#durations.max, this.#fractionDigits),
|
|
67
|
+
avg: round(this.#durations.mean, this.#fractionDigits),
|
|
68
|
+
stdDev: round(this.#durations.stdDev, this.#fractionDigits),
|
|
69
|
+
p50: round(this.#durations.percentile(50), this.#fractionDigits),
|
|
70
|
+
p90: round(this.#durations.percentile(90), this.#fractionDigits),
|
|
71
|
+
p95: round(this.#durations.percentile(95), this.#fractionDigits),
|
|
72
|
+
p99: round(this.#durations.percentile(99), this.#fractionDigits)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async init() {
|
|
76
|
+
this.observer.observe({ entryTypes: ['http'] });
|
|
77
|
+
}
|
|
78
|
+
async destroy() {
|
|
79
|
+
this.observer.disconnect();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Forget the requests: the count and the statistics restart from 0. Use it to measure a period, such as between
|
|
83
|
+
* two reports.
|
|
84
|
+
*/
|
|
85
|
+
reset() {
|
|
86
|
+
this.#durations.reset();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as os from 'node:os';
|
|
2
|
+
import { Component } from '@lakutata/core';
|
|
3
|
+
import { round, Samples } from './lib/Samples.js';
|
|
4
|
+
/**
|
|
5
|
+
* The memory monitor: measures the memory of the process, in bytes, with `process.memoryUsage()`: its resident set
|
|
6
|
+
* (the physical memory it occupies, `rss`), its V8 heap (`heapTotal` reserved, `heapUsed` occupied by the JavaScript
|
|
7
|
+
* objects) and its external memory (the C++ objects bound to JavaScript ones, the `Buffer`s included). It takes a
|
|
8
|
+
* sample when it is created then every second, and keeps the statistics of every sample since it was created or last
|
|
9
|
+
* {@link MemoryMonitor.reset}: the minimum, the maximum, the mean and percentiles, in constant memory.
|
|
10
|
+
*
|
|
11
|
+
* Declare it in the `components` of the application (`{class: MemoryMonitor}`) and get it with `@Inject` or
|
|
12
|
+
* `getObject()`; it is a singleton. It starts sampling when it is created: list it in `bootstrap` to measure from the
|
|
13
|
+
* launch. Its timer does not keep the process alive, and stops when the component is destroyed.
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import {Application} from 'lakutata'
|
|
17
|
+
* import {MemoryMonitor, type IMemoryMonitorStatistics} from 'lakutata/com/monitor'
|
|
18
|
+
*
|
|
19
|
+
* Application
|
|
20
|
+
* .run(() => ({
|
|
21
|
+
* id: 'memory.app',
|
|
22
|
+
* name: 'Memory',
|
|
23
|
+
* components: {
|
|
24
|
+
* memory: {class: MemoryMonitor}
|
|
25
|
+
* },
|
|
26
|
+
* bootstrap: ['memory']
|
|
27
|
+
* }))
|
|
28
|
+
* .onLaunched(async (app: Application): Promise<void> => {
|
|
29
|
+
* const memory: MemoryMonitor = await app.getObject('memory')
|
|
30
|
+
* setInterval((): void => {
|
|
31
|
+
* const statistics: IMemoryMonitorStatistics = memory.statistics
|
|
32
|
+
* const megabytes = (bytes: number): string => (bytes / 1048576).toFixed(1)
|
|
33
|
+
* console.log(`RSS ${megabytes(statistics.physicalUsed)} MB (${statistics.physicalUsage}% of the host), heap ${statistics.heapUsage}% used`)
|
|
34
|
+
* }, 60000)
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export class MemoryMonitor extends Component {
|
|
39
|
+
#fractionDigits = 2;
|
|
40
|
+
#intervalDelay = 1000;
|
|
41
|
+
#totalMemorySize = os.totalmem();
|
|
42
|
+
#physical = new Samples();
|
|
43
|
+
#heap = new Samples();
|
|
44
|
+
#external = new Samples();
|
|
45
|
+
#latest = process.memoryUsage();
|
|
46
|
+
#sampleInterval;
|
|
47
|
+
/**
|
|
48
|
+
* The memory statistics, computed at each read: the latest sample (`physicalUsed`, `heapTotal`, `heapUsed`,
|
|
49
|
+
* `externalUsed`, in bytes, and the percentages `physicalUsage` and `heapUsage`, rounded to 2 decimals), the host's
|
|
50
|
+
* total memory, and the minimum, maximum, mean (rounded to 2 decimals) and percentiles of the samples since the
|
|
51
|
+
* monitor was created or reset. The percentiles are approximate (3 significant digits). The statistics of the
|
|
52
|
+
* samples are 0 after a reset until the next sample.
|
|
53
|
+
*/
|
|
54
|
+
get statistics() {
|
|
55
|
+
const latest = this.#latest;
|
|
56
|
+
return {
|
|
57
|
+
physicalTotal: this.#totalMemorySize,
|
|
58
|
+
physicalUsed: latest.rss,
|
|
59
|
+
physicalUsage: round(latest.rss / this.#totalMemorySize * 100, this.#fractionDigits),
|
|
60
|
+
physicalUsedMin: this.#physical.min,
|
|
61
|
+
physicalUsedMax: this.#physical.max,
|
|
62
|
+
physicalUsedAvg: round(this.#physical.mean, this.#fractionDigits),
|
|
63
|
+
physicalUsedP50: this.#physical.percentile(50),
|
|
64
|
+
physicalUsedP90: this.#physical.percentile(90),
|
|
65
|
+
physicalUsedP95: this.#physical.percentile(95),
|
|
66
|
+
physicalUsedP99: this.#physical.percentile(99),
|
|
67
|
+
heapTotal: latest.heapTotal,
|
|
68
|
+
heapUsed: latest.heapUsed,
|
|
69
|
+
heapUsage: round(latest.heapTotal ? latest.heapUsed / latest.heapTotal * 100 : 0, this.#fractionDigits),
|
|
70
|
+
heapUsedMin: this.#heap.min,
|
|
71
|
+
heapUsedMax: this.#heap.max,
|
|
72
|
+
heapUsedAvg: round(this.#heap.mean, this.#fractionDigits),
|
|
73
|
+
heapUsedP50: this.#heap.percentile(50),
|
|
74
|
+
heapUsedP90: this.#heap.percentile(90),
|
|
75
|
+
heapUsedP95: this.#heap.percentile(95),
|
|
76
|
+
heapUsedP99: this.#heap.percentile(99),
|
|
77
|
+
externalUsed: latest.external,
|
|
78
|
+
externalUsedMin: this.#external.min,
|
|
79
|
+
externalUsedMax: this.#external.max,
|
|
80
|
+
externalUsedAvg: round(this.#external.mean, this.#fractionDigits),
|
|
81
|
+
externalUsedP50: this.#external.percentile(50),
|
|
82
|
+
externalUsedP90: this.#external.percentile(90),
|
|
83
|
+
externalUsedP95: this.#external.percentile(95),
|
|
84
|
+
externalUsedP99: this.#external.percentile(99)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Take a sample of the memory of the process: it becomes the latest usage and is added to the statistics. Called
|
|
89
|
+
* when the monitor is created, then every second.
|
|
90
|
+
* @protected
|
|
91
|
+
*/
|
|
92
|
+
sampleMemoryUsage() {
|
|
93
|
+
const usage = process.memoryUsage();
|
|
94
|
+
this.#latest = usage;
|
|
95
|
+
this.#physical.record(usage.rss);
|
|
96
|
+
this.#heap.record(usage.heapUsed);
|
|
97
|
+
this.#external.record(usage.external);
|
|
98
|
+
}
|
|
99
|
+
async init() {
|
|
100
|
+
this.sampleMemoryUsage();
|
|
101
|
+
this.#sampleInterval = setInterval(() => this.sampleMemoryUsage(), this.#intervalDelay).unref();
|
|
102
|
+
}
|
|
103
|
+
async destroy() {
|
|
104
|
+
clearInterval(this.#sampleInterval);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Forget the samples: the minimum, maximum, mean and percentiles are 0 until the next sample, then cover the samples
|
|
108
|
+
* taken since. The latest usage is kept.
|
|
109
|
+
*/
|
|
110
|
+
reset() {
|
|
111
|
+
this.#physical.reset();
|
|
112
|
+
this.#heap.reset();
|
|
113
|
+
this.#external.reset();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createHistogram } from 'node:perf_hooks';
|
|
2
|
+
/**
|
|
3
|
+
* The statistics of the samples of a measure, in a constant memory: the latest value, the minimum, the maximum, the
|
|
4
|
+
* mean and the standard deviation are exact, the percentiles come from a histogram (3 significant digits)
|
|
5
|
+
*/
|
|
6
|
+
export class Samples {
|
|
7
|
+
/**
|
|
8
|
+
* The histogram of the percentiles: it records integers from 1, so a value is recorded multiplied by the scale,
|
|
9
|
+
* plus 1 (a value may be 0)
|
|
10
|
+
*/
|
|
11
|
+
#histogram = createHistogram();
|
|
12
|
+
#count = 0;
|
|
13
|
+
#latest = 0;
|
|
14
|
+
#min = 0;
|
|
15
|
+
#max = 0;
|
|
16
|
+
#mean = 0;
|
|
17
|
+
//The sum of the squares of the differences from the mean (Welford's algorithm)
|
|
18
|
+
#m2 = 0;
|
|
19
|
+
/**
|
|
20
|
+
* @param scale the factor of the precision of the percentiles (100 keeps 2 decimals)
|
|
21
|
+
*/
|
|
22
|
+
constructor(scale = 1) {
|
|
23
|
+
this.scale = scale;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Record a sample (a negative value is recorded as 0)
|
|
27
|
+
* @param value
|
|
28
|
+
*/
|
|
29
|
+
record(value) {
|
|
30
|
+
const sample = Number.isFinite(value) && value > 0 ? value : 0;
|
|
31
|
+
this.#histogram.record(Math.round(sample * this.scale) + 1);
|
|
32
|
+
this.#count++;
|
|
33
|
+
this.#latest = sample;
|
|
34
|
+
if (this.#count === 1) {
|
|
35
|
+
this.#min = sample;
|
|
36
|
+
this.#max = sample;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
if (sample < this.#min)
|
|
40
|
+
this.#min = sample;
|
|
41
|
+
if (sample > this.#max)
|
|
42
|
+
this.#max = sample;
|
|
43
|
+
}
|
|
44
|
+
const delta = sample - this.#mean;
|
|
45
|
+
this.#mean += delta / this.#count;
|
|
46
|
+
this.#m2 += delta * (sample - this.#mean);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The number of samples
|
|
50
|
+
*/
|
|
51
|
+
get count() {
|
|
52
|
+
return this.#count;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The latest sample, 0 without sample
|
|
56
|
+
*/
|
|
57
|
+
get latest() {
|
|
58
|
+
return this.#latest;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The minimum, 0 without sample
|
|
62
|
+
*/
|
|
63
|
+
get min() {
|
|
64
|
+
return this.#min;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The maximum, 0 without sample
|
|
68
|
+
*/
|
|
69
|
+
get max() {
|
|
70
|
+
return this.#max;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The mean, 0 without sample
|
|
74
|
+
*/
|
|
75
|
+
get mean() {
|
|
76
|
+
return this.#mean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The (population) standard deviation, 0 without sample
|
|
80
|
+
*/
|
|
81
|
+
get stdDev() {
|
|
82
|
+
return this.#count ? Math.sqrt(this.#m2 / this.#count) : 0;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A percentile, 0 without sample
|
|
86
|
+
* @param percentile from 0 to 100
|
|
87
|
+
*/
|
|
88
|
+
percentile(percentile) {
|
|
89
|
+
if (!this.#count)
|
|
90
|
+
return 0;
|
|
91
|
+
return Math.min(Math.max((this.#histogram.percentile(percentile) - 1) / this.scale, this.#min), this.#max);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Forget the samples
|
|
95
|
+
*/
|
|
96
|
+
reset() {
|
|
97
|
+
this.#histogram.reset();
|
|
98
|
+
this.#count = 0;
|
|
99
|
+
this.#latest = 0;
|
|
100
|
+
this.#min = 0;
|
|
101
|
+
this.#max = 0;
|
|
102
|
+
this.#mean = 0;
|
|
103
|
+
this.#m2 = 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Round a number to a number of decimals
|
|
108
|
+
* @param value
|
|
109
|
+
* @param fractionDigits
|
|
110
|
+
* @constructor
|
|
111
|
+
*/
|
|
112
|
+
export function round(value, fractionDigits) {
|
|
113
|
+
return parseFloat(value.toFixed(fractionDigits));
|
|
114
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from '../monitor/CpuMonitor.js';
|
|
2
|
+
export * from '../monitor/EventLoopMonitor.js';
|
|
3
|
+
export * from '../monitor/HttpRequestMonitor.js';
|
|
4
|
+
export * from '../monitor/MemoryMonitor.js';
|
|
5
|
+
export * from '../monitor/interfaces/IMonitor.js';
|
|
6
|
+
export * from '../monitor/interfaces/ICpuMonitorStatistics.js';
|
|
7
|
+
export * from '../monitor/interfaces/IEventLoopMonitorStatistics.js';
|
|
8
|
+
export * from '../monitor/interfaces/IHttpRequestMonitorStatistics.js';
|
|
9
|
+
export * from '../monitor/interfaces/IMemoryMonitorStatistics.js';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Component } from '@lakutata/core';
|
|
2
|
+
import { IMonitor } from './interfaces/IMonitor.js';
|
|
3
|
+
import { ICpuMonitorStatistics } from './interfaces/ICpuMonitorStatistics.js';
|
|
4
|
+
/**
|
|
5
|
+
* The CPU monitor: measures the CPU time the process uses (user plus system, all its threads), as a percentage of one
|
|
6
|
+
* core: 100 is one core fully busy, and a process using several cores (worker threads, the libuv thread pool) goes
|
|
7
|
+
* above 100. It takes a sample every second, each the average usage over that second, and keeps the statistics of
|
|
8
|
+
* every sample since it was created or last {@link CpuMonitor.reset}: the latest, the minimum, the maximum, the mean
|
|
9
|
+
* and percentiles, in constant memory.
|
|
10
|
+
*
|
|
11
|
+
* Declare it in the `components` of the application (`{class: CpuMonitor}`) and get it with `@Inject` or
|
|
12
|
+
* `getObject()`; it is a singleton. It starts sampling when it is created: list it in `bootstrap` to measure from the
|
|
13
|
+
* launch. The statistics are all 0 until the first sample, one second after its creation. Its timer does not keep the
|
|
14
|
+
* process alive, and stops when the component is destroyed.
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import {Application} from 'lakutata'
|
|
18
|
+
* import {CpuMonitor, type ICpuMonitorStatistics} from 'lakutata/com/monitor'
|
|
19
|
+
*
|
|
20
|
+
* Application
|
|
21
|
+
* .run(() => ({
|
|
22
|
+
* id: 'cpu.app',
|
|
23
|
+
* name: 'Cpu',
|
|
24
|
+
* components: {
|
|
25
|
+
* cpu: {class: CpuMonitor}
|
|
26
|
+
* },
|
|
27
|
+
* //Created at the launch, so that it samples from the start
|
|
28
|
+
* bootstrap: ['cpu']
|
|
29
|
+
* }))
|
|
30
|
+
* .onLaunched(async (app: Application): Promise<void> => {
|
|
31
|
+
* const cpu: CpuMonitor = await app.getObject('cpu')
|
|
32
|
+
* setInterval((): void => {
|
|
33
|
+
* const statistics: ICpuMonitorStatistics = cpu.statistics
|
|
34
|
+
* console.log(`CPU ${statistics.usage}% (p95 ${statistics.usageP95}%, max ${statistics.usageMax}%)`)
|
|
35
|
+
* }, 60000)
|
|
36
|
+
* })
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare class CpuMonitor extends Component implements IMonitor<ICpuMonitorStatistics> {
|
|
40
|
+
#private;
|
|
41
|
+
/**
|
|
42
|
+
* The statistics of the CPU samples since the monitor was created or reset, computed at each read: the latest
|
|
43
|
+
* usage and the minimum, maximum, mean and percentiles (P50 to P99) of the samples, in percent of one core, rounded
|
|
44
|
+
* to 5 decimals. The percentiles are approximate (3 significant digits). All 0 before the first sample.
|
|
45
|
+
*/
|
|
46
|
+
get statistics(): ICpuMonitorStatistics;
|
|
47
|
+
/**
|
|
48
|
+
* Take a sample: the CPU usage since the previous sample (user plus system time over the elapsed time, in percent of
|
|
49
|
+
* one core). Called every second.
|
|
50
|
+
* @protected
|
|
51
|
+
*/
|
|
52
|
+
protected sampleCpuUsage(): void;
|
|
53
|
+
protected init(): Promise<void>;
|
|
54
|
+
protected destroy(): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Forget the samples: the statistics are 0 until the next sample, then cover the samples taken since. Use it to
|
|
57
|
+
* measure a period, such as between two reports.
|
|
58
|
+
*/
|
|
59
|
+
reset(): void;
|
|
60
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type EventLoopUtilization, type IntervalHistogram } from 'node:perf_hooks';
|
|
2
|
+
import { Component } from '@lakutata/core';
|
|
3
|
+
import { IEventLoopMonitorStatistics } from './interfaces/IEventLoopMonitorStatistics.js';
|
|
4
|
+
import { IMonitor } from './interfaces/IMonitor.js';
|
|
5
|
+
/**
|
|
6
|
+
* The event loop monitor: measures how late the event loop of the main thread runs its timers (the event loop delay,
|
|
7
|
+
* in milliseconds, with Node's `monitorEventLoopDelay` at a 1 ms resolution) and how busy it is (its utilization, in
|
|
8
|
+
* percent of the time). A growing delay means synchronous work (CPU-bound code, synchronous I/O, large JSON) blocks the
|
|
9
|
+
* other requests; a utilization near 100 means the loop has no idle time left.
|
|
10
|
+
*
|
|
11
|
+
* Both cover the time since the monitor was created or last {@link EventLoopMonitor.reset}: the delays are a
|
|
12
|
+
* histogram of every measure, the utilization is the share of that whole time the loop was busy. Declare it in the
|
|
13
|
+
* `components` of the application (`{class: EventLoopMonitor}`) and get it with `@Inject` or `getObject()`; it is a
|
|
14
|
+
* singleton. It starts measuring when it is created: list it in `bootstrap` to measure from the launch; it stops when
|
|
15
|
+
* the component is destroyed.
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import {Application} from 'lakutata'
|
|
19
|
+
* import {EventLoopMonitor, type IEventLoopMonitorStatistics} from 'lakutata/com/monitor'
|
|
20
|
+
*
|
|
21
|
+
* Application
|
|
22
|
+
* .run(() => ({
|
|
23
|
+
* id: 'loop.app',
|
|
24
|
+
* name: 'Loop',
|
|
25
|
+
* components: {
|
|
26
|
+
* eventLoop: {class: EventLoopMonitor}
|
|
27
|
+
* },
|
|
28
|
+
* bootstrap: ['eventLoop']
|
|
29
|
+
* }))
|
|
30
|
+
* .onLaunched(async (app: Application): Promise<void> => {
|
|
31
|
+
* const eventLoop: EventLoopMonitor = await app.getObject('eventLoop')
|
|
32
|
+
* setInterval((): void => {
|
|
33
|
+
* const statistics: IEventLoopMonitorStatistics = eventLoop.statistics
|
|
34
|
+
* if (statistics.p99 > 100) console.log(`The event loop lags: p99 ${statistics.p99} ms, ${statistics.utilRate}% busy`)
|
|
35
|
+
* //Each report covers the last minute
|
|
36
|
+
* eventLoop.reset()
|
|
37
|
+
* }, 60000)
|
|
38
|
+
* })
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare class EventLoopMonitor extends Component implements IMonitor<IEventLoopMonitorStatistics> {
|
|
42
|
+
/**
|
|
43
|
+
* The histogram of the event loop delays, in nanoseconds, sampled every millisecond while enabled (from the
|
|
44
|
+
* monitor's initialization to its destruction).
|
|
45
|
+
*/
|
|
46
|
+
protected readonly histogram: IntervalHistogram;
|
|
47
|
+
/**
|
|
48
|
+
* The event loop utilization when the monitor was created or reset: the start of the period that
|
|
49
|
+
* {@link IEventLoopMonitorStatistics.utilRate} covers.
|
|
50
|
+
*/
|
|
51
|
+
protected initUtil: EventLoopUtilization;
|
|
52
|
+
/**
|
|
53
|
+
* The event loop statistics since the monitor was created or reset, computed at each read: the minimum, maximum,
|
|
54
|
+
* mean, standard deviation and percentiles of the delays in milliseconds (not rounded; all 0 before the first
|
|
55
|
+
* measure), and the utilization in percent.
|
|
56
|
+
*/
|
|
57
|
+
get statistics(): IEventLoopMonitorStatistics;
|
|
58
|
+
protected init(): Promise<void>;
|
|
59
|
+
protected destroy(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Forget the delays and restart the utilization: the statistics then cover the time since this call. Use it to
|
|
62
|
+
* measure a period, such as between two reports.
|
|
63
|
+
*/
|
|
64
|
+
reset(): void;
|
|
65
|
+
}
|