refactor(downloader): 重构下载器分片与系统资源分配逻辑 - #3506
Conversation
审查者指南该 PR 将旧版的逐文件分块下载替换为自适应的、基于 Range 的大文件下载流程,并提供顺序下载回退机制;同时通过全局连接、缓冲区、带宽和监控资源进行协调。该 PR 还新增了 HTTP 协议选择、面向连接的配置、更智能的加载器复用与探测机制,以及相应的 UI 更新。 自适应大文件下载时序图sequenceDiagram
participant LoaderDownload
participant FileDownloader
participant AdaptiveRangeDownloader
participant DownloadResourceManager
participant HTTPServer
participant Disk
LoaderDownload->>FileDownloader: DownloadAsync
FileDownloader->>AdaptiveRangeDownloader: TryDownloadAsync
AdaptiveRangeDownloader->>DownloadResourceManager: AcquireConnectionAsync
AdaptiveRangeDownloader->>HTTPServer: Range probe
HTTPServer-->>AdaptiveRangeDownloader: PartialContent with ContentRange
AdaptiveRangeDownloader->>DownloadResourceManager: AcquireConnectionAsync
AdaptiveRangeDownloader->>HTTPServer: Range segment requests
HTTPServer-->>AdaptiveRangeDownloader: PartialContent
AdaptiveRangeDownloader->>DownloadResourceManager: ReserveBufferAsync
AdaptiveRangeDownloader->>DownloadResourceManager: ThrottleAsync
AdaptiveRangeDownloader->>Disk: RandomAccess.WriteAsync
AdaptiveRangeDownloader->>DownloadResourceManager: RecordDownloadedBytes
AdaptiveRangeDownloader-->>FileDownloader: completed
FileDownloader->>FileDownloader: PromoteTempFile
下载策略选择流程图flowchart TD
Start[DownloadSingleAsync] --> Size{expectedSize known}
Size -->|less than 4 MiB| Sequential[DownloadSequentiallyAsync]
Size -->|unknown| Probe[TryDownloadAsync]
Size -->|at least 4 MiB| Range[TryDownloadAsync]
Probe -->|Range unsupported or small| Sequential
Range -->|Range supported| Parallel[Adaptive range workers]
Range -->|Range unsupported| Sequential
Sequential --> Promote[PromoteTempFile]
Parallel --> Promote
Promote --> Complete[MarkDownloadCompleted]
文件级变更
针对关联 Issue 的评估
可能关联的 Issue
提示和命令与 Sourcery 交互
自定义使用体验访问你的控制面板:
获取帮助Original review guide in EnglishReviewer's GuideThe PR replaces legacy per-file chunked downloading with an adaptive, range-based large-file pipeline and sequential fallback, coordinated by global connection, buffer, bandwidth, and monitoring resources; it also adds HTTP protocol selection, connection-oriented configuration, smarter loader reuse/probing, and corresponding UI updates. Sequence diagram for adaptive large-file downloadingsequenceDiagram
participant LoaderDownload
participant FileDownloader
participant AdaptiveRangeDownloader
participant DownloadResourceManager
participant HTTPServer
participant Disk
LoaderDownload->>FileDownloader: DownloadAsync
FileDownloader->>AdaptiveRangeDownloader: TryDownloadAsync
AdaptiveRangeDownloader->>DownloadResourceManager: AcquireConnectionAsync
AdaptiveRangeDownloader->>HTTPServer: Range probe
HTTPServer-->>AdaptiveRangeDownloader: PartialContent with ContentRange
AdaptiveRangeDownloader->>DownloadResourceManager: AcquireConnectionAsync
AdaptiveRangeDownloader->>HTTPServer: Range segment requests
HTTPServer-->>AdaptiveRangeDownloader: PartialContent
AdaptiveRangeDownloader->>DownloadResourceManager: ReserveBufferAsync
AdaptiveRangeDownloader->>DownloadResourceManager: ThrottleAsync
AdaptiveRangeDownloader->>Disk: RandomAccess.WriteAsync
AdaptiveRangeDownloader->>DownloadResourceManager: RecordDownloadedBytes
AdaptiveRangeDownloader-->>FileDownloader: completed
FileDownloader->>FileDownloader: PromoteTempFile
Flow diagram for download strategy selectionflowchart TD
Start[DownloadSingleAsync] --> Size{expectedSize known}
Size -->|less than 4 MiB| Sequential[DownloadSequentiallyAsync]
Size -->|unknown| Probe[TryDownloadAsync]
Size -->|at least 4 MiB| Range[TryDownloadAsync]
Probe -->|Range unsupported or small| Sequential
Range -->|Range supported| Parallel[Adaptive range workers]
Range -->|Range unsupported| Sequential
Sequential --> Promote[PromoteTempFile]
Parallel --> Promote
Promote --> Complete[MarkDownloadCompleted]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
# Conflicts: # PCL.Core/App/Localization/Languages/zh-CN.xaml # Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml
There was a problem hiding this comment.
嘿——我发现了 4 个问题
AI Agent 提示词
请处理这次代码审查中的评论:
## 单独评论
### 评论 1
<location path="Plain Craft Launcher 2/Modules/Minecraft/ModSkin.cs" line_range="156-158" />
<code_context>
if (!File.Exists(fileAddress))
{
- FileDownloader.DownloadAsync(address, fileAddress + ModNet.netDownloadEnd).GetAwaiter().GetResult();
+ FileDownloader.DownloadAsync(address, fileAddress + ModNet.NetDownloadEnd).GetAwaiter().GetResult();
File.Delete(fileAddress);
- FileSystem.Rename(fileAddress + ModNet.netDownloadEnd, fileAddress);
+ FileSystem.Rename(fileAddress + ModNet.NetDownloadEnd, fileAddress);
ModBase.Log("[Minecraft] 皮肤下载成功:" + fileAddress);
}
</code_context>
<issue_to_address>
**issue (bug_risk):** `McSkinDownload` 将 `fileAddress + ModNet.NetDownloadEnd` 作为 `localPath` 传入,但重构后的下载器始终会写入 `localPath + ModNet.NetDownloadEnd`。因此,下载会创建 `fileAddress + .PCLDownloading + .PCLDownloading`,而调用方尝试重命名 `fileAddress + .PCLDownloading`,由于该路径不存在,重命名会失败。
**触发条件:** 通过 `McSkinDownload` 下载皮肤时。
**建议修复:** 将 `fileAddress` 传递给 `FileDownloader.DownloadAsync`,或者让下载器统一接受已经添加后缀的临时路径。
</issue_to_address>
### 评论 2
<location path="Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs" line_range="138" />
<code_context>
+ var totalSize = response.Content.Headers.ContentLength ?? -1;
+ // 某些源使用 chunked 传输没有 Content-Length,但清单校验信息仍可能带有文件大小
+ // 这个大小只用于慢速判断,完整性仍按响应头的 totalSize 校验
+ var slowCheckSize = totalSize > 0 ? totalSize : trackedFile?.Check?.actualSize ?? -1;
+ if (trackedFile is not null)
+ {
</code_context>
<issue_to_address>
**issue (broader_impact):** 当响应没有 `Content-Length` 时,顺序下载不会验证清单中的 `trackedFile.Check.actualSize`。`slowCheckSize` 仅用于慢速判断,而最终大小检查由于 `totalSize` 为 `-1` 被跳过,因此,如果分块传输的响应提前结束,文件仍会被当作成功完成的完整文件处理。
**触发条件:** 源使用不带 `Content-Length` 的分块传输,并且在达到清单声明的大小之前终止传输时。
**建议修复:** 将预期大小与响应的 `Content-Length` 分开跟踪,并在已下载字节数与 `trackedFile.Check.actualSize` 不一致时拒绝完成下载。
</issue_to_address>
### 评论 3
<location path="Plain Craft Launcher 2/Modules/Network/Downloader/DownloadResourceManager.cs" line_range="54" />
<code_context>
+ CancellationToken cancellationToken)
+ {
+ var host = Uri.TryCreate(url, UriKind.Absolute, out var uri) ? uri.Host : url;
+ var hostQuota = HostConnectionQuotas.GetOrAdd(host, static _ => new AsyncQuota());
+ var hostLease = await hostQuota.AcquireAsync(1, () => ModNet.NetTaskConnectionsPerHostLimit, cancellationToken)
+ .ConfigureAwait(false);
</code_context>
<issue_to_address>
**issue (bug_risk):** 所有之前见过的主机都会永久保留在 `HostConnectionQuotas` 中,下载完成后也不会移除配额。因此,来自不受限制或由用户控制的主机名的下载会在进程的整个生命周期内为每个主机累积一个 `AsyncQuota` 对象。
**触发条件:** 启动器会话从许多不同的主机下载内容时,例如不断变化的签名 CDN 或镜像 URL。
**建议修复:** 在主机配额的使用量降为零后移除处于空闲状态的配额,或者使用有界的、会驱逐条目的主机配额注册表。
</issue_to_address>
### 评论 4
<location path="Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs" line_range="259-270" />
<code_context>
+ internal static async Task<HttpResponseMessage> SendDownloadRequestAsync(string url,
+ HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ using var requestTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ requestTimeout.CancelAfter(RequestTimeoutMilliseconds);
+ try
+ {
+ return await GetHttpClient(url)
+ .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, requestTimeout.Token)
+ .ConfigureAwait(false);
+ }
+ catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested &&
+ requestTimeout.IsCancellationRequested)
+ {
+ throw new TimeoutException($"等待下载源响应超时(30 秒):{url}", ex);
+ }
+ }
</code_context>
<issue_to_address>
**issue (bug_risk):** 等待响应头时发生的请求超时会被转换为 `TimeoutException`,而 `AdaptiveRangeDownloader.TryRecover` 处理的异常类型中不包括该异常。因此,单个分段的响应头超时会取消整个自适应下载,而不是按照所宣称的慢速连接恢复机制重试该分段。
**触发条件:** 某个范围分段请求接收响应头的时间超过 30 秒,而其他分段仍然可用时。
**建议修复:** 将转换后的 `TimeoutException` 视为可恢复的分段失败,或者为范围分段超时保留一种可重试的异常类型。
</issue_to_address>帮助我变得更有用!请在每条评论上点击 👍 或 👎,我会利用这些反馈来改进审查结果。
Original comment in English
Hey - I've found 4 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="Plain Craft Launcher 2/Modules/Minecraft/ModSkin.cs" line_range="156-158" />
<code_context>
if (!File.Exists(fileAddress))
{
- FileDownloader.DownloadAsync(address, fileAddress + ModNet.netDownloadEnd).GetAwaiter().GetResult();
+ FileDownloader.DownloadAsync(address, fileAddress + ModNet.NetDownloadEnd).GetAwaiter().GetResult();
File.Delete(fileAddress);
- FileSystem.Rename(fileAddress + ModNet.netDownloadEnd, fileAddress);
+ FileSystem.Rename(fileAddress + ModNet.NetDownloadEnd, fileAddress);
ModBase.Log("[Minecraft] 皮肤下载成功:" + fileAddress);
}
</code_context>
<issue_to_address>
**issue (bug_risk):** `McSkinDownload` passes `fileAddress + ModNet.NetDownloadEnd` as `localPath`, but the refactored downloader always writes to `localPath + ModNet.NetDownloadEnd`. The download therefore creates `fileAddress + .PCLDownloading + .PCLDownloading`, while the caller renames `fileAddress + .PCLDownloading`, so the rename fails because that path does not exist.
**Triggers:** When downloading a skin through `McSkinDownload`.
**Suggested fix:** Pass `fileAddress` to `FileDownloader.DownloadAsync`, or make the downloader consistently accept an already-suffixed temporary path.
</issue_to_address>
### Comment 2
<location path="Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs" line_range="138" />
<code_context>
+ var totalSize = response.Content.Headers.ContentLength ?? -1;
+ // 某些源使用 chunked 传输没有 Content-Length,但清单校验信息仍可能带有文件大小
+ // 这个大小只用于慢速判断,完整性仍按响应头的 totalSize 校验
+ var slowCheckSize = totalSize > 0 ? totalSize : trackedFile?.Check?.actualSize ?? -1;
+ if (trackedFile is not null)
+ {
</code_context>
<issue_to_address>
**issue (broader_impact):** Sequential downloads do not validate the manifest's `trackedFile.Check.actualSize` when the response has no `Content-Length`. `slowCheckSize` is used only for the slow-speed heuristic, and the final size check is skipped because `totalSize` is `-1`, so a chunked response that ends early is promoted as a successful complete file.
**Triggers:** When a source uses chunked transfer without `Content-Length` and terminates before the manifest-declared size.
**Suggested fix:** Track the expected size separately from the response `Content-Length` and reject completion when the downloaded byte count differs from `trackedFile.Check.actualSize`.
</issue_to_address>
### Comment 3
<location path="Plain Craft Launcher 2/Modules/Network/Downloader/DownloadResourceManager.cs" line_range="54" />
<code_context>
+ CancellationToken cancellationToken)
+ {
+ var host = Uri.TryCreate(url, UriKind.Absolute, out var uri) ? uri.Host : url;
+ var hostQuota = HostConnectionQuotas.GetOrAdd(host, static _ => new AsyncQuota());
+ var hostLease = await hostQuota.AcquireAsync(1, () => ModNet.NetTaskConnectionsPerHostLimit, cancellationToken)
+ .ConfigureAwait(false);
</code_context>
<issue_to_address>
**issue (bug_risk):** Every previously seen host is retained permanently in `HostConnectionQuotas`, and no quota is removed after its downloads finish. Downloads from unbounded or user-controlled hostnames therefore accumulate one `AsyncQuota` object per host for the lifetime of the process.
**Triggers:** When a launcher session downloads from many distinct hosts, such as changing signed CDN or mirror URLs.
**Suggested fix:** Remove an idle host quota after its usage reaches zero, or use a bounded/evicting host-quota registry.
</issue_to_address>
### Comment 4
<location path="Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs" line_range="259-270" />
<code_context>
+ internal static async Task<HttpResponseMessage> SendDownloadRequestAsync(string url,
+ HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ using var requestTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ requestTimeout.CancelAfter(RequestTimeoutMilliseconds);
+ try
+ {
+ return await GetHttpClient(url)
+ .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, requestTimeout.Token)
+ .ConfigureAwait(false);
+ }
+ catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested &&
+ requestTimeout.IsCancellationRequested)
+ {
+ throw new TimeoutException($"等待下载源响应超时(30 秒):{url}", ex);
+ }
+ }
</code_context>
<issue_to_address>
**issue (bug_risk):** A request timeout while waiting for response headers is converted to `TimeoutException`, which is not one of the exceptions handled by `AdaptiveRangeDownloader.TryRecover`; a single segment header timeout therefore cancels the entire adaptive download instead of retrying that segment under the advertised slow-connection recovery path.
**Triggers:** When a range-segment request takes longer than 30 seconds to receive response headers while other segments are still usable.
**Suggested fix:** Treat the converted `TimeoutException` as a recoverable segment failure, or preserve a retryable exception type for range-segment timeouts.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1a2a8f061d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Generated by GPT-5.6 Terra Max
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 292138af6f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
This is generated by gpt-5.6-sol (max)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 70d67f1cd3
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 65311c96ca
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
- 在分段与顺序下载中,读取完成后立即重置倒计时,防止限速等待期间误触发超时 - 重构 AsyncQuota 为严格 FIFO 定向转让模式,消除全量广播惊群并提高队列调度性能 本PR由Gemini 3.8对代码进行了审查。
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 561c7b5165
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 59755ff7ab
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
本 PR 对下载器的分片策略与系统资源分配进行了重构。
修改内容:针对文件下载回退到 HTTP/1.1(至少在我这里高峰期 1.1 的速度显著快于 2.0),元数据与 API 请求仍然使用 HTTP/2.0;仅针对大于 4 MiB 的文件尝试分片;批量任务中未知大小的文件直接下载,不进行 Range 探测;大文件下载采用新的动态分片机制,分片大小更加灵活;内存缓存改为全局共享,并及时将内容写入硬盘,避免极端情况下内存过高占用。
测试时进行 Minecraft 26.2 清洁安装,下载高峰阶段相比 PCL 2.13.1.0 的相同阶段内存占用可减少约 2/3(稳定在 180 - 220 MB),同时保持下载速度基本持平甚至略微更快;与 PCL CE 2.15.0 相比内存占用差距不大(此 PR 版本占用略微减少),主要改进了部分情况下下载速度极其缓慢的问题。
同时,close #3356 。
本 PR 主要使用 AI 完成,因此需要较为细致的测试与检查。同时,回退到 HTTP/1.1 还是保留 HTTP/2.0 可能仍需要进一步调查与讨论。
Sourcery 摘要
重构下载器的分片、连接管理和资源调度逻辑,以提升大文件下载性能、稳定性及内存使用效率。
新功能:
错误修复:
增强功能:
杂项:
Original summary in English
Summary by Sourcery
重构下载器的分片、连接管理和资源调度逻辑,以提升大文件下载性能、稳定性及内存使用效率。
New Features:
Bug Fixes:
Enhancements:
Chores:
Original summary in English
Original summary in English
Sourcery 总结
重构下载器,使其能够根据文件大小自适应分段并共享资源,从而提升大文件下载的可靠性和内存使用效率,同时改进连接控制和 HTTP 行为。
新功能:
错误修复:
增强功能:
Original summary in English
Sourcery 总结
重构下载器的分片、资源管理与连接控制逻辑,以改善大文件下载性能、稳定性和内存使用。
新功能:
错误修复:
改进:
Original summary in English
Sourcery 摘要
重构下载器的分片策略与资源管理,以提升大文件下载性能、稳定性和内存使用效率。
新功能:
错误修复:
增强功能:
Original summary in English
Sourcery 摘要
重构下载器的分片、连接管理和资源调度逻辑,以提升大文件下载性能、稳定性及内存使用效率。
新功能:
错误修复:
改进:
日常维护:
Original summary in English