keymetrics/pm2-io-apm

View on GitHub
src/profilers/inspectorProfiler.ts

Summary

Maintainability
C
1 day
Test Coverage

import { ProfilerType } from '../features/profiling'
import Configuration from '../configuration'
import { ServiceManager } from '../serviceManager'
import { Transport } from '../services/transport'
import { ActionService } from '../services/actions'
import MiscUtils from '../utils/miscellaneous'
import { InspectorService } from '../services/inspector'
import * as inspector from 'inspector'
import * as Debug from 'debug'
import * as semver from 'semver'

class CurrentProfile {
  uuid: string
  startTime: number
  initiated: string
}

export default class InspectorProfiler implements ProfilerType {

  private profiler: InspectorService | undefined = undefined
  private actionService: ActionService | undefined
  private transport: Transport | undefined
  private currentProfile: CurrentProfile | null = null
  private logger: Function = Debug('axm:features:profiling:inspector')
  private isNode11: boolean = semver.satisfies(semver.clean(process.version), '>11.x')

  init () {
    this.profiler = ServiceManager.get('inspector')
    if (this.profiler === undefined) {
      Configuration.configureModule({
        heapdump: false,
        'feature.profiler.heap_snapshot': false,
        'feature.profiler.heap_sampling': false,
        'feature.profiler.cpu_js': false
      })
      return console.error(`Failed to require the profiler via inspector, disabling profiling ...`)
    }

    this.profiler.getSession().post('Profiler.enable')
    this.profiler.getSession().post('HeapProfiler.enable')
    this.logger('init')

    this.actionService = ServiceManager.get('actions')
    if (this.actionService === undefined) {
      return this.logger(`Fail to get action service`)
    }
    this.transport = ServiceManager.get('transport')
    if (this.transport === undefined) {
      return this.logger(`Fail to get transport service`)
    }

    Configuration.configureModule({
      heapdump: true,
      'feature.profiler.heapsnapshot': !this.isNode11,
      'feature.profiler.heapsampling': true,
      'feature.profiler.cpu_js': true
    })
    this.register()
  }

  register () {
    if (this.actionService === undefined) {
      return this.logger(`Fail to get action service`)
    }
    this.logger('register')
    this.actionService.registerAction('km:heapdump', this.onHeapdump.bind(this))
    this.actionService.registerAction('km:cpu:profiling:start', this.onCPUProfileStart.bind(this))
    this.actionService.registerAction('km:cpu:profiling:stop', this.onCPUProfileStop.bind(this))
    this.actionService.registerAction('km:heap:sampling:start', this.onHeapProfileStart.bind(this))
    this.actionService.registerAction('km:heap:sampling:stop', this.onHeapProfileStop.bind(this))
  }

  destroy () {
    this.logger('Inspector Profiler destroyed !')
    if (this.profiler === undefined) return
    this.profiler.getSession().post('Profiler.disable')
    this.profiler.getSession().post('HeapProfiler.disable')
  }

  private onHeapProfileStart (opts, cb) {
    if (typeof cb !== 'function') {
      cb = opts
      opts = {}
    }
    if (typeof opts !== 'object' || opts === null) {
      opts = {}
    }

    // not possible but thanks mr typescript
    if (this.profiler === undefined) {
      return cb({
        err: 'Profiler not available',
        success: false
      })
    }

    if (this.currentProfile !== null) {
      return cb({
        err: 'A profiling is already running',
        success: false
      })
    }
    this.currentProfile = new CurrentProfile()
    this.currentProfile.uuid = MiscUtils.generateUUID()
    this.currentProfile.startTime = Date.now()
    this.currentProfile.initiated = typeof opts.initiated === 'string'
      ? opts.initiated : 'manual'

     // run the callback to acknowledge that we received the action
    cb({ success: true, uuid: this.currentProfile.uuid })

    const defaultSamplingInterval = 16384
    this.profiler.getSession().post('HeapProfiler.startSampling', {
      samplingInterval: typeof opts.samplingInterval === 'number'
        ? opts.samplingInterval : defaultSamplingInterval
    })

    if (isNaN(parseInt(opts.timeout, 10))) return
    // if the duration is included, handle that ourselves
    const duration = parseInt(opts.timeout, 10)
    setTimeout(_ => {
      // it will send the profiling itself
      this.onHeapProfileStop(_ => {
        return
      })
    }, duration)
  }

  private onHeapProfileStop (cb) {
    if (this.currentProfile === null) {
      return cb({
        err: 'No profiling are already running',
        success: false
      })
    }
    // not possible but thanks mr typescript
    if (this.profiler === undefined) {
      return cb({
        err: 'Profiler not available',
        success: false
      })
    }

    // run the callback to acknowledge that we received the action
    cb({ success: true, uuid: this.currentProfile.uuid })

    this.profiler.getSession().post('HeapProfiler.stopSampling', (_: Error, { profile }: inspector.HeapProfiler.StopSamplingReturnType) => {
      // not possible but thanks mr typescript
      if (this.currentProfile === null) return
      if (this.transport === undefined) return

      const data = JSON.stringify(profile)

      this.transport.send('profilings', {
        uuid: this.currentProfile.uuid,
        duration: Date.now() - this.currentProfile.startTime,
        at: this.currentProfile.startTime,
        data,
        success: true,
        initiated: this.currentProfile.initiated,
        type: 'heapprofile',
        heapprofile: true
      })
      this.currentProfile = null
    })
  }

  private onCPUProfileStart (opts, cb) {
    if (typeof cb !== 'function') {
      cb = opts
      opts = {}
    }
    if (typeof opts !== 'object' || opts === null) {
      opts = {}
    }
    // not possible but thanks mr typescript
    if (this.profiler === undefined) {
      return cb({
        err: 'Profiler not available',
        success: false
      })
    }

    if (this.currentProfile !== null) {
      return cb({
        err: 'A profiling is already running',
        success: false
      })
    }
    this.currentProfile = new CurrentProfile()
    this.currentProfile.uuid = MiscUtils.generateUUID()
    this.currentProfile.startTime = Date.now()
    this.currentProfile.initiated = typeof opts.initiated === 'string'
      ? opts.initiated : 'manual'

     // run the callback to acknowledge that we received the action
    cb({ success: true, uuid: this.currentProfile.uuid })

    // start the idle time reporter to tell V8 when node is idle
    // See https://github.com/nodejs/node/issues/19009#issuecomment-403161559.
    if (process.hasOwnProperty('_startProfilerIdleNotifier') === true) {
      (process as any)._startProfilerIdleNotifier()
    }

    this.profiler.getSession().post('Profiler.start')

    if (isNaN(parseInt(opts.timeout, 10))) return
    // if the duration is included, handle that ourselves
    const duration = parseInt(opts.timeout, 10)
    setTimeout(_ => {
      // it will send the profiling itself
      this.onCPUProfileStop(_ => {
        return
      })
    }, duration)
  }

  private onCPUProfileStop (cb) {
    if (this.currentProfile === null) {
      return cb({
        err: 'No profiling are already running',
        success: false
      })
    }
    // not possible but thanks mr typescript
    if (this.profiler === undefined) {
      return cb({
        err: 'Profiler not available',
        success: false
      })
    }

    // run the callback to acknowledge that we received the action
    cb({ success: true, uuid: this.currentProfile.uuid })

    // stop the idle time reporter to tell V8 when node is idle
    // See https://github.com/nodejs/node/issues/19009#issuecomment-403161559.
    if (process.hasOwnProperty('_stopProfilerIdleNotifier') === true) {
      (process as any)._stopProfilerIdleNotifier()
    }

    this.profiler.getSession().post('Profiler.stop', (_: Error, res: any) => {
      // not possible but thanks mr typescript
      if (this.currentProfile === null) return
      if (this.transport === undefined) return

      const profile: inspector.Profiler.Profile = res.profile
      const data = JSON.stringify(profile)

      // send the profile to the transporter
      this.transport.send('profilings', {
        uuid: this.currentProfile.uuid,
        duration: Date.now() - this.currentProfile.startTime,
        at: this.currentProfile.startTime,
        data,
        success: true,
        initiated: this.currentProfile.initiated,
        type: 'cpuprofile',
        cpuprofile: true
      })
      this.currentProfile = null
    })
  }

  /**
   * Custom action implementation to make a heap snapshot
   */
  private onHeapdump (opts, cb) {
    if (typeof cb !== 'function') {
      cb = opts
      opts = {}
    }
    if (typeof opts !== 'object' || opts === null) {
      opts = {}
    }
    // not possible but thanks mr typescript
    if (this.profiler === undefined) {
      return cb({
        err: 'Profiler not available',
        success: false
      })
    }

    // run the callback to acknowledge that we received the action
    cb({ success: true })

    // wait few ms to be sure we sended the ACK because the snapshot stop the world
    setTimeout(() => {
      const startTime = Date.now()
      this.takeSnapshot()
        .then(data => {
          // @ts-ignore thanks mr typescript but its not possible
          return this.transport.send('profilings', {
            data,
            at: startTime,
            initiated: typeof opts.initiated === 'string' ? opts.initiated : 'manual',
            duration: Date.now() - startTime,
            type: 'heapdump'
          })
        }).catch(err => {
          return cb({
            success: err.message,
            err: err
          })
        })
    }, 200)
  }

  takeSnapshot () {
    return new Promise(async (resolve, reject) => {
      // not possible but thanks mr typescript
      if (this.profiler === undefined) return reject(new Error(`Profiler not available`))

      const chunks: Array<string> = []
      const chunkHandler = (raw: any) => {
        const data = raw.params as inspector.HeapProfiler.AddHeapSnapshotChunkEventDataType
        chunks.push(data.chunk)
      }
      this.profiler.getSession().on('HeapProfiler.addHeapSnapshotChunk', chunkHandler)
      // tslint:disable-next-line
      await this.profiler.getSession().post('HeapProfiler.takeHeapSnapshot', {
        reportProgress: false
      })
      // remove the listeners
      this.profiler.getSession().removeListener('HeapProfiler.addHeapSnapshotChunk', chunkHandler)
      return resolve(chunks.join(''))
    })
  }
}