<NME> time.js
<BEF> const config = require('../config.json')

const GRAPH_UPDATE_TIME_GAP = 60 * 1000 // 60 seconds

class TimeTracker {
  constructor (app) {
    this._app = app
    this._serverGraphPoints = []
    this._graphPoints = []
  }

  newPointTimestamp () {
    const timestamp = TimeTracker.getEpochMillis()

    TimeTracker.pushAndShift(this._serverGraphPoints, timestamp, TimeTracker.getMaxServerGraphDataLength())

    // Flag each group as history graph additions each minute
    return timestamp
  }

  getPoints () {
    return this._points
  }

  static getMaxServerGraphDataLength () {
    }

    return {
      timestamp,
      updateHistoryGraph
    }
  }

  loadGraphPoints (startTime, timestamps) {
    // This is a copy of ServerRegistration#loadGraphPoints
    // timestamps contains original timestamp data and needs to be filtered into minutes
    this._graphPoints = TimeTracker.everyN(timestamps, startTime, GRAPH_UPDATE_TIME_GAP, (i) => timestamps[i])
  }

  getGraphPointAt (i) {
    return TimeTracker.toSeconds(this._graphPoints[i])
  }

  getServerGraphPoints () {
    return this._serverGraphPoints.map(TimeTracker.toSeconds)
  }

  getGraphPoints () {
    return this._graphPoints.map(TimeTracker.toSeconds)
  }

  static toSeconds = (timestamp) => {
    return Math.floor(timestamp / 1000)
  }

  static getEpochMillis () {
    return new Date().getTime()
  }

  static getMaxServerGraphDataLength () {
    return Math.ceil(config.serverGraphDuration / config.rates.pingAll)
  }

  static getMaxGraphDataLength () {
    return Math.ceil(config.graphDuration / GRAPH_UPDATE_TIME_GAP)
  }

  static everyN (array, start, diff, adapter) {
    const selected = []
    let lastPoint = start

    for (let i = 0; i < array.length; i++) {
      const point = array[i]

      if (point - lastPoint >= diff) {
        lastPoint = point
        selected.push(adapter(i))
      }
    }

    return selected
  }

  static pushAndShift (array, value, maxLength) {
    array.push(value)

    if (array.length > maxLength) {
      array.splice(0, array.length - maxLength)
    }
  }
}

module.exports = {
  GRAPH_UPDATE_TIME_GAP,
  TimeTracker
}

<MSG> merge graph timestamps into single array, use seconds

<DFF> @@ -18,8 +18,8 @@ class TimeTracker {
     return timestamp
   }
 
-  getPoints () {
-    return this._points
+  getPointsSeconds () {
+    return this._points.map(value => Math.floor(value / 1000))
   }
 
   static getMaxServerGraphDataLength () {

{'commit': 'diff --git a/assets/js/graph.js b/assets/js/graph.js\nindex 72a99ac..b3f48c6 100644\n--- a/assets/js/graph.js\n+++ b/assets/js/graph.js\n@@ -2,7 +2,7 @@ import uPlot from \'../lib/uPlot.esm\'\n \n import { RelativeScale } from \'./scale\'\n \n-import { formatNumber, formatTimestamp, isMobileBrowser } from \'./util\'\n+import { formatNumber, formatTimestampSeconds, isMobileBrowser } from \'./util\'\n import { uPlotTooltipPlugin } from \'./tooltip\'\n \n import { FAVORITE_SERVERS_STORAGE_KEY } from \'./favorites\'\n@@ -23,7 +23,7 @@ export class GraphDisplayManager {\n     this._showOnlyFavorites = false\n   }\n \n-  addGraphPoint (serverId, timestamp, playerCount) {\n+  addGraphPoint (timestamp, playerCounts) {\n     if (!this._hasLoadedSettings) {\n       // _hasLoadedSettings is controlled by #setGraphData\n       // It will only be true once the context has been loaded and initial payload received\n@@ -32,7 +32,11 @@ export class GraphDisplayManager {\n       return\n     }\n \n-    // FIXME\n+    this._graphTimestamps.push(timestamp)\n+\n+    for (let i = 0; i < playerCounts.length; i++) {\n+      this._graphData[i].push(playerCounts[i])\n+    }\n   }\n \n   loadLocalStorage () {\n@@ -106,7 +110,7 @@ export class GraphDisplayManager {\n     }\n   }\n \n-  buildPlotInstance (graphData) {\n+  buildPlotInstance (timestamps, data) {\n     // Lazy load settings from localStorage, if any and if enabled\n     if (!this._hasLoadedSettings) {\n       this._hasLoadedSettings = true\n@@ -114,14 +118,8 @@ export class GraphDisplayManager {\n       this.loadLocalStorage()\n     }\n \n-    // FIXME: timestamps are not shared!\n-    this._graphTimestamps = graphData[0].map(val => Math.floor(val[0] / 1000))\n-    this._graphData = Object.values(graphData).map(val => {\n-      return val.map(element => {\n-        // Safely handle null data points, they represent gaps in the graph\n-        return element === null ? null : element[1]\n-      })\n-    })\n+    this._graphTimestamps = timestamps\n+    this._graphData = data\n \n     const series = this._app.serverRegistry.getServerRegistrations().map(serverRegistration => {\n       return {\n@@ -141,7 +139,7 @@ export class GraphDisplayManager {\n         uPlotTooltipPlugin((pos, id, plot) => {\n           if (pos) {\n             // FIXME\n-            let text = \'<strong>\' + formatTimestamp(this._graphTimestamps[id] * 1000) + \'</strong><br><br>\'\n+            let text = \'<strong>\' + formatTimestampSeconds(this._graphTimestamps[id]) + \'</strong><br><br>\'\n \n             for (let i = 1; i < plot.series.length; i++) {\n               const serverRegistration = this._app.serverRegistry.getServerRegistration(i - 1)\ndiff --git a/assets/js/servers.js b/assets/js/servers.js\nindex 68113f7..bca0d40 100644\n--- a/assets/js/servers.js\n+++ b/assets/js/servers.js\n@@ -2,7 +2,7 @@ import uPlot from \'../lib/uPlot.esm\'\n \n import { RelativeScale } from \'./scale\'\n \n-import { formatNumber, formatTimestamp, formatDate, formatMinecraftServerAddress, formatMinecraftVersions } from \'./util\'\n+import { formatNumber, formatTimestampSeconds, formatDate, formatMinecraftServerAddress, formatMinecraftVersions } from \'./util\'\n import { uPlotTooltipPlugin } from \'./tooltip\'\n \n import MISSING_FAVICON from \'../images/missing_favicon.svg\'\n@@ -69,7 +69,7 @@ export class ServerRegistration {\n \n   addGraphPoints (points, timestampPoints) {\n     this._graphData = [\n-      timestampPoints.map(val => Math.floor(val / 1000)),\n+      timestampPoints.slice(),\n       points\n     ]\n   }\n@@ -90,8 +90,7 @@ export class ServerRegistration {\n               return\n             }\n \n-            // FIXME: update timestamp schema\n-            const text = formatNumber(playerCount) + \' Players<br>\' + formatTimestamp(plot.data[0][id] * 1000)\n+            const text = formatNumber(playerCount) + \' Players<br>\' + formatTimestampSeconds(plot.data[0][id])\n \n             this._app.tooltip.set(pos.left, pos.top, 10, 10, text)\n           } else {\n@@ -171,7 +170,7 @@ export class ServerRegistration {\n     }\n \n     // Use payload.playerCount so nulls WILL be pushed into the graphing data\n-    this._graphData[0].push(Math.floor(timestamp / 1000))\n+    this._graphData[0].push(timestamp)\n     this._graphData[1].push(payload.playerCount)\n \n     // Trim graphData to within the max length by shifting out the leading elements\n@@ -182,21 +181,6 @@ export class ServerRegistration {\n     }\n \n     this.redraw()\n-\n-    if (typeof payload.playerCount !== \'undefined\') {\n-      this.playerCount = payload.playerCount || 0\n-\n-      // Use payload.playerCount so nulls WILL be pushed into the graphing data\n-      this._graphData[0].push(Math.floor(timestamp / 1000))\n-      this._graphData[1].push(payload.playerCount)\n-\n-      // Trim graphData to within the max length by shifting out the leading elements\n-      for (const series of this._graphData) {\n-        if (series.length > this._app.publicConfig.serverGraphMaxLength) {\n-          series.shift()\n-        }\n-      }\n-    }\n   }\n \n   redraw () {\n@@ -219,7 +203,7 @@ export class ServerRegistration {\n     const peakValueElement = document.getElementById(\'peak-value_\' + this.serverId)\n \n     peakValueElement.innerText = formatNumber(data.playerCount)\n-    peakLabelElement.title = \'At \' + formatTimestamp(data.timestamp)\n+    peakLabelElement.title = \'At \' + formatTimestampSeconds(data.timestamp)\n \n     this.lastPeakData = data\n   }\n@@ -245,7 +229,7 @@ export class ServerRegistration {\n       // Safely handle legacy recordData that may not include the timestamp payload\n       if (recordData.timestamp > 0) {\n         recordValueElement.innerHTML = formatNumber(recordData.playerCount) + \' (\' + formatDate(recordData.timestamp) + \')\'\n-        recordLabelElement.title = \'At \' + formatDate(recordData.timestamp) + \' \' + formatTimestamp(recordData.timestamp)\n+        recordLabelElement.title = \'At \' + formatDate(recordData.timestamp) + \' \' + formatTimestampSeconds(recordData.timestamp)\n       } else {\n         recordValueElement.innerText = formatNumber(recordData.playerCount)\n       }\ndiff --git a/assets/js/socket.js b/assets/js/socket.js\nindex b60645d..24e1049 100644\n--- a/assets/js/socket.js\n+++ b/assets/js/socket.js\n@@ -81,8 +81,6 @@ export class SocketManager {\n           break\n \n         case \'updateServers\': {\n-          let requestGraphRedraw = false\n-\n           for (let serverId = 0; serverId < payload.updates.length; serverId++) {\n             // The backend may send "update" events prior to receiving all "add" events\n             // A server has only been added once it\'s ServerRegistration is defined\n@@ -92,30 +90,20 @@ export class SocketManager {\n \n             if (serverRegistration) {\n               serverRegistration.handlePing(serverUpdate, payload.timestamp)\n-\n               serverRegistration.updateServerStatus(serverUpdate, this._app.publicConfig.minecraftVersions)\n             }\n+          }\n \n-            // Use update payloads to conditionally append data to graph\n-            // Skip any incoming updates if the graph is disabled\n-            if (serverUpdate.updateHistoryGraph && this._app.graphDisplayManager.isVisible) {\n-              // Update may not be successful, safely append 0 points\n-              const playerCount = serverUpdate.playerCount || 0\n-\n-              this._app.graphDisplayManager.addGraphPoint(serverRegistration.serverId, payload.timestamp, playerCount)\n+          // Bulk add playerCounts into graph during #updateHistoryGraph\n+          if (payload.updateHistoryGraph) {\n+            this._app.graphDisplayManager.addGraphPoint(payload.timestamp, Object.values(payload.updates).map(update => update.playerCount))\n \n-              // Only redraw the graph if not mutating hidden data\n-              if (serverRegistration.isVisible) {\n-                requestGraphRedraw = true\n-              }\n+            // Run redraw tasks after handling bulk updates\n+            if (this._app.graphDisplayManager.isVisible) {\n+              this._app.graphDisplayManager.redraw()\n             }\n           }\n \n-          // Run redraw tasks after handling bulk updates\n-          if (requestGraphRedraw) {\n-            this._app.graphDisplayManager.redraw()\n-          }\n-\n           this._app.percentageBar.redraw()\n           this._app.updateGlobalStats()\n \n@@ -132,7 +120,7 @@ export class SocketManager {\n           // This is used for the manual graph load request behavior\n           this._app.graphDisplayManager.isVisible = true\n \n-          this._app.graphDisplayManager.buildPlotInstance(payload.graphData)\n+          this._app.graphDisplayManager.buildPlotInstance(payload.timestamps, payload.graphData)\n \n           // Build checkbox elements for graph controls\n           let lastRowCounter = 0\ndiff --git a/assets/js/util.js b/assets/js/util.js\nindex 6e55e42..31f89b3 100644\n--- a/assets/js/util.js\n+++ b/assets/js/util.js\n@@ -99,15 +99,15 @@ export function formatMinecraftVersions (versions, knownVersions) {\n   }).join(\', \')\n }\n \n-export function formatTimestamp (millis) {\n+export function formatTimestampSeconds (secs) {\n   const date = new Date(0)\n-  date.setUTCSeconds(millis / 1000)\n+  date.setUTCSeconds(secs)\n   return date.toLocaleTimeString()\n }\n \n-export function formatDate (millis) {\n+export function formatDate (secs) {\n   const date = new Date(0)\n-  date.setUTCSeconds(millis / 1000)\n+  date.setUTCSeconds(secs)\n   return date.toLocaleDateString()\n }\n \ndiff --git a/lib/app.js b/lib/app.js\nindex 36fdc90..9ca1ce4 100644\n--- a/lib/app.js\n+++ b/lib/app.js\n@@ -41,18 +41,19 @@ class App {\n     if (config.logToDatabase) {\n       client.on(\'message\', (message) => {\n         if (message === \'requestHistoryGraph\') {\n-          // Send historical graphData built from all serverRegistrations\n-          const graphData = {}\n+          // FIXME: update schema, remove timestamp\n+          // Since all history graph updates share timestamps, pull the timestamps from a single ServerRegistration\n+          const timestamps = this.serverRegistrations[0].graphData.map(point => Math.floor(point[0] / 1000))\n \n-          this.serverRegistrations.forEach((serverRegistration) => {\n-            graphData[serverRegistration.serverId] = serverRegistration.graphData\n-          })\n+          // Send historical graphData built from all serverRegistrations\n+          const graphData = this.serverRegistrations.map(serverRegistration => serverRegistration.graphData.map(point => point[1]))\n \n           // Send graphData in object wrapper to avoid needing to explicity filter\n           // any header data being appended by #MessageOf since the graph data is fed\n           // directly into the graphing system\n           client.send(MessageOf(\'historyGraph\', {\n-            graphData: graphData\n+            timestamps,\n+            graphData\n           }))\n         }\n       })\n@@ -77,7 +78,7 @@ class App {\n         }\n       })(),\n       mojangServices: this.mojangUpdater.getLastUpdate(),\n-      timestampPoints: this.timeTracker.getPoints(),\n+      timestampPoints: this.timeTracker.getPointsSeconds(),\n       servers: this.serverRegistrations.map(serverRegistration => serverRegistration.getPingHistory())\n     }\n \ndiff --git a/lib/database.js b/lib/database.js\nindex 60f795a..c27e30d 100644\n--- a/lib/database.js\n+++ b/lib/database.js\n@@ -63,8 +63,8 @@ class Database {\n       // When complete increment completeTasks to know when complete\n       this.getRecord(serverRegistration.data.ip, (playerCount, timestamp) => {\n         serverRegistration.recordData = {\n-          playerCount: playerCount,\n-          timestamp: timestamp\n+          playerCount,\n+          timestamp: Math.floor(timestamp / 1000)\n         }\n \n         // Check if completedTasks hit the finish value\ndiff --git a/lib/ping.js b/lib/ping.js\nindex 58ef9c8..dcdd549 100644\n--- a/lib/ping.js\n+++ b/lib/ping.js\n@@ -85,6 +85,14 @@ class PingController {\n   pingAll = () => {\n     const timestamp = this._app.timeTracker.newTimestamp()\n \n+    // Flag each group as history graph additions each minute\n+    // This is sent to the frontend for graph updates\n+    const updateHistoryGraph = config.logToDatabase && (!this._lastHistoryGraphUpdate || timestamp - this._lastHistoryGraphUpdate >= 60 * 1000)\n+\n+    if (updateHistoryGraph) {\n+      this._lastHistoryGraphUpdate = timestamp\n+    }\n+\n     this.startPingTasks(results => {\n       const updates = []\n \n@@ -102,7 +110,7 @@ class PingController {\n         // Generate a combined update payload\n         // This includes any modified fields and flags used by the frontend\n         // This will not be cached and can contain live metadata\n-        const update = serverRegistration.handlePing(timestamp, result.resp, result.err, result.version)\n+        const update = serverRegistration.handlePing(timestamp, result.resp, result.err, result.version, updateHistoryGraph)\n \n         updates[serverRegistration.serverId] = update\n       }\n@@ -110,7 +118,8 @@ class PingController {\n       // Send object since updates uses serverIds as keys\n       // Send a single timestamp entry since it is shared\n       this._app.server.broadcast(MessageOf(\'updateServers\', {\n-        timestamp,\n+        timestamp: Math.floor(timestamp / 1000),\n+        updateHistoryGraph,\n         updates\n       }))\n     })\ndiff --git a/lib/servers.js b/lib/servers.js\nindex a0ee646..6ef2839 100644\n--- a/lib/servers.js\n+++ b/lib/servers.js\n@@ -20,7 +20,7 @@ class ServerRegistration {\n     this._pingHistory = []\n   }\n \n-  handlePing (timestamp, resp, err, version) {\n+  handlePing (timestamp, resp, err, version, updateHistoryGraph) {\n     // Use null to represent a failed ping\n     const playerCount = resp ? resp.players.online : null\n \n@@ -35,19 +35,21 @@ class ServerRegistration {\n     // Only notify the frontend to append to the historical graph\n     // if both the graphing behavior is enabled and the backend agrees\n     // that the ping is eligible for addition\n-    let updateHistoryGraph = false\n+    if (updateHistoryGraph) {\n+      // FIXME: update schema, remove timestamp\n+      this.graphData.push([timestamp, playerCount])\n \n-    if (config.logToDatabase) {\n-      if (this.addGraphPoint(playerCount, timestamp)) {\n-        updateHistoryGraph = true\n+      // Trim old graphPoints according to #getMaxGraphDataLength\n+      if (this.graphData.length > TimeTracker.getMaxGraphDataLength()) {\n+        this.graphData.shift()\n       }\n     }\n \n     // Delegate out update payload generation\n-    return this.getUpdate(timestamp, resp, err, version, updateHistoryGraph)\n+    return this.getUpdate(timestamp, resp, err, version)\n   }\n \n-  getUpdate (timestamp, resp, err, version, updateHistoryGraph) {\n+  getUpdate (timestamp, resp, err, version) {\n     const update = {}\n \n     if (resp) {\n@@ -59,7 +61,7 @@ class ServerRegistration {\n       if (config.logToDatabase && (!this.recordData || resp.players.online > this.recordData.playerCount)) {\n         this.recordData = {\n           playerCount: resp.players.online,\n-          timestamp: timestamp\n+          timestamp: Math.floor(timestamp / 1000)\n         }\n \n         // Append an updated recordData\n@@ -80,12 +82,6 @@ class ServerRegistration {\n         if (this.findNewGraphPeak()) {\n           update.graphPeakData = this.getGraphPeak()\n         }\n-\n-        // Handled inside logToDatabase to validate logic from #getUpdate call\n-        // Only append when true since an undefined value == false\n-        if (updateHistoryGraph) {\n-          update.updateHistoryGraph = true\n-        }\n       }\n     } else if (err) {\n       // Append a filtered copy of err\n@@ -155,18 +151,6 @@ class ServerRegistration {\n     }\n   }\n \n-  addGraphPoint (playerCount, timestamp) {\n-    // FIXME: update schema, remove timestamp\n-    this.graphData.push([timestamp, playerCount])\n-\n-    // Trim old graphPoints according to #getMaxGraphDataLength\n-    if (this.graphData.length > TimeTracker.getMaxGraphDataLength()) {\n-      this.graphData.shift()\n-    }\n-\n-    return true\n-  }\n-\n   findNewGraphPeak () {\n     let index = -1\n     for (let i = 0; i < this.graphData.length; i++) {\n@@ -192,7 +176,7 @@ class ServerRegistration {\n     const graphPeak = this.graphData[this._graphPeakIndex]\n     return {\n       playerCount: graphPeak[1],\n-      timestamp: graphPeak[0]\n+      timestamp: Math.floor(graphPeak[0] / 1000)\n     }\n   }\n \ndiff --git a/lib/time.js b/lib/time.js\nindex 966272a..3556dfe 100644\n--- a/lib/time.js\n+++ b/lib/time.js\n@@ -18,8 +18,8 @@ class TimeTracker {\n     return timestamp\n   }\n \n-  getPoints () {\n-    return this._points\n+  getPointsSeconds () {\n+    return this._points.map(value => Math.floor(value / 1000))\n   }\n \n   static getMaxServerGraphDataLength () {\n', 'commit_subject': 'merge graph timestamps into single array, use seconds', 'repo_name': 'Cryptkeeper/Minetrack', 'committer': {'name': 'Nick Krecklow', 'email': 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d@github.nklow.com', 'time_sec': '1589188366', 'tz_offset': '-300', 'date': {'seconds': '1589188366'}}, 'commit_tree': 'b1f42a73c979847c57ed13ff659cfabbf86079d6', 'commit_message': 'merge graph timestamps into single array, use seconds\n', 'language_name': 'Shell', 'license': 'mit', 'hunks': '@@ -18,8 +18,8 @@ class TimeTracker {\n     return timestamp\n   }\n \n-  getPoints () {\n-    return this._points\n+  getPointsSeconds () {\n+    return this._points.map(value => Math.floor(value / 1000))\n   }\n \n   static getMaxServerGraphDataLength () {\n', 'addition_count': 2, 'deletion_count': 2, 'src_file': 'a/lib/time.js', 'tgt_file': 'b/lib/time.js', 'file_extension': '.js', 'before_file': "const config = require('../config.json')\n\nconst GRAPH_UPDATE_TIME_GAP = 60 * 1000 // 60 seconds\n\nclass TimeTracker {\n  constructor (app) {\n    this._app = app\n    this._serverGraphPoints = []\n    this._graphPoints = []\n  }\n\n  newPointTimestamp () {\n    const timestamp = TimeTracker.getEpochMillis()\n\n    TimeTracker.pushAndShift(this._serverGraphPoints, timestamp, TimeTracker.getMaxServerGraphDataLength())\n\n    // Flag each group as history graph additions each minute\n    return timestamp\n  }\n\n  getPoints () {\n    return this._points\n  }\n\n  static getMaxServerGraphDataLength () {\n    }\n\n    return {\n      timestamp,\n      updateHistoryGraph\n    }\n  }\n\n  loadGraphPoints (startTime, timestamps) {\n    // This is a copy of ServerRegistration#loadGraphPoints\n    // timestamps contains original timestamp data and needs to be filtered into minutes\n    this._graphPoints = TimeTracker.everyN(timestamps, startTime, GRAPH_UPDATE_TIME_GAP, (i) => timestamps[i])\n  }\n\n  getGraphPointAt (i) {\n    return TimeTracker.toSeconds(this._graphPoints[i])\n  }\n\n  getServerGraphPoints () {\n    return this._serverGraphPoints.map(TimeTracker.toSeconds)\n  }\n\n  getGraphPoints () {\n    return this._graphPoints.map(TimeTracker.toSeconds)\n  }\n\n  static toSeconds = (timestamp) => {\n    return Math.floor(timestamp / 1000)\n  }\n\n  static getEpochMillis () {\n    return new Date().getTime()\n  }\n\n  static getMaxServerGraphDataLength () {\n    return Math.ceil(config.serverGraphDuration / config.rates.pingAll)\n  }\n\n  static getMaxGraphDataLength () {\n    return Math.ceil(config.graphDuration / GRAPH_UPDATE_TIME_GAP)\n  }\n\n  static everyN (array, start, diff, adapter) {\n    const selected = []\n    let lastPoint = start\n\n    for (let i = 0; i < array.length; i++) {\n      const point = array[i]\n\n      if (point - lastPoint >= diff) {\n        lastPoint = point\n        selected.push(adapter(i))\n      }\n    }\n\n    return selected\n  }\n\n  static pushAndShift (array, value, maxLength) {\n    array.push(value)\n\n    if (array.length > maxLength) {\n      array.splice(0, array.length - maxLength)\n    }\n  }\n}\n\nmodule.exports = {\n  GRAPH_UPDATE_TIME_GAP,\n  TimeTracker\n}\n", 'message': 'merge graph timestamps into single array, use seconds\n', 'author_name': 'Nick Krecklow', 'lang': 'js'}