| @@ -7,8 +7,36 @@ | ||
| 7 | 7 | */ |
| 8 | 8 | class WP_Optimizer { |
| 9 | 9 | |
| 10 | 10 | /** |
| 11 | + * Table count limit to consider as too many tables to load in a single request | |
| 12 | + * | |
| 13 | + * @var int | |
| 14 | + */ | |
| 15 | + const HIGH_TABLE_COUNT_THRESHOLD = 150; | |
| 16 | + | |
| 17 | + /** | |
| 18 | + * Option name to store all the status information for the tables | |
| 19 | + * | |
| 20 | + * @var string | |
| 21 | + */ | |
| 22 | + const TABLES_LIST_OPTION_NAME = 'all-tables-status'; | |
| 23 | + | |
| 24 | + /** | |
| 25 | + * Option name to save the state of the table scan | |
| 26 | + * | |
| 27 | + * @var string | |
| 28 | + */ | |
| 29 | + const TABLES_LIST_STATE_OPTION_NAME = 'all-tables-names-scan-status'; | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * Option name to save the total table count | |
| 33 | + * | |
| 34 | + * @var string | |
| 35 | + */ | |
| 36 | + const TABLES_LIST_COUNT_OPTION_NAME = 'all-tables-names-count'; | |
| 37 | + | |
| 38 | + /** | |
| 11 | 39 | * Returns singleton instance object |
| 12 | 40 | * |
| 13 | 41 | * @return WP_Optimizer Returns `WP_Optimizer` object |
| 14 | 42 | */ |
| @@ -265,9 +293,8 @@ | ||
| 265 | 293 | $optimization->get_info(); |
| 266 | 294 | } |
| 267 | 295 | |
| 268 | 296 | $optimization->after_get_info(); |
| 269 | - | |
| 270 | 297 | return $optimization->get_results(); |
| 271 | 298 | } |
| 272 | 299 | |
| 273 | 300 | /** |
| @@ -281,9 +308,11 @@ | ||
| 281 | 308 | |
| 282 | 309 | $results = array(); |
| 283 | 310 | |
| 284 | 311 | if (empty($optimization_options)) return $results; |
| 285 | - | |
| 312 | + | |
| 313 | + $this->snapshot_cleaned_before_run(); | |
| 314 | + | |
| 286 | 315 | $optimizations = $this->sort_optimizations($this->get_optimizations(), 'run_sort_order'); |
| 287 | 316 | |
| 288 | 317 | foreach ($optimizations as $optimization_id => $optimization) { |
| 289 | 318 | $option_id = call_user_func(array($optimization, 'get_'.$which_option.'_id')); |
| @@ -299,8 +328,9 @@ | ||
| 299 | 328 | $results[$optimization_id] = $this->do_optimization($optimization); |
| 300 | 329 | } |
| 301 | 330 | } |
| 302 | 331 | |
| 332 | + $this->save_last_run_cleaned(); | |
| 303 | 333 | // Run action after all optimizations completed. |
| 304 | 334 | do_action('wp_optimize_after_optimizations'); |
| 305 | 335 | |
| 306 | 336 | return $results; |
| @@ -305,8 +335,28 @@ | ||
| 305 | 335 | |
| 306 | 336 | return $results; |
| 307 | 337 | |
| 308 | 338 | } |
| 339 | + | |
| 340 | + /** | |
| 341 | + * Saves a snapshot of total-cleaned before a run so the per-run diff can be computed afterwards. | |
| 342 | + */ | |
| 343 | + public function snapshot_cleaned_before_run() { | |
| 344 | + $options = WP_Optimize()->get_options(); | |
| 345 | + $options->update_option('wpo_last_run_cleaned_snapshot', $options->get_option('total-cleaned', '0')); | |
| 346 | + } | |
| 347 | + | |
| 348 | + /** | |
| 349 | + * Computes bytes cleaned in the last run (current total minus pre-run snapshot) | |
| 350 | + * and persists it as last-run-cleaned-bytes. | |
| 351 | + */ | |
| 352 | + public function save_last_run_cleaned() { | |
| 353 | + $options = WP_Optimize()->get_options(); | |
| 354 | + $snapshot = floatval($options->get_option('wpo_last_run_cleaned_snapshot', '0')); | |
| 355 | + $now = floatval($options->get_option('total-cleaned', '0')); | |
| 356 | + $last_run_bytes = strval(max(0, $now - $snapshot)); | |
| 357 | + $options->update_option('last-run-cleaned-bytes', $last_run_bytes); | |
| 358 | + } | |
| 309 | 359 | |
| 310 | 360 | public function get_table_prefix($allow_override = false) { |
| 311 | 361 | $wpdb = $GLOBALS['wpdb']; |
| 312 | 362 | if (is_multisite() && !defined('MULTISITE')) { |
| @@ -318,8 +368,60 @@ | ||
| 318 | 368 | return ($allow_override) ? apply_filters('wp_optimize_get_table_prefix', $prefix) : $prefix; |
| 319 | 369 | } |
| 320 | 370 | |
| 321 | 371 | /** |
| 372 | + * Verify if the number of tables in the database is too large | |
| 373 | + * | |
| 374 | + * @param int $total_table_count Passed by reference. Will carry the actual count value | |
| 375 | + * @return bool | |
| 376 | + */ | |
| 377 | + public function is_table_count_too_high(&$total_table_count = null) { | |
| 378 | + $wpo_db_info = WP_Optimize()->get_db_info(); | |
| 379 | + $table_prefix = $this->get_table_prefix(); | |
| 380 | + | |
| 381 | + $total_table_count = $wpo_db_info->get_table_count($table_prefix); | |
| 382 | + return self::HIGH_TABLE_COUNT_THRESHOLD < $total_table_count; | |
| 383 | + } | |
| 384 | + | |
| 385 | + /** | |
| 386 | + * Check the scan job state in options | |
| 387 | + * | |
| 388 | + * @return bool | |
| 389 | + */ | |
| 390 | + public function is_large_table_count_scan_complete() { | |
| 391 | + $options = WP_Optimize()->get_options(); | |
| 392 | + | |
| 393 | + $state = $options->get_option(self::TABLES_LIST_STATE_OPTION_NAME); | |
| 394 | + | |
| 395 | + if (is_array($state) && isset($state['state'])) { | |
| 396 | + return 'done' == $state['state']; | |
| 397 | + } | |
| 398 | + | |
| 399 | + return false; | |
| 400 | + } | |
| 401 | + | |
| 402 | + /** | |
| 403 | + * Get the status for all scanned tables | |
| 404 | + * | |
| 405 | + * @return array | |
| 406 | + */ | |
| 407 | + private function get_table_status_from_options() { | |
| 408 | + $options = WP_Optimize()->get_options(); | |
| 409 | + $tables = $options->get_option(WP_Optimizer::TABLES_LIST_OPTION_NAME); | |
| 410 | + return is_array($tables) ? $tables : array(); | |
| 411 | + } | |
| 412 | + | |
| 413 | + /** | |
| 414 | + * Get the count of the already scanned tables | |
| 415 | + * | |
| 416 | + * @return int | |
| 417 | + */ | |
| 418 | + public function get_table_status_count() { | |
| 419 | + $statuses = $this->get_table_status_from_options(); | |
| 420 | + return is_array($statuses) ? count($statuses) : 0; | |
| 421 | + } | |
| 422 | + | |
| 423 | + /** | |
| 322 | 424 | * Returns information about database tables. |
| 323 | 425 | * |
| 324 | 426 | * @param bool $update refresh or no cached data |
| 325 | 427 | * |
| @@ -331,10 +433,18 @@ | ||
| 331 | 433 | if (false === $update && null !== $tables_info) return $tables_info; |
| 332 | 434 | |
| 333 | 435 | $wpo_db_info = WP_Optimize()->get_db_info(); |
| 334 | 436 | |
| 335 | - $table_status = $wpo_db_info->get_show_table_status($update); | |
| 437 | + $too_many_tables = $this->is_table_count_too_high(); | |
| 438 | + $scan_complete = $this->is_large_table_count_scan_complete(); | |
| 336 | 439 | |
| 440 | + $table_status = array(); | |
| 441 | + if (!$too_many_tables) { | |
| 442 | + $table_status = $wpo_db_info->get_show_table_status($update); | |
| 443 | + } elseif ($scan_complete) { | |
| 444 | + $table_status = $this->get_table_status_from_options(); | |
| 445 | + } | |
| 446 | + | |
| 337 | 447 | // Filter on the site's DB prefix (was not done in releases up to 1.9.1). |
| 338 | 448 | $table_prefix = $this->get_table_prefix(); |
| 339 | 449 | |
| 340 | 450 | if (is_array($table_status)) { |
| @@ -354,14 +464,19 @@ | ||
| 354 | 464 | $array_index_was_unset = true; |
| 355 | 465 | continue; |
| 356 | 466 | } |
| 357 | 467 | |
| 358 | - $table_status[$index]->Engine = $wpo_db_info->get_table_type($table_name); | |
| 468 | + // Pass the already-fetched $table object through so these lookups don't have to | |
| 469 | + // re-search for it by name again (that search is O(n), and doing it here for every | |
| 470 | + // table turns the whole loop into O(n^2) on sites with a large table count). | |
| 471 | + $table_type = $wpo_db_info->get_table_type($table_name, $table); | |
| 359 | 472 | |
| 360 | - $table_status[$index]->is_optimizable = $wpo_db_info->is_table_optimizable($table_name); | |
| 361 | - $table_status[$index]->is_type_supported = $wpo_db_info->is_table_type_optimize_supported($table_name); | |
| 473 | + $table_status[$index]->Engine = $table_type; | |
| 474 | + | |
| 475 | + $table_status[$index]->is_optimizable = $wpo_db_info->is_table_optimizable($table_name, $table_type); | |
| 476 | + $table_status[$index]->is_type_supported = $wpo_db_info->is_table_type_optimize_supported($table_name, $table_type); | |
| 362 | 477 | // add information about corrupted tables. |
| 363 | - $is_needing_repair = $wpo_db_info->is_table_needing_repair($table_name); | |
| 478 | + $is_needing_repair = $wpo_db_info->is_table_needing_repair($table_name, $table_type); | |
| 364 | 479 | $table_status[$index]->is_needing_repair = $is_needing_repair; |
| 365 | 480 | if ($is_needing_repair) $corrupted_tables_count++; |
| 366 | 481 | |
| 367 | 482 | $table_status[$index] = $this->join_plugin_information($table_name, $table_status[$index]); |
| @@ -412,9 +527,9 @@ | ||
| 412 | 527 | * @param object $table_obj |
| 413 | 528 | * |
| 414 | 529 | * @return object |
| 415 | 530 | */ |
| 416 | - public function join_plugin_information($table_name, $table_obj) { | |
| 531 | + private function join_plugin_information($table_name, $table_obj) { | |
| 417 | 532 | // set can be removed flag. |
| 418 | 533 | $can_be_removed = false; |
| 419 | 534 | // set WP core table flag. |
| 420 | 535 | $wp_core_table = false; |
| @@ -419,13 +534,19 @@ | ||
| 419 | 534 | // set WP core table flag. |
| 420 | 535 | $wp_core_table = false; |
| 421 | 536 | // set WP actionscheduler table flag. |
| 422 | 537 | $wp_actionscheduler_table = (false !== stripos($table_name, 'actionscheduler_')); |
| 423 | - // add information about using table by any of installed plugins. | |
| 424 | - $table_obj->is_using = WP_Optimize()->get_db_info()->is_table_using_by_plugin($table_name); | |
| 425 | 538 | // if table belongs to any plugin then add plugins status. |
| 426 | 539 | $plugins = WP_Optimize()->get_db_info()->get_table_plugin($table_name); |
| 427 | 540 | |
| 541 | + if (false === $plugins) { | |
| 542 | + // if we can't determine which plugin use $table then mark table as used. | |
| 543 | + $table_obj->is_using = true; | |
| 544 | + } else { | |
| 545 | + // if any plugin is installed which use table - $table_name then mark table as used. | |
| 546 | + $table_obj->is_using = WP_Optimize()->get_db_info()->is_any_plugin_installed($plugins); | |
| 547 | + } | |
| 548 | + | |
| 428 | 549 | $plugin_status = array(); |
| 429 | 550 | |
| 430 | 551 | if (false !== $plugins) { |
| 431 | 552 | // if belongs to any of plugin then we can remove table if plugin not active. |
| @@ -474,8 +595,16 @@ | ||
| 474 | 595 | $table_information['non_inno_db_tables'] = 0; |
| 475 | 596 | $table_information['table_list'] = ''; |
| 476 | 597 | $table_information['is_optimizable'] = true; |
| 477 | 598 | |
| 599 | + // When too many tables exist and the batch scan hasn't completed yet, get_tables() returns an empty array. | |
| 600 | + // Fall back to a lightweight COUNT query so the UI shows the real table count instead of zero. | |
| 601 | + if (empty($tablesstatus) && $this->is_table_count_too_high() && !$this->is_large_table_count_scan_complete()) { | |
| 602 | + $prefix = $this->get_table_prefix(); | |
| 603 | + $table_information['non_inno_db_tables'] = (int) WP_Optimize()->get_db_info()->get_table_count($prefix); | |
| 604 | + return $table_information; | |
| 605 | + } | |
| 606 | + | |
| 478 | 607 | // Make a list of tables to optimize. |
| 479 | 608 | foreach ($tablesstatus as $each_table) { |
| 480 | 609 | // if $blog_id is set then filter tables |
| 481 | 610 | if ($blog_id > 0 && $blog_id !== $each_table->blog_id) continue; |
| @@ -610,8 +739,11 @@ | ||
| 610 | 739 | $total_now = strval($previously_saved + $converted_current); |
| 611 | 740 | |
| 612 | 741 | $options->update_option('total-cleaned-current-month', $total_now); |
| 613 | 742 | |
| 743 | + $previously_saved = floatval($options->get_option('db_bytes_saved', '0')); | |
| 744 | + $options->update_option('db_bytes_saved', strval($previously_saved + $converted_current)); | |
| 745 | + | |
| 614 | 746 | return $total_now; |
| 615 | 747 | } |
| 616 | 748 | |
| 617 | 749 | |
| @@ -664,6 +796,140 @@ | ||
| 664 | 796 | */ |
| 665 | 797 | public function show_innodb_force_optimize() { |
| 666 | 798 | $tablesstatus = $this->get_table_information(); |
| 667 | 799 | return false === $tablesstatus['is_optimizable'] && $tablesstatus['inno_db_tables'] > 0; |
| 800 | + } | |
| 801 | + | |
| 802 | + /** | |
| 803 | + * Queue handler for this class | |
| 804 | + * | |
| 805 | + * @return WP_Optimize_Tasks_Queue | |
| 806 | + */ | |
| 807 | + private function _tasks_queue() { | |
| 808 | + return WP_Optimize_Tasks_Queue::this('optimizer'); | |
| 809 | + } | |
| 810 | + | |
| 811 | + /** | |
| 812 | + * Add a batch scan job to the queue | |
| 813 | + * | |
| 814 | + * @param int $offset Batch offset | |
| 815 | + * @return void | |
| 816 | + */ | |
| 817 | + public function add_load_tables_task($offset) { | |
| 818 | + $this->_tasks_queue()->add_task(new WP_Optimize_Queue_Task(array(get_class($this), 'load_tables_in_batches'), array('offset' => $offset), '', 1)); | |
| 819 | + } | |
| 820 | + | |
| 821 | + /** | |
| 822 | + * Persist queue tasks in DB | |
| 823 | + * | |
| 824 | + * @return void | |
| 825 | + */ | |
| 826 | + public function flush_tasks() { | |
| 827 | + $this->_tasks_queue()->flush(); | |
| 828 | + } | |
| 829 | + | |
| 830 | + /** | |
| 831 | + * Run next available task for this queue | |
| 832 | + * | |
| 833 | + * @return void | |
| 834 | + */ | |
| 835 | + public function do_next_load_table_task() { | |
| 836 | + $this->_tasks_queue()->do_next_task(); | |
| 837 | + } | |
| 838 | + | |
| 839 | + /** | |
| 840 | + * Check if the queue ran all jobs | |
| 841 | + * | |
| 842 | + * @return bool | |
| 843 | + */ | |
| 844 | + public function is_task_queue_empty() { | |
| 845 | + return $this->_tasks_queue()->is_empty(); | |
| 846 | + } | |
| 847 | + | |
| 848 | + /** | |
| 849 | + * Find tables names and load their status, in sequenced requests so it does not timeout | |
| 850 | + * | |
| 851 | + * @param int $offset The pagination offset | |
| 852 | + * @return void | |
| 853 | + */ | |
| 854 | + public function load_tables_in_batches($offset) { | |
| 855 | + global $wpdb; | |
| 856 | + | |
| 857 | + $prefix = $wpdb->base_prefix; | |
| 858 | + | |
| 859 | + $options = WP_Optimize()->get_options(); | |
| 860 | + | |
| 861 | + $db_info = WP_Optimize_Database_Information::instance(); | |
| 862 | + | |
| 863 | + $tables = $options->get_option(WP_Optimizer::TABLES_LIST_OPTION_NAME); | |
| 864 | + $tables = is_array($tables) ? $tables : array(); | |
| 865 | + $tables_count = $options->get_option(WP_Optimizer::TABLES_LIST_COUNT_OPTION_NAME); | |
| 866 | + $tables_count = false !== $tables_count ? $tables_count : 0; | |
| 867 | + | |
| 868 | + $tables_batch = $db_info->get_tables_names($prefix, $offset, WP_Optimizer::HIGH_TABLE_COUNT_THRESHOLD); | |
| 869 | + | |
| 870 | + | |
| 871 | + foreach ($tables_batch as $table_name) { | |
| 872 | + $tables[$table_name] = $db_info->get_table_status($table_name); | |
| 873 | + } | |
| 874 | + | |
| 875 | + $options->update_option(WP_Optimizer::TABLES_LIST_OPTION_NAME, $tables, false); | |
| 876 | + | |
| 877 | + $done = 0 < count($tables_batch) && count($tables) >= (int) $tables_count; | |
| 878 | + if (true === $done) { | |
| 879 | + $options->update_option(WP_Optimizer::TABLES_LIST_STATE_OPTION_NAME, array('state' => 'done')); | |
| 880 | + } | |
| 881 | + } | |
| 882 | + | |
| 883 | + /** | |
| 884 | + * Initialize the table scan job | |
| 885 | + * | |
| 886 | + * @return int | |
| 887 | + */ | |
| 888 | + public function scan_job_init() { | |
| 889 | + global $wpdb; | |
| 890 | + | |
| 891 | + $options = WP_Optimize()->get_options(); | |
| 892 | + $db_info = WP_Optimize_Database_Information::instance(); | |
| 893 | + | |
| 894 | + $this->_tasks_queue()->unlock(); | |
| 895 | + $this->_tasks_queue()->delete_queue(); | |
| 896 | + | |
| 897 | + $prefix = $wpdb->base_prefix; | |
| 898 | + $tables_count = $db_info->get_table_count($prefix); | |
| 899 | + $options->update_option(self::TABLES_LIST_COUNT_OPTION_NAME, $tables_count); | |
| 900 | + $options->update_option(self::TABLES_LIST_OPTION_NAME, array(), false); | |
| 901 | + $options->update_option(self::TABLES_LIST_STATE_OPTION_NAME, array( | |
| 902 | + 'state' => 'working', | |
| 903 | + 'start' => time() | |
| 904 | + )); | |
| 905 | + | |
| 906 | + return $tables_count; | |
| 907 | + } | |
| 908 | + | |
| 909 | + /** | |
| 910 | + * Check if all scan is done, or it has been idle for too long | |
| 911 | + * | |
| 912 | + * @return bool | |
| 913 | + */ | |
| 914 | + public function maybe_restart_scan_job() { | |
| 915 | + if ($this->is_large_table_count_scan_complete()) { | |
| 916 | + return false; | |
| 917 | + } | |
| 918 | + | |
| 919 | + if ($this->_tasks_queue()->is_empty()) { | |
| 920 | + return true; | |
| 921 | + } | |
| 922 | + | |
| 923 | + $options = WP_Optimize()->get_options(); | |
| 924 | + | |
| 925 | + $idle_max_time = HOUR_IN_SECONDS; | |
| 926 | + | |
| 927 | + $scan = $options->get_option(self::TABLES_LIST_STATE_OPTION_NAME); | |
| 928 | + | |
| 929 | + if (is_array($scan) && isset($scan['start'])) { | |
| 930 | + return (time() - $scan['start']) > $idle_max_time; | |
| 931 | + } | |
| 932 | + | |
| 933 | + return false; | |
| 668 | 934 | } |
| 669 | 935 | } |