[swift-evolution] Pitch: Progress Tracking in Swift

cocoadev at charlessoft.com cocoadev at charlessoft.com
Tue Jan 19 16:24:09 CST 2016


On 2016-01-19 14:01, Tony Parker wrote:
> Hi Charles,

Hi Tony,

Thanks very much for responding. I do really appreciate the opportunity 
to communicate with members of the development team on these lists.

> I see Rob already pointed this out, but I’ll respond as well since I
> own NSProgress and I also was involved in the WWDC session he
> references.
> 
> NSProgress actually received a significant upgrade in iOS 9 and OS X
> 10.11 to address most of your concerns. I’ve never really liked the
> ‘current’ progress idea either, so I’ve changed Foundation to
> make using explicit progress objects possible (and encouraged). There
> are still cases where the idea of having a current progress is useful.
> For example, when there is no opportunity to grab a progress object,
> because the method you are calling is a proxy, or a global function,
> etc.).

While this is true, I feel that the syntax for using it is quite 
verbose, which feels out of place in Swift code. Mainly what it comes 
down to, though, is that I actually rather like the concept behind what 
they were trying to do with the "current" progress object, which was to 
make it possible for all sorts of low-level methods and functions (like 
-[NSData dataWithContentsOfURL:options:error:]) to automatically and 
painlessly support progress operations, so that an application that was 
doing time-consuming operations could get a lot of this "for free." It's 
just the *implementation* that I quibble with, due to various issues it 
brings up. I feel like my pitch *works* like the explicit composition 
method while *feeling* like the implicit composition method, if that 
makes sense. At any rate, anything that would make progress more 
attractive to implement, thus increasing its prevalence in the 
frameworks, would be a plus; I would love to see something like 
NSFileManager's file copy APIs supporting some sort of progress 
reporting (in an extension, of course, unless we had some way for 
Objective-C to call into the Swift progress-reporting mechanism). I also 
feel like writing it in pure Swift and minimizing the amount of work 
that is done on the current thread could reduce the performance costs of 
using it, which is important for these sorts of APIs since you don't 
want to add unnecessary overhead in the case where you're not interested 
in monitoring the progress.

> You can now create progress trees explicitly. There are two parts to
> this:
> 
>  @available(OSX 10.11, *)
>  public /*not inherited*/ init(totalUnitCount unitCount: Int64,
> parent: NSProgress, pendingUnitCount portionOfParentTotalUnitCount:
> Int64)
> 
> and a new protocol:
> 
> public protocol NSProgressReporting : NSObjectProtocol {
>  @available(OSX 10.9, *)
>  public var progress: NSProgress { get }
> }
> 
> So you can mark classes that publish progress with the protocol, and
> clients can create their trees using the new initializer.

This seems oriented toward classes such as NSOperation subclasses that 
represent a singular operation of some sort, but doesn't seem as 
applicable to single-function APIs like 
NSFileManager.copyItemAtURL(toURL:). It can of course be used that way, 
but it feels somewhat awkward. It also encourages the use of one big 
monolithic NSProgress object for the whole operation/thread/dispatch 
queue, which undermines some of what makes NSProgress so cool; the 
tree-based approach is really great for complex operations which are 
composed of a lot of separate steps, each of which may have distinct 
sub-steps of their own.

> Yes, agreed. I would encourage the discrete progress if possible, but
> I’ve had lots of requests for a block-based version of
> becomeCurrent. This is another good reason to add one.

I've been using this rather simple one, which alleviates things quite a 
bit:

(caveat: written from memory in Mail, as I'm not at my main development 
machine at the moment)

extension NSProgress {
     public func doWithPendingUnitCount<T>(unitCount: Int64, block: () 
throws -> T) rethrows -> T {
         self.becomeCurrentWithPendingUnitCount(unitCount)
         defer { self.resignCurrent() }
         return try block()
     }
}

It does not, however, address the performance concerns, nor is it quite 
as clean (IMO of course) as my suggestion.

> I changed the algorithm for how we add child progress objects, so now
> we only will add the first one. This wasn’t strictly backwards
> compatible, but it was far too easy to add more progress objects than
> you intended.

When did this change occur? I feel like I've managed to get the 
fractionCompleted to go over 1.0 this way on El Capitan, but don't 
remember exactly how I did it. Good to know this is being worked on, 
though; if I manage to get this to happen again, I'll file a Radar.

>> 5. NSProgress posts KVO notifications on the main thread.
> 
> This actually isn’t true. NSProgress posts KVO notifications on the
> thread in which the change was made.

As I mentioned in my followup post written a few minutes after the 
original one, this was a typo (or, perhaps, a Freudian slip, since 
posting on the main thread, or more generally on a dispatch queue 
settable by the user, is what I wish it did). Sorry about that.

>> In addition to complicating the interface and causing undefined
>> behavior if one mistakenly binds it to a UI element without
>> inserting something in between to forward the KVO notifications to
>> the main thread, this also hurts performance in the worker thread,
>> since KVO notifications carry a non-trivial performance penalty,
>> involving not only message sends but also packaging things into an
>> NSDictionary for the "change" parameter. Notifications for multiple
>> properties are fired every time completedUnitCount is changed, which
>> then can cause similar KVO notifications to occur up the family
>> tree, depending on how many KVO observers are installed. This can
>> show up as a non-trivial performance cost in Instruments if the
>> NSProgress object is updated frequently enough.
> 
> There is a cost associated with updating progress, it’s true. We can
> always do more to optimize it (and we continue to do so), but there
> comes a point at which the updater of the progress object has to add
> some intelligence of its own. This is why the documentation and
> release notes encourage callers to avoid doing things like updating
> the progress for each byte in a file (because that is far too many
> updates).

It seems to me that a lot of the work for this could be handled by the 
progress object. Of course, you still wouldn't want to update after 
every byte, but you wouldn't need to be nearly as vigilant as you do 
currently. I also feel like it would be better for this to be in pure 
Swift and to use mechanisms more performant than KVO, since by its very 
nature this is an API that is intended to run in worker code, which is 
where you really do care about performance. I realize of course the need 
for NSProgress to exist, since a Swift-native solution would not be 
available to pure Objective-C code, but moving forward it would be 
really nice to have a Swift-native way to do this.

My ideal implementation for the progress objects would probably look 
something like this:

- The progress object contains some simple integer and boolean 
properties for the usual things: total units, completed units, and 
cancelled.

- When one of these properties is changed, the change is propagated to 
the parent progress for completed units, or to the children for 
cancelled. This is the only real work that's done on the current thread.

- If and only if someone has set something for at least one of our 
handlers:
     * Optionally: Check if we've fired the handlers within some 
threshold, either time based (only fire if we haven't fired in some 
interval of time) or fraction-based (only fire if the fraction changed 
has increased enough for us to care about the change), and exit if the 
threshold hasn't been reached.
     * Fire any handlers that are non-nil, on the dispatch queue which we 
have as a separate property (defaulting to the main thread). Calculated 
properties such as fractionCompleted, localizedDescription (in its 
default implementation), etc. are computed on the dispatch queue as 
well, rather on the current thread.

This way, the overhead on the current thread when nothing's watching 
would be limited to a few integer stores, which are fast in Swift, and 
some nil checks. In the case where we have handlers, we add whatever it 
costs to run dispatch_async (and some floating-point operations if we 
implement the coalescing behavior). This should be much more performant 
than all the dynamic lookups, message sends, change dictionary building, 
multiple string operations (I'm finding that the KVO notification for 
localizedDescription seems to get called twice per change when I test 
it, busting out NSNumberFormatter to rebuild the "X% completed" string 
each time), and other overhead that we have now.

I could of course end up writing my own implementation, and I may end up 
doing that. It would be nice to have something like this available as 
part of the standard distribution, though.

Charles



More information about the swift-evolution mailing list