Skip to content

CacheableNetworkOperation: avoid unnecessarily creating operations for cache hits#2135

Merged
NachoSoto merged 1 commit into
mainfrom
operation-avoid-creation
Dec 21, 2022
Merged

CacheableNetworkOperation: avoid unnecessarily creating operations for cache hits#2135
NachoSoto merged 1 commit into
mainfrom
operation-avoid-creation

Conversation

@NachoSoto

@NachoSoto NachoSoto commented Dec 12, 2022

Copy link
Copy Markdown
Contributor

Changes:

  • Created CacheableNetworkOperationFactory to abstract operation and cache key creation
  • Instead of always creating an operation, we create a factory first, which pre-computes the cache key
  • This cache key is used to determine if it's a cache hit, as well as for the subsequently created operation

I'm still investigating a leak (#2105), and I suspect it's coming from us creating and storing completion blocks, and those staying in memory beyond the lifetime of the operation.
This refactor and performance improvement helps us avoid get more things in memory than we need to.

See #2138 for how this is tested.

@NachoSoto NachoSoto added the perf label Dec 12, 2022
@NachoSoto NachoSoto requested a review from a team December 12, 2022 20:26
@NachoSoto NachoSoto force-pushed the operation-avoid-creation branch from 5e851df to cd79981 Compare December 12, 2022 20:37
NachoSoto added a commit that referenced this pull request Dec 12, 2022
…llbacks

See also #2135.
I'm looking into the leak in #2105, and I have a theory that it's due to callbacks being stored in this cache, but sometimes not being dequeued, so leaking all references in them.

This assertion will only run in RC tests, and will ensure that this doesn't happen.
NachoSoto added a commit that referenced this pull request Dec 13, 2022
…llbacks (#2137)

See also #2135.
I'm looking into the leak in #2105, and I have a theory that it's due to
callbacks being stored in this cache, but sometimes not being dequeued,
so leaking all references in them.

This assertion will only run in RC tests, and will ensure that this
doesn't happen.
…for cache hits

- Created `CacheableNetworkOperationFactory` to abstract operation and cache key creation
- Instead of always creating an operation, we create a factory first, which pre-computes the cache key
- This cache key is used to determine if it's a cache hit, as well as for the subsequently created operation

I'm still investigating a leak (#2105), and I suspect it's coming from us creating and storing completion blocks, and those staying in memory beyond the lifetime of the operation.
This refactor and performance improvement helps us avoid get more things in memory than we need to.
@NachoSoto NachoSoto force-pushed the operation-avoid-creation branch from cd79981 to beaf492 Compare December 13, 2022 20:55
NachoSoto added a commit that referenced this pull request Dec 13, 2022
…mpletion

Looking into the leak in #2135, I realized that we have an operation which doesn't call `completion`, we would be leaking the completion block in the cache.
This ensures that it doesn't happen in tests. In production, we won't risk crashing.

@tonidero tonidero left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative could be to have a method in the cache that accepts an operation type and cacheKey and tells you if it exists in the cache already, and we can create the operation or not depending on that. Seems simpler to me than having all operations require a factory to calculate the cache key beforehand, but it might be less extensible than this... So I'm ok with this.


let operation = GetCustomerInfoOperation(configuration: config,
customerInfoCallbackCache: self.customerInfoCallbackCache)
let factory = GetCustomerInfoOperation.createFactory(configuration: config,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Maybe rename to operationFactory? It's only used in the local scope so it's NABD though. Same in the other operations.

source: T.Type,
completion: @escaping Completion) {
self.cacheKey = cacheKey
self.source = T.self

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The source parameter in the constructor seems unused right now. Probably meant to pass it in here? Though if it works with this, we might want to remove the parameter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used by callbacks(ofType type: NetworkOperation.Type).

final class CacheableNetworkOperationFactory<T: CacheableNetworkOperation> {

let individualizedCacheKeyPart: String
let creator: (_ cacheKey: String) -> T

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Maybe rename to creatorBlock?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swift's guidelines say to not include typed in variables names.

@NachoSoto

NachoSoto commented Dec 21, 2022

Copy link
Copy Markdown
Contributor Author

That was my first approach, but it wasn't possible to abstract that with a dynamic Self type. I forget the specifics cause it's been a while, but I remember I couldn't make it work with operations being Obj-C types.

Seems simpler to me than having all operations require a factory to calculate the cache key beforehand,

For sure, I didn't want to have to create a separate type either :/

@NachoSoto NachoSoto merged commit 1234c4e into main Dec 21, 2022
@NachoSoto NachoSoto deleted the operation-avoid-creation branch December 21, 2022 16:13
NachoSoto added a commit that referenced this pull request Dec 21, 2022
…mpletion (#2138)

Looking into the leak in #2135, I realized that we have an operation
which doesn't call `completion`, we would be leaking the completion
block in the cache.
This ensures that it doesn't happen in tests. In production, we won't
risk crashing.
NachoSoto pushed a commit that referenced this pull request Dec 21, 2022
**This is an automatic release.**

### Bugfixes
* `ErrorUtils.purchasesError(withUntypedError:)`: handle `PublicError`s
(#2165) via NachoSoto (@NachoSoto)
* Fixed race condition finishing `SK1` transactions (#2148) via
NachoSoto (@NachoSoto)
* `IntroEligibilityStatus`: added `CustomStringConvertible`
implementation (#2182) via NachoSoto (@NachoSoto)
* `BundleSandboxEnvironmentDetector`: fixed logic for `macOS` (#2176)
via NachoSoto (@NachoSoto)
* Fixed `AttributionFetcher.adServicesToken` hanging when called in
simulator (#2157) via NachoSoto (@NachoSoto)
### Other Changes
* `Purchase Tester`: added ability to purchase products directly with
`StoreKit` (#2172) via NachoSoto (@NachoSoto)
* `DNSChecker`: simplified `NetworkError` initialization (#2166) via
NachoSoto (@NachoSoto)
* `Purchases` initialization: refactor to avoid multiple concurrent
instances in memory (#2180) via NachoSoto (@NachoSoto)
* `Purchase Tester`: added button to clear messages on logger view
(#2179) via NachoSoto (@NachoSoto)
* `NetworkOperation`: added assertion to ensure that subclasses call
completion (#2138) via NachoSoto (@NachoSoto)
* `CacheableNetworkOperation`: avoid unnecessarily creating operations
for cache hits (#2135) via NachoSoto (@NachoSoto)
* `PurchaseTester`: fixed `macOS` support (#2175) via NachoSoto
(@NachoSoto)
* `IntroEligibilityCalculator`: added log including `AppleReceipt`
(#2181) via NachoSoto (@NachoSoto)
* `Purchase Tester`: fixed scene manifest (#2170) via NachoSoto
(@NachoSoto)
* `HTTPClientTests`: refactored to use `waitUntil` (#2168) via NachoSoto
(@NachoSoto)
* `Integration Tests`: split up tests in smaller files (#2158) via
NachoSoto (@NachoSoto)
* `StoreKitRequestFetcher`: removed unnecessary dispatch (#2152) via
NachoSoto (@NachoSoto)
* `Purchase Tester`: added companion `watchOS` app (#2140) via NachoSoto
(@NachoSoto)
* `StoreKit1Wrapper`: added warning if receiving too many updated
transactions (#2117) via NachoSoto (@NachoSoto)
* `StoreKitTestHelpers`: cleaned up unnecessary log (#2177) via
NachoSoto (@NachoSoto)
* `TrialOrIntroPriceEligibilityCheckerSK1Tests`: use `waitUntilValue`
(#2173) via NachoSoto (@NachoSoto)
* `DNSChecker`: added log with resolved host (#2167) via NachoSoto
(@NachoSoto)
* `MagicWeatherSwiftUI`: updated `README` to point to workspace (#2142)
via NachoSoto (@NachoSoto)
* `Purchase Tester`: fixed `.storekit` config file reference (#2171) via
NachoSoto (@NachoSoto)
* `Purchase Tester`: fixed error alerts (#2169) via NachoSoto
(@NachoSoto)
* `CI`: don't make releases until `release-checks` pass (#2162) via
NachoSoto (@NachoSoto)
* `Fastfile`: changed `match` to `readonly` (#2161) via NachoSoto
(@NachoSoto)
@vegaro vegaro added pr:other and removed pr:perf labels Sep 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants