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

369 lines
13 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 Term Stress Test — admin tab JS (v2.13.0)
*
* Mirrors wpdo-post-stress-test.js (v2.11.4) but:
* - Talks to /wpdo/v1/term-stress-test/* endpoints
* - Sends taxonomy in start payload (not post_type)
* - DOM IDs prefixed wpdo-tst-* (term stress test)
*
* @since 2.13.0
*/
( function () {
'use strict';
const cfg = window.wpdoTermStressTest;
if ( ! cfg || ! cfg.restUrl ) {
return;
}
if ( ! document.querySelector( '.wpdo-term-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-tst-progress-card' );
if ( card ) {
card.style.display = ( isRunning || state.status === 'completed' || state.status === 'failed' || state.status === 'cancelled' ) ? '' : 'none';
}
setText( '#wpdo-tst-pg-status', state.status || 'idle' );
setText( '#wpdo-tst-pg-taxonomy', state.taxonomy || '' );
setText( '#wpdo-tst-pg-mode', state.mode || '' );
setText( '#wpdo-tst-pg-pct', ( state.pct || 0 ) + '%' );
setText( '#wpdo-tst-pg-processed', fmt( state.processed || 0 ) );
setText( '#wpdo-tst-pg-target', fmt( state.target || 0 ) );
setText( '#wpdo-tst-pg-rate', state.rate_per_sec || 0 );
setText( '#wpdo-tst-pg-elapsed', state.elapsed_sec || 0 );
setText( '#wpdo-tst-pg-eta', state.eta_sec || 0 );
setText( '#wpdo-tst-pg-batches', state.batches_done || 0 );
setText( '#wpdo-tst-pg-mem', ( ( state.peak_memory || 0 ) / 1048576 ).toFixed( 1 ) );
const bar = $( '#wpdo-tst-pg-bar' );
if ( bar ) {
bar.style.width = ( state.pct || 0 ) + '%';
}
const count = state.test_term_count || 0;
setText( '#wpdo-tst-count', fmt( count ) );
setText( '#wpdo-tst-count-mirror', fmt( count ) );
const startBtn = $( '#wpdo-tst-start' );
const cancelBtn = $( '#wpdo-tst-cancel' );
const cleanupBtn = $( '#wpdo-tst-cleanup' );
const benchBtn = $( '#wpdo-tst-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-tst-bench-card' );
const content = $( '#wpdo-tst-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_default: 'Point lookup (hp_default = 1)',
range_sort_top: 'Range scan (hp_sort_order > 0 ORDER BY DESC)',
eav_baseline: 'EAV baseline (wp_termmeta 直查 hp_default)',
};
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>Taxonomy</td><td><code>${ esc( w.taxonomy ) }</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> terms/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 容量(term 相關表)</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 with EAV baseline last for visual speedup 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_termmetaflat probes 走 wpdo_term_hp_taxonomy。倍率即此規模下反 EAV 的查詢加速。</p>';
content.innerHTML = html;
}
// ── Polling ─────────────────────────────────────────────────────────────
async function poll() {
const r = await apiCall( '/term-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 taxonomy = $( '#wpdo-tst-taxonomy' ).value;
const target = parseInt( $( '#wpdo-tst-target' ).value, 10 );
const batch = parseInt( $( '#wpdo-tst-batch' ).value, 10 );
const mode = document.querySelector( 'input[name="wpdo-tst-mode"]:checked' ).value;
if ( ! taxonomy ) {
alert( '請選擇 taxonomy' );
return;
}
if ( ! target || target < 1 ) {
alert( '請輸入有效的 term 數量' );
return;
}
if ( mode === 'realistic' && batch > 50 ) {
if ( ! confirm( `Realistic 模式每個 term 約需 50-150 mswp_insert_term + 3 個 update_term_meta),batch_size=${ batch } 可能超過後端 8s deadline。建議 batch=10-30。要繼續嗎?` ) ) {
return;
}
}
// Soft warn for combinations that won't demonstrate反 EAV 優化效果
const termMode = String( cfg.termMode || 'disabled' );
if ( mode === 'fast' && termMode === 'aeav_only' ) {
if ( ! confirm( `⚠️ Fast 模式直接 $wpdb->insert 繞過 Hook Bus,即使 term mode=aeav_only 也會寫滿 wp_termmeta。\n\n要驗證反 EAV 優化效果(wp_termmeta 應為 0),請改用 🐢 Realistic 模式。\n\n仍以 Fast 模式繼續嗎?` ) ) {
return;
}
} else if ( mode === 'realistic' && termMode !== 'aeav_only' ) {
if ( ! confirm( `⚠️ 目前 term mode = ${ termMode }Realistic 寫入仍會雙寫 wp_termmeta(不展示優化效果)。\n\n要看 wp_termmeta 完全短路請先升 mode 至 aeav_only(設定 tab)。\n\n仍以 ${ termMode } 模式繼續嗎?` ) ) {
return;
}
}
const big = target >= 5000;
const msg = `即將以 ${ mode } 模式建立 ${ target.toLocaleString() }${ taxonomy } 測試 term${ big ? '(規模較大)' : '' }\n\n所有 term 的 slug 會以 wpdo-stress- 開頭,可一鍵清除。\n\n確定要開始嗎?`;
if ( ! confirm( msg ) ) {
return;
}
const r = await apiCall( '/term-stress-test/start', 'POST', {
taxonomy,
target,
mode,
batch_size: batch,
} );
if ( ! r.ok ) {
alert( '啟動失敗:' + ( r.data?.error || r.status ) );
return;
}
const card = $( '#wpdo-tst-progress-card' );
if ( card ) {
card.style.display = '';
}
const benchCard = $( '#wpdo-tst-bench-card' );
if ( benchCard ) {
benchCard.style.display = 'none';
}
setText( '#wpdo-tst-pg-status', 'running' );
setText( '#wpdo-tst-pg-taxonomy', taxonomy );
setText( '#wpdo-tst-pg-mode', mode );
setText( '#wpdo-tst-pg-target', target.toLocaleString() );
startPolling();
}
async function handleCancel() {
if ( ! confirm( '確定要取消當前測試?已建立的 term 不會被刪除。' ) ) {
return;
}
const cancelBtn = $( '#wpdo-tst-cancel' );
if ( cancelBtn ) {
cancelBtn.disabled = true;
cancelBtn.textContent = '⏹ 取消中…';
}
setText( '#wpdo-tst-pg-status', 'cancelling' );
const r = await apiCall( '/term-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 terms\n\n此動作會:\n- DELETE 所有 slug 前綴 wpdo-stress- 的 term\n- DELETE 對應 wp_termmeta + wp_term_taxonomy\n- DELETE flat 表中對應 term_id 的列\n\n不可復原!' ) ) {
return;
}
const r = await apiCall( '/term-stress-test/cleanup', 'DELETE' );
if ( ! r.ok ) {
alert( '清除失敗:' + ( r.data?.error || r.status ) );
return;
}
alert( `已清除 ${ r.data.deleted } 筆測試 term。` );
const card = $( '#wpdo-tst-progress-card' );
const benchCard = $( '#wpdo-tst-bench-card' );
if ( card ) card.style.display = 'none';
if ( benchCard ) benchCard.style.display = 'none';
poll();
}
async function handleRerunBench() {
const btn = $( '#wpdo-tst-rerun-bench' );
if ( btn ) {
btn.disabled = true;
btn.textContent = '⏳ 執行中...';
}
const r = await apiCall( '/term-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-tst-start' );
const cancelBtn = $( '#wpdo-tst-cancel' );
const cleanupBtn = $( '#wpdo-tst-cleanup' );
const benchBtn = $( '#wpdo-tst-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-tst-pg-status' )?.textContent || '' ).trim();
if ( status === 'running' || status === 'benchmarking' ) {
startPolling();
}
} );
} );
} )();