Skip to content

Conversation

chloe-yeo
Copy link
Contributor

@chloe-yeo chloe-yeo commented Sep 24, 2025

Proposal change to reflect minor implementation change due to performance concerns of previously-proposed version of ProgressManager API.

Updated implementation found here: #1468

@chloe-yeo
Copy link
Contributor Author

@swift-ci please test

We initially allowed the full flexibility of allowing developers to declare `ProgressManager.Property` types to be of any type, including structs. However, we realized that this has a severely negative impact on performance of the API. Thus, for now, we allow developers to only declare `ProgressManager.Property` with only certain `Value` and `Summary` types.

### Introduce `withProperties` closure as an entry point to atomically mutate multiple properties
We initially considered introducing a withProperties closure as an entry point to atomically mutate multiple properties. This approach would have used a dedicated ProgressManager.Values struct with `@dynamicMemberLookup` to provide a unified interface for modifying all properties within a single atomic operation.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
We initially considered introducing a withProperties closure as an entry point to atomically mutate multiple properties. This approach would have used a dedicated ProgressManager.Values struct with `@dynamicMemberLookup` to provide a unified interface for modifying all properties within a single atomic operation.
We initially considered introducing a `withProperties` closure as an entry point to atomically mutate multiple properties. This approach would have used a dedicated `ProgressManager.Values` struct with `@dynamicMemberLookup` to provide a unified interface for modifying all properties within a single atomic operation.

We initially considered introducing a withProperties closure as an entry point to atomically mutate multiple properties. This approach would have used a dedicated ProgressManager.Values struct with `@dynamicMemberLookup` to provide a unified interface for modifying all properties within a single atomic operation.

However, this design had several drawbacks:
- Limited atomicity: While it allowed atomic mutation of `totalCount` and `completedCount`, it could not achieve the same atomicity for additional properties due to their differences in backing storage
Copy link
Contributor

@itingliu itingliu Sep 24, 2025

Choose a reason for hiding this comment

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

I would add a few sentences here about why backing storage (an implementation detail) would affect this change of design. Why does it matter whether they're all stored into one single storage or multiple separate ones?

Either way, is it correct to say that if we are to enforce a whole atomicity for all of the additional properties, we would have to synchronize the access with a big lock for all of the properties, and would hinder performances?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For reasons related to backing storage, my line of thinking is that If we were to allow totalFileCount and completedFileCount to be stored in a struct the way totalCount and completedCount are, we should then also allow developers to do the same. But this will then then mean we have to allow developers to declare any ProgressManager.Property, making us store (any Sendable). But for performance reasons, we do restrict them to only certain types such as Int, UInt64 etc. I think it would be more about consistency between what our pre-declared custom properties can do vs what developers' custom properties can do.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why should an API user care about how totalFileCount and completedFileCount are stored, whether they're stored in a struct or not 😉 ? It could also be guarded by a semaphore for concurrent access. Would that allow atomically mutating everything?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm you're right, those are implementation details, I think the thing that would matter most to developers, especially UI developers, will be the performance cost that comes with having withProperties. Because withProperties acquires the lock and does the access and mutation of additional properties after, and releasing the lock before returning the values, the way we make these properties Observable is much less performant. We have to register all access and mutations of additional properties onto only one keypath because the access and withMutation calls on the ObservationRegistrar, which may cause unexpected and unnecessary redraws of the UI, putting performance cost on developers who opt to report additional properties with their progress via withProperties.


However, this design had several drawbacks:
- Limited atomicity: While it allowed atomic mutation of `totalCount` and `completedCount`, it could not achieve the same atomicity for additional properties due to their differences in backing storage
- Less intuitive API: The withProperties closure promoted a less natural pattern for accessing and mutating additional properties, particularly when combined with `@dynamicMemberLookup` on the nested `ProgressManager.Values` struct
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
- Less intuitive API: The withProperties closure promoted a less natural pattern for accessing and mutating additional properties, particularly when combined with `@dynamicMemberLookup` on the nested `ProgressManager.Values` struct
- Less intuitive API: The `withProperties` closure promoted a less natural pattern for accessing and mutating additional properties, particularly when combined with `@dynamicMemberLookup` on the nested `ProgressManager.Values` struct

However, this design had several drawbacks:
- Limited atomicity: While it allowed atomic mutation of `totalCount` and `completedCount`, it could not achieve the same atomicity for additional properties due to their differences in backing storage
- Less intuitive API: The withProperties closure promoted a less natural pattern for accessing and mutating additional properties, particularly when combined with `@dynamicMemberLookup` on the nested `ProgressManager.Values` struct
- Inconsistent access patterns: We would need to use different patterns for accessing fraction-related properties and additional properties
Copy link
Contributor

Choose a reason for hiding this comment

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

Were there discussions or feedback in the current version that was supportive of this pattern? What was the justification back then, and are they no longer valid?

Copy link
Contributor Author

@chloe-yeo chloe-yeo Sep 24, 2025

Choose a reason for hiding this comment

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

Actually the discussion that I had in mind was covered under less intuitive API, this is not needed.

- Inconsistent access patterns: We would need to use different patterns for accessing fraction-related properties and additional properties

We ultimately decided to replace this approach with a more streamlined design:
- `setCounts` closure: Atomically update `totalCount` and `completedCount`
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you clarify why only totalCount and completedCount are considered here, and why do you think that there needs to be a closure to atomically update both of them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, sounds good!

With this change, we can atomically update `totalCount` and `completedCount` via the `setCounts` closure, and access and mutate additional properties via dot syntax.

### Minimal requirements for `ProgressManager.Property` protocol
We initially considered a simpler version of the ProgressManager.Property protocol that required only two components:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
We initially considered a simpler version of the ProgressManager.Property protocol that required only two components:
We initially considered a simpler version of the `ProgressManager.Property` protocol that had two requirements:

I think we just call them requirements

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll change this!

- `Value` - The type for individual property values
- `defaultValue` - A default value when the property isn't explicitly set

In this simplified approach, the framework would automatically:
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you clarify what "the framework" refers to? Did you mean Foundation would do the below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, but let me reword this!

@chloe-yeo chloe-yeo requested a review from parkera September 24, 2025 18:16
@chloe-yeo
Copy link
Contributor Author

@swift-ci please test

@chloe-yeo chloe-yeo merged commit 77f9c33 into swiftlang:main Oct 8, 2025
3 checks passed
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.

3 participants