-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathclocksync.js
More file actions
188 lines (157 loc) · 4.89 KB
/
clocksync.js
File metadata and controls
188 lines (157 loc) · 4.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
/*
Time synchronization protocol for networked clients
Example usage:
// setup a sync method using ajax (or websockets etc)
// use clocksync.data() to generate the data needed to send to the server
// handle server response to clocksync.syncResponse
clocksync.syncMethod = function () {
$.post('/sync', clocksync.data(), clocksync.syncResponse, 'text');
}
// sync
clocksync.sync(function (now, delta) {
console.log('obtained synched time:', now, ' local delta: ', delta);
});
Algorithm (http://www.mine-control.com/zack/timesync/timesync.html):
1. Client stamps current local time on a "time request" packet and sends to
server
2. Upon receipt by server, server stamps server-time and returns
3. Upon receipt by client,
4. The first result should immediately be used to update the clock since it
will get the local clock into at least the right ballpark (at least the
right timezone!)
5. The client repeats steps 1 through 3 five or more times, pausing a few
seconds each time. Other traffic may be allowed in the interim, but
should be minimized for best results
6. The results of the packet receipts are accumulated and sorted in lowest
-latency to highest-latency order. The median latency is determined by
picking the mid-point sample from this ordered list.
7. All samples above approximately 1 standard-deviation from the median are
discarded and the remaining samples are averaged using an arithmetic mean.
*/
(function (root) {
var samples = [],
clockDelta = 0;
var now = function () {
return +new Date;
};
var nowsync = function () {
return now() - clockDelta;
};
var createSyncData = function () {
return {clientLocalTime: now()};
};
var handleSyncResponse = function (data) {
var receiveTime = now();
if (typeof(data) === 'string') {
data = JSON.parse(data);
}
data.clientLocalTime = parseInt(data.clientLocalTime)
data.receiveTime = receiveTime;
data.latency = (data.receiveTime - data.clientLocalTime) * 0.5;
data.clockDelta = data.receiveTime - data.serverLocalTime - data.latency;
updateSync(data);
}
var updateSync = function (sample) {
console.log(sample);
clockDelta = sample.clockDelta;
pushSample(sample);
if (samples.length < 9) {
return setTimeout(root.clocksync.syncMethod, 100);
}
completeSync();
};
var completeSync = function () {
var list = filterSamples(samples);
clockDelta = getAverageClockDelta(list);
if (root.clocksync.callback) {
root.clocksync.callback(nowsync(), clockDelta);
}
};
var pushSample = function (sample) {
var i, len = samples.length;
for(i = 0; i < len; i++) {
if (sample.latency < samples[i].latency) {
samples.splice(i, 0, sample);
return;
}
}
samples.push(sample);
};
var calcMedian = function (values) {
var half = Math.floor(values.length/2);
if(values.length % 2) {
return values[half];
} else {
return (values[half-1] + values[half]) / 2.0;
}
};
var filterSamples = function (samples) {
var list = [],
i,
latency,
len = samples.length,
sd = getStandardDeviation(samples),
median = calcMedian(samples);
for (i=0; i < len; i++) {
latency = samples[i].latency;
if (latency > median.latency - sd && latency < median.latency + sd) {
list.push(samples[i]);
}
}
return list;
}
var getAverageClockDelta = function (samples) {
var i = samples.length,
sum = 0;
while( i-- ){
sum += samples[i].clockDelta;
}
return sum / samples.length;
}
// http://bateru.com/news/2011/03/javascript-standard-deviation-variance-average-functions/
var isArray = function (obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
},
getNumWithSetDec = function( num, numOfDec ){
var pow10s = Math.pow( 10, numOfDec || 0 );
return ( numOfDec ) ? Math.round( pow10s * num ) / pow10s : num;
},
getAverageFromNumArr = function( numArr, numOfDec ){
if( !isArray( numArr ) ){ return false; }
var i = numArr.length,
sum = 0;
while( i-- ){
sum += numArr[ i ].latency;
}
return getNumWithSetDec( (sum / numArr.length ), numOfDec );
},
getVariance = function( numArr, numOfDec ){
if( !isArray(numArr) ){ return false; }
var avg = getAverageFromNumArr( numArr, numOfDec ),
i = numArr.length,
v = 0;
while( i-- ){
v += Math.pow( (numArr[ i ].latency - avg), 2 );
}
v /= numArr.length;
return getNumWithSetDec( v, numOfDec );
},
getStandardDeviation = function( numArr, numOfDec ){
if( !isArray(numArr) ){ return false; }
var stdDev = Math.sqrt( getVariance( numArr, numOfDec ) );
return getNumWithSetDec( stdDev, numOfDec );
};
root.clocksync = (function() {
return {
delta: clockDelta,
time: nowsync,
sync: function(callback) {
this.callback = callback;
this.syncMethod();
},
syncMethod: function () { throw "no sync method implemented"},
data: createSyncData,
syncResponse: handleSyncResponse
};
}());
})(this);