Files
LWLT-AI/archive/handoff/2026-07-12/legacy-erp-handoff/tools/erp_performance_profile.js
2026-07-13 19:57:46 +08:00

384 lines
14 KiB
JavaScript

const fs = require('fs/promises');
const path = require('path');
const { pathToFileURL } = require('url');
const { chromium } = require('playwright-core');
const WORKSPACE = path.resolve(__dirname, '..');
const BASE = 'https://ltjt.yunzhi.run/system/Business/';
const DEFAULT_GROUPS = [
{ groupNo: 'LW-260713A-A', ddid: 14021, tid: 13944 },
{ groupNo: 'LW-260714A-A', ddid: 14022, tid: 13945 },
{ groupNo: 'LW-260715A-A', ddid: 14023, tid: 13946 },
];
const EXPORT_SPECS = [
{ key: 'liantai-confirm', href: group => `orders_confirm_new.asp?did=${group.ddid}&tid=${group.tid}` },
{ key: 'xingyou-confirm', href: group => `orders_confirm_news.asp?did=${group.ddid}&tid=${group.tid}` },
{ key: 'job-order', href: group => `teams_beian1.asp?did=${group.ddid}` },
];
const chromeCandidates = [
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Google\\Chrome\\Application\\chrome.exe'),
].filter(Boolean);
function nowStamp() {
const d = new Date();
const pad = n => String(n).padStart(2, '0');
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
}
function parseArgs() {
const args = new Map();
for (const raw of process.argv.slice(2)) {
const [key, ...rest] = raw.replace(/^--/, '').split('=');
args.set(key, rest.length ? rest.join('=') : 'true');
}
return {
skipExport: args.get('skip-export') === 'true',
skipConvert: args.get('skip-convert') === 'true',
render: args.get('render') || 'sample', // none | sample | all
exportMethod: args.get('export-method') || 'auto', // auto | request | browser
convertConcurrency: Number(args.get('convert-concurrency') || 1),
profile: args.get('profile') || path.join(WORKSPACE, 'diagnostics', 'erp-playwright-profile'),
groupsFile: args.get('groups'),
sourceDir: args.get('source-dir'),
};
}
async function findChrome() {
for (const p of chromeCandidates) {
try {
await fs.access(p);
return p;
} catch {}
}
throw new Error('Chrome executable not found');
}
async function mkdirs(...dirs) {
for (const dir of dirs) await fs.mkdir(dir, { recursive: true });
}
async function timed(label, timings, fn) {
const start = performance.now();
try {
const result = await fn();
const ms = Math.round(performance.now() - start);
timings.push({ label, ms, ok: true });
return result;
} catch (err) {
const ms = Math.round(performance.now() - start);
timings.push({ label, ms, ok: false, error: String(err && err.stack || err) });
throw err;
}
}
async function loadGroups(groupsFile) {
if (!groupsFile) return DEFAULT_GROUPS;
const text = await fs.readFile(groupsFile, 'utf8');
const parsed = JSON.parse(text);
if (!Array.isArray(parsed)) throw new Error('groups file must contain an array');
return parsed;
}
async function requestExport(request, url, file, startedAt) {
const response = await request.get(url, { timeout: 20000 });
const body = await response.body();
await fs.writeFile(file, body);
return {
method: 'request',
status: response.status(),
url,
file,
bytes: body.length,
contentType: response.headers()['content-type'] || '',
contentDisposition: response.headers()['content-disposition'] || '',
ms: Date.now() - startedAt,
ok: response.ok() && body.length > 5000,
};
}
async function browserExport(page, url, file, startedAt) {
const downloadPromise = page.waitForEvent('download', { timeout: 12000 }).catch(() => null);
let gotoError = null;
let response = null;
try {
response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
} catch (err) {
gotoError = err;
}
const download = await downloadPromise;
if (download) {
await download.saveAs(file);
const stat = await fs.stat(file);
return {
method: 'download',
url,
file,
bytes: stat.size,
suggestedFilename: download.suggestedFilename(),
ms: Date.now() - startedAt,
ok: stat.size > 5000,
};
}
if (gotoError && String(gotoError).includes('Download is starting')) {
throw new Error(`Download started but Playwright did not expose file: ${url}`);
}
if (gotoError) throw gotoError;
if (!response) throw new Error(`No response: ${url}`);
const body = await response.body();
await fs.writeFile(file, body);
return {
method: 'response-body',
status: response.status(),
url,
file,
bytes: body.length,
contentType: response.headers()['content-type'] || '',
ms: Date.now() - startedAt,
ok: response.ok() && body.length > 5000,
};
}
async function exportOne(context, page, downloadDir, group, spec, method) {
const url = BASE + spec.href(group);
const file = path.join(downloadDir, `${group.groupNo}_${spec.key}.doc`);
const startedAt = Date.now();
const withMeta = record => ({ groupNo: group.groupNo, key: spec.key, ...record });
if (method === 'request' || method === 'auto') {
try {
const record = await requestExport(context.request, url, file, startedAt);
if (record.ok || method === 'request') return withMeta(record);
} catch (err) {
if (method === 'request') throw err;
}
}
return withMeta(await browserExport(page, url, file, startedAt));
}
async function exportDocs(run, groups, executablePath) {
const context = await chromium.launchPersistentContext(run.profile, {
executablePath,
headless: false,
acceptDownloads: true,
downloadsPath: run.downloadsDir,
viewport: { width: 1440, height: 900 },
args: ['--disable-extensions-except=', '--disable-component-extensions-with-background-pages'],
});
const page = context.pages()[0] || await context.newPage();
page.setDefaultTimeout(60000);
const results = [];
for (const group of groups) {
for (const spec of EXPORT_SPECS) {
const start = Date.now();
const record = await exportOne(context, page, run.downloadsDir, group, spec, run.exportMethod).catch(err => ({
groupNo: group.groupNo,
key: spec.key,
error: String(err && err.stack || err),
ms: Date.now() - start,
ok: false,
}));
results.push(record);
console.log(`[export] ${record.groupNo} ${record.key} ${record.ok ? 'ok' : 'fail'} ${record.ms || 0}ms`);
}
}
await context.close();
return results;
}
async function copySources(sourceDir, outputDir) {
const copied = [];
const names = (await fs.readdir(sourceDir)).filter(n => n.toLowerCase().endsWith('.doc')).sort();
for (const name of names) {
const src = path.join(sourceDir, name);
const dest = path.join(outputDir, name);
await fs.copyFile(src, dest);
copied.push(dest);
}
return copied;
}
async function mapLimit(items, limit, worker) {
const results = new Array(items.length);
let index = 0;
const runners = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
while (index < items.length) {
const current = index++;
results[current] = await worker(items[current], current);
}
});
await Promise.all(runners);
return results;
}
async function convertDocs(run, executablePath) {
const browser = await chromium.launch({ executablePath, headless: true });
const docs = (await fs.readdir(run.outputsDir)).filter(n => n.toLowerCase().endsWith('.doc')).sort();
const results = await mapLimit(docs, run.convertConcurrency, async (name) => {
const page = await browser.newPage({ viewport: { width: 1240, height: 1754 } });
const startedAt = Date.now();
try {
const src = path.join(run.outputsDir, name);
const html = path.join(run.renderHtmlDir, name.replace(/\.doc$/i, '.html'));
const pdf = path.join(run.outputsDir, name.replace(/\.doc$/i, '.pdf'));
await fs.copyFile(src, html);
await page.goto(pathToFileURL(html).href, { waitUntil: 'load', timeout: 60000 });
await page.emulateMedia({ media: 'print' });
await page.pdf({
path: pdf,
format: 'A4',
printBackground: true,
margin: { top: '10mm', right: '8mm', bottom: '10mm', left: '8mm' },
preferCSSPageSize: true,
});
const srcStat = await fs.stat(src);
const pdfStat = await fs.stat(pdf);
const record = {
source: src,
pdf,
sourceBytes: srcStat.size,
pdfBytes: pdfStat.size,
ms: Date.now() - startedAt,
ok: pdfStat.size > 1000,
};
console.log(`[convert] ${name} ${record.ms}ms`);
return record;
} finally {
await page.close().catch(() => {});
}
});
await browser.close();
return results;
}
async function verifyPdfs(run, executablePath, renderMode) {
const pdfs = (await fs.readdir(run.outputsDir)).filter(n => n.toLowerCase().endsWith('.pdf')).sort();
const selected = renderMode === 'all'
? pdfs
: renderMode === 'sample'
? pdfs.filter((_, index) => index === 0 || index === Math.floor(pdfs.length / 2) || index === pdfs.length - 1)
: [];
const quick = [];
for (const name of pdfs) {
const p = path.join(run.outputsDir, name);
const stat = await fs.stat(p);
quick.push({ pdf: p, bytes: stat.size, ok: stat.size > 1000 });
}
const renders = [];
if (selected.length) {
const browser = await chromium.launch({ executablePath, headless: true });
const page = await browser.newPage({ viewport: { width: 1200, height: 1600 } });
for (const name of selected) {
const startedAt = Date.now();
const pdf = path.join(run.outputsDir, name);
const png = path.join(run.rendersDir, name.replace(/\.pdf$/i, '.png'));
await page.goto(pathToFileURL(pdf).href, { waitUntil: 'networkidle', timeout: 60000 });
await page.screenshot({ path: png, fullPage: false });
const stat = await fs.stat(png);
renders.push({ pdf, png, bytes: stat.size, ms: Date.now() - startedAt, ok: stat.size > 1000 });
console.log(`[render] ${name} ${Date.now() - startedAt}ms`);
}
await browser.close();
}
return { quick, renders, renderMode };
}
function summarize(profile) {
const sum = arr => arr.reduce((acc, x) => acc + (x.ms || 0), 0);
const stageMs = label => profile.timings.find(x => x.label === label)?.ms || 0;
const exportMs = stageMs('export-docs');
const convertMs = stageMs('convert-docs-to-pdf');
const renderMs = stageMs('verify-pdfs');
const totalMs = profile.timings.reduce((acc, x) => acc + (x.ms || 0), 0);
return {
totalMeasuredMs: totalMs,
exportMs,
convertMs,
renderMs,
exportWorkMs: sum(profile.exports || []),
convertWorkMs: sum(profile.conversions || []),
renderWorkMs: sum(profile.pdfVerification?.renders || []),
docs: profile.conversions?.length || profile.exports?.length || 0,
exportAvgMs: profile.exports?.length ? Math.round(exportMs / profile.exports.length) : 0,
convertAvgMs: profile.conversions?.length ? Math.round(convertMs / profile.conversions.length) : 0,
renderAvgMs: profile.pdfVerification?.renders?.length ? Math.round(renderMs / profile.pdfVerification.renders.length) : 0,
};
}
async function main() {
const args = parseArgs();
const runRoot = path.join(WORKSPACE, 'diagnostics', 'perf', `run-${nowStamp()}`);
const run = {
root: runRoot,
downloadsDir: path.join(runRoot, 'downloads'),
outputsDir: path.join(runRoot, 'outputs'),
renderHtmlDir: path.join(runRoot, 'render-html'),
rendersDir: path.join(runRoot, 'renders'),
profile: args.profile,
exportMethod: args.exportMethod,
convertConcurrency: Math.max(1, args.convertConcurrency || 1),
};
await mkdirs(run.downloadsDir, run.outputsDir, run.renderHtmlDir, run.rendersDir);
const groups = await loadGroups(args.groupsFile);
const executablePath = await findChrome();
const timings = [];
const profile = {
startedAt: new Date().toISOString(),
args,
run,
groups,
timings,
};
if (!args.skipExport) {
profile.exports = await timed('export-docs', timings, () => exportDocs(run, groups, executablePath));
for (const record of profile.exports.filter(r => r.file && r.ok)) {
await fs.copyFile(record.file, path.join(run.outputsDir, path.basename(record.file)));
}
} else {
const source = args.sourceDir || path.join(WORKSPACE, 'diagnostics', 'erp-downloads');
profile.copiedSources = await timed('copy-existing-docs', timings, () => copySources(source, run.outputsDir));
}
if (!args.skipConvert) {
profile.conversions = await timed('convert-docs-to-pdf', timings, () => convertDocs(run, executablePath));
}
profile.pdfVerification = await timed('verify-pdfs', timings, () => verifyPdfs(run, executablePath, args.render));
profile.summary = summarize(profile);
profile.finishedAt = new Date().toISOString();
const jsonPath = path.join(runRoot, 'performance.json');
await fs.writeFile(jsonPath, JSON.stringify(profile, null, 2), 'utf8');
const md = [
'# ERP Performance Profile',
'',
`- Run: ${runRoot}`,
`- Export files: ${profile.exports?.length || 0}`,
`- Converted PDFs: ${profile.conversions?.length || 0}`,
`- Render mode: ${args.render}`,
'',
'## Summary',
'',
`- Export total: ${(profile.summary.exportMs / 1000).toFixed(1)}s, avg ${profile.summary.exportAvgMs}ms/file`,
`- PDF convert total: ${(profile.summary.convertMs / 1000).toFixed(1)}s, avg ${profile.summary.convertAvgMs}ms/file`,
`- PDF render total: ${(profile.summary.renderMs / 1000).toFixed(1)}s, avg ${profile.summary.renderAvgMs}ms/file`,
'',
'## Files',
'',
`- JSON: ${jsonPath}`,
`- Outputs: ${run.outputsDir}`,
].join('\n');
await fs.writeFile(path.join(runRoot, 'summary.md'), md, 'utf8');
console.log(JSON.stringify({ runRoot, summary: profile.summary }, null, 2));
}
main().catch(err => {
console.error(err);
process.exit(1);
});