-
-
Notifications
You must be signed in to change notification settings - Fork 131
Fix compatibility bugs and performance issues #668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -33,6 +33,7 @@ import java.util.concurrent.ConcurrentHashMap | |||||||||||||||||||||
| object BestConfigService { | ||||||||||||||||||||||
| private const val API_BASE_URL = "https://gamenative-best-config-worker.gamenative.workers.dev/api/best-config" | ||||||||||||||||||||||
| private const val TIMEOUT_SECONDS = 10L | ||||||||||||||||||||||
| private const val MAX_CACHE_SIZE = 100 | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private val httpClient = OkHttpClient.Builder() | ||||||||||||||||||||||
| .connectTimeout(10, TimeUnit.SECONDS) | ||||||||||||||||||||||
|
|
@@ -108,32 +109,40 @@ object BestConfigService { | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| val response = httpClient.newCall(request).execute() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (!response.isSuccessful) { | ||||||||||||||||||||||
| Timber.tag("BestConfigService") | ||||||||||||||||||||||
| .w("API request failed - HTTP ${response.code}") | ||||||||||||||||||||||
| return@withTimeout null | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| response.use { | ||||||||||||||||||||||
| if (!it.isSuccessful) { | ||||||||||||||||||||||
| Timber.tag("BestConfigService") | ||||||||||||||||||||||
| .w("API request failed - HTTP ${it.code}") | ||||||||||||||||||||||
| return@withTimeout null | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| val responseBody = it.body?.string() ?: return@withTimeout null | ||||||||||||||||||||||
| val jsonResponse = JSONObject(responseBody) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| val responseBody = response.body?.string() ?: return@withTimeout null | ||||||||||||||||||||||
| val jsonResponse = JSONObject(responseBody) | ||||||||||||||||||||||
| val bestConfigJson = jsonResponse.getJSONObject("bestConfig") | ||||||||||||||||||||||
| val bestConfig = Json.parseToJsonElement(bestConfigJson.toString()).jsonObject | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| val bestConfigJson = jsonResponse.getJSONObject("bestConfig") | ||||||||||||||||||||||
| val bestConfig = Json.parseToJsonElement(bestConfigJson.toString()).jsonObject | ||||||||||||||||||||||
| val bestConfigResponse = BestConfigResponse( | ||||||||||||||||||||||
| bestConfig = bestConfig, | ||||||||||||||||||||||
| matchType = jsonResponse.getString("matchType"), | ||||||||||||||||||||||
| matchedGpu = jsonResponse.getString("matchedGpu"), | ||||||||||||||||||||||
| matchedDeviceId = jsonResponse.getInt("matchedDeviceId") | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| val bestConfigResponse = BestConfigResponse( | ||||||||||||||||||||||
| bestConfig = bestConfig, | ||||||||||||||||||||||
| matchType = jsonResponse.getString("matchType"), | ||||||||||||||||||||||
| matchedGpu = jsonResponse.getString("matchedGpu"), | ||||||||||||||||||||||
| matchedDeviceId = jsonResponse.getInt("matchedDeviceId") | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| // Evict oldest entries if cache is full | ||||||||||||||||||||||
| if (cache.size >= MAX_CACHE_SIZE) { | ||||||||||||||||||||||
| val keysToRemove = cache.keys().toList().take(cache.size - MAX_CACHE_SIZE + 1) | ||||||||||||||||||||||
| keysToRemove.forEach { cache.remove(it) } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+132
to
+136
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cache eviction does not evict "oldest" entries.
For truly FIFO eviction, consider using Option 1: Fix comment to reflect actual behavior- // Evict oldest entries if cache is full
+ // Evict entries if cache is full (arbitrary order, not FIFO)
if (cache.size >= MAX_CACHE_SIZE) {Option 2: Use Collections.synchronizedMap with LinkedHashMap for FIFO// At class level:
private val cache = Collections.synchronizedMap(
object : LinkedHashMap<String, BestConfigResponse>(MAX_CACHE_SIZE, 0.75f, false) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, BestConfigResponse>): Boolean {
return size > MAX_CACHE_SIZE
}
}
)This auto-evicts the eldest entry when size exceeds the limit. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Cache the response | ||||||||||||||||||||||
| cache[cacheKey] = bestConfigResponse | ||||||||||||||||||||||
| // Cache the response | ||||||||||||||||||||||
| cache[cacheKey] = bestConfigResponse | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Timber.tag("BestConfigService") | ||||||||||||||||||||||
| .d("Fetched best config for $gameName on $gpuName (matchType: ${bestConfigResponse.matchType})") | ||||||||||||||||||||||
| Timber.tag("BestConfigService") | ||||||||||||||||||||||
| .d("Fetched best config for $gameName on $gpuName (matchType: ${bestConfigResponse.matchType})") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| bestConfigResponse | ||||||||||||||||||||||
| bestConfigResponse | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } catch (e: java.util.concurrent.TimeoutException) { | ||||||||||||||||||||||
| Timber.tag("BestConfigService") | ||||||||||||||||||||||
|
|
@@ -313,7 +322,6 @@ object BestConfigService { | |||||||||||||||||||||
| if (version.isNotEmpty() && !ManifestComponentHelper.versionExists(version, availableDxvk)) { | ||||||||||||||||||||||
| Timber.tag("BestConfigService").w("DXVK version $version not found, updating to PrefManager default") | ||||||||||||||||||||||
| return "DXVK $version" | ||||||||||||||||||||||
| filteredJson.put("dxwrapperConfig", PrefManager.dxWrapperConfig) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -324,7 +332,6 @@ object BestConfigService { | |||||||||||||||||||||
| if (version.isNotEmpty() && !ManifestComponentHelper.versionExists(version, availableVkd3d)) { | ||||||||||||||||||||||
| Timber.tag("BestConfigService").w("VKD3D version $version not found, updating to PrefManager default") | ||||||||||||||||||||||
| return "VKD3D $version" | ||||||||||||||||||||||
| filteredJson.put("dxwrapperConfig", PrefManager.dxWrapperConfig) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -341,7 +348,6 @@ object BestConfigService { | |||||||||||||||||||||
| if (!ManifestComponentHelper.versionExists(box64Version, box64VersionsToCheck)) { | ||||||||||||||||||||||
| Timber.tag("BestConfigService").w("Box64 version $box64Version not found in $containerVariant variant entries, updating to PrefManager default") | ||||||||||||||||||||||
| return "Box64 $box64Version" | ||||||||||||||||||||||
| filteredJson.put("box64Version", PrefManager.box64Version) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -357,7 +363,6 @@ object BestConfigService { | |||||||||||||||||||||
| if (fexcoreVersion.isNotEmpty() && !ManifestComponentHelper.versionExists(fexcoreVersion, availableFexcore)) { | ||||||||||||||||||||||
| Timber.tag("BestConfigService").w("FEXCore version $fexcoreVersion not found, updating to PrefManager default") | ||||||||||||||||||||||
| return "FEXCore $fexcoreVersion" | ||||||||||||||||||||||
| filteredJson.put("fexcoreVersion", PrefManager.fexcoreVersion) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Validate Wine/Proton version (check separately based on container variant) | ||||||||||||||||||||||
|
|
@@ -373,7 +378,6 @@ object BestConfigService { | |||||||||||||||||||||
| if (!ManifestComponentHelper.versionExists(wineVersion, wineVersionsToCheck)) { | ||||||||||||||||||||||
| Timber.tag("BestConfigService").w("Wine version $wineVersion not found in $containerVariant variant entries, updating to PrefManager default") | ||||||||||||||||||||||
| return "Wine $wineVersion" | ||||||||||||||||||||||
| filteredJson.put("wineVersion", PrefManager.wineVersion) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -399,7 +403,6 @@ object BestConfigService { | |||||||||||||||||||||
| if (preset == null) { | ||||||||||||||||||||||
| Timber.tag("BestConfigService").w("Box64 preset $box64Preset not found, updating to PrefManager default") | ||||||||||||||||||||||
| return "Box64 preset $box64Preset" | ||||||||||||||||||||||
| filteredJson.put("box64Preset", PrefManager.box64Preset) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -410,7 +413,6 @@ object BestConfigService { | |||||||||||||||||||||
| if (preset == null) { | ||||||||||||||||||||||
| Timber.tag("BestConfigService").w("FEXCore preset $fexcorePreset not found, updating to PrefManager default") | ||||||||||||||||||||||
| return "FEXCore preset $fexcorePreset" | ||||||||||||||||||||||
| filteredJson.put("fexcorePreset", PrefManager.fexcorePreset) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,16 +6,17 @@ import kotlinx.serialization.encodeToString | |
| import kotlinx.serialization.decodeFromString | ||
| import kotlinx.serialization.json.Json | ||
| import timber.log.Timber | ||
| import java.util.concurrent.ConcurrentHashMap | ||
|
|
||
| /** | ||
| * Persistent cache for game compatibility responses with 7-day TTL. | ||
| * Persistent cache for game compatibility responses with 6-hour TTL. | ||
| * Uses lazy expiration - checks expiration on access, not on load (optimizes performance). | ||
| */ | ||
| object GameCompatibilityCache { | ||
| private const val CACHE_TTL_MS = 6 * 60 * 60 * 1000L // 6 hours | ||
|
|
||
| private val inMemoryCache = mutableMapOf<String, GameCompatibilityService.GameCompatibilityResponse>() | ||
| private val timestamps = mutableMapOf<String, Long>() | ||
| private val inMemoryCache = ConcurrentHashMap<String, GameCompatibilityService.GameCompatibilityResponse>() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Thread-safety fix is incomplete: while the maps are now Prompt for AI agents |
||
| private val timestamps = ConcurrentHashMap<String, Long>() | ||
| private var cacheLoaded = false | ||
|
Comment on lines
+18
to
20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thread-safety gap with While Consider using Proposed fix using AtomicBoolean+import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.ConcurrentHashMap
...
- private var cacheLoaded = false
+ private val cacheLoaded = AtomicBoolean(false)
...
private fun loadCache() {
- if (cacheLoaded) return
+ if (!cacheLoaded.compareAndSet(false, true)) return🤖 Prompt for AI Agents |
||
|
|
||
| @Serializable | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -27,11 +27,25 @@ public abstract class GPUInformation { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| System.loadLibrary("extras"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Cache parsed GPU cards JSON to avoid re-parsing on every call | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static JSONArray cachedGpuCards = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static synchronized JSONArray getGpuCards(Context context) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (cachedGpuCards == null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String gpuNameList = FileUtils.readString(context, "gpu_cards.json"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Missing null check for file read result may lead to NullPointerException. FileUtils.readString() returns null when the file doesn't exist or can't be read. If gpuNameList is null, new JSONArray(null) will throw an unchecked NullPointerException that won't be caught by the JSONException handler, causing the app to crash. Prompt for AI agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cachedGpuCards = new JSONArray(gpuNameList); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (JSONException e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cachedGpuCards = new JSONArray(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cachedGpuCards; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+33
to
+43
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential NullPointerException if asset read fails.
Proposed fix with null check private static synchronized JSONArray getGpuCards(Context context) {
if (cachedGpuCards == null) {
try {
String gpuNameList = FileUtils.readString(context, "gpu_cards.json");
+ if (gpuNameList == null) {
+ cachedGpuCards = new JSONArray();
+ return cachedGpuCards;
+ }
cachedGpuCards = new JSONArray(gpuNameList);
} catch (JSONException e) {
cachedGpuCards = new JSONArray();
}
}
return cachedGpuCards;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public static String getDeviceIdFromGPUName(Context context, String gpuName) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String gpuNameList = FileUtils.readString(context, "gpu_cards.json"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String deviceId = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JSONArray jsonArray = new JSONArray(gpuNameList); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JSONArray jsonArray = getGpuCards(context); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (int i = 0; i < jsonArray.length(); i++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JSONObject jobj = jsonArray.getJSONObject(i); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (jobj.getString("name").contains(gpuName)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -45,10 +59,9 @@ public static String getDeviceIdFromGPUName(Context context, String gpuName) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public static String getVendorIdFromGPUName(Context context, String gpuName) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String gpuNameList = FileUtils.readString(context, "gpu_cards.json"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String vendorId = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JSONArray jsonArray = new JSONArray(gpuNameList); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JSONArray jsonArray = getGpuCards(context); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (int i = 0; i < jsonArray.length(); i++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JSONObject jobj = jsonArray.getJSONObject(i); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (jobj.getString("name").contains(gpuName)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: The comment says "Evict oldest entries" but
ConcurrentHashMapdoes not maintain insertion order.cache.keys().toList().take(...)will remove arbitrary entries, not the oldest. Either update the comment to reflect the actual behavior (arbitrary eviction) or use aLinkedHashMapwith synchronization for true FIFO eviction.Prompt for AI agents