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
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* 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 ) => ( {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
}[ 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_postmeta,flat 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-300ms,batch 太大會撞 nginx 60s timeout 之前的 8s deadline
|
||||
if ( mode === 'realistic' && batch > 30 ) {
|
||||
if ( ! confirm( `Realistic 模式每個 post 約需 100-300 ms,batch_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_postmeta(fixture 用途,非優化驗證)。\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();
|
||||
}
|
||||
} );
|
||||
} );
|
||||
} )();
|
||||
Reference in New Issue
Block a user