/**
* 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 += `| 模式 | ${ esc( w.mode ) } |
`;
html += `| Post ID | #${ esc( w.post_id ) } |
`;
html += `| 完成 / 目標 | ${ fmt( w.processed ) } / ${ fmt( w.target ) } |
`;
html += `| 總耗時 | ${ fmt( w.elapsed_sec ) } 秒 |
`;
html += `| 平均速率 | ${ fmt( w.rate_per_sec ) } comments/sec |
`;
html += `| 批次數 | ${ fmt( w.batches_done ) } |
`;
html += `| 批次最快/平均/最慢 | ${ fmt( w.batch_min_ms ) } / ${ fmt( w.batch_avg_ms ) } / ${ fmt( w.batch_max_ms ) } ms |
`;
html += `| PHP Peak Memory | ${ fmt( w.peak_memory_mb ) } MB |
`;
html += '
';
// DB sizes
html += '▍ DB 容量(comment 相關表)
';
html += '';
html += '| Table | Rows | Data MB | Index MB | Total MB | Avg bytes/row | ';
html += '
';
dbSizes.forEach( ( r ) => {
html += `${ esc( r.table ) } | ${ fmt( r.rows ) } | `;
html += `${ r.data_mb ?? '—' } | ${ r.index_mb ?? '—' } | `;
html += `${ r.total_mb ?? '—' } | ${ fmt( r.avg_bytes ) } |
`;
} );
html += '
';
// Query perf — 3 probes with EAV baseline last for visual speedup comparison
html += '▍ 查詢效能
';
html += '';
html += '| 測試項目 | 耗時 (ms) | ';
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 += `| ${ esc( label ) }${ speedup } | ${ v.duration_ms } |
`;
} );
html += '
';
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();
}
} );
} );
} )();