UNPKG

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