memory.coffee 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. # record memory usage each minute and run a periodic gc(), keeping cpu
  2. # usage within allowable range of 1ms per minute. Also, dynamically
  3. # adjust the period between gc()'s to reach a target of the gc saving
  4. # 4 megabytes each time.
  5. oneMinute = 60 * 1000
  6. oneMegaByte = 1024 * 1024
  7. CpuTimeBucket = 100 # current cpu time allowance in milliseconds
  8. CpuTimeBucketMax = 100 # maximum amount of cpu time allowed in bucket
  9. CpuTimeBucketRate = 10 # add this many milliseconds per minute
  10. gcInterval = 1 # how many minutes between gc (parameter is dynamically adjusted)
  11. countSinceLastGc = 0 # how many minutes since last gc
  12. MemoryChunkSize = 4 # how many megabytes we need to free to consider gc worth doing
  13. readyToGc = () ->
  14. # update allowed cpu time
  15. CpuTimeBucket = CpuTimeBucket + CpuTimeBucketRate
  16. CpuTimeBucket = if CpuTimeBucket < CpuTimeBucketMax then CpuTimeBucket else CpuTimeBucketMax
  17. # update counts since last gc
  18. countSinceLastGc = countSinceLastGc + 1
  19. # check there is enough time since last gc and we have enough cpu
  20. return (countSinceLastGc > gcInterval) && (CpuTimeBucket > 0)
  21. executeAndTime = (fn) ->
  22. # time the execution of fn() and subtract from cpu allowance
  23. t0 = process.hrtime()
  24. fn()
  25. dt = process.hrtime(t0)
  26. timeTaken = (dt[0] + dt[1]*1e-9) * 1e3 # in milliseconds
  27. CpuTimeBucket -= Math.ceil timeTaken
  28. return timeTaken
  29. inMegaBytes = (obj) ->
  30. # convert process.memoryUsage hash {rss,heapTotal,heapFreed} into megabytes
  31. result = {}
  32. for k, v of obj
  33. result[k] = (v / oneMegaByte).toFixed(2)
  34. return result
  35. updateMemoryStats = (oldMem, newMem) ->
  36. countSinceLastGc = 0
  37. delta = {}
  38. for k of newMem
  39. delta[k] = (newMem[k] - oldMem[k]).toFixed(2)
  40. # take the max of all memory measures
  41. savedMemory = Math.max -delta.rss, -delta.heapTotal, -delta.heapUsed
  42. delta.megabytesFreed = savedMemory
  43. # did it do any good?
  44. if savedMemory < MemoryChunkSize
  45. gcInterval = gcInterval + 1 # no, so wait longer next time
  46. else
  47. gcInterval = Math.max gcInterval - 1, 1 # yes, wait less time
  48. return delta
  49. module.exports = MemoryMonitor =
  50. monitor: (logger) ->
  51. interval = setInterval () ->
  52. MemoryMonitor.Check(logger)
  53. , oneMinute
  54. Metrics = require "./metrics"
  55. Metrics.registerDestructor () ->
  56. clearInterval(interval)
  57. Check: (logger) ->
  58. Metrics = require "./metrics"
  59. memBeforeGc = mem = inMegaBytes process.memoryUsage()
  60. Metrics.gauge("memory.rss", mem.rss)
  61. Metrics.gauge("memory.heaptotal", mem.heapTotal)
  62. Metrics.gauge("memory.heapused", mem.heapUsed)
  63. Metrics.gauge("memory.gc-interval", gcInterval)
  64. #Metrics.gauge("memory.cpu-time-bucket", CpuTimeBucket)
  65. logger.log mem, "process.memoryUsage()"
  66. if global.gc? && readyToGc()
  67. gcTime = (executeAndTime global.gc).toFixed(2)
  68. memAfterGc = inMegaBytes process.memoryUsage()
  69. deltaMem = updateMemoryStats(memBeforeGc, memAfterGc)
  70. logger.log {gcTime, memBeforeGc, memAfterGc, deltaMem, gcInterval, CpuTimeBucket}, "global.gc() forced"
  71. #Metrics.timing("memory.gc-time", gcTime)
  72. Metrics.gauge("memory.gc-rss-freed", -deltaMem.rss)
  73. Metrics.gauge("memory.gc-heaptotal-freed", -deltaMem.heapTotal)
  74. Metrics.gauge("memory.gc-heapused-freed", -deltaMem.heapUsed)