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); });