UNPKG

8.6 kBJavaScriptView Raw
1'use strict'
2
3const events = require('events')
4const fs = require('fs')
5const os = require('os')
6const path = require('path')
7const pump = require('pump')
8const pumpify = require('pumpify')
9const stream = require('./lib/destroyable-stream')
10const { spawn } = require('child_process')
11const Analysis = require('./analysis/index.js')
12const Stringify = require('streaming-json-stringify')
13const browserify = require('browserify')
14const streamTemplate = require('stream-template')
15const joinTrace = require('node-trace-log-join')
16const getLoggingPaths = require('@nearform/clinic-common').getLoggingPaths('doctor')
17const SystemInfoDecoder = require('./format/system-info-decoder.js')
18const TraceEventDecoder = require('./format/trace-event-decoder.js')
19const ProcessStatDecoder = require('./format/process-stat-decoder.js')
20const RenderRecommendations = require('./recommendations/index.js')
21const minifyStream = require('minify-stream')
22const v8 = require('v8')
23const HEAP_MAX = v8.getHeapStatistics().heap_size_limit
24
25class ClinicDoctor extends events.EventEmitter {
26 constructor (settings = {}) {
27 super()
28
29 // define default parameters
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 // run program, but inject the sampler
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 // use NODE_PATH to work around issues with spaces in inject path
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 // get logging directory structure
81 const options = { identifier: proc.pid, path: this.path }
82 const paths = getLoggingPaths(options)
83 // relay SIGINT to process
84 process.once('SIGINT', function () {
85 // we cannot kill(SIGINT) on windows but it seems
86 // to relay the ctrl-c signal per default, so only do this
87 // if not windows
88 /* istanbul ignore next */
89 if (os.platform() !== 'win32') proc.kill('SIGINT')
90 })
91
92 proc.once('exit', (code, signal) => {
93 // Windows exit code STATUS_CONTROL_C_EXIT 0xC000013A returns 3221225786
94 // if not caught. See https://msdn.microsoft.com/en-us/library/cc704588.aspx
95 /* istanbul ignore next */
96 if (code === 3221225786 && os.platform() === 'win32') signal = 'SIGINT'
97
98 // report if the process did not exit normally.
99 if (code !== 0 && signal !== 'SIGINT') {
100 /* istanbul ignore next */
101 if (code !== null) {
102 console.error(`process exited with exit code ${code}`)
103 } else {
104 /* istanbul ignore next */
105 return callback(
106 new Error(`process exited by signal ${signal}`),
107 paths['/']
108 )
109 }
110 }
111
112 this.emit('analysing')
113
114 // move trace_event file to logging directory
115 joinTrace(
116 'node_trace.*.log', paths['/traceevent'],
117 function (err) {
118 /* istanbul ignore if: the node_trace file should always exists */
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 // Load data
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 // create analysis
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 // render recommendations as HTML templates
201 const recommendations = new RenderRecommendations()
202
203 // open logo
204 const logoFile = fs.createReadStream(logoPath)
205 const nearFormLogoFile = fs.createReadStream(nearFormLogoPath)
206 const clinicFaviconBase64 = fs.createReadStream(clinicFaviconPath)
207
208 // create script-file stream
209 const b = browserify({
210 'basedir': __dirname,
211 // 'debug': true,
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 // create style-file stream
226 const styleFile = fs.createReadStream(stylePath)
227
228 // forward dataFile errors to the scriptFile explicitly
229 // we cannot use destroy until nodejs/node#18172 and nodejs/node#18171 are fixed
230 dataFile.on('error', (err) => scriptFile.emit('error', err))
231
232 // build output file
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
276module.exports = ClinicDoctor