Files
2meet-data-optimizer/admin/assets/wpdo-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

340 lines
12 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 User Stress Test — admin tab JS
*
* - 2s polling while running/benchmarking
* - Start / Cancel / Cleanup / Re-run benchmark
* - Renders progress + benchmark report
*
* @since 2.6.7
*/
( function () {
'use strict';
const cfg = window.wpdoStressTest;
if ( ! cfg || ! cfg.restUrl ) {
return;
}
if ( ! document.querySelector( '.wpdo-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 renderProgress( state ) {
const isRunning = state.status === 'running' || state.status === 'benchmarking';
const card = $( '#wpdo-st-progress-card' );
if ( card ) {
card.style.display = ( isRunning || state.status === 'completed' || state.status === 'failed' || state.status === 'cancelled' ) ? '' : 'none';
}
setText( '#wpdo-st-pg-status', state.status || 'idle' );
setText( '#wpdo-st-pg-mode', state.mode || '' );
setText( '#wpdo-st-pg-pct', ( state.pct || 0 ) + '%' );
setText( '#wpdo-st-pg-processed', fmt( state.processed || 0 ) );
setText( '#wpdo-st-pg-target', fmt( state.target || 0 ) );
setText( '#wpdo-st-pg-rate', state.rate_per_sec || 0 );
setText( '#wpdo-st-pg-elapsed', state.elapsed_sec || 0 );
setText( '#wpdo-st-pg-eta', state.eta_sec || 0 );
setText( '#wpdo-st-pg-batches', state.batches_done || 0 );
setText( '#wpdo-st-pg-mem', ( ( state.peak_memory || 0 ) / 1048576 ).toFixed( 1 ) );
const bar = $( '#wpdo-st-pg-bar' );
if ( bar ) {
bar.style.width = ( state.pct || 0 ) + '%';
}
setText( '#wpdo-st-count', fmt( state.test_user_count || 0 ) );
const startBtn = $( '#wpdo-st-start' );
const cancelBtn = $( '#wpdo-st-cancel' );
const cleanupBtn = $( '#wpdo-st-cleanup' );
const benchBtn = $( '#wpdo-st-rerun-bench' );
if ( startBtn ) {
startBtn.disabled = isRunning;
}
if ( cancelBtn ) {
cancelBtn.disabled = ! isRunning;
}
if ( cleanupBtn ) {
cleanupBtn.disabled = isRunning || ( state.test_user_count || 0 ) === 0;
}
if ( benchBtn ) {
benchBtn.disabled = isRunning || ( state.test_user_count || 0 ) === 0;
}
}
function renderBenchmark( bench ) {
if ( ! bench ) {
return;
}
const card = $( '#wpdo-st-bench-card' );
const content = $( '#wpdo-st-bench-content' );
if ( ! card || ! content ) {
return;
}
card.style.display = '';
const w = bench.write || {};
const dbSizes = bench.db_sizes || [];
const q = bench.query || {};
const labels = {
get_field_membership_level: 'WPDO_API::get_field (membership_level) ×100',
get_entity_full: 'WPDO_API::get_entity (整筆) ×100',
range_gold_high_points: '索引範圍:gold + points>5000',
sort_recent_active_100: '排序:last_active_at DESC LIMIT 100',
join_top_gold_active: 'JOINtop gold + active LIMIT 100',
eav_range_baseline: '原生 EAV 等價查詢(baseline',
};
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>完成 / 目標</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> users/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
html += '<h4 style="margin-bottom:6px;">▍ DB 容量(user 相關表)</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
html += '<h4 style="margin-bottom:6px;">▍ 查詢效能</h4>';
html += '<table class="widefat striped" style="margin-bottom:8px;"><thead><tr>';
html += '<th>測試項目</th><th>樣本</th><th>總時間 (ms)</th><th>平均 (ms)</th><th>QPS</th>';
html += '</tr></thead><tbody>';
Object.keys( labels ).forEach( ( key ) => {
const v = q[ key ];
if ( ! v ) {
return;
}
const total = v.total_ms ?? v.duration_ms ?? '—';
html += `<tr><td>${ esc( labels[ key ] ) }</td><td>${ v.n || 1 }</td>`;
html += `<td><strong>${ total }</strong></td><td>${ v.avg_ms ?? '—' }</td><td>${ v.qps ?? '—' }</td></tr>`;
} );
html += '</tbody></table>';
html += '<p class="description">原生 EAV baseline 與 flat table 範圍查詢的時間差,即代表此規模下反 EAV 帶來的查詢加速倍數。</p>';
content.innerHTML = html;
}
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 ] ) );
}
// ── Polling ─────────────────────────────────────────────────────────────
async function poll() {
const r = await apiCall( '/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 target = parseInt( $( '#wpdo-st-target' ).value, 10 );
let batch = parseInt( $( '#wpdo-st-batch' ).value, 10 );
const mode = document.querySelector( 'input[name="wpdo-st-mode"]:checked' ).value;
if ( ! target || target < 1 ) {
alert( '請輸入有效的使用者數量' );
return;
}
// Realistic 模式每 user ~3.5 秒,batch_size 太大會撞 nginx 60s timeout
// 後端有 25s wall-clock deadline 保護,但前端先給友善提示
if ( mode === 'realistic' && batch > 10 ) {
if ( ! confirm( `Realistic 模式每個 user 約需 3 秒,batch_size=${ batch } 會超過後端 25s deadline 上限。建議使用 batch=10。要繼續嗎?` ) ) {
return;
}
}
const big = target >= 10000;
const msg = `即將以 ${ mode } 模式建立 ${ target.toLocaleString() } 筆測試使用者${ big ? '(規模較大,可能耗時數分鐘)' : '' }\n\n密碼一律為 PassWord2026!user_login 為 test{n}。\n\n確定要開始嗎?`;
if ( ! confirm( msg ) ) {
return;
}
const r = await apiCall( '/stress-test/start', 'POST', { target, mode, batch_size: batch } );
if ( ! r.ok ) {
alert( '啟動失敗:' + ( r.data?.error || r.status ) );
return;
}
// 啟動後立即顯示進度卡片,不必等第一次 poll 才出現
const card = $( '#wpdo-st-progress-card' );
if ( card ) {
card.style.display = '';
}
setText( '#wpdo-st-pg-status', 'running' );
setText( '#wpdo-st-pg-mode', mode );
setText( '#wpdo-st-pg-target', target.toLocaleString() );
startPolling();
}
async function handleCancel() {
if ( ! confirm( '確定要取消當前測試?已建立的使用者不會被刪除。' ) ) {
return;
}
// 立刻 UI 反饋:避免使用者再點一次
const cancelBtn = $( '#wpdo-st-cancel' );
if ( cancelBtn ) {
cancelBtn.disabled = true;
cancelBtn.textContent = '⏹ 取消中…';
}
setText( '#wpdo-st-pg-status', 'cancelling' );
const r = await apiCall( '/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 可能還要 ~3 秒才會真正中止;polling 會看到 cancelled
poll();
if ( cancelBtn ) {
cancelBtn.textContent = '⏹ 取消';
}
}
async function handleCleanup() {
if ( ! confirm( '確定要清除所有 test_* 使用者?\n\n此動作會:\n- DELETE 所有 user_login LIKE "test%" 的使用者\n- DELETE 對應 wp_usermeta\n- DELETE 所有 wp_wpdo_user_* flat tables 中對應 user_id 的列\n\n不可復原!' ) ) {
return;
}
const r = await apiCall( '/stress-test/cleanup', 'DELETE' );
if ( ! r.ok ) {
alert( '清除失敗:' + ( r.data?.error || r.status ) );
return;
}
alert( `已清除 ${ r.data.deleted } 筆測試使用者。` );
const card = $( '#wpdo-st-progress-card' );
const benchCard = $( '#wpdo-st-bench-card' );
if ( card ) card.style.display = 'none';
if ( benchCard ) benchCard.style.display = 'none';
poll();
}
async function handleRerunBench() {
const btn = $( '#wpdo-st-rerun-bench' );
if ( btn ) {
btn.disabled = true;
btn.textContent = '⏳ 執行中...';
}
const r = await apiCall( '/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-st-start' );
const cancelBtn = $( '#wpdo-st-cancel' );
const cleanupBtn = $( '#wpdo-st-cleanup' );
const benchBtn = $( '#wpdo-st-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,如果正在執行就會自動開始 polling
poll().then( () => {
const card = $( '#wpdo-st-progress-card' );
if ( card && card.style.display !== 'none' ) {
const status = ( $( '#wpdo-st-pg-status' )?.textContent || '' ).trim();
if ( status === 'running' || status === 'benchmarking' ) {
startPolling();
}
}
} );
} );
} )();