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:
+139
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user