From 9fa5fbb3c6617f14027300e33b96b4f380a2304b Mon Sep 17 00:00:00 2001 From: Charlie Nolan Date: Sun, 12 May 2024 04:03:14 -0700 Subject: [PATCH] Made turf contents listing more performant. (#25188) * Made turf contents listing more performant. * Made images resend and reload if they don't arrive. * Runtime fix. * Remove redundant code. * Lint. * Apply suggestions from code review Co-authored-by: Contrabang <91113370+Contrabang@users.noreply.github.com> Signed-off-by: Charlie Nolan * Safety. --------- Signed-off-by: Charlie Nolan Co-authored-by: FunnyMan3595 (Charlie Nolan) Co-authored-by: Contrabang <91113370+Contrabang@users.noreply.github.com> --- code/__HELPERS/iconprocs.dm | 16 +- code/controllers/subsystem/SSstatpanel.dm | 37 ++- code/modules/client/client_procs.dm | 12 + html/statbrowser.css | 25 +- html/statbrowser.js | 296 +++++++++++++++++----- 5 files changed, 292 insertions(+), 94 deletions(-) diff --git a/code/__HELPERS/iconprocs.dm b/code/__HELPERS/iconprocs.dm index 533d66d52d3..e3e948f64f1 100644 --- a/code/__HELPERS/iconprocs.dm +++ b/code/__HELPERS/iconprocs.dm @@ -445,9 +445,9 @@ GLOBAL_LIST_EMPTY(bicon_cache) * * frame - what frame of the icon_state's animation for the icon being used * * moving - whether or not to use a moving state for the given icon * * sourceonly - if TRUE, only generate the asset and send back the asset url, instead of tags that display the icon to players - * * extra_clases - string of extra css classes to use when returning the icon string + * * extra_classes - string of extra css classes to use when returning the icon string */ -/proc/icon2html(atom/thing, client/target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null) +/proc/icon2asset(atom/thing, client/target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null) if(!thing) return @@ -516,17 +516,15 @@ GLOBAL_LIST_EMPTY(bicon_cache) SSassets.transport.register_asset(key, rsc_ref, file_hash, icon_path) for(var/client_target in targets) SSassets.transport.send_assets(client_target, key) - if(sourceonly) - return SSassets.transport.get_asset_url(key) - return "" + return key -/// Costlier version of icon2html() that uses getFlatIcon() to account for overlays, underlays, etc. Use with extreme moderation, ESPECIALLY on mobs. -/proc/costly_icon2html(thing, target, sourceonly = FALSE) +/// Costlier version of icon2asset() that uses getFlatIcon() to account for overlays, underlays, etc. Use with extreme moderation, ESPECIALLY on mobs. +/proc/costly_icon2asset(thing, target, sourceonly = FALSE) if(!thing) return if(isicon(thing)) - return icon2html(thing, target) + return icon2asset(thing, target) var/icon/I = getFlatIcon(thing) - return icon2html(I, target, sourceonly = sourceonly) + return icon2asset(I, target) diff --git a/code/controllers/subsystem/SSstatpanel.dm b/code/controllers/subsystem/SSstatpanel.dm index e584162e566..08840da9541 100644 --- a/code/controllers/subsystem/SSstatpanel.dm +++ b/code/controllers/subsystem/SSstatpanel.dm @@ -112,11 +112,10 @@ SUBSYSTEM_DEF(statpanels) /// Set the atoms we're meant to display var/datum/object_window_info/obj_window = istype(target.obj_window) ? target.obj_window : new(target) obj_window.atoms_to_show = atoms_to_display - START_PROCESSING(SSobj_tab_items, obj_window) - refresh_client_obj_view(target) + refresh_client_obj_view(target, obj_window.min_index, obj_window.max_index) -/datum/controller/subsystem/statpanels/proc/refresh_client_obj_view(client/refresh) - var/list/turf_items = return_object_images(refresh) +/datum/controller/subsystem/statpanels/proc/refresh_client_obj_view(client/refresh, min_index = 0, max_index = 30) + var/list/turf_items = return_object_images(refresh, min_index, max_index) if(!length(turf_items) || !refresh.mob?.listed_turf) return refresh.stat_panel.send_message("update_listedturf", turf_items) @@ -125,7 +124,7 @@ SUBSYSTEM_DEF(statpanels) /// Returns all our ready object tab images /// Returns a list in the form list(list(object_name, object_ref, loaded_image), ...) -/datum/controller/subsystem/statpanels/proc/return_object_images(client/load_from) +/datum/controller/subsystem/statpanels/proc/return_object_images(client/load_from, min_index, max_index) // You might be inclined to think that this is a waste of cpu time, since we // A: Double iterate over atoms in the build case, or // B: Generate these lists over and over in the refresh case @@ -137,10 +136,18 @@ SUBSYSTEM_DEF(statpanels) return list() var/datum/object_window_info/obj_window = load_from.obj_window + if(!obj_window) + return list() var/list/already_seen = obj_window.atoms_to_images var/list/to_make = obj_window.atoms_to_imagify var/list/turf_items = list() + var/i = 0 for(var/atom/turf_item as anything in obj_window.atoms_to_show) + // Limit what we send to the client's rendered section. + i++ + if(i <= min_index || i > max_index) + continue + // First, we fill up the list of refs to display // If we already have one, just use that var/existing_image = already_seen[turf_item] @@ -148,12 +155,17 @@ SUBSYSTEM_DEF(statpanels) continue // We already have it. Success! if(existing_image) - turf_items[++turf_items.len] = list("[turf_item.name]", turf_item.UID(), existing_image) + turf_items["[i]"] = list("[turf_item.name]", turf_item.UID(), SSassets.transport.get_asset_url(existing_image), existing_image) continue // Now, we're gonna queue image generation out of those refs to_make += turf_item already_seen[turf_item] = OBJ_IMAGE_LOADING obj_window.RegisterSignal(turf_item, COMSIG_PARENT_QDELETING, TYPE_PROC_REF(/datum/object_window_info, viewing_atom_deleted), override = TRUE) // we reset cache if anything in it gets deleted + turf_items["total"] = i + obj_window.min_index = min_index + obj_window.max_index = max_index + if(length(to_make)) + START_PROCESSING(SSobj_tab_items, obj_window) return turf_items #undef OBJ_IMAGE_LOADING @@ -219,6 +231,10 @@ SUBSYSTEM_DEF(statpanels) var/client/parent /// Are we currently tracking a turf? var/actively_tracking = FALSE + /// The minimum index currently sent to the client. + var/min_index = 0 + /// The maximum index currently sent to the client. + var/max_index = 30 /datum/object_window_info/New(client/parent) . = ..() @@ -245,9 +261,9 @@ SUBSYSTEM_DEF(statpanels) var/generated_string if(ismob(thing) || length(thing.overlays) > 2) - generated_string = costly_icon2html(thing, parent, sourceonly=TRUE) + generated_string = costly_icon2asset(thing, parent) else - generated_string = icon2html(thing, parent, sourceonly=TRUE) + generated_string = icon2asset(thing, parent) newly_seen[thing] = generated_string if(TICK_CHECK) @@ -257,7 +273,7 @@ SUBSYSTEM_DEF(statpanels) // If we've not cut yet, do it now if(index) to_make.Cut(1, index + 1) - SSstatpanels.refresh_client_obj_view(parent) + SSstatpanels.refresh_client_obj_view(parent, min_index, max_index) if(!length(to_make)) return PROCESS_KILL @@ -302,6 +318,9 @@ SUBSYSTEM_DEF(statpanels) if(listed_turf) client.stat_panel.send_message("create_listedturf", listed_turf.name) client.obj_window.start_turf_tracking() + client.obj_window.min_index = 0 + client.obj_window.max_index = 30 + SSstatpanels.set_turf_examine_tab(client, src) else client.stat_panel.send_message("remove_listedturf") client.obj_window.stop_turf_tracking() diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 6330afcfa70..66d1d4ad21e 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -1230,6 +1230,18 @@ if("Set-Tab") stat_tab = payload["tab"] SSstatpanels.immediate_send_stat_data(src) + if("Listedturf-Scroll") + if(payload["min"] == payload["max"]) + // Not properly loaded yet, send the default set. + SSstatpanels.refresh_client_obj_view(src) + else + SSstatpanels.refresh_client_obj_view(src, payload["min"], payload["max"]) + // Uncomment to enable log_debug in stat panel code. + // Disabled normally due to HREF exploit concerns. + //if("Statpanel-Debug") + // log_debug(payload) + if("Resend-Asset") + SSassets.transport.send_assets(src, list(payload)) if("Debug-Stat-Entry") var/stat_item = locateUID(payload["stat_item_uid"]) if(!check_rights(R_DEBUG | R_VIEWRUNTIMES) || !stat_item) diff --git a/html/statbrowser.css b/html/statbrowser.css index 11eda40d03d..d78fe76184a 100644 --- a/html/statbrowser.css +++ b/html/statbrowser.css @@ -158,19 +158,24 @@ td { pointer-events: none; } -.link { - display: inline; +.link, +.listedturf_link { background: none; border: none; - padding: 7px 14px; color: black; text-decoration: none; cursor: pointer; font-size: 13px; +} + +.link { + display: inline; + padding: 7px 14px; margin: 2px 2px; } -.link:hover { +.link:hover, +.listedturf_link:hover { text-decoration: underline; } @@ -225,7 +230,8 @@ body.dark { color: #b2c4dd; } -.dark .link { +.dark .link, +.dark .listedturf_link{ color: #abc6ec; } @@ -268,7 +274,8 @@ body.ntos { color: #b2c4dd; } -.ntos .link { +.ntos .link, +.ntos .listedturf_link{ color: #abc6ec; } @@ -311,7 +318,8 @@ body.paradise { color: #dec5bd; } -.paradise .link { +.paradise .link, +.paradise .listedturf_link{ color: #edc1b2; } @@ -354,6 +362,7 @@ body.syndicate { color: #debdbd; } -.syndicate .link { +.syndicate .link, +.syndicate .listedturf_link { color: #edb2b2; } diff --git a/html/statbrowser.js b/html/statbrowser.js index 7fa1a1f8bcc..c44d424a1b3 100644 --- a/html/statbrowser.js +++ b/html/statbrowser.js @@ -14,6 +14,13 @@ if (!String.prototype.trim) { }; } +// For sending BYOND debug logs ----------------------------------------------- +// If you use this, you'll need to uncomment the Statpanel-Debug message +// handling, currently in code/modules/client/client_procs.dm +function log_debug(data) { + Byond.sendMessage("Statpanel-Debug", JSON.stringify(data)); +} + // Status panel implementation ------------------------------------------------ var status_tab_parts = [["Loading...", ""]]; var current_tab = null; @@ -22,14 +29,20 @@ var href_token = null; var verb_tabs = []; var verbs = [["", ""]]; // list with a list inside var permanent_tabs = []; // tabs that won't be cleared by wipes -var turfcontents = []; +var turf_row_inner_height = 33; +var turf_row_outer_height = 35; +var turf_rows = {}; +var turf_incomplete_rows = {}; +var turf_size = 0; +var turf_image_errors = {}; +var turfcontents = {"total": 0}; var turfname = ""; +var imageFirstRetryDelay = 50; var imageRetryDelay = 500; var imageRetryLimit = 50; var menu = document.getElementById('menu'); var under_menu = document.getElementById('under_menu'); var statcontentdiv = document.getElementById('statcontent'); -var storedimages = []; var split_admin_tabs = false; // Any BYOND commands that could result in the client's focus changing go through this @@ -371,79 +384,217 @@ function draw_mc() { document.getElementById("statcontent").appendChild(table); } -function iconError(e) { - if(current_tab != turfname) { +function listedturf_add_row(table, table_index, true_index) { + let row = table.insertRow(table_index); + row.style.height = turf_row_inner_height + "px" + row.style.padding = "0px" + row.style.margin = "0px" + turf_rows[true_index] = row; + turf_incomplete_rows[true_index] = true_index + 1; +} + +function listedturf_fill_row(row, item_index) { + let object_info = turfcontents["" + item_index]; + if(!object_info) { + return false; + } + + let cell = document.createElement("td"); + cell.style.height = turf_row_inner_height + "px" + cell.style.padding = "0px" + cell.style.margin = "0px" + row.appendChild(cell) + + var button = document.createElement("div"); + button.className = "listedturf_link"; + var clickcatcher = ""; + button.onmousedown = function (object_info) { + // The outer function is used to close over a fresh "object_info" + // variable, rather than every onmousedown getting the "object_info" + // of the last entry. + return function (e) { + e.preventDefault(); + clickcatcher = "?src=" + object_info[1]; + switch (e.button) { + case 1: + clickcatcher += ";statpanel_item_click=middle" + break; + case 2: + clickcatcher += ";statpanel_item_click=right" + break; + default: + clickcatcher += ";statpanel_item_click=left" + } + if (e.shiftKey) { + clickcatcher += ";statpanel_item_shiftclick=1"; + } + if (e.ctrlKey) { + clickcatcher += ";statpanel_item_ctrlclick=1"; + } + if (e.altKey) { + clickcatcher += ";statpanel_item_altclick=1"; + } + window.location.href = clickcatcher; + } + }(object_info); + cell.appendChild(button); + + let img = document.createElement("img"); + img.id = object_info[1]; + img.src = object_info[2]; + img.style.verticalAlign = "middle"; + img.onerror = function (object_info) { + return function () { + let delay = imageRetryDelay; + if (!turf_image_errors[object_info[3]]) { + turf_image_errors[object_info[3]] = 0; + delay = imageFirstRetryDelay; + } + turf_image_errors[object_info[3]]++; + if (turf_image_errors[object_info[3]] > imageRetryLimit) { + return; + } + + Byond.sendMessage("Resend-Asset", object_info[3]); + setTimeout(function () { + // Use the failure count as a cachebreaker to force-reload. + let img = document.getElementById(object_info[1]); + img.src = object_info[2] + "?" + turf_image_errors[object_info[3]]; + }, imageRetryDelay); + } + }(object_info); + button.appendChild(img); + + var label = document.createElement("span"); + label.style.marginLeft = "5px"; + label.textContent = object_info[0]; + button.appendChild(label); + + return true; +} + +function listedturf_fill_all() { + for(let i in turf_incomplete_rows) { + let item_index = turf_incomplete_rows[i]; + if(!turf_rows[i] || listedturf_fill_row(turf_rows[i], item_index)) { + delete turf_incomplete_rows[i]; + } + } +} + +var suppress_next_scroll_message = false; +/* We keep a sliding "window" of listedturf items loded. On scroll, we add and + * remove table rows to maintain that window, and update the size of the + * padding row at the top of the table to keep them in the right spot. + */ +function listedturf_scrolled() { + let top_edge = document.documentElement.scrollTop; + let height = document.documentElement.clientHeight; + let bottom_edge = top_edge + height; + let total = document.documentElement.scrollHeight; + let table = document.getElementById("listedturf_table"); + let padding = document.getElementById("listedturf_padding"); + + if (!turf_rows.initialized) { + turf_rows = { + initialized: true, + min_row: 0, + max_row: 0, + }; + } + + if (turf_size === 0) { return; } - setTimeout(function () { - var node = e.target; - var current_attempts = Number(node.getAttribute("data-attempts")) || 0 - if (current_attempts > imageRetryLimit) { - return; + + let desired_min_row = Math.min(turf_size, Math.max(0, Math.floor(top_edge / turf_row_outer_height) - 10)); + let desired_max_row = Math.min(turf_size, desired_min_row + Math.ceil(height / turf_row_outer_height) + 21); + padding.style.height = (desired_min_row * turf_row_outer_height) + "px"; + if(desired_min_row == turf_rows.min_row && desired_max_row == turf_rows.max_row) { + listedturf_fill_all(); + suppress_next_scroll_message = false; + return; + } + + if (desired_min_row < turf_rows.min_row) { + for (let i = desired_min_row; i < turf_rows.min_row; i++) { + listedturf_add_row(table, i - desired_min_row + 1, i); } - var src = node.src; - node.src = null; - node.src = src + '#' + current_attempts; - node.setAttribute("data-attempts", current_attempts + 1) - draw_listedturf(); - }, imageRetryDelay); + } else if (desired_min_row > turf_rows.min_row) { + for (let i = turf_rows.min_row; i < desired_min_row && i < turf_rows.max_row; i++) { + if(turf_rows[i]) { + turf_rows[i].remove(); + delete turf_rows[i]; + } + } + } + turf_rows.min_row = desired_min_row; + + padding.style.height = turf_rows.min_row * turf_row_outer_height + "px" + + + if (desired_max_row < turf_rows.max_row) { + for (let i = Math.max(desired_max_row, turf_rows.min_row); i < turf_rows.max_row; i++) { + if(turf_rows[i]) { + turf_rows[i].remove(); + delete turf_rows[i]; + } + } + } else if (desired_max_row > turf_rows.max_row) { + for (let i = Math.max(turf_rows.min_row, turf_rows.max_row); i < desired_max_row; i++) { + listedturf_add_row(table, i - turf_rows.min_row + 1, i); + } + } + turf_rows.max_row = desired_max_row; + + listedturf_fill_all(); + + if (!suppress_next_scroll_message) { + Byond.sendMessage("Listedturf-Scroll", {"min": turf_rows.min_row, "max": turf_rows.max_row}) + } + suppress_next_scroll_message = false; } function draw_listedturf() { - statcontentdiv.textContent = ""; - var table = document.createElement("table"); - for (var i = 0; i < turfcontents.length; i++) { - var part = turfcontents[i]; - if (storedimages[part[1]] == null && part[2]) { - var img = document.createElement("img"); - img.src = part[2]; - img.id = part[1]; - storedimages[part[1]] = part[2]; - img.onerror = iconError; - table.appendChild(img); - } else { - var img = document.createElement("img"); - img.onerror = iconError; - img.src = storedimages[part[1]]; - img.id = part[1]; - table.appendChild(img); - } - var b = document.createElement("div"); - var clickcatcher = ""; - b.className = "link"; - b.onmousedown = function (part) { - // The outer function is used to close over a fresh "part" variable, - // rather than every onmousedown getting the "part" of the last entry. - return function (e) { - e.preventDefault(); - clickcatcher = "?src=" + part[1]; - switch (e.button) { - case 1: - clickcatcher += ";statpanel_item_click=middle" - break; - case 2: - clickcatcher += ";statpanel_item_click=right" - break; - default: - clickcatcher += ";statpanel_item_click=left" - } - if (e.shiftKey) { - clickcatcher += ";statpanel_item_shiftclick=1"; - } - if (e.ctrlKey) { - clickcatcher += ";statpanel_item_ctrlclick=1"; - } - if (e.altKey) { - clickcatcher += ";statpanel_item_altclick=1"; - } - window.location.href = clickcatcher; - } - }(part); - b.textContent = part[0]; - table.appendChild(b); - table.appendChild(document.createElement("br")); + if(document.getElementById("listedturf_div")) { + let div = document.getElementById("listedturf_div"); + div.style.height = (turf_row_outer_height * turf_size) + "px"; + suppress_next_scroll_message = true; + listedturf_scrolled(); + return } - document.getElementById("statcontent").appendChild(table); + + statcontentdiv.textContent = ""; + turf_rows = {}; + window.onscroll = function() { listedturf_scrolled(); }; + + let div = document.createElement("div"); + div.id = "listedturf_div"; + div.style.height = (turf_row_outer_height * turf_size) + "px"; + document.getElementById("statcontent").appendChild(div); + + let table = document.createElement("table"); + table.id = "listedturf_table"; + table.style.width = "100%" + table.style.height = "100%" + div.appendChild(table) + + let padding = document.createElement("tr"); + padding.id = "listedturf_padding"; + padding.style.height = "0px"; + padding.style.padding = "0px" + padding.style.margin = "0px" + table.appendChild(padding); + + let end_flex = document.createElement("tr"); + end_flex.id = "listedturf_end_flex"; + end_flex.style.height = "100%"; + end_flex.style.padding = "0px" + end_flex.style.margin = "0px" + table.appendChild(end_flex); + + suppress_next_scroll_message = true; + listedturf_scrolled(); } function remove_listedturf() { @@ -452,6 +603,14 @@ function remove_listedturf() { if (current_tab == turfname) { tab_change("Status"); } + if(document.getElementById("listedturf_div")) { + document.getElementById("listedturf_div").remove(); + } + turf_rows = {}; + turf_incomplete_rows = {}; + turf_size = 0; + turfcontents = {"total": 0}; + turfname = ""; } function remove_mc() { @@ -738,6 +897,7 @@ Byond.subscribeTo('remove_mc_tab', function (removeHref) { Byond.subscribeTo('update_listedturf', function (TC) { turfcontents = TC; + turf_size = TC["total"]; if (current_tab == turfname) { draw_listedturf(); }