diff --git a/assets/_styles.scss b/assets/_styles.scss
index c08994b2c..1f2408880 100644
--- a/assets/_styles.scss
+++ b/assets/_styles.scss
@@ -590,7 +590,8 @@ body.admin-color-light #wp-admin-bar-query-monitor:not(.qm-all-clear):not(:hover
margin: 20px !important;
}
- .qm-concerns table {
+ .qm-concerns table,
+ .qm-discovered table {
border-top: 1px solid $qm-cell-border !important;
margin-bottom: 20px !important;
}
diff --git a/assets/query-monitor.css b/assets/query-monitor.css
index 1af641606..6dfc5072d 100644
--- a/assets/query-monitor.css
+++ b/assets/query-monitor.css
@@ -539,7 +539,8 @@ body.admin-color-light #wp-admin-bar-query-monitor:not(.qm-all-clear):not(:hover
font-size: 14px !important;
margin: 20px !important;
}
-#query-monitor-main .qm-concerns table {
+#query-monitor-main .qm-concerns table,
+#query-monitor-main .qm-discovered table {
border-top: 1px solid #e0e0e0 !important;
margin-bottom: 20px !important;
}
diff --git a/classes/Dispatcher.php b/classes/Dispatcher.php
index 77b0416c4..2954f2e15 100644
--- a/classes/Dispatcher.php
+++ b/classes/Dispatcher.php
@@ -15,6 +15,10 @@ public function __construct( QM_Plugin $qm ) {
define( 'QM_COOKIE', 'wp-query_monitor_' . COOKIEHASH );
}
+ if ( ! defined( 'QM_MAX_HOOKS_DISCOVERED' ) ) {
+ define( 'QM_MAX_HOOKS_DISCOVERED', 500 );
+ }
+
add_action( 'init', array( $this, 'init' ) );
}
diff --git a/collectors/hooks_within_bounds.php b/collectors/hooks_within_bounds.php
new file mode 100644
index 000000000..078ab445a
--- /dev/null
+++ b/collectors/hooks_within_bounds.php
@@ -0,0 +1,138 @@
+data = array(
+ 'hooks' => array(),
+ 'bounds' => array(),
+ 'counts' => array(),
+ );
+ }
+
+ function action_function_listener_start( $id ) {
+ if ( $this->is_active( $id ) ) {
+ return;
+ }
+
+ if ( array_key_exists( $id, $this->data['bounds'] ) ) {
+ trigger_error( sprintf(
+ /* translators: %s: Hook discovery ID */
+ esc_html__( 'Hook discovery ID %s already exists', 'query-monitor' ),
+ '' . $id . '
'
+ ), E_USER_NOTICE );
+ return;
+ }
+
+ $this->maybe_add_all_callback();
+
+ $this->active[ $id ] = 1;
+ $this->data['hooks'][ $id ] = array();
+ $this->data['bounds'][ $id ] = array(
+ 'start' => new QM_Backtrace(),
+ 'stop' => null,
+ );
+ $this->data['counts'][ $id ] = 0;
+ }
+
+ function action_function_all( $var = null ) {
+ if ( ! $this->is_active() ) {
+ remove_action( 'all', array( $this, 'action_function_all' ) );
+ return $var;
+ }
+
+ if ( in_array( current_action(), array(
+ 'qm/listen/start',
+ 'qm/listen/stop',
+ ) ) ) {
+ return $var;
+ }
+
+ global $wp_actions;
+
+ foreach ( array_keys( $this->active ) as $id ) {
+ end( $this->data['hooks'][ $id ] );
+ $last = current( $this->data['hooks'][ $id ] );
+
+ if ( current_action() === $last['hook'] ) {
+ $i = key( $this->data['hooks'][ $id ] );
+ $this->data['hooks'][ $id ][ $i ]['fires']++;
+ } else {
+ $this->data['hooks'][ $id ][] = array(
+ 'hook' => current_action(),
+ 'is_action' => array_key_exists( current_action(), $wp_actions ),
+ 'fires' => 1,
+ );
+ }
+
+ if ( QM_MAX_HOOKS_DISCOVERED < ++$this->data['counts'][ $id ] ) {
+ $this->action_function_listener_stop( $id );
+ $this->data['bounds'][ $id ]['was_terminated'] = true;
+ }
+
+ return $var;
+ }
+ }
+
+ function action_function_listener_stop( $id ) {
+ if ( ! $this->is_active( $id ) && ! array_key_exists( $id, $this->data['hooks'] ) ) {
+ trigger_error( sprintf(
+ /* translators: %s: Hook discovery ID */
+ esc_html__( 'Hook discovery starting bound for %s has not been set', 'query-monitor' ),
+ '' . $id . '
'
+ ), E_USER_NOTICE );
+ return;
+ }
+
+ unset( $this->active[ $id ] );
+ $this->data['bounds'][ $id ]['stop'] = new QM_Backtrace();
+
+ if ( ! $this->is_active() ) {
+ remove_action( 'all', array( $this, 'action_function_all' ) );
+ }
+ }
+
+ function action_function_shutdown() {
+ if ( $this->is_active() ) {
+ foreach ( array_keys( $this->active ) as $id ) {
+ $this->action_function_listener_stop( $id );
+ }
+ }
+ }
+
+ function is_active( $id = false ) {
+ return false !== $id ? array_key_exists( $id, $this->active ) : ! empty( $this->active );
+ }
+
+ function maybe_add_all_callback() {
+ if ( ! $this->is_active() ) {
+ add_action( 'all', array( $this, 'action_function_all' ), 0 );
+ }
+ }
+
+}
+
+# Load early so early hooks can be discovered
+QM_Collectors::add( new QM_Collector_Hooks_Within_Bounds() );
diff --git a/dispatchers/Html.php b/dispatchers/Html.php
index 495edaf4a..e0dc3395d 100644
--- a/dispatchers/Html.php
+++ b/dispatchers/Html.php
@@ -381,39 +381,47 @@ protected function after_output() {
echo '
' . esc_html( $hook['name'] ) . '
';
if ( 'all' === $hook['name'] ) {
+ $_hooks_within_bounds = QM_Collectors::get( 'hooks_within_bounds' );
+ $hooks_within_bounds = $_hooks_within_bounds->get_data();
+
+ $extra = ' Try to avoid using it.';
+
+ if ( $_hooks_within_bounds && !empty( $hooks_within_bounds['hooks'] ) )
+ $extra = 'all
'
+ esc_html__( 'Warning: The %1$s action is extremely resource intensive.%2$s', 'query-monitor' ),
+ 'all
',
+ $extra
);
echo '';
}
diff --git a/output/html/hooks_within_bounds.php b/output/html/hooks_within_bounds.php
new file mode 100644
index 000000000..ce56ccff7
--- /dev/null
+++ b/output/html/hooks_within_bounds.php
@@ -0,0 +1,136 @@
+menu( array(
+ 'title' => '└ ' . esc_html__( 'Discovered Hooks', 'query-monitor' ),
+ ) );
+ }
+
+ return $menu;
+ }
+
+ public function output() {
+ $data = $this->collector->get_data();
+
+ if ( empty( $data['hooks'] ) ) {
+ $this->before_non_tabular_output();
+
+ $_notice = 'No discovered hooks.';
+
+ if ( defined( 'QM_DISABLE_HOOK_DISCOVERY' ) && QM_DISABLE_HOOK_DISCOVERY ) {
+ $_notice = 'Hook discovery disabled.';
+ }
+
+ $notice = __( $_notice, 'query-monitor' );
+ echo $this->build_notice( $notice ); // WPCS: XSS ok.
+
+ $this->after_non_tabular_output();
+ return;
+ }
+
+ printf(
+ '' . esc_html__( 'Label', 'query-monitor' ) . ' | '; + echo ''; + echo ' | ' . esc_html__( 'Hook', 'query-monitor' ) . ' | '; + echo '' . esc_html__( 'Type', 'query-monitor' ) . ' | '; + echo '
---|---|---|---|
'; + echo ''; + echo esc_html( $id ); + echo $trace_text__start; // WPCS: XSS ok. + echo $trace_text__stop; // WPCS: XSS ok. + echo ''; + echo ' | '; + } + + echo '' . esc_html( ++$i ) . ' | '; + + echo '';
+ echo '' . esc_html( $hook['hook'] ) . ' ';
+
+ if ( 1 < $hook['fires'] ) {
+ echo 'Fired ' . esc_html( $hook['fires'] ) . ' times'; + } + echo ' | ';
+ echo '' . ( $hook['is_action'] ? 'Action' : 'Filter' ) . ' | '; + echo '