Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test NSString with autoreleasepool leak?

Was trying to fix a 300MB memory-leak, and after finding leak-reason;

(Which was calls to NSString's stringFromUTF8String:, from C++ thread (without @autoreleasepool-block wrapper))

I edited the code, to enforce reference-counting (instead of auto-release), something like below:

public func withNSString(
    _ chars: UnsafePointer<Int8>,
    _ callback: (NSString) -> Void
) {
    let result: NSString = NSString(utf8String: chars)!;
    callback(result);
}

As personal policy, with a Unit-Test, like:

import Foundation
import XCTest
@testable import MyApp

class AppTest: XCTestCase {
    func testWithNSString_hasNoMemoryLeak() {
        weak var weakRef: NSString? = nil
        autoreleasepool {
            let chars = ("some data" as NSString).utf8String!;
            withNSString(chars, { strongRef in
                weakRef = strongRef;
                XCTAssertNotNil(weakRef);
            })
            // Checks if reference-counting is used.
            XCTAssertNil(weakRef); // Fails, so no reference-counting. 
        }
        // Checks if autoreleased.
        XCTAssertNil(weakRef); // Fails, OMG! what is this?
    }
}

But now, not even auto-release seems to work anymore (-_- )
Why does last XCTAssertNil call fail?
(In other words, how can I fix memory-leaks?)

like image 633
Top-Master Avatar asked Mar 01 '23 10:03

Top-Master


1 Answers

The problem is that you're using a very short string. It's getting inlined onto the stack, so it's not released until the entire stack frame goes out of scope. If you made the string a little bit longer (2 characters longer), this would behave the way you expect. This is an implementation detail, of course, and could change due to different versions of the compiler, different versions of the OS, different optimization settings, or different architectures.

Keep in mind that testing this kind of thing with static strings of any kind can be tricky, since static strings are placed into the binary. So if the compiler notices that you've indirectly made a pointer to a static string, then it might optimize out the indirection and not release it.

In none of these cases is there a memory leak, though. Your memory leak is more likely in the calling code of withNSString. I would mostly suspect that you're not properly dealing with the bytes passed as chars. We would need to see more about why you think there's a leak to evaluate that. (Foundation also has some small leaks, and Instruments has false positives on leaks, so if you're chasing an allocation that is smaller than 50 bytes and doesn't recur on every operation, you probably are chasing ghosts.)


Note that this is a bit dangerous:

let chars = ("some data" as NSString).utf8String!
withNSString(chars, { strongRef in

The utf8String inner pointer is not promised to live longer than the NSString, and Swift is free to destroy objects after their last reference (which may be before they go out of scope). As the docs note:

This C string is a pointer to a structure inside the string object, which may have a lifetime shorter than the string object and will certainly not have a longer lifetime. Therefore, you should copy the C string if it needs to be stored outside of the memory context in which you use this property.

In this case the object is a constant string, which is in the binary and cannot be destroyed. But in more general cases this is is a classic cause of crashes. I would highly recommend moving away from the NSString interfaces and using String. It offers utf8CString, which returns a proper ContinguousArray, which is much safer.

let chars = "some data".utf8CString
chars.withUnsafeBufferPointer { buffer in
    withNSString(buffer.baseAddress!, { strongRef in
        weakRef = strongRef;
        XCTAssertNotNil(weakRef);
    })
}

withUnsafeBufferPointer ensures that chars cannot be destroyed before the block completes.

You can also ensure the lifetime of the string if needed (this is mostly useful for fixing older code you don't want to rewrite in safer ways):

let string = "some data"

withExtendedLifetime(string) {
    let chars = string.utf8CString
    chars.withUnsafeBufferPointer { buffer in
        withNSString(buffer.baseAddress!, { strongRef in
            weakRef = strongRef;
            XCTAssertNotNil(weakRef);
        })
    }
}
like image 116
Rob Napier Avatar answered Mar 12 '23 15:03

Rob Napier