Skip to content

Fix compatibility bugs and performance issues#668

Closed
gigachadmma69 wants to merge 2 commits intoutkarshdalal:masterfrom
gigachadmma69:fix/compatibility-and-performance-optimizations
Closed

Fix compatibility bugs and performance issues#668
gigachadmma69 wants to merge 2 commits intoutkarshdalal:masterfrom
gigachadmma69:fix/compatibility-and-performance-optimizations

Conversation

@gigachadmma69
Copy link

@gigachadmma69 gigachadmma69 commented Feb 27, 2026

Summary

  • Remove unreachable dead code in BestConfigService.validateComponentVersions() — 7 instances of filteredJson.put() after return statements that never executed. These were intended fallbacks that silently failed, meaning games with missing component versions would get no fallback config applied.
  • Fix OkHttp response body leaks in BestConfigService.fetchBestConfig() and GameCompatibilityService.fetchCompatibility() by wrapping responses in use{} blocks. Without this, HTTP connections leak and eventually exhaust the connection pool.
  • Fix double forceUpdate() call in Drawable.drawImage() for depth 24/32. The method called forceUpdate() inside the depth branch AND unconditionally after it, causing unnecessary duplicate texture re-uploads during gameplay (performance hit on every frame that draws images).
  • Fix thread safety in GameCompatibilityCache by replacing mutableMapOf() with ConcurrentHashMap. The original maps could throw ConcurrentModificationException when accessed from multiple coroutines.
  • Cache parsed gpu_cards.json in GPUInformation via a synchronized lazy singleton. Previously, every call to getDeviceIdFromGPUName() or getVendorIdFromGPUName() re-read and re-parsed the JSON from assets.
  • Add cache eviction to BestConfigService with a MAX_CACHE_SIZE of 100 entries to prevent unbounded memory growth in long sessions.

Files Changed

File Change
BestConfigService.kt Remove dead code, add response.use{}, add cache eviction
GameCompatibilityService.kt Add response.use{}
GameCompatibilityCache.kt mutableMapOfConcurrentHashMap
GPUInformation.java Add cached getGpuCards() method
Drawable.java Remove duplicate forceUpdate() call

Test plan

  • Verify games with known best-config entries still apply configs correctly
  • Verify API calls to best-config and game-compatibility endpoints work (check Timber logs for proper response handling)
  • Verify no connection pool exhaustion after many API calls
  • Verify game rendering performance (drawImage path) is not regressed
  • Verify GPU card lookup returns correct results after caching change

🤖 Generated with Claude Code


Summary by cubic

Fixes HTTP leaks and thread-safety issues, ensures fallback configs apply, and improves rendering and caching. Adds FIFO cache eviction and a 6-hour TTL to reduce memory use and stale compatibility data.

  • Bug Fixes

    • Removed unreachable code so fallback configs apply when versions are missing.
    • Wrapped OkHttp calls in response.use {} in BestConfigService and GameCompatibilityService to prevent leaks.
    • Made GameCompatibilityCache thread-safe with ConcurrentHashMap and @volatile cacheLoaded.
  • Performance

    • Implemented FIFO eviction (max 100) in BestConfigService using a synchronized LinkedHashMap.
    • Set a 6-hour TTL for compatibility data; cached gpu_cards.json with null-safety to avoid repeated parsing.
    • Removed duplicate forceUpdate() in Drawable.drawImage() to stop redundant texture uploads.

Written for commit f813af1. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Improved HTTP response handling and error/time-out behavior for config and compatibility fetches.
  • Performance Improvements

    • Enforced a bounded FIFO cache to prevent unbounded memory growth.
    • Reduced compatibility cache TTL for fresher results.
    • Added lazy, thread-safe caching for GPU lookups to reduce file I/O.
  • Rendering

    • Deferred certain texture/buffer updates to avoid redundant updates and improve rendering efficiency.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a46d559-ce2b-4cce-9e54-ebfe379b3309

📥 Commits

Reviewing files that changed from the base of the PR and between 933ba0a and f813af1.

📒 Files selected for processing (5)
  • app/src/main/java/app/gamenative/utils/BestConfigService.kt
  • app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt
  • app/src/main/java/app/gamenative/utils/GameCompatibilityService.kt
  • app/src/main/java/com/winlator/core/GPUInformation.java
  • app/src/main/java/com/winlator/xserver/Drawable.java
💤 Files with no reviewable changes (1)
  • app/src/main/java/com/winlator/xserver/Drawable.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt

📝 Walkthrough

Walkthrough

Replaces unbounded/thread-unsafe caches with bounded or concurrent variants, reduces GameCompatibilityCache TTL to 6 hours, scopes HTTP response handling with response.use{} and in-block JSON parsing, adds an in-memory cached GPU cards JSONArray, and defers an immediate texture update in Drawable.drawImage. (31 words)

Changes

Cohort / File(s) Summary
BestConfig & Compatibility Caches
app/src/main/java/app/gamenative/utils/BestConfigService.kt, app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt
BestConfigService: introduced MAX_CACHE_SIZE = 100 and replaced the ConcurrentHashMap cache with a synchronized FIFO LinkedHashMap that evicts eldest entries. GameCompatibilityCache: TTL changed to 6 hours, in-memory maps switched to ConcurrentHashMap, and cacheLoaded marked @Volatile.
HTTP response & JSON parsing
app/src/main/java/app/gamenative/utils/BestConfigService.kt, app/src/main/java/app/gamenative/utils/GameCompatibilityService.kt
Both services now use response.use { ... }, perform response success checks and parse bodies inside the scoped block, return null on non-success, and adjust logging. BestConfigService also removed in-path mutations of filtered JSON with PrefManager defaults.
GPU data caching
app/src/main/java/com/winlator/core/GPUInformation.java
Adds a private cached JSONArray (cachedGpuCards) with a synchronized loader helper; GPU lookup methods read from the cached array instead of re-reading the file on each call.
Graphics update timing
app/src/main/java/com/winlator/xserver/Drawable.java
Removes an internal immediate rewind and forceUpdate() inside the 24/32-bit branch, deferring buffer rewinds and the texture update to the outer/common code path.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • phobos665

Poem

🐰 I hop through caches, neat and small,
Evicting eldest so none fall,
Responses closed, JSON read in place,
GPU cards cached for faster chase.
I nibble bytes and leave a tidy trace. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: it addresses compatibility bugs (dead code removal, response body leaks, thread-safety) and performance issues (cache eviction, caching gpu_cards.json, removing duplicate calls).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/utils/BestConfigService.kt`:
- Around line 132-136: The eviction code using cache (a ConcurrentHashMap) with
MAX_CACHE_SIZE doesn't evict the oldest entries because ConcurrentHashMap is
unordered and the size/keys() check has a race; replace the
ConcurrentHashMap-based manual eviction with a synchronized LinkedHashMap-based
bounded cache by initializing cache as Collections.synchronizedMap(object :
LinkedHashMap<String, BestConfigResponse>(MAX_CACHE_SIZE, 0.75f, false) {
override fun removeEldestEntry(eldest) = size > MAX_CACHE_SIZE }) and remove the
manual eviction block that computes keysToRemove, or if you prefer to keep
ConcurrentHashMap, update the comment to state that eviction is arbitrary and
(optionally) track insertion timestamps to implement true FIFO using a
PriorityQueue or LinkedHashMap-backed structure instead.

In `@app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt`:
- Around line 18-20: The plain Boolean cacheLoaded is not thread-safe and can
race in loadCache(); replace it with a thread-safe flag (e.g., an AtomicBoolean
or mark it `@Volatile`) and update loadCache() to atomically check-and-set the
flag (use AtomicBoolean.compareAndSet or synchronized block) so only one
coroutine proceeds to populate inMemoryCache and timestamps; ensure any
early-return logic uses the same atomic check and clear any existing non-atomic
reads of cacheLoaded.

In `@app/src/main/java/com/winlator/core/GPUInformation.java`:
- Around line 33-43: getGpuCards currently calls FileUtils.readString(context,
"gpu_cards.json") and passes the result directly to new JSONArray, but
FileUtils.readString can return null on IO errors which will cause a
NullPointerException not caught by the JSONException handler; update getGpuCards
(and use cachedGpuCards) to check if gpuNameList is null or empty before
constructing the JSONArray and fall back to new JSONArray() in that case (or
when parsing fails), and ensure the try/catch covers both JSONException and the
null case so cachedGpuCards is always initialized.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 761d93b and be4f843.

📒 Files selected for processing (5)
  • app/src/main/java/app/gamenative/utils/BestConfigService.kt
  • app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt
  • app/src/main/java/app/gamenative/utils/GameCompatibilityService.kt
  • app/src/main/java/com/winlator/core/GPUInformation.java
  • app/src/main/java/com/winlator/xserver/Drawable.java
💤 Files with no reviewable changes (1)
  • app/src/main/java/com/winlator/xserver/Drawable.java

Comment on lines 18 to 20
private val inMemoryCache = ConcurrentHashMap<String, GameCompatibilityService.GameCompatibilityResponse>()
private val timestamps = ConcurrentHashMap<String, Long>()
private var cacheLoaded = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Thread-safety gap with cacheLoaded flag.

While inMemoryCache and timestamps are now thread-safe ConcurrentHashMap instances, the cacheLoaded flag on line 20 is a plain Boolean without synchronization. Multiple coroutines could race past the if (cacheLoaded) return check in loadCache(), causing duplicate initialization.

Consider using @Volatile or an AtomicBoolean, or synchronizing the entire loadCache() method:

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
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt` around
lines 18 - 20, The plain Boolean cacheLoaded is not thread-safe and can race in
loadCache(); replace it with a thread-safe flag (e.g., an AtomicBoolean or mark
it `@Volatile`) and update loadCache() to atomically check-and-set the flag (use
AtomicBoolean.compareAndSet or synchronized block) so only one coroutine
proceeds to populate inMemoryCache and timestamps; ensure any early-return logic
uses the same atomic check and clear any existing non-atomic reads of
cacheLoaded.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/utils/BestConfigService.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/BestConfigService.kt:132">
P3: The comment says "Evict oldest entries" but `ConcurrentHashMap` does 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 a `LinkedHashMap` with synchronization for true FIFO eviction.</violation>
</file>

<file name="app/src/main/java/com/winlator/core/GPUInformation.java">

<violation number="1" location="app/src/main/java/com/winlator/core/GPUInformation.java:36">
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.</violation>
</file>

<file name="app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt:18">
P2: Thread-safety fix is incomplete: while the maps are now `ConcurrentHashMap`, the `cacheLoaded` flag remains a plain `Boolean` without any synchronization. Multiple coroutines could race past the `if (cacheLoaded) return` check in `loadCache()`, causing duplicate initialization. Consider using `AtomicBoolean` with `compareAndSet` or marking it `@Volatile` at minimum.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt (1)

20-20: ⚠️ Potential issue | 🟠 Major

@Volatile alone still leaves a racy cache initialization path.

At Line 71, the if (cacheLoaded) return check is non-atomic with the subsequent population/writes. Concurrent callers can initialize in parallel, and one initializer can overwrite fresher entries written by another thread during cache()/cacheAll().

Suggested fix (single-flight initialization via lock)
 object GameCompatibilityCache {
@@
     private val inMemoryCache = ConcurrentHashMap<String, GameCompatibilityService.GameCompatibilityResponse>()
     private val timestamps = ConcurrentHashMap<String, Long>()
     `@Volatile` private var cacheLoaded = false
+    private val loadLock = Any()
@@
     private fun loadCache() {
         if (cacheLoaded) return
-
-        try {
-            val cacheJson = PrefManager.gameCompatibilityCache
-            if (cacheJson.isEmpty() || cacheJson == "{}") {
-                cacheLoaded = true
-                return
-            }
-
-            val cacheMap = Json.decodeFromString<Map<String, CachedCompatibilityResponse>>(cacheJson)
-
-            // Load all entries into memory (no expiration check here - lazy expiration)
-            // Store both response and timestamp for expiration checking
-            cacheMap.forEach { (gameName, cached) ->
-                inMemoryCache[gameName] = cached.response.toResponse()
-                timestamps[gameName] = cached.timestamp
-            }
-
-            Timber.tag("GameCompatibilityCache").d("Loaded ${inMemoryCache.size} cached entries from persistent storage")
-            cacheLoaded = true
-        } catch (e: Exception) {
-            Timber.tag("GameCompatibilityCache").e(e, "Failed to load cache from persistent storage")
-            cacheLoaded = true // Mark as loaded to avoid retrying
+        synchronized(loadLock) {
+            if (cacheLoaded) return
+            try {
+                val cacheJson = PrefManager.gameCompatibilityCache
+                if (cacheJson.isEmpty() || cacheJson == "{}") {
+                    cacheLoaded = true
+                    return
+                }
+
+                val cacheMap = Json.decodeFromString<Map<String, CachedCompatibilityResponse>>(cacheJson)
+                cacheMap.forEach { (gameName, cached) ->
+                    inMemoryCache[gameName] = cached.response.toResponse()
+                    timestamps[gameName] = cached.timestamp
+                }
+                Timber.tag("GameCompatibilityCache").d("Loaded ${inMemoryCache.size} cached entries from persistent storage")
+            } catch (e: Exception) {
+                Timber.tag("GameCompatibilityCache").e(e, "Failed to load cache from persistent storage")
+            } finally {
+                cacheLoaded = true
+            }
         }
     }

Run this to verify the current non-atomic check/set path is still present:

#!/bin/bash
# Expected: you'll see `if (cacheLoaded) return` and separate `cacheLoaded = true`
# without synchronization around the whole initialization block.

FILE="app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt"

sed -n '65,110p' "$FILE"
echo "----"
rg -nP 'if\s*\(cacheLoaded\)\s*return|cacheLoaded\s*=\s*true|@Synchronized\s+private\s+fun\s+loadCache|synchronized\s*\(' "$FILE"

Also applies to: 70-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt` at line 20,
The cacheLoaded volatile flag is checked and set non-atomically causing racey
initialization in GameCompatibilityCache; wrap the entire initialization path in
a single-flight lock so the "if (cacheLoaded) return" check and the population
(calls to cache() / cacheAll()) plus the assignment "cacheLoaded = true" occur
under the same synchronization. Concretely, add a private lock object or make
loadCache synchronized (e.g., synchronize around a private Any() or use
`@Synchronized` on loadCache), move the early-return check inside that
synchronized block, perform cache/cacheAll() there, then set cacheLoaded = true
before leaving the block to ensure only one thread initializes and others return
immediately. Ensure callers still call the same entrypoint (loadCache or init
method) so no other code paths bypass the lock.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/utils/BestConfigService.kt (1)

125-125: Add a warning for null response bodies.

Line 125 returns null silently when the body is absent; adding a warning here would improve production diagnostics.

🔧 Suggested tweak
-                    val responseBody = it.body?.string() ?: return@withTimeout null
+                    val responseBody = it.body?.string()
+                    if (responseBody == null) {
+                        Timber.tag("BestConfigService").w("API response body is null")
+                        return@withTimeout null
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/utils/BestConfigService.kt` at line 125, The
silent early return on null response body in BestConfigService (the line
assigning responseBody: val responseBody = it.body?.string() ?:
return@withTimeout null) should emit a warning before returning; update the
withTimeout block to check for it.body == null and call the module's logging
facility (e.g., logger.warn / Timber.w / processLogger.warn used elsewhere in
this file) with a descriptive message including context (endpoint/request id if
available) and then return@withTimeout null so production diagnostics capture
when responses have no body.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt`:
- Line 20: The cacheLoaded volatile flag is checked and set non-atomically
causing racey initialization in GameCompatibilityCache; wrap the entire
initialization path in a single-flight lock so the "if (cacheLoaded) return"
check and the population (calls to cache() / cacheAll()) plus the assignment
"cacheLoaded = true" occur under the same synchronization. Concretely, add a
private lock object or make loadCache synchronized (e.g., synchronize around a
private Any() or use `@Synchronized` on loadCache), move the early-return check
inside that synchronized block, perform cache/cacheAll() there, then set
cacheLoaded = true before leaving the block to ensure only one thread
initializes and others return immediately. Ensure callers still call the same
entrypoint (loadCache or init method) so no other code paths bypass the lock.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/utils/BestConfigService.kt`:
- Line 125: The silent early return on null response body in BestConfigService
(the line assigning responseBody: val responseBody = it.body?.string() ?:
return@withTimeout null) should emit a warning before returning; update the
withTimeout block to check for it.body == null and call the module's logging
facility (e.g., logger.warn / Timber.w / processLogger.warn used elsewhere in
this file) with a descriptive message including context (endpoint/request id if
available) and then return@withTimeout null so production diagnostics capture
when responses have no body.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be4f843 and 933ba0a.

📒 Files selected for processing (3)
  • app/src/main/java/app/gamenative/utils/BestConfigService.kt
  • app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt
  • app/src/main/java/com/winlator/core/GPUInformation.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/winlator/core/GPUInformation.java

Joe and others added 2 commits March 4, 2026 16:49
- Remove unreachable dead code after return statements in
  BestConfigService.validateComponentVersions() (7 instances of
  filteredJson.put() after return that never executed)
- Fix OkHttp response body leaks in BestConfigService and
  GameCompatibilityService by wrapping responses in use{} blocks
- Fix double forceUpdate() call in Drawable.drawImage() for depth
  24/32 that caused unnecessary texture re-uploads during gameplay
- Fix thread safety in GameCompatibilityCache by using
  ConcurrentHashMap instead of mutableMapOf()
- Cache parsed gpu_cards.json in GPUInformation to avoid expensive
  re-parsing on every getDeviceIdFromGPUName/getVendorIdFromGPUName call
- Add MAX_CACHE_SIZE (100) eviction to BestConfigService to prevent
  unbounded memory growth

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- BestConfigService: replace ConcurrentHashMap with synchronized
  LinkedHashMap for proper FIFO cache eviction
- GameCompatibilityCache: mark cacheLoaded as @volatile
- GPUInformation: null-check FileUtils.readString before JSONArray ctor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants