From 7b2ab786a2965395142298e229f3dd0907d0fb7c Mon Sep 17 00:00:00 2001 From: wangbo Date: Fri, 31 Jul 2026 04:55:31 +0800 Subject: [PATCH] =?UTF-8?q?perf(acceptance):=20=E6=B5=81=E5=BC=8F=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=94=AF=E4=B8=80=E5=9B=BE=E7=89=87=E8=BE=93=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 避免千请求验收重复复用同一图片内容而争用同一资产记录,使每个任务走真实独立上传与物化链路。\n\nGemini 请求体改为流式 Base64 编码,降低压测客户端的大对象内存峰值;新增尺寸、哈希唯一性和请求体还原测试。\n\n验证:Go 全量测试、go vet、迁移安全检查、bash -n、ShellCheck、gofmt、git diff --check 均通过。 --- apps/api/cmd/acceptance-load/main.go | 60 +++++++++++++++++------ apps/api/cmd/acceptance-load/main_test.go | 52 ++++++++++++++++++++ 2 files changed, 97 insertions(+), 15 deletions(-) diff --git a/apps/api/cmd/acceptance-load/main.go b/apps/api/cmd/acceptance-load/main.go index df2a4ba..13dc486 100644 --- a/apps/api/cmd/acceptance-load/main.go +++ b/apps/api/cmd/acceptance-load/main.go @@ -247,20 +247,6 @@ func runGemini( realUpstream bool, ) phaseResult { startedAt := time.Now() - input := paddedPNG(inputBytes) - requestBody, _ := json.Marshal(map[string]any{ - "contents": []any{map[string]any{ - "role": "user", - "parts": []any{ - map[string]any{"text": "将参考图编辑为蓝色赛博朋克风格,保留主体结构"}, - map[string]any{"inlineData": map[string]any{ - "mimeType": "image/png", - "data": base64.StdEncoding.EncodeToString(input), - }}, - }, - }}, - "generationConfig": map[string]any{"responseModalities": []any{"IMAGE"}}, - }) expectedHash := "" if expectedOutputBytes > 0 { sum := sha256.Sum256(paddedPNG(expectedOutputBytes)) @@ -287,7 +273,16 @@ func runGemini( defer func() { <-slots }() requestStarted := time.Now() endpoint := opts.gateways[index%len(opts.gateways)] + "/v1beta/models/" + url.PathEscape(opts.geminiModel) + ":generateContent" - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(requestBody)) + requestBody := streamGeminiRequestBody(paddedPNGVariant(inputBytes, index+1)) + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + endpoint, + requestBody, + ) + if err != nil { + _ = requestBody.Close() + } if err == nil { opts.setHeaders(req, index, realUpstream) req.Header.Set("Content-Type", "application/json") @@ -344,6 +339,34 @@ func runGemini( return phaseResult{report: result, latencies: latencies, err: firstErr} } +func streamGeminiRequestBody(input []byte) io.ReadCloser { + reader, writer := io.Pipe() + go func() { + closeWithError := func(err error) { + _ = writer.CloseWithError(err) + } + if _, err := io.WriteString(writer, `{"contents":[{"role":"user","parts":[{"text":"将参考图编辑为蓝色赛博朋克风格,保留主体结构"},{"inlineData":{"mimeType":"image/png","data":"`); err != nil { + closeWithError(err) + return + } + encoder := base64.NewEncoder(base64.StdEncoding, writer) + if _, err := encoder.Write(input); err != nil { + closeWithError(err) + return + } + if err := encoder.Close(); err != nil { + closeWithError(err) + return + } + if _, err := io.WriteString(writer, `"}}]}],"generationConfig":{"responseModalities":["IMAGE"]}}`); err != nil { + closeWithError(err) + return + } + _ = writer.Close() + }() + return reader +} + func runVideo( ctx context.Context, client *http.Client, @@ -778,6 +801,10 @@ func percentileMilliseconds(values []time.Duration, quantile float64) float64 { } func paddedPNG(size int) []byte { + return paddedPNGVariant(size, 0) +} + +func paddedPNGVariant(size int, variant int) []byte { base, _ := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=") if size <= len(base) { return base @@ -790,6 +817,9 @@ func paddedPNG(size int) []byte { chunk := make([]byte, 12+paddingLength) binary.BigEndian.PutUint32(chunk[:4], uint32(paddingLength)) copy(chunk[4:8], []byte("teST")) + if variant != 0 && paddingLength >= 8 { + binary.BigEndian.PutUint64(chunk[8:16], uint64(variant)) + } binary.BigEndian.PutUint32(chunk[8+paddingLength:], crc32.ChecksumIEEE(chunk[4:8+paddingLength])) out := make([]byte, 0, size) out = append(out, base[:iendOffset]...) diff --git a/apps/api/cmd/acceptance-load/main_test.go b/apps/api/cmd/acceptance-load/main_test.go index 10fed3d..79b9558 100644 --- a/apps/api/cmd/acceptance-load/main_test.go +++ b/apps/api/cmd/acceptance-load/main_test.go @@ -27,6 +27,58 @@ func TestStreamGeminiImageHashDoesNotNeedWholeResponse(t *testing.T) { } } +func TestPaddedPNGVariantsAreExactSizeAndUnique(t *testing.T) { + first := paddedPNGVariant(256<<10, 1) + second := paddedPNGVariant(256<<10, 2) + if len(first) != 256<<10 || len(second) != 256<<10 { + t.Fatalf("variant sizes=%d/%d", len(first), len(second)) + } + firstHash := sha256.Sum256(first) + secondHash := sha256.Sum256(second) + if firstHash == secondHash { + t.Fatal("distinct variants have the same SHA-256") + } +} + +func TestStreamGeminiRequestBodyPreservesInput(t *testing.T) { + input := paddedPNGVariant(2<<20, 37) + var body struct { + Contents []struct { + Parts []struct { + InlineData *struct { + MIMEType string `json:"mimeType"` + Data string `json:"data"` + } `json:"inlineData"` + } `json:"parts"` + } `json:"contents"` + GenerationConfig struct { + ResponseModalities []string `json:"responseModalities"` + } `json:"generationConfig"` + } + stream := streamGeminiRequestBody(input) + defer stream.Close() + if err := json.NewDecoder(stream).Decode(&body); err != nil { + t.Fatalf("decode streamed Gemini request: %v", err) + } + if len(body.Contents) != 1 || len(body.Contents[0].Parts) != 2 || body.Contents[0].Parts[1].InlineData == nil { + t.Fatalf("unexpected Gemini body structure: %+v", body) + } + inlineData := body.Contents[0].Parts[1].InlineData + if inlineData.MIMEType != "image/png" { + t.Fatalf("mime type=%q", inlineData.MIMEType) + } + decoded, err := base64.StdEncoding.DecodeString(inlineData.Data) + if err != nil { + t.Fatalf("decode streamed input: %v", err) + } + if !bytes.Equal(decoded, input) { + t.Fatal("streamed input differs from source") + } + if len(body.GenerationConfig.ResponseModalities) != 1 || body.GenerationConfig.ResponseModalities[0] != "IMAGE" { + t.Fatalf("response modalities=%v", body.GenerationConfig.ResponseModalities) + } +} + func TestVideoCombinationsProvide128UniqueInputs(t *testing.T) { images := make([]string, 16) for index := range images {