├── .DS_Store ├── .gitignore ├── scripts ├── src │ ├── store │ │ ├── resolvers.js │ │ ├── controls.js │ │ ├── index.js │ │ ├── selectors.js │ │ ├── actions.js │ │ └── reducer.js │ ├── index.js │ ├── TestResults.js │ ├── App.js │ ├── ModuleOption.js │ └── ModuleCard.js └── package.json ├── styles └── settings.css ├── composer.json ├── src ├── Checkpoint.php ├── Runner.php ├── Utils.php ├── modules │ ├── Get_Posts.php │ ├── Abstract_Module.php │ └── Actions.php ├── Settings.php ├── Results.php └── Main.php ├── composer.lock ├── wp-benchy.php ├── README.md └── LICENSE /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pressable/wp-benchy/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | /vendor/ 4 | /scripts/build/ 5 | /scripts/node_modules/ 6 | -------------------------------------------------------------------------------- /scripts/src/store/resolvers.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | 3 | export function *getModules() { 4 | const modules = yield actions.fetchModules(); 5 | return actions.setModules(modules); 6 | } -------------------------------------------------------------------------------- /scripts/src/index.js: -------------------------------------------------------------------------------- 1 | const {render} = wp.element; 2 | import App from './App'; 3 | 4 | if (document.getElementById('wp-benchy-settings')) { 5 | render(, document.getElementById('wp-benchy-settings')); 6 | } -------------------------------------------------------------------------------- /styles/settings.css: -------------------------------------------------------------------------------- 1 | h3.components-heading { 2 | margin-bottom: 0.5em; 3 | } 4 | 5 | .benchy-module-options { 6 | padding-bottom: 1em; 7 | padding-top: 1em; 8 | } 9 | 10 | .run-test-button { 11 | margin-left: auto; 12 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pressable/wp-benchy", 3 | "type": "wordpress-plugin", 4 | "autoload": { 5 | "psr-4": { 6 | "Pressable\\WP_Benchy\\": "src/" 7 | } 8 | }, 9 | "require": {} 10 | } 11 | -------------------------------------------------------------------------------- /scripts/src/store/controls.js: -------------------------------------------------------------------------------- 1 | import apiFetch from "@wordpress/api-fetch"; 2 | 3 | export function FETCH_MODULES(action) { 4 | return apiFetch({ 5 | path: '/wp-benchy/v1/modules' 6 | }); 7 | } 8 | 9 | export function RUN_MODULE(action) { 10 | return apiFetch({ 11 | path: `/wp-benchy/v1/module/${action.moduleId}` 12 | }); 13 | } -------------------------------------------------------------------------------- /scripts/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createReduxStore, register } from '@wordpress/data'; 2 | 3 | import * as actions from './actions'; 4 | import * as selectors from './selectors'; 5 | import reducer from './reducer'; 6 | import * as resolvers from './resolvers'; 7 | import * as controls from './controls'; 8 | 9 | const store = createReduxStore('benchy', { 10 | reducer, 11 | actions, 12 | selectors, 13 | resolvers, 14 | controls 15 | }); 16 | 17 | register(store); 18 | 19 | export { store }; -------------------------------------------------------------------------------- /src/Checkpoint.php: -------------------------------------------------------------------------------- 1 | id = $id; 13 | } 14 | 15 | public function get_id() { 16 | return $this->id; 17 | } 18 | 19 | public function hit() { 20 | $this->hits[] = hrtime(true); 21 | } 22 | 23 | public function get_hits() { 24 | return $this->hits; 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-benchy-settings", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "wp-scripts build", 8 | "start": "wp-scripts start" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@wordpress/scripts": "^26.12.0" 15 | }, 16 | "dependencies": { 17 | "@wordpress/api-fetch": "^6.39.0", 18 | "@wordpress/components": "^25.7.0", 19 | "@wordpress/data": "^9.12.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Runner.php: -------------------------------------------------------------------------------- 1 | module = $module; 13 | } 14 | 15 | public function run() { 16 | $this->module->set_runner($this); 17 | $this->module->register_checkpoints(); 18 | $this->module->run(); 19 | } 20 | 21 | public function get_results() { 22 | return $this->module->get_results(); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/Utils.php: -------------------------------------------------------------------------------- 1 | array( 15 | 'type' => 'int', 16 | 'min' => 1, 17 | 'max' => 25, 18 | 'value' => 1 19 | ) 20 | ); 21 | 22 | protected $checkpoint_ids = array( 23 | 'get_posts_start', 24 | 'get_posts_end', 25 | ); 26 | 27 | public function run() { 28 | $this->checkpoint_hit('get_posts_start'); 29 | 30 | $posts = get_posts(); 31 | 32 | $this->checkpoint_hit('get_posts_end'); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /scripts/src/TestResults.js: -------------------------------------------------------------------------------- 1 | import { __experimentalHeading as Heading } from '@wordpress/components'; 2 | 3 | const TestResults = ({results}) => { 4 | 5 | if ( ! results.length > 0 ) { 6 | return null; 7 | } 8 | 9 | return ( 10 |
11 | Results 12 | 21 | {results.length > 0 && 22 |

Average: {results.reduce((a, b) => a + b.total, 0) / results.length}ms

23 | } 24 | 25 |
26 | ); 27 | } 28 | 29 | export default TestResults; -------------------------------------------------------------------------------- /scripts/src/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | __experimentalGrid as Grid, 3 | __experimentalHeading as Heading, 4 | } from '@wordpress/components'; 5 | 6 | import { useSelect } from '@wordpress/data'; 7 | import { store as benchyStore } from './store'; 8 | import ModuleCard from './ModuleCard.js'; 9 | 10 | const App = () => { 11 | 12 | const { modules } = useSelect(select => ({ 13 | modules: select( benchyStore ).getModules(), 14 | })); 15 | 16 | return ( 17 |
18 | Pressable Benchy Test Suite 19 | 20 | 21 | 22 | {Object.keys(modules).map((moduleId, i) => { 23 | return ( 24 | 25 | ); 26 | })} 27 | 28 | 29 |
30 | 31 | ); 32 | 33 | }; 34 | 35 | export default App; -------------------------------------------------------------------------------- /wp-benchy.php: -------------------------------------------------------------------------------- 1 | init(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WP Benchy 2 | 3 | A plugin to assist with running benchmark tests. For more information about the approaches used here, see our article about [how to benchmark WordPress sites](https://pressable.com/benchmarking-wordpress). 4 | 5 | *(Yes, sending you to this post isn't the best experience, but it's good enough for now. PRs open if you want to make this readme better.)* 6 | 7 | ## Installing 8 | 9 | **Don't try to just install this as-is from the repo - you'll be missing dependencies.** 10 | 11 | For an installable plugin zip, download the latest release. 12 | 13 | ## Building 14 | 15 | You're going to need Composer and NPM. 16 | 17 | 1. `cd wp-benchy` 18 | 2. `composer install` 19 | 3. `cd scripts` 20 | 4. `npm start` from dev. `npm run build` for prod. 21 | 22 | ## Contributing 23 | 24 | Contributions are wide open. Go wild. 25 | 26 | A few things this could use: 27 | 28 | - Write better docs 29 | - A UI that doesn't suck. 30 | - More tests 31 | -------------------------------------------------------------------------------- /scripts/src/ModuleOption.js: -------------------------------------------------------------------------------- 1 | import { useSelect, useDispatch } from '@wordpress/data'; 2 | import { store as benchyStore } from './store'; 3 | import { RangeControl } from '@wordpress/components'; 4 | 5 | const ModuleOption = ({ moduleId, optionId }) => { 6 | 7 | const { moduleConfig } = useSelect(select => ({ 8 | moduleConfig: select( benchyStore ).getModuleConfig(moduleId, optionId), 9 | })); 10 | 11 | const { setModuleConfig } = useDispatch( benchyStore ); 12 | 13 | const onChange = (value) => { 14 | setModuleConfig(moduleId, optionId, value); 15 | }; 16 | 17 | return ( 18 |
19 | {optionId === 'iterations' && ( 20 | 28 | )} 29 | 30 |
31 | ); 32 | }; 33 | 34 | export default ModuleOption; -------------------------------------------------------------------------------- /scripts/src/store/selectors.js: -------------------------------------------------------------------------------- 1 | export function getModules(state) { 2 | return state.modules; 3 | } 4 | 5 | export function getModule(state, moduleId) { 6 | return state.modules[moduleId]; 7 | } 8 | 9 | export function getModuleConfig(state, moduleId, optionId) { 10 | return state.modules[moduleId].options[optionId]; 11 | } 12 | 13 | export function getModuleConfigValue(state, moduleId, optionId) { 14 | return state.modules[moduleId].options[optionId].value; 15 | } 16 | 17 | export function getTestInProgress(state) { 18 | return state.testInProgress; 19 | } 20 | 21 | export function getTestResults(state) { 22 | return state.testResults; 23 | } 24 | 25 | export function getModuleTestResults(state, moduleId) { 26 | if ( Array.isArray( state.testResults[moduleId] ) ) { 27 | return state.testResults[moduleId]; 28 | } 29 | 30 | return []; 31 | } 32 | 33 | export function getAverageResult(state, moduleId) { 34 | console.log(state.testResults); 35 | if (state.testResults[moduleId] && state.testResults[moduleId].length > 0) { 36 | let total = 0; 37 | state.testResults[moduleId].forEach((result) => { 38 | total += result.total; 39 | }); 40 | return total / state.testResults[moduleId].length; 41 | } 42 | return false; 43 | } -------------------------------------------------------------------------------- /scripts/src/store/actions.js: -------------------------------------------------------------------------------- 1 | export function setModules(modules) { 2 | return { 3 | type: 'SET_MODULES', 4 | modules, 5 | }; 6 | } 7 | 8 | export function setModuleConfig(moduleId, optionId, optionValue) { 9 | return { 10 | type: 'SET_MODULE_CONFIG', 11 | moduleId, 12 | optionId, 13 | optionValue, 14 | }; 15 | } 16 | 17 | export function setTestInProgress(testInProgress) { 18 | return { 19 | type: 'SET_TEST_IN_PROGRESS', 20 | testInProgress, 21 | }; 22 | } 23 | 24 | export function setTestResults(testResults) { 25 | return { 26 | type: 'SET_TEST_RESULTS', 27 | testResults, 28 | }; 29 | } 30 | 31 | export function addTestResult(moduleId, testResult) { 32 | return { 33 | type: 'ADD_TEST_RESULT', 34 | moduleId, 35 | testResult, 36 | }; 37 | } 38 | 39 | export function setModuleTestResults(moduleId, testResults) { 40 | return { 41 | type: 'SET_MODULE_TEST_RESULTS', 42 | moduleId, 43 | testResults, 44 | }; 45 | } 46 | 47 | export function fetchModules() { 48 | return { 49 | type: 'FETCH_MODULES', 50 | }; 51 | } 52 | 53 | export function runModule(moduleId) { 54 | return { 55 | type: 'RUN_MODULE', 56 | moduleId, 57 | }; 58 | } -------------------------------------------------------------------------------- /src/Settings.php: -------------------------------------------------------------------------------- 1 | id; 20 | 21 | if ( $current_screen === 'tools_page_wp-benchy' ) { 22 | $asset_props = require_once( WP_BENCHY_DIR_PATH . '/scripts/build/index.asset.php' ); 23 | 24 | wp_enqueue_script( 25 | 'wp-benchy-settings', 26 | WP_BENCHY_DIR_URL . 'scripts/build/index.js', 27 | $asset_props['dependencies'], 28 | $asset_props['version'], 29 | true 30 | ); 31 | 32 | wp_enqueue_style('wp-benchy-settings', WP_BENCHY_DIR_URL . 'styles/settings.css', array('wp-components'), '1.0.0', 'all'); 33 | } 34 | } 35 | 36 | public static function render_admin_settings() { 37 | if ( ! current_user_can( 'manage_options' ) ) { 38 | wp_die( esc_html__( 'You do not have sufficient capabilities to access this page.', 'wp-benchy' ) ); 39 | } 40 | 41 | ?> 42 | 43 |
44 |
45 |
46 | 47 | checkpoints = $checkpoints; 13 | } 14 | 15 | public function get_checkpoints() { 16 | return $this->checkpoints; 17 | } 18 | 19 | public function flatten_checkpoint_hits() { 20 | $flattened = array(); 21 | 22 | foreach ( $this->checkpoints as $checkpoint_id => $checkpoint ) { 23 | foreach ( $checkpoint->get_hits() as $hit_key => $hit ) { 24 | $flattened[ $checkpoint_id . '_' . $hit_key ] = $hit; 25 | } 26 | } 27 | 28 | return $flattened; 29 | } 30 | 31 | public function get_checkpoint_hits() { 32 | if ( empty( $this->checkpoint_hits ) ) { 33 | $hits_flattened = $this->flatten_checkpoint_hits(); 34 | 35 | asort( $hits_flattened ); 36 | 37 | $this->checkpoint_hits = $hits_flattened; 38 | } 39 | 40 | return $this->checkpoint_hits; 41 | } 42 | 43 | public function get_first_hit_time() { 44 | $hits = $this->get_checkpoint_hits(); 45 | 46 | return $hits[ array_key_first( $hits ) ]; 47 | } 48 | 49 | public function get_last_hit_time() { 50 | $hits = $this->get_checkpoint_hits(); 51 | 52 | return $hits[ array_key_last( $hits ) ]; 53 | } 54 | 55 | public function get_total_time() { 56 | $first = $this->get_first_hit_time(); 57 | $last = $this->get_last_hit_time(); 58 | 59 | return $last - $first; 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /scripts/src/store/reducer.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_STATE = { 2 | modules: {}, 3 | testInProgress: false, 4 | testResults: {}, 5 | }; 6 | 7 | export default function reducer(state = DEFAULT_STATE, action) { 8 | switch (action.type) { 9 | case 'SET_MODULES': 10 | return { 11 | ...state, 12 | modules: action.modules, 13 | }; 14 | case 'SET_MODULE_CONFIG': 15 | state.modules[action.moduleId].options[action.optionId].value = action.optionValue; 16 | return { 17 | ...state, 18 | modules: state.modules, 19 | }; 20 | case 'SET_TEST_IN_PROGRESS': 21 | return { 22 | ...state, 23 | testInProgress: action.testInProgress, 24 | }; 25 | case 'SET_TEST_RESULTS': 26 | return { 27 | ...state, 28 | testResults: action.testResults, 29 | }; 30 | case 'ADD_TEST_RESULT': 31 | const newState = state; 32 | 33 | if ( newState.testResults[action.moduleId] === undefined ) { 34 | newState.testResults[action.moduleId] = []; 35 | } 36 | 37 | newState.testResults[action.moduleId].push(action.testResult); 38 | return { 39 | ...state, 40 | testResults: newState.testResults, 41 | }; 42 | case 'SET_MODULE_TEST_RESULTS': 43 | return { 44 | ...state, 45 | testResults: { 46 | ...state.testResults, 47 | [action.moduleId]: action.testResults, 48 | }, 49 | }; 50 | default: 51 | return state; 52 | } 53 | } -------------------------------------------------------------------------------- /src/modules/Abstract_Module.php: -------------------------------------------------------------------------------- 1 | id; 27 | } 28 | 29 | public function get_title() { 30 | return $this->title; 31 | } 32 | 33 | public function get_description() { 34 | return $this->description; 35 | } 36 | 37 | public function get_options() { 38 | return $this->options; 39 | } 40 | 41 | public function set_runner( $runner ) { 42 | $this->runner = $runner; 43 | } 44 | 45 | public function get_checkpoints() { 46 | return $this->checkpoints; 47 | } 48 | 49 | public function register_checkpoint( $id ) { 50 | if ( ! array_key_exists($id, $this->checkpoints) ) { 51 | $this->checkpoints[ $id ] = new Checkpoint(); 52 | } 53 | } 54 | 55 | public function register_checkpoints() { 56 | foreach ( $this->checkpoint_ids as $id ) { 57 | $this->register_checkpoint( $id ); 58 | } 59 | } 60 | 61 | public function checkpoint_hit( $id ) { 62 | if ( array_key_exists( $id, $this->checkpoints) ) { 63 | $this->checkpoints[ $id ]->hit(); 64 | } else { 65 | $this->register_checkpoint( $id ); 66 | $this->checkpoints[ $id ]->hit(); 67 | } 68 | } 69 | 70 | abstract public function run(); 71 | 72 | public function get_results() { 73 | if ( empty( $this->results ) ) { 74 | $this->results = new Results( $this->checkpoints ); 75 | } 76 | 77 | return $this->results; 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /scripts/src/ModuleCard.js: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardHeader, 4 | CardBody, 5 | CardFooter, 6 | __experimentalSpacer as Spacer, 7 | __experimentalHeading as Heading, 8 | Button, 9 | CardDivider, 10 | } from '@wordpress/components'; 11 | 12 | import apiFetch from '@wordpress/api-fetch'; 13 | import { useSelect } from '@wordpress/data'; 14 | import { store as benchyStore } from './store'; 15 | import ModuleOption from './ModuleOption.js'; 16 | import React, { useState } from 'react'; 17 | import TestResults from './TestResults'; 18 | 19 | const ModuleCard = ({ moduleId }) => { 20 | 21 | const { module } = useSelect(select => ({ 22 | module: select( benchyStore ).getModule(moduleId), 23 | })); 24 | 25 | const [results, setResults] = useState([]); 26 | 27 | const runTest = (moduleId) => { 28 | const moduleOptions = module.options; 29 | 30 | let res = []; 31 | (async function () { 32 | for ( let i = 0; i < moduleOptions.iterations.value; i++ ) { 33 | res.push( await sendRequest(moduleId)); 34 | } 35 | 36 | setResults(res); 37 | })(); 38 | } 39 | 40 | const sendRequest = async (moduleId) => { 41 | const response = await apiFetch({ path: `/wp-benchy/v1/module/${moduleId}` }); 42 | return response; 43 | } 44 | 45 | return ( 46 | 47 | 48 | {module.title} 49 | 50 | 51 | 52 |

53 | {module.description} 54 |

55 | 56 | 57 | 58 |
59 | Options 60 | 61 | {Object.keys(module.options).map((optionId, i) => { 62 | return ( 63 | 64 | ); 65 | })} 66 |
67 | 68 | 69 | 70 |
71 | 72 | 73 |