Skip to content

Commit 8b04f3b

Browse files
authored
Merge pull request #2786 from GET-R3AL/Application-Profile-Fix
Fix Application Profile graph slowdown with projected effects
2 parents ab6bc94 + d9ec550 commit 8b04f3b

5 files changed

Lines changed: 80 additions & 21 deletions

File tree

‎eos/modifiedAttributeDict.py‎

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
minLimitAttrKeyCache = {}
3333
maxLimitAttrKeyCache = {}
3434
resistanceCache = {}
35+
resAttrNameCache = {}
3536

3637

3738
def getAttrDefault(key, fallback=None):
@@ -48,6 +49,19 @@ def getAttrDefault(key, fallback=None):
4849
return default
4950

5051

52+
def getAttrNameByID(attrID):
53+
# Resolve a resistance attribute ID to its name. Attribute metadata is
54+
# immutable gamedata, so memoize it: gamedata query caching is disabled
55+
# globally, and this runs in the hot getExtended() loop used by the graph
56+
# projected-effect calculations (see minLimitAttrKeyCache above).
57+
try:
58+
return resAttrNameCache[attrID]
59+
except KeyError:
60+
attrInfo = getAttributeInfo(attrID)
61+
name = resAttrNameCache[attrID] = None if attrInfo is None else attrInfo.attributeName
62+
return name
63+
64+
5165
def getResistanceAttrID(modifyingItem, effect):
5266
# If it doesn't exist on the effect, check the modifying module's attributes.
5367
# If it's there, cache it and return
@@ -314,16 +328,16 @@ def __calculateValue(self, key, extraMultipliers=None, preIncAdj=None, multAdj=N
314328
minLimitKey = minLimitAttrKeyCache[key]
315329
except KeyError:
316330
attrInfo = getAttributeInfo(key)
317-
if attrInfo is None:
318-
cappingId = minLimitAttrKeyCache[key] = None
319-
else:
320-
cappingId = attrInfo.minAttributeID
331+
cappingId = None if attrInfo is None else attrInfo.minAttributeID
321332
if cappingId is None:
322333
minLimitKey = None
323334
else:
324335
cappingAttrInfo = getAttributeInfo(cappingId)
325336
minLimitKey = None if cappingAttrInfo is None else cappingAttrInfo.name
326-
minLimitAttrKeyCache[key] = minLimitKey
337+
# Always memoize the result (including None): attribute metadata is
338+
# immutable, and getExtended() recomputes this in a hot loop. Missing
339+
# the None case here re-queried the DB on every extended attr lookup.
340+
minLimitAttrKeyCache[key] = minLimitKey
327341
if minLimitKey:
328342
minLimitValue = self[minLimitKey]
329343
minLimitValue = minLimitValue.value if hasattr(minLimitValue, "value") else minLimitValue
@@ -334,16 +348,13 @@ def __calculateValue(self, key, extraMultipliers=None, preIncAdj=None, multAdj=N
334348
maxLimitKey = maxLimitAttrKeyCache[key]
335349
except KeyError:
336350
attrInfo = getAttributeInfo(key)
337-
if attrInfo is None:
338-
cappingId = maxLimitAttrKeyCache[key] = None
339-
else:
340-
cappingId = attrInfo.maxAttributeID
351+
cappingId = None if attrInfo is None else attrInfo.maxAttributeID
341352
if cappingId is None:
342353
maxLimitKey = None
343354
else:
344355
cappingAttrInfo = getAttributeInfo(cappingId)
345356
maxLimitKey = None if cappingAttrInfo is None else cappingAttrInfo.name
346-
maxLimitAttrKeyCache[key] = maxLimitKey
357+
maxLimitAttrKeyCache[key] = maxLimitKey
347358

348359
if maxLimitKey:
349360
maxLimitValue = self[maxLimitKey]
@@ -375,11 +386,11 @@ def __calculateValue(self, key, extraMultipliers=None, preIncAdj=None, multAdj=N
375386
if not resAttrID:
376387
multipliers.append(mult)
377388
continue
378-
resAttrInfo = getAttributeInfo(resAttrID)
379-
if not resAttrInfo:
389+
resAttrName = getAttrNameByID(resAttrID)
390+
if not resAttrName:
380391
multipliers.append(mult)
381392
continue
382-
resMult = self.fit.ship.itemModifiedAttributes[resAttrInfo.attributeName]
393+
resMult = self.fit.ship.itemModifiedAttributes[resAttrName]
383394
if resMult is None or resMult == 1:
384395
multipliers.append(mult)
385396
continue

‎graphs/data/fitApplicationProfile/calc/launcher.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import math
2121
from bisect import bisect_right
2222

23-
from .projected import getProjectedParamsAtDistance
23+
from .projected import getProjectedParamsAtDistance, getSampleStep
2424

2525

2626
# =============================================================================
@@ -577,7 +577,7 @@ def _updateParamsWithCache(baseTgtSpeed, baseTgtSigRadius, projectedCache, dista
577577

578578

579579
def calculateTransitions(chargeData, baseTgtSpeed, baseTgtSigRadius,
580-
projectedCache, maxDistance=300000, resolution=100):
580+
projectedCache, maxDistance=300000, resolution=None):
581581
"""
582582
Calculate distances where optimal missile ammo changes.
583583
@@ -595,6 +595,11 @@ def calculateTransitions(chargeData, baseTgtSpeed, baseTgtSigRadius,
595595
if not chargeData:
596596
return []
597597

598+
# Scale the scan step with range so long-range fits don't do 25x the work
599+
# for no extra fidelity (falls back to the fine 100m step for short ranges).
600+
if resolution is None:
601+
resolution = getSampleStep(maxDistance)
602+
598603
transitions = []
599604
currentCharge = None
600605

‎graphs/data/fitApplicationProfile/calc/optimize_ammo.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from logbook import Logger
2323

2424
from .turret import calculateAppliedVolley
25-
from .projected import getProjectedParamsAtDistance
25+
from .projected import getProjectedParamsAtDistance, getSampleStep
2626

2727

2828
pyfalog = Logger(__name__)
@@ -111,7 +111,7 @@ def _updateTrackingWithCache(baseTrackingParams, projectedCache, distance):
111111

112112
def calculateTransitions(chargeData, turretBase, baseTrackingParams,
113113
projectedCache,
114-
maxDistance=300000, resolution=100):
114+
maxDistance=300000, resolution=None):
115115
"""
116116
Calculate distances where optimal ammo changes.
117117
@@ -135,6 +135,11 @@ def calculateTransitions(chargeData, turretBase, baseTrackingParams,
135135
if not chargeData:
136136
return []
137137

138+
# Scale the scan step with range so long-range fits don't do 25x the work
139+
# for no extra fidelity (falls back to the fine 100m step for short ranges).
140+
if resolution is None:
141+
resolution = getSampleStep(maxDistance)
142+
138143
transitions = []
139144
currentCharge = None
140145

‎graphs/data/fitApplicationProfile/calc/projected.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,40 @@
3939
)
4040

4141

42+
# =============================================================================
43+
# Distance Sample Step
44+
# =============================================================================
45+
46+
def getSampleStep(maxDistance, minStep=100, targetPoints=300):
47+
"""
48+
Pick a distance-scan step that scales with the range being scanned.
49+
50+
Several hot loops (ammo transition scanning, segment plotting) sample the
51+
curve at a fixed interval from 0 to the fit's max effective range. With a
52+
hardcoded step, cost grows linearly with range - a 250km fit does 25x the
53+
work of a 10km fit for no extra visual fidelity.
54+
55+
This keeps the fine step for short-range fits (no accuracy change for the
56+
common case) and coarsens it for long-range fits so the number of sampled
57+
points stays roughly bounded by targetPoints.
58+
59+
Args:
60+
maxDistance: Range to be scanned (m)
61+
minStep: Finest step; also the step used for short ranges (m)
62+
targetPoints: Approximate upper bound on the number of scan points
63+
64+
Returns:
65+
Step size in meters (always a multiple of minStep, >= minStep)
66+
"""
67+
if not maxDistance or maxDistance <= 0:
68+
return minStep
69+
step = maxDistance / targetPoints
70+
if step <= minStep:
71+
return minStep
72+
# Round up to a multiple of minStep so steps stay on tidy boundaries
73+
return int(math.ceil(step / minStep) * minStep)
74+
75+
4276
# =============================================================================
4377
# Distance-Keyed Projected Cache
4478
# =============================================================================

‎graphs/data/fitApplicationProfile/getter.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
getVolleyAtDistance
4343
)
4444
from .calc.projected import (
45-
buildProjectedCache
45+
buildProjectedCache,
46+
getSampleStep
4647
)
4748
from .calc.launcher import (
4849
getAllMultipliers as getLauncherMultipliers,
@@ -747,7 +748,7 @@ def _getCommonData(self, miscParams, src, tgt):
747748
baseTgtSpeed=tgtSpeed,
748749
baseTgtSigRadius=tgtSigRadius,
749750
maxDistance=maxEffectiveRange,
750-
resolution=100, # 100m intervals
751+
resolution=getSampleStep(maxEffectiveRange),
751752
existingCache=existingCache
752753
)
753754

@@ -908,6 +909,10 @@ def getSegments(self, xRange, miscParams, src, tgt):
908909
# Generate segments
909910
segments = []
910911

912+
# Scale plotting resolution with the visible range so long-range fits
913+
# don't sample thousands of points (falls back to 100m for short ranges).
914+
step = getSampleStep(maxX - minX)
915+
911916
for i, transition in enumerate(validTransitions):
912917
transDist, _, ammoName, _ = transition
913918
segStart = max(transDist, minX)
@@ -921,8 +926,7 @@ def getSegments(self, xRange, miscParams, src, tgt):
921926
if segStart >= segEnd:
922927
continue
923928

924-
# Generate points at fixed 100m resolution for performance
925-
step = 100
929+
# Generate points at the range-scaled resolution for performance
926930
xs, ys = [], []
927931
x = segStart
928932
while x <= segEnd:

0 commit comments

Comments
 (0)