-
Notifications
You must be signed in to change notification settings - Fork 224
/
stats.js
213 lines (177 loc) · 6.24 KB
/
stats.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
/*
* Copyright Elasticsearch B.V. and other contributors where applicable.
* Licensed under the BSD 2-Clause License; you may not use this file except in
* compliance with the BSD 2-Clause License.
*/
'use strict';
const fs = require('fs');
const afterAll = require('after-all-results');
const whitespace = /\s+/;
class Stats {
constructor(opts) {
opts = opts || {};
this.files = {
processFile: opts.processFile || '/proc/self/stat',
memoryFile: opts.memoryFile || '/proc/meminfo',
cpuFile: opts.cpuFile || '/proc/stat',
};
this.previous = {
cpuTotal: 0,
cpuUsage: 0,
memTotal: 0,
memAvailable: 0,
utime: 0,
stime: 0,
vsize: 0,
rss: 0,
};
this.stats = {
'system.cpu.total.norm.pct': 0,
'system.memory.actual.free': 0,
'system.memory.total': 0,
'system.process.cpu.total.norm.pct': 0,
'system.process.cpu.system.norm.pct': 0,
'system.process.cpu.user.norm.pct': 0,
'system.process.memory.size': 0,
'system.process.memory.rss.bytes': 0,
};
this.inProgress = false;
this.timer = null;
// Do initial load
const files = [
this.files.processFile,
this.files.memoryFile,
this.files.cpuFile,
];
try {
const datas = files.map(readFileSync);
this.previous = this.readStats(datas);
this.update(datas);
} catch (err) {}
}
toJSON() {
return this.stats;
}
collect(cb) {
if (this.inProgress) {
if (cb) process.nextTick(cb);
return;
}
this.inProgress = true;
const files = [
this.files.processFile,
this.files.memoryFile,
this.files.cpuFile,
];
const next = afterAll((err, files) => {
if (!err) this.update(files);
if (cb) cb();
});
for (const file of files) {
fs.readFile(file, next());
}
}
readStats([processFile, memoryFile, cpuFile]) {
// CPU data
//
// Example of line we're trying to parse:
// cpu 13978 30 2511 9257 2248 0 102 0 0 0
const cpuLine = firstLineOfBufferAsString(cpuFile);
const cpuTimes = cpuLine.split(whitespace);
let cpuTotal = 0;
for (let i = 1; i < cpuTimes.length; i++) {
cpuTotal += Number(cpuTimes[i]);
}
// We're off-by-one in relation to the expected index, because we include
// the `cpu` label at the beginning of the line
const idle = Number(cpuTimes[4]);
const iowait = Number(cpuTimes[5]);
const cpuUsage = cpuTotal - idle - iowait;
// Memory data
let memAvailable = 0;
let memTotal = 0;
let matches = 0;
for (const line of memoryFile.toString().split('\n')) {
if (/^MemAvailable:/.test(line)) {
memAvailable = parseInt(line.split(whitespace)[1], 10) * 1024;
matches++;
} else if (/^MemTotal:/.test(line)) {
memTotal = parseInt(line.split(whitespace)[1], 10) * 1024;
matches++;
}
if (matches === 2) break;
}
// Process data
//
// Example of line we're trying to parse:
//
// 44 (node /app/node_) R 1 44 44 0 -1 4210688 7948 0 0 0 109 21 0 0 20 0 10 0 133652 954462208 12906 18446744073709551615 4194304 32940036 140735797366336 0 0 0 0 4096 16898 0 0 0 17 0 0 0 0 0 0 35037200 35143856 41115648 140735797369050 140735797369131 140735797369131 140735797370852 0
//
// We can't just split on whitespace as the 2nd field might contain
// whitespace. However, the parentheses will always be there, so we can
// ignore everything from before the `)` to get rid of the whitespace
// problem.
//
// For details about each field, see:
// http://man7.org/linux/man-pages/man5/proc.5.html
const processLine = firstLineOfBufferAsString(processFile);
const processData = processLine
.slice(processLine.lastIndexOf(')'))
.split(whitespace);
// all fields are referenced by their index, but are off by one because
// we're dropping the first field from the line due to the whitespace
// problem described above
const utime = parseInt(processData[12], 10); // position in file: 14
const stime = parseInt(processData[13], 10); // position in file: 15
const vsize = parseInt(processData[21], 10); // position in file: 23
return {
cpuUsage,
cpuTotal,
memTotal,
memAvailable,
utime,
stime,
vsize,
rss: process.memoryUsage().rss, // TODO: Calculate using field 24 (rss) * PAGE_SIZE
};
}
update(files) {
const prev = this.previous;
const next = this.readStats(files);
const stats = this.stats;
const cpuTotal = next.cpuTotal - prev.cpuTotal;
const cpuUsage = next.cpuUsage - prev.cpuUsage;
const utime = next.utime - prev.utime;
const stime = next.stime - prev.stime;
stats['system.cpu.total.norm.pct'] = cpuUsage / cpuTotal || 0;
stats['system.memory.actual.free'] = next.memAvailable;
stats['system.memory.total'] = next.memTotal;
// We use Math.min to guard against an edge case where /proc/self/stat
// reported more clock ticks than /proc/stat, in which case it looks like
// the process spent more CPU time than was used by the system. In that
// case we just assume it was a 100% CPU.
//
// This might happen because we don't read the process file at the same
// time as the system file. In between the two reads, the process will
// spend some time on the CPU and hence the two reads are not 100% synced
// up.
const cpuProcessPercent = Math.min((utime + stime) / cpuTotal || 0, 1);
const cpuProcessUserPercent = Math.min(utime / cpuTotal || 0, 1);
const cpuProcessSystemPercent = Math.min(stime / cpuTotal || 0, 1);
stats['system.process.cpu.total.norm.pct'] = cpuProcessPercent;
stats['system.process.cpu.user.norm.pct'] = cpuProcessUserPercent;
stats['system.process.cpu.system.norm.pct'] = cpuProcessSystemPercent;
stats['system.process.memory.size'] = next.vsize;
stats['system.process.memory.rss.bytes'] = next.rss;
this.previous = next;
this.inProgress = false;
}
}
function firstLineOfBufferAsString(buff) {
const newline = buff.indexOf('\n');
return buff.toString('utf8', 0, newline === -1 ? buff.length : newline);
}
function readFileSync(file) {
return fs.readFileSync(file);
}
module.exports = Stats;