task-20: Similar ASIN 端到端压测、GC 分析与结果文件兼容回归
SimilarAsinPerfFixture 新增三个功能点: - endToEndBenchmark:生成→分 chunk→序列化,记录行数/chunk 数/payload 字节/耗时/吞吐/峰值堆; - gcStressAnalysis:多轮生成/序列化/释放循环,采样 GC 计数差与堆峰值; - compatRoundTrip:payload 序列化往返恢复全量行并校验字段稳定(结果文件兼容回归)。 全量测试 508 通过。
This commit is contained in:
+135
@@ -1,14 +1,17 @@
|
||||
package com.nanri.aiimage.modules.similarasin.util;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParsedRowVo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Similar ASIN 性能基线夹具:生成 1000/5000 行解析数据、图片开关两种模式、
|
||||
@@ -20,6 +23,7 @@ public class SimilarAsinPerfFixture {
|
||||
|
||||
public static final int MAX_ROWS = 5000;
|
||||
public static final int DEFAULT_CHUNK_SIZE = 200;
|
||||
private static final int GC_ROUNDS = 10;
|
||||
|
||||
private static final String[] COUNTRIES = {"英国", "德国", "法国", "意大利", "西班牙"};
|
||||
private static final String[] TITLES = {
|
||||
@@ -126,6 +130,137 @@ public class SimilarAsinPerfFixture {
|
||||
public record Metrics(int rowCount, int chunkCount, long payloadBytes) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 端到端基准:生成 → 分 chunk → 序列化全量 payload,记录行数、chunk 数、
|
||||
* payload 字节、组装耗时(毫秒)、吞吐(行/秒)与峰值堆(字节)。
|
||||
* 依赖失败(序列化抛异常)时向上抛出 IllegalStateException,不产生部分结果。
|
||||
*/
|
||||
public EndToEndMetrics endToEndBenchmark(String sourceFileKey, int rowCount, boolean withImages, int chunkSize) {
|
||||
List<SimilarAsinParsedRowVo> rows = generateRows(sourceFileKey, rowCount, withImages);
|
||||
List<List<SimilarAsinParsedRowVo>> chunks = splitChunks(rows, chunkSize);
|
||||
if (rows.isEmpty()) {
|
||||
return new EndToEndMetrics(0, 0, 0, 0, 0, 0);
|
||||
}
|
||||
long startMillis = System.currentTimeMillis();
|
||||
long peakHeapBefore = sampledPeakHeapBytes();
|
||||
long payloadBytes;
|
||||
try {
|
||||
byte[] bytes = objectMapper.writeValueAsBytes(rows);
|
||||
payloadBytes = bytes.length;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("端到端基准序列化失败", ex);
|
||||
}
|
||||
long elapsedMillis = Math.max(1L, System.currentTimeMillis() - startMillis);
|
||||
long peakHeap = Math.max(peakHeapBefore, sampledPeakHeapBytes());
|
||||
double throughput = rows.size() * 1000.0 / elapsedMillis;
|
||||
return new EndToEndMetrics(rows.size(), chunks.size(), payloadBytes, elapsedMillis, throughput, peakHeap);
|
||||
}
|
||||
|
||||
/**
|
||||
* GC 压力分析:多轮生成/序列化/释放循环,每轮记录 GC 计数差与堆峰值。
|
||||
* 返回最后采样样本;rounds 表示实际执行轮数。临时对象随轮释放,堆峰值有界。
|
||||
*/
|
||||
public GcStressSample gcStressAnalysis(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);
|
||||
}
|
||||
long before = totalGcCount();
|
||||
long peak = sampledPeakHeapBytes();
|
||||
int rounds = 0;
|
||||
for (int round = 1; round <= GC_ROUNDS; round++) {
|
||||
List<SimilarAsinParsedRowVo> rows = generateRows(sourceFileKey, rowCount, withImages);
|
||||
if (!rows.isEmpty()) {
|
||||
try {
|
||||
objectMapper.writeValueAsBytes(rows);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("GC 压力分析序列化失败", ex);
|
||||
}
|
||||
}
|
||||
rounds++;
|
||||
peak = Math.max(peak, sampledPeakHeapBytes());
|
||||
}
|
||||
return new GcStressSample(rounds, totalGcCount() - before, peak);
|
||||
}
|
||||
|
||||
/** payload 序列化往返兼容回归:序列化全量行 → 反序列化恢复 → 校验行数一致与字段稳定。 */
|
||||
public CompatResult compatRoundTrip(String sourceFileKey, int rowCount, boolean withImages) {
|
||||
List<SimilarAsinParsedRowVo> rows = generateRows(sourceFileKey, rowCount, withImages);
|
||||
if (rows.isEmpty()) {
|
||||
return new CompatResult(0, 0, true);
|
||||
}
|
||||
byte[] bytes;
|
||||
try {
|
||||
bytes = objectMapper.writeValueAsBytes(rows);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("兼容回归序列化失败", ex);
|
||||
}
|
||||
List<SimilarAsinParsedRowVo> recovered;
|
||||
try {
|
||||
recovered = objectMapper.readValue(bytes, new TypeReference<List<SimilarAsinParsedRowVo>>() {
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("兼容回归反序列化失败", ex);
|
||||
}
|
||||
return new CompatResult(rows.size(), recovered.size(), fieldsStable(rows, recovered));
|
||||
}
|
||||
|
||||
public record EndToEndMetrics(int rowCount, int chunkCount, long payloadBytes, long assembleMillis,
|
||||
double throughputRowsPerSec, long peakHeapBytes) {
|
||||
}
|
||||
|
||||
public record GcStressSample(int rounds, long gcCount, long peakHeapBytes) {
|
||||
}
|
||||
|
||||
public record CompatResult(int rowCount, int recoveredCount, boolean fieldStable) {
|
||||
}
|
||||
|
||||
private static boolean fieldsStable(List<SimilarAsinParsedRowVo> original, List<SimilarAsinParsedRowVo> recovered) {
|
||||
if (original.size() != recovered.size()) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < original.size(); i++) {
|
||||
SimilarAsinParsedRowVo a = original.get(i);
|
||||
SimilarAsinParsedRowVo b = recovered.get(i);
|
||||
if (!Objects.equals(a.getAsin(), b.getAsin())
|
||||
|| !Objects.equals(a.getCountry(), b.getCountry())
|
||||
|| !Objects.equals(a.getSku(), b.getSku())
|
||||
|| !Objects.equals(a.getTitle(), b.getTitle())
|
||||
|| !Objects.equals(a.getUrl(), b.getUrl())
|
||||
|| !Objects.equals(a.getRowToken(), b.getRowToken())
|
||||
|| !Objects.equals(a.getSourceFileKey(), b.getSourceFileKey())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 采样当前堆已用字节峰值;MXBean 不可用时返回 -1,与 "> 0" 类断言不冲突(GC 场景始终可用)。 */
|
||||
private static long sampledPeakHeapBytes() {
|
||||
try {
|
||||
return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
|
||||
} catch (Exception ex) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static long totalGcCount() {
|
||||
long total = 0;
|
||||
try {
|
||||
for (java.lang.management.GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||
long count = bean.getCollectionCount();
|
||||
if (count >= 0) {
|
||||
total += count;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
return 0;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static String deterministicAsin(long seed) {
|
||||
StringBuilder sb = new StringBuilder("B0");
|
||||
long state = seed;
|
||||
|
||||
Reference in New Issue
Block a user