UNPKG

8.74 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 streamTemplate = require('stream-template')
14const joinTrace = require('node-trace-log-join')
15const getLoggingPaths = require('@nearform/clinic-common').getLoggingPaths('doctor')
16const SystemInfoDecoder = require('./format/system-info-decoder.js')
17const TraceEventDecoder = require('./format/trace-event-decoder.js')
18const ProcessStatDecoder = require('./format/process-stat-decoder.js')
19const RenderRecommendations = require('./recommendations/index.js')
20const minifyStream = require('minify-stream')
21const v8 = require('v8')
22const HEAP_MAX = v8.getHeapStatistics().heap_size_limit
23const buildJs = require('@nearform/clinic-common/scripts/build-js')
24const buildCss = require('@nearform/clinic-common/scripts/build-css')
25const mainTemplate = require('@nearform/clinic-common/templates/main')
26
27class ClinicDoctor extends events.EventEmitter {
28 constructor (settings = {}) {
29 super()
30
31 // define default parameters
32 const {
33 sampleInterval = 10,
34 detectPort = false,
35 debug = false,
36 dest = null
37 } = settings
38
39 this.sampleInterval = sampleInterval
40 this.detectPort = detectPort
41 this.debug = debug
42 this.path = dest
43 }
44
45 collect (args, callback) {
46 // run program, but inject the sampler
47 const logArgs = [
48 '-r', 'no-cluster.js',
49 '-r', 'sampler.js',
50 '--trace-events-enabled', '--trace-event-categories', 'v8'
51 ]
52
53 const stdio = ['inherit', 'inherit', 'inherit']
54
55 if (this.detectPort) {
56 logArgs.push('-r', 'detect-port.js')
57 stdio.push('pipe')
58 }
59
60 let NODE_PATH = path.join(__dirname, 'injects')
61 // use NODE_PATH to work around issues with spaces in inject path
62 if (process.env.NODE_PATH) {
63 NODE_PATH += `${process.platform === 'win32' ? ';' : ':'}${process.env.NODE_PATH}`
64 }
65
66 const customEnv = {
67 // use NODE_PATH to work around issues with spaces in inject path
68 NODE_PATH,
69 NODE_OPTIONS: logArgs.join(' ') + (
70 process.env.NODE_OPTIONS ? ' ' + process.env.NODE_OPTIONS : ''
71 ),
72 NODE_CLINIC_DOCTOR_SAMPLE_INTERVAL: this.sampleInterval
73 }
74
75 if (this.path) {
76 customEnv.NODE_CLINIC_DOCTOR_DATA_PATH = this.path
77 }
78
79 const proc = spawn(args[0], args.slice(1), {
80 stdio,
81 env: Object.assign({}, process.env, customEnv)
82 })
83
84 if (this.detectPort) {
85 proc.stdio[3].once('data', data => this.emit('port', Number(data), proc, () => proc.stdio[3].destroy()))
86 }
87
88 // get logging directory structure
89 const options = { identifier: proc.pid, path: this.path }
90 const paths = getLoggingPaths(options)
91 // relay SIGINT to process
92 process.once('SIGINT', function () {
93 // we cannot kill(SIGINT) on windows but it seems
94 // to relay the ctrl-c signal per default, so only do this
95 // if not windows
96 /* istanbul ignore next */
97 if (os.platform() !== 'win32') proc.kill('SIGINT')
98 })
99
100 proc.once('exit', (code, signal) => {
101 // Windows exit code STATUS_CONTROL_C_EXIT 0xC000013A returns 3221225786
102 // if not caught. See https://msdn.microsoft.com/en-us/library/cc704588.aspx
103 /* istanbul ignore next */
104 if (code === 3221225786 && os.platform() === 'win32') signal = 'SIGINT'
105
106 // report if the process did not exit normally.
107 if (code !== 0 && signal !== 'SIGINT') {
108 /* istanbul ignore next */
109 if (code !== null) {
110 console.error(`process exited with exit code ${code}`)
111 } else {
112 /* istanbul ignore next */
113 return callback(
114 new Error(`process exited by signal ${signal}`),
115 paths['/']
116 )
117 }
118 }
119
120 this.emit('analysing')
121
122 // move trace_event file to logging directory
123 joinTrace(
124 'node_trace.*.log', paths['/traceevent'],
125 function (err) {
126 /* istanbul ignore if: the node_trace file should always exists */
127 if (err) return callback(err, paths['/'])
128 callback(null, paths['/'])
129 }
130 )
131 })
132 }
133
134 visualize (dataDirname, outputFilename, callback) {
135 const fakeDataPath = path.join(__dirname, 'visualizer', 'data.json')
136 const stylePath = path.join(__dirname, 'visualizer', 'style.css')
137 const scriptPath = path.join(__dirname, 'visualizer', 'main.js')
138 const logoPath = path.join(__dirname, 'visualizer', 'app-logo.svg')
139 const nearFormLogoPath = path.join(__dirname, 'visualizer', 'nearform-logo.svg')
140 const clinicFaviconPath = path.join(__dirname, 'visualizer', 'clinic-favicon.png.b64')
141
142 // Load data
143 const paths = getLoggingPaths({ path: dataDirname })
144
145 const systemInfoReader = pumpify.obj(
146 fs.createReadStream(paths['/systeminfo']),
147 new SystemInfoDecoder()
148 )
149 const traceEventReader = pumpify.obj(
150 fs.createReadStream(paths['/traceevent']),
151 new TraceEventDecoder(systemInfoReader)
152 )
153 const processStatReader = pumpify.obj(
154 fs.createReadStream(paths['/processstat']),
155 new ProcessStatDecoder()
156 )
157
158 // create analysis
159 const analysisStringified = pumpify(
160 new Analysis(traceEventReader, processStatReader),
161 new stream.Transform({
162 readableObjectMode: false,
163 writableObjectMode: true,
164 transform (data, encoding, callback) {
165 callback(null, JSON.stringify(data))
166 }
167 })
168 )
169
170 const traceEventStringify = pumpify(
171 traceEventReader,
172 new Stringify({
173 seperator: ',\n',
174 stringifier: JSON.stringify
175 })
176 )
177
178 const processStatStringify = pumpify(
179 processStatReader,
180 new Stringify({
181 seperator: ',\n',
182 stringifier: JSON.stringify
183 })
184 )
185
186 const hasFreeMemory = () => {
187 const used = process.memoryUsage().heapTotal / HEAP_MAX
188 if (used > 0.5) {
189 systemInfoReader.destroy()
190 traceEventReader.destroy()
191 processStatReader.destroy()
192 analysisStringified.destroy()
193 this.emit('truncate')
194 this.emit('warning', 'Truncating input data due to memory constrains')
195 }
196 }
197
198 const checkHeapInterval = setInterval(hasFreeMemory, 50)
199
200 const dataFile = streamTemplate`
201 {
202 "traceEvent": ${traceEventStringify},
203 "processStat": ${processStatStringify},
204 "analysis": ${analysisStringified}
205 }
206 `
207
208 // render recommendations as HTML templates
209 const recommendations = new RenderRecommendations()
210
211 // open logo
212 const logoFile = fs.createReadStream(logoPath)
213 const nearFormLogoFile = fs.createReadStream(nearFormLogoPath)
214 const clinicFaviconBase64 = fs.createReadStream(clinicFaviconPath)
215
216 // build JS
217 let scriptFile = buildJs({
218 basedir: __dirname,
219 debug: this.debug,
220 fakeDataPath,
221 scriptPath,
222 beforeBundle: b => b.require(dataFile, {
223 file: fakeDataPath
224 })
225 })
226
227 if (!this.debug) {
228 scriptFile = scriptFile.pipe(minifyStream({ sourceMap: false, mangle: false }))
229 }
230
231 // build CSS
232 const styleFile = buildCss({
233 stylePath,
234 debug: this.debug
235 })
236
237 // forward dataFile errors to the scriptFile explicitly
238 // we cannot use destroy until nodejs/node#18172 and nodejs/node#18171 are fixed
239 dataFile.on('error', (err) => scriptFile.emit('error', err))
240
241 // Create body contents with recommendations
242 const body = streamTemplate`
243 <div id="front-matter">
244 <div id="alert"></div>
245 <div id="menu"></div>
246 </div>
247 <div id="graph"></div>
248 <div id="recommendation-space"></div>
249 <div id="recommendation"></div>
250 ${recommendations}
251 `
252
253 // build output file
254 const outputFile = mainTemplate({
255 htmlClass: 'grid-layout',
256 favicon: clinicFaviconBase64,
257 title: 'Clinic Doctor',
258 styles: styleFile,
259 script: scriptFile,
260 headerLogoUrl: 'https://github.com/nearform/node-clinic-doctor',
261 headerLogoTitle: 'Clinic Doctor on GitHub',
262 headerLogo: logoFile,
263 headerText: 'Doctor',
264 nearFormLogo: nearFormLogoFile,
265 uploadId: outputFilename.split('/').pop().split('.html').shift(),
266 body
267 })
268
269 pump(
270 outputFile,
271 fs.createWriteStream(outputFilename),
272 function (err) {
273 clearInterval(checkHeapInterval)
274 callback(err)
275 }
276 )
277 }
278}
279
280module.exports = ClinicDoctor