@nearform/doctor
Version:
Programmable interface to clinic doctor
276 lines (238 loc) • 8.51 kB
JavaScript
const events = require('events')
const fs = require('fs')
const os = require('os')
const path = require('path')
const pump = require('pump')
const pumpify = require('pumpify')
const stream = require('./lib/destroyable-stream')
const { spawn } = require('child_process')
const Analysis = require('./analysis/index.js')
const Stringify = require('streaming-json-stringify')
const browserify = require('browserify')
const streamTemplate = require('stream-template')
const joinTrace = require('node-trace-log-join')
const getLoggingPaths = require('./collect/get-logging-paths.js')
const SystemInfoDecoder = require('./format/system-info-decoder.js')
const TraceEventDecoder = require('./format/trace-event-decoder.js')
const ProcessStatDecoder = require('./format/process-stat-decoder.js')
const RenderRecommendations = require('./recommendations/index.js')
const minifyStream = require('minify-stream')
const v8 = require('v8')
const HEAP_MAX = v8.getHeapStatistics().heap_size_limit
class ClinicDoctor extends events.EventEmitter {
constructor (settings = {}) {
super()
// define default parameters
const {
sampleInterval = 10,
detectPort = false,
debug = false,
dest = null
} = settings
this.sampleInterval = sampleInterval
this.detectPort = detectPort
this.debug = debug
this.path = dest
}
collect (args, callback) {
// run program, but inject the sampler
const logArgs = [
'-r', 'no-cluster.js',
'-r', 'sampler.js',
'--trace-events-enabled', '--trace-event-categories', 'v8'
]
const stdio = ['inherit', 'inherit', 'inherit']
if (this.detectPort) {
logArgs.push('-r', 'detect-port.js')
stdio.push('pipe')
}
const customEnv = {
// use NODE_PATH to work around issues with spaces in inject path
NODE_PATH: path.join(__dirname, 'injects'),
NODE_OPTIONS: logArgs.join(' ') + (
process.env.NODE_OPTIONS ? ' ' + process.env.NODE_OPTIONS : ''
),
NODE_CLINIC_DOCTOR_SAMPLE_INTERVAL: this.sampleInterval
}
if (this.path) {
customEnv.NODE_CLINIC_DOCTOR_DATA_PATH = this.path
}
const proc = spawn(args[0], args.slice(1), {
stdio,
env: Object.assign({}, process.env, customEnv)
})
if (this.detectPort) {
proc.stdio[3].once('data', data => this.emit('port', Number(data), proc, () => proc.stdio[3].destroy()))
}
// get logging directory structure
const options = { identifier: proc.pid, path: this.path }
const paths = getLoggingPaths(options)
// relay SIGINT to process
process.once('SIGINT', function () {
// we cannot kill(SIGINT) on windows but it seems
// to relay the ctrl-c signal per default, so only do this
// if not windows
/* istanbul ignore else: windows hack */
if (os.platform() !== 'win32') proc.kill('SIGINT')
})
proc.once('exit', function (code, signal) {
// Windows exit code STATUS_CONTROL_C_EXIT 0xC000013A returns 3221225786
// if not caught. See https://msdn.microsoft.com/en-us/library/cc704588.aspx
/* istanbul ignore next: windows hack */
if (code === 3221225786 && os.platform() === 'win32') signal = 'SIGINT'
// Abort if the process did not exit normally.
if (code !== 0 && signal !== 'SIGINT') {
if (code !== null) {
return callback(
new Error(`process exited with exit code ${code}`),
paths['/']
)
} else {
return callback(
new Error(`process exited by signal ${signal}`),
paths['/']
)
}
}
// move trace_event file to logging directory
joinTrace(
'node_trace.*.log', paths['/traceevent'],
function (err) {
/* istanbul ignore if: the node_trace file should always exists */
if (err) return callback(err, paths['/'])
callback(null, paths['/'])
}
)
})
}
visualize (dataDirname, outputFilename, callback) {
const fakeDataPath = path.join(__dirname, 'visualizer', 'data.json')
const stylePath = path.join(__dirname, 'visualizer', 'style.css')
const scriptPath = path.join(__dirname, 'visualizer', 'main.js')
const logoPath = path.join(__dirname, 'visualizer', 'app-logo.svg')
const nearFormLogoPath = path.join(__dirname, 'visualizer', 'nearform-logo.svg')
const clinicFaviconPath = path.join(__dirname, 'visualizer', 'clinic-favicon.png.b64')
// Load data
const paths = getLoggingPaths({ path: dataDirname })
const systemInfoReader = pumpify.obj(
fs.createReadStream(paths['/systeminfo']),
new SystemInfoDecoder()
)
const traceEventReader = pumpify.obj(
fs.createReadStream(paths['/traceevent']),
new TraceEventDecoder(systemInfoReader)
)
const processStatReader = pumpify.obj(
fs.createReadStream(paths['/processstat']),
new ProcessStatDecoder()
)
// create analysis
const analysisStringified = pumpify(
new Analysis(traceEventReader, processStatReader),
new stream.Transform({
readableObjectMode: false,
writableObjectMode: true,
transform (data, encoding, callback) {
callback(null, JSON.stringify(data))
}
})
)
const traceEventStringify = pumpify(
traceEventReader,
new Stringify({
seperator: ',\n',
stringifier: JSON.stringify
})
)
const processStatStringify = pumpify(
processStatReader,
new Stringify({
seperator: ',\n',
stringifier: JSON.stringify
})
)
const hasFreeMemory = () => {
const used = process.memoryUsage().heapTotal / HEAP_MAX
if (used > 0.5) {
systemInfoReader.destroy()
traceEventReader.destroy()
processStatReader.destroy()
analysisStringified.destroy()
this.emit('truncate')
this.emit('warning', 'Truncating input data due to memory constrains')
}
}
const checkHeapInterval = setInterval(hasFreeMemory, 50)
const dataFile = streamTemplate`
{
"traceEvent": ${traceEventStringify},
"processStat": ${processStatStringify},
"analysis": ${analysisStringified}
}
`
// render recommendations as HTML templates
const recommendations = new RenderRecommendations()
// open logo
const logoFile = fs.createReadStream(logoPath)
const nearFormLogoFile = fs.createReadStream(nearFormLogoPath)
const clinicFaviconBase64 = fs.createReadStream(clinicFaviconPath)
// create script-file stream
const b = browserify({
'basedir': __dirname,
// 'debug': true,
'noParse': [fakeDataPath]
})
b.require(dataFile, {
'file': fakeDataPath
})
b.add(scriptPath)
b.transform('brfs')
const scriptFile = b.bundle()
if (!this.debug) {
scriptFile.pipe(minifyStream({ sourceMap: false, mangle: false }))
}
// create style-file stream
const styleFile = fs.createReadStream(stylePath)
// forward dataFile errors to the scriptFile explicitly
// we cannot use destroy until nodejs/node#18172 and nodejs/node#18171 are fixed
dataFile.on('error', (err) => scriptFile.emit('error', err))
// build output file
const outputFile = streamTemplate`
<!DOCTYPE html>
<html lang="en" class="grid-layout">
<meta charset="utf8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" type="image/png" href="${clinicFaviconBase64}">
<title>Clinic Doctor</title>
<style>${styleFile}</style>
<div id="banner">
<a href="https://github.com/nearform/node-clinic-doctor" title="Clinic Doctor on GitHub" target="_blank">
${logoFile}
</a>
<a href="https://nearform.com" title="nearForm" target="_blank">
${nearFormLogoFile}
</a>
</div>
<div id="front-matter">
<div id="alert"></div>
<div id="menu"></div>
</div>
<div id="graph"></div>
<div id="recommendation-space"></div>
<div id="recommendation"></div>
${recommendations}
<script>${scriptFile}</script>
</html>
`
pump(
outputFile,
fs.createWriteStream(outputFilename),
function (err) {
clearInterval(checkHeapInterval)
callback(err)
}
)
}
}
module.exports = ClinicDoctor