memory.coffee 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  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. # use setTimeout add some randomisation of the start time
  52. setTimeout () ->
  53. interval = setInterval () ->
  54. MemoryMonitor.Check(logger)
  55. , oneMinute
  56. , Math.floor Math.random()*oneMinute
  57. Metrics = require "./metrics"
  58. Metrics.registerDestructor () ->
  59. clearInterval(interval)
  60. Check: (logger) ->
  61. Metrics = require "./metrics"
  62. memBeforeGc = mem = inMegaBytes process.memoryUsage()
  63. Metrics.gauge("memory.rss", mem.rss)
  64. Metrics.gauge("memory.heaptotal", mem.heapTotal)
  65. Metrics.gauge("memory.heapused", mem.heapUsed)
  66. Metrics.gauge("memory.gc-interval", gcInterval)
  67. Metrics.gauge("memory.cpu-time-bucket", CpuTimeBucket)
  68. if global.gc? && readyToGc()
  69. gcTime = (executeAndTime global.gc).toFixed(2)
  70. memAfterGc = inMegaBytes process.memoryUsage()
  71. deltaMem = updateMemoryStats(memBeforeGc, memAfterGc)
  72. logger.log {gcTime, memBeforeGc, memAfterGc, deltaMem, gcInterval, CpuTimeBucket}, "global.gc() forced"
  73. Metrics.timing("memory.gc-time", gcTime)
  74. Metrics.gauge("memory.gc-rss-freed", -deltaMem.rss)
  75. Metrics.gauge("memory.gc-heaptotal-freed", -deltaMem.heapTotal)
  76. Metrics.gauge("memory.gc-heapused-freed", -deltaMem.heapUsed)