task-1: 建立 Similar ASIN 性能基线夹具 (1000/5000行、图片开关、chunk与payload采样)

新增 SimilarAsinPerfFixture 确定性夹具:按 sourceFileKey+rowIndex 派生字段,
支持 0/1/1000/5000 行、图片开关两种模式、chunk 划分与 payload 字节采样;
空输入返回空、超限(>5000行/非法chunk/空key)抛 IllegalArgumentException。
含 10 个测试覆盖默认路径/批量/幂等/边界/非法输入/依赖失败恢复。
This commit is contained in:
2026-08-29 14:22:07 +08:00
parent 322905607a
commit 2039acdfe6
4 changed files with 1097 additions and 0 deletions
@@ -0,0 +1,139 @@
package com.nanri.aiimage.modules.similarasin.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParsedRowVo;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Similar ASIN 性能基线夹具:生成 1000/5000 行解析数据、图片开关两种模式、
* chunk 划分与 payload 大小采样,供性能基线测试与压测复用。
* 上限约束:单次最多 MAX_ROWS 行,防止基线夹具本身造成无界内存增长。
*/
@Slf4j
public class SimilarAsinPerfFixture {
public static final int MAX_ROWS = 5000;
public static final int DEFAULT_CHUNK_SIZE = 200;
private static final String[] COUNTRIES = {"英国", "德国", "法国", "意大利", "西班牙"};
private static final String[] TITLES = {
"Women Floral Dress Summer Casual",
"Men Cotton T-Shirt Crew Neck",
"Kids Waterproof Rain Jacket",
"Fitness Yoga Pants High Waist",
"Home Office Desk Lamp LED",
"Stainless Steel Water Bottle 750ml",
"Wireless Bluetooth Earbuds Pro",
"Pet Grooming Brush Cat Dog"
};
private static final String[] SKU_PREFIX = {"SKU", "MSKU", "ASIN-ITEM", "PROD"};
private final ObjectMapper objectMapper;
public SimilarAsinPerfFixture(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
/** 生成 rowCount 行解析行数据;withImages=true 时为每行生成 http 图片 URL。rowCount 超出 [0, MAX_ROWS] 时拒绝。
* 所有字段由 sourceFileKey + rowIndex 确定性派生,同一输入必然产生相同输出(幂等)。 */
public List<SimilarAsinParsedRowVo> generateRows(String sourceFileKey, int rowCount, boolean withImages) {
if (sourceFileKey == null || sourceFileKey.isBlank()) {
throw new IllegalArgumentException("sourceFileKey 不能为空");
}
if (rowCount < 0 || rowCount > MAX_ROWS) {
throw new IllegalArgumentException("rowCount 必须在 [0, " + MAX_ROWS + "] 范围内,实际 " + rowCount);
}
List<SimilarAsinParsedRowVo> rows = new ArrayList<>(rowCount);
for (int i = 0; i < rowCount; i++) {
SimilarAsinParsedRowVo row = new SimilarAsinParsedRowVo();
int rowIndex = i + 1;
long seed = (sourceFileKey.hashCode() * 31L + rowIndex) & 0x7fffffffL;
String sourceId = String.valueOf(rowIndex);
row.setSourceFileKey(sourceFileKey);
row.setSourceFilename(sourceFileKey.substring(sourceFileKey.lastIndexOf('/') + 1));
row.setRowIndex(rowIndex);
row.setSourceId(sourceId);
row.setDisplayId(sourceId);
row.setRowToken(rowTokenFor(sourceFileKey, rowIndex));
row.setAsin(deterministicAsin(seed));
row.setCountry(COUNTRIES[(int) (seed >> 5) % COUNTRIES.length]);
row.setSku(SKU_PREFIX[(int) (seed >> 9) % SKU_PREFIX.length] + "-" + (1000 + rowIndex));
row.setTitle(TITLES[(int) (seed >> 13) % TITLES.length]);
if (withImages) {
row.setUrl("https://m.media-amazon.com/images/I/" + deterministicAsin(seed ^ 0x5DEDE5B5L) + ".jpg");
} else {
row.setUrl("");
}
Map<String, String> values = new LinkedHashMap<>();
values.put("id", sourceId);
values.put("asin", row.getAsin());
values.put("国家", row.getCountry());
values.put("价格", String.format("%.2f", 1 + (seed % 9900) / 100.0));
values.put("货号", row.getSku());
values.put("标题", row.getTitle());
if (withImages) {
values.put("主图URL", row.getUrl());
}
row.setValues(values);
rows.add(row);
}
return rows;
}
public String rowTokenFor(String sourceFileKey, Integer rowIndex) {
return sourceFileKey + "::row::" + rowIndex;
}
/** 按 chunkSize 顺序划分;chunkSize 必须为正数,行集合不能为 null。 */
public List<List<SimilarAsinParsedRowVo>> splitChunks(List<SimilarAsinParsedRowVo> rows, int chunkSize) {
if (rows == null) {
throw new IllegalArgumentException("rows 不能为 null");
}
if (chunkSize <= 0) {
throw new IllegalArgumentException("chunkSize 必须为正数,实际 " + chunkSize);
}
List<List<SimilarAsinParsedRowVo>> chunks = new ArrayList<>();
if (rows.isEmpty()) {
return chunks;
}
for (int from = 0; from < rows.size(); from += chunkSize) {
int to = Math.min(from + chunkSize, rows.size());
chunks.add(new ArrayList<>(rows.subList(from, to)));
}
return chunks;
}
/** 采样全量行 payload 大小与 chunk 划分数。序列化失败时向上抛出不产生部分结果。 */
public Metrics samplePayload(List<SimilarAsinParsedRowVo> rows, boolean withImages, int chunkSize) {
List<List<SimilarAsinParsedRowVo>> chunks = splitChunks(rows, chunkSize);
if (rows.isEmpty()) {
return new Metrics(0, 0, 0);
}
try {
byte[] bytes = objectMapper.writeValueAsString(rows).getBytes(StandardCharsets.UTF_8);
return new Metrics(rows.size(), chunks.size(), bytes.length);
} catch (Exception ex) {
throw new IllegalStateException("payload 采样序列化失败", ex);
}
}
public record Metrics(int rowCount, int chunkCount, long payloadBytes) {
}
private static String deterministicAsin(long seed) {
StringBuilder sb = new StringBuilder("B0");
long state = seed;
for (int i = 0; i < 8; i++) {
state = state * 6364136223846793005L + 1442695040888963407L;
int pick = (int) ((state >>> 33) % 36);
sb.append(pick < 10 ? (char) ('0' + pick) : (char) ('A' + pick - 10));
}
return sb.toString();
}
}