Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Combine - CFString (Storage) memory leak

Im seeing some memory leaks when using the memory graph debugger in xcode. The Backtrace is not linking directly to any of my code, but guessing by the trace its save to assume that its related to combine and some DataTaskPublisher.

enter image description here

Next I checked inside Instruments, where I also see some memory leaks. All leaks mention "specialized static UIApplicationDelegate.main()" inside the stack trace, but its not really linking to something that can cause a memory leak.

enter image description here

Removing the the ViewModel, that is responsible for loading Data from an API, gets rid of the leaks. The memory graph debugger was showing a dataTaskPublisher, so this kinda makes sense.

import Foundation
import Combine

enum API {
    static func games() -> AnyPublisher<[GameResult], Error> {
        let requestHeaderGames = gamesRequest()
        
        return URLSession.shared.dataTaskPublisher(for: requestHeaderGames)
            .map(\.data)
            .decode(type: [GameResult].self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
    
    private static func gamesRequest() -> URLRequest {
        let url = URL(string: "http://localhost:8080/api/games")!
        var requestHeader = URLRequest.init(url: url)
        requestHeader.httpBody =
            "filter ..."
            .data(using: .utf8, allowLossyConversion: false)

        requestHeader.httpMethod = "POST"
        requestHeader.setValue("application/json", forHTTPHeaderField: "Accept")

        return requestHeader
    }
}

struct GameResult: Decodable, Identifiable, Equatable, Hashable {
    let id: Int
    // ...
}

final class ViewModel: ObservableObject {
    @Published private(set) var games: [GameResult] = []
    
    private var subscriptions = Set<AnyCancellable>()
    
    public func unsubscribe() -> Void {
        subscriptions.forEach {
            $0.cancel()
        }
        subscriptions.removeAll()
    }
    
    func load() -> Void {
        API.games()
            .sink(receiveCompletion: { _ in }, receiveValue: { [weak self] results in
                self?.games = results
            })
            .store(in: &subscriptions)
    }
}

Im already put much time into figuring out whats causing this leak, but im more confused than anything else. CFString is nothing that im using. Also I cant find out why my code is seemingly causing this leak. Is there something that im just simply missing, or could someone help me by giving me some advise how to go about this problem?

like image 215
KevinP Avatar asked Nov 06 '22 04:11

KevinP


1 Answers

The first image you posted seems to indicate it's an issue with the Decoder publisher. I'd try a couple different scenarios to see if we can isolate the problem with the decoding:

enum API {

    static var fakeGameData: Data {
        let encoder = JSONEncoder()
        return try! encoder.encode(fakeGames)
    }

    static var fakeGames: [GameResult] {
        return [GameResult(id: 0), GameResult(id: 2), GameResult(id: 3)]
    }

    static func gamesFromData() -> AnyPublisher<[GameResult], Error> {
        return Just(fakeGameData)
            .decode(type: [GameResult].self, decoder: JSONDecoder())
            .mapError({ $0 as Error })
            .eraseToAnyPublisher()
    }

    static func gamesFromArray() -> AnyPublisher<[GameResult], Error> {
        return Just(fakeGames)
            .mapError({ $0 as Error })
            .eraseToAnyPublisher()
    }
}

See what happens if you subscribe to API.gamesFromData(). If you still see a leak, then it may be an issue with the decoder. If not, then it's more likely that you have an issue with the dataTaskPublisher.

Then, see what happens if you subscribe to API.gamesFromArray(). If you still see a leak, then you know the issue isn't the Decoder publisher.

like image 133
Rob C Avatar answered Nov 15 '22 18:11

Rob C