RFC: CSProgress 2.0: Concurrency-native rewrite based around Actors for synchronization

Hello all,

Some of you may remember CSProgress, my Swift-native reimplementation of the NSProgress class, with a heavy focus on performance in the worker threads. Well, in this new world of structured concurrency, it's a lot more likely that your worker code will be running asynchronously, so I decided to rewrite CSProgress around actors rather than the semaphores that the existing version used for thread synchronization.

I'm making this thread to see what the community thinks of the interface and whether you have any suggestions. I'd also like to gauge interest in some of the peripheral features that CSProgress 1.x had which haven't been implemented for 2.0 yet, because I'm not as convinced of their utility anymore, and I'm wondering if anyone disagrees.

The code is here: GitHub - CharlesJS/CSProgress at concurrency

The upshot:

  • It is now 100% based on Swift Concurrency. No locks, no semaphores, no Foundation usage, no Objective-C bridge, no excuses.
  • It no longer requires your app to import the Foundation framework (although there's an optional second product with some extensions for Foundation-specific behavior if you want it).
  • Still allows you to specify the frequency with which notifications are delivered, to avoid flooding the system with excessive progress notifications
  • The optional Foundation bindings still allow you to wrap an NSProgress and cause notifications to be sent to it (and all updates to NSProgress objects happen exclusively on the main actor to avoid UI updates on threads caused by KVO notifications)
  • A few awkward naming situations are cleaned up (in particular, CSProgress.ParentReference has been shortened to the more manageable ProgressPortion).
  • Still a lot more performant than NSProgress

What's not implemented in this version:

  • This is not a drop-in replacement for NSProgress anymore (it can't really be, since most of the entry points need await now)
  • The optional Foundation bindings are not two-way anymore; updates from CSProgress go to NSProgress, but notifications in the other direction are currently not supported. This wouldn't be too hard to implement, but I haven't used it for years, so I am wondering if anyone else ever relies on this.
  • Pause and resume, user info dictionaries, and publish and subscribe also fall under the umbrella of NSProgress features that I've never really had a use for, although I could be convinced to support them if someone presents me with a use case.

What do you guys think? Do you like the new interface? Do you have any suggestions and/or potential improvements?

Thanks!
Charles

7 Likes

It's great to see Swift-native replacements for antiquated Foundation components, especially for different platforms.

By the way, did you mean to link to the concurrency branch instead of main?

Yep! main currently has the original implementation of CSProgress; if the reactions to this reimplementation are positive, I'll merge it into main after a while. Just wanted to give a little time for community reactions before doing so, since this is a rather major breaking change from the original.

Very cool, looking forward to it.

I don't have any specific API comments to 2.0, however as a user of CSProgress with child progress instances, I wonder whether you could add a convenience constructor that takes an array of children and automatically computes the necessary totalUnitCount based on the sum of their individual totalUnitCounts.

Right now I'm doing something like that:

        self.steps.forEach { self.progress.addChild($0.progress, withPendingUnitCount: $0.progress.totalUnitCount) }
        self.progress.totalUnitCount = self.steps.reduce(0, { $0 + $1.progress.totalUnitCount })

and I think this could be slightly easier.

This isn't what I'd recommend doing, actually. The totalUnitCount does not necessary relate to the totalUnitCount of the child progress instances at all. Instead, the amount of the parent progress's totalUnitCount that each step takes should reflect the proportional amount of time the step takes relative to its siblings. Then, the child progress instances' totalUnitCount can simply be set to whatever makes it easy for them to organize their work units.