> Does anybody actually use address-sanitizers or valgrind or heaptrack and 
> eradicates potential memory leaks to the point that nothing shows up while 
> they run?

Yes, I use sanitizers when developing:

  * Weave
  * nim-taskpools
  * Constantine and Constantine's threadpool



However:

  * it's incompatible with Nim `refc` (the conservative stack scanning will 
trigger an alert when reading unallocated stack memory)
  * it needs `useMalloc` to avoid Nim allocator and provides precise heap 
checks.
  * it needs low-level understanding of implementation details when globals are 
involved.



In your case you call asyncdispatch `getGlobalDispatcher` and it's possible 
that global ref are not freed at the end of the program. The C codegen should 
be checked here, in NimMain, PreMainInner and the exit procs.

In your case the leak points to 
<https://github.com/nim-lang/Nim/blob/v2.0.2/lib/pure/ioselects/ioselectors_epoll.nim#L101>
    
    
    proc newSelector*[T](): Selector[T] =
      proc initialNumFD(): int {.inline.} =
        when defined(nuttx):
          result = NEPOLL_MAX
        else:
          result = 1024
      # Retrieve the maximum fd count (for current OS) via getrlimit()
      var maxFD = maxDescriptors()
      doAssert(maxFD > 0)
      # Start with a reasonable size, checkFd() will grow this on demand
      let numFD = initialNumFD()
      
      var epollFD = epoll_create1(O_CLOEXEC)
      if epollFD < 0:
        raiseOSError(osLastError())
      
      when hasThreadSupport:
        result = cast[Selector[T]](allocShared0(sizeof(SelectorImpl[T])))
        result.epollFD = epollFD
        result.maxFD = maxFD
        result.numFD = numFD
        result.fds = allocSharedArray[SelectorKey[T]](numFD)
      else:
        result = Selector[T]()
        result.epollFD = epollFD
        result.maxFD = maxFD
        result.numFD = numFD
        result.fds = newSeq[SelectorKey[T]](numFD)
    
    
    Run

This is indeed instantiated from asyncdispatch: 
<https://github.com/nim-lang/Nim/blob/v2.0.2/lib/pure/asyncdispatch.nim#L1207-L1209>
    
    
    proc newDispatcher*(): owned(PDispatcher) =
        new result
        result.selector = newSelector[AsyncData]()
    
    
    Run

And related to the global dispatcher: 
<https://github.com/nim-lang/Nim/blob/v2.0.2/lib/pure/asyncdispatch.nim#L1228-L1239>
    
    
    proc setGlobalDispatcher*(disp: owned PDispatcher) =
        if not gDisp.isNil:
          assert gDisp.callbacks.len == 0
        gDisp = disp
        initCallSoonProc()
      
      proc getGlobalDispatcher*(): PDispatcher =
        if gDisp.isNil:
          setGlobalDispatcher(newDispatcher())
          when defined(nuttx):
            addFinalyzer()
        result = gDisp
    
    
    Run

And if we look further how the global dispatcher is defined:

<https://github.com/nim-lang/Nim/blob/v2.0.2/lib/pure/asyncdispatch.nim#L356>
    
    
    var gDisp{.threadvar.}: owned PDispatcher ## Global dispatcher
    
    
    Run

It's a thread-local variable.

> Or rather, gut feeling says that this is address sanitizer not seeing the 
> destruction of the global dispatcher and thus falsely claiming a memory leak.

It is a memory leak, if the gDisp thread is joined, memory becomes unclaimable.

> 2 If it is a leak, what is supposed to be done here?

Raising a bug.

I'm not too sure what's the correct fix here.

Maybe having a magic `destroyThreadLocalVariables` that can be called before 
`joinThread` or exiting a program.

Reply via email to