Tuesday, September 21, 2010

TimeSync was a time sink

An important part of my networking code for Championship Ultimate is getting all the devices clocks synced up, so that they can talk intelligently about what time events occurred in the game. It turns out that this is not a trivial problem, and I spent a couple days working on it.

This question I asked on stackoverflow.com goes into more detail, and I got some helpful answers: http://stackoverflow.com/questions/3755208/measuring-time-difference-between-networked-devices

This wikipedia page was also helpful:
http://en.wikipedia.org/wiki/Precision_Time_Protocol

So, with all that info, I made a class to do the job that I'm fairly proud of. Perhaps it will be useful to someone else someday. Do whatever you want with the code, but it would be fun if you left a comment to say it was useful. The actual network communication stuff obviously needs to be changed to fit in your code.

TimeSync.h


//Call [timeSync startSync] on the client. The offset variable will be continually updated
//throughout the life of the program, and it will be sent back to the server also.



#define TS_DELAY_MAX 10000


@interface TimeSync : NSObject 
{
    NSTimeInterval t1, t2; //t1 is client time - server time, and t2 is server time - client time
    NSTimeInterval offset; //this doesn't get calculated from t1 and t2 until it's stabilized
    uint synccount;
}

-(NSTimeInterval)offset;
-(void)checkSyncStatus;
-(void)sendTimeSync;
-(void)sendDelayRequest;
-(void)sendDelayResponse:(NSTimeInterval)from;
-(void)processPacketType:(uint8_t)type TimeStamp:(NSTimeInterval)timeStamp;
-(void)sendSyncRequest;
-(void)publishOffset;


TimeSync.m


@implementation TimeSync

-(id)init
{
    if (self = [super init])
    {
        t1 = TS_DELAY_MAX;
        t2 = TS_DELAY_MAX;
        offset = TS_DELAY_MAX;
        synccount = 0;
    }
    return self;
}

-(void)sendSyncRequest
{
    [[Basket shared].connection sendPacket:PT_SYNC_REQ Data:nil Mode:GKSendDataReliable];
}

-(void)sendTimeSync
{
    NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
    [[Basket shared].connection sendPacket:PT_TIME_SYNC Data:[NSData dataWithBytes:&now  length:sizeof(NSTimeInterval)] Mode:GKSendDataUnreliable];
}

-(void)sendDelayRequest
{
    NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
    [[Basket shared].connection sendPacket:PT_DELAY_REQ Data:[NSData dataWithBytes:&now length:sizeof(NSTimeInterval)] Mode:GKSendDataUnreliable];
}

-(void)sendDelayResponse:(NSTimeInterval)from
{
    NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
    NSTimeInterval diff = now - from;
    [[Basket shared].connection sendPacket:PT_DELAY_RSP Data:[NSData dataWithBytes:&diff length:sizeof(NSTimeInterval)] Mode:GKSendDataUnreliable];
}

-(void)sendSyncNotify
{
    [[Basket shared].connection sendPacket:PT_SYNC_NOTIFY Data:[NSData dataWithBytes:&offset length:sizeof(NSTimeInterval)] Mode:GKSendDataReliable];
}

-(void)startSync
{
    //get a first quick measurement, then only measure every 10 seconds
    //basically, a new offset will be published every minute
    if (offset == TS_DELAY_MAX)
    {
        [self performSelector:@selector(startSync) withObject:nil afterDelay:0.5];
    }
    else
    {
        [self performSelector:@selector(startSync) withObject:nil afterDelay:10.0];
    }
    [self sendSyncRequest];
}

-(void)publishOffset
{
    offset = (t2 - t1)/2.0;
    NSLog(@"TimeSync found delay to be %f", offset);
    NSLog(@"Ping was %f", (t2 + t1)/2.0);
    //reset so we can measure a new drift
    t1 = TS_DELAY_MAX;
    t2 = TS_DELAY_MAX;
    NSLog(@"Notifying server of offset...");
    [self sendSyncNotify];
}

-(void)processPacketType:(uint8_t)type TimeStamp:(NSTimeInterval)timeStamp
{
    NSTimeInterval timediff;
    switch (type)
    {
         case PT_SYNC_REQ:
             [self sendTimeSync];
             break;
         case PT_TIME_SYNC:
             timediff = [NSDate timeIntervalSinceReferenceDate] - timeStamp;
             if (timediff < t1)
                 t1 = timediff;
             [self sendDelayRequest];
             break;
         case PT_DELAY_REQ:
             [self sendDelayResponse:timeStamp];
             break;
         case PT_DELAY_RSP:
             if (timeStamp < t2)
                 t2 = timeStamp;

             synccount++;

             if (synccount % 5 == 0) //publish the new offset every 5 measurements
             {
                 [self publishOffset];
             }
             break;
         case PT_SYNC_NOTIFY:
             offset = -timeStamp;
             NSLog(@"Received notification of offset:%f", offset);
         default:
             break;
    }
}

-(NSTimeInterval)offset
{
    return offset;
}

1 comment:

Anonymous said...

Great blog and very useful PTP wiki link.

I have been looking at mobile device fingerprinting for a payments solution and investigated whether mobile-server clock offset would be meaningful and stay reasonably consistent over days/weeks

I wrote a small script to measure 20 round trip requests, discards a handful of the longest journeys then discards the outlier clock offsets. The remaining offsets are then averaged.

The script is at http://sandboxing.net/sandbox/deviceFingerprint/

I borrowed many of the device fingerprinting concepts from the publically published https://panopticlick.eff.org

Mobile-server clock offest turned out to be very consistent in the short term, even when comparing WiFi, 3G, Edge and 2G where the round trips would vary considerably

Although over time the clocks would drift by seconds, which made the clock offset feature unusable when comparing mobile visits separated by days/weeks

One of the possible sources of this drift is the automatic clock setting on most mobiles, which may or may not be enabled