/** * Stress test payload generator for PDF export benchmarking. * * Generates a realistic XWX AI Chat Exporter PDF HTML document: * - chat message bubbles (AI + user) * - markdown-rendered content: headings, paragraphs, tables * - Shiki-style syntax highlighted code blocks (default 'github' theme) * - optional base64 images (small PNGs, deterministic) * * Deterministic output: same profile => byte-identical HTML, so before/after * optimization runs are comparable. */ const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const CSS = ` @media print { @page { size: A4; margin: 15mm 10mm; } body { -webkit-print-color-adjust: exact; } } body { font-family: -apple-system, sans-serif; font-size: 14px; line-height: 1.6; max-width: 746px; margin: 0 auto; padding: 20px; } h1,h2,h3 { font-weight: 600; margin: 16px 0 8px; } pre { background: #f6f8fa; padding: 16px; border-radius: 6px; overflow-x: auto; border: 1px solid #e1e4e8; } code { font-family: monospace; font-size: 13px; } p { margin: 8px 0; } table { border-collapse: collapse; width: 100%; margin: 12px 0; } th,td { border: 1px solid #dfe2e5; padding: 8px 12px; } th { background: #f1f3f4; } .chat-container { display: flex; flex-direction: column; gap: 16px; } .message-row { display: flex; gap: 10px; } .message-bubble { max-width: 85%; padding: 12px 16px; border-radius: 12px; } .ai-bubble { background: #fff; border: 1px solid #eee; } .user-bubble { background: #e8f0fe; } .avatar { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } `.trim(); function escapeHtml(s) { return s.replace(/&/g, '&').replace(//g, '>'); } const CODE_LINES = [ 'async function fetchData(url, options = {}) {', ' const controller = new AbortController();', ' const timeout = setTimeout(() => controller.abort(), 30000);', ' try {', ' const response = await fetch(url, {', ' ...options,', ' signal: controller.signal,', " headers: { 'Content-Type': 'application/json' },", ' });', ' if (!response.ok) {', ' throw new Error(`HTTP ${response.status}: ${response.statusText}`);', ' }', ' const data = await response.json();', " console.log('Data received:', data);", ' return data;', ' } catch (error) {', " console.error('Fetch failed:', error.message);", ' throw error;', ' } finally {', ' clearTimeout(timeout);', ' }', '}', ]; function shikiWrap(token, color) { return `${escapeHtml(token)}`; } // Deterministic pseudo-random generator (mulberry32) so payloads are stable. function mulberry32(seed) { return function () { let t = (seed += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function generateShikiCode(rng) { return CODE_LINES.map((line) => { const tokens = line.split(/(\s+|[^a-zA-Z0-9_\s]+)/g); return tokens.map((tok) => { if (!tok) return ''; if (/^\s+$/.test(tok)) return tok; let color = '#24292e'; if (/^(const|let|var|function|return|if|else|for|async|await|import|from|export|class|extends|new|try|catch|throw|typeof|this|switch|case|break|continue|while|do|of|in|static|get|set|super)$/.test(tok)) color = '#d73a49'; else if (/^(console|log|error|fetch|Promise|Math|Date|JSON|Map|Set|Array|Object|String|Number|Error|setTimeout|clearTimeout|AbortController|AbortSignal|require|module|exports|process)$/.test(tok)) color = '#6f42c1'; else if (/^\d+$/.test(tok)) color = '#005cc5'; else if (/^[{}()\[\];,.:=+\-*/<>!&|?%@~^'"`]+$/.test(tok)) color = '#24292e'; else if (/^[A-Z]/.test(tok) && tok.length > 1) color = '#e36209'; return shikiWrap(tok, color); }).join(''); }).join('\n'); } // Tiny 1x1 PNG base64 (repeated to simulate distinct images) const TINY_PNG_B64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='; function buildParagraph(rng, words) { const vocab = [ '性能', '优化', '浏览器', '渲染', 'PDF', '导出', '并发', '线程', '内存', '缓存', '请求', '服务器', 'docker', 'huggingface', 'puppeteer', 'chromium', 'the', 'quick', 'brown', 'fox', 'jumps', 'over', 'lazy', 'dog', 'data', 'stream', 'buffer', 'async', 'await', 'promise', 'queue', 'worker', ]; const parts = []; for (let i = 0; i < words; i++) { parts.push(vocab[Math.floor(rng() * vocab.length)]); } return `
${parts.join(' ')}
`; } /** * @param {object} opts * @param {number} opts.textMB target text-only size in MB * @param {number} opts.images number of base64 images to embed * @param {number} opts.codeRatio fraction (0-1) of content that is code blocks * @param {number} opts.messages number of chat messages */ function buildHtml(opts = {}) { const textMB = opts.textMB ?? 0.3; const images = opts.images ?? 0; const codeRatio = opts.codeRatio ?? 0.35; const rng = mulberry32(0x5eed1234); const codeBlock = `${generateShikiCode(rng)}`;
const codeBlockSize = Buffer.byteLength(codeBlock, 'utf8');
const paraWords = 40;
const paraBlock = buildParagraph(rng, paraWords);
const paraBlockSize = Buffer.byteLength(paraBlock, 'utf8');
const overhead = 4096;
const targetBytes = Math.max(2048, textMB * 1024 * 1024 - overhead);
const codeBytes = targetBytes * codeRatio;
const textBytes = targetBytes * (1 - codeRatio);
const codeIters = Math.max(1, Math.ceil(codeBytes / codeBlockSize));
const textIters = Math.max(1, Math.ceil(textBytes / paraBlockSize));
const imageTag = `