Files
2meet-data-optimizer/admin/assets/wpdo-post-stress-test.js
T
wpdev d36bb954d1 chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 05:06:36 +08:00

384 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* WPDO Post Stress Test — admin tab JS (v2.11.4)
*
* Mirrors wpdo-stress-test.js (user side) but:
* - Talks to /wpdo/v1/post-stress-test/* endpoints
* - Sends post_type in start payload (user side has no entity selector)
* - DOM IDs prefixed wpdo-pst-* to coexist on the same admin page
* - Renders a smaller benchmark report (3 probes per post_type vs 6 for user)
*
* @since 2.11.4
*/
( function () {
'use strict';
const cfg = window.wpdoPostStressTest;
if ( ! cfg || ! cfg.restUrl ) {
return;
}
if ( ! document.querySelector( '.wpdo-post-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 ) => ( {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
}[ c ] ) );
}
function renderProgress( state ) {
const isRunning = state.status === 'running' || state.status === 'benchmarking';
const card = $( '#wpdo-pst-progress-card' );
if ( card ) {
card.style.display = ( isRunning || state.status === 'completed' || state.status === 'failed' || state.status === 'cancelled' ) ? '' : 'none';
}
setText( '#wpdo-pst-pg-status', state.status || 'idle' );
setText( '#wpdo-pst-pg-post-type', state.post_type || '' );
setText( '#wpdo-pst-pg-mode', state.mode || '' );
setText( '#wpdo-pst-pg-pct', ( state.pct || 0 ) + '%' );
setText( '#wpdo-pst-pg-processed', fmt( state.processed || 0 ) );
setText( '#wpdo-pst-pg-target', fmt( state.target || 0 ) );
setText( '#wpdo-pst-pg-rate', state.rate_per_sec || 0 );
setText( '#wpdo-pst-pg-elapsed', state.elapsed_sec || 0 );
setText( '#wpdo-pst-pg-eta', state.eta_sec || 0 );
setText( '#wpdo-pst-pg-batches', state.batches_done || 0 );
setText( '#wpdo-pst-pg-mem', ( ( state.peak_memory || 0 ) / 1048576 ).toFixed( 1 ) );
const bar = $( '#wpdo-pst-pg-bar' );
if ( bar ) {
bar.style.width = ( state.pct || 0 ) + '%';
}
const count = state.test_post_count || 0;
setText( '#wpdo-pst-count', fmt( count ) );
setText( '#wpdo-pst-count-mirror', fmt( count ) );
const startBtn = $( '#wpdo-pst-start' );
const cancelBtn = $( '#wpdo-pst-cancel' );
const cleanupBtn = $( '#wpdo-pst-cleanup' );
const benchBtn = $( '#wpdo-pst-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-pst-bench-card' );
const content = $( '#wpdo-pst-bench-content' );
if ( ! card || ! content ) {
return;
}
card.style.display = '';
const w = bench.write || {};
const dbSizes = bench.db_sizes || [];
const q = bench.query || {};
// Friendly labels for the per-post_type query probes
const labels = {
point_stock_status: 'Point lookup (_stock_status)',
point_status: 'Point lookup (hp_status)',
point_verified: 'Point lookup (hp_verified)',
point_alt_present: 'Point lookup (alt 文字)',
point_type: 'Point lookup (_menu_item_type)',
point_thumbnail: 'Point lookup (_thumbnail_id)',
range_price_above: 'Range scan (price > 100)',
range_budget_above: 'Range scan (budget > 100)',
range_rate_above: 'Range scan (hourly_rate > 50)',
range_id_above: 'Range scan (post_id 排序)',
eav_baseline: 'EAV baseline (wp_postmeta 直查)',
};
let html = '';
// Write metrics
html += '<h4 style="margin-bottom:6px;">▍ 寫入指標</h4>';
html += '<table class="widefat" style="margin-bottom:14px;"><tbody>';
html += `<tr><td>模式</td><td><code>${ esc( w.mode ) }</code></td></tr>`;
html += `<tr><td>Post Type</td><td><code>${ esc( w.post_type ) }</code></td></tr>`;
html += `<tr><td>完成 / 目標</td><td>${ fmt( w.processed ) } / ${ fmt( w.target ) }</td></tr>`;
html += `<tr><td>總耗時</td><td>${ fmt( w.elapsed_sec ) } 秒</td></tr>`;
html += `<tr><td>平均速率</td><td><strong>${ fmt( w.rate_per_sec ) }</strong> posts/sec</td></tr>`;
html += `<tr><td>批次數</td><td>${ fmt( w.batches_done ) }</td></tr>`;
html += `<tr><td>批次最快/平均/最慢</td><td>${ fmt( w.batch_min_ms ) } / ${ fmt( w.batch_avg_ms ) } / ${ fmt( w.batch_max_ms ) } ms</td></tr>`;
html += `<tr><td>PHP Peak Memory</td><td>${ fmt( w.peak_memory_mb ) } MB</td></tr>`;
html += '</tbody></table>';
// DB sizes (wp_posts + wp_postmeta + flat table for this run)
html += '<h4 style="margin-bottom:6px;">▍ DB 容量(post 相關表)</h4>';
html += '<table class="widefat striped" style="margin-bottom:14px;"><thead><tr>';
html += '<th>Table</th><th>Rows</th><th>Data MB</th><th>Index MB</th><th>Total MB</th><th>Avg bytes/row</th>';
html += '</tr></thead><tbody>';
dbSizes.forEach( ( r ) => {
html += `<tr><td><code>${ esc( r.table ) }</code></td><td>${ fmt( r.rows ) }</td>`;
html += `<td>${ r.data_mb ?? '—' }</td><td>${ r.index_mb ?? '—' }</td>`;
html += `<td><strong>${ r.total_mb ?? '—' }</strong></td><td>${ fmt( r.avg_bytes ) }</td></tr>`;
} );
html += '</tbody></table>';
// Query perf — 3 probes per post_type, with EAV baseline last for visual comparison
html += '<h4 style="margin-bottom:6px;">▍ 查詢效能</h4>';
html += '<table class="widefat striped" style="margin-bottom:8px;"><thead><tr>';
html += '<th>測試項目</th><th>耗時 (ms)</th>';
html += '</tr></thead><tbody>';
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 = ` <span style="color:#28a745;font-weight:600;">(${ ratio.toFixed( 2 ) }× faster)</span>`;
}
}
html += `<tr><td>${ esc( label ) }${ speedup }</td><td><strong>${ v.duration_ms }</strong></td></tr>`;
} );
html += '</tbody></table>';
html += '<p class="description">EAV baseline 走 wp_postmetaflat probes 走專屬 group 表。倍率即此規模下反 EAV 的查詢加速。</p>';
content.innerHTML = html;
}
// ── Polling ─────────────────────────────────────────────────────────────
async function poll() {
const r = await apiCall( '/post-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 postType = $( '#wpdo-pst-post-type' ).value;
const target = parseInt( $( '#wpdo-pst-target' ).value, 10 );
const batch = parseInt( $( '#wpdo-pst-batch' ).value, 10 );
const mode = document.querySelector( 'input[name="wpdo-pst-mode"]:checked' ).value;
if ( ! postType ) {
alert( '請選擇 post_type' );
return;
}
if ( ! target || target < 1 ) {
alert( '請輸入有效的 post 數量' );
return;
}
// Realistic 模式每 post 100-300msbatch 太大會撞 nginx 60s timeout 之前的 8s deadline
if ( mode === 'realistic' && batch > 30 ) {
if ( ! confirm( `Realistic 模式每個 post 約需 100-300 msbatch_size=${ batch } 可能超過後端 8s deadline 上限。建議使用 batch=10-20。要繼續嗎?` ) ) {
return;
}
}
// Soft warn for combinations that won't demonstrate反 EAV 優化效果
const postMode = String( cfg.postMode || 'disabled' );
if ( mode === 'fast' && postMode === 'aeav_only' ) {
if ( ! confirm( `⚠️ Fast 模式直接 $wpdb->insert 繞過 Hook Bus,即使 post mode=aeav_only 也會寫滿 wp_postmetafixture 用途,非優化驗證)。\n\n如果你想驗證反 EAV 優化效果(wp_postmeta 應為 0),請改用 🐢 Realistic 模式。\n\n仍以 Fast 模式繼續嗎?` ) ) {
return;
}
} else if ( mode === 'realistic' && postMode !== 'aeav_only' ) {
if ( ! confirm( `⚠️ 目前 post mode = ${ postMode },此模式下 Realistic 寫入仍會雙寫 wp_postmeta(不會展示優化效果,ratio 不變)。\n\n要看 wp_postmeta 完全短路(0 寫入)需要先把 mode 升到 aeav_only(設定 tab)。\n\n仍以 ${ postMode } 模式繼續測試嗎?` ) ) {
return;
}
}
const big = target >= 5000;
const msg = `即將以 ${ mode } 模式建立 ${ target.toLocaleString() }${ postType } 測試 post${ big ? '(規模較大,可能耗時數分鐘)' : '' }\n\n所有 post 的 post_title 會以 WPDO_STRESS_TEST_ 開頭,可一鍵清除。\n\n確定要開始嗎?`;
if ( ! confirm( msg ) ) {
return;
}
const r = await apiCall( '/post-stress-test/start', 'POST', {
post_type: postType,
target,
mode,
batch_size: batch,
} );
if ( ! r.ok ) {
alert( '啟動失敗:' + ( r.data?.error || r.status ) );
return;
}
// 啟動後立即顯示進度卡片
const card = $( '#wpdo-pst-progress-card' );
if ( card ) {
card.style.display = '';
}
// Hide the previous benchmark card (a fresh run will replace it)
const benchCard = $( '#wpdo-pst-bench-card' );
if ( benchCard ) {
benchCard.style.display = 'none';
}
setText( '#wpdo-pst-pg-status', 'running' );
setText( '#wpdo-pst-pg-post-type', postType );
setText( '#wpdo-pst-pg-mode', mode );
setText( '#wpdo-pst-pg-target', target.toLocaleString() );
startPolling();
}
async function handleCancel() {
if ( ! confirm( '確定要取消當前測試?已建立的 post 不會被刪除。' ) ) {
return;
}
const cancelBtn = $( '#wpdo-pst-cancel' );
if ( cancelBtn ) {
cancelBtn.disabled = true;
cancelBtn.textContent = '⏹ 取消中…';
}
setText( '#wpdo-pst-pg-status', 'cancelling' );
const r = await apiCall( '/post-stress-test/cancel', 'POST' );
if ( ! r.ok ) {
alert( '取消失敗:' + ( r.data?.error || r.status ) );
if ( cancelBtn ) {
cancelBtn.disabled = false;
cancelBtn.textContent = '⏹ 取消';
}
return;
}
// realistic 模式 in-flight batch 可能還要 ~1 秒才會真正中止
poll();
if ( cancelBtn ) {
cancelBtn.textContent = '⏹ 取消';
}
}
async function handleCleanup() {
if ( ! confirm( '確定要清除所有 stress test posts\n\n此動作會:\n- DELETE 所有 post_title 前綴 WPDO_STRESS_TEST_ 的 post\n- DELETE 對應 wp_postmeta\n- DELETE 7 張 wp_wpdo_post_* flat tables 中對應 post_id 的列\n\n不可復原!' ) ) {
return;
}
const r = await apiCall( '/post-stress-test/cleanup', 'DELETE' );
if ( ! r.ok ) {
alert( '清除失敗:' + ( r.data?.error || r.status ) );
return;
}
alert( `已清除 ${ r.data.deleted } 筆測試 post。` );
const card = $( '#wpdo-pst-progress-card' );
const benchCard = $( '#wpdo-pst-bench-card' );
if ( card ) card.style.display = 'none';
if ( benchCard ) benchCard.style.display = 'none';
poll();
}
async function handleRerunBench() {
const btn = $( '#wpdo-pst-rerun-bench' );
if ( btn ) {
btn.disabled = true;
btn.textContent = '⏳ 執行中...';
}
const r = await apiCall( '/post-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-pst-start' );
const cancelBtn = $( '#wpdo-pst-cancel' );
const cleanupBtn = $( '#wpdo-pst-cleanup' );
const benchBtn = $( '#wpdo-pst-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:若 status==running 自動接手 polling;若 completed 渲染 benchmark
poll().then( () => {
const status = ( $( '#wpdo-pst-pg-status' )?.textContent || '' ).trim();
if ( status === 'running' || status === 'benchmarking' ) {
startPolling();
}
} );
} );
} )();