perf(app): throttle upload progress callbacks to 256KB steps
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+10
-2
@@ -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()
|
||||
}
|
||||
|
||||
+30
-1
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user