Skip to content

Component.name optimized: replace String(describing:) with _typeName - #489

Open
ChrisBenua wants to merge 1 commit into
uber:masterfrom
ChrisBenua:name-optimization
Open

ChrisBenua wants to merge 1 commit into
uber:masterfrom
ChrisBenua:name-optimization

Conversation

@ChrisBenua

Copy link
Copy Markdown

Previously, String(describing: self) was used to get fully qualified name (including module name).

But using String(describing: self) to get type name is extremely ineffective (see #486 and swiftlang/swift#77369). In the end String(describing:) calls _typeName(:qualified:) function. Also, if we pass qualified: false, we don't need to do any splitting work

Previous callstack when using String(describing:):

@CLAassistant

CLAassistant commented Jan 31, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@ChrisBenua

Copy link
Copy Markdown
Author

@rudro Hi, Rudro! This PR will significantly improve performance for large apps.

If this PR matches your expectations, I'd appreciate your guidance on how we can proceed with merging the PR.

Thanks in advance!

let parts = fullyQualifiedSelfName.components(separatedBy: ".")
return parts.last ?? fullyQualifiedSelfName
}()
private lazy var name: String = _typeName(type(of: self), qualified: false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is wonderful, but it uses a private API. Up to the maintainers to decide if that's ok

@ChrisBenua ChrisBenua Jan 31, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It's not truly private API. This function is public but not documented.

Moreover, it can be accessed indirectly by calling regular string interpolation. Like "\(type(of: self))"

@Simimi-dot

Copy link
Copy Markdown

We hit exactly this in a large app and can confirm the impact with a profile.

Setup: Xcode 26.4, iPhone 17 Pro simulator (iOS 26.4), Debug build, Time Profiler + os_signpost intervals around the container getters.

Before the patch: building one root-level object that transitively creates ~47 distinct Component subclasses (feature containers plus their assemblies) took 780 ms. Of 675 main-thread samples inside that interval, 616 were in Component.init(parent:), 566 in Component.name, and 532 had swift_conformsToProtocol as the leaf frame. Each first-time Component subclass cost 12–15 ms.

After applying this change locally: the same interval takes 156 ms, and a first-time Component subclass costs ~1 ms. Everything else in the app (screen assemblies are also components) got the same per-type win. No registry lookup failures; the app starts and navigates normally.

Root cause matches the PR description: String(describing: self) on an instance tries TextOutputStreamable, CustomStringConvertible and CustomDebugStringConvertible casts, then goes through Mirror. Each cast is a swift_conformsToProtocol lookup that is cold for every new type, so the cost scales with the number of distinct component classes. Simulator/Debug numbers are inflated relative to device/Release, but the share stays the same.

One note on behavior parity, checked with a small test: for top-level and nested classes _typeName(type(of: self), qualified: false) returns exactly what the old components(separatedBy: ".").last produced (TopLevel, Inner). The only difference is generic component classes, where the old code returned a truncated name (Int> for Generic<Int>) and the new one returns Generic<Int>.

Would be great to get this in — @rudro @alanzeino, is there anything blocking a review?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants