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,
|
realUpstream bool,
|
||||||
) phaseResult {
|
) phaseResult {
|
||||||
startedAt := time.Now()
|
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 := ""
|
expectedHash := ""
|
||||||
if expectedOutputBytes > 0 {
|
if expectedOutputBytes > 0 {
|
||||||
sum := sha256.Sum256(paddedPNG(expectedOutputBytes))
|
sum := sha256.Sum256(paddedPNG(expectedOutputBytes))
|
||||||
@@ -287,7 +273,16 @@ func runGemini(
|
|||||||
defer func() { <-slots }()
|
defer func() { <-slots }()
|
||||||
requestStarted := time.Now()
|
requestStarted := time.Now()
|
||||||
endpoint := opts.gateways[index%len(opts.gateways)] + "/v1beta/models/" + url.PathEscape(opts.geminiModel) + ":generateContent"
|
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 {
|
if err == nil {
|
||||||
opts.setHeaders(req, index, realUpstream)
|
opts.setHeaders(req, index, realUpstream)
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
@@ -344,6 +339,34 @@ func runGemini(
|
|||||||
return phaseResult{report: result, latencies: latencies, err: firstErr}
|
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(
|
func runVideo(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
client *http.Client,
|
client *http.Client,
|
||||||
@@ -778,6 +801,10 @@ func percentileMilliseconds(values []time.Duration, quantile float64) float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func paddedPNG(size int) []byte {
|
func paddedPNG(size int) []byte {
|
||||||
|
return paddedPNGVariant(size, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func paddedPNGVariant(size int, variant int) []byte {
|
||||||
base, _ := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=")
|
base, _ := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=")
|
||||||
if size <= len(base) {
|
if size <= len(base) {
|
||||||
return base
|
return base
|
||||||
@@ -790,6 +817,9 @@ func paddedPNG(size int) []byte {
|
|||||||
chunk := make([]byte, 12+paddingLength)
|
chunk := make([]byte, 12+paddingLength)
|
||||||
binary.BigEndian.PutUint32(chunk[:4], uint32(paddingLength))
|
binary.BigEndian.PutUint32(chunk[:4], uint32(paddingLength))
|
||||||
copy(chunk[4:8], []byte("teST"))
|
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]))
|
binary.BigEndian.PutUint32(chunk[8+paddingLength:], crc32.ChecksumIEEE(chunk[4:8+paddingLength]))
|
||||||
out := make([]byte, 0, size)
|
out := make([]byte, 0, size)
|
||||||
out = append(out, base[:iendOffset]...)
|
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) {
|
func TestVideoCombinationsProvide128UniqueInputs(t *testing.T) {
|
||||||
images := make([]string, 16)
|
images := make([]string, 16)
|
||||||
for index := range images {
|
for index := range images {
|
||||||
|
|||||||
Reference in New Issue
Block a user