-
Notifications
You must be signed in to change notification settings - Fork 163
Fix ResolvedTopicReference memory leak #153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| /* | ||
| This source file is part of the Swift.org open source project | ||
|
|
||
| Copyright (c) 2022 Apple Inc. and the Swift project authors | ||
| Licensed under Apache License v2.0 with Runtime Library Exception | ||
|
|
||
| See https://swift.org/LICENSE.txt for license information | ||
| See https://swift.org/CONTRIBUTORS.txt for Swift project authors | ||
| */ | ||
|
|
||
| /// A wrapper that provides weak ownership of a value. | ||
| struct Weak<T: AnyObject> { | ||
| weak var value: T? | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| /* | ||
| This source file is part of the Swift.org open source project | ||
|
|
||
| Copyright (c) 2021 Apple Inc. and the Swift project authors | ||
| Copyright (c) 2021-2022 Apple Inc. and the Swift project authors | ||
| Licensed under Apache License v2.0 with Runtime Library Exception | ||
|
|
||
| See https://swift.org/LICENSE.txt for license information | ||
|
|
@@ -2159,11 +2159,35 @@ let expected = """ | |
| XCTAssertNotNil(try context.entity(with: referenceForPath("/Collisions/SharedStruct/iOSVar"))) | ||
| } | ||
|
|
||
| func testContextDoesNotLeakReferences() throws { | ||
| #if os(Linux) || os(Android) | ||
| throw XCTSkip("'autoreleasepool' is not supported on this platform so this test can not be run reliably.") | ||
| #endif | ||
|
|
||
| // Verify there is no pool bucket for the bundle we're about to test | ||
| XCTAssertNil(ResolvedTopicReference.sharedPool.sync({ $0[#function] })) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This just duplicates the below test but without the addition of holding onto the |
||
|
|
||
| try autoreleasepool { | ||
| let (url, _, _) = try testBundleAndContext(copying: "TestBundle", excludingPaths: [], codeListings: [:], configureBundle: { rootURL in | ||
| let infoPlistURL = rootURL.appendingPathComponent("Info.plist", isDirectory: false) | ||
| try! String(contentsOf: infoPlistURL) | ||
| .replacingOccurrences(of: "org.swift.docc.example", with: #function) | ||
| .write(to: infoPlistURL, atomically: true, encoding: .utf8) | ||
| }) | ||
|
|
||
| try FileManager.default.removeItem(at: url) | ||
| } | ||
|
|
||
|
|
||
| // Verify there is no pool bucket for the bundle after we've released all references to it | ||
| XCTAssertNil(ResolvedTopicReference.sharedPool.sync({ $0[#function] })) | ||
| } | ||
|
|
||
| func testContextCachesReferences() throws { | ||
| // Verify there is no pool bucket for the bundle we're about to test | ||
| XCTAssertNil(ResolvedTopicReference.sharedPool.sync({ $0[#function] })) | ||
|
|
||
| let (url, _, _) = try testBundleAndContext(copying: "TestBundle", excludingPaths: [], codeListings: [:], configureBundle: { rootURL in | ||
| let (url, bundle, context) = try testBundleAndContext(copying: "TestBundle", excludingPaths: [], codeListings: [:], configureBundle: { rootURL in | ||
| let infoPlistURL = rootURL.appendingPathComponent("Info.plist", isDirectory: false) | ||
| try! String(contentsOf: infoPlistURL) | ||
| .replacingOccurrences(of: "org.swift.docc.example", with: #function) | ||
|
|
@@ -2195,6 +2219,11 @@ let expected = """ | |
| // Verify creating a new reference added to the ones loaded with the context | ||
| XCTAssertNotEqual(beforeCount, ResolvedTopicReference.sharedPool.sync({ $0[#function]!.count })) | ||
|
|
||
| // Access the bundle and the context so that they're kept in memory and the resolved topic | ||
| // references are not released from the cache | ||
| XCTAssertEqual(bundle.identifier, #function) | ||
| XCTAssertEqual(context.knownPages.count, 39) | ||
|
|
||
| // Purge the pool | ||
| ResolvedTopicReference.purgePool(for: #function) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| /* | ||
| This source file is part of the Swift.org open source project | ||
|
|
||
| Copyright (c) 2021 Apple Inc. and the Swift project authors | ||
| Copyright (c) 2021-2022 Apple Inc. and the Swift project authors | ||
| Licensed under Apache License v2.0 with Runtime Library Exception | ||
|
|
||
| See https://swift.org/LICENSE.txt for license information | ||
|
|
@@ -78,4 +78,49 @@ class ResolvedTopicReferenceTests: XCTestCase { | |
| _ = topicReference.absoluteString | ||
| } | ||
| } | ||
|
|
||
| func testResolvedTopicReferenceDoesNotLeakMemory() throws { | ||
| #if os(Linux) || os(Android) | ||
| throw XCTSkip("'autoreleasepool' is not supported on this platform so this test can not be run reliably.") | ||
| #endif | ||
|
|
||
| // Use the name of the test as a unique bundle identifier for this test. Otherwise, when | ||
| // these tests are run sequentially, this test could fail because of stale topic references | ||
| // that still exist in other pools of the cache. | ||
| // | ||
| // This isn't a concern when running the tests in parallel because each test is run in its | ||
| // own process. | ||
| let bundleIdentifier = #function | ||
|
|
||
| autoreleasepool { | ||
| var topicReference: ResolvedTopicReference? = ResolvedTopicReference( | ||
| bundleIdentifier: bundleIdentifier, | ||
| path: "/documentation/path/sub-path", | ||
| fragment: nil, | ||
| sourceLanguage: .swift | ||
| ) | ||
|
|
||
| ResolvedTopicReference.sharedPool.sync { sharedPool in | ||
| XCTAssertNotNil(sharedPool[bundleIdentifier]) | ||
| XCTAssertEqual( | ||
| sharedPool[bundleIdentifier]?.values.first?.value?.path, | ||
| "/documentation/path/sub-path" | ||
| ) | ||
| } | ||
|
|
||
| topicReference = nil | ||
|
|
||
| // The below assertion just adds a use of the 'topicReference' variable. Otherwise | ||
| // we end up with a warning: | ||
| // | ||
| // Variable 'topicReference' was written to, but never read | ||
|
|
||
| XCTAssertNil(topicReference) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this will always hold true, regardless of the changes introduced by this PR. What are we actually trying to test here? That the underlying storage is no longer being referenced?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just there to add a use of |
||
| } | ||
|
|
||
| ResolvedTopicReference.sharedPool.sync { sharedPool in | ||
| XCTAssertNil(sharedPool[bundleIdentifier]?.values.first?.value) | ||
| XCTAssertNil(sharedPool[bundleIdentifier]) | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any mechanism that drains the shared pool or otherwise removes unused elements? If not, I think we'll still end up with a shared pool full of empty weak boxes.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was worried about that too, but the added logic in the
deinitof theStorageshould handle this.The test that asserts on the
nilcache pool after converting a bundle should cover this.