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 | const customEnv = {
|
61 |
|
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 |
|
83 | const options = { identifier: proc.pid, path: this.path }
|
84 | const paths = getLoggingPaths(options)
|
85 |
|
86 | process.once('SIGINT', function () {
|
87 |
|
88 |
|
89 |
|
90 |
|
91 | if (os.platform() !== 'win32') proc.kill('SIGINT')
|
92 | })
|
93 |
|
94 | proc.once('exit', (code, signal) => {
|
95 |
|
96 |
|
97 |
|
98 | if (code === 3221225786 && os.platform() === 'win32') signal = 'SIGINT'
|
99 |
|
100 |
|
101 | if (code !== 0 && signal !== 'SIGINT') {
|
102 |
|
103 | if (code !== null) {
|
104 | console.error(`process exited with exit code ${code}`)
|
105 | } else {
|
106 |
|
107 | return callback(
|
108 | new Error(`process exited by signal ${signal}`),
|
109 | paths['/']
|
110 | )
|
111 | }
|
112 | }
|
113 |
|
114 | this.emit('analysing')
|
115 |
|
116 |
|
117 | joinTrace(
|
118 | 'node_trace.*.log', paths['/traceevent'],
|
119 | function (err) {
|
120 |
|
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 |
|
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 |
|
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 |
|
203 | const recommendations = new RenderRecommendations()
|
204 |
|
205 |
|
206 | const logoFile = fs.createReadStream(logoPath)
|
207 | const nearFormLogoFile = fs.createReadStream(nearFormLogoPath)
|
208 | const clinicFaviconBase64 = fs.createReadStream(clinicFaviconPath)
|
209 |
|
210 |
|
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 |
|
226 | const styleFile = buildCss({
|
227 | stylePath,
|
228 | debug: this.debug
|
229 | })
|
230 |
|
231 |
|
232 |
|
233 | dataFile.on('error', (err) => scriptFile.emit('error', err))
|
234 |
|
235 |
|
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 |
|
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 |
|
274 | module.exports = ClinicDoctor
|