From c784ca902e1dbc84a65568da726866581525da46 Mon Sep 17 00:00:00 2001 From: MrStonedOne Date: Sun, 15 Nov 2015 23:49:05 -0800 Subject: [PATCH] Ports /vg/'s asset cache system; lowers interface lag when clients connect. Byond will queue all browse(), browse_rsc(), and winset() calls to the client, to ensure they load in order, and ensure any resources from a browse_rsc call (css/html/js) are already at the client by the time any html windows loads. How ever, each file is processed separately, and byond will wait for the reply from the client before it will send the next file. Our current system sends all of these html resource files to the client at once when they connect, this is the sole cause of the lag when a client connects. Byond will not send the client a file it already has, but it has to ask the client first, and it does so one file at a time, waiting for a reply from the client before sending the next one down the pipe. This system fixes that. Basically it works like this: Client connects: nothing happens, no massive queuing of browse_rsc() calls, so no interface delay Client opens a asset_cache controlled html based interface Asset cache gets notified by the html based interface what assets the client needs to have. Asset cache checks to see if it's sent that client those files. Asset cache sends the missing ones, adding them to the list of assets the client has. This basically spreads out the delay to when you first open a window that uses resources, where it is much more manageable. I've kinda done a halfass port without too much thought, I see some room for improvement to better fit /tg/'s coding style and make the system more flexible. I'm PRing this because if I don't, it will never get finished. PDAs and html_interface has been imported in to the new system lazily to test. at 100ms connection start interface lag went from 35 seconds to 16 seconds. Nanoui hasn't been imported, and once it is, that should drop down to almost nothing. I'll work on this some more after some sleep. --- code/controllers/master_controller.dm | 1 + code/game/objects/items/devices/PDA/PDA.dm | 4 +- code/modules/client/asset_cache.dm | 174 ++++++++++++++++++ code/modules/client/client procs.dm | 34 ---- code/modules/html_interface/cards/cards.dm | 9 +- code/modules/html_interface/html_interface.dm | 30 ++- .../html_interface/nanotrasen/nanotrasen.dm | 11 +- tgstation.dme | 1 + 8 files changed, 207 insertions(+), 57 deletions(-) create mode 100644 code/modules/client/asset_cache.dm diff --git a/code/controllers/master_controller.dm b/code/controllers/master_controller.dm index dc1d0fdcdf0..5eed9591ec3 100644 --- a/code/controllers/master_controller.dm +++ b/code/controllers/master_controller.dm @@ -67,6 +67,7 @@ calculate the longest number of ticks the MC can wait between each cycle without sleep(-1) crewmonitor.generateMiniMaps() + populate_asset_cache() world << "Initializations complete" diff --git a/code/game/objects/items/devices/PDA/PDA.dm b/code/game/objects/items/devices/PDA/PDA.dm index d7384c6b6d1..324b2cd4dd6 100644 --- a/code/game/objects/items/devices/PDA/PDA.dm +++ b/code/game/objects/items/devices/PDA/PDA.dm @@ -256,9 +256,9 @@ var/global/list/obj/item/device/pda/PDAs = list() return attack_self(M) return -//NOTE: graphic resources are loaded on client login /obj/item/device/pda/attack_self(mob/user) - + var/datum/asset/simple/pda/assetcache = new() + send_asset_list(user, assetcache.assets, verify = FALSE) user.set_machine(src) if(active_uplink_check(user)) diff --git a/code/modules/client/asset_cache.dm b/code/modules/client/asset_cache.dm new file mode 100644 index 00000000000..74404c10fd2 --- /dev/null +++ b/code/modules/client/asset_cache.dm @@ -0,0 +1,174 @@ +#define ASSET_CACHE_SEND_TIMEOUT 25 // Amount of time MAX to send an asset, if this get exceeded we cancel the sleeping. + +//List of ALL assets for the above, format is list(filename = asset). +/var/global/list/asset_cache = list() + +/client + var/list/cache = list() // List of all assets sent to this client by the asset cache. + var/list/completed_asset_jobs = list() // List of all completed jobs, awaiting acknowledgement. + var/list/sending = list() + var/last_asset_job = 0 // Last job done. + +//This proc sends the asset to the client, but only if it needs it. +/proc/send_asset(var/client/client, var/asset_name, var/verify = TRUE) + if(!istype(client)) + if(ismob(client)) + var/mob/M = client + if(M.client) + client = M.client + + else + return 0 + + else + return 0 + + if(client.cache.Find(asset_name) || client.sending.Find(asset_name)) + return 0 + + client << browse_rsc(asset_cache[asset_name], asset_name) + if(!verify || !winexists(client, "asset_cache_browser")) // Can't access the asset cache browser, rip. + client.cache += asset_name + return 1 + + client.sending |= asset_name + var/job = ++client.last_asset_job + + client << browse({" + + "}, "window=asset_cache_browser") + + var/t = 0 + var/timeout_time = ASSET_CACHE_SEND_TIMEOUT * client.sending.len + while(client && !client.completed_asset_jobs.Find(job) && t < timeout_time) // Reception is handled in Topic() + sleep(1) // Lock up the caller until this is received. + t++ + + if(client) + client.sending -= asset_name + client.cache |= asset_name + client.completed_asset_jobs -= job + + return 1 + +/proc/send_asset_list(var/client/client, var/list/asset_list, var/verify = TRUE) + if(!istype(client)) + if(ismob(client)) + var/mob/M = client + if(M.client) + client = M.client + + else + return 0 + + else + return 0 + + var/list/unreceived = asset_list - (client.cache + client.sending) + if(!unreceived || !unreceived.len) + return 0 + + for(var/asset in unreceived) + client << browse_rsc(asset_cache[asset], asset) + + if(!verify || !winexists(client, "asset_cache_browser")) // Can't access the asset cache browser, rip. + client.cache += unreceived + return 1 + + client.sending |= unreceived + var/job = ++client.last_asset_job + + client << browse({" + + "}, "window=asset_cache_browser") + + var/t = 0 + var/timeout_time = ASSET_CACHE_SEND_TIMEOUT * client.sending.len + while(client && !client.completed_asset_jobs.Find(job) && t < timeout_time) // Reception is handled in Topic() + sleep(1) // Lock up the caller until this is received. + t++ + + if(client) + client.sending -= unreceived + client.cache |= unreceived + client.completed_asset_jobs -= job + + return 1 + +//This proc "registers" an asset, it adds it to the cache for further use, you cannot touch it from this point on or you'll fuck things up. +//if it's an icon or something be careful, you'll have to copy it before further use. +/proc/register_asset(var/asset_name, var/asset) + asset_cache |= asset_name + asset_cache[asset_name] = asset + + +//From here on out it's populating the asset cache. + +/proc/populate_asset_cache() + for(var/type in typesof(/datum/asset) - list(/datum/asset, /datum/asset/simple)) + var/datum/asset/A = new type() + A.register() + +//These datums are used to populate the asset cache, the proc "register()" does this. +/datum/asset/proc/register() + return + +//If you don't need anything complicated. +/datum/asset/simple + var/assets = list() + +/datum/asset/simple/register() + for(var/asset_name in assets) + register_asset(asset_name, assets[asset_name]) + +//DEFINITIONS FOR ASSET DATUMS START HERE. + + +/datum/asset/simple/pda + assets = list( + "pda_atmos.png" = 'icons/pda_icons/pda_atmos.png', + "pda_back.png" = 'icons/pda_icons/pda_back.png', + "pda_bell.png" = 'icons/pda_icons/pda_bell.png', + "pda_blank.png" = 'icons/pda_icons/pda_blank.png', + "pda_boom.png" = 'icons/pda_icons/pda_boom.png', + "pda_bucket.png" = 'icons/pda_icons/pda_bucket.png', + "pda_medbot.png" = 'icons/pda_icons/pda_medbot.png', + "pda_floorbot.png" = 'icons/pda_icons/pda_floorbot.png', + "pda_cleanbot.png" = 'icons/pda_icons/pda_cleanbot.png', + "pda_crate.png" = 'icons/pda_icons/pda_crate.png', + "pda_cuffs.png" = 'icons/pda_icons/pda_cuffs.png', + "pda_eject.png" = 'icons/pda_icons/pda_eject.png', + "pda_exit.png" = 'icons/pda_icons/pda_exit.png', + "pda_flashlight.png" = 'icons/pda_icons/pda_flashlight.png', + "pda_honk.png" = 'icons/pda_icons/pda_honk.png', + "pda_mail.png" = 'icons/pda_icons/pda_mail.png', + "pda_medical.png" = 'icons/pda_icons/pda_medical.png', + "pda_menu.png" = 'icons/pda_icons/pda_menu.png', + "pda_mule.png" = 'icons/pda_icons/pda_mule.png', + "pda_notes.png" = 'icons/pda_icons/pda_notes.png', + "pda_power.png" = 'icons/pda_icons/pda_power.png', + "pda_rdoor.png" = 'icons/pda_icons/pda_rdoor.png', + "pda_reagent.png" = 'icons/pda_icons/pda_reagent.png', + "pda_refresh.png" = 'icons/pda_icons/pda_refresh.png', + "pda_scanner.png" = 'icons/pda_icons/pda_scanner.png', + "pda_signaler.png" = 'icons/pda_icons/pda_signaler.png', + "pda_status.png" = 'icons/pda_icons/pda_status.png' + ) + + +//Registers HTML Interface assets. +/datum/asset/HTML_interface/register() + for(var/path in typesof(/datum/html_interface)) + var/datum/html_interface/hi = new path() + hi.registerResources() + + +//Registers NanoUI assets. +/datum/asset/NanoUI/register() + for(var/path in typesof(/datum/html_interface)) + var/datum/html_interface/hi = new path() + hi.registerResources() diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index 295d39ceef3..deab198401a 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -318,13 +318,6 @@ var/next_external_rsc = 0 //send resources to the client. It's here in its own proc so we can move it around easiliy if need be /client/proc/send_resources() - spawn - // Preload the HTML interface. This needs to be done due to BYOND bug http://www.byond.com/forum/?post=1487244 - var/datum/html_interface/hi - for (var/type in typesof(/datum/html_interface)) - hi = new type(null) - hi.sendResources(src) - // Preload the crew monitor. This needs to be done due to BYOND bug http://www.byond.com/forum/?post=1487244 spawn if (crewmonitor && crewmonitor.initialized) @@ -338,33 +331,6 @@ var/next_external_rsc = 0 'html/browser/common.css', 'html/browser/scannernew.css', 'html/browser/playeroptions.css', - 'icons/pda_icons/pda_atmos.png', - 'icons/pda_icons/pda_back.png', - 'icons/pda_icons/pda_bell.png', - 'icons/pda_icons/pda_blank.png', - 'icons/pda_icons/pda_boom.png', - 'icons/pda_icons/pda_bucket.png', - 'icons/pda_icons/pda_medbot.png', - 'icons/pda_icons/pda_floorbot.png', - 'icons/pda_icons/pda_cleanbot.png', - 'icons/pda_icons/pda_crate.png', - 'icons/pda_icons/pda_cuffs.png', - 'icons/pda_icons/pda_eject.png', - 'icons/pda_icons/pda_exit.png', - 'icons/pda_icons/pda_flashlight.png', - 'icons/pda_icons/pda_honk.png', - 'icons/pda_icons/pda_mail.png', - 'icons/pda_icons/pda_medical.png', - 'icons/pda_icons/pda_menu.png', - 'icons/pda_icons/pda_mule.png', - 'icons/pda_icons/pda_notes.png', - 'icons/pda_icons/pda_power.png', - 'icons/pda_icons/pda_rdoor.png', - 'icons/pda_icons/pda_reagent.png', - 'icons/pda_icons/pda_refresh.png', - 'icons/pda_icons/pda_scanner.png', - 'icons/pda_icons/pda_signaler.png', - 'icons/pda_icons/pda_status.png', 'icons/stamp_icons/large_stamp-clown.png', 'icons/stamp_icons/large_stamp-deny.png', 'icons/stamp_icons/large_stamp-ok.png', diff --git a/code/modules/html_interface/cards/cards.dm b/code/modules/html_interface/cards/cards.dm index ced6446ca95..88e4311bf5d 100644 --- a/code/modules/html_interface/cards/cards.dm +++ b/code/modules/html_interface/cards/cards.dm @@ -7,8 +7,7 @@ src.head = src.head + "" src.updateLayout("
") -/datum/html_interface/cards/sendResources(client/client) - . = ..() // we need the default resources - - client << browse_rsc('cards.css') - client << browse_rsc('cards.png') \ No newline at end of file +/datum/html_interface/cards/registerResources(var/list/resources = list()) + resources["cards.css"] = 'cards.css' + resources["cards.png"] = 'cards.png' + ..(resources) \ No newline at end of file diff --git a/code/modules/html_interface/html_interface.dm b/code/modules/html_interface/html_interface.dm index ffc04407f66..07a436813ac 100644 --- a/code/modules/html_interface/html_interface.dm +++ b/code/modules/html_interface/html_interface.dm @@ -63,6 +63,8 @@ Closes the interface on all clients. When working with byond:// links make sure to reference the HTML interface object and NOT the original object. Topic() will still be called on your object, but it will pass through the HTML interface first allowing interception at a higher level. +If you want to use custom resources(images/css/js) with an existing interface: +You have to use modules/client/asset_cache to ensure they get sent BEFORE the interface opens ** Sample code ** @@ -105,6 +107,10 @@ mob/verb/test() // The initial height of the browser control, used when the window is first shown to a client. var/height + // A type associated list of assets the interface needs. + //Sent to the client when the interface opens on the client for the first time. + var/static/asset_list = list() + /datum/html_interface/New(atom/ref, title, width = 700, height = 480, head = "") html_interfaces.Add(src) @@ -126,12 +132,19 @@ mob/verb/test() /* * Hooks */ /datum/html_interface/proc/specificRenderTitle(datum/html_interface_client/hclient, ignore_cache = FALSE) -/datum/html_interface/proc/sendResources(client/client) - client << browse_rsc('js/jquery.min.js', "jquery.min.js") - client << browse_rsc('js/bootstrap.min.js', "bootstrap.min.js") - client << browse_rsc('css/bootstrap.min.css', "bootstrap.min.css") - client << browse_rsc('css/html_interface.css', "html_interface.css") - client << browse_rsc('js/html_interface.js', "html_interface.js") +//if you need to override this, either call ..() or add your resources to asset_list +/datum/html_interface/proc/registerResources(var/list/resources = list()) + resources["jquery.min.js"] = 'js/jquery.min.js' + resources["bootstrap.min.js"] = 'js/bootstrap.min.js' + resources["bootstrap.min.css"] = 'css/bootstrap.min.css' + resources["html_interface.css"] = 'css/html_interface.css' + resources["html_interface.js"] = 'js/html_interface.js' + var/assetlist = list() + for (var/R in resources) + register_asset(R,resources[R]) + assetlist += R + + asset_list[type] = assetlist /datum/html_interface/proc/createWindow(datum/html_interface_client/hclient) winclone(hclient.client, "window", "browser_\ref[src]") @@ -207,10 +220,7 @@ mob/verb/test() hclient = getClient(hclient, TRUE) if (istype(hclient)) - // This needs to be commented out due to BYOND bug http://www.byond.com/forum/?post=1487244 - // /client/proc/send_resources() executes this per client to avoid the bug, but by using it here files may be deleted just as the HTML is loaded, - // causing file not found errors. -// src.sendResources(hclient.client) + send_asset_list(hclient.client,asset_list[type], TRUE) if (!winexists(hclient.client, "browser_\ref[src]")) src.createWindow(hclient) diff --git a/code/modules/html_interface/nanotrasen/nanotrasen.dm b/code/modules/html_interface/nanotrasen/nanotrasen.dm index 3c1cfa47ef6..5ae9407093f 100644 --- a/code/modules/html_interface/nanotrasen/nanotrasen.dm +++ b/code/modules/html_interface/nanotrasen/nanotrasen.dm @@ -29,12 +29,11 @@ The client is optional and may be a /mob, /client or /html_interface_client obje // Update the title in our custom header (in addition to default functionality) winset(hclient.client, "browser_\ref[src].uiTitle", list2params(list("text" = "[src.title]"))) -/datum/html_interface/nanotrasen/sendResources(client/client) - . = ..() // we need the default resources - - client << browse_rsc('uiBg.png') - client << browse_rsc('uiBgcenter.png') - client << browse_rsc('nanotrasen.css') +/datum/html_interface/nanotrasen/registerResources(var/list/resources = list()) + resources["uiBg.png"] = 'uiBg.png' + resources["uiBgcenter.png"] = 'uiBgcenter.png' + resources["nanotrasen.css"] = 'nanotrasen.css' + ..(resources) /datum/html_interface/nanotrasen/createWindow(datum/html_interface_client/hclient) . = ..() // we want the default window diff --git a/tgstation.dme b/tgstation.dme index d21a9b4294e..72b87999480 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -923,6 +923,7 @@ #include "code\modules\awaymissions\mission_code\spacebattle.dm" #include "code\modules\awaymissions\mission_code\stationCollision.dm" #include "code\modules\awaymissions\mission_code\wildwest.dm" +#include "code\modules\client\asset_cache.dm" #include "code\modules\client\client defines.dm" #include "code\modules\client\client procs.dm" #include "code\modules\client\message.dm"