/** * WPDO Comment Stress Test — admin tab JS (v2.13.1) * * Mirrors wpdo-term-stress-test.js (v2.13.0) but: * - Talks to /wpdo/v1/comment-stress-test/* endpoints * - Sends post_id in start payload (not taxonomy) * - DOM IDs prefixed wpdo-cst-* (comment stress test) * * @since 2.13.1 */ ( function () { 'use strict'; const cfg = window.wpdoCommentStressTest; if ( ! cfg || ! cfg.restUrl ) { return; } if ( ! document.querySelector( '.wpdo-comment-stress-test-tab' ) ) { return; } const $ = ( sel ) => document.querySelector( sel ); const restUrl = cfg.restUrl.replace( /\/$/, '' ); const headers = { 'Content-Type': 'application/json', 'X-WP-Nonce': cfg.nonce }; let pollTimer = null; // ── API helpers ───────────────────────────────────────────────────────── async function apiCall( path, method = 'GET', body = null ) { const opts = { method, headers, credentials: 'same-origin' }; if ( body ) { opts.body = JSON.stringify( body ); } const resp = await fetch( restUrl + path, opts ); const text = await resp.text(); try { return { ok: resp.ok, status: resp.status, data: text ? JSON.parse( text ) : null }; } catch ( e ) { return { ok: false, status: resp.status, data: { error: text } }; } } // ── Rendering ─────────────────────────────────────────────────────────── function fmt( n ) { if ( n === null || n === undefined ) { return '—'; } return Number( n ).toLocaleString(); } function setText( sel, text ) { const el = $( sel ); if ( el ) { el.textContent = String( text ); } } function esc( s ) { if ( s === null || s === undefined ) { return ''; } return String( s ).replace( /[&<>"']/g, ( c ) => ( { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[ c ] ) ); } function renderProgress( state ) { const isRunning = state.status === 'running' || state.status === 'benchmarking'; const card = $( '#wpdo-cst-progress-card' ); if ( card ) { card.style.display = ( isRunning || state.status === 'completed' || state.status === 'failed' || state.status === 'cancelled' ) ? '' : 'none'; } setText( '#wpdo-cst-pg-status', state.status || 'idle' ); setText( '#wpdo-cst-pg-post-id', state.post_id ? ( 'post #' + state.post_id ) : '' ); setText( '#wpdo-cst-pg-mode', state.mode || '' ); setText( '#wpdo-cst-pg-pct', ( state.pct || 0 ) + '%' ); setText( '#wpdo-cst-pg-processed', fmt( state.processed || 0 ) ); setText( '#wpdo-cst-pg-target', fmt( state.target || 0 ) ); setText( '#wpdo-cst-pg-rate', state.rate_per_sec || 0 ); setText( '#wpdo-cst-pg-elapsed', state.elapsed_sec || 0 ); setText( '#wpdo-cst-pg-eta', state.eta_sec || 0 ); setText( '#wpdo-cst-pg-batches', state.batches_done || 0 ); setText( '#wpdo-cst-pg-mem', ( ( state.peak_memory || 0 ) / 1048576 ).toFixed( 1 ) ); const bar = $( '#wpdo-cst-pg-bar' ); if ( bar ) { bar.style.width = ( state.pct || 0 ) + '%'; } const count = state.test_comment_count || 0; setText( '#wpdo-cst-count', fmt( count ) ); setText( '#wpdo-cst-count-mirror', fmt( count ) ); const startBtn = $( '#wpdo-cst-start' ); const cancelBtn = $( '#wpdo-cst-cancel' ); const cleanupBtn = $( '#wpdo-cst-cleanup' ); const benchBtn = $( '#wpdo-cst-rerun-bench' ); if ( startBtn ) { startBtn.disabled = isRunning; } if ( cancelBtn ) { cancelBtn.disabled = ! isRunning; } if ( cleanupBtn ) { cleanupBtn.disabled = isRunning || count === 0; } if ( benchBtn ) { benchBtn.disabled = isRunning || count === 0; } } function renderBenchmark( bench ) { if ( ! bench ) { return; } const card = $( '#wpdo-cst-bench-card' ); const content = $( '#wpdo-cst-bench-content' ); if ( ! card || ! content ) { return; } card.style.display = ''; const w = bench.write || {}; const dbSizes = bench.db_sizes || []; const q = bench.query || {}; const labels = { point_rating_5: 'Point lookup (hp_rating = 5)', range_rating_top: 'Range scan (hp_rating >= 4 ORDER BY DESC)', eav_baseline: 'EAV baseline (wp_commentmeta 直查 hp_rating)', }; let html = ''; // Write metrics html += '

▍ 寫入指標

'; html += ''; html += ``; html += ``; html += ``; html += ``; html += ``; html += ``; html += ``; html += ``; html += '
模式${ esc( w.mode ) }
Post ID#${ esc( w.post_id ) }
完成 / 目標${ fmt( w.processed ) } / ${ fmt( w.target ) }
總耗時${ fmt( w.elapsed_sec ) } 秒
平均速率${ fmt( w.rate_per_sec ) } comments/sec
批次數${ fmt( w.batches_done ) }
批次最快/平均/最慢${ fmt( w.batch_min_ms ) } / ${ fmt( w.batch_avg_ms ) } / ${ fmt( w.batch_max_ms ) } ms
PHP Peak Memory${ fmt( w.peak_memory_mb ) } MB
'; // DB sizes html += '

▍ DB 容量(comment 相關表)

'; html += ''; html += ''; html += ''; dbSizes.forEach( ( r ) => { html += ``; html += ``; html += ``; } ); html += '
TableRowsData MBIndex MBTotal MBAvg bytes/row
${ esc( r.table ) }${ fmt( r.rows ) }${ r.data_mb ?? '—' }${ r.index_mb ?? '—' }${ r.total_mb ?? '—' }${ fmt( r.avg_bytes ) }
'; // Query perf — 3 probes with EAV baseline last for visual speedup comparison html += '

▍ 查詢效能

'; html += ''; html += ''; html += ''; const baselineMs = q.eav_baseline?.duration_ms ?? null; Object.keys( q ).forEach( ( key ) => { const v = q[ key ]; if ( ! v || typeof v.duration_ms !== 'number' ) { return; } const label = labels[ key ] || key; let speedup = ''; if ( baselineMs !== null && key !== 'eav_baseline' && v.duration_ms > 0 ) { const ratio = baselineMs / v.duration_ms; if ( ratio >= 1 ) { speedup = ` (${ ratio.toFixed( 2 ) }× faster)`; } } html += ``; } ); html += '
測試項目耗時 (ms)
${ esc( label ) }${ speedup }${ v.duration_ms }
'; html += '

EAV baseline 走 wp_commentmeta,flat probes 走 wpdo_comment_hp_review。倍率即此規模下反 EAV 的查詢加速。

'; content.innerHTML = html; } // ── Polling ───────────────────────────────────────────────────────────── async function poll() { const r = await apiCall( '/comment-stress-test/status' ); if ( ! r.ok || ! r.data ) { return; } renderProgress( r.data ); if ( r.data.benchmark ) { renderBenchmark( r.data.benchmark ); } if ( r.data.status !== 'running' && r.data.status !== 'benchmarking' ) { stopPolling(); } } function startPolling() { stopPolling(); poll(); pollTimer = setInterval( poll, 2000 ); } function stopPolling() { if ( pollTimer ) { clearInterval( pollTimer ); pollTimer = null; } } // ── Event handlers ────────────────────────────────────────────────────── async function handleStart() { const post_id = parseInt( $( '#wpdo-cst-post-id' ).value, 10 ); const target = parseInt( $( '#wpdo-cst-target' ).value, 10 ); const batch = parseInt( $( '#wpdo-cst-batch' ).value, 10 ); const mode = document.querySelector( 'input[name="wpdo-cst-mode"]:checked' ).value; if ( ! post_id || post_id < 1 ) { alert( '請選擇目標 post' ); return; } if ( ! target || target < 1 ) { alert( '請輸入有效的 comment 數量' ); return; } if ( mode === 'realistic' && batch > 50 ) { if ( ! confirm( `Realistic 模式每個 comment 約需 30-100 ms(wp_insert_comment + update_comment_meta),batch_size=${ batch } 可能超過後端 8s deadline。建議 batch=10-30。要繼續嗎?` ) ) { return; } } // Soft warn for combinations that won't demonstrate反 EAV 優化效果 const commentMode = String( cfg.commentMode || 'disabled' ); if ( mode === 'fast' && commentMode === 'aeav_only' ) { if ( ! confirm( `⚠️ Fast 模式直接 $wpdb->insert 繞過 Hook Bus,即使 comment mode=aeav_only 也會寫滿 wp_commentmeta。\n\n要驗證反 EAV 優化效果(wp_commentmeta 應為 0),請改用 🐢 Realistic 模式。\n\n仍以 Fast 模式繼續嗎?` ) ) { return; } } else if ( mode === 'realistic' && commentMode !== 'aeav_only' ) { if ( ! confirm( `⚠️ 目前 comment mode = ${ commentMode },Realistic 寫入仍會雙寫 wp_commentmeta(不展示優化效果)。\n\n要看 wp_commentmeta 完全短路請先升 mode 至 aeav_only(設定 tab)。\n\n仍以 ${ commentMode } 模式繼續嗎?` ) ) { return; } } const big = target >= 5000; const msg = `即將以 ${ mode } 模式建立 ${ target.toLocaleString() } 筆 comment(attached to post #${ post_id })${ big ? '(規模較大)' : '' }。\n\n所有 comment 的 author email 會以 @wpdo-stress.local 結尾,可一鍵清除。\n\n確定要開始嗎?`; if ( ! confirm( msg ) ) { return; } const r = await apiCall( '/comment-stress-test/start', 'POST', { post_id, target, mode, batch_size: batch, } ); if ( ! r.ok ) { alert( '啟動失敗:' + ( r.data?.error || r.status ) ); return; } const card = $( '#wpdo-cst-progress-card' ); if ( card ) { card.style.display = ''; } const benchCard = $( '#wpdo-cst-bench-card' ); if ( benchCard ) { benchCard.style.display = 'none'; } setText( '#wpdo-cst-pg-status', 'running' ); setText( '#wpdo-cst-pg-post-id', 'post #' + post_id ); setText( '#wpdo-cst-pg-mode', mode ); setText( '#wpdo-cst-pg-target', target.toLocaleString() ); startPolling(); } async function handleCancel() { if ( ! confirm( '確定要取消當前測試?已建立的 comment 不會被刪除。' ) ) { return; } const cancelBtn = $( '#wpdo-cst-cancel' ); if ( cancelBtn ) { cancelBtn.disabled = true; cancelBtn.textContent = '⏹ 取消中…'; } setText( '#wpdo-cst-pg-status', 'cancelling' ); const r = await apiCall( '/comment-stress-test/cancel', 'POST' ); if ( ! r.ok ) { alert( '取消失敗:' + ( r.data?.error || r.status ) ); if ( cancelBtn ) { cancelBtn.disabled = false; cancelBtn.textContent = '⏹ 取消'; } return; } poll(); if ( cancelBtn ) { cancelBtn.textContent = '⏹ 取消'; } } async function handleCleanup() { if ( ! confirm( '確定要清除所有 stress test comments?\n\n此動作會:\n- DELETE 所有 email 後綴 @wpdo-stress.local 的 comment\n- DELETE 對應 wp_commentmeta\n- DELETE flat 表中對應 comment_id 的列\n\n不可復原!' ) ) { return; } const r = await apiCall( '/comment-stress-test/cleanup', 'DELETE' ); if ( ! r.ok ) { alert( '清除失敗:' + ( r.data?.error || r.status ) ); return; } alert( `已清除 ${ r.data.deleted } 筆測試 comment。` ); const card = $( '#wpdo-cst-progress-card' ); const benchCard = $( '#wpdo-cst-bench-card' ); if ( card ) card.style.display = 'none'; if ( benchCard ) benchCard.style.display = 'none'; poll(); } async function handleRerunBench() { const btn = $( '#wpdo-cst-rerun-bench' ); if ( btn ) { btn.disabled = true; btn.textContent = '⏳ 執行中...'; } const r = await apiCall( '/comment-stress-test/benchmark', 'POST' ); if ( btn ) { btn.disabled = false; btn.textContent = '📊 重跑 Benchmark(不新增資料)'; } if ( ! r.ok ) { alert( 'Benchmark 失敗:' + ( r.data?.error || r.status ) ); return; } renderBenchmark( r.data.benchmark ); } // ── Init ──────────────────────────────────────────────────────────────── document.addEventListener( 'DOMContentLoaded', () => { const startBtn = $( '#wpdo-cst-start' ); const cancelBtn = $( '#wpdo-cst-cancel' ); const cleanupBtn = $( '#wpdo-cst-cleanup' ); const benchBtn = $( '#wpdo-cst-rerun-bench' ); if ( startBtn ) startBtn.addEventListener( 'click', handleStart ); if ( cancelBtn ) cancelBtn.addEventListener( 'click', handleCancel ); if ( cleanupBtn ) cleanupBtn.addEventListener( 'click', handleCleanup ); if ( benchBtn ) benchBtn.addEventListener( 'click', handleRerunBench ); poll().then( () => { const status = ( $( '#wpdo-cst-pg-status' )?.textContent || '' ).trim(); if ( status === 'running' || status === 'benchmarking' ) { startPolling(); } } ); } ); } )();