perf(acceptance): 流式生成唯一图片输入
避免千请求验收重复复用同一图片内容而争用同一资产记录,使每个任务走真实独立上传与物化链路。\n\nGemini 请求体改为流式 Base64 编码,降低压测客户端的大对象内存峰值;新增尺寸、哈希唯一性和请求体还原测试。\n\n验证:Go 全量测试、go vet、迁移安全检查、bash -n、ShellCheck、gofmt、git diff --check 均通过。
This commit is contained in:
@@ -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]...)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user