-
Notifications
You must be signed in to change notification settings - Fork 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
[Perf improvement] Implementing memoization for sanitizeNamespace and replaceGenericParameters funcs. #273
Conversation
static func sanitizeNamespace(ofTypeName typeName: String) -> String { | ||
var str = typeName | ||
|
||
if let sanitized = sanitizedNamespacesCache[typeName] { |
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 moved the exact implementation from the String
extension to here and added the memoization calls here and in line 26.
if balance == 0 { | ||
return String(typeName[..<start]) + | ||
replacement + | ||
replaceGenericParameters(inTypeName: String(typeName[current...]), |
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.
We still call into the memoized func to potentially leverage from memoized subtypes.
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.
Smart!
private static var sanitizedNamespacesCache: [String: String] = [:] | ||
|
||
// swiftlint:disable force_try | ||
private static let sanitizeNamespaceRegex = try! NSRegularExpression(pattern: "\\.\\(unknown context at ..........\\)") |
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.
We thought it'd be better to keep this regex as a constant instead of recreating it every time sanitizeNamespace
is called.
static func guardType(value: Any, namespacedPrefixes: [String], inspectionCall: String) throws { | ||
|
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.
@nalexn do you have a preference regarding the style of tabs / spaces / empty lines?
I can revert those to the original state if you prefer. Let me know.
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.
Nah, I like this more. I've googled how to tweak the Xcode settings to not insert spaces
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.
Sounds good! I like this better too!
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.
@nalexn in case it's helpful, the Airbnb style guide has a script that you can run that enables various editor settings like this one that we find helpful in our large project.
That is a substantial performance improvement, good job! Before merging this in, I suggest an additional slight refactor of |
Thank you for the super quick turnaround, @nalexn! 🙏 I left the new pattern disabled for now with a comment. Let me know what you think of the approach. |
|
||
private static let sanitizeNamespaceRegex = { | ||
// swiftlint:disable force_try | ||
try! NSRegularExpression(pattern: sanitizeNamespacePatterns.joined(separator: "|")) |
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 believe having multiple patterns joined by an OR operator (|
) might be faster than running the stringByReplacingMatches
for each of them. Since we're not looking to replace different patterns with different strings, this seemed like the most performant approach.
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.
This makes sense, though I wonder if we want to wrap patterns in (
and )
before OR'ing them together to ensure that each pattern is considered atomically?
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 believe the fact that we add an |
already treats both sides of it as individual patterns to be matched. But I don't think it's an issue adding them, and it makes it more clear too, so I added them. 👍
// This pattern may be helpful to solve issue #268. | ||
// It will remain disabled until it is confirmed. | ||
// https://github.com/nalexn/ViewInspector/issues/268 | ||
//"\\(extension in [a-zA-Z0-9]*\\)\\:", |
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.
static func guardType(value: Any, namespacedPrefixes: [String], inspectionCall: String) throws { | ||
|
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.
Sounds good! I like this better too!
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.
Great work @rafael-assis and thanks for the quick review @nalexn ! 🙏
private static var replacedGenericParametersCache: [String : [String : String]] = [:] | ||
private static var sanitizedNamespacesCache: [String: String] = [:] |
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.
This strategy for caching is not thread-safe. We think that's OK (i.e. thread safety isn't necessary) but I wanted to flag it to @nalexn out of an abundance of caution 👍
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.
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.
Yeah, no need to solve a problem that may not even exist. I'm 99% sure it should be fine without synchronization, we can add it later if needed
/// - Parameter typeName: The raw type name. (e.g. ``SomeTypeName.(unknown context at $138b3290c).SomePropertyName``) | ||
/// - Returns: The sanitized type name. (e.g. ``SomeTypeName.SomePropertyName``) |
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.
/// - Parameter typeName: The raw type name. (e.g. ``SomeTypeName.(unknown context at $138b3290c).SomePropertyName``) | |
/// - Returns: The sanitized type name. (e.g. ``SomeTypeName.SomePropertyName``) | |
/// - Parameter typeName: The raw type name. (e.g. `SomeTypeName.(unknown context at $138b3290c).SomePropertyName`) | |
/// - Returns: The sanitized type name. (e.g. `SomeTypeName.SomePropertyName`) |
Nit: My understanding is that the double backticks are used to create links to other types when documentation comments are displayed in Xcode. Since these are example types, I think single backticks are more appropriate.
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.
Makes sense! I'll update to single backticks! 👍
/// - typeName: The original type name. (e.g. ``SomeTypeName<SomeGenericTypeParameter>``) | ||
/// - replacement: The string to replace the generic parameters with. (e.g. "<EmptyView>") | ||
/// - Returns: The type name with its generic parameters replaced. (e.g. ``SomeTypeName<EmptyView>``) |
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.
/// - typeName: The original type name. (e.g. ``SomeTypeName<SomeGenericTypeParameter>``) | |
/// - replacement: The string to replace the generic parameters with. (e.g. "<EmptyView>") | |
/// - Returns: The type name with its generic parameters replaced. (e.g. ``SomeTypeName<EmptyView>``) | |
/// - typeName: The original type name. (e.g. `SomeTypeName<SomeGenericTypeParameter>`) | |
/// - replacement: The string to replace the generic parameters with. (e.g. "<EmptyView>") | |
/// - Returns: The type name with its generic parameters replaced. (e.g. `SomeTypeName<EmptyView>`) |
Nit: See similar comment above for rationale.
if balance == 0 { | ||
return String(typeName[..<start]) + | ||
replacement + | ||
replaceGenericParameters(inTypeName: String(typeName[current...]), |
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.
Smart!
|
||
private static let sanitizeNamespaceRegex = { | ||
// swiftlint:disable force_try | ||
try! NSRegularExpression(pattern: sanitizeNamespacePatterns.joined(separator: "|")) |
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.
This makes sense, though I wonder if we want to wrap patterns in (
and )
before OR'ing them together to ensure that each pattern is considered atomically?
static func guardType(value: Any, namespacedPrefixes: [String], inspectionCall: String) throws { | ||
|
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.
@nalexn in case it's helpful, the Airbnb style guide has a script that you can run that enables various editor settings like this one that we find helpful in our large project.
Hi @nalexn. Do you think the changes I made fully addressed your suggestions? Thank you! |
Merged. Thank you @rafael-assis and @bachand ! |
Hi @nalexn. Picking up were we left off on #263, we have identified another opportunity for performance improvements in ViewInspector. @bachand and I discussed it and we'd love to get your thoughts on it.
Thank you so much for your time! 🙏
Context: performance improvements under larger SwiftUI View hierarchies
We have profiled and traced some of our slowest tests in Instruments showed that the 2 funcs defined in the
String
extension were the top offenders in terms of running time.String.replacingGenericParameters(_:)
String.sanitizingNamespace()
The combined time of all calls to both of these functions accounted for almost 50% of the entire test running time:
It was already known that these 2 functions are called thousands of times for a single test case in our codebase.
This fact motivated an investigation on how many times these 2 funcs are called for the exact same input in order to inform a potential decision to memoize the calls.
A histogram of the calls to
sanitizingNamespace
for each input was built from a single feature integration test run.We found that for some types, the func
sanitizingNamespace
thousands of times for them. For example:Changes
It was then clear that caching the output of these expensive string manipulation operations would be worth it.
This is exactly what theses changes do.
It's a simple memoization which stores the computed value to be returned directly instead of recalculating them repeatedly.
Testing
Local runs of the the test used to profile ViewInspector's execution time showed a substantial improvement of running time. During the 2 Unit Tests CI runs of the changes in this PR, we noticed an improvement of about 86% (19 seconds). That particular test took ~22 seconds to run before the optimizations and ~3 seconds after the optimizations:
Although the unit tests in ViewInspector are more lightweight compared to the tests we run at Airbnb, we could also see a good improvement of the overall running time of all tests of the library:
Please review:
@nalexn
@michael-bachand