winsmith
winsmith

Reputation: 21572

Memory Leak using URLSessionDataTask and @Published

I have this APIRepresentative class as a singleton EnvironmentObject in my SwiftUI app. The class has @Published var that holds all InsightData structs by ID.

Even in a view that has no dependencies on insightData, repeatedly calling the getInsightData function causes memory usage in my app to rise by about 200Kb or so each time. Over the lifetime of my app, this will cause memory usage to balloon to several gigabytes.

Here's the kicker: The memory leak vanishes when I remove the @Published modifier for insightData. I can then call my function as much as I like, with no increase in memory usage. Any idea why that is the case? I would very much like to keep the @Published property.

import Foundation
import Combine

final class APIRepresentative: ObservableObject {
    private static let baseURLString = "https://apptelemetry.io/api/v1/"

    @Published var insightData: [UUID: InsightDataTransferObject] = [:]

    // More published properties
    // ...
}


extension APIRepresentative {
    func getInsightData(for insight: Insight, in insightGroup: InsightGroup, in app: TelemetryApp, callback: ((Result<InsightDataTransferObject, TransferError>) -> ())? = nil) {
        let timeWindowEndDate = timeWindowEnd ?? Date()
        let timeWindowBeginDate = timeWindowBeginning ?? timeWindowEndDate.addingTimeInterval(-60 * 60 * 24 * 30)

        let url = urlForPath("apps", app.id.uuidString, "insightgroups", insightGroup.id.uuidString, "insights",
                             insight.id.uuidString,
                             Formatter.iso8601noFS.string(from: timeWindowBeginDate),
                             Formatter.iso8601noFS.string(from: timeWindowEndDate)
        )

        let request = self.authenticatedURLRequest(for: url, httpMethod: "GET")
        URLSession.shared.dataTask(with: request) { data, response, error in
            if let data = data {
                let decoded = try! JSONDecoder.telemetryDecoder.decode(InsightDataTransferObject.self, from: data)

                DispatchQueue.main.async { [weak self] in
                    guard let self = self else {
                        print("Self is gone")
                        return
                    }

                    var newInsightData = self.insightData
                    newInsightData[decoded.id] = decoded

                    self.insightData = newInsightData
                }
            }
        }.resume()
    }
}

// more retrieval functions for the other published properties
// ...

Here's the full file but I'm pretty sure I included all relevant parts in this whittled down version

Upvotes: 3

Views: 454

Answers (1)

Rob Napier
Rob Napier

Reputation: 299275

First, I would assume that memory would grow given this line:

        self.insightData[decoded.id] = decoded

You're storing new values for every download, and don't seem to ever release them unless decoded.id repeats. That's not a leak; that's just storing more data in memory.

That said, if you're testing this with your while loop, you should expect substantial memory growth because you never drain the autorelease pool. The autorelease pool is drained when the current run loop cycle completes, but this while loop never ends. So you'd want to create a new pool:

while true {
    @autoreleasepool { 
        api.getInsightData(for: insight, in: insightGroup, in: app)
        sleep(1)
    }
}

Upvotes: 1

Related Questions