Files
pj_supermarket_cloud/database/test/index.js
T

284 lines
8.8 KiB
JavaScript

const { spawn } = require("child_process");
const fs = require("fs");
const path = require("path");
const CONFIG = {
url:
process.env.API_URL ||
"https://dzc-test-1.fy.takin.cc/wallet/wallet-setting/ad/get-discounts-list?userId=1875049673022279681&hardId=1887466308336488450",
headers: {
"CLIENT-TOC": process.env.CLIENT_TOC || "Y",
Authorization:
process.env.API_AUTHORIZATION ||
"Bearer 2913ff2a-a570-46c2-adb4-26d397aa9725",
"TENANT-ID": process.env.API_TENANT_ID || "1",
},
rounds: {
round1: { total: 10, concurrency: 2, description: "保守压测第一轮" },
round2: { total: 15, concurrency: 3, description: "保守压测第二轮" },
round3: { total: 20, concurrency: 4, description: "观察是否出现慢尖刺" },
},
outputDir: path.join(__dirname, "output"),
};
function ensureOutputDir() {
fs.mkdirSync(CONFIG.outputDir, { recursive: true });
}
function formatNow() {
const now = new Date();
const pad = (value) => String(value).padStart(2, "0");
return [
now.getFullYear(),
pad(now.getMonth() + 1),
pad(now.getDate()),
"-",
pad(now.getHours()),
pad(now.getMinutes()),
pad(now.getSeconds()),
].join("");
}
function getRoundName() {
const roundName = process.argv[2];
if (!roundName || !CONFIG.rounds[roundName]) {
const available = Object.entries(CONFIG.rounds)
.map(
([name, round]) =>
` - ${name}: total=${round.total}, concurrency=${round.concurrency}, ${round.description}`,
)
.join("\n");
console.log("请指定要执行的轮次,例如: node index.js round1");
console.log("可用轮次:\n" + available);
process.exit(1);
}
return roundName;
}
function buildCurlArgs(requestId) {
const writeOut = JSON.stringify({
requestId,
http_code: "%{http_code}",
time_connect: "%{time_connect}",
time_starttransfer: "%{time_starttransfer}",
time_total: "%{time_total}",
});
const args = ["-o", "/dev/null", "-sS"];
Object.entries(CONFIG.headers).forEach(([key, value]) => {
args.push("-H", `${key}: ${value}`);
});
args.push("-w", writeOut, CONFIG.url);
return args;
}
function runCurl(requestId) {
return new Promise((resolve) => {
const startedAt = new Date().toISOString();
const child = spawn("curl", buildCurlArgs(requestId), {
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("close", (code) => {
let parsed = null;
try {
parsed = JSON.parse(stdout.trim());
} catch (error) {
parsed = null;
}
const result = {
requestId,
startedAt,
finishedAt: new Date().toISOString(),
exitCode: code,
httpCode: Number(parsed?.http_code || 0),
connect: Number(parsed?.time_connect || 0),
starttransfer: Number(parsed?.time_starttransfer || 0),
total: Number(parsed?.time_total || 0),
ok: code === 0 && Number(parsed?.http_code || 0) === 200,
error: stderr.trim() || null,
raw: stdout.trim(),
};
const summary = [
`#${String(requestId).padStart(2, "0")}`,
`code=${result.httpCode || code}`,
`connect=${result.connect.toFixed(3)}s`,
`ttfb=${result.starttransfer.toFixed(3)}s`,
`total=${result.total.toFixed(3)}s`,
];
if (!result.ok && result.error) {
summary.push(`error=${result.error}`);
}
console.log(summary.join(" | "));
resolve(result);
});
});
}
async function runWithConcurrency(total, concurrency) {
const results = [];
let nextId = 1;
async function worker() {
while (nextId <= total) {
const current = nextId;
nextId += 1;
const result = await runCurl(current);
results.push(result);
}
}
const workers = Array.from(
{ length: Math.min(concurrency, total) },
() => worker(),
);
await Promise.all(workers);
results.sort((a, b) => a.requestId - b.requestId);
return results;
}
function percentile(numbers, ratio) {
if (!numbers.length) return 0;
const sorted = [...numbers].sort((a, b) => a - b);
const index = Math.min(
sorted.length - 1,
Math.max(0, Math.ceil(sorted.length * ratio) - 1),
);
return sorted[index];
}
function buildSummary(results) {
const totals = results.map((item) => item.total);
const avg =
totals.reduce((sum, current) => sum + current, 0) / (totals.length || 1);
const max = Math.max(...totals, 0);
const slow3 = results.filter((item) => item.total > 3).length;
const slow5 = results.filter((item) => item.total > 5).length;
const non200 = results.filter((item) => item.httpCode !== 200).length;
return {
count: results.length,
avg,
p50: percentile(totals, 0.5),
p90: percentile(totals, 0.9),
p95: percentile(totals, 0.95),
max,
slow3,
slow5,
non200,
slowest: [...results]
.sort((a, b) => b.total - a.total)
.slice(0, 5)
.map((item) => ({
requestId: item.requestId,
httpCode: item.httpCode,
connect: item.connect,
starttransfer: item.starttransfer,
total: item.total,
error: item.error,
})),
};
}
function writeOutputs(roundName, results, summary) {
ensureOutputDir();
const suffix = `${formatNow()}-${roundName}`;
const jsonPath = path.join(CONFIG.outputDir, `${suffix}.json`);
const tsvPath = path.join(CONFIG.outputDir, `${suffix}.tsv`);
fs.writeFileSync(
jsonPath,
JSON.stringify(
{
roundName,
url: CONFIG.url,
headers: {
"CLIENT-TOC": CONFIG.headers["CLIENT-TOC"],
Authorization: "***masked***",
"TENANT-ID": CONFIG.headers["TENANT-ID"],
},
summary,
results,
},
null,
2,
),
);
const headerLine =
"requestId\thttpCode\tconnect\tstarttransfer\ttotal\texitCode\tstartedAt\tfinishedAt\terror";
const lines = results.map((item) =>
[
item.requestId,
item.httpCode,
item.connect,
item.starttransfer,
item.total,
item.exitCode,
item.startedAt,
item.finishedAt,
(item.error || "").replace(/\s+/g, " ").trim(),
].join("\t"),
);
fs.writeFileSync(tsvPath, [headerLine, ...lines].join("\n"));
return { jsonPath, tsvPath };
}
async function main() {
const roundName = getRoundName();
const round = CONFIG.rounds[roundName];
console.log(`开始执行 ${roundName}: ${round.description}`);
console.log(`URL: ${CONFIG.url}`);
console.log(`总请求数: ${round.total}, 并发数: ${round.concurrency}`);
const results = await runWithConcurrency(round.total, round.concurrency);
const summary = buildSummary(results);
const files = writeOutputs(roundName, results, summary);
console.log("\n=== 汇总 ===");
console.log(`count: ${summary.count}`);
console.log(`avg_total: ${summary.avg.toFixed(3)}s`);
console.log(`p50_total: ${summary.p50.toFixed(3)}s`);
console.log(`p90_total: ${summary.p90.toFixed(3)}s`);
console.log(`p95_total: ${summary.p95.toFixed(3)}s`);
console.log(`max_total: ${summary.max.toFixed(3)}s`);
console.log(`over_3s: ${summary.slow3}`);
console.log(`over_5s: ${summary.slow5}`);
console.log(`non_200: ${summary.non200}`);
console.log("\n=== 最慢 5 条 ===");
summary.slowest.forEach((item) => {
console.log(
`#${String(item.requestId).padStart(2, "0")} | code=${item.httpCode} | connect=${item.connect.toFixed(3)}s | ttfb=${item.starttransfer.toFixed(3)}s | total=${item.total.toFixed(3)}s`,
);
});
console.log("\n结果文件:");
console.log(`JSON: ${files.jsonPath}`);
console.log(`TSV : ${files.tsvPath}`);
}
main().catch((error) => {
console.error("执行失败:", error);
process.exit(1);
});