Amortize the cost of creating preference assets by caching them per git revision on production, reducing best case init times by ~20 seconds (#63503)

Preference asset creation, which while consistently created in early assets, can be requested at any time before then and often is, currently takes about 15 to 25 seconds to produce. Because of extremely hard to reproduce BYOND icon bugs, most of this is done on the same tick.

Lowering the cost of initialization itself is very tricky. Some of it we can theoretically optimize, such as creating humans for antagonists, others we can't, such as the raw cost of icon blending.

Furthermore, adding new icons later down the line would just increase this initialization time even more.

Instead of optimizing the asset creation, which is an uphill battle, this instead chooses to amortize the cost by caching preference assets created per git revision. This means that preference assets will be created, with their long delay, only once whenever the code changes.

This is done on a config, defaulting to on so that production needs no changes, as the whole point of these being made at runtime at all is that it keeps assets/art styles consistent, and PRs making subtle bugs that break preference generation in some way is not uncommon. On development, your git revision will stay the same until you commit, no matter what code changes you make.
This commit is contained in:
Mothblocks
2022-01-01 04:36:30 +00:00
committed by GitHub
parent d4b3462362
commit af8902331d
8 changed files with 134 additions and 37 deletions
+107 -7
View File
@@ -1,3 +1,4 @@
#define ASSET_CROSS_ROUND_CACHE_DIRECTORY "tmp/assets"
//These datums are used to populate the asset cache, the proc "register()" does this.
//Place any asset datums you create in asset_list_items.dm
@@ -16,6 +17,11 @@ GLOBAL_LIST_EMPTY(asset_datums)
/// Whether or not this asset should be loaded in the "early assets" SS
var/early = FALSE
/// Whether or not this asset can be cached across rounds of the same commit under the `CACHE_ASSETS` config.
/// This is not a *guarantee* the asset will be cached. Not all asset subtypes respect this field, and the
/// config can, of course, be disabled.
var/cross_round_cachable = FALSE
/datum/asset/New()
GLOB.asset_datums[type] = src
register()
@@ -36,6 +42,9 @@ GLOBAL_LIST_EMPTY(asset_datums)
/datum/asset/proc/send(client)
return
/// Returns whether or not the asset should attempt to read from cache
/datum/asset/proc/should_refresh()
return !cross_round_cachable || !CONFIG_GET(flag/cache_assets)
/// If you don't need anything complicated.
/datum/asset/simple
@@ -105,10 +114,33 @@ GLOBAL_LIST_EMPTY(asset_datums)
var/name
var/list/sizes = list() // "32x32" -> list(10, icon/normal, icon/stripped)
var/list/sprites = list() // "foo_bar" -> list("32x32", 5)
var/list/cached_spritesheets_needed
var/generating_cache = FALSE
/datum/asset/spritesheet/should_refresh()
if (..())
return TRUE
// Static so that the result is the same, even when the files are created, for this run
var/static/should_refresh = null
if (isnull(should_refresh))
// `fexists` seems to always fail on static-time
should_refresh = !fexists("[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name].css")
return should_refresh
/datum/asset/spritesheet/register()
SHOULD_NOT_OVERRIDE(TRUE)
if (!name)
CRASH("spritesheet [type] cannot register without a name")
if (!should_refresh() && read_from_cache())
return
create_spritesheets()
ensure_stripped()
for(var/size_id in sizes)
var/size = sizes[size_id]
@@ -120,23 +152,32 @@ GLOBAL_LIST_EMPTY(asset_datums)
SSassets.transport.register_asset(res_name, fcopy_rsc(fname))
fdel(fname)
/datum/asset/spritesheet/send(client/C)
if (CONFIG_GET(flag/cache_assets) && cross_round_cachable)
write_to_cache()
/datum/asset/spritesheet/send(client/client)
if (!name)
return
if (!should_refresh())
return send_from_cache(client)
var/all = list("spritesheet_[name].css")
for(var/size_id in sizes)
all += "[name]_[size_id].png"
. = SSassets.transport.send_assets(C, all)
. = SSassets.transport.send_assets(client, all)
/datum/asset/spritesheet/get_url_mappings()
if (!name)
return
if (!should_refresh())
return get_cached_url_mappings()
. = list("spritesheet_[name].css" = SSassets.transport.get_asset_url("spritesheet_[name].css"))
for(var/size_id in sizes)
.["[name]_[size_id].png"] = SSassets.transport.get_asset_url("[name]_[size_id].png")
/datum/asset/spritesheet/proc/ensure_stripped(sizes_to_strip = sizes)
for(var/size_id in sizes_to_strip)
var/size = sizes[size_id]
@@ -158,7 +199,7 @@ GLOBAL_LIST_EMPTY(asset_datums)
for (var/size_id in sizes)
var/size = sizes[size_id]
var/icon/tiny = size[SPRSZ_ICON]
out += ".[name][size_id]{display:inline-block;width:[tiny.Width()]px;height:[tiny.Height()]px;background:url('[SSassets.transport.get_asset_url("[name]_[size_id].png")]') no-repeat;}"
out += ".[name][size_id]{display:inline-block;width:[tiny.Width()]px;height:[tiny.Height()]px;background:url('[get_background_url("[name]_[size_id].png")]') no-repeat;}"
for (var/sprite_id in sprites)
var/sprite = sprites[sprite_id]
@@ -176,6 +217,64 @@ GLOBAL_LIST_EMPTY(asset_datums)
return out.Join("\n")
/datum/asset/spritesheet/proc/read_from_cache()
var/replaced_css = file2text("[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name].css")
var/regex/find_background_urls = regex(@"background:url\('%(.+?)%'\)", "g")
while (find_background_urls.Find(replaced_css))
var/asset_id = find_background_urls.group[1]
var/asset_cache_item = SSassets.transport.register_asset(asset_id, "[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[asset_id]")
var/asset_url = SSassets.transport.get_asset_url(asset_cache_item = asset_cache_item)
replaced_css = replacetext(replaced_css, find_background_urls.match, "background:url('[asset_url]')")
LAZYADD(cached_spritesheets_needed, asset_id)
var/replaced_css_filename = "data/spritesheets/spritesheet_[name].css"
rustg_file_write(replaced_css, replaced_css_filename)
SSassets.transport.register_asset("spritesheet_[name].css", replaced_css_filename)
fdel(replaced_css_filename)
return TRUE
/datum/asset/spritesheet/proc/send_from_cache(client/client)
if (isnull(cached_spritesheets_needed))
stack_trace("cached_spritesheets_needed was null when sending assets from [type] from cache")
cached_spritesheets_needed = list()
return SSassets.transport.send_assets(client, cached_spritesheets_needed + "spritesheet_[name].css")
/// Returns the URL to put in the background:url of the CSS asset
/datum/asset/spritesheet/proc/get_background_url(asset)
if (generating_cache)
return "%[asset]%"
else
return SSassets.transport.get_asset_url(asset)
/datum/asset/spritesheet/proc/write_to_cache()
for (var/size_id in sizes)
fcopy(SSassets.cache["[name]_[size_id].png"].resource, "[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name]_[size_id].png")
generating_cache = TRUE
var/mock_css = generate_css()
generating_cache = FALSE
rustg_file_write(mock_css, "[ASSET_CROSS_ROUND_CACHE_DIRECTORY]/spritesheet.[name].css")
/datum/asset/spritesheet/proc/get_cached_url_mappings()
var/list/mappings = list()
mappings["spritesheet_[name].css"] = SSassets.transport.get_asset_url("spritesheet_[name].css")
for (var/asset_name in cached_spritesheets_needed)
mappings[asset_name] = SSassets.transport.get_asset_url(asset_name)
return mappings
/// Override this in order to start the creation of the spritehseet.
/// This is where all your Insert, InsertAll, etc calls should be inside.
/datum/asset/spritesheet/proc/create_spritesheets()
SHOULD_CALL_PARENT(FALSE)
CRASH("create_spritesheets() not implemented for [type]!")
/datum/asset/spritesheet/proc/Insert(sprite_name, icon/I, icon_state="", dir=SOUTH, frame=1, moving=FALSE)
I = icon(I, icon_state=icon_state, dir=dir, frame=frame, moving=moving)
if (!I || !length(icon_states(I))) // that direction or state doesn't exist
@@ -281,10 +380,9 @@ GLOBAL_LIST_EMPTY(asset_datums)
_abstract = /datum/asset/spritesheet/simple
var/list/assets
/datum/asset/spritesheet/simple/register()
/datum/asset/spritesheet/simple/create_spritesheets()
for (var/key in assets)
Insert(key, assets[key])
..()
//Generates assets based on iconstates of a single icon
/datum/asset/simple/icon_states
@@ -393,3 +491,5 @@ GLOBAL_LIST_EMPTY(asset_datums)
/datum/asset/json/proc/generate()
SHOULD_CALL_PARENT(FALSE)
CRASH("generate() not implemented for [type]!")
#undef ASSET_CROSS_ROUND_CACHE_DIRECTORY
+10 -20
View File
@@ -153,7 +153,7 @@
/datum/asset/spritesheet/chat
name = "chat"
/datum/asset/spritesheet/chat/register()
/datum/asset/spritesheet/chat/create_spritesheets()
InsertAll("emoji", EMOJI_SET)
// pre-loading all lanugage icons also helps to avoid meta
InsertAll("language", 'icons/misc/language.dmi')
@@ -164,7 +164,6 @@
if (icon != 'icons/misc/language.dmi')
var/icon_state = initial(L.icon_state)
Insert("language-[icon_state]", icon, icon_state=icon_state)
..()
/datum/asset/simple/lobby
assets = list(
@@ -208,9 +207,8 @@
/datum/asset/spritesheet/simple/achievements
name ="achievements"
/datum/asset/spritesheet/simple/achievements/register()
/datum/asset/spritesheet/simple/achievements/create_spritesheets()
InsertAll("", ACHIEVEMENTS_SET)
return ..()
/datum/asset/spritesheet/simple/pills
name = "pills"
@@ -270,15 +268,14 @@
/datum/asset/spritesheet/pipes
name = "pipes"
/datum/asset/spritesheet/pipes/register()
/datum/asset/spritesheet/pipes/create_spritesheets()
for (var/each in list('icons/obj/atmospherics/pipes/pipe_item.dmi', 'icons/obj/atmospherics/pipes/disposal.dmi', 'icons/obj/atmospherics/pipes/transit_tube.dmi', 'icons/obj/plumbing/fluid_ducts.dmi'))
InsertAll("", each, GLOB.alldirs)
..()
/datum/asset/spritesheet/supplypods
name = "supplypods"
/datum/asset/spritesheet/supplypods/register()
/datum/asset/spritesheet/supplypods/create_spritesheets()
for (var/style in 1 to length(GLOB.podstyles))
if (style == STYLE_SEETHROUGH)
Insert("pod_asset[style]", icon('icons/obj/supplypods.dmi' , "seethrough-icon"))
@@ -302,13 +299,12 @@
glow = "pod_glow_[glow]"
podIcon.Blend(icon('icons/obj/supplypods.dmi', glow), ICON_OVERLAY)
Insert("pod_asset[style]", podIcon)
return ..()
// Representative icons for each research design
/datum/asset/spritesheet/research_designs
name = "design"
/datum/asset/spritesheet/research_designs/register()
/datum/asset/spritesheet/research_designs/create_spritesheets()
for (var/path in subtypesof(/datum/design))
var/datum/design/D = path
@@ -367,12 +363,11 @@
I.Blend(icon(icon_file, keyboard, SOUTH), ICON_OVERLAY)
Insert(initial(D.id), I)
return ..()
/datum/asset/spritesheet/vending
name = "vending"
/datum/asset/spritesheet/vending/register()
/datum/asset/spritesheet/vending/create_spritesheets()
for (var/k in GLOB.vending_products)
var/atom/item = k
if (!ispath(item, /atom))
@@ -405,7 +400,6 @@
var/imgid = replacetext(replacetext("[item]", "/obj/item/", ""), "/", "-")
Insert(imgid, I)
return ..()
/datum/asset/simple/genetics
assets = list(
@@ -427,19 +421,17 @@
/datum/asset/spritesheet/sheetmaterials
name = "sheetmaterials"
/datum/asset/spritesheet/sheetmaterials/register()
/datum/asset/spritesheet/sheetmaterials/create_spritesheets()
InsertAll("", 'icons/obj/stack_objects.dmi')
// Special case to handle Bluespace Crystals
Insert("polycrystal", 'icons/obj/telescience.dmi', "polycrystal")
..()
/datum/asset/spritesheet/mafia
name = "mafia"
/datum/asset/spritesheet/mafia/register()
/datum/asset/spritesheet/mafia/create_spritesheets()
InsertAll("", 'icons/obj/mafia.dmi')
..()
/datum/asset/simple/portraits
assets = list()
@@ -470,7 +462,7 @@
/datum/asset/spritesheet/fish
name = "fish"
/datum/asset/spritesheet/fish/register()
/datum/asset/spritesheet/fish/create_spritesheets()
for (var/path in subtypesof(/datum/aquarium_behaviour/fish))
var/datum/aquarium_behaviour/fish/fish_type = path
var/fish_icon = initial(fish_type.icon)
@@ -479,7 +471,6 @@
if(sprites[id]) //no dupes
continue
Insert(id, fish_icon, fish_icon_state)
..()
/datum/asset/simple/adventure
assets = list(
@@ -524,12 +515,11 @@
name = "moods"
var/iconinserted = 1
/datum/asset/spritesheet/moods/register()
/datum/asset/spritesheet/moods/create_spritesheets()
for(var/i in 1 to 9)
var/target_to_insert = "mood"+"[iconinserted]"
Insert(target_to_insert, 'icons/hud/screen_gen.dmi', target_to_insert)
iconinserted++
..()
/datum/asset/spritesheet/moods/ModifyInserted(icon/pre_asset)
var/blended_color