Skip to content

Reduce Epic downloader CPU and thermal overhead on tablets#1599

Open
Misazam wants to merge 4 commits into
utkarshdalal:masterfrom
Misazam:pr/epic-download-overhead-current
Open

Reduce Epic downloader CPU and thermal overhead on tablets#1599
Misazam wants to merge 4 commits into
utkarshdalal:masterfrom
Misazam:pr/epic-download-overhead-current

Conversation

@Misazam

@Misazam Misazam commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Description

This is a follow-up to #1036, rebased/adapted after the recent Epic downloader refactor in #1554.

The current Flow-based Epic downloader is faster and more reliable than the previous implementation, but on large chunked installs it can still create excessive local CPU/thermal overhead on thermally constrained Android tablets.

Test device:

  • Device: OnePlus Pad 3 / OPD2415
  • Android: 16
  • Game tested: NBA 2K21 from Epic
  • Approx install size: ~99 GB
  • Chunk count observed: ~99,110 chunks

What I observed on current master during a real Epic download:

  • app.gamenative around ~180–225% CPU
  • around ~127 threads
  • heavy DefaultDispatcher / HeapTaskDaemon activity
  • repeated per-chunk log output
  • CPU0 observed up to roughly ~63°C
  • device became noticeably hot to the touch

This patch reduces local downloader overhead while preserving the core download behavior.

It does not change:

  • Epic manifest parsing
  • chunk hash verification
  • install layout
  • CDN selection
  • ownership/login behavior

What changed

  • Use the existing Download Speed setting / DownloadSpeedConfig for Epic parallelism instead of hardcoded Epic-specific caps.
  • Batch byte progress updates instead of updating on every small read.
  • Remove hot-path per-chunk debug logging.
  • Pre-index chunk-to-file assembly metadata so assembly does not repeatedly scan every file/chunk part for each downloaded chunk.
  • Keep lower-bound safety so download/decompress parallelism never falls below 1.

Recording

I do not have a full screen recording yet, but I can attach terminal/logcat screenshots from the on-device test if needed.

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

Validation

Built locally:

  • :app:compileModernDebugKotlin

Installed and tested on-device with a real Epic NBA 2K21 download before the last review adjustment.

Patched behavior observed during that on-device test:

  • download continued successfully
  • progress advanced from 11.41% to 11.75% in 30 seconds
  • approximate throughput: ~11 MB/s / ~88 Mbps
  • later snapshot: 21.24% downloaded and still stable
  • app.gamenative around ~96–129% CPU
  • around ~70–73 threads
  • thermal status stayed at 0
  • CPU0 around ~43–45°C
  • battery around ~37°C
  • skin around ~40–41°C

Latest PR revision validation:

  • :app:compileModernDebugKotlin

The latest revision removes Epic-specific hardcoded parallelism caps and now follows the app's existing Download Speed setting via DownloadSpeedConfig.

The goal is not to maximize peak throughput at all costs. The goal is to keep large Epic downloads stable and usable on Android tablets without creating unnecessary sustained CPU/thermal pressure, while still respecting the user's configured Download Speed setting.

Checklist

  • If I have access to #code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR.
  • This change aligns with the current project scope (core functionality, stability, or performance).
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

@Misazam Misazam requested a review from utkarshdalal as a code owner June 19, 2026 15:13
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

EpicDownloadManager refactors progress reporting, parallelism configuration, and chunk assembly efficiency by batching decompression progress updates by byte threshold and time interval, configuring parallelism from DownloadSpeedConfig with minimum bounds, precomputing chunk-to-assembly-part mappings for faster readiness checks, and adding per-file cancellation checks during pre-allocation.

Changes

EpicDownloadManager Optimization

Layer / File(s) Summary
Import cleanup and progress constant
app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt
Removes unused android.util.Log import. Adds PROGRESS_UPDATE_GRANULARITY_BYTES (256KB) constant used to batch progress byte updates during decompression.
Parallelism bounds from DownloadSpeedConfig
app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt
Updates downloadOverlay's parallelDownloads and downloadAndAssembleEpicChunks' parallelDownloads/parallelAssemble to read from DownloadSpeedConfig().maxDownloads with coerceAtLeast(1) minimum bounds instead of hardcoded maximums.
Byte-granular progress accumulation in decompression
app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt
Introduces reportDownloadedBytes(bytes) helper that accumulates bytes in pendingProgressBytes, calls downloadInfo.updateBytesDownloaded(...) only when threshold PROGRESS_UPDATE_GRANULARITY_BYTES is reached, and gates emitProgressChange() on time interval. Removes firstRead debug tracking and replaces all inline per-read progress calls with this helper in compressed and uncompressed decompression paths. Flushes remaining pendingProgressBytes after loop exit and removes unconditional final emitProgressChange() call.
Chunk-assembly precomputation and direct GUID lookup
app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt
Introduces ChunkAssemblyPart data class and precomputes chunkPartsByGuid mapping and chunkUsageCounts from each chunk GUID to associated file/chunk parts. Refactors assembleReady(finishChunk) to directly look up precomputed parts by GUID instead of scanning all files, and derives assembly success count from per-part results.
Pre-allocation cancellation checks and direct emit
app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt
Adds downloadInfo.isActive() cancellation check per file during pre-allocation iteration with early return. Removes chunksAdded intermediate bookkeeping and logging; emits each chunkInfo directly into networkChunkFlow.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • utkarshdalal/GameNative#1554: Introduced the MutableSharedFlow-based downloadAndAssembleEpicChunks pipeline that this PR directly refactors for parallelism configuration and assembly precomputation.
  • utkarshdalal/GameNative#321: Main PR throttles downloadInfo.updateBytesDownloaded(...)/emitProgressChange() calls, while this PR changes DownloadInfo's behavior/signature and adds progress persistence that those byte updates feed into.

Suggested reviewers

  • utkarshdalal

Poem

🐇 Progress bytes batch up till granularity's met,
Then one wise update—no progress regret!
Assembly parts precomputed, GUID lookup so fast,
No more scanning all files—this refactor will last.
Parallelism configured from settings so true,
A download manager refined—efficient and new! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: reducing CPU and thermal overhead in the Epic downloader specifically for tablets, which is the core objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering the problem statement, observations, changes made, and validation results with clear alignment to the template.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt`:
- Around line 882-883: The parallelDownloads and parallelAssemble variables in
the EpicDownloadManager.kt file only use coerceAtMost for upper bounds but are
missing lower bounds. If speedConfig.maxDownloads or speedConfig.maxDecompress
returns 0, this would result in flatMapMerge receiving concurrency of 0 and
boundedFlowCapacity being 0, which prevents any items from being processed. Add
coerceAtLeast(1) to both the parallelDownloads and parallelAssemble assignments
to match the pattern already used in downloadOverlay, ensuring these concurrency
values never drop below 1.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f37f67a9-d5b5-4015-8d02-5cdf00bc7908

📥 Commits

Reviewing files that changed from the base of the PR and between e00ad33 and bc6a8c2.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt

Comment thread app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt Outdated

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 1 file

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt`:
- Around line 916-918: The MutableSharedFlow declarations for networkChunkFlow
and assembleFlow at lines 916-918 use extraBufferCapacity set to Int.MAX_VALUE,
which creates unbounded queues that bypass proper backpressure. Replace the
Int.MAX_VALUE values with a bounded capacity derived from the downloader's
concurrency levels to restore backpressure safety. Additionally, locate all
tryEmit() calls associated with these flows (including at lines 969, 997, 1020,
and 1103) and replace them with emit() to ensure proper backpressure handling in
the suspend context, preventing silent drops of work and deadlocks in
pendingChunks tracking.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4101a7e5-8f78-46aa-ac2f-fc101b31f582

📥 Commits

Reviewing files that changed from the base of the PR and between bc6a8c2 and 1a8042f.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt`:
- Around line 916-918: The MutableSharedFlow declarations for networkChunkFlow
and assembleFlow at lines 916-918 use extraBufferCapacity set to Int.MAX_VALUE,
which creates unbounded queues that bypass proper backpressure. Replace the
Int.MAX_VALUE values with a bounded capacity derived from the downloader's
concurrency levels to restore backpressure safety. Additionally, locate all
tryEmit() calls associated with these flows (including at lines 969, 997, 1020,
and 1103) and replace them with emit() to ensure proper backpressure handling in
the suspend context, preventing silent drops of work and deadlocks in
pendingChunks tracking.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4101a7e5-8f78-46aa-ac2f-fc101b31f582

📥 Commits

Reviewing files that changed from the base of the PR and between bc6a8c2 and 1a8042f.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt
🛑 Comments failed to post (1)
app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt (1)

916-918: ⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read-only verification for Epic downloader SharedFlow buffering/emission.
# Expect: no Int.MAX_VALUE buffer capacity in EpicDownloadManager queues, and retry/result paths use emit(...) or handle tryEmit(...) == false.

rg -n -C3 'MutableSharedFlow<|extraBufferCapacity\s*=\s*Int\.MAX_VALUE|\.tryEmit\s*\(' app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt

Repository: utkarshdalal/GameNative

Length of output: 2144


🏁 Script executed:

#!/bin/bash
# Check if tryEmit() return values are being handled/checked in the identified lines
echo "=== Checking tryEmit() handling at lines 969, 997, 1020, 1103 ==="
rg -n -B2 -A2 '(assembleFlow|networkChunkFlow)\.tryEmit' app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt

echo ""
echo "=== Searching for parallelDownloads and parallelAssemble variable definitions ==="
rg -n 'val\s+(parallelDownloads|parallelAssemble)' app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt

echo ""
echo "=== Check context around line 916 for variable scope ==="
sed -n '880,920p' app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt

Repository: utkarshdalal/GameNative

Length of output: 3720


🏁 Script executed:

#!/bin/bash
# Understand the flow context and verify if emit() suspension is safe
echo "=== Check flow declaration context and collectors ==="
sed -n '916,960p' app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt

echo ""
echo "=== Check how networkChunkFlow and assembleFlow are collected/processed ==="
rg -n -A3 'networkChunkFlow\.|assembleFlow\.' app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt | head -60

echo ""
echo "=== Check pendingChunks decrement logic ==="
rg -n 'pendingChunks' app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt

Repository: utkarshdalal/GameNative

Length of output: 3565


🏁 Script executed:

#!/bin/bash
# Check context around line 1065 where emit() is used (not tryEmit)
echo "=== Context around line 1065 where emit() is used ==="
sed -n '1055,1075p' app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt

echo ""
echo "=== Look for assembleFlow collection/consumption ==="
rg -n -B5 -A10 'assembleFlow\s*\.' app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt | grep -A15 '\.collect\|\.onEach\|\.forEach'

Repository: utkarshdalal/GameNative

Length of output: 1256


Restore bounded backpressure for the downloader queues.

extraBufferCapacity = Int.MAX_VALUE makes the network/result queues effectively unbounded, allowing pre-allocation and retry re-emission to enqueue large manifests without backpressure. The ignored tryEmit(...) calls are also unsafe once the buffer is bounded, as a failed emit would silently drop work and leave pendingChunks stuck.

Replace the unbounded capacity with a bounded value derived from concurrency levels, and switch all tryEmit() calls to emit() to ensure backpressure safety in the suspend context.

Proposed fix
+            val boundedFlowCapacity = ((parallelDownloads + parallelAssemble) * 4).coerceAtLeast(1)
             val networkChunkFlow = MutableSharedFlow<app.gamenative.service.epic.manifest.ChunkInfo>(extraBufferCapacity = Int.MAX_VALUE)
             val assembleFlow =
                 MutableSharedFlow<Pair<app.gamenative.service.epic.manifest.ChunkInfo, Result<File>>>(extraBufferCapacity = Int.MAX_VALUE)
-            val networkChunkFlow = MutableSharedFlow<app.gamenative.service.epic.manifest.ChunkInfo>(extraBufferCapacity = Int.MAX_VALUE)
+            val networkChunkFlow = MutableSharedFlow<app.gamenative.service.epic.manifest.ChunkInfo>(
+                extraBufferCapacity = boundedFlowCapacity,
+            )
             val assembleFlow =
-                MutableSharedFlow<Pair<app.gamenative.service.epic.manifest.ChunkInfo, Result<File>>>(extraBufferCapacity = Int.MAX_VALUE)
+                MutableSharedFlow<Pair<app.gamenative.service.epic.manifest.ChunkInfo, Result<File>>>(
+                    extraBufferCapacity = boundedFlowCapacity,
+                )
-                            assembleFlow.tryEmit(chunk to result)
+                            assembleFlow.emit(chunk to result)
-                                    networkChunkFlow.tryEmit(chunk)
+                                    networkChunkFlow.emit(chunk)
-                                        networkChunkFlow.tryEmit(chunk)
+                                        networkChunkFlow.emit(chunk)
-                        missingChunks.forEach { networkChunkFlow.tryEmit(it) }
+                        missingChunks.forEach { networkChunkFlow.emit(it) }

Also applies to lines 969, 997, 1020, 1103.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            val boundedFlowCapacity = ((parallelDownloads + parallelAssemble) * 4).coerceAtLeast(1)
            val networkChunkFlow = MutableSharedFlow<app.gamenative.service.epic.manifest.ChunkInfo>(
                extraBufferCapacity = boundedFlowCapacity,
            )
            val assembleFlow =
                MutableSharedFlow<Pair<app.gamenative.service.epic.manifest.ChunkInfo, Result<File>>>(
                    extraBufferCapacity = boundedFlowCapacity,
                )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt` around
lines 916 - 918, The MutableSharedFlow declarations for networkChunkFlow and
assembleFlow at lines 916-918 use extraBufferCapacity set to Int.MAX_VALUE,
which creates unbounded queues that bypass proper backpressure. Replace the
Int.MAX_VALUE values with a bounded capacity derived from the downloader's
concurrency levels to restore backpressure safety. Additionally, locate all
tryEmit() calls associated with these flows (including at lines 969, 997, 1020,
and 1103) and replace them with emit() to ensure proper backpressure handling in
the suspend context, preventing silent drops of work and deadlocks in
pendingChunks tracking.

@utkarshdalal

Copy link
Copy Markdown
Owner

@joshuatam thoughts here?

I think we should be using the settings for the download speed for the parallelism, not hardcoding it like this @Misazam

Comment on lines +463 to +465
val parallelDownloads = PrefManager.downloadSpeed
.coerceAtLeast(1)
.coerceAtMost(EPIC_MAX_PARALLEL_DOWNLOADS)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Misazam I didn't check other usage of PrefManager.downloadSpeed in this class on #1554

The real calculation of the parallel download value, is based on a class DownloadSpeedConfig.

Comment on lines +68 to +69
private const val EPIC_MAX_PARALLEL_DOWNLOADS = 6
private const val EPIC_MAX_PARALLEL_ASSEMBLY = 2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hardcoding with max values would only help reducing CPU usage, but would also slow down the download throughput. did you compare real game download time in this PR?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Besides, if you tried set the Download Speed in app setting to Medium, the device will be cooler similar to your test result.

@Misazam

Misazam commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback.

I understand the concern about Epic's specific speed limit, and I'm not attached to that particular implementation.

What I need is guidance on the appropriate official solution for this issue.

My device is a OnePlus Pad 3, a high-end Android tablet. During large Epic installations, the current official behavior can cause the tablet to overheat for extended periods. I'm concerned not only about download speed but also battery drain and long-term hardware stress during very large downloads.

If this occurs on a high-end tablet, I'm worried that the Epic download manager's behavior will be even worse on more modest devices.

If the proposed solution is for "users to manually set the download speed to medium for large Epic downloads," I can accept that, but I think the app should indicate this more clearly or, possibly, handle fragmented Epic installations more conservatively by default. Epic downloads aren't just network downloads; They also involve fragment decompression, file verification, and assembly, so the CPU/thermal cost is different from a simple download.

I don't want to maintain a local fork indefinitely just to protect my tablet with every GameNative update. If this pull request isn't suitable, could you suggest a design you consider acceptable?

For example:

  • Limit this pull request to batch processing progress and metadata indexing;

  • Adapt DownloadSpeedConfig so that large Epic installations use safer ratios;

  • Add a tablet-optimized mode with temperature control;

  • Improve the existing download speed settings/warning so users know which option to choose for large Epic installations.

My main goal is simple: I want to be able to use official GameNative versions on my tablet without having to reapply the Epic downloader's local optimizations after every update.

@utkarshdalal

Copy link
Copy Markdown
Owner

@Misazam - you should see the settings of the app. There's a slider there for download speed which says that it trades off between download speed and heat. This setting is used in Steam downloads for the parallelism, like you are doing here. The point is that it should be configurable by the user.

@Misazam

Misazam commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

I updated the PR to follow the existing Download Speed setting instead of hardcoding Epic-specific caps.

Epic now uses DownloadSpeedConfig for parallelism, including the overlay path. The PR keeps the internal overhead reductions: batched progress updates, reduced hot-path logging, and pre-indexed chunk assembly metadata.

Validated locally with :app:compileModernDebugKotlin.

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.

3 participants