From da33d1576abe56811d173350b85ab402704adf5f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 1 Jun 2024 17:07:58 +0200 Subject: [PATCH] WebHost: update trackers only if they're visible. (#3407) --- WebHostLib/static/assets/trackerCommon.js | 85 ++++++++++-------- WebHostLib/templates/multitracker.html | 2 +- WebHostLib/tracker.py | 100 +++++++++++++++------- 3 files changed, 120 insertions(+), 67 deletions(-) diff --git a/WebHostLib/static/assets/trackerCommon.js b/WebHostLib/static/assets/trackerCommon.js index b8e089ece5d3..6324837b2816 100644 --- a/WebHostLib/static/assets/trackerCommon.js +++ b/WebHostLib/static/assets/trackerCommon.js @@ -27,7 +27,7 @@ const adjustTableHeight = () => { * @returns {string} */ const secondsToHours = (seconds) => { - let hours = Math.floor(seconds / 3600); + let hours = Math.floor(seconds / 3600); let minutes = Math.floor((seconds - (hours * 3600)) / 60).toString().padStart(2, '0'); return `${hours}:${minutes}`; }; @@ -38,18 +38,18 @@ window.addEventListener('load', () => { info: false, dom: "t", stateSave: true, - stateSaveCallback: function(settings, data) { + stateSaveCallback: function (settings, data) { delete data.search; localStorage.setItem(`DataTables_${settings.sInstance}_/tracker`, JSON.stringify(data)); }, - stateLoadCallback: function(settings) { + stateLoadCallback: function (settings) { return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`)); }, - footerCallback: function(tfoot, data, start, end, display) { + footerCallback: function (tfoot, data, start, end, display) { if (tfoot) { const activityData = this.api().column('lastActivity:name').data().toArray().filter(x => !isNaN(x)); Array.from(tfoot?.children).find(td => td.classList.contains('last-activity')).innerText = - (activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None'; + (activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None'; } }, columnDefs: [ @@ -123,49 +123,64 @@ window.addEventListener('load', () => { event.preventDefault(); } }); - const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker'); - const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3; + const target_second = parseInt(document.getElementById('tracker-wrapper').getAttribute('data-second')) + 3; + console.log("Target second of refresh: " + target_second); - function getSleepTimeSeconds(){ + function getSleepTimeSeconds() { // -40 % 60 is -40, which is absolutely wrong and should burn var sleepSeconds = (((target_second - new Date().getSeconds()) % 60) + 60) % 60; return sleepSeconds || 60; } + let update_on_view = false; const update = () => { - const target = $("
"); - console.log("Updating Tracker..."); - target.load(location.href, function (response, status) { - if (status === "success") { - target.find(".table").each(function (i, new_table) { - const new_trs = $(new_table).find("tbody>tr"); - const footer_tr = $(new_table).find("tfoot>tr"); - const old_table = tables.eq(i); - const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop(); - const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft(); - old_table.clear(); - if (footer_tr.length) { - $(old_table.table).find("tfoot").html(footer_tr); - } - old_table.rows.add(new_trs); - old_table.draw(); - $(old_table.settings()[0].nScrollBody).scrollTop(topscroll); - $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll); - }); - $("#multi-stream-link").replaceWith(target.find("#multi-stream-link")); - } else { - console.log("Failed to connect to Server, in order to update Table Data."); - console.log(response); - } - }) - setTimeout(update, getSleepTimeSeconds()*1000); + if (document.hidden) { + console.log("Document reporting as not visible, not updating Tracker..."); + update_on_view = true; + } else { + update_on_view = false; + const target = $("
"); + console.log("Updating Tracker..."); + target.load(location.href, function (response, status) { + if (status === "success") { + target.find(".table").each(function (i, new_table) { + const new_trs = $(new_table).find("tbody>tr"); + const footer_tr = $(new_table).find("tfoot>tr"); + const old_table = tables.eq(i); + const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop(); + const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft(); + old_table.clear(); + if (footer_tr.length) { + $(old_table.table).find("tfoot").html(footer_tr); + } + old_table.rows.add(new_trs); + old_table.draw(); + $(old_table.settings()[0].nScrollBody).scrollTop(topscroll); + $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll); + }); + $("#multi-stream-link").replaceWith(target.find("#multi-stream-link")); + } else { + console.log("Failed to connect to Server, in order to update Table Data."); + console.log(response); + } + }) + } + updater = setTimeout(update, getSleepTimeSeconds() * 1000); } - setTimeout(update, getSleepTimeSeconds()*1000); + let updater = setTimeout(update, getSleepTimeSeconds() * 1000); window.addEventListener('resize', () => { adjustTableHeight(); tables.draw(); }); + window.addEventListener('visibilitychange', () => { + if (!document.hidden && update_on_view) { + console.log("Page became visible, tracker should be refreshed."); + clearTimeout(updater); + update(); + } + }); + adjustTableHeight(); }); diff --git a/WebHostLib/templates/multitracker.html b/WebHostLib/templates/multitracker.html index b16d4714ec6a..1b371b1229e5 100644 --- a/WebHostLib/templates/multitracker.html +++ b/WebHostLib/templates/multitracker.html @@ -10,7 +10,7 @@ {% include "header/dirtHeader.html" %} {% include "multitrackerNavigation.html" %} -
+
diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 71ee9c7fcafe..36ebaacbcb0b 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -3,8 +3,9 @@ from dataclasses import dataclass from typing import Any, Callable, Dict, List, Optional, Set, Tuple, NamedTuple, Counter from uuid import UUID +from email.utils import parsedate_to_datetime -from flask import render_template +from flask import render_template, make_response, Response, request from werkzeug.exceptions import abort from MultiServer import Context, get_saving_second @@ -297,56 +298,92 @@ def get_spheres(self) -> List[List[int]]: return self._multidata.get("spheres", []) +def _process_if_request_valid(incoming_request, room: Optional[Room]) -> Optional[Response]: + if not room: + abort(404) + + if_modified = incoming_request.headers.get("If-Modified-Since", None) + if if_modified: + if_modified = parsedate_to_datetime(if_modified) + # if_modified has less precision than last_activity, so we bring them to same precision + if if_modified >= room.last_activity.replace(microsecond=0): + return make_response("", 304) + + @app.route("/tracker///") -def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> str: +def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> Response: key = f"{tracker}_{tracked_team}_{tracked_player}_{generic}" - tracker_page = cache.get(key) - if tracker_page: - return tracker_page + response: Optional[Response] = cache.get(key) + if response: + return response + + # Room must exist. + room = Room.get(tracker=tracker) + + response = _process_if_request_valid(request, room) + if response: + return response + + timeout, last_modified, tracker_page = get_timeout_and_player_tracker(room, tracked_team, tracked_player, generic) + response = make_response(tracker_page) + response.last_modified = last_modified + cache.set(key, response, timeout) + return response + + +def get_timeout_and_player_tracker(room: Room, tracked_team: int, tracked_player: int, generic: bool)\ + -> Tuple[int, datetime.datetime, str]: + tracker_data = TrackerData(room) + + # Load and render the game-specific player tracker, or fallback to generic tracker if none exists. + game_specific_tracker = _player_trackers.get(tracker_data.get_player_game(tracked_team, tracked_player), None) + if game_specific_tracker and not generic: + tracker = game_specific_tracker(tracker_data, tracked_team, tracked_player) + else: + tracker = render_generic_tracker(tracker_data, tracked_team, tracked_player) - timeout, tracker_page = get_timeout_and_tracker(tracker, tracked_team, tracked_player, generic) - cache.set(key, tracker_page, timeout) - return tracker_page + return ((tracker_data.get_room_saving_second() - datetime.datetime.now().second) + % TRACKER_CACHE_TIMEOUT_IN_SECONDS or TRACKER_CACHE_TIMEOUT_IN_SECONDS, room.last_activity, tracker) @app.route("/generic_tracker///") -def get_generic_game_tracker(tracker: UUID, tracked_team: int, tracked_player: int) -> str: +def get_generic_game_tracker(tracker: UUID, tracked_team: int, tracked_player: int) -> Response: return get_player_tracker(tracker, tracked_team, tracked_player, True) @app.route("/tracker/", defaults={"game": "Generic"}) @app.route("/tracker//") -@cache.memoize(timeout=TRACKER_CACHE_TIMEOUT_IN_SECONDS) -def get_multiworld_tracker(tracker: UUID, game: str): +def get_multiworld_tracker(tracker: UUID, game: str) -> Response: + key = f"{tracker}_{game}" + response: Optional[Response] = cache.get(key) + if response: + return response + # Room must exist. room = Room.get(tracker=tracker) - if not room: - abort(404) - tracker_data = TrackerData(room) - enabled_trackers = list(get_enabled_multiworld_trackers(room).keys()) - if game not in _multiworld_trackers: - return render_generic_multiworld_tracker(tracker_data, enabled_trackers) + response = _process_if_request_valid(request, room) + if response: + return response - return _multiworld_trackers[game](tracker_data, enabled_trackers) + timeout, last_modified, tracker_page = get_timeout_and_multiworld_tracker(room, game) + response = make_response(tracker_page) + response.last_modified = last_modified + cache.set(key, response, timeout) + return response -def get_timeout_and_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool) -> Tuple[int, str]: - # Room must exist. - room = Room.get(tracker=tracker) - if not room: - abort(404) - +def get_timeout_and_multiworld_tracker(room: Room, game: str)\ + -> Tuple[int, datetime.datetime, str]: tracker_data = TrackerData(room) - - # Load and render the game-specific player tracker, or fallback to generic tracker if none exists. - game_specific_tracker = _player_trackers.get(tracker_data.get_player_game(tracked_team, tracked_player), None) - if game_specific_tracker and not generic: - tracker = game_specific_tracker(tracker_data, tracked_team, tracked_player) + enabled_trackers = list(get_enabled_multiworld_trackers(room).keys()) + if game in _multiworld_trackers: + tracker = _multiworld_trackers[game](tracker_data, enabled_trackers) else: - tracker = render_generic_tracker(tracker_data, tracked_team, tracked_player) + tracker = render_generic_multiworld_tracker(tracker_data, enabled_trackers) - return (tracker_data.get_room_saving_second() - datetime.datetime.now().second) % 60 or 60, tracker + return ((tracker_data.get_room_saving_second() - datetime.datetime.now().second) + % TRACKER_CACHE_TIMEOUT_IN_SECONDS or TRACKER_CACHE_TIMEOUT_IN_SECONDS, room.last_activity, tracker) def get_enabled_multiworld_trackers(room: Room) -> Dict[str, Callable]: @@ -416,6 +453,7 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker videos=tracker_data.get_room_videos(), item_id_to_name=tracker_data.item_id_to_name, location_id_to_name=tracker_data.location_id_to_name, + saving_second=tracker_data.get_room_saving_second(), )