Skip to content
TORNLIFE More

Time synchronization problems

Started by xentac [3354782] on in Bugs & Issues.

3 replies · 89 views · thread synced · 4 days ago · View on torn.com
About this thread

Posts archived: 4 / 4 posts (100%) · the total is Torn's reply count + the opening post at the last fetch

Counted by TornLife from the archived posts.

Archived posts
4
Discussion span
→
People posting
3
Likes on archived posts
4
Posts by staff, officers and moderators
1
Authority score
57 / 100
Historical score
35 / 100
Story score
40 / 100
Engagement score
50 / 100
xentac [3354782]

I've been trying to debug why torn timers disagree across tabs and within Torn War Stuff Enhanced and I've uncovered some improvements that could be made.

 

Overview

 

Time synchronization between two points is an impossible problem in physics, in computer science we have approximations we can use but it will never be perfect.

 

However, the Torn frontend's time synchronization code has a number of common errors in it that can cause timers to be out of sync by a second or more as well as counting faster or slower than a second.

 

TL;DR

 

Don't count ticks, calculate the server offset by sampling over time and apply it to `new Date().getTime()` in a function called `this.timeNow()`. Call that function from `window.getCurrentTimestamp()`.

Current implementation

 

```javascript

function ServerTimeService() {
  var self = this;
  var serverSyncFrequency = 120000;
  this.timeNow = Date.now();

 

  window.addEventListener("focus", function () {
    self.syncTimeWithServer();
  });

 

  this.startClock = function () {
    if (window.Worker) {
      var worker = new Worker("/js/script/lib/custom/timers_web_worker.js");

      worker.addEventListener("message", function (e) {
        if (e.data === "tick") {
          self.timeNow += 1000;
        }
      });

      worker.postMessage("run the timer");
    } else {
      setInterval(function () {
        self.timeNow += 1000;
      }, 1000);
    }
  };

 

  this.syncTimeWithServer = function () {
    if (typeof getAction === "function" && getCookie("isLoggedIn") === "1") {
      var start = Date.now();
      var action = "/sidebarAjaxAction.php?action=servertime&t=" + start;
      getAction({
        type: "get",
        action,
        success: function (resp) {
          try {
            var data = JSON.parse(resp);
            var latency = (Date.now() - start) / 2;

            self.timeNow = data && data.time + latency;
          } catch (e) {
            console.error("Some error happen during JSON parsing: ", resp);
          }
        },
      });
    }
  };

 

  self.syncTimeWithServer();

 

  setInterval(function () {
    if (document.hidden === false) {
      self.syncTimeWithServer();
    }
  }, serverSyncFrequency);
}

 

var serverTimeService = new ServerTimeService();

 

serverTimeService.startClock();

 

function getCurrentTimestamp() {
  return serverTimeService.timeNow;
}```

 

Don't look inside `/js/script/lib/custom/timers_web_worker.js` unless you want to be saddened by another setInterval.

Problems

  • The actual offset value is thrown away inside syncTimeWithServer. This is the MOST IMPORTANT part of time synchronization.
  • Offset is only calculated with a single value instead of being sampled over time. This means that one delayed response can cause the clock to be out of sync.
  • Calculating time by counting seconds will always cause drift. NEVER count time with an interval. Not only will it force you to have more synchronization points (like every focus, for example), it will also mean the clock doesn't support millisecond precision.
  • Clocks aren't shared across tabs. This means that every tab is guaranteed to have a different time, varying by as much 2 seconds or more.

Solutions (sorted by easiest to hardest)

Calculate time based on browser time + offset

Instead of making your own javascript-based ticker, store the offset as an instance variable (`this.offset`). Create a function called `timeNow` that calculates current time plus offset.

 

```javascript
this.timeNow = function () {
  return new Date().getTime() + this.offset;
};
```

 

Call `timeNow()` in `getCurrentTimestamp()`. Now your clock will be synchronized with millisecond precision and you save yourself a useless interval timer.

Use NTP-style offset calculations

There are actually two variables you're trying to account for when synchronizing a clock `offset` and `round trip delay`. To make it possible to calculate each of those independently, `/sidebarAjaxAction.php?action=servertime&t=...` needs to return both its packet acceptance time `t1` and its packet response time `t2`. Once you have those two values you can calculate `θ` (theta) and `δ` (delta). Even just storing theta would be an improvement over what you have now.

 

An additional improvement would be to store the last X theta and delta values, throw away outliers, and take the average theta.

Put ServerTimeService in a SharedWorker so all tabs can share the same offset

Really, the only thing your `ServerTimeService` exists to do is calculate how the offset changes over time. The rest is just math against `Date.now()`. Why not do the synchronization and calculation in a SharedWorker so that all tabs can benefit from the same calculation and get the same time? This would make hospital timers on mini-profiles and profile pages in different tabs agree.

 

Bonus points if you store the offset data for some amount of time so that browser restarts can benefit from the synchronization work you already did. Don't store it for too long because clocks drift though!

Only ever compare server time to getCurrentTimestamp

I just found another example in the same file `checkClientAndServerTime(selector)`. It reads server time from a data attribute called `stime` but then it compares that time to `$.now()` (browser time). This is a no-no and will always give an incorrect result. At the very least it should compare to `getCurrentTimestamp()`. Luckily I couldn't find any `.countdown` elements on the page, so maybe this is just dead code that can be deleted. I'm sure I'd find more examples in your code if I looked harder.

xentac [3354782]

Since it's been a month and no fix, I got AI to write a monkey patch to the serverTimeService: https://greasyfork.org/en/scripts/585293-torn-ntp-style-time-sync

 

Anyone who installs it will get the benefit of synchronized time across all tabs as well as their seconds will be much closer to real second length instead of long and short seconds. They will also send fewer requests to /sidebarAjaxAction.php. It would be very easy to borrow the ideas from this code directly into the ServerTimeService.

joshuap1 [3672467]

Also noticed this on headline timers, the timer I set to end at the start of the war was a couple seconds off the war timer.