1 | 'use strict'
|
2 |
|
3 | const events = require('events')
|
4 | const fs = require('fs')
|
5 | const os = require('os')
|
6 | const path = require('path')
|
7 | const pump = require('pump')
|
8 | const pumpify = require('pumpify')
|
9 | const stream = require('./lib/destroyable-stream')
|
10 | const { spawn } = require('child_process')
|
11 | const Analysis = require('./analysis/index.js')
|
12 | const Stringify = require('streaming-json-stringify')
|
13 | const browserify = require('browserify')
|
14 | const streamTemplate = require('stream-template')
|
15 | const joinTrace = require('node-trace-log-join')
|
16 | const getLoggingPaths = require('@nearform/clinic-common').getLoggingPaths('doctor')
|
17 | const SystemInfoDecoder = require('./format/system-info-decoder.js')
|
18 | const TraceEventDecoder = require('./format/trace-event-decoder.js')
|
19 | const ProcessStatDecoder = require('./format/process-stat-decoder.js')
|
20 | const RenderRecommendations = require('./recommendations/index.js')
|
21 | const minifyStream = require('minify-stream')
|
22 | const v8 = require('v8')
|
23 | const HEAP_MAX = v8.getHeapStatistics().heap_size_limit
|
24 |
|
25 | class ClinicDoctor extends events.EventEmitter {
|
26 | constructor (settings = {}) {
|
27 | super()
|
28 |
|
29 |
|
30 | const {
|
31 | sampleInterval = 10,
|
32 | detectPort = false,
|
33 | debug = false,
|
34 | dest = null
|
35 | } = settings
|
36 |
|
37 | this.sampleInterval = sampleInterval
|
38 | this.detectPort = detectPort
|
39 | this.debug = debug
|
40 | this.path = dest
|
41 | }
|
42 |
|
43 | collect (args, callback) {
|
44 |
|
45 | const logArgs = [
|
46 | '-r', 'no-cluster.js',
|
47 | '-r', 'sampler.js',
|
48 | '--trace-events-enabled', '--trace-event-categories', 'v8'
|
49 | ]
|
50 |
|
51 | const stdio = ['inherit', 'inherit', 'inherit']
|
52 |
|
53 | if (this.detectPort) {
|
54 | logArgs.push('-r', 'detect-port.js')
|
55 | stdio.push('pipe')
|
56 | }
|
57 |
|
58 | const customEnv = {
|
59 |
|
60 | NODE_PATH: path.join(__dirname, 'injects'),
|
61 | NODE_OPTIONS: logArgs.join(' ') + (
|
62 | process.env.NODE_OPTIONS ? ' ' + process.env.NODE_OPTIONS : ''
|
63 | ),
|
64 | NODE_CLINIC_DOCTOR_SAMPLE_INTERVAL: this.sampleInterval
|
65 | }
|
66 |
|
67 | if (this.path) {
|
68 | customEnv.NODE_CLINIC_DOCTOR_DATA_PATH = this.path
|
69 | }
|
70 |
|
71 | const proc = spawn(args[0], args.slice(1), {
|
72 | stdio,
|
73 | env: Object.assign({}, process.env, customEnv)
|
74 | })
|
75 |
|
76 | if (this.detectPort) {
|
77 | proc.stdio[3].once('data', data => this.emit('port', Number(data), proc, () => proc.stdio[3].destroy()))
|
78 | }
|
79 |
|
80 |
|
81 | const options = { identifier: proc.pid, path: this.path }
|
82 | const paths = getLoggingPaths(options)
|
83 |
|
84 | process.once('SIGINT', function () {
|
85 |
|
86 |
|
87 |
|
88 |
|
89 | if (os.platform() !== 'win32') proc.kill('SIGINT')
|
90 | })
|
91 |
|
92 | proc.once('exit', (code, signal) => {
|
93 |
|
94 |
|
95 |
|
96 | if (code === 3221225786 && os.platform() === 'win32') signal = 'SIGINT'
|
97 |
|
98 |
|
99 | if (code !== 0 && signal !== 'SIGINT') {
|
100 |
|
101 | if (code !== null) {
|
102 | console.error(`process exited with exit code ${code}`)
|
103 | } else {
|
104 |
|
105 | return callback(
|
106 | new Error(`process exited by signal ${signal}`),
|
107 | paths['/']
|
108 | )
|
109 | }
|
110 | }
|
111 |
|
112 | this.emit('analysing')
|
113 |
|
114 |
|
115 | joinTrace(
|
116 | 'node_trace.*.log', paths['/traceevent'],
|
117 | function (err) {
|
118 |
|
119 | if (err) return callback(err, paths['/'])
|
120 | callback(null, paths['/'])
|
121 | }
|
122 | )
|
123 | })
|
124 | }
|
125 |
|
126 | visualize (dataDirname, outputFilename, callback) {
|
127 | const fakeDataPath = path.join(__dirname, 'visualizer', 'data.json')
|
128 | const stylePath = path.join(__dirname, 'visualizer', 'style.css')
|
129 | const scriptPath = path.join(__dirname, 'visualizer', 'main.js')
|
130 | const logoPath = path.join(__dirname, 'visualizer', 'app-logo.svg')
|
131 | const nearFormLogoPath = path.join(__dirname, 'visualizer', 'nearform-logo.svg')
|
132 | const clinicFaviconPath = path.join(__dirname, 'visualizer', 'clinic-favicon.png.b64')
|
133 |
|
134 |
|
135 | const paths = getLoggingPaths({ path: dataDirname })
|
136 |
|
137 | const systemInfoReader = pumpify.obj(
|
138 | fs.createReadStream(paths['/systeminfo']),
|
139 | new SystemInfoDecoder()
|
140 | )
|
141 | const traceEventReader = pumpify.obj(
|
142 | fs.createReadStream(paths['/traceevent']),
|
143 | new TraceEventDecoder(systemInfoReader)
|
144 | )
|
145 | const processStatReader = pumpify.obj(
|
146 | fs.createReadStream(paths['/processstat']),
|
147 | new ProcessStatDecoder()
|
148 | )
|
149 |
|
150 |
|
151 | const analysisStringified = pumpify(
|
152 | new Analysis(traceEventReader, processStatReader),
|
153 | new stream.Transform({
|
154 | readableObjectMode: false,
|
155 | writableObjectMode: true,
|
156 | transform (data, encoding, callback) {
|
157 | callback(null, JSON.stringify(data))
|
158 | }
|
159 | })
|
160 | )
|
161 |
|
162 | const traceEventStringify = pumpify(
|
163 | traceEventReader,
|
164 | new Stringify({
|
165 | seperator: ',\n',
|
166 | stringifier: JSON.stringify
|
167 | })
|
168 | )
|
169 |
|
170 | const processStatStringify = pumpify(
|
171 | processStatReader,
|
172 | new Stringify({
|
173 | seperator: ',\n',
|
174 | stringifier: JSON.stringify
|
175 | })
|
176 | )
|
177 |
|
178 | const hasFreeMemory = () => {
|
179 | const used = process.memoryUsage().heapTotal / HEAP_MAX
|
180 | if (used > 0.5) {
|
181 | systemInfoReader.destroy()
|
182 | traceEventReader.destroy()
|
183 | processStatReader.destroy()
|
184 | analysisStringified.destroy()
|
185 | this.emit('truncate')
|
186 | this.emit('warning', 'Truncating input data due to memory constrains')
|
187 | }
|
188 | }
|
189 |
|
190 | const checkHeapInterval = setInterval(hasFreeMemory, 50)
|
191 |
|
192 | const dataFile = streamTemplate`
|
193 | {
|
194 | "traceEvent": ${traceEventStringify},
|
195 | "processStat": ${processStatStringify},
|
196 | "analysis": ${analysisStringified}
|
197 | }
|
198 | `
|
199 |
|
200 |
|
201 | const recommendations = new RenderRecommendations()
|
202 |
|
203 |
|
204 | const logoFile = fs.createReadStream(logoPath)
|
205 | const nearFormLogoFile = fs.createReadStream(nearFormLogoPath)
|
206 | const clinicFaviconBase64 = fs.createReadStream(clinicFaviconPath)
|
207 |
|
208 |
|
209 | const b = browserify({
|
210 | 'basedir': __dirname,
|
211 |
|
212 | 'noParse': [fakeDataPath]
|
213 | })
|
214 | b.require(dataFile, {
|
215 | 'file': fakeDataPath
|
216 | })
|
217 | b.add(scriptPath)
|
218 | b.transform('brfs')
|
219 | let scriptFile = b.bundle()
|
220 |
|
221 | if (!this.debug) {
|
222 | scriptFile = scriptFile.pipe(minifyStream({ sourceMap: false, mangle: false }))
|
223 | }
|
224 |
|
225 |
|
226 | const styleFile = fs.createReadStream(stylePath)
|
227 |
|
228 |
|
229 |
|
230 | dataFile.on('error', (err) => scriptFile.emit('error', err))
|
231 |
|
232 |
|
233 | const outputFile = streamTemplate`
|
234 | <!DOCTYPE html>
|
235 | <html lang="en" class="grid-layout">
|
236 | <meta charset="utf8">
|
237 | <meta name="viewport" content="width=device-width, initial-scale=1">
|
238 | <link rel="shortcut icon" type="image/png" href="${clinicFaviconBase64}">
|
239 | <title>Clinic Doctor</title>
|
240 |
|
241 | <style>${styleFile}</style>
|
242 |
|
243 | <div id="banner">
|
244 | <a id="main-logo" href="https://github.com/nearform/node-clinic-doctor" title="Clinic Doctor on GitHub" target="_blank">
|
245 | ${logoFile} <span>Doctor</span>
|
246 | </a>
|
247 | <a id="company-logo" href="https://nearform.com" title="nearForm" target="_blank">
|
248 | ${nearFormLogoFile}
|
249 | </a>
|
250 | </div>
|
251 | <div id="front-matter">
|
252 | <div id="alert"></div>
|
253 | <div id="menu"></div>
|
254 | </div>
|
255 | <div id="graph"></div>
|
256 | <div id="recommendation-space"></div>
|
257 | <div id="recommendation"></div>
|
258 |
|
259 | ${recommendations}
|
260 |
|
261 | <script>${scriptFile}</script>
|
262 | </html>
|
263 | `
|
264 |
|
265 | pump(
|
266 | outputFile,
|
267 | fs.createWriteStream(outputFilename),
|
268 | function (err) {
|
269 | clearInterval(checkHeapInterval)
|
270 | callback(err)
|
271 | }
|
272 | )
|
273 | }
|
274 | }
|
275 |
|
276 | module.exports = ClinicDoctor
|