apple / swift-async-dns-resolver

A Swift library for asynchronous DNS requests, wrapping c-ares with Swift-friendly APIs and data structures.
Apache License 2.0
82 stars 12 forks source link

CPU utilization when using CAresDNSResolver #15

Closed dieb closed 5 months ago

dieb commented 5 months ago

Hi there,

I'm testing using the lib to periodically resolve a list of domains and it works great. I'm using something like this:

class Resolver {
  private let resolver: AsyncDNSResolver

  init() {
    do {
      var options = CAresDNSResolver.Options.default
      options.timeoutMillis = TimeoutMillis
      options.servers = dnsServers
      options.rotate = true
      resolver = try AsyncDNSResolver(options: options)
    } catch {
      // Use a fallback
    }
  }

  ...

  func resolve(hostname: String) async -> [String] {
    let aRecords = try? await resolver.queryA(name: hostname)
    let aaaaRecords = try? await resolver.queryAAAA(name: hostname)
    ...
  }
}

During testing I noticed CPU utilization resolving domains is light and reasonable, though I noticed some utilization while idling. CPU idles at 1-2% and goes up to 5% while resolving a list of 30 domains concurrently.

Peeking the code it looks like the 10ms polling here could explain the 1-2% idling, correct? I'm assuming the TODO mentioning the NIO EventLoop could help.

I'm looking for a way to improve the idling 1-2% CPU utilization. Since my traffic pattern is spiky and I'm driving it, I'm thinking it may be possible to improve by getting a new resolver only when needed, then disposing it.

Here are some things I tried with no success:

  1. Get rid of the resolver after using it, remove references hoping it'd be destroyed and shutdown. Didn't work and CPU profiling instrument still shows the polling activity in c-ares.
  2. I wanted to try to increase the polling time, perhaps during no usage periods. I might try this in a fork but not sure if it's a good idea.

Any thoughts / recommendations?

Thanks!

PS: Thank you for swift-async-dns-resolver!

Environment:

uname -a
Darwin genesis.local 23.2.0 Darwin Kernel Version 23.2.0: Wed Nov 15 21:53:34 PST 2023; root:xnu-10002.61.3~2/RELEASE_ARM64_T8103 arm64

swift --version
swift-driver version: 1.87.3 Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5)
Target: arm64-apple-macosx14.0
yim-lee commented 5 months ago

Hi @dieb, I wonder if https://github.com/apple/swift-async-dns-resolver/pull/16 might help issue (1)?

Increasing polling time (i.e., issue/option (2)) would slow down the resolver I think, at least that's what I observed in tests locally.

dieb commented 5 months ago

Thanks! Gave #16 a try but still same issue. I added some prints to my code and to your code and looks like QueryProcessor deinit isn't running. Double-checked my deinit is definitely running.

Edit: checked further and CAresDNSResolver and Ares are definitely getting deinit'ed. It may be just QueryProcessor.

dieb commented 5 months ago

On top of #16 : looks like the modification below works to allow QueryProcessor to deinit, but I'll admit I'm not confident enough in Swift to know whether this is good.

        private func schedule() {
            self.pollingTask = Task { [weak self] in
                try await Task.sleep(nanoseconds: self?.pollIntervalNanos ?? 0)
                await self?.poll()
            }
        }

Testing locally seems to allow cleanup on deref, and when I keep a ref polling continues as expected.

yim-lee commented 5 months ago

I applied your change with minor tweaks. https://github.com/apple/swift-async-dns-resolver/pull/16/commits/2a38874648327439179aa160958ba2a380a39566

Can you please give it a try?

dieb commented 5 months ago

Thanks! It works.

Screenshot 2024-02-06 at 11 26 15 PM

yim-lee commented 5 months ago

Awesome! Thanks for checking.

dieb commented 5 months ago

Much appreciated!

yim-lee commented 5 months ago

Thanks @dieb. Patch: https://github.com/apple/swift-async-dns-resolver/releases/tag/0.1.3