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 streamTemplate = require('stream-template')
|
14 | const joinTrace = require('node-trace-log-join')
|
15 | const getLoggingPaths = require('@nearform/clinic-common').getLoggingPaths('doctor')
|
16 | const SystemInfoDecoder = require('./format/system-info-decoder.js')
|
17 | const TraceEventDecoder = require('./format/trace-event-decoder.js')
|
18 | const ProcessStatDecoder = require('./format/process-stat-decoder.js')
|
19 | const RenderRecommendations = require('./recommendations/index.js')
|
20 | const minifyStream = require('minify-stream')
|
21 | const v8 = require('v8')
|
22 | const HEAP_MAX = v8.getHeapStatistics().heap_size_limit
|
23 | const buildJs = require('@nearform/clinic-common/scripts/build-js')
|
24 | const buildCss = require('@nearform/clinic-common/scripts/build-css')
|
25 | const mainTemplate = require('@nearform/clinic-common/templates/main')
|
26 |
|
27 | class ClinicDoctor extends events.EventEmitter {
|
28 | constructor (settings = {}) {
|
29 | super()
|
30 |
|
31 |
|
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 |
|
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 |
|
62 | if (process.env.NODE_PATH) {
|
63 | NODE_PATH += `${process.platform === 'win32' ? ';' : ':'}${process.env.NODE_PATH}`
|
64 | }
|
65 |
|
66 | const customEnv = {
|
67 |
|
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 |
|
89 | const options = { identifier: proc.pid, path: this.path }
|
90 | const paths = getLoggingPaths(options)
|
91 |
|
92 | process.once('SIGINT', function () {
|
93 |
|
94 |
|
95 |
|
96 |
|
97 | if (os.platform() !== 'win32') proc.kill('SIGINT')
|
98 | })
|
99 |
|
100 | proc.once('exit', (code, signal) => {
|
101 |
|
102 |
|
103 |
|
104 | if (code === 3221225786 && os.platform() === 'win32') signal = 'SIGINT'
|
105 |
|
106 |
|
107 | if (code !== 0 && signal !== 'SIGINT') {
|
108 |
|
109 | if (code !== null) {
|
110 | console.error(`process exited with exit code ${code}`)
|
111 | } else {
|
112 |
|
113 | return callback(
|
114 | new Error(`process exited by signal ${signal}`),
|
115 | paths['/']
|
116 | )
|
117 | }
|
118 | }
|
119 |
|
120 | this.emit('analysing')
|
121 |
|
122 |
|
123 | joinTrace(
|
124 | 'node_trace.*.log', paths['/traceevent'],
|
125 | function (err) {
|
126 |
|
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 |
|
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 |
|
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 |
|
209 | const recommendations = new RenderRecommendations()
|
210 |
|
211 |
|
212 | const logoFile = fs.createReadStream(logoPath)
|
213 | const nearFormLogoFile = fs.createReadStream(nearFormLogoPath)
|
214 | const clinicFaviconBase64 = fs.createReadStream(clinicFaviconPath)
|
215 |
|
216 |
|
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 |
|
232 | const styleFile = buildCss({
|
233 | stylePath,
|
234 | debug: this.debug
|
235 | })
|
236 |
|
237 |
|
238 |
|
239 | dataFile.on('error', (err) => scriptFile.emit('error', err))
|
240 |
|
241 |
|
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 |
|
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 |
|
280 | module.exports = ClinicDoctor
|