performance: Unique caching revamp!

- Cache civ uniques ignoring conditionals, for better reuse
- Cache civ uniques *when querying city uniques*, same

This allows us to use the same UniqueCache between cities, and we still get the performance boost of "search once filter always", since the searching is the heavier part, and in any case we'll always have to do the filtering by conditionals either way
This commit is contained in:
Yair Morgenstern 2023-08-31 14:15:32 +03:00
parent 1e027199a6
commit 97b16d2b5f
6 changed files with 33 additions and 19 deletions

View File

@ -33,6 +33,7 @@ import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.Speed
import com.unciv.models.ruleset.nation.Difficulty
import com.unciv.models.ruleset.unique.LocalUniqueCache
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr
import com.unciv.ui.audio.MusicMood
@ -669,10 +670,11 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
civInfo.cache.updateCitiesConnectedToCapital(true)
// We need to determine the GLOBAL happiness state in order to determine the city stats
val localUniqueCache = LocalUniqueCache()
for (city in civInfo.cities) {
city.cityStats.updateTileStats() // Some nat wonders can give happiness!
city.cityStats.updateTileStats(localUniqueCache) // Some nat wonders can give happiness!
city.cityStats.updateCityHappiness(
city.cityConstructions.getStats()
city.cityConstructions.getStats(localUniqueCache)
)
}
@ -689,7 +691,8 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
if (!ruleset.tileResources.containsKey(city.demandedResource))
city.demandedResource = ""
city.cityStats.update()
// No uniques have changed since the cache was created, so we can still use it
city.cityStats.update(localUniqueCache=localUniqueCache)
}
}
}

View File

@ -345,8 +345,8 @@ object Automation {
return when {
powerLevelComparison > 2 -> ThreatLevel.VeryHigh
powerLevelComparison > 1.5f -> ThreatLevel.High
powerLevelComparison < (1 / 1.5f) -> ThreatLevel.Low
powerLevelComparison < 0.5f -> ThreatLevel.VeryLow
powerLevelComparison < (1 / 1.5f) -> ThreatLevel.Low
else -> ThreatLevel.Medium
}
}

View File

@ -622,7 +622,7 @@ class City : IsPartOfGameInfoSerialization {
}
// Uniques special to this city
private fun getLocalMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals = StateForConditionals(civ, this)): Sequence<Unique> {
fun getLocalMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals = StateForConditionals(civ, this)): Sequence<Unique> {
return (
cityConstructions.builtBuildingUniqueMap.getUniques(uniqueType).filter { it.isLocalEffect }
+ religion.getUniques().filter { it.isOfType(uniqueType) }

View File

@ -110,9 +110,8 @@ class CityConstructions : IsPartOfGameInfoSerialization {
/**
* @return [Stats] provided by all built buildings in city plus the bonus from Library
*/
fun getStats(): StatTreeNode {
fun getStats(localUniqueCache: LocalUniqueCache): StatTreeNode {
val stats = StatTreeNode()
val localUniqueCache = LocalUniqueCache()
for (building in getBuiltBuildings())
stats.addStats(building.getStats(city, localUniqueCache), building.name)
return stats

View File

@ -354,9 +354,8 @@ class CityStats(val city: City) {
//endregion
//region State-Changing Methods
fun updateTileStats() {
fun updateTileStats(localUniqueCache:LocalUniqueCache = LocalUniqueCache()) {
val stats = Stats()
val localUniqueCache = LocalUniqueCache()
val workedTiles = city.tilesInRange.asSequence()
.filter {
city.location == it.position
@ -490,12 +489,14 @@ class CityStats(val city: City) {
fun update(currentConstruction: IConstruction = city.cityConstructions.getCurrentConstruction(),
updateTileStats:Boolean = true,
updateCivStats:Boolean = true) {
if (updateTileStats) updateTileStats()
updateCivStats:Boolean = true,
localUniqueCache:LocalUniqueCache = LocalUniqueCache()) {
if (updateTileStats) updateTileStats(localUniqueCache)
// We need to compute Tile yields before happiness
val statsFromBuildings = city.cityConstructions.getStats() // this is performance heavy, so calculate once
val statsFromBuildings = city.cityConstructions.getStats(localUniqueCache) // this is performance heavy, so calculate once
updateBaseStatList(statsFromBuildings)
updateCityHappiness(statsFromBuildings)
updateStatPercentBonusList(currentConstruction)

View File

@ -325,11 +325,18 @@ class LocalUniqueCache(val cache:Boolean = true) {
uniqueType: UniqueType,
ignoreConditionals: Boolean = false
): Sequence<Unique> {
val stateForConditionals = if (ignoreConditionals) StateForConditionals.IgnoreConditionals
else StateForConditionals(city.civ, city)
// City uniques are a combination of *global civ* uniques plus *city relevant* uniques (see City.getMatchingUniques())
// We can cache the civ uniques separately, so if we have several cities using the same cache,
// we can cache the list of *civ uniques* to reuse between cities.
// This is assuming that we're ignoring conditionals, because otherwise -
// the conditionals will render the the *filtered uniques* different anyway, so there's no reason to cache...
val uniques = if (!ignoreConditionals) city.getMatchingUniques(uniqueType, StateForConditionals(city.civ, city))
else forCivGetMatchingUniques(city.civ, uniqueType, StateForConditionals.IgnoreConditionals) +
city.getLocalMatchingUniques(uniqueType, StateForConditionals.IgnoreConditionals)
return get(
"city-${city.id}-${uniqueType.name}-${ignoreConditionals}",
city.getMatchingUniques(uniqueType, stateForConditionals)
uniques
)
}
@ -340,12 +347,16 @@ class LocalUniqueCache(val cache:Boolean = true) {
civ
)
): Sequence<Unique> {
val sequence = civ.getMatchingUniques(uniqueType, stateForConditionals)
if (!cache) return sequence // So we don't need to toString the stateForConditionals
val sequence = civ.getMatchingUniques(uniqueType, StateForConditionals.IgnoreConditionals)
// The uniques CACHED are ALL civ uniques, regardless of conditional matching.
// The uniques RETURNED are uniques AFTER conditional matching.
// This allows reuse of the cached values, between runs with different conditionals -
// for example, iterate on all tiles and get StatPercentForObject uniques relevant for each tile,
// each tile will have different conditional state, but they will all reuse the same list of uniques for the civ
return get(
"civ-${civ.civName}-${uniqueType.name}-${stateForConditionals}",
"civ-${civ.civName}-${uniqueType.name}",
sequence
)
).filter { it.conditionalsApply(stateForConditionals) }
}
/** Get cached results as a sequence */