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.RulesetCache
import com.unciv.models.ruleset.Speed import com.unciv.models.ruleset.Speed
import com.unciv.models.ruleset.nation.Difficulty 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.ruleset.unique.UniqueType
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.audio.MusicMood import com.unciv.ui.audio.MusicMood
@ -669,10 +670,11 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
civInfo.cache.updateCitiesConnectedToCapital(true) civInfo.cache.updateCitiesConnectedToCapital(true)
// We need to determine the GLOBAL happiness state in order to determine the city stats // We need to determine the GLOBAL happiness state in order to determine the city stats
val localUniqueCache = LocalUniqueCache()
for (city in civInfo.cities) { 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.cityStats.updateCityHappiness(
city.cityConstructions.getStats() city.cityConstructions.getStats(localUniqueCache)
) )
} }
@ -689,7 +691,8 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
if (!ruleset.tileResources.containsKey(city.demandedResource)) if (!ruleset.tileResources.containsKey(city.demandedResource))
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 { return when {
powerLevelComparison > 2 -> ThreatLevel.VeryHigh powerLevelComparison > 2 -> ThreatLevel.VeryHigh
powerLevelComparison > 1.5f -> ThreatLevel.High powerLevelComparison > 1.5f -> ThreatLevel.High
powerLevelComparison < (1 / 1.5f) -> ThreatLevel.Low
powerLevelComparison < 0.5f -> ThreatLevel.VeryLow powerLevelComparison < 0.5f -> ThreatLevel.VeryLow
powerLevelComparison < (1 / 1.5f) -> ThreatLevel.Low
else -> ThreatLevel.Medium else -> ThreatLevel.Medium
} }
} }

View File

@ -622,7 +622,7 @@ class City : IsPartOfGameInfoSerialization {
} }
// Uniques special to this city // 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 ( return (
cityConstructions.builtBuildingUniqueMap.getUniques(uniqueType).filter { it.isLocalEffect } cityConstructions.builtBuildingUniqueMap.getUniques(uniqueType).filter { it.isLocalEffect }
+ religion.getUniques().filter { it.isOfType(uniqueType) } + 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 * @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 stats = StatTreeNode()
val localUniqueCache = LocalUniqueCache()
for (building in getBuiltBuildings()) for (building in getBuiltBuildings())
stats.addStats(building.getStats(city, localUniqueCache), building.name) stats.addStats(building.getStats(city, localUniqueCache), building.name)
return stats return stats

View File

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

View File

@ -325,11 +325,18 @@ class LocalUniqueCache(val cache:Boolean = true) {
uniqueType: UniqueType, uniqueType: UniqueType,
ignoreConditionals: Boolean = false ignoreConditionals: Boolean = false
): Sequence<Unique> { ): Sequence<Unique> {
val stateForConditionals = if (ignoreConditionals) StateForConditionals.IgnoreConditionals // City uniques are a combination of *global civ* uniques plus *city relevant* uniques (see City.getMatchingUniques())
else StateForConditionals(city.civ, city) // 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( return get(
"city-${city.id}-${uniqueType.name}-${ignoreConditionals}", "city-${city.id}-${uniqueType.name}-${ignoreConditionals}",
city.getMatchingUniques(uniqueType, stateForConditionals) uniques
) )
} }
@ -340,12 +347,16 @@ class LocalUniqueCache(val cache:Boolean = true) {
civ civ
) )
): Sequence<Unique> { ): Sequence<Unique> {
val sequence = civ.getMatchingUniques(uniqueType, stateForConditionals) val sequence = civ.getMatchingUniques(uniqueType, StateForConditionals.IgnoreConditionals)
if (!cache) return sequence // So we don't need to toString the stateForConditionals // 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( return get(
"civ-${civ.civName}-${uniqueType.name}-${stateForConditionals}", "civ-${civ.civName}-${uniqueType.name}",
sequence sequence
) ).filter { it.conditionalsApply(stateForConditionals) }
} }
/** Get cached results as a sequence */ /** Get cached results as a sequence */