/*
 * decaffeinate suggestions:
 * DS102: Remove unnecessary code created because of implicit returns
 * DS207: Consider shorter variations of null checks
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
// record memory usage each minute and run a periodic gc(), keeping cpu
// usage within allowable range of 1ms per minute. Also, dynamically
// adjust the period between gc()'s to reach a target of the gc saving
// 4 megabytes each time.

let MemoryMonitor
const oneMinute = 60 * 1000
const oneMegaByte = 1024 * 1024

let CpuTimeBucket = 100 // current cpu time allowance in milliseconds
const CpuTimeBucketMax = 100 // maximum amount of cpu time allowed in bucket
const CpuTimeBucketRate = 10 // add this many milliseconds per minute

let gcInterval = 1 // how many minutes between gc (parameter is dynamically adjusted)
let countSinceLastGc = 0 // how many minutes since last gc
const MemoryChunkSize = 4 // how many megabytes we need to free to consider gc worth doing

const readyToGc = function () {
  // update allowed cpu time
  CpuTimeBucket = CpuTimeBucket + CpuTimeBucketRate
  CpuTimeBucket =
    CpuTimeBucket < CpuTimeBucketMax ? CpuTimeBucket : CpuTimeBucketMax
  // update counts since last gc
  countSinceLastGc = countSinceLastGc + 1
  // check there is enough time since last gc and we have enough cpu
  return countSinceLastGc > gcInterval && CpuTimeBucket > 0
}

const executeAndTime = function (fn) {
  // time the execution of fn() and subtract from cpu allowance
  const t0 = process.hrtime()
  fn()
  const dt = process.hrtime(t0)
  const timeTaken = (dt[0] + dt[1] * 1e-9) * 1e3 // in milliseconds
  CpuTimeBucket -= Math.ceil(timeTaken)
  return timeTaken
}

const inMegaBytes = function (obj) {
  // convert process.memoryUsage hash {rss,heapTotal,heapFreed} into megabytes
  const result = {}
  for (const k in obj) {
    const v = obj[k]
    result[k] = (v / oneMegaByte).toFixed(2)
  }
  return result
}

const updateMemoryStats = function (oldMem, newMem) {
  countSinceLastGc = 0
  const delta = {}
  for (const k in newMem) {
    delta[k] = (newMem[k] - oldMem[k]).toFixed(2)
  }
  // take the max of all memory measures
  const savedMemory = Math.max(-delta.rss, -delta.heapTotal, -delta.heapUsed)
  delta.megabytesFreed = savedMemory
  // did it do any good?
  if (savedMemory < MemoryChunkSize) {
    gcInterval = gcInterval + 1 // no, so wait longer next time
  } else {
    gcInterval = Math.max(gcInterval - 1, 1) // yes, wait less time
  }
  return delta
}

module.exports = MemoryMonitor = {
  monitor(logger) {
    const interval = setInterval(() => MemoryMonitor.Check(logger), oneMinute)
    const Metrics = require('./index')
    return Metrics.registerDestructor(() => clearInterval(interval))
  },

  Check(logger) {
    let mem
    const Metrics = require('./index')
    const memBeforeGc = (mem = inMegaBytes(process.memoryUsage()))
    Metrics.gauge('memory.rss', mem.rss)
    Metrics.gauge('memory.heaptotal', mem.heapTotal)
    Metrics.gauge('memory.heapused', mem.heapUsed)
    Metrics.gauge('memory.gc-interval', gcInterval)
    // Metrics.gauge("memory.cpu-time-bucket", CpuTimeBucket)

    logger.debug(mem, 'process.memoryUsage()')

    if (global.gc != null && readyToGc()) {
      const gcTime = executeAndTime(global.gc).toFixed(2)
      const memAfterGc = inMegaBytes(process.memoryUsage())
      const deltaMem = updateMemoryStats(memBeforeGc, memAfterGc)
      logger.debug(
        {
          gcTime,
          memBeforeGc,
          memAfterGc,
          deltaMem,
          gcInterval,
          CpuTimeBucket,
        },
        'global.gc() forced'
      )
      Metrics.timing('memory.gc-time', +gcTime)
      Metrics.gauge('memory.gc-rss-freed', -deltaMem.rss)
      Metrics.gauge('memory.gc-heaptotal-freed', -deltaMem.heapTotal)
      return Metrics.gauge('memory.gc-heapused-freed', -deltaMem.heapUsed)
    }
  },
}
