Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best Practice for Instrumenting RACSignal

I've been tasked with adding some instrumentation logic to an app to track the latency of various API calls. I'm struggling to come up with a clean, non side-effecting way to add timing instrumentation to methods that return RACSignal's (deferred execution API calls).

Considerations

  • Using ReactiveCocoa @ 1.9.5 (can't upgrade at the moment)
  • Using Parse-RACExtensions @ 0.0.2
  • I'd prefer to set up timings at the ViewModel layer as opposed to modifying Parse-RACExtensions. This is because the VM has extra info I'd like to log, like query parameters, and I don't need every API call instrumented.
  • Only record timings upon receipt of a completed event
  • In the spirit of painless instrumentation, the burden on the caller should be as small as is practical

Attempted Solution

The only thing I've been able to come up with is to create a concrete RACSubscriber subclass that that handles the timer logic. Besides the nasty subclass, this obviously isn't ideal as it requires an explicit subscribe:, which in turn requires a replay on the source signal. Additionally, there is a burden on the caller as they have to at least refactor to get a temporary handle to the signal.

@interface SignalTimer : RACSubscriber

@property (nonatomic) NSDate *startDate;

@end

@implementation SignalTimer

- (void)didSubscribeWithDisposable:(RACDisposable *)disposable
{
    [super didSubscribeWithDisposable:disposable];

    self.startDate = [NSDate date];
}

- (void)sendCompleted
{
    NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:self.startDate];
    NSLog(@"Time elapsed: %f", duration);

    [super sendCompleted];
}

@end

Usage would look like this:

- (RACSignal*)saveFoo:(Foo*)fooParseObj {
    RACSignal *save = [[fooParseObj rac_save] replay]; // Don't forget replay!
    [save subscribe:[[SignalTimer alloc] initWithName@"Saving the user's foo object"]];
    return save;
}

Obviously, I'm not happy with this implementation.

Final Thoughts

Ideally, I'd like a chain-able method like this, but I wasn't sure how to accomplish it/if it was possible to handle a cold signal without nasty side-effects inside a category method (like calling replay on the receiver).

[[[fooParseObj rac_save] logTimingsWithName:@"Saving the user's foo object"] subscribeNext:...];

Thoughts?

like image 691
Matt Hupman Avatar asked Dec 31 '13 20:12

Matt Hupman


1 Answers

So I think I was making this way harder than it needed to be. The following category solution seems much more idiomatic, but I'd love any feedback.

@interface RACSignal (Timing)

- (instancetype)logTimingWithName:(NSString*)name;

@end

@implementation RACSignal (Timing)

- (instancetype)logTimingWithName:(NSString*)name
{
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

        NSDate* startDate = [NSDate date];

        return [self subscribeNext:^(id x) {
            [subscriber sendNext:x];
        } error:^(NSError *error) {
            [subscriber sendError:error];
        } completed:^{
            NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:startDate];
            NSLog(@"%@: %f sec", name, duration);

            [subscriber sendCompleted];
        }];
    }];
}

@end
like image 116
Matt Hupman Avatar answered Oct 22 '22 04:10

Matt Hupman