summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorDominik Picheta <dominikpicheta@gmail.com>2019-05-05 21:33:25 +0100
committerDominik Picheta <dominikpicheta@gmail.com>2019-05-06 17:18:30 +0100
commitabe634aefe555ce40b18104addb4d44204c27114 (patch)
tree9e180d263cfb14f824415b3ea128bce910da6911
parent9c3e23e0751881b16fab813fd3c2edc1b98b7f68 (diff)
downloadNim-abe634aefe555ce40b18104addb4d44204c27114.tar.gz
Implements pending future tracking in async.
-rw-r--r--lib/pure/asyncdispatch.nim21
-rw-r--r--lib/pure/asyncfutures.nim51
-rw-r--r--lib/system/excpt.nim6
3 files changed, 75 insertions, 3 deletions
diff --git a/lib/pure/asyncdispatch.nim b/lib/pure/asyncdispatch.nim
index 2fb00d6a7..3cc574afd 100644
--- a/lib/pure/asyncdispatch.nim
+++ b/lib/pure/asyncdispatch.nim
@@ -140,6 +140,27 @@
 ## asynchronous IO. A good place to start is the
 ## `asyncnet module <asyncnet.html>`_.
 ##
+## Investigating pending futures
+## =============================
+##
+## It's possible to get into a situation where an async proc, or more accurately
+## a ``Future[T]`` gets stuck and
+## never completes. This can happen for various reasons and can cause serious
+## memory leaks. When this occurs it's hard to identify the procedure that is
+## stuck.
+##
+## Thankfully there is a mechanism which tracks the count of each pending future.
+## All you need to do to enable it is compile with ``-d:futureLogging`` and
+## use the ``getFuturesInProgress`` procedure to get the list of pending futures
+## together with the stack traces to the moment of their creation.
+##
+## You may also find it useful to use this
+## `prometheus package <https://github.com/dom96/prometheus>`_ which will log
+## the pending futures into prometheus, allowing you to analyse them via a nice
+## graph.
+##
+##
+##
 ## Limitations/Bugs
 ## ================
 ##
diff --git a/lib/pure/asyncfutures.nim b/lib/pure/asyncfutures.nim
index 5037c8a24..9af72f8b3 100644
--- a/lib/pure/asyncfutures.nim
+++ b/lib/pure/asyncfutures.nim
@@ -15,7 +15,7 @@ type
     error*: ref Exception ## Stored exception
     errorStackTrace*: string
     when not defined(release):
-      stackTrace: string ## For debugging purposes only.
+      stackTrace: seq[StackTraceEntry] ## For debugging purposes only.
       id: int
       fromProc: string
 
@@ -30,6 +30,44 @@ type
 when not defined(release):
   var currentID = 0
 
+const isFutureLoggingEnabled* = defined(futureLogging)
+when isFutureLoggingEnabled:
+  import hashes
+  type
+    FutureInfo* = object
+      stackTrace*: seq[StackTraceEntry]
+      fromProc*: string
+
+  var futuresInProgress {.threadvar.}: Table[FutureInfo, int]
+
+  proc getFuturesInProgress*(): var Table[FutureInfo, int] =
+    return futuresInProgress
+
+  proc hash(s: StackTraceEntry): Hash =
+    result = hash(s.procname) !& hash(s.line) !&
+      hash(s.filename)
+    result = !$result
+
+  proc hash(fi: FutureInfo): Hash =
+    result = hash(fi.stackTrace) !& hash(fi.fromProc)
+    result = !$result
+
+  proc getFutureInfo(fut: FutureBase): FutureInfo =
+    let info = FutureInfo(
+      stackTrace: fut.stackTrace,
+      fromProc: fut.fromProc
+    )
+    return info
+
+  proc logFutureStart(fut: FutureBase) =
+    let info = getFutureInfo(fut)
+    if info notin getFuturesInProgress():
+      getFuturesInProgress()[info] = 0
+    getFuturesInProgress()[info].inc()
+
+  proc logFutureFinish(fut: FutureBase) =
+    getFuturesInProgress()[getFutureInfo(fut)].dec()
+
 var callSoonProc {.threadvar.}: proc (cbproc: proc ()) {.gcsafe.}
 
 proc getCallSoonProc*(): (proc(cbproc: proc ()) {.gcsafe.}) =
@@ -56,7 +94,7 @@ template setupFutureBase(fromProc: string) =
   new(result)
   result.finished = false
   when not defined(release):
-    result.stackTrace = getStackTrace()
+    result.stackTrace = getStackTraceEntries()
     result.id = currentID
     result.fromProc = fromProc
     currentID.inc()
@@ -67,6 +105,7 @@ proc newFuture*[T](fromProc: string = "unspecified"): Future[T] =
   ## Specifying ``fromProc``, which is a string specifying the name of the proc
   ## that this future belongs to, is a good habit as it helps with debugging.
   setupFutureBase(fromProc)
+  when isFutureLoggingEnabled: logFutureStart(result)
 
 proc newFutureVar*[T](fromProc = "unspecified"): FutureVar[T] =
   ## Create a new ``FutureVar``. This Future type is ideally suited for
@@ -75,6 +114,7 @@ proc newFutureVar*[T](fromProc = "unspecified"): FutureVar[T] =
   ## Specifying ``fromProc``, which is a string specifying the name of the proc
   ## that this future belongs to, is a good habit as it helps with debugging.
   result = FutureVar[T](newFuture[T](fromProc))
+  when isFutureLoggingEnabled: logFutureStart(Future[T](result))
 
 proc clean*[T](future: FutureVar[T]) =
   ## Resets the ``finished`` status of ``future``.
@@ -92,7 +132,7 @@ proc checkFinished[T](future: Future[T]) =
       msg.add("\n  Future ID: " & $future.id)
       msg.add("\n  Created in proc: " & future.fromProc)
       msg.add("\n  Stack trace to moment of creation:")
-      msg.add("\n" & indent(future.stackTrace.strip(), 4))
+      msg.add("\n" & indent(($future.stackTrace).strip(), 4))
       when T is string:
         msg.add("\n  Contents (string): ")
         msg.add("\n" & indent(future.value.repr, 4))
@@ -143,6 +183,7 @@ proc complete*[T](future: Future[T], val: T) =
   future.value = val
   future.finished = true
   future.callbacks.call()
+  when isFutureLoggingEnabled: logFutureFinish(future)
 
 proc complete*(future: Future[void]) =
   ## Completes a void ``future``.
@@ -151,6 +192,7 @@ proc complete*(future: Future[void]) =
   assert(future.error == nil)
   future.finished = true
   future.callbacks.call()
+  when isFutureLoggingEnabled: logFutureFinish(future)
 
 proc complete*[T](future: FutureVar[T]) =
   ## Completes a ``FutureVar``.
@@ -159,6 +201,7 @@ proc complete*[T](future: FutureVar[T]) =
   assert(fut.error == nil)
   fut.finished = true
   fut.callbacks.call()
+  when isFutureLoggingEnabled: logFutureFinish(Future[T](future))
 
 proc complete*[T](future: FutureVar[T], val: T) =
   ## Completes a ``FutureVar`` with value ``val``.
@@ -170,6 +213,7 @@ proc complete*[T](future: FutureVar[T], val: T) =
   fut.finished = true
   fut.value = val
   fut.callbacks.call()
+  when isFutureLoggingEnabled: logFutureFinish(future)
 
 proc fail*[T](future: Future[T], error: ref Exception) =
   ## Completes ``future`` with ``error``.
@@ -180,6 +224,7 @@ proc fail*[T](future: Future[T], error: ref Exception) =
   future.errorStackTrace =
     if getStackTrace(error) == "": getStackTrace() else: getStackTrace(error)
   future.callbacks.call()
+  when isFutureLoggingEnabled: logFutureFinish(future)
 
 proc clearCallbacks*(future: FutureBase) =
   future.callbacks.function = nil
diff --git a/lib/system/excpt.nim b/lib/system/excpt.nim
index b421d9650..b59840f77 100644
--- a/lib/system/excpt.nim
+++ b/lib/system/excpt.nim
@@ -445,6 +445,12 @@ proc getStackTraceEntries*(e: ref Exception): seq[StackTraceEntry] =
   else:
     result = move(e.trace)
 
+proc getStackTraceEntries*(): seq[StackTraceEntry] =
+  ## Returns the stack trace entries for the current stack trace.
+  ## This is not yet available for the JS backend.
+  when hasSomeStackTrace:
+    rawWriteStackTrace(result)
+
 const nimCallDepthLimit {.intdefine.} = 2000
 
 proc callDepthLimitReached() {.noinline.} =