perf(app): throttle upload progress callbacks to 256KB steps

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-28 14:10:29 +08:00
parent 203f62e7cb
commit 45dbde3719
3 changed files with 47 additions and 4 deletions
@@ -38,11 +38,15 @@ sealed class PutOutcome {
* - 请求头:严格携带 Dart 传入的 credential.headers(Content-Type 经
* RequestBody.contentType 表达,Content-Length 由 okhttp 按声明长度
* 发送,二者不重复注入);
* - 进度:写 body 驱动 [onProgress](已发字节/总字节,IO 线程回调);
* - 进度:写 body 驱动 [onProgress](已发字节/总字节,IO 线程回调;
* 256KB 步长节流,末块必报 sent==total);
* - OkHttp 独立实例(4.12,与 Flutter 引擎无耦合;连接失败内置静默重试)。
*/
object UploadPutExecutor {
/** 进度回调最小步长(字节);末块必报。 */
private const val PROGRESS_STEP_BYTES = 256L * 1024
private val defaultClient: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(Duration.ofSeconds(15))
.writeTimeout(Duration.ofMinutes(3))
@@ -75,6 +79,7 @@ object UploadPutExecutor {
file.inputStream().use { input ->
val buffer = ByteArray(64 * 1024)
var sent = 0L
var lastReported = 0L
while (true) {
val read = input.read(buffer)
if (read < 0) {
@@ -82,7 +87,10 @@ object UploadPutExecutor {
}
sink.write(buffer, 0, read)
sent += read
onProgress?.invoke(sent, total)
if (sent - lastReported >= PROGRESS_STEP_BYTES || sent == total) {
lastReported = sent
onProgress?.invoke(sent, total)
}
}
sink.flush()
}
@@ -14,7 +14,7 @@ import org.junit.Test
* UploadPutExecutor 结局分类测试(P3 计划 Task 7;MockWebServer 驱动):
* 200→Success(etag)、500→Retryable(server)、403→RenewRequired、
* 4xx→HttpFail、连接失败→Retryable(network)、缺文件→SourceMissing、
* 请求头透传与进度回调。
* 请求头透传与进度回调(单调性 + 256KB 步长节流/末块必报)。
*/
class UploadPutExecutorTest {
@@ -147,6 +147,35 @@ class UploadPutExecutorTest {
samples.forEach { assertEquals(file.length(), it.second) }
}
@Test
fun `progress callback throttles to 256KB steps and reports final chunk`() {
// 700KB 夹具:64KB 写循环下未节流会有 ~11 次回调;节流后仅在
// 累计 ≥256KB 或末块(sent==total)时上报。
server.enqueue(MockResponse().setResponseCode(200))
val big = Files.createTempFile("upload-put-big", ".bin")
big.toFile().deleteOnExit()
val bigFile = big.toFile()
Files.write(big, ByteArray(700_000) { (it % 251).toByte() })
val samples = mutableListOf<Pair<Long, Long>>()
UploadPutExecutor.execute(
url = server.url("/staging/x.jpg").toString(),
method = "PUT",
headers = mapOf("Content-Type" to "image/jpeg"),
filePath = bigFile.absolutePath,
client = client,
) { sent, total -> samples.add(sent to total) }
// 末块必报:sent == total。
assertEquals(bigFile.length(), samples.last().first)
assertEquals(bigFile.length(), samples.last().second)
// 步长节流:相邻回调字节差 ≥256KB(末次为余量,可小于步长)。
val step = 256L * 1024
samples.zipWithNext().dropLast(1).forEach { (prev, next) ->
assertTrue(next.first - prev.first >= step)
}
// 节流生效:回调次数少于 64KB 写循环次数。
assertTrue(samples.size < (bigFile.length() / (64 * 1024)).toInt())
}
@Test
fun `blank method defaults to PUT and uppercases`() {
assertEquals("PUT", UploadPutExecutor.normalizeMethod(""))
@@ -139,9 +139,15 @@ class DartHttpTransport implements Transport {
final file = File(filePath);
final length = await file.length();
var sent = 0;
var lastReported = 0;
// 256KB 步长节流(与原生 UploadPutExecutor 一致);末块必报 sent==length。
const progressStep = 256 * 1024;
final progressStream = file.openRead().map((List<int> chunk) {
sent += chunk.length;
onProgress?.call(sent, length);
if (sent - lastReported >= progressStep || sent == length) {
lastReported = sent;
onProgress?.call(sent, length);
}
return chunk;
});
try {