nimprof.nim 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2015 Andreas Rumpf
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## Profiling support for Nim. This is an embedded profiler that requires
  10. ## ``--profiler:on``. You only need to import this module to get a profiling
  11. ## report at program exit.
  12. when not defined(profiler) and not defined(memProfiler):
  13. {.error: "Profiling support is turned off! Enable profiling by passing `--profiler:on --stackTrace:on` to the compiler (see the Nim Compiler User Guide for more options).".}
  14. # We don't want to profile the profiling code ...
  15. {.push profiler: off.}
  16. import hashes, algorithm, strutils, tables, sets
  17. when not defined(memProfiler):
  18. include "system/timers"
  19. const
  20. withThreads = compileOption("threads")
  21. tickCountCorrection = 50_000
  22. when not declared(system.StackTrace):
  23. type StackTrace = object
  24. lines: array[0..20, cstring]
  25. files: array[0..20, cstring]
  26. proc `[]`*(st: StackTrace, i: int): cstring = st.lines[i]
  27. # We use a simple hash table of bounded size to keep track of the stack traces:
  28. type
  29. ProfileEntry = object
  30. total: int
  31. st: StackTrace
  32. ProfileData = array[0..64*1024-1, ptr ProfileEntry]
  33. proc `==`(a, b: StackTrace): bool =
  34. for i in 0 .. high(a.lines):
  35. if a[i] != b[i]: return false
  36. result = true
  37. # XXX extract this data structure; it is generally useful ;-)
  38. # However a chain length of over 3000 is suspicious...
  39. var
  40. profileData: ProfileData
  41. emptySlots = profileData.len * 3 div 2
  42. maxChainLen = 0
  43. totalCalls = 0
  44. when not defined(memProfiler):
  45. var interval: Nanos = 5_000_000 - tickCountCorrection # 5ms
  46. proc setSamplingFrequency*(intervalInUs: int) =
  47. ## set this to change the sampling frequency. Default value is 5ms.
  48. ## Set it to 0 to disable time based profiling; it uses an imprecise
  49. ## instruction count measure instead then.
  50. if intervalInUs <= 0: interval = 0
  51. else: interval = intervalInUs * 1000 - tickCountCorrection
  52. when withThreads:
  53. import locks
  54. var
  55. profilingLock: Lock
  56. initLock profilingLock
  57. proc hookAux(st: StackTrace, costs: int) =
  58. # this is quite performance sensitive!
  59. when withThreads: acquire profilingLock
  60. inc totalCalls
  61. var last = high(st.lines)
  62. while last > 0 and isNil(st[last]): dec last
  63. var h = hash(pointer(st[last])) and high(profileData)
  64. # we use probing for maxChainLen entries and replace the encountered entry
  65. # with the minimal 'total' value:
  66. if emptySlots == 0:
  67. var minIdx = h
  68. var probes = maxChainLen
  69. while probes >= 0:
  70. if profileData[h].st == st:
  71. # wow, same entry found:
  72. inc profileData[h].total, costs
  73. return
  74. if profileData[minIdx].total < profileData[h].total:
  75. minIdx = h
  76. h = ((5 * h) + 1) and high(profileData)
  77. dec probes
  78. profileData[minIdx].total = costs
  79. profileData[minIdx].st = st
  80. else:
  81. var chain = 0
  82. while true:
  83. if profileData[h] == nil:
  84. profileData[h] = cast[ptr ProfileEntry](
  85. allocShared0(sizeof(ProfileEntry)))
  86. profileData[h].total = costs
  87. profileData[h].st = st
  88. dec emptySlots
  89. break
  90. if profileData[h].st == st:
  91. # wow, same entry found:
  92. inc profileData[h].total, costs
  93. break
  94. h = ((5 * h) + 1) and high(profileData)
  95. inc chain
  96. maxChainLen = max(maxChainLen, chain)
  97. when withThreads: release profilingLock
  98. when defined(memProfiler):
  99. const
  100. SamplingInterval = 50_000
  101. var
  102. gTicker {.threadvar.}: int
  103. proc requestedHook(): bool {.nimcall, locks: 0.} =
  104. if gTicker == 0:
  105. gTicker = SamplingInterval
  106. result = true
  107. dec gTicker
  108. proc hook(st: StackTrace, size: int) {.nimcall, locks: 0.} =
  109. when defined(ignoreAllocationSize):
  110. hookAux(st, 1)
  111. else:
  112. hookAux(st, size)
  113. else:
  114. var
  115. t0 {.threadvar.}: Ticks
  116. gTicker: int # we use an additional counter to
  117. # avoid calling 'getTicks' too frequently
  118. proc requestedHook(): bool {.nimcall, locks: 0.} =
  119. if interval == 0: result = true
  120. elif gTicker == 0:
  121. gTicker = 500
  122. if getTicks() - t0 > interval:
  123. result = true
  124. else:
  125. dec gTicker
  126. proc hook(st: StackTrace) {.nimcall, locks: 0.} =
  127. #echo "profiling! ", interval
  128. if interval == 0:
  129. hookAux(st, 1)
  130. else:
  131. hookAux(st, 1)
  132. t0 = getTicks()
  133. proc getTotal(x: ptr ProfileEntry): int =
  134. result = if isNil(x): 0 else: x.total
  135. proc cmpEntries(a, b: ptr ProfileEntry): int =
  136. result = b.getTotal - a.getTotal
  137. proc `//`(a, b: int): string =
  138. result = format("$1/$2 = $3%", a, b, formatFloat(a / b * 100.0, ffDecimal, 2))
  139. proc writeProfile() {.noconv.} =
  140. system.profilingRequestedHook = nil
  141. when declared(system.StackTrace):
  142. system.profilerHook = nil
  143. const filename = "profile_results.txt"
  144. echo "writing " & filename & "..."
  145. var f: File
  146. if open(f, filename, fmWrite):
  147. sort(profileData, cmpEntries)
  148. writeLine(f, "total executions of each stack trace:")
  149. var entries = 0
  150. for i in 0..high(profileData):
  151. if profileData[i] != nil: inc entries
  152. var perProc = initCountTable[string]()
  153. for i in 0..entries-1:
  154. var dups = initSet[string]()
  155. for ii in 0..high(StackTrace.lines):
  156. let procname = profileData[i].st[ii]
  157. if isNil(procname): break
  158. let p = $procname
  159. if not containsOrIncl(dups, p):
  160. perProc.inc(p, profileData[i].total)
  161. var sum = 0
  162. # only write the first 100 entries:
  163. for i in 0..min(100, entries-1):
  164. if profileData[i].total > 1:
  165. inc sum, profileData[i].total
  166. writeLine(f, "Entry: ", i+1, "/", entries, " Calls: ",
  167. profileData[i].total // totalCalls, " [sum: ", sum, "; ",
  168. sum // totalCalls, "]")
  169. for ii in 0..high(StackTrace.lines):
  170. let procname = profileData[i].st[ii]
  171. let filename = profileData[i].st.files[ii]
  172. if isNil(procname): break
  173. writeLine(f, " ", $filename & ": " & $procname, " ", perProc[$procname] // totalCalls)
  174. close(f)
  175. echo "... done"
  176. else:
  177. echo "... failed"
  178. var
  179. disabled: int
  180. proc disableProfiling*() =
  181. when declared(system.StackTrace):
  182. atomicDec disabled
  183. system.profilingRequestedHook = nil
  184. proc enableProfiling*() =
  185. when declared(system.StackTrace):
  186. if atomicInc(disabled) >= 0:
  187. system.profilingRequestedHook = requestedHook
  188. when declared(system.StackTrace):
  189. system.profilingRequestedHook = requestedHook
  190. system.profilerHook = hook
  191. addQuitProc(writeProfile)
  192. {.pop.}