summary refs log tree commit diff stats
path: root/lib/pure/nimprof.nim
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pure/nimprof.nim')
-rw-r--r--lib/pure/nimprof.nim233
1 files changed, 233 insertions, 0 deletions
diff --git a/lib/pure/nimprof.nim b/lib/pure/nimprof.nim
new file mode 100644
index 000000000..bf8367d1d
--- /dev/null
+++ b/lib/pure/nimprof.nim
@@ -0,0 +1,233 @@
+#
+#
+#            Nim's Runtime Library
+#        (c) Copyright 2015 Andreas Rumpf
+#
+#    See the file "copying.txt", included in this
+#    distribution, for details about the copyright.
+#
+
+## Profiling support for Nim. This is an embedded profiler that requires
+## `--profiler:on`. You only need to import this module to get a profiling
+## report at program exit. See `Embedded Stack Trace Profiler <estp.html>`_
+## for usage.
+
+when not defined(profiler) and not defined(memProfiler):
+  {.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).".}
+
+{.used.}
+
+# We don't want to profile the profiling code ...
+{.push profiler: off.}
+
+import std/[hashes, algorithm, strutils, tables, sets]
+
+when defined(nimPreviewSlimSystem):
+  import std/[syncio, sysatomics]
+
+when not defined(memProfiler):
+  include "system/timers"
+
+const
+  withThreads = compileOption("threads")
+  tickCountCorrection = 50_000
+
+when not declared(system.StackTrace):
+  type StackTrace = object
+    lines: array[0..20, cstring]
+    files: array[0..20, cstring]
+  proc `[]`*(st: StackTrace, i: int): cstring = st.lines[i]
+
+# We use a simple hash table of bounded size to keep track of the stack traces:
+type
+  ProfileEntry = object
+    total: int
+    st: StackTrace
+  ProfileData = array[0..64*1024-1, ptr ProfileEntry]
+
+proc `==`(a, b: StackTrace): bool =
+  for i in 0 .. high(a.lines):
+    if a[i] != b[i]: return false
+  result = true
+
+# XXX extract this data structure; it is generally useful ;-)
+# However a chain length of over 3000 is suspicious...
+var
+  profileData: ProfileData
+  emptySlots = profileData.len * 3 div 2
+  maxChainLen = 0
+  totalCalls = 0
+
+when not defined(memProfiler):
+  var interval: Nanos = 5_000_000 - tickCountCorrection # 5ms
+
+  proc setSamplingFrequency*(intervalInUs: int) =
+    ## set this to change the sampling frequency. Default value is 5ms.
+    ## Set it to 0 to disable time based profiling; it uses an imprecise
+    ## instruction count measure instead then.
+    if intervalInUs <= 0: interval = 0
+    else: interval = intervalInUs * 1000 - tickCountCorrection
+
+when withThreads:
+  import std/locks
+  var
+    profilingLock: Lock
+
+  initLock profilingLock
+
+proc hookAux(st: StackTrace, costs: int) =
+  # this is quite performance sensitive!
+  when withThreads: acquire profilingLock
+  inc totalCalls
+  var last = high(st.lines)
+  while last > 0 and isNil(st[last]): dec last
+  var h = hash(pointer(st[last])) and high(profileData)
+
+  # we use probing for maxChainLen entries and replace the encountered entry
+  # with the minimal 'total' value:
+  if emptySlots == 0:
+    var minIdx = h
+    var probes = maxChainLen
+    while probes >= 0:
+      if profileData[h].st == st:
+        # wow, same entry found:
+        inc profileData[h].total, costs
+        return
+      if profileData[minIdx].total < profileData[h].total:
+        minIdx = h
+      h = ((5 * h) + 1) and high(profileData)
+      dec probes
+    profileData[minIdx].total = costs
+    profileData[minIdx].st = st
+  else:
+    var chain = 0
+    while true:
+      if profileData[h] == nil:
+        profileData[h] = cast[ptr ProfileEntry](
+                             allocShared0(sizeof(ProfileEntry)))
+        profileData[h].total = costs
+        profileData[h].st = st
+        dec emptySlots
+        break
+      if profileData[h].st == st:
+        # wow, same entry found:
+        inc profileData[h].total, costs
+        break
+      h = ((5 * h) + 1) and high(profileData)
+      inc chain
+    maxChainLen = max(maxChainLen, chain)
+  when withThreads: release profilingLock
+
+when defined(memProfiler):
+  const
+    SamplingInterval = 50_000
+  var
+    gTicker {.threadvar.}: int
+
+  proc requestedHook(): bool {.nimcall.} =
+    if gTicker == 0:
+      gTicker = SamplingInterval
+      result = true
+    dec gTicker
+
+  proc hook(st: StackTrace, size: int) {.nimcall.} =
+    when defined(ignoreAllocationSize):
+      hookAux(st, 1)
+    else:
+      hookAux(st, size)
+
+else:
+  var
+    t0 {.threadvar.}: Ticks
+    gTicker: int # we use an additional counter to
+                 # avoid calling 'getTicks' too frequently
+
+  proc requestedHook(): bool {.nimcall.} =
+    if interval == 0: result = true
+    elif gTicker == 0:
+      gTicker = 500
+      if getTicks() - t0 > interval:
+        result = true
+    else:
+      dec gTicker
+
+  proc hook(st: StackTrace) {.nimcall.} =
+    #echo "profiling! ", interval
+    if interval == 0:
+      hookAux(st, 1)
+    else:
+      hookAux(st, 1)
+      t0 = getTicks()
+
+proc getTotal(x: ptr ProfileEntry): int =
+  result = if isNil(x): 0 else: x.total
+
+proc cmpEntries(a, b: ptr ProfileEntry): int =
+  result = b.getTotal - a.getTotal
+
+proc `//`(a, b: int): string =
+  result = format("$1/$2 = $3%", a, b, formatFloat(a / b * 100.0, ffDecimal, 2))
+
+proc writeProfile() {.noconv.} =
+  system.profilingRequestedHook = nil
+  when declared(system.StackTrace):
+    system.profilerHook = nil
+  const filename = "profile_results.txt"
+  echo "writing " & filename & "..."
+  var f: File
+  if open(f, filename, fmWrite):
+    sort(profileData, cmpEntries)
+    writeLine(f, "total executions of each stack trace:")
+    var entries = 0
+    for i in 0..high(profileData):
+      if profileData[i] != nil: inc entries
+
+    var perProc = initCountTable[string]()
+    for i in 0..entries-1:
+      var dups = initHashSet[string]()
+      for ii in 0..high(StackTrace.lines):
+        let procname = profileData[i].st[ii]
+        if isNil(procname): break
+        let p = $procname
+        if not containsOrIncl(dups, p):
+          perProc.inc(p, profileData[i].total)
+
+    var sum = 0
+    # only write the first 100 entries:
+    for i in 0..min(100, entries-1):
+      if profileData[i].total > 1:
+        inc sum, profileData[i].total
+        writeLine(f, "Entry: ", i+1, "/", entries, " Calls: ",
+          profileData[i].total // totalCalls, " [sum: ", sum, "; ",
+          sum // totalCalls, "]")
+        for ii in 0..high(StackTrace.lines):
+          let procname = profileData[i].st[ii]
+          let filename = profileData[i].st.files[ii]
+          if isNil(procname): break
+          writeLine(f, "  ", $filename & ": " & $procname, " ",
+                    perProc[$procname] // totalCalls)
+    close(f)
+    echo "... done"
+  else:
+    echo "... failed"
+
+var
+  disabled: int
+
+proc disableProfiling*() =
+  when declared(system.StackTrace):
+    atomicDec disabled
+    system.profilingRequestedHook = nil
+
+proc enableProfiling*() =
+  when declared(system.StackTrace):
+    if atomicInc(disabled) >= 0:
+      system.profilingRequestedHook = requestedHook
+
+when declared(system.StackTrace):
+  import std/exitprocs
+  system.profilingRequestedHook = requestedHook
+  system.profilerHook = hook
+  addExitProc(writeProfile)
+
+{.pop.}