Plugin Directory

Changeset 3320647


Ignore:
Timestamp:
07/01/2025 01:02:25 PM (9 months ago)
Author:
loghin
Message:

Delivered final tune-ups to ensure seamless continuity in the upcoming version branch.

Location:
dynamic-front-end-heartbeat-control
Files:
35 added
12 edited

Legend:

Unmodified
Added
Removed
  • dynamic-front-end-heartbeat-control/trunk/defibrillator/load-estimator.php

    r3310561 r3320647  
    66class Dfehc_ServerLoadEstimator {
    77    const BASELINE_TRANSIENT_PREFIX = 'dfehc_baseline_';
    8     const LOAD_CACHE_TRANSIENT = 'dfehc_last_known_load';
    9     const LOAD_SPIKE_TRANSIENT = 'dfehc_load_spike_score';
    10 
    11     public static function get_server_load($duration = 0.025) {
     8    const LOAD_CACHE_TRANSIENT      = 'dfehc_last_known_load';
     9    const LOAD_SPIKE_TRANSIENT      = 'dfehc_load_spike_score';
     10
     11    public static function get_server_load(float $duration = 0.025) {
    1212        if (!function_exists('microtime') || (defined('DFEHC_DISABLE_LOAD_ESTIMATION') && DFEHC_DISABLE_LOAD_ESTIMATION)) {
    1313            return false;
    1414        }
    1515
    16         $hostname = self::get_hostname_key();
    17         $baseline_transient = self::get_baseline_transient_name($hostname);
    18         $cached_load = get_transient(self::LOAD_CACHE_TRANSIENT);
    19 
    20         if ($cached_load !== false) return $cached_load;
    21 
    22         $baseline_expiration = apply_filters('dfehc_baseline_expiration', 7 * DAY_IN_SECONDS);
    23         $cache_ttl = apply_filters('dfehc_load_cache_ttl', 90);
    24 
    25         $baseline = self::get_baseline_value($baseline_transient);
     16        $hostname   = self::get_hostname_key();
     17        $baseline_t = self::get_baseline_transient_name($hostname);
     18        $cache_ttl  = (int) apply_filters('dfehc_load_cache_ttl', 90);
     19        $cached     = get_transient(self::LOAD_CACHE_TRANSIENT);
     20
     21        if ($cached !== false) {
     22            return $cached;
     23        }
     24
     25        $sys_avg = self::try_sys_getloadavg();
     26        if ($sys_avg !== null) {
     27            set_transient(self::LOAD_CACHE_TRANSIENT, $sys_avg, $cache_ttl);
     28            return $sys_avg;
     29        }
     30
     31        $baseline = self::get_baseline_value($baseline_t);
    2632        if (!$baseline) {
    27             $baseline = self::calibrate_baseline($duration);
    28             self::set_baseline_value($baseline_transient, $baseline, $baseline_expiration);
    29         }
    30 
     33            $baseline = self::maybe_calibrate($baseline_t, $duration);
     34        }
     35
     36        $loops_per_sec = self::run_loop($duration);
     37        if ($loops_per_sec <= 0) {
     38            return false;
     39        }
     40
     41        $load_ratio   = ($baseline * 0.125) / max($loops_per_sec, 1);
     42        $load_percent = round(min(100, max(0, $load_ratio * 100)), 2);
     43
     44        self::update_spike_score($load_percent);
     45        set_transient(self::LOAD_CACHE_TRANSIENT, $load_percent, $cache_ttl);
     46
     47        return $load_percent;
     48    }
     49
     50    public static function calibrate_baseline(float $duration = 0.025): float {
     51        return self::run_loop($duration);
     52    }
     53
     54    public static function maybe_calibrate_during_cron(): void {
     55        if (!defined('DOING_CRON') || !DOING_CRON) {
     56            return;
     57        }
     58        self::ensure_baseline();
     59    }
     60
     61    public static function maybe_calibrate_if_idle(): void {
     62        if (is_admin() || is_user_logged_in()) {
     63            return;
     64        }
     65        self::ensure_baseline();
     66    }
     67
     68    private static function try_sys_getloadavg(): ?float {
     69        if (!function_exists('sys_getloadavg')) {
     70            return null;
     71        }
     72        $avg = sys_getloadavg();
     73        if (!is_array($avg) || !isset($avg[0])) {
     74            return null;
     75        }
     76        $cores = function_exists('dfehc_get_cpu_cores') ? dfehc_get_cpu_cores() : 1;
     77        if ($cores <= 0) {
     78            $cores = 1;
     79        }
     80        return min(100, round(($avg[0] / $cores) * 100, 2));
     81    }
     82
     83    private static function run_loop(float $duration): float {
    3184        $start = microtime(true);
    32         $end = $start + $duration;
    33         $count = 0;
    34         $now = $start;
    35 
     85        $end   = $start + $duration;
     86        $cnt   = 0;
     87        $now   = $start;
    3688        while ($now < $end) {
    37             $count++;
     89            ++$cnt;
    3890            $now = microtime(true);
    3991        }
    40 
    41         $time_taken = microtime(true) - $start;
    42         $loops_per_second = $count / max($time_taken, 0.0001);
    43         $load_ratio = ($baseline * 0.125) / max($loops_per_second, 1);
    44         $load_percent = round(min(100, max(0, $load_ratio * 100)), 2);
    45 
    46         do_action('dfehc_debug_load_metrics', compact('baseline', 'count', 'loops_per_second', 'load_percent'));
    47 
    48         $spike_score = (float) get_transient(self::LOAD_SPIKE_TRANSIENT);
    49         $spike_decay = apply_filters('dfehc_spike_decay', 0.5);
    50         $spike_increment = apply_filters('dfehc_spike_increment', 1.0);
    51         $spike_threshold = apply_filters('dfehc_spike_threshold', 3.0);
     92        $elapsed = $now - $start;
     93        return $elapsed > 0 ? $cnt / $elapsed : 0.0;
     94    }
     95
     96    private static function maybe_calibrate(string $baseline_t, float $duration): float {
     97        $hostname  = self::get_hostname_key();
     98        $lock_key  = 'dfehc_calibrating_' . $hostname;
     99        $lock      = self::acquire_lock($lock_key, 30);
     100
     101        $baseline = self::run_loop($duration);
     102        if ($lock) {
     103            $exp = (int) apply_filters('dfehc_baseline_expiration', 7 * DAY_IN_SECONDS);
     104            self::set_baseline_value($baseline_t, $baseline, $exp);
     105            self::release_lock($lock, $lock_key);
     106        }
     107        return $baseline;
     108    }
     109
     110    private static function update_spike_score(float $load_percent): void {
     111        $score         = (float) get_transient(self::LOAD_SPIKE_TRANSIENT);
     112        $decay         = (float) apply_filters('dfehc_spike_decay', 0.5);
     113        $increment     = (float) apply_filters('dfehc_spike_increment', 1.0);
     114        $threshold     = (float) apply_filters('dfehc_spike_threshold', 3.0);
     115        $hostname      = self::get_hostname_key();
     116        $baseline_name = self::get_baseline_transient_name($hostname);
    52117
    53118        if ($load_percent > 90) {
    54             $spike_score += $spike_increment;
    55         } else {
    56             $spike_score = max(0, $spike_score - $spike_decay);
    57         }
    58 
    59         if ($spike_score >= $spike_threshold) {
    60             self::delete_baseline_value($baseline_transient);
     119            $score += $increment;
     120        } else {
     121            $score = max(0.0, $score - $decay);
     122        }
     123
     124        if ($score >= $threshold) {
     125            self::delete_baseline_value($baseline_name);
    61126            delete_transient(self::LOAD_SPIKE_TRANSIENT);
    62127        } else {
    63             set_transient(self::LOAD_SPIKE_TRANSIENT, $spike_score, HOUR_IN_SECONDS);
    64         }
    65 
    66         set_transient(self::LOAD_CACHE_TRANSIENT, $load_percent, $cache_ttl);
    67         return $load_percent;
    68     }
    69 
    70     public static function calibrate_baseline($duration = 0.025) {
    71         $start = microtime(true);
    72         $end = $start + $duration;
    73         $count = 0;
    74         $now = $start;
    75 
    76         while ($now < $end) {
    77             $count++;
    78             $now = microtime(true);
    79         }
    80 
    81         $time_taken = microtime(true) - $start;
    82         return $count / max($time_taken, 0.0001);
    83     }
    84 
    85     public static function maybe_calibrate_during_cron() {
    86         if (defined('DOING_CRON') && DOING_CRON) {
    87             $hostname = self::get_hostname_key();
    88             $baseline_transient = self::get_baseline_transient_name($hostname);
    89             if (!self::get_baseline_value($baseline_transient)) {
    90                 $baseline = self::calibrate_baseline();
    91                 $baseline_expiration = apply_filters('dfehc_baseline_expiration', 7 * DAY_IN_SECONDS);
    92                 self::set_baseline_value($baseline_transient, $baseline, $baseline_expiration);
    93             }
    94         }
    95     }
    96 
    97     public static function maybe_calibrate_if_idle() {
    98         if (!is_admin() && !is_user_logged_in()) {
    99             $hostname = self::get_hostname_key();
    100             $baseline_transient = self::get_baseline_transient_name($hostname);
    101             if (!self::get_baseline_value($baseline_transient)) {
    102                 $baseline = self::calibrate_baseline();
    103                 $baseline_expiration = apply_filters('dfehc_baseline_expiration', 7 * DAY_IN_SECONDS);
    104                 self::set_baseline_value($baseline_transient, $baseline, $baseline_expiration);
    105             }
    106         }
    107     }
    108 
    109     private static function get_baseline_transient_name($hostname) {
     128            set_transient(self::LOAD_SPIKE_TRANSIENT, $score, HOUR_IN_SECONDS);
     129        }
     130    }
     131
     132    private static function ensure_baseline(): void {
     133        $hostname   = self::get_hostname_key();
     134        $baseline_t = self::get_baseline_transient_name($hostname);
     135        if (self::get_baseline_value($baseline_t)) {
     136            return;
     137        }
     138        $lock_key = 'dfehc_calibrating_' . $hostname;
     139        $lock     = self::acquire_lock($lock_key, 30);
     140        if (!$lock) {
     141            return;
     142        }
     143        $baseline = self::run_loop(0.025);
     144        $exp      = (int) apply_filters('dfehc_baseline_expiration', 7 * DAY_IN_SECONDS);
     145        self::set_baseline_value($baseline_t, $baseline, $exp);
     146        self::release_lock($lock, $lock_key);
     147    }
     148
     149    private static function acquire_lock(string $key, int $ttl) {
     150        if (class_exists('WP_Lock')) {
     151            $lock = new \WP_Lock($key, $ttl);
     152            return $lock->acquire() ? $lock : null;
     153        }
     154        return wp_cache_add($key, 1, $ttl) ? (object) ['key' => $key] : null;
     155    }
     156
     157    private static function release_lock($lock, string $key): void {
     158        if ($lock instanceof \WP_Lock) {
     159            $lock->release();
     160        } else {
     161            wp_cache_delete($key);
     162        }
     163    }
     164
     165    private static function get_baseline_transient_name(string $hostname): string {
    110166        return self::BASELINE_TRANSIENT_PREFIX . $hostname;
    111167    }
    112168
    113     private static function get_hostname_key() {
    114         return sanitize_key(php_uname('n'));
    115     }
    116 
    117     private static function get_baseline_value($name) {
     169    private static function get_hostname_key(): string {
     170        return substr(md5(php_uname('n')), 0, 10);
     171    }
     172
     173    private static function get_baseline_value(string $name) {
    118174        return is_multisite() ? get_site_transient($name) : get_transient($name);
    119175    }
    120176
    121     private static function set_baseline_value($name, $value, $expiration) {
     177    private static function set_baseline_value(string $name, $value, int $exp): void {
    122178        if (is_multisite()) {
    123             set_site_transient($name, $value, $expiration);
    124         } else {
    125             set_transient($name, $value, $expiration);
    126         }
    127     }
    128 
    129     private static function delete_baseline_value($name) {
    130         return is_multisite() ? delete_site_transient($name) : delete_transient($name);
     179            set_site_transient($name, $value, $exp);
     180        } else {
     181            set_transient($name, $value, $exp);
     182        }
     183    }
     184
     185    private static function delete_baseline_value(string $name): void {
     186        if (is_multisite()) {
     187            delete_site_transient($name);
     188        } else {
     189            delete_transient($name);
     190        }
    131191    }
    132192}
     193
    133194add_action('init', [Dfehc_ServerLoadEstimator::class, 'maybe_calibrate_during_cron']);
    134195add_action('template_redirect', [Dfehc_ServerLoadEstimator::class, 'maybe_calibrate_if_idle']);
    135196
    136 add_filter('heartbeat_settings', function($settings) {
    137     if (!class_exists(Dfehc_ServerLoadEstimator::class)) return $settings;
     197add_filter('heartbeat_settings', function ($settings) {
     198    if (!class_exists(Dfehc_ServerLoadEstimator::class)) {
     199        return $settings;
     200    }
    138201
    139202    $load = Dfehc_ServerLoadEstimator::get_server_load();
    140 
    141     if ($load === false) return $settings;
    142 
    143     $thresholds = apply_filters('dfehc_heartbeat_thresholds', [
    144         'low' => 20,
     203    if ($load === false) {
     204        return $settings;
     205    }
     206
     207    $ths = apply_filters('dfehc_heartbeat_thresholds', [
     208        'low'    => 20,
    145209        'medium' => 50,
    146         'high' => 75,
     210        'high'   => 75,
    147211    ]);
    148212
    149213    if (!is_admin() && !current_user_can('edit_posts')) {
    150         if ($load < $thresholds['low']) {
    151             $settings['interval'] = 50;
    152         } elseif ($load < $thresholds['medium']) {
    153             $settings['interval'] = 60;
    154         } elseif ($load < $thresholds['high']) {
    155             $settings['interval'] = 120;
    156         } else {
    157             $settings['interval'] = 180;
    158         }
     214        $settings['interval'] = $load < $ths['low'] ? 50 : ($load < $ths['medium'] ? 60 : ($load < $ths['high'] ? 120 : 180));
    159215    } elseif (current_user_can('editor')) {
    160         $settings['interval'] = ($load < $thresholds['high']) ? 30 : 60;
     216        $settings['interval'] = $load < $ths['high'] ? 30 : 60;
    161217    } elseif (current_user_can('administrator')) {
    162         $settings['interval'] = ($load < $thresholds['high']) ? 20 : 40;
     218        $settings['interval'] = $load < $ths['high'] ? 20 : 40;
    163219    }
    164220
  • dynamic-front-end-heartbeat-control/trunk/engine/interval-helper.php

    r3310561 r3320647  
    11<?php
    2 function dfehc_weighted_sum(array $factors, array $weights)
     2declare(strict_types=1);
     3
     4define('DFEHC_OPTIONS_PREFIX', 'dfehc_');
     5define('DFEHC_OPTION_MIN_INTERVAL', DFEHC_OPTIONS_PREFIX . 'min_interval');
     6define('DFEHC_OPTION_MAX_INTERVAL', DFEHC_OPTIONS_PREFIX . 'max_interval');
     7define('DFEHC_OPTION_PRIORITY_SLIDER', DFEHC_OPTIONS_PREFIX . 'priority_slider');
     8define('DFEHC_OPTION_EMA_ALPHA', DFEHC_OPTIONS_PREFIX . 'ema_alpha');
     9define('DFEHC_OPTION_MAX_SERVER_LOAD', DFEHC_OPTIONS_PREFIX . 'max_server_load');
     10define('DFEHC_OPTION_MAX_RESPONSE_TIME', DFEHC_OPTIONS_PREFIX . 'max_response_time');
     11define('DFEHC_OPTION_SMA_WINDOW', DFEHC_OPTIONS_PREFIX . 'sma_window');
     12define('DFEHC_OPTION_MAX_DECREASE_RATE', DFEHC_OPTIONS_PREFIX . 'max_decrease_rate');
     13
     14define('DFEHC_DEFAULT_MIN_INTERVAL', 15);
     15define('DFEHC_DEFAULT_MAX_INTERVAL', 300);
     16define('DFEHC_DEFAULT_MAX_SERVER_LOAD', 85);
     17define('DFEHC_DEFAULT_MAX_RESPONSE_TIME', 5.0);
     18define('DFEHC_DEFAULT_EMA_ALPHA', 0.4);
     19define('DFEHC_DEFAULT_SMA_WINDOW', 5);
     20define('DFEHC_DEFAULT_MAX_DECREASE_RATE', 0.25);
     21define('DFEHC_DEFAULT_EMA_TTL', 600);
     22
     23function dfehc_store_lockfree(string $key, $value, int $ttl): bool
    324{
    4     $sum = 0;
     25    if (function_exists('wp_cache_add') && wp_cache_add($key, $value, '', $ttl)) {
     26        return true;
     27    }
     28    return set_transient($key, $value, $ttl);
     29}
     30
     31function dfehc_set_transient(string $key, float $value, float $interval): void
     32{
     33    $ttl = (int) apply_filters(
     34        'dfehc_transient_ttl',
     35        max(60, (int) ceil($interval) * 2),
     36        $key,
     37        $value,
     38        $interval
     39    );
     40    dfehc_store_lockfree($key, $value, $ttl);
     41}
     42
     43function dfehc_weighted_sum(array $factors, array $weights): float
     44{
     45    $sum = 0.0;
    546    foreach ($factors as $k => $v) {
    6         $w = $weights[$k] ?? 0;
    7         $sum += $v * $w;
     47        $sum += ($weights[$k] ?? 0.0) * $v;
    848    }
    949    return $sum;
    1050}
    1151
    12 function dfehc_apply_exponential_moving_average(float $interval): float
     52function dfehc_normalize_weights(array $weights): array
    1353{
    14     $alpha             = 0.4;
    15     $key               = 'dfehc_previous_intervals';
    16     $previous          = get_transient($key);
    17     if ($previous === false) {
    18         $previous = [];
     54    $total = array_sum($weights);
     55    if ($total <= 0) {
     56        return array_fill_keys(array_keys($weights), 1 / max(1, count($weights)));
    1957    }
    20     array_unshift($previous, $interval);
    21     $previous = array_slice($previous, 0, 100);
    22     $ema = $interval;
    23     foreach ($previous as $idx => $val) {
    24         if ($idx === 0) {
    25             $ema = $val;
    26         } else {
    27             $ema = $alpha * $val + (1 - $alpha) * $ema;
    28         }
     58    foreach ($weights as $k => $w) {
     59        $weights[$k] = $w / $total;
    2960    }
    30     set_transient($key, $previous, 10 * MINUTE_IN_SECONDS);
     61    return $weights;
     62}
     63
     64function dfehc_apply_exponential_moving_average(float $current): float
     65{
     66    $alpha = max(
     67        0.01,
     68        min(1.0, (float) get_option(DFEHC_OPTION_EMA_ALPHA, DFEHC_DEFAULT_EMA_ALPHA))
     69    );
     70    $key  = 'dfehc_ema_' . get_current_blog_id();
     71    $prev = get_transient($key);
     72    $ema  = ($prev === false) ? $current : $alpha * $current + (1 - $alpha) * (float) $prev;
     73    $ttl  = (int) apply_filters('dfehc_ema_ttl', DFEHC_DEFAULT_EMA_TTL, $current, $ema);
     74    dfehc_store_lockfree($key, $ema, $ttl);
    3175    return $ema;
    3276}
    3377
    34 function dfehc_calculate_recommended_interval(float $time_elapsed, float $load_average, float $server_response_time): float
    35 {
    36     $min_interval = (int) get_option('DFEHC_MIN_INTERVAL', DFEHC_MIN_INTERVAL);
    37     $max_interval = (int) get_option('DFEHC_MAX_INTERVAL', DFEHC_MAX_INTERVAL);
     78function dfehc_calculate_recommended_interval(
     79    float $time_elapsed,
     80    float $load_average,
     81    float $server_response_time
     82): float {
     83    $min_interval      = max(1, (int) get_option(DFEHC_OPTION_MIN_INTERVAL, DFEHC_DEFAULT_MIN_INTERVAL));
     84    $max_interval      = max($min_interval, (int) get_option(DFEHC_OPTION_MAX_INTERVAL, DFEHC_DEFAULT_MAX_INTERVAL));
     85    $max_server_load   = max(0.1, (float) get_option(DFEHC_OPTION_MAX_SERVER_LOAD, DFEHC_DEFAULT_MAX_SERVER_LOAD));
     86    $max_response_time = max(0.1, (float) get_option(DFEHC_OPTION_MAX_RESPONSE_TIME, DFEHC_DEFAULT_MAX_RESPONSE_TIME));
    3887
    3988    $factors = [
    40         'user_activity' => $time_elapsed / $max_interval,
    41         'server_load'   => 1 - ($load_average / DFEHC_MAX_SERVER_LOAD),
    42         'response_time' => $server_response_time > 0 ? ($server_response_time / DFEHC_MAX_RESPONSE_TIME) : 0,
     89        'user_activity' => max(0.0, min(1.0, $time_elapsed / $max_interval)),
     90        'server_load'   => max(0.0, min(1.0, $load_average / $max_server_load)),
     91        'response_time' => $server_response_time > 0
     92            ? max(0.0, min(1.0, $server_response_time / $max_response_time))
     93            : 0.0,
    4394    ];
    4495
    45     $slider = (float) get_option('dfehc_priority_slider', 0);
     96    $slider  = max(-1.0, min(1.0, (float) get_option(DFEHC_OPTION_PRIORITY_SLIDER, 0.0)));
     97    $weights = [
     98        'user_activity' => 0.4 - 0.2 * $slider,
     99        'server_load'   => (0.6 + 0.2 * $slider) / 2,
     100        'response_time' => (0.6 + 0.2 * $slider) / 2,
     101    ];
     102    $weights = dfehc_normalize_weights($weights);
    46103
    47     if ($slider < 0) {
    48         $weights = [
    49             'user_activity' => 0.4 + (0.1 * $slider),
    50             'server_load'   => 0.3 - (0.1 * $slider / 2),
    51             'response_time' => 0.3 - (0.1 * $slider / 2),
    52         ];
    53     } else {
    54         $weights = [
    55             'user_activity' => 0.4 - (0.1 * $slider),
    56             'server_load'   => 0.3 + (0.1 * $slider / 2),
    57             'response_time' => 0.3 + (0.1 * $slider / 2),
    58         ];
     104    $raw      = $min_interval + dfehc_weighted_sum($factors, $weights) * ($max_interval - $min_interval);
     105    $smoothed = dfehc_apply_exponential_moving_average($raw);
     106    $lagged   = dfehc_defensive_stance($smoothed);
     107
     108    return max($min_interval, min($max_interval, $lagged));
     109}
     110
     111function dfehc_calculate_interval_based_on_duration(
     112    float $avg_duration,
     113    float $load_average
     114): float {
     115    $min_interval = max(1, (int) get_option(DFEHC_OPTION_MIN_INTERVAL, DFEHC_DEFAULT_MIN_INTERVAL));
     116    $max_interval = max($min_interval, (int) get_option(DFEHC_OPTION_MAX_INTERVAL, DFEHC_DEFAULT_MAX_INTERVAL));
     117    if ($avg_duration <= $min_interval) {
     118        return (float) $min_interval;
     119    }
     120    if ($avg_duration >= $max_interval) {
     121        return (float) $max_interval;
     122    }
     123    return dfehc_calculate_recommended_interval($avg_duration, $load_average, 0.0);
     124}
     125
     126function dfehc_smooth_moving(array $values): float
     127{
     128    if (!$values) {
     129        return 0.0;
     130    }
     131    $window = max(1, (int) get_option(DFEHC_OPTION_SMA_WINDOW, DFEHC_DEFAULT_SMA_WINDOW));
     132    $subset = array_slice($values, -$window);
     133    if (!$subset) {
     134        return 0.0;
     135    }
     136    return array_sum($subset) / count($subset);
     137}
     138
     139function dfehc_defensive_stance(float $proposed): float
     140{
     141    $key      = 'dfehc_prev_int_' . get_current_blog_id();
     142    $previous = get_transient($key);
     143    if ($previous === false) {
     144        dfehc_store_lockfree($key, $proposed, (int) ceil($proposed));
     145        return $proposed;
    59146    }
    60147
    61     $interval = $min_interval + dfehc_weighted_sum($factors, $weights) * ($max_interval - $min_interval);
     148    $previous  = (float) $previous;
     149    $max_drop  = max(
     150        0.0,
     151        min(1.0, (float) get_option(DFEHC_OPTION_MAX_DECREASE_RATE, DFEHC_DEFAULT_MAX_DECREASE_RATE))
     152    );
     153    $max_rise  = (float) apply_filters('dfehc_max_increase_rate', 0.5);
    62154
    63     return dfehc_apply_exponential_moving_average($interval);
     155    $lower = $previous * (1 - $max_drop);
     156    $upper = $previous * (1 + $max_rise);
     157    $final = max($lower, min($upper, $proposed));
     158
     159    dfehc_store_lockfree($key, $final, (int) ceil($final));
     160    return $final;
    64161}
    65 
    66 function dfehc_calculate_interval_based_on_duration(float $avg_duration, float $load_average): float
    67 {
    68     $min_interval = (int) get_option('DFEHC_MIN_INTERVAL', DFEHC_MIN_INTERVAL);
    69     $max_interval = (int) get_option('DFEHC_MAX_INTERVAL', DFEHC_MAX_INTERVAL);
    70 
    71     if ($avg_duration <= $min_interval) {
    72         return $min_interval;
    73     }
    74     if ($avg_duration >= $max_interval) {
    75         return $max_interval;
    76     }
    77     return dfehc_calculate_recommended_interval($avg_duration, $load_average, 0);
    78 }
    79 
    80 function dfehc_smooth_moving(array $vals): float
    81 {
    82     $window = 5;
    83     if (!$vals) {
    84         return 0;
    85     }
    86     $subset = array_slice($vals, -$window);
    87     return array_sum($subset) / count($subset);
    88 }
  • dynamic-front-end-heartbeat-control/trunk/engine/server-load.php

    r3310561 r3320647  
    22declare(strict_types=1);
    33
    4 if (!defined('DFEHC_SERVER_LOAD_TTL')) {
    5     define('DFEHC_SERVER_LOAD_TTL', (int) apply_filters('dfehc_server_load_ttl', 3 * MINUTE_IN_SECONDS));
     4if (!function_exists('str_starts_with')) {
     5    function str_starts_with(string $haystack, string $needle): bool
     6    {
     7        return $needle === '' || strpos($haystack, $needle) === 0;
     8    }
     9}
     10
     11define('DFEHC_SERVER_LOAD_TTL', (int) apply_filters('dfehc_server_load_ttl', 180));
     12define('DFEHC_UNKNOWN_LOAD', 0.404);
     13define('DFEHC_SERVER_LOAD_CACHE_KEY', 'dfehc:server_load');
     14define('DFEHC_SERVER_LOAD_PAYLOAD_KEY', 'dfehc_server_load_payload');
     15define('DFEHC_LOAD_LOCK', 'dfehc_load_lock');
     16
     17function _dfehc_get_cache_client(): array
     18{
     19    static $cached = null;
     20    static $ts = 0;
     21    $retryAfter = (int) apply_filters('dfehc_cache_retry_after', 60);
     22    if ($cached !== null && ($cached['type'] !== 'none' || $ts > time() - $retryAfter)) {
     23        return $cached;
     24    }
     25    $ts = time();
     26    global $wp_object_cache;
     27    if (is_object($wp_object_cache) && isset($wp_object_cache->redis) && $wp_object_cache->redis instanceof Redis) {
     28        return $cached = ['client' => $wp_object_cache->redis, 'type' => 'redis'];
     29    }
     30    if (is_object($wp_object_cache) && isset($wp_object_cache->mc) && $wp_object_cache->mc instanceof Memcached) {
     31        return $cached = ['client' => $wp_object_cache->mc, 'type' => 'memcached'];
     32    }
     33    if (class_exists('Redis')) {
     34        try {
     35            $redis = new Redis();
     36            if ($redis->connect(dfehc_get_redis_server(), dfehc_get_redis_port(), 1.0)) {
     37                $pass = apply_filters('dfehc_redis_auth', getenv('REDIS_PASSWORD'));
     38                if ($pass) {
     39                    $redis->auth($pass);
     40                }
     41                return $cached = ['client' => $redis, 'type' => 'redis'];
     42            }
     43        } catch (Throwable $e) {
     44            trigger_error('DFEHC Redis connect error: ' . $e->getMessage(), E_USER_WARNING);
     45        }
     46    }
     47    if (class_exists('Memcached')) {
     48        try {
     49            $mc = new Memcached();
     50            $mc->addServer(dfehc_get_memcached_server(), dfehc_get_memcached_port());
     51            $user = getenv('MEMCACHED_USERNAME');
     52            $pass = getenv('MEMCACHED_PASSWORD');
     53            if ($user && $pass && method_exists($mc, 'setSaslAuthData')) {
     54                $mc->setOption(Memcached::OPT_BINARY_PROTOCOL, true);
     55                $mc->setSaslAuthData($user, $pass);
     56            }
     57            $versions = $mc->getVersion();
     58            $ok       = $versions && is_array($versions) && reset($versions) && reset($versions) !== '0.0.0';
     59            if ($ok) {
     60                return $cached = ['client' => $mc, 'type' => 'memcached'];
     61            }
     62        } catch (Throwable $e) {
     63            trigger_error('DFEHC Memcached connect error: ' . $e->getMessage(), E_USER_WARNING);
     64        }
     65    }
     66    return $cached = ['client' => null, 'type' => 'none'];
     67}
     68
     69function dfehc_cache_server_load(float $value): void
     70{
     71    ['client' => $client, 'type' => $type] = _dfehc_get_cache_client();
     72    if (!$client) {
     73        return;
     74    }
     75    try {
     76        if ($type === 'redis') {
     77            $client->setex(DFEHC_SERVER_LOAD_CACHE_KEY, DFEHC_SERVER_LOAD_TTL, $value);
     78        } elseif ($type === 'memcached') {
     79            $client->set(DFEHC_SERVER_LOAD_CACHE_KEY, $value, DFEHC_SERVER_LOAD_TTL);
     80        }
     81    } catch (Throwable $e) {
     82        trigger_error('DFEHC cache write error: ' . $e->getMessage(), E_USER_WARNING);
     83    }
    684}
    785
    886function dfehc_get_server_load(): float
    987{
    10     $key = 'dfehc_server_load_raw';
    11     $payload = get_transient($key);
    12 
    13     if (is_array($payload) && array_key_exists('load', $payload) && array_key_exists('source', $payload)) {
    14         $load = (float) $payload['load'];
    15         $source = (string) $payload['source'];
    16     } else {
    17         $data = dfehc_detect_load_raw_with_source();
    18         $raw = $data['load'];
    19         $source = $data['source'];
    20         $cores = dfehc_get_cpu_cores();
    21         $load = ($source === 'cpu_load' && $cores > 0) ? $raw / $cores : $raw;
    22         set_transient($key, ['load' => $load, 'source' => $source], DFEHC_SERVER_LOAD_TTL);
    23     }
    24 
     88    $payload = get_transient(DFEHC_SERVER_LOAD_PAYLOAD_KEY);
     89    if (!(is_array($payload) && isset($payload['raw'], $payload['cores'], $payload['source']))) {
     90        $lock = dfehc_acquire_lock();
     91        if (!$lock) {
     92            return DFEHC_UNKNOWN_LOAD;
     93        }
     94        $data    = dfehc_detect_load_raw_with_source();
     95        $payload = [
     96            'raw'    => (float) $data['load'],
     97            'cores'  => dfehc_get_cpu_cores(),
     98            'source' => (string) $data['source'],
     99        ];
     100        set_transient(DFEHC_SERVER_LOAD_PAYLOAD_KEY, $payload, DFEHC_SERVER_LOAD_TTL);
     101        dfehc_release_lock($lock);
     102    }
     103    $raw    = (float) $payload['raw'];
     104    $cores  = (int) ($payload['cores'] ?: dfehc_get_cpu_cores());
     105    $source = (string) $payload['source'];
     106    $divide = (bool) apply_filters('dfehc_divide_cpu_load', true, $raw, $cores, $source);
     107    $load   = ($source === 'cpu_load' && $divide && $cores > 0) ? $raw / $cores : $raw;
    25108    return (float) apply_filters('dfehc_contextual_load_value', $load, $source);
    26109}
     
    34117        }
    35118    }
    36 
    37119    if (is_readable('/proc/loadavg')) {
    38120        $txt = file_get_contents('/proc/loadavg');
     
    44126        }
    45127    }
    46 
    47128    $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
    48     if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true)) {
    49         $out = shell_exec('LANG=C uptime');
     129    if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true) && !ini_get('open_basedir')) {
     130        $out = shell_exec('LANG=C uptime 2>&1');
    50131        if ($out && preg_match('/load average: ([0-9.]+)/', $out, $m)) {
    51132            return ['load' => (float) $m[1], 'source' => 'cpu_load'];
    52133        }
    53134    }
    54 
    55135    if (defined('DFEHC_PLUGIN_PATH')) {
    56136        $est = DFEHC_PLUGIN_PATH . 'defibrillator/load-estimator.php';
     
    58138            require_once $est;
    59139            if (class_exists('DynamicHeartbeat\\Dfehc_ServerLoadEstimator')) {
    60                 return ['load' => (float) DynamicHeartbeat\Dfehc_ServerLoadEstimator::get_server_load(), 'source' => 'fallback'];
    61             }
    62         }
    63     }
    64 
    65     return ['load' => 0.404, 'source' => 'fallback'];
     140                return [
     141                    'load'   => (float) DynamicHeartbeat\Dfehc_ServerLoadEstimator::get_server_load(),
     142                    'source' => 'fallback',
     143                ];
     144            }
     145        }
     146    }
     147    return ['load' => DFEHC_UNKNOWN_LOAD, 'source' => 'fallback'];
    66148}
    67149
     
    69151{
    70152    if (is_readable('/sys/fs/cgroup/cpu.max')) {
    71         [$quota, $period] = array_map('intval', explode(' ', trim((string) file_get_contents('/sys/fs/cgroup/cpu.max'))));
    72         if ($quota > 0 && $period > 0) {
    73             return max(1, (int) ceil($quota / $period));
    74         }
    75     }
    76 
    77     $cg = '/proc/self/cgroup';
    78     if (is_readable($cg) && preg_match('/^[0-9]+:[^:]*cpu[^:]*:(.+)$/m', file_get_contents($cg) ?: '', $m)) {
    79         $base = '/sys/fs/cgroup' . rtrim($m[1]);
    80         if (is_readable("$base/cpu.cfs_quota_us") && is_readable("$base/cpu.cfs_period_us")) {
    81             $quota = (int) file_get_contents("$base/cpu.cfs_quota_us");
    82             $period = (int) file_get_contents("$base/cpu.cfs_period_us");
     153        [$quota, $period] = explode(' ', trim(file_get_contents('/sys/fs/cgroup/cpu.max')));
     154        if ($quota !== 'max') {
     155            $quota  = (int) $quota;
     156            $period = (int) $period;
    83157            if ($quota > 0 && $period > 0) {
    84158                return max(1, (int) ceil($quota / $period));
     
    86160        }
    87161    }
    88 
     162    if (is_readable('/proc/self/cgroup')) {
     163        $content = file_get_contents('/proc/self/cgroup');
     164        if ($content !== false && preg_match('/^[0-9]+:[^:]*cpu[^:]*:(.+)$/m', $content, $m)) {
     165            $path       = '/' . ltrim(trim($m[1]), '/');
     166            $base       = '/sys/fs/cgroup' . $path;
     167            $quotaFile  = "$base/cpu.cfs_quota_us";
     168            $periodFile = "$base/cpu.cfs_period_us";
     169            if (is_readable($quotaFile) && is_readable($periodFile)) {
     170                $quota  = (int) file_get_contents($quotaFile);
     171                $period = (int) file_get_contents($periodFile);
     172                if ($quota > 0 && $period > 0) {
     173                    return max(1, (int) ceil($quota / $period));
     174                }
     175            }
     176        }
     177    }
    89178    if (is_readable('/sys/fs/cgroup/cpu/cpu.cfs_quota_us') && is_readable('/sys/fs/cgroup/cpu/cpu.cfs_period_us')) {
    90         $quota = (int) file_get_contents('/sys/fs/cgroup/cpu/cpu.cfs_quota_us');
     179        $quota  = (int) file_get_contents('/sys/fs/cgroup/cpu/cpu.cfs_quota_us');
    91180        $period = (int) file_get_contents('/sys/fs/cgroup/cpu/cpu.cfs_period_us');
    92181        if ($quota > 0 && $period > 0) {
     
    94183        }
    95184    }
    96 
    97185    $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
    98     if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true)) {
     186    if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true) && !ini_get('open_basedir')) {
    99187        $n = shell_exec('nproc 2>/dev/null');
    100188        if ($n && ctype_digit(trim($n))) {
     
    102190        }
    103191    }
    104 
    105192    if (is_readable('/proc/cpuinfo')) {
    106         $cnt = preg_match_all('/^processor/m', file_get_contents('/proc/cpuinfo') ?: '');
    107         if ($cnt) {
    108             return $cnt;
    109         }
    110     }
    111 
     193        $info = file_get_contents('/proc/cpuinfo');
     194        if ($info !== false) {
     195            $cnt = preg_match_all('/^processor/m', $info);
     196            if ($cnt) {
     197                return $cnt;
     198            }
     199        }
     200    }
    112201    return 1;
    113202}
     
    115204function dfehc_log_server_load(): void
    116205{
    117     $load = dfehc_get_server_load();
    118     $logs = get_option('dfehc_server_load_logs', []);
     206    $load   = dfehc_get_server_load();
     207    $optKey = 'dfehc_server_load_logs';
     208    $logs   = get_site_option($optKey, []);
    119209    if (!is_array($logs)) {
    120210        $logs = [];
    121211    }
    122 
    123     $now = time();
     212    $now    = time();
    124213    $cutoff = $now - DAY_IN_SECONDS;
    125     $logs = array_filter($logs, static fn(array $row): bool => (isset($row['timestamp']) && $row['timestamp'] >= $cutoff));
     214    $logs   = array_filter(
     215        $logs,
     216        static fn(array $row): bool => isset($row['timestamp']) && $row['timestamp'] >= $cutoff
     217    );
    126218    $logs[] = ['timestamp' => $now, 'load' => $load];
    127     update_option('dfehc_server_load_logs', array_values($logs), false);
     219    update_site_option($optKey, array_values($logs), false);
    128220}
    129221add_action('dfehc_log_server_load_hook', 'dfehc_log_server_load');
     
    131223function dfehc_get_server_load_ajax_handler(): void
    132224{
    133     if (!isset($_REQUEST['nonce']) || !wp_verify_nonce((string) $_REQUEST['nonce'], 'dfehc-ajax-nonce')) {
     225    $nonce = $_POST['nonce'] ?? $_GET['nonce'] ?? '';
     226    if (!wp_verify_nonce((string) $nonce, 'dfehc-ajax-nonce')) {
    134227        wp_send_json_error('Invalid nonce.');
    135228    }
    136 
    137229    if (!is_user_logged_in() && !apply_filters('dfehc_allow_public_server_load', false)) {
    138230        wp_send_json_error('Not authorised.');
    139231    }
    140 
    141     wp_send_json_success(dfehc_get_server_load());
     232    wp_send_json_success(dfehc_get_server_load_persistent());
    142233}
    143234add_action('wp_ajax_get_server_load', 'dfehc_get_server_load_ajax_handler');
     
    150241        return $cached;
    151242    }
    152 
    153     $redis_available = class_exists('Redis');
    154     $memcached_available = class_exists('Memcached');
    155 
    156     try {
    157         if ($redis_available) {
    158             $redis = new Redis();
    159             if ($redis->connect(dfehc_get_redis_server(), dfehc_get_redis_port())) {
    160                 $val = $redis->get('dfehc:server_load');
    161                 $redis->close();
    162                 return $cached = (float) $val;
    163             }
    164         }
    165 
    166         if ($memcached_available) {
    167             $mc = new Memcached();
    168             if ($mc->addServer(dfehc_get_memcached_server(), dfehc_get_memcached_port())) {
    169                 $val = $mc->get('dfehc:server_load');
    170                 $mc->quit();
    171                 return $cached = (float) $val;
    172             }
    173         }
    174     } catch (Throwable $e) {
    175         if (defined('WP_DEBUG') && WP_DEBUG) {
    176             error_log('[dfehc] ' . $e->getMessage());
    177         }
    178     }
    179 
    180     return $cached = 0.404;
     243    ['client' => $client, 'type' => $type] = _dfehc_get_cache_client();
     244    $val = false;
     245    if ($client) {
     246        try {
     247            $val = $client->get(DFEHC_SERVER_LOAD_CACHE_KEY);
     248        } catch (Throwable $e) {
     249            trigger_error('DFEHC cache read error: ' . $e->getMessage(), E_USER_WARNING);
     250        }
     251    }
     252    if ($val !== false) {
     253        return $cached = (float) $val;
     254    }
     255    $fresh = dfehc_get_server_load();
     256    dfehc_cache_server_load($fresh);
     257    return $cached = $fresh;
    181258}
    182259
     
    186263        $schedules['dfehc_minute'] = [
    187264            'interval' => 60,
    188             'display' => __('Every minute (DFEHC)', 'dfehc'),
     265            'display'  => __('Server load (DFEHC)', 'dfehc'),
    189266        ];
    190267    }
     
    193270add_filter('cron_schedules', 'dfehc_register_minute_schedule');
    194271
    195 if (!wp_next_scheduled('dfehc_log_server_load_hook')) {
    196     wp_schedule_event(time(), 'dfehc_minute', 'dfehc_log_server_load_hook');
    197 }
     272if (apply_filters('dfehc_enable_load_logging', true)) {
     273    if (!wp_next_scheduled('dfehc_log_server_load_hook')) {
     274        $start = time() - (time() % 60) + 60;
     275        wp_schedule_event($start, 'dfehc_minute', 'dfehc_log_server_load_hook');
     276    }
     277}
  • dynamic-front-end-heartbeat-control/trunk/engine/server-response.php

    r3310561 r3320647  
    22declare(strict_types=1);
    33
     4defined('DFEHC_DEFAULT_RESPONSE_TIME') || define('DFEHC_DEFAULT_RESPONSE_TIME', 50.0);
     5defined('DFEHC_HEAD_NEG_TTL')          || define('DFEHC_HEAD_NEG_TTL', 600);
     6defined('DFEHC_HEAD_POS_TTL')          || define('DFEHC_HEAD_POS_TTL', WEEK_IN_SECONDS);
     7defined('DFEHC_SPIKE_OPT_EPS')         || define('DFEHC_SPIKE_OPT_EPS', 0.1);
     8defined('DFEHC_BASELINE_EXP')          || define('DFEHC_BASELINE_EXP', 7 * DAY_IN_SECONDS);
     9
    410function dfehc_get_server_response_time(): array
    511{
    6     $default_ms = defined('DFEHC_DEFAULT_RESPONSE_TIME') ? DFEHC_DEFAULT_RESPONSE_TIME : apply_filters('dfehc_default_response_time', 50.0);
     12    $default_ms = (float) apply_filters('dfehc_default_response_time', DFEHC_DEFAULT_RESPONSE_TIME);
     13
     14    $defaults = [
     15        'main_response_ms' => null,
     16        'db_response_ms'   => null,
     17        'method'           => '',
     18        'measurements'     => [],
     19        'recalibrated'     => false,
     20        'timestamp'        => current_time('mysql'),
     21        'baseline_used'    => null,
     22        'spike_score'      => 0.0,
     23    ];
    724
    825    $cached = get_transient('dfehc_cached_response_data');
    926    if ($cached !== false && is_array($cached)) {
    10         return $cached;
     27        return array_merge($defaults, $cached);
    1128    }
    1229
    1330    if (dfehc_is_high_traffic()) {
    14         return [
     31        $high = [
    1532            'main_response_ms' => $default_ms,
    1633            'db_response_ms'   => null,
     
    1936            'recalibrated'     => false,
    2037            'timestamp'        => current_time('mysql'),
     38            'baseline_used'    => null,
     39            'spike_score'      => 0.0,
    2140        ];
     41        set_transient('dfehc_cached_response_data', $high, (int) apply_filters('dfehc_high_traffic_cache_expiration', 300));
     42        return $high;
    2243    }
    2344
    2445    if (!dfehc_acquire_lock()) {
    25         return $cached ?: [];
     46        return array_merge($defaults, is_array($cached) ? $cached : []);
    2647    }
    2748
    2849    $results  = dfehc_perform_response_measurements($default_ms);
    2950    $baseline = get_transient('dfehc_baseline_response_data');
    30     $spike    = (float) get_option('dfehc_spike_score', 0.0);
     51    $spike    = (float) get_transient('dfehc_spike_score');
    3152    $now      = time();
    32     $max_age  = apply_filters('dfehc_max_baseline_age', 7 * DAY_IN_SECONDS);
     53    $max_age  = (int) apply_filters('dfehc_max_baseline_age', DFEHC_BASELINE_EXP);
    3354
    3455    if (is_array($baseline)) {
     
    4162
    4263    if ($baseline === false && $results['method'] === 'http_loopback' && $results['main_response_ms'] !== null) {
    43         $exp                        = apply_filters('dfehc_baseline_expiration', 7 * DAY_IN_SECONDS);
    44         $results['timestamp']       = current_time('mysql');
     64        $exp                  = (int) apply_filters('dfehc_baseline_expiration', DFEHC_BASELINE_EXP);
     65        $results['timestamp'] = current_time('mysql');
    4566        set_transient('dfehc_baseline_response_data', $results, $exp);
    4667        $baseline = $results;
     
    5172
    5273    if (is_array($baseline) && $results['method'] === 'http_loopback' && isset($results['main_response_ms'], $baseline['main_response_ms'])) {
    53         $base_ms   = (float) $baseline['main_response_ms'];
     74        $base_ms   = max(1.0, (float) $baseline['main_response_ms']);
    5475        $curr_ms   = (float) $results['main_response_ms'];
    55         $factor    = apply_filters('dfehc_spike_threshold_factor', 2.0);
    56         $increment = apply_filters('dfehc_spike_increment', 1.0);
    57         $decay     = apply_filters('dfehc_spike_decay', 0.25);
    58         $threshold = apply_filters('dfehc_recalibrate_threshold', 5.0);
     76        $factor    = (float) apply_filters('dfehc_spike_threshold_factor', 2.0);
     77        $increment = (float) apply_filters('dfehc_spike_increment', max(1.0, $curr_ms / $base_ms - $factor));
     78        $decay     = (float) apply_filters('dfehc_spike_decay', 0.25);
     79        $threshold = (float) apply_filters('dfehc_recalibrate_threshold', 5.0);
    5980
    6081        if ($curr_ms > $base_ms * $factor) {
     
    6586
    6687        if ($spike >= $threshold) {
    67             set_transient('dfehc_baseline_response_data', $results, apply_filters('dfehc_baseline_expiration', 7 * DAY_IN_SECONDS));
    68             $spike                     = 0.0;
    69             $results['recalibrated']   = true;
     88            set_transient('dfehc_baseline_response_data', $results, (int) apply_filters('dfehc_baseline_expiration', DFEHC_BASELINE_EXP));
     89            $spike                   = 0.0;
     90            $results['recalibrated'] = true;
    7091        }
    7192    }
    7293
    7394    $results['spike_score'] = $spike;
    74     update_option('dfehc_spike_score', $spike);
    75 
    76     set_transient('dfehc_cached_response_data', $results, apply_filters('dfehc_cache_expiration', 3 * MINUTE_IN_SECONDS));
     95    $prev_spike             = (float) get_transient('dfehc_spike_score');
     96
     97    if (abs($spike - $prev_spike) >= DFEHC_SPIKE_OPT_EPS) {
     98        set_transient('dfehc_spike_score', $spike, DFEHC_BASELINE_EXP);
     99    }
     100
     101    set_transient('dfehc_cached_response_data', $results, (int) apply_filters('dfehc_cache_expiration', 3 * MINUTE_IN_SECONDS));
    77102
    78103    dfehc_release_lock();
    79104
    80     return $results;
     105    return array_merge($defaults, $results);
    81106}
    82107
     
    94119    global $wpdb;
    95120    $db_start = microtime(true);
    96     if ($wpdb->get_var('SELECT 1') !== null) {
    97         $r['db_response_ms'] = (microtime(true) - $db_start) * 1000;
    98     }
    99 
    100     $times = [];
    101     $url   = get_rest_url() ?: home_url('/wp-json/');
     121    $wpdb->get_var("SELECT option_value FROM {$wpdb->options} LIMIT 1");
     122    $r['db_response_ms'] = (microtime(true) - $db_start) * 1000;
     123
     124    $url = get_rest_url() ?: home_url('/wp-json/');
    102125    if (!get_option('permalink_structure')) {
    103         $url = home_url('/index.php?rest_route=/');
    104     }
    105 
    106     $n            = max(1, min((int) apply_filters('dfehc_num_requests', 3), 10));
     126        $url = add_query_arg('rest_route', '/', home_url('/index.php'));
     127    }
     128
     129    $n            = max(1, min((int) apply_filters('dfehc_num_requests', 2), 5));
    107130    $sleep_us     = (int) apply_filters('dfehc_request_pause_us', 50000);
    108131    $timeout      = (int) apply_filters('dfehc_request_timeout', 10);
    109     $get_fallback = apply_filters('dfehc_use_get_fallback', true);
    110 
    111     $args = [
    112         'timeout'   => $timeout,
    113         'sslverify' => apply_filters('dfehc_ssl_verify', true),
    114         'headers'   => ['Cache-Control' => 'no-cache'],
    115     ];
     132    $sslverify    = (bool) apply_filters('dfehc_ssl_verify', true);
     133    $get_fallback = (bool) apply_filters('dfehc_use_get_fallback', true);
     134    $use_head     = (bool) apply_filters('dfehc_use_head_method', true);
     135
     136    $head_key       = 'dfehc_head_supported_' . md5($url);
     137    $head_supported = get_transient($head_key);
     138    if ($head_supported === false) {
     139        $head_supported = null;
     140    }
     141
     142    $times         = [];
     143    $hard_deadline = microtime(true) + (float) apply_filters('dfehc_total_timeout', $timeout + 2);
    116144
    117145    for ($i = 0; $i < $n; $i++) {
     146        $remaining = $hard_deadline - microtime(true);
     147        if ($remaining <= 0) {
     148            break;
     149        }
     150
     151        $probe_url = add_query_arg('_dfehc_ts', sprintf('%.6f', microtime(true)), $url);
     152
     153        $args = [
     154            'timeout'   => (int) ceil($remaining),
     155            'sslverify' => $sslverify,
     156            'headers'   => ['Cache-Control' => 'no-cache'],
     157        ];
     158
    118159        $start = microtime(true);
    119         $resp  = wp_remote_head($url, $args);
    120 
    121         if (is_wp_error($resp) && $get_fallback) {
    122             $resp = wp_remote_get($url, $args);
     160        $resp  = null;
     161
     162        if ($use_head && $head_supported !== false) {
     163            $resp = wp_remote_head($probe_url, $args);
     164            if (is_wp_error($resp) || wp_remote_retrieve_response_code($resp) >= 400) {
     165                if ($head_supported === null) {
     166                    set_transient($head_key, 0, (int) apply_filters('dfehc_head_negative_ttl', DFEHC_HEAD_NEG_TTL));
     167                }
     168                $resp = null;
     169            } else {
     170                if ($head_supported === null) {
     171                    set_transient($head_key, 1, DFEHC_HEAD_POS_TTL);
     172                }
     173            }
     174        }
     175
     176        if ($resp === null && $get_fallback) {
     177            $resp = wp_remote_get($probe_url, $args);
    123178        }
    124179
     
    128183        }
    129184
     185        if (count($times) >= $n) {
     186            break;
     187        }
     188
    130189        if ($i < $n - 1 && $sleep_us > 0) {
    131190            usleep($sleep_us);
     
    135194    if ($times) {
    136195        sort($times);
    137         $cnt                  = count($times);
    138         $r['measurements']    = $times;
     196        $cnt                   = count($times);
     197        $r['measurements']     = $times;
    139198        $r['main_response_ms'] = $cnt % 2 ? $times[intdiv($cnt, 2)] : ($times[$cnt / 2 - 1] + $times[$cnt / 2]) / 2;
    140199    } else {
     
    154213function dfehc_is_high_traffic(): bool
    155214{
     215    $flag_key = 'dfehc_high_traffic_flag';
     216    $flag     = get_transient($flag_key);
     217    if ($flag !== false) {
     218        return (bool) $flag;
     219    }
     220
    156221    $threshold = (int) apply_filters('dfehc_high_traffic_threshold', 100);
    157     $count     = function_exists('dfehc_get_website_visitors') ? (int) dfehc_get_website_visitors() : 0;
    158     return $count >= $threshold;
     222    $cnt_key   = 'dfehc_cached_visitor_cnt';
     223    $count     = get_transient($cnt_key);
     224    if ($count === false) {
     225        $count = function_exists('dfehc_get_website_visitors') && is_numeric(dfehc_get_website_visitors()) ? (int) dfehc_get_website_visitors() : 0;
     226        set_transient($cnt_key, $count, 60);
     227    }
     228
     229    $high = $count >= $threshold;
     230    set_transient($flag_key, $high ? 1 : 0, 60);
     231
     232    return $high;
    159233}
    160234
    161235function dfehc_acquire_lock(): bool
    162236{
    163     $ttl = (int) apply_filters('dfehc_measurement_lock_ttl', 60);
    164 
    165     if (wp_using_ext_object_cache()) {
    166         return wp_cache_add('dfehc_measurement_lock', 1, '', $ttl);
    167     }
    168 
    169     global $wpdb;
    170     $added = $wpdb->query($wpdb->prepare("INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %d, 'no')", 'dfehc_measurement_lock', time()));
    171     if ($added) {
     237    $key = 'dfehc_measure_lock_' . get_current_blog_id();
     238
     239    if (class_exists('WP_Lock')) {
     240        $lock = new WP_Lock($key, 60);
     241        if ($lock->acquire()) {
     242            $GLOBALS['dfehc_rt_lock'] = $lock;
     243            return true;
     244        }
     245        return false;
     246    }
     247
     248    if (function_exists('wp_cache_add') && wp_cache_add($key, 1, '', 60)) {
     249        $GLOBALS['dfehc_rt_lock_cache_key'] = $key;
    172250        return true;
    173251    }
    174252
    175     $value = (int) $wpdb->get_var($wpdb->prepare("SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", 'dfehc_measurement_lock'));
    176     if ($value < time() - $ttl) {
    177         $wpdb->update($wpdb->options, ['option_value' => time()], ['option_name' => 'dfehc_measurement_lock']);
     253    if (false !== get_transient($key)) {
     254        return false;
     255    }
     256
     257    if (set_transient($key, 1, 60)) {
     258        $GLOBALS['dfehc_rt_lock_transient_key'] = $key;
    178259        return true;
    179260    }
     
    184265function dfehc_release_lock(): void
    185266{
    186     if (wp_using_ext_object_cache()) {
    187         wp_cache_delete('dfehc_measurement_lock');
    188     } else {
    189         delete_option('dfehc_measurement_lock');
    190     }
    191 }
     267    if (isset($GLOBALS['dfehc_rt_lock']) && $GLOBALS['dfehc_rt_lock'] instanceof WP_Lock) {
     268        $GLOBALS['dfehc_rt_lock']->release();
     269        unset($GLOBALS['dfehc_rt_lock']);
     270        return;
     271    }
     272
     273    if (isset($GLOBALS['dfehc_rt_lock_cache_key'])) {
     274        wp_cache_delete($GLOBALS['dfehc_rt_lock_cache_key']);
     275        unset($GLOBALS['dfehc_rt_lock_cache_key']);
     276        return;
     277    }
     278
     279    if (isset($GLOBALS['dfehc_rt_lock_transient_key'])) {
     280        delete_transient($GLOBALS['dfehc_rt_lock_transient_key']);
     281        unset($GLOBALS['dfehc_rt_lock_transient_key']);
     282    }
     283}
  • dynamic-front-end-heartbeat-control/trunk/engine/system-load-fallback.php

    r3310561 r3320647  
    11<?php
    22declare(strict_types=1);
     3
     4defined('DFEHC_SERVER_LOAD_TTL')   || define('DFEHC_SERVER_LOAD_TTL', 60);
     5defined('DFEHC_SENTINEL_NO_LOAD')  || define('DFEHC_SENTINEL_NO_LOAD', 0.404);
     6defined('DFEHC_SYSTEM_LOAD_KEY')   || define('DFEHC_SYSTEM_LOAD_KEY', 'dfehc_system_load_avg');
     7
     8if (!function_exists('dfehc_get_cpu_cores')) {
     9    function dfehc_get_cpu_cores(): int
     10    {
     11        static $cached = null;
     12        if ($cached !== null) {
     13            return $cached;
     14        }
     15
     16        $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
     17
     18        if (PHP_OS_FAMILY !== 'Windows'
     19            && function_exists('shell_exec')
     20            && !in_array('shell_exec', $disabled, true)
     21            && !ini_get('open_basedir')
     22        ) {
     23            $val = trim((string) shell_exec('getconf _NPROCESSORS_ONLN 2>NUL'));
     24            if (ctype_digit($val) && (int) $val > 0) {
     25                return $cached = (int) $val;
     26            }
     27        }
     28
     29        if (PHP_OS_FAMILY === 'Windows'
     30            && function_exists('shell_exec')
     31            && !in_array('shell_exec', $disabled, true)
     32        ) {
     33            $out = shell_exec('wmic cpu get NumberOfLogicalProcessors /value 2>NUL');
     34            if ($out && preg_match('/NumberOfLogicalProcessors=(\d+)/i', $out, $m) && (int) $m[1] > 0) {
     35                return $cached = (int) $m[1];
     36            }
     37        }
     38
     39        if (is_readable('/proc/cpuinfo')) {
     40            $cnt = preg_match_all('/^processor/m', (string) file_get_contents('/proc/cpuinfo'));
     41            if ($cnt > 0) {
     42                return $cached = $cnt;
     43            }
     44        }
     45
     46        return $cached = 1;
     47    }
     48}
    349
    450if (!function_exists('dfehc_get_system_load_average')) {
    551    function dfehc_get_system_load_average(): float
    652    {
    7         $key   = 'dfehc_system_load_avg';
     53        $ttl   = (int) apply_filters('dfehc_system_load_ttl', DFEHC_SERVER_LOAD_TTL);
     54        $key   = DFEHC_SYSTEM_LOAD_KEY;
    855        $cache = get_transient($key);
    956        if ($cache !== false) {
     
    1158        }
    1259
     60        $raw = null;
     61
    1362        if (function_exists('dfehc_get_server_load')) {
    14             $load = dfehc_get_server_load();
    15             if ($load !== 0.404) {
    16                 dfehc_set_transient_noautoload($key, $load, DFEHC_SERVER_LOAD_TTL);
    17                 return $load;
     63            $val = dfehc_get_server_load();
     64            if ($val !== DFEHC_SENTINEL_NO_LOAD) {
     65                dfehc_set_transient_noautoload($key, $val, $ttl);
     66                return (float) $val;
    1867            }
    1968        }
    2069
    21         $raw = 0.0;
    22 
    2370        if (function_exists('sys_getloadavg')) {
    2471            $arr = sys_getloadavg();
    25             $raw = $arr[0] ?? 0.0;
    26         } elseif (is_readable('/proc/loadavg')) {
    27             $raw = (float) explode(' ', file_get_contents('/proc/loadavg'))[0];
    28         } else {
     72            if ($arr && isset($arr[0])) {
     73                $raw = (float) $arr[0];
     74            }
     75        }
     76
     77        if ($raw === null && PHP_OS_FAMILY !== 'Windows' && is_readable('/proc/loadavg')) {
     78            $parts = explode(' ', (string) file_get_contents('/proc/loadavg'));
     79            if (isset($parts[0])) {
     80                $raw = (float) $parts[0];
     81            }
     82        }
     83
     84        if ($raw === null) {
    2985            $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
    3086            if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true)) {
    31                 $out = shell_exec('LANG=C uptime');
    32                 if ($out && preg_match('/load average: ([0-9.]+)/', $out, $m)) {
    33                     $raw = (float) $m[1];
     87                if (PHP_OS_FAMILY === 'Windows') {
     88                    $out = shell_exec('wmic cpu get loadpercentage /value 2>NUL');
     89                    if ($out && preg_match('/loadpercentage=(\d+)/i', $out, $m)) {
     90                        $raw = ((float) $m[1]) / 100.0 * dfehc_get_cpu_cores();
     91                    }
     92                } else {
     93                    $out = shell_exec('LANG=C uptime 2>/dev/null');
     94                    if ($out && preg_match('/load average[s]?:\s*([0-9.]+)/', $out, $m)) {
     95                        $raw = (float) $m[1];
     96                    }
    3497                }
    3598            }
    3699        }
    37100
    38         if ($raw === 0.0 && class_exists('\\DynamicHeartbeat\\Dfehc_ServerLoadEstimator')) {
     101        if ($raw === null && class_exists('\\DynamicHeartbeat\\Dfehc_ServerLoadEstimator')) {
    39102            $raw = (float) \DynamicHeartbeat\Dfehc_ServerLoadEstimator::get_server_load();
    40103        }
    41         if ($raw === 0.0) {
    42             $raw = 0.404;
     104
     105        if ($raw === null) {
     106            $sentinel_ttl = (int) apply_filters('dfehc_sentinel_ttl', 5);
     107            dfehc_set_transient_noautoload($key, DFEHC_SENTINEL_NO_LOAD, $sentinel_ttl);
     108            return DFEHC_SENTINEL_NO_LOAD;
    43109        }
    44110
    45         if ($raw <= 100) {
     111        $divide = (bool) apply_filters('dfehc_divide_load_by_cores', true, $raw);
     112        if ($divide) {
    46113            $cores = dfehc_get_cpu_cores();
    47             $raw   = $cores > 0 ? $raw / $cores : $raw;
     114            if ($cores > 0) {
     115                $raw = $raw / $cores;
     116            }
    48117        }
    49118
    50         dfehc_set_transient_noautoload($key, $raw, DFEHC_SERVER_LOAD_TTL);
     119        dfehc_set_transient_noautoload($key, $raw, $ttl);
    51120        return (float) $raw;
    52121    }
  • dynamic-front-end-heartbeat-control/trunk/heartbeat-async.php

    r3310561 r3320647  
    55define('DFEHC_MIN_INTERVAL', 15);
    66define('DFEHC_MAX_INTERVAL', 300);
     7define('DFEHC_FALLBACK_INTERVAL', 60);
    78define('DFEHC_LOAD_AVERAGES', 'dfehc_load_averages');
    89define('DFEHC_SERVER_LOAD', 'dfehc_server_load');
    910define('DFEHC_RECOMMENDED_INTERVAL', 'dfehc_recommended_interval');
    1011define('DFEHC_CAPABILITY', 'read');
     12define('DFEHC_LOAD_LOCK_BASE', 'dfehc_compute_load_lock');
     13
     14function dfehc_store_lockfree(string $key, $value, int $ttl): bool
     15{
     16    if (function_exists('wp_cache_add') && wp_cache_add($key, $value, '', $ttl)) {
     17        return true;
     18    }
     19    return set_transient($key, $value, $ttl);
     20}
    1121
    1222function dfehc_register_ajax(string $action, callable $callback): void
    1323{
    1424    add_action("wp_ajax_$action", $callback);
    15     add_action("wp_ajax_nopriv_$action", $callback);
     25    if (apply_filters('dfehc_allow_public_server_load', false)) {
     26        add_action("wp_ajax_nopriv_$action", $callback);
     27    }
    1628}
    1729
     
    2234        return;
    2335    }
    24     set_transient($key, $value, $expiration);
     36    dfehc_store_lockfree($key, $value, $expiration);
    2537    global $wpdb;
    26     $wpdb->update(
    27         $wpdb->options,
    28         ['autoload' => 'no'],
    29         ['option_name' => "_transient_$key"]
     38    $wpdb->query(
     39        $wpdb->prepare(
     40            "UPDATE {$wpdb->options} SET autoload='no' WHERE option_name=%s AND autoload='yes' LIMIT 1",
     41            "_transient_$key"
     42        )
    3043    );
    3144}
    3245
    33 function dfehc_get_cpu_cores(): int
    34 {
    35     if (function_exists('nproc')) {
    36         $cores = trim((string) shell_exec('nproc 2>/dev/null'));
    37         return is_numeric($cores) ? (int) $cores : 1;
    38     }
    39     if (is_readable('/proc/cpuinfo')) {
    40         return (int) preg_match_all('/^processor/m', (string) file_get_contents('/proc/cpuinfo'));
    41     }
    42     return 1;
     46if (!function_exists('dfehc_get_cpu_cores')) {
     47    function dfehc_get_cpu_cores(): int
     48    {
     49        static $cached = null;
     50        if ($cached !== null) {
     51            return $cached;
     52        }
     53        if (is_readable('/proc/cpuinfo')) {
     54            $cnt = preg_match_all('/^processor/m', (string) file_get_contents('/proc/cpuinfo'));
     55            if ($cnt > 0) {
     56                return $cached = $cnt;
     57            }
     58        }
     59        $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
     60        if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true)) {
     61            $out = (string) (shell_exec('nproc 2>/dev/null') ?? '');
     62            if (ctype_digit(trim($out)) && (int) $out > 0) {
     63                return $cached = (int) trim($out);
     64            }
     65        }
     66        return $cached = 1;
     67    }
     68}
     69
     70function dfehc_acquire_lock(string $base, int $ttl)
     71{
     72    $key = $base . '_' . get_current_blog_id();
     73    if (class_exists('WP_Lock')) {
     74        $lock = new WP_Lock($key, $ttl);
     75        if ($lock->acquire()) {
     76            return $lock;
     77        }
     78        return null;
     79    }
     80    if (function_exists('wp_cache_add') && wp_cache_add($key, 1, '', $ttl)) {
     81        return (object) ['cache_key' => $key];
     82    }
     83    if (get_transient($key) !== false) {
     84        return null;
     85    }
     86    if (set_transient($key, 1, $ttl)) {
     87        return (object) ['transient_key' => $key];
     88    }
     89    return null;
     90}
     91
     92function dfehc_release_lock($lock): void
     93{
     94    if ($lock instanceof WP_Lock) {
     95        $lock->release();
     96        return;
     97    }
     98    if (is_object($lock) && isset($lock->cache_key)) {
     99        wp_cache_delete($lock->cache_key);
     100        return;
     101    }
     102    if (is_object($lock) && isset($lock->transient_key)) {
     103        delete_transient($lock->transient_key);
     104    }
     105}
     106
     107function dfehc_get_or_calculate_server_load(): float|false
     108{
     109    $load = get_transient(DFEHC_SERVER_LOAD);
     110    if ($load !== false) {
     111        return (float) $load;
     112    }
     113    $ttl = (int) apply_filters('dfehc_server_load_ttl', 180);
     114    $lock = dfehc_acquire_lock(DFEHC_LOAD_LOCK_BASE, $ttl + 5);
     115    if (!$lock) {
     116        return false;
     117    }
     118    $raw = dfehc_calculate_server_load();
     119    dfehc_release_lock($lock);
     120    if ($raw === false) {
     121        return false;
     122    }
     123    $cores = max(1, dfehc_get_cpu_cores());
     124    $load_pct = min(100.0, round($raw / $cores * 100, 2));
     125    dfehc_set_transient_noautoload(DFEHC_SERVER_LOAD, $load_pct, $ttl);
     126    return $load_pct;
    43127}
    44128
    45129function dfehc_get_server_load_ajax_handler(): void
    46130{
    47     $nonce = sanitize_text_field(
    48     filter_input(INPUT_POST, 'nonce', FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) ?? ''
    49 );
    50 
    51     if (!wp_verify_nonce($nonce, 'dfehc-ajax-nonce')) {
    52         wp_send_json_error('Heartbeat: Invalid nonce provided.');
    53     }
    54 
     131    $nonce = $_REQUEST['nonce'] ?? '';
     132    if (!wp_verify_nonce((string) $nonce, 'dfehc-ajax-nonce')) {
     133        wp_send_json_error('Heartbeat: Invalid nonce.');
     134    }
     135    $cap = apply_filters('dfehc_required_capability', DFEHC_CAPABILITY);
    55136    $allow_public = apply_filters('dfehc_allow_public_server_load', false);
    56     if (!current_user_can(DFEHC_CAPABILITY) && !$allow_public) {
     137    if (!current_user_can($cap) && !$allow_public) {
    57138        wp_send_json_error('Heartbeat: Not authorised.');
    58139    }
    59 
    60     $load = get_transient(DFEHC_SERVER_LOAD);
    61 
     140    $load = dfehc_get_or_calculate_server_load();
    62141    if ($load === false) {
    63         $load_raw = dfehc_calculate_server_load();
    64         if ($load_raw === false) {
    65             wp_send_json_error('Heartbeat: Failed to determine server load.');
    66         }
    67         $cores = max(1, dfehc_get_cpu_cores());
    68         $load  = round($load_raw / $cores * 100, 2);
    69         dfehc_set_transient_noautoload(DFEHC_SERVER_LOAD, $load, 180);
    70     }
    71 
     142        wp_send_json_success(DFEHC_FALLBACK_INTERVAL);
     143    }
    72144    $interval = dfehc_calculate_recommended_interval_user_activity($load);
    73 
    74     if ($interval > 0) {
    75         wp_send_json_success($interval);
    76     }
    77 
    78     wp_send_json_error('Heartbeat: Interval calculation failed.');
     145    if ($interval <= 0) {
     146        $interval = DFEHC_FALLBACK_INTERVAL;
     147    }
     148    wp_send_json_success($interval);
    79149}
    80150dfehc_register_ajax('get_server_load', 'dfehc_get_server_load_ajax_handler');
    81151
    82 function dfehc_calculate_server_load()
     152function dfehc_calculate_server_load(): float|false
    83153{
    84154    if (function_exists('sys_getloadavg')) {
    85155        $load = sys_getloadavg();
    86         return $load[0];
     156        if (isset($load[0])) {
     157            return (float) $load[0];
     158        }
    87159    }
    88160    if (is_readable('/proc/loadavg')) {
    89         $data = explode(' ', (string) file_get_contents('/proc/loadavg'));
    90         return (float) $data[0];
    91     }
    92     if (function_exists('shell_exec')) {
    93         $output = trim((string) shell_exec('uptime 2>/dev/null'));
    94         if ($output && preg_match('/load average[s]?:\s*([0-9.]+)/', $output, $m)) {
     161        $parts = explode(' ', (string) file_get_contents('/proc/loadavg'));
     162        if (isset($parts[0])) {
     163            return (float) $parts[0];
     164        }
     165    }
     166    $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
     167    if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true)) {
     168        $out = (string) (shell_exec('LANG=C uptime 2>/dev/null') ?? '');
     169        if ($out && preg_match('/load average[s]?:\s*([0-9.]+)/', $out, $m)) {
    95170            return (float) $m[1];
    96171        }
     
    108183{
    109184    protected string $action = 'dfehc_async_heartbeat';
    110 
     185    protected bool $scheduled = false;
    111186    public function __construct()
    112187    {
     188        add_action('init', [$this, 'maybe_schedule']);
    113189        dfehc_register_ajax($this->action, [$this, 'handle_async_request']);
     190    }
     191    public function maybe_schedule(): void
     192    {
     193        if ($this->scheduled) {
     194            return;
     195        }
     196        $this->scheduled = true;
     197        $interval = 300;
     198        $aligned = time() - (time() % $interval) + $interval;
    114199        if (!wp_next_scheduled($this->action)) {
    115             wp_schedule_event(time(), 'dfehc_5_minutes', $this->action);
    116         }
    117         register_deactivation_hook(__FILE__, [$this, 'deactivate']);
    118     }
    119 
    120     public function deactivate(): void
    121     {
    122         wp_clear_scheduled_hook($this->action);
    123     }
    124 
     200            wp_schedule_event($aligned, 'dfehc_5_minutes', $this->action);
     201        } else {
     202            $next = wp_next_scheduled($this->action);
     203            if ($next === false || $next - time() > $interval * 2) {
     204                wp_schedule_single_event(time() + $interval, $this->action);
     205            }
     206        }
     207    }
    125208    public function handle_async_request(): void
    126209    {
    127         $max_retry = (int) apply_filters('dfehc_async_retry', 3);
    128         $retry = 0;
    129         while ($retry < $max_retry) {
    130             try {
    131                 $this->run_action();
    132                 break;
    133             } catch (\Throwable $e) {
    134                 $retry++;
    135                 if ($retry >= $max_retry) {
    136                     error_log('[DFEHC Async] ' . $e->getMessage());
    137                 }
    138                 usleep(1000000);
     210        try {
     211            $this->run_action();
     212        } catch (\Throwable $e) {
     213            if (defined('WP_DEBUG') && WP_DEBUG) {
     214                error_log('[DFEHC Async] ' . $e->getMessage());
    139215            }
    140216        }
    141217        wp_die();
    142218    }
    143 
    144219    protected function run_action(): void
    145220    {
    146221        $last_activity = get_transient('dfehc_last_user_activity');
    147222        if ($last_activity === false) {
    148             throw new \Exception('Missing last activity time.');
    149         }
    150 
    151         $now = time();
    152         $elapsed = $now - (int) $last_activity;
     223            return;
     224        }
     225        $elapsed = time() - (int) $last_activity;
    153226        $load_raw = dfehc_calculate_server_load();
    154227        if ($load_raw === false) {
    155             throw new \Exception('Failed to get server load.');
     228            return;
    156229        }
    157230        $cores = max(1, dfehc_get_cpu_cores());
    158         $load_pct = round($load_raw / $cores * 100, 2);
    159 
    160         dfehc_set_transient_noautoload(DFEHC_SERVER_LOAD, $load_pct, 300);
    161 
     231        $load_pct = min(100.0, round($load_raw / $cores * 100, 2));
     232        dfehc_set_transient_noautoload(DFEHC_SERVER_LOAD, $load_pct, (int) apply_filters('dfehc_server_load_ttl', 300));
    162233        $samples = get_transient(DFEHC_LOAD_AVERAGES) ?: [];
    163234        $samples[] = $load_pct;
     
    166237        }
    167238        dfehc_set_transient_noautoload(DFEHC_LOAD_AVERAGES, $samples, 1800);
    168 
    169         $weights = apply_filters('dfehc_load_weights', [5, 4, 3, 2, 1]);
    170         $weights = array_slice($weights + array_fill(0, count($samples), 1), 0, count($samples));
     239        $weights_raw = apply_filters('dfehc_load_weights', [5, 4, 3, 2, 1]);
     240        $weights = array_slice(array_pad(array_values((array) $weights_raw), count($samples), 1), 0, count($samples));
    171241        $avg_load = dfehc_weighted_average($samples, $weights);
    172242        $interval = $this->calculate_interval($elapsed, $avg_load);
    173 
    174243        dfehc_set_transient_noautoload(DFEHC_RECOMMENDED_INTERVAL, $interval, 300);
    175244    }
    176 
    177245    protected function calculate_interval(int $elapsed, float $load_pct): int
    178246    {
     
    183251            return DFEHC_MAX_INTERVAL;
    184252        }
    185 
    186253        $load_factor = min(1.0, $load_pct / DFEHC_MAX_SERVER_LOAD);
    187         $activity_factor = min(1.0, max(DFEHC_MIN_INTERVAL, $elapsed) - DFEHC_MIN_INTERVAL) / (DFEHC_MAX_INTERVAL - DFEHC_MIN_INTERVAL);
    188         $dominant_factor = max($load_factor, $activity_factor);
    189 
    190         return (int) round(DFEHC_MIN_INTERVAL + $dominant_factor * (DFEHC_MAX_INTERVAL - DFEHC_MIN_INTERVAL));
     254        $activity_factor = ($elapsed - DFEHC_MIN_INTERVAL) / (DFEHC_MAX_INTERVAL - DFEHC_MIN_INTERVAL);
     255        $activity_factor = max(0.0, min(1.0, $activity_factor));
     256        $dominant = max($load_factor, $activity_factor);
     257        return (int) round(DFEHC_MIN_INTERVAL + $dominant * (DFEHC_MAX_INTERVAL - DFEHC_MIN_INTERVAL));
    191258    }
    192259}
     
    194261function dfehc_weighted_average(array $values, array $weights): float
    195262{
    196     $total_value = 0.0;
    197     $total_weight = 0.0;
    198     foreach ($values as $index => $value) {
    199         $weight = $weights[$index] ?? 1;
    200         $total_value += $value * $weight;
    201         $total_weight += $weight;
    202     }
    203     return $total_weight > 0 ? round($total_value / $total_weight, 2) : 0.0;
     263    $tv = 0.0;
     264    $tw = 0.0;
     265    foreach ($values as $i => $v) {
     266        $w = $weights[$i] ?? 1;
     267        $tv += $v * $w;
     268        $tw += $w;
     269    }
     270    return $tw > 0 ? round($tv / $tw, 2) : 0.0;
    204271}
    205272
     
    213280}
    214281
    215 function dfehc_custom_cron_interval_addition(array $schedules): array
    216 {
    217     if (!isset($schedules['dfehc_5_minutes'])) {
    218         $schedules['dfehc_5_minutes'] = ['interval' => 300, 'display' => __('Every 5 Minutes', 'dfehc')];
    219     }
    220     return $schedules;
    221 }
    222 add_filter('cron_schedules', 'dfehc_custom_cron_interval_addition');
     282function dfehc_register_schedules(array $s): array
     283{
     284    if (!isset($s['dfehc_5_minutes'])) {
     285        $s['dfehc_5_minutes'] = ['interval' => 300, 'display' => __('Every 5 Minutes', 'dfehc')];
     286    }
     287    if (!isset($s['dfehc_daily'])) {
     288        $s['dfehc_daily'] = ['interval' => DAY_IN_SECONDS, 'display' => __('Once a day (DFEHC)', 'dfehc')];
     289    }
     290    return $s;
     291}
     292add_filter('cron_schedules', 'dfehc_register_schedules');
     293
     294function dfehc_prune_server_load_logs(): void
     295{
     296    $max_age = (int) apply_filters('dfehc_log_retention_seconds', DAY_IN_SECONDS);
     297    $max_cnt = (int) apply_filters('dfehc_log_retention_max', 1440);
     298    $now = time();
     299    $all_ids = function_exists('get_sites')
     300        ? array_map(static fn($s) => (int) $s->blog_id, get_sites(['fields' => 'ids']))
     301        : [get_current_blog_id()];
     302    $chunk_size = (int) apply_filters('dfehc_prune_chunk_size', 50);
     303    $offset_key = 'dfehc_prune_offset';
     304    $offset = (int) get_site_option($offset_key, 0);
     305    $chunk = array_slice($all_ids, $offset, $chunk_size);
     306    if ($chunk === []) {
     307        $offset = 0;
     308        $chunk = array_slice($all_ids, 0, $chunk_size);
     309    }
     310    foreach ($chunk as $id) {
     311        if (is_multisite()) {
     312            switch_to_blog($id);
     313        }
     314        $option = 'dfehc_server_load_logs_' . get_current_blog_id();
     315        $logs = get_option($option, []);
     316        if ($logs) {
     317            $cutoff = $now - $max_age;
     318            $logs = array_filter(
     319                $logs,
     320                static fn($row) => isset($row['timestamp']) && $row['timestamp'] >= $cutoff
     321            );
     322            if (count($logs) > $max_cnt) {
     323                $logs = array_slice($logs, -$max_cnt);
     324            }
     325            update_option($option, array_values($logs), false);
     326        }
     327        if (is_multisite()) {
     328            restore_current_blog();
     329        }
     330    }
     331    $offset += $chunk_size;
     332    update_site_option($offset_key, $offset);
     333}
     334
     335add_action('dfehc_prune_logs_hook', 'dfehc_prune_server_load_logs');
     336
     337if (!wp_next_scheduled('dfehc_prune_logs_hook')) {
     338    $t = strtotime('today 03:00');
     339    if ($t < time()) {
     340        $t += DAY_IN_SECONDS;
     341    }
     342    wp_schedule_event($t, 'dfehc_daily', 'dfehc_prune_logs_hook');
     343}
     344
     345add_filter('dfehc_required_capability', fn() => 'manage_options');
     346add_filter('dfehc_server_load_ttl', fn() => 120);
     347add_filter('dfehc_load_weights', fn() => [3, 2, 1]);
     348add_filter('dfehc_async_retry', fn() => 1);
     349add_filter('dfehc_log_retention_seconds', fn() => 2 * DAY_IN_SECONDS);
     350add_filter('dfehc_log_retention_max', fn() => 3000);
    223351
    224352new Heartbeat_Async();
  • dynamic-front-end-heartbeat-control/trunk/heartbeat-controller.php

    r3310561 r3320647  
    44Plugin URI: https://heartbeat.support
    55Description: An enhanced solution to optimize the performance of your WordPress website. Stabilize your website's load averages and enhance the browsing experience for visitors during high-traffic fluctuations.
    6 Version: 1.2.98
     6Version: 1.2.99
    77Author: Codeloghin
    88Author URI: https://codeloghin.com
     
    4646    define('DFEHC_NONCE_ACTION', 'dfehc_get_recommended_intervals');
    4747}
     48if (!defined('DFEHC_LOCK_TTL')) {
     49    define('DFEHC_LOCK_TTL', 60);
     50}
     51if (!defined('DFEHC_USER_ACTIVITY_TTL')) {
     52    define('DFEHC_USER_ACTIVITY_TTL', HOUR_IN_SECONDS);
     53}
    4854
    4955function dfehc_set_transient_noautoload(string $key, $value, int $expiration): void
     
    5157    if (wp_using_ext_object_cache()) {
    5258        wp_cache_set($key, $value, '', $expiration);
    53     } else {
    54         set_transient($key, $value, $expiration);
    55         global $wpdb;
    56         $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => '_transient_' . $key]);
    57     }
     59        return;
     60    }
     61    set_transient($key, $value, $expiration);
     62    global $wpdb;
     63    $wpdb->query(
     64        $wpdb->prepare(
     65            "UPDATE {$wpdb->options} SET autoload='no' WHERE option_name=%s AND autoload='yes' LIMIT 1",
     66            '_transient_' . $key
     67        )
     68    );
    5869}
    5970
    6071function dfehc_enqueue_scripts(): void
    6172{
    62     wp_enqueue_script('heartbeat', plugin_dir_url(__FILE__) . 'js/heartbeat.min.js', ['jquery'], '1.5.0', true);
     73    wp_enqueue_script('heartbeat', plugin_dir_url(__FILE__) . 'js/heartbeat.min.js', ['jquery'], '1.5.2', true);
    6374    if (function_exists('wp_script_add_data')) {
    6475        wp_script_add_data('heartbeat', 'async', true);
    6576    }
    66     $load_average        = dfehc_get_system_load_average();
    67     $recommendedInterval = dfehc_calculate_recommended_interval_user_activity($load_average, DFEHC_BATCH_SIZE);
    68 
     77    $load_average = dfehc_get_system_load_average();
     78    $recommended  = dfehc_calculate_recommended_interval_user_activity($load_average, DFEHC_BATCH_SIZE);
    6979    wp_localize_script('heartbeat', 'dfehc_heartbeat_vars', [
    70         'recommendedInterval'      => $recommendedInterval,
     80        'recommendedInterval'       => $recommended,
    7181        'heartbeat_control_enabled' => get_option('dfehc_heartbeat_control_enabled', '1'),
    72         'cache_duration'           => 5 * 60 * 1000,
    73         'nonce'                    => wp_create_nonce(DFEHC_NONCE_ACTION),
     82        'cache_duration'            => 5 * 60 * 1000,
     83        'nonce'                     => wp_create_nonce(DFEHC_NONCE_ACTION),
    7484    ]);
    7585}
    7686add_action('wp_enqueue_scripts', 'dfehc_enqueue_scripts');
    7787
    78 function dfehc_calculate_recommended_interval_user_activity($load_average, int $batch_size = DFEHC_BATCH_SIZE): float
    79 {
    80     if (PHP_OS_FAMILY !== 'Unix' || !function_exists('sys_getloadavg')) {
    81         return 60;
    82     }
    83     $load_average = dfehc_get_system_load_average();
    84     $user_data    = dfehc_gather_user_activity_data($batch_size);
     88function dfehc_get_user_activity_summary(int $batch_size = DFEHC_BATCH_SIZE): array
     89{
     90    $cached = get_transient('dfehc_user_activity_summary');
     91    if ($cached !== false) {
     92        return $cached;
     93    }
     94    $summary = dfehc_gather_user_activity_data($batch_size);
     95    dfehc_set_transient_noautoload('dfehc_user_activity_summary', $summary, DFEHC_USER_ACTIVITY_TTL);
     96    return $summary;
     97}
     98
     99function dfehc_calculate_recommended_interval_user_activity(?float $load_average = null, int $batch_size = DFEHC_BATCH_SIZE): float
     100{
     101    if (!function_exists('sys_getloadavg')) {
     102        return 60.0;
     103    }
     104    if ($load_average === null) {
     105        $load_average = dfehc_get_system_load_average();
     106    }
     107    $user_data = dfehc_get_user_activity_summary($batch_size);
    85108    if ($user_data['total_weight'] === 0) {
    86         return DFEHC_MIN_INTERVAL;
     109        return (float) DFEHC_MIN_INTERVAL;
    87110    }
    88111    $avg_duration = $user_data['total_duration'] / max(1, $user_data['total_weight']);
     
    95118    $total_weight            = 0;
    96119    $offset                  = 0;
    97     do {
    98         $transient_key = 'dfehc_user_batch_' . $offset;
    99         $userBatch     = get_transient($transient_key);
    100         if ($userBatch === false) {
    101             $userBatch = dfehc_get_users_in_batches($batch_size, $offset);
    102             dfehc_set_transient_noautoload($transient_key, $userBatch, HOUR_IN_SECONDS);
     120    while (true) {
     121        $userBatch = dfehc_get_users_in_batches($batch_size, $offset);
     122        if (!$userBatch) {
     123            break;
    103124        }
    104125        foreach ($userBatch as $user) {
     
    107128                continue;
    108129            }
    109             $weight     = count($activity['durations']);
    110             $avg        = array_sum($activity['durations']) / $weight;
     130            $weight   = count($activity['durations']);
     131            $avg      = array_sum($activity['durations']) / $weight;
    111132            $total_weighted_duration += $weight * $avg;
    112133            $total_weight            += $weight;
    113134        }
    114135        $offset += $batch_size;
    115     } while ($userBatch);
     136    }
    116137    return ['total_duration' => $total_weighted_duration, 'total_weight' => $total_weight];
    117138}
     
    123144    }
    124145    if (!class_exists('Heartbeat_Async')) {
    125         return DFEHC_MIN_INTERVAL;
    126     }
    127     class Dfehc_Get_Recommended_Heartbeat_Interval_Async extends Heartbeat_Async
    128     {
    129         protected string $action = 'dfehc_get_recommended_interval_async';
    130         protected function run_action(): void
     146        return get_transient('dfehc_recommended_interval');
     147    }
     148    if (!class_exists('Dfehc_Get_Recommended_Heartbeat_Interval_Async')) {
     149        class Dfehc_Get_Recommended_Heartbeat_Interval_Async extends Heartbeat_Async
    131150        {
    132             $last_activity = (int) get_transient('dfehc_last_user_activity');
    133             $elapsed       = time() - $last_activity;
    134             $load          = dfehc_get_system_load_average();
    135             $interval      = dfehc_calculate_recommended_interval($elapsed, $load, 0);
    136             dfehc_set_transient_noautoload('dfehc_recommended_interval', $interval, 5 * MINUTE_IN_SECONDS);
     151            protected string $action = 'dfehc_get_recommended_interval_async';
     152            protected function run_action(): void
     153            {
     154                $lock = dfehc_acquire_lock('dfehc_interval_calculation_lock', DFEHC_LOCK_TTL);
     155                if (!$lock) {
     156                    return;
     157                }
     158                $last_activity = (int) get_transient('dfehc_last_user_activity');
     159                $elapsed       = time() - $last_activity;
     160                $load          = dfehc_get_system_load_average();
     161                $interval      = dfehc_calculate_recommended_interval($elapsed, $load, 0);
     162                dfehc_set_transient_noautoload('dfehc_recommended_interval', $interval, 5 * MINUTE_IN_SECONDS);
     163                dfehc_release_lock($lock);
     164            }
    137165        }
    138166    }
    139167    $current = dfehc_get_website_visitors();
    140168    $prev    = get_transient('dfehc_previous_visitor_count');
    141     if ($prev === false || abs($current - $prev) > $current * 0.2) {
     169    $ratio   = (float) apply_filters('dfehc_visitors_delta_ratio', 0.2);
     170    if ($prev === false || abs($current - $prev) > $current * $ratio) {
    142171        delete_transient('dfehc_recommended_interval');
    143172        dfehc_set_transient_noautoload('dfehc_previous_visitor_count', $current, 5 * MINUTE_IN_SECONDS);
    144173    }
    145     if (get_transient('dfehc_recommended_interval') === false) {
     174    $cached = get_transient('dfehc_recommended_interval');
     175    if ($cached !== false) {
     176        return (float) $cached;
     177    }
     178    $lock = dfehc_acquire_lock('dfehc_interval_calculation_lock', DFEHC_LOCK_TTL);
     179    if ($lock) {
    146180        (new Dfehc_Get_Recommended_Heartbeat_Interval_Async())->dispatch();
    147     }
    148     return (float) get_transient('dfehc_recommended_interval');
     181        dfehc_release_lock($lock);
     182    }
     183    return get_transient('dfehc_recommended_interval');
    149184}
    150185
     
    160195function dfehc_override_heartbeat_interval(array $settings): array
    161196{
    162     $interval = (int) ($settings['interval'] ?? DFEHC_MIN_INTERVAL);
    163     $interval = min(max($interval, DFEHC_MIN_INTERVAL), DFEHC_MAX_INTERVAL);
     197    $interval            = (int) ($settings['interval'] ?? DFEHC_MIN_INTERVAL);
     198    $interval            = min(max($interval, DFEHC_MIN_INTERVAL), DFEHC_MAX_INTERVAL);
    164199    $settings['interval'] = $interval;
    165200    return $settings;
     
    167202add_filter('heartbeat_settings', 'dfehc_override_heartbeat_interval');
    168203
    169 add_filter('dfehc_safe_transient_get', function ($value, $key) {
     204add_filter('dfehc_safe_transient_get', static function ($value, $key) {
    170205    if ($value === false) {
    171         dfehc_set_transient_noautoload("dfehc_retry_{$key}", true, 60);
     206        dfehc_set_transient_noautoload("dfehc_retry_{$key}", true, DFEHC_LOCK_TTL);
    172207    }
    173208    return $value;
  • dynamic-front-end-heartbeat-control/trunk/js/heartbeat.js

    r3310561 r3320647  
    11((wp) => {
    22  const intervals = {
    3     low: [15, 30, 60, 120, 180, 240, 300],
     3    low:    [15, 30, 60, 120, 180, 240, 300],
    44    medium: [30, 60, 120, 180, 240, 300],
    5     high: [60, 120, 180, 240, 300],
     5    high:   [60, 120, 180, 240, 300],
    66  };
    77
    8   const cacheTimeout = dfehc_heartbeat_vars.cache_duration || 5 * 60 * 1000;
    9   const cache = {};
     8  const cacheTimeout   = dfehc_heartbeat_vars.cache_duration || 5 * 60 * 1000;
     9  const memoryCache    = Object.create(null);
     10  const localCacheKey  = 'dfehc_heartbeat_server_load';
    1011
    11   const getCache = (key) => {
     12  const getLocalCache = (key) => {
    1213    try {
    13       const cachedData = localStorage.getItem(key);
    14       if (cachedData) {
    15         const { timestamp, data } = JSON.parse(cachedData);
    16         if (Date.now() - timestamp < cacheTimeout) {
    17           return data;
    18         }
    19       }
    20     } catch (e) {
    21       console.warn('DFEHC: localStorage cache corrupted, clearing.');
     14      const raw = localStorage.getItem(key);
     15      if (!raw) return null;
     16      const { timestamp, data } = JSON.parse(raw);
     17      return Date.now() - timestamp < cacheTimeout ? data : null;
     18    } catch {
    2219      localStorage.removeItem(key);
    23     }
    24     return null;
    25   };
    26 
    27   const setCache = (key, data) => {
    28     try {
    29       localStorage.setItem(key, JSON.stringify({ timestamp: Date.now(), data }));
    30     } catch (e) {
    31       console.warn('DFEHC: Failed to cache data to localStorage.');
     20      return null;
    3221    }
    3322  };
    3423
    35   const getRecommendedIntervals = async (nonce) => {
    36     const cacheKey = 'dfehc_heartbeat_server_load';
    37     const cachedData = getCache(cacheKey);
    38     if (cachedData) return cachedData;
     24  const setLocalCache = (key, data) => {
     25    try {
     26      localStorage.setItem(key, JSON.stringify({ timestamp: Date.now(), data }));
     27    } catch {
     28    }
     29  };
     30
     31  const fetchServerLoad = async (nonce) => {
     32    const cached = getLocalCache(localCacheKey);
     33    if (cached !== null) return cached;
    3934
    4035    try {
    41       const response = await fetch(ajaxurl, {
    42         method: 'POST',
    43         body: JSON.stringify({
    44           action: 'get_server_load',
    45           nonce,
    46         }),
    47         headers: {
    48           'Content-Type': 'application/json',
    49         },
     36      const body = new URLSearchParams({ action: 'get_server_load', nonce });
     37      const res  = await fetch(ajaxurl, {
     38        method:       'POST',
     39        body,
     40        credentials:  'same-origin',
    5041      });
    5142
    52       if (!response.ok) throw new Error(`Server responded ${response.status}`);
     43      if (!res.ok) throw new Error(`HTTP ${res.status}`);
     44      const json = await res.json();
     45      if (!json.success || typeof json.data !== 'number') throw new Error('Bad payload');
    5346
    54       const result = await response.json();
    55       if (!result.success || typeof result.data !== 'number') {
    56         throw new Error('Invalid data received');
    57       }
    58 
    59       setCache(cacheKey, result.data);
    60       return result.data;
     47      setLocalCache(localCacheKey, json.data);
     48      return json.data;
    6149    } catch (err) {
    6250      console.error('DFEHC fetch error:', err.message);
     
    6553  };
    6654
    67   const smoothMoving = (x) => {
    68     let sum = 0;
    69     const y = [];
    70     x.forEach((val) => {
    71       if (y.length >= 5) sum -= y.shift();
    72       y.push(val);
    73       sum += val;
    74     });
    75     return sum / y.length;
     55  const smoothMoving = (arr, windowSize = 5) => {
     56    const slice = arr.slice(-windowSize);
     57    return slice.reduce((s, v) => s + v, 0) / slice.length;
    7658  };
    7759
    78   const getWeightedAverage = (recentIntervals) =>
    79     recentIntervals.reduce((total, value, index) => total + value * (5 - index), 0) / 15;
    80 
    81   const getTrafficLevel = (serverLoad) => {
    82     if (serverLoad <= 50) return 'low';
    83     if (serverLoad <= 75) return 'medium';
    84     return 'high';
     60  const weightedAverage = (arr) => {
     61    const len     = arr.length;
     62    const denom   = (len * (len + 1)) / 2;
     63    return arr.reduce((sum, v, i) => sum + v * (i + 1), 0) / denom;
    8564  };
    8665
    87   const calculateRecommendedInterval = (serverLoad) => {
    88     if (cache[serverLoad]) return cache[serverLoad];
     66  const trafficLevel = (load) =>
     67    load <= 50 ? 'low' : load <= 75 ? 'medium' : 'high';
    8968
    90     const trafficLevel = getTrafficLevel(serverLoad);
    91     const recentIntervals = intervals[trafficLevel].slice(-5);
    92     const weightedAverage = getWeightedAverage(recentIntervals);
    93     const smoothedInterval = smoothMoving(recentIntervals);
    94     const maxServerLoad = 85;
    95     const loadFactor = 1 - serverLoad / maxServerLoad;
    96     const recommendedInterval = Math.round(weightedAverage + smoothedInterval * loadFactor);
     69  const calcRecommendedInterval = (load) => {
     70    const bucket = Math.round(load / 5) * 5;
     71    if (bucket in memoryCache) return memoryCache[bucket];
    9772
    98     cache[serverLoad] = recommendedInterval;
    99     return Math.min(Math.max(recommendedInterval, 30), 300);
     73    const level           = trafficLevel(load);
     74    const recent          = intervals[level];
     75    const avg             = weightedAverage(recent);
     76    const smooth          = smoothMoving(recent);
     77    const loadFactor      = 1 - Math.min(load / 85, 1);
     78    const rawInterval     = Math.round(avg + smooth * loadFactor);
     79    const clampedInterval = Math.min(Math.max(rawInterval, 30), 300);
     80
     81    memoryCache[bucket] = clampedInterval;
     82    return clampedInterval;
    10083  };
    10184
    10285  const heartbeat = {
    103     updateHeartbeatInterval: (interval) => wp.heartbeat.interval(interval),
    104 
    105     updateUI: function (recommendedInterval) {
    106       const intervalSelect = document.querySelector('#dfehc-heartbeat-interval');
    107       if (intervalSelect) {
    108         intervalSelect.value = recommendedInterval;
    109         this.updateHeartbeatInterval(recommendedInterval);
     86    update: (interval) => {
     87      if (wp && wp.heartbeat && typeof wp.heartbeat.interval === 'function') {
     88        wp.heartbeat.interval(interval);
    11089      }
    11190    },
    112 
     91    updateUI: (interval) => {
     92      const sel = document.querySelector('#dfehc-heartbeat-interval');
     93      if (sel) sel.value = interval;
     94      this.update(interval);
     95    },
    11396    init: async function (nonce) {
    11497      if ('deviceMemory' in navigator && navigator.deviceMemory < 2) return;
    11598      if (navigator.connection && navigator.connection.effectiveType === '2g') return;
    11699
    117       try {
    118         const serverLoad = await getRecommendedIntervals(nonce);
    119         const recommendedInterval = calculateRecommendedInterval(serverLoad);
    120         this.updateUI(recommendedInterval);
    121       } catch (error) {
    122         console.error(`DFEHC Error: ${error.message}`);
    123       }
     100      const load          = await fetchServerLoad(nonce);
     101      const recommended   = calcRecommendedInterval(load);
     102      this.updateUI(recommended);
    124103    },
    125104  };
    126105
    127106  document.addEventListener('DOMContentLoaded', () => {
    128     if (dfehc_heartbeat_vars.heartbeat_control_enabled === '1') {
    129       const { nonce } = dfehc_heartbeat_vars;
     107    if (dfehc_heartbeat_vars.heartbeat_control_enabled !== '1') return;
    130108
    131       if ('requestIdleCallback' in window) {
    132         requestIdleCallback(() => heartbeat.init(nonce));
    133       } else {
    134         setTimeout(() => heartbeat.init(nonce), 100);
    135       }
     109    const { nonce } = dfehc_heartbeat_vars;
    136110
    137       const intervalSelect = document.querySelector('#dfehc-heartbeat-interval');
    138       if (intervalSelect) {
    139         intervalSelect.addEventListener('change', function () {
    140           const newInterval = parseInt(this.value, 10);
    141           if (!isNaN(newInterval)) {
    142             heartbeat.updateHeartbeatInterval(newInterval);
    143           }
    144         });
    145       }
     111    if ('requestIdleCallback' in window) {
     112      requestIdleCallback(() => heartbeat.init(nonce));
     113    } else {
     114      setTimeout(() => heartbeat.init(nonce), 100);
     115    }
     116
     117    const sel = document.querySelector('#dfehc-heartbeat-interval');
     118    if (sel) {
     119      sel.addEventListener('change', function () {
     120        const val = parseInt(this.value, 10);
     121        if (!isNaN(val)) heartbeat.update(val);
     122      });
    146123    }
    147124  });
    148 })(wp);
     125})(window.wp || {});
  • dynamic-front-end-heartbeat-control/trunk/js/heartbeat.min.js

    r3310561 r3320647  
    1 ((wp)=>{const intervals={low:[15,30,60,120,180,240,300],medium:[30,60,120,180,240,300],high:[60,120,180,240,300]},cacheTimeout=dfehc_heartbeat_vars.cache_duration||3e5,cache={},getCache=e=>{try{const t=localStorage.getItem(e);if(t){const{timestamp:r,data:a}=JSON.parse(t);if(Date.now()-r<cacheTimeout)return a}}catch(t){console.warn(\"DFEHC: localStorage cache corrupted, clearing.\"),localStorage.removeItem(e)}return null},setCache=(e,t)=>{try{localStorage.setItem(e,JSON.stringify({timestamp:Date.now(),data:t}))}catch(e){console.warn(\"DFEHC: Failed to cache data to localStorage.\")}},getRecommendedIntervals=async e=>{const t=\"dfehc_heartbeat_server_load\",r=getCache(t);if(r)return r;try{const r=await fetch(ajaxurl,{method:\"POST\",body:JSON.stringify({action:\"get_server_load\",nonce:e}),headers:{\"Content-Type\":\"application/json\"}});if(!r.ok)throw new Error(\"Server responded \"+r.status);const a=await r.json();if(!a.success||\"number\"!=typeof a.data)throw new Error(\"Invalid data received\");return setCache(t,a.data),a.data}catch(e){return console.error(\"DFEHC fetch error:\",e.message),60}},smoothMoving=e=>{let t=0;const r=[];return e.forEach(e=>{r.length>=5&&(t-=r.shift()),r.push(e),t+=e}),t/r.length},getWeightedAverage=e=>e.reduce((e,t,r)=>e+t*(5-r),0)/15,getTrafficLevel=e=>e<=50?\"low\":e<=75?\"medium\":\"high\",calculateRecommendedInterval=e=>{if(cache[e])return cache[e];const t=getTrafficLevel(e),r=intervals[t].slice(-5),a=getWeightedAverage(r),n=smoothMoving(r),o=1-e/85,s=Math.round(a+n*o);return cache[e]=s,Math.min(Math.max(s,30),300)};const heartbeat={updateHeartbeatInterval:e=>wp.heartbeat.interval(e),updateUI:function(e){const t=document.querySelector(\"#dfehc-heartbeat-interval\");t&&(t.value=e,this.updateHeartbeatInterval(e))},init:async function(e){if(\"deviceMemory\"in navigator&&navigator.deviceMemory<2)return;if(navigator.connection&&\"2g\"===navigator.connection.effectiveType)return;try{const t=await getRecommendedIntervals(e),r=calculateRecommendedInterval(t);this.updateUI(r)}catch(e){console.error(`DFEHC Error: ${e.message}`)}}};document.addEventListener(\"DOMContentLoaded\",()=>{if(\"1\"===dfehc_heartbeat_vars.heartbeat_control_enabled){const{nonce:e}=dfehc_heartbeat_vars;\"requestIdleCallback\"in window?requestIdleCallback(()=>heartbeat.init(e)):setTimeout(()=>heartbeat.init(e),100);const t=document.querySelector(\"#dfehc-heartbeat-interval\");t&&t.addEventListener(\"change\",function(){const e=parseInt(this.value,10);isNaN(e)||heartbeat.updateHeartbeatInterval(e)})}})})(wp);
     1((wp)=>{const i={low:[15,30,60,120,180,240,300],medium:[30,60,120,180,240,300],high:[60,120,180,240,300]},c=dfehc_heartbeat_vars.cache_duration||3e5,l=Object.create(null),d="dfehc_heartbeat_server_load",a=e=>{try{const{timestamp:t,data:n}=JSON.parse(localStorage.getItem(e));return Date.now()-t<c?n:null}catch{return localStorage.removeItem(e),null}},s=(e,t)=>{try{localStorage.setItem(e,JSON.stringify({timestamp:Date.now(),data:t}))}catch{}},u=async e=>{const t=a(d);if(t!==null)return t;try{const t=new URLSearchParams({action:"get_server_load",nonce:e}),n=await fetch(ajaxurl,{method:"POST",body:t,credentials:"same-origin"});if(!n.ok)throw new Error(`HTTP ${n.status}`);const r=await n.json();if(!r.success||typeof r.data!=="number")throw new Error("Bad payload");return s(d,r.data),r.data}catch(e){return console.error("DFEHC fetch error:",e.message),60}},f=(e,t=5)=>{const n=e.slice(-t);return n.reduce((e,t)=>e+t,0)/n.length},p=e=>{const t=e.length;return e.reduce((e,n,r)=>e+n*(r+1),0)/(t*(t+1)/2)},m=e=>e<=50?"low":e<=75?"medium":"high",h=e=>{const t=Math.round(e/5)*5;if(t in l)return l[t];const n=m(e),r=i[n],o=p(r),g=f(r),a=1-Math.min(e/85,1),c_=Math.round(o+g*a),s_=Math.min(Math.max(c_,30),300);return l[t]=s_,s_},g={update:e=>{wp&&wp.heartbeat&&typeof wp.heartbeat.interval=="function"&&wp.heartbeat.interval(e)},updateUI(e){const t=document.querySelector("#dfehc-heartbeat-interval");t&&(t.value=e),this.update(e)},async init(e){if("deviceMemory"in navigator&&navigator.deviceMemory<2)return;if(navigator.connection&&navigator.connection.effectiveType==="2g")return;const t=await u(e),n=h(t);this.updateUI(n)}};document.addEventListener("DOMContentLoaded",()=>{if(dfehc_heartbeat_vars.heartbeat_control_enabled!=="1")return;const{nonce:e}=dfehc_heartbeat_vars;"requestIdleCallback"in window?requestIdleCallback(()=>g.init(e)):setTimeout(()=>g.init(e),100);const t=document.querySelector("#dfehc-heartbeat-interval");t&&t.addEventListener("change",function(){const e=parseInt(this.value,10);isNaN(e)||g.update(e)})})})(window.wp||{});
  • dynamic-front-end-heartbeat-control/trunk/readme.txt

    r3310561 r3320647  
    11Dynamic Front-End Heartbeat Control
    22Requires at least: 5.5
    3 Tested up to:      6.8.1
     3Tested up to:      6.8
    44Requires PHP:      7.2
    5 Stable tag:        1.2.98
     5Stable tag:        1.2.99
    66License:           GPLv2 or later
    77License URI:       https://www.gnu.org/licenses/gpl-2.0.html
     
    4141
    4242== Changelog ==
     43
     44= 1.2.99 =
     45
     46* Applied final tune-ups for seamless continuity in the upcoming branch; the current plugin version is performing at its best to date. The plugin was subjected to rigorous stress tests—far beyond the traffic levels most websites will ever face.
     47
     48* Plugin update recommended to benefit from these improvements.
    4349
    4450= 1.2.98 =
  • dynamic-front-end-heartbeat-control/trunk/visitor/cookie-helper.php

    r3310564 r3320647  
    22declare(strict_types=1);
    33
     4function dfehc_get_bot_pattern(): string
     5{
     6    static $pattern = null;
     7    if ($pattern !== null) {
     8        return $pattern;
     9    }
     10    $sigs   = (array) apply_filters('dfehc_bot_signatures', [
     11        'bot', 'crawl', 'slurp', 'spider', 'mediapartners', 'bingpreview',
     12        'yandex', 'duckduckbot', 'baiduspider', 'sogou', 'exabot',
     13        'facebot', 'facebookexternalhit', 'ia_archiver',
     14    ]);
     15    $tokens = array_map(
     16        static fn(string $s): string => '(?:^|\\b)' . preg_quote($s, '/'),
     17        $sigs
     18    );
     19    return $pattern = '/(' . implode('|', $tokens) . ')/i';
     20}
     21
     22function dfehc_is_request_bot(): bool
     23{
     24    static $cached = null;
     25    if ($cached !== null) {
     26        return $cached;
     27    }
     28
     29    $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
     30    if ($ua === '' || preg_match(dfehc_get_bot_pattern(), $ua)) {
     31        return $cached = true;
     32    }
     33
     34    $accept = $_SERVER['HTTP_ACCEPT'] ?? '';
     35    $sec_ch = $_SERVER['HTTP_SEC_CH_UA'] ?? '';
     36    if (($accept === '' || stripos($accept, 'text/html') === false) && $sec_ch === '') {
     37        return $cached = true;
     38    }
     39
     40    $ip     = $_SERVER['REMOTE_ADDR'] ?? '';
     41    $ipKey  = $ip ? 'dfehc_bad_ip_' . md5($ip) : '';
     42    $group  = apply_filters('dfehc_cache_group', 'dfehc');
     43
     44    if ($ipKey && wp_using_ext_object_cache() && function_exists('wp_cache_get')) {
     45        if (wp_cache_get($ipKey, $group)) {
     46            return $cached = true;
     47        }
     48    }
     49    return $cached = false;
     50}
     51
    452function dfehc_set_user_cookie(): void
    553{
    6     static $bot_pattern = null;
    7     if ($bot_pattern === null) {
    8         $signatures = apply_filters('dfehc_bot_signatures', [
    9             'bot', 'crawl', 'slurp', 'spider', 'mediapartners', 'bingpreview',
    10             'yandex', 'duckduckbot', 'baiduspider', 'sogou', 'exabot',
    11             'facebot', 'facebookexternalhit', 'ia_archiver',
    12         ]);
    13         $bot_pattern = '/' . implode('|', array_map('preg_quote', $signatures)) . '/i';
    14     }
    15 
    16     if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match($bot_pattern, strtolower($_SERVER['HTTP_USER_AGENT']))) {
     54    if (dfehc_is_request_bot()) {
    1755        return;
    1856    }
    1957
    20     $name      = 'dfehc_user';
    21     $lifetime  = 400;
    22     $path      = apply_filters('dfehc_cookie_path', '/');
    23     $secure    = is_ssl();
    24     $httponly  = true;
    25     $same_site = apply_filters('dfehc_cookie_samesite', 'Strict');
     58    $ip     = $_SERVER['REMOTE_ADDR'] ?? '';
     59    $group  = apply_filters('dfehc_cache_group', 'dfehc');
     60    $maxRPM = (int) apply_filters('dfehc_max_rpm', 120);
    2661
    27     $visitor_id = empty($_COOKIE[$name])
    28         ? (function_exists('random_bytes') ? bin2hex(random_bytes(16)) : uniqid('visitor_', true) . mt_rand())
    29         : sanitize_text_field($_COOKIE[$name]);
    30 
    31     if (PHP_VERSION_ID >= 70300) {
    32         setcookie($name, $visitor_id, [
    33             'expires'  => time() + $lifetime,
    34             'path'     => $path,
    35             'secure'   => $secure,
    36             'httponly' => $httponly,
    37             'samesite' => $same_site,
    38         ]);
    39     } else {
    40         setcookie($name, $visitor_id, time() + $lifetime, $path . '; samesite=' . strtolower($same_site), '', $secure, $httponly);
    41     }
    42 
    43     $fallback = true;
    44 
    45     if (extension_loaded('redis') && class_exists('Redis')) {
    46         try {
    47             $redis     = new Redis();
    48             $socket    = get_option('dfehc_redis_socket', '');
    49             $connected = $socket
    50                 ? @$redis->connect($socket)
    51                 : @$redis->connect(
    52                     function_exists('dfehc_get_redis_server') ? dfehc_get_redis_server() : '127.0.0.1',
    53                     function_exists('dfehc_get_redis_port')   ? dfehc_get_redis_port()   : 6379,
    54                     1
    55                 );
    56             if ($connected && $redis->ping()) {
    57                 $redis->incr('dfehc_total_visitors');
    58                 $redis->close();
    59                 $fallback = false;
    60             }
    61         } catch (RedisException $e) {
     62    if ($ip && wp_using_ext_object_cache() && function_exists('wp_cache_incr')) {
     63        $rpmKey = 'dfehc_iprpm_' . md5($ip);
     64        $rpm    = wp_cache_incr($rpmKey, 1, $group);
     65        if ($rpm === false) {
     66            wp_cache_set($rpmKey, 1, $group, 60);
     67            $rpm = 1;
     68        }
     69        if ($rpm > $maxRPM) {
     70            wp_cache_set('dfehc_bad_ip_' . md5($ip), 1, $group, HOUR_IN_SECONDS);
     71            return;
    6272        }
    6373    }
    6474
    65     if ($fallback && extension_loaded('memcached') && class_exists('Memcached')) {
    66         try {
    67             $memcached = new Memcached();
    68             $connected = @$memcached->addServer(
    69                 function_exists('dfehc_get_memcached_server') ? dfehc_get_memcached_server() : '127.0.0.1',
    70                 function_exists('dfehc_get_memcached_port')   ? dfehc_get_memcached_port()   : 11211
    71             );
    72             if ($connected) {
    73                 $result = $memcached->increment('dfehc_total_visitors', 1);
    74                 if ($result === false && $memcached->getResultCode() === Memcached::RES_NOTFOUND) {
    75                     $memcached->set('dfehc_total_visitors', 1);
    76                 }
    77                 $memcached->quit();
    78                 $fallback = false;
    79             }
    80         } catch (Exception $e) {
    81         }
     75    $name      = 'dfehc_user';
     76    $lifetime  = (int) apply_filters('dfehc_cookie_lifetime', 400);
     77    $path      = (string) apply_filters('dfehc_cookie_path', '/');
     78    $domain    = (string) apply_filters('dfehc_cookie_domain', parse_url(home_url(), PHP_URL_HOST));
     79    $sameSite  = (string) apply_filters('dfehc_cookie_samesite', 'Strict');
     80    $secure    = is_ssl() || $sameSite === 'None';
     81    $httpOnly  = true;
     82
     83    $val = $_COOKIE[$name] ?? '';
     84    if (!preg_match('/^[A-Za-z0-9]{32,64}$/', $val)) {
     85        $val = function_exists('random_bytes')
     86            ? bin2hex(random_bytes(16))
     87            : uniqid('v_', true) . mt_rand();
     88    }
     89    $expires = time() + $lifetime;
     90
     91    if (PHP_VERSION_ID >= 70300) {
     92        setcookie($name, $val, [
     93            'expires'  => $expires,
     94            'path'     => $path,
     95            'domain'   => $domain,
     96            'secure'   => $secure,
     97            'httponly' => $httpOnly,
     98            'samesite' => $sameSite,
     99        ]);
     100    } else {
     101        header(sprintf(
     102            'Set-Cookie: %s=%s; Expires=%s; Path=%s; Domain=%s%s; HttpOnly; SameSite=%s',
     103            rawurlencode($name),
     104            rawurlencode($val),
     105            gmdate('D, d-M-Y H:i:s T', $expires),
     106            $path,
     107            $domain,
     108            $secure ? '; Secure' : '',
     109            $sameSite
     110        ));
    82111    }
    83112
    84     if ($fallback) {
    85         $count = get_transient('dfehc_total_visitors');
    86         set_transient('dfehc_total_visitors', ($count !== false ? (int) $count : 0) + 1, 4 * MINUTE_IN_SECONDS);
     113    if (isset($_COOKIE[$name])) {
     114        return;
     115    }
     116
     117    $visitorKey = 'dfehc_total_visitors';
     118    if (wp_using_ext_object_cache() && function_exists('wp_cache_incr')) {
     119        if (false === wp_cache_incr($visitorKey, 1, $group)) {
     120            wp_cache_set($visitorKey, 1, $group, $lifetime);
     121        }
     122        return;
     123    }
     124
     125static $client = null;
     126
     127if (!$client && extension_loaded('redis') && class_exists('Redis')) {
     128    try {
     129        $client = new \Redis();
     130
     131        $sock = get_option('dfehc_redis_socket', '');
     132        $ok   = $sock
     133            ? $client->pconnect($sock)
     134            : $client->pconnect(
     135                function_exists('dfehc_get_redis_server') ? dfehc_get_redis_server() : '127.0.0.1',
     136                function_exists('dfehc_get_redis_port')   ? dfehc_get_redis_port()   : 6379,
     137                1.0
     138            );
     139
     140        if (!$ok || $client->ping() !== '+PONG') {
     141            $client = null;
     142        }
     143    } catch (\Throwable $e) {
     144        $client = null;
    87145    }
    88146}
    89147
     148if ($client) {
     149    $client->incr($visitorKey);
     150    $client->expire($visitorKey, $lifetime);
     151    return;
     152}
     153
     154    static $mem = null;
     155    if (!$mem && extension_loaded('memcached') && class_exists('Memcached')) {
     156        $mem = new \Memcached();
     157        $mem->addServer(
     158            function_exists('dfehc_get_memcached_server') ? dfehc_get_memcached_server() : '127.0.0.1',
     159            function_exists('dfehc_get_memcached_port') ? dfehc_get_memcached_port() : 11211
     160        );
     161        if (empty($mem->getStats())) {
     162            $mem = null;
     163        }
     164    }
     165    if ($mem) {
     166        $val = $mem->increment($visitorKey, 1);
     167        if ($val === false) {
     168            $mem->set($visitorKey, 1, $lifetime);
     169        } else {
     170            $mem->touch($visitorKey, $lifetime);
     171        }
     172        return;
     173    }
     174
     175    $cnt = (int) get_transient($visitorKey);
     176    set_transient($visitorKey, $cnt + 1, $lifetime);
     177}
     178
    90179add_action('send_headers', 'dfehc_set_user_cookie');
  • dynamic-front-end-heartbeat-control/trunk/visitor/manager.php

    r3310561 r3320647  
    22declare(strict_types=1);
    33
    4 function dfehc_set_default_last_activity_time(int $user_id): void
    5 {
    6     update_user_meta($user_id, 'last_activity_time', current_time('timestamp'));
     4if (!function_exists('dfehc_acquire_lock')) {
     5    function dfehc_acquire_lock(string $key, int $ttl = 60) {
     6        if (class_exists('WP_Lock')) {
     7            $lock = new WP_Lock($key, $ttl);
     8            return $lock->acquire() ? $lock : null;
     9        }
     10        return wp_cache_add($key, 1, '', $ttl) ? (object) ['cache_key' => $key] : null;
     11    }
     12    function dfehc_release_lock($lock): void {
     13        if ($lock instanceof WP_Lock) {
     14            $lock->release();
     15        } elseif (is_object($lock) && isset($lock->cache_key)) {
     16            wp_cache_delete($lock->cache_key, '');
     17        }
     18    }
     19}
     20
     21function dfehc_set_default_last_activity_time(int $user_id): void {
     22    update_user_meta($user_id, 'last_activity_time', time());
    723}
    824add_action('user_register', 'dfehc_set_default_last_activity_time');
    925
    10 function dfehc_custom_cron_interval_addition(array $schedules): array
    11 {
    12     $schedules['dfehc_5_minutes'] = [
    13         'interval' => 300,
    14         'display'  => __('Every 5 Minutes', 'dfehc'),
    15     ];
    16     return $schedules;
    17 }
    18 add_filter('cron_schedules', 'dfehc_custom_cron_interval_addition');
    19 
    20 function dfehc_schedule_user_activity_processing(): void
    21 {
    22     if (!wp_next_scheduled('dfehc_process_user_activity')) {
    23         wp_schedule_event(time(), 'dfehc_5_minutes', 'dfehc_process_user_activity');
     26function dfehc_add_intervals(array $s): array {
     27    $s['dfehc_5_minutes'] ??= ['interval' => 300, 'display' => __('Every 5 minutes (DFEHC)', 'dfehc')];
     28    return $s;
     29}
     30add_filter('cron_schedules', 'dfehc_add_intervals');
     31
     32function dfehc_schedule_user_activity_processing(): void {
     33    if (!get_option('dfehc_activity_cron_scheduled') && !wp_next_scheduled('dfehc_process_user_activity')) {
     34        wp_schedule_event(time() - time() % 300 + 300, 'dfehc_5_minutes', 'dfehc_process_user_activity');
     35        update_option('dfehc_activity_cron_scheduled', 1, false);
    2436    }
    2537}
    2638add_action('init', 'dfehc_schedule_user_activity_processing');
    2739
    28 function dfehc_throttled_user_activity_handler(): void
    29 {
    30     if (false === get_transient('dfehc_recent_user_processing')) {
    31         set_transient('dfehc_recent_user_processing', true, 300);
    32         try {
    33             dfehc_process_user_activity();
    34         } finally {
    35             delete_transient('dfehc_recent_user_processing');
    36         }
     40function dfehc_throttled_user_activity_handler(): void {
     41    $lock = dfehc_acquire_lock('dfehc_recent_user_processing', 300);
     42    if (!$lock) {
     43        return;
     44    }
     45    try {
     46        dfehc_process_user_activity();
     47    } finally {
     48        dfehc_release_lock($lock);
    3749    }
    3850}
    3951add_action('dfehc_process_user_activity', 'dfehc_throttled_user_activity_handler');
    4052
    41 function dfehc_process_user_activity(): void
    42 {
    43     $batch_size = 75;
    44     $offset     = 0;
    45     while ($users = dfehc_get_users_in_batches($batch_size, $offset)) {
    46         foreach ($users as $user) {
    47             $last = (int) get_user_meta($user->ID, 'last_activity_time', true);
    48             if (!$last) {
    49                 update_user_meta($user->ID, 'last_activity_time', current_time('timestamp'));
    50             }
    51         }
    52         $offset += $batch_size;
    53     }
    54 }
    55 
     53function dfehc_process_user_activity(): void {
     54    if (get_transient('dfehc_activity_backfill_complete')) {
     55        return;
     56    }
     57    global $wpdb;
     58    $batch   = (int) apply_filters('dfehc_activity_processing_batch_size', 75);
     59    $last_id = (int) get_option('dfehc_activity_last_id', 0);
     60    $ids     = $wpdb->get_col($wpdb->prepare(
     61        "SELECT ID FROM $wpdb->users WHERE ID > %d ORDER BY ID ASC LIMIT %d",
     62        $last_id, $batch
     63    ));
     64    if (!$ids) {
     65        set_transient('dfehc_activity_backfill_complete', true, DAY_IN_SECONDS);
     66        delete_option('dfehc_activity_last_id');
     67        return;
     68    }
     69    $now = time();
     70    foreach ($ids as $id) {
     71        if (!get_user_meta($id, 'last_activity_time', true)) {
     72            update_user_meta($id, 'last_activity_time', $now);
     73        }
     74    }
     75    update_option('dfehc_activity_last_id', end($ids), false);
     76}
     77
     78function dfehc_record_user_activity(): void {
     79    if (!is_user_logged_in()) {
     80        return;
     81    }
     82    static $cache = [];
     83    $uid      = get_current_user_id();
     84    $now      = time();
     85    $interval = (int) apply_filters('dfehc_activity_update_interval', 900);
     86    $last     = $cache[$uid] ?? (int) get_user_meta($uid, 'last_activity_time', true);
     87    if ($now - $last >= $interval) {
     88        update_user_meta($uid, 'last_activity_time', $now);
     89        $cache[$uid] = $now;
     90    }
     91}
     92add_action('wp', 'dfehc_record_user_activity');
     93
     94function dfehc_cleanup_user_activity(int $last_id = 0, int $batch_size = 75): void {
     95    $lock = dfehc_acquire_lock('dfehc_cleanup_lock', 600);
     96    if (!$lock) {
     97        return;
     98    }
     99    try {
     100        global $wpdb;
     101        $batch_size = (int) apply_filters('dfehc_cleanup_batch_size', $batch_size);
     102        $ids = $wpdb->get_col($wpdb->prepare(
     103            "SELECT ID FROM $wpdb->users WHERE ID > %d ORDER BY ID ASC LIMIT %d",
     104            $last_id, $batch_size
     105        ));
     106        if (!$ids) {
     107            return;
     108        }
     109        foreach ($ids as $id) {
     110            delete_user_meta($id, 'last_activity_time');
     111        }
     112        if (count($ids) === $batch_size) {
     113            wp_schedule_single_event(time() + 15, 'dfehc_cleanup_user_activity', [end($ids), $batch_size]);
     114        }
     115    } finally {
     116        dfehc_release_lock($lock);
     117    }
     118}
     119add_action('dfehc_cleanup_user_activity', 'dfehc_cleanup_user_activity', 10, 2);
     120
     121function dfehc_increment_total_visitors(): void {
     122    $key = 'dfehc_total_visitors';
     123    $grp = apply_filters('dfehc_cache_group', 'dfehc');
     124    $ttl = HOUR_IN_SECONDS;
     125
     126    if (wp_using_ext_object_cache() && function_exists('wp_cache_incr')) {
     127        if (false === wp_cache_incr($key, 1, $grp)) {
     128            wp_cache_set($key, 1, $grp, $ttl);
     129        }
     130        return;
     131    }
     132
     133  static $conn;
     134
     135if (!$conn && extension_loaded('redis') && class_exists('Redis')) {
     136    try {
     137        $conn = new \Redis();
     138        if (
     139            !$conn->pconnect('127.0.0.1', 6379) ||
     140            $conn->ping() !== '+PONG'
     141        ) {
     142            $conn = null;
     143        }
     144    } catch (\Throwable $e) {
     145        $conn = null;
     146    }
     147}
     148
     149if ($conn) {
     150    $conn->incr($key);
     151    $conn->expire($key, $ttl);
     152    return;
     153}
     154
     155
     156    $cnt = (int) get_transient($key);
     157    set_transient($key, $cnt + 1, $ttl);
     158}
     159function dfehc_increment_total_visitors_fallback(): void {
     160    dfehc_increment_total_visitors();
     161}
     162
     163function dfehc_safe_cache_get(string $key): int {
     164    $grp = apply_filters('dfehc_cache_group', 'dfehc');
     165    if (wp_using_ext_object_cache() && function_exists('wp_cache_get')) {
     166        $v = wp_cache_get($key, $grp);
     167        return $v !== false ? (int) $v : 0;
     168    }
     169    $v = get_transient($key);
     170    return $v !== false ? (int) $v : 0;
     171}
     172
     173function dfehc_safe_cache_delete(string $key): void {
     174    $grp = apply_filters('dfehc_cache_group', 'dfehc');
     175    if (wp_using_ext_object_cache() && function_exists('wp_cache_delete')) {
     176        wp_cache_delete($key, $grp);
     177    }
     178    delete_transient($key);
     179}
     180
     181function dfehc_get_website_visitors(): int {
     182    $cache = get_transient('dfehc_total_visitors');
     183    if ($cache !== false) {
     184        return (int) $cache;
     185    }
     186    if (get_transient('dfehc_regenerating_cache')) {
     187        return (int) get_option('dfehc_stale_total_visitors', 0);
     188    }
     189
     190    set_transient('dfehc_regenerating_cache', true, MINUTE_IN_SECONDS);
     191    $total = dfehc_safe_cache_get('dfehc_total_visitors');
     192    set_transient('dfehc_total_visitors', $total, 4 * MINUTE_IN_SECONDS);
     193    update_option('dfehc_stale_total_visitors', $total, false);
     194
     195    delete_transient('dfehc_regenerating_cache');
     196    return (int) apply_filters('dfehc_get_website_visitors_result', $total);
     197}
    56198function dfehc_get_users_in_batches(int $batch_size, int $offset): array
    57199{
     
    63205    return $query->get_results();
    64206}
    65 
    66 function dfehc_record_user_activity(): void
    67 {
    68     if (is_user_logged_in()) {
    69         $user          = wp_get_current_user();
    70         $time          = current_time('timestamp');
    71         $interval      = (int) apply_filters('dfehc_activity_update_interval', 900);
    72         $last_activity = (int) get_user_meta($user->ID, 'last_activity_time', true);
    73         if ($time - $last_activity >= $interval) {
    74             update_user_meta($user->ID, 'last_activity_time', $time);
    75         }
    76     }
    77 }
    78 add_action('wp_footer', 'dfehc_record_user_activity');
    79 add_action('wp', 'dfehc_record_user_activity');
    80 
    81 function dfehc_cleanup_user_activity(int $offset = 0, int $batch_size = 75): void
    82 {
    83     if (get_transient('dfehc_cleanup_lock')) {
    84         return;
    85     }
    86     set_transient('dfehc_cleanup_lock', true, 600);
    87     try {
    88         $users = get_users(['number' => $batch_size, 'offset' => $offset, 'fields' => ['ID']]);
    89         if (!$users) {
     207function dfehc_reset_total_visitors(): void {
     208    $lock = dfehc_acquire_lock('dfehc_resetting_visitors', 60);
     209    if (!$lock) {
     210        return;
     211    }
     212    try {
     213        $threshold = (float) apply_filters('dfehc_reset_load_threshold', 15.0);
     214        $load      = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : 0;
     215        if ($load === \DFEHC_SENTINEL_NO_LOAD || $load >= $threshold) {
    90216            return;
    91217        }
    92         foreach ($users as $user) {
    93             delete_user_meta($user->ID, 'last_activity_time');
    94         }
    95         if (count($users) === $batch_size) {
    96             wp_schedule_single_event(time() + 15, 'dfehc_cleanup_user_activity', [$offset + $batch_size, $batch_size]);
    97         }
     218        dfehc_safe_cache_delete('dfehc_total_visitors');
     219        delete_option('dfehc_stale_total_visitors');
     220        delete_transient('dfehc_total_visitors');
    98221    } finally {
    99         delete_transient('dfehc_cleanup_lock');
    100     }
    101 }
    102 
    103 function dfehc_increment_total_visitors_fallback(): void
    104 {
    105     $count = (int) get_transient('dfehc_total_visitors');
    106     set_transient('dfehc_total_visitors', $count + 1, 0);
    107 }
    108 
    109 function dfehc_safe_cache_get(string $key, int $timeout = 1): int
    110 {
    111     if (extension_loaded('redis') && class_exists('Redis')) {
    112         try {
    113             $redis = new Redis();
    114             if (@$redis->connect(function_exists('dfehc_get_redis_server') ? dfehc_get_redis_server() : '127.0.0.1', function_exists('dfehc_get_redis_port') ? dfehc_get_redis_port() : 6379, $timeout) && $redis->ping()) {
    115                 $val = $redis->get($key);
    116                 $redis->close();
    117                 if ($val !== false) {
    118                     return (int) $val;
    119                 }
    120             }
    121         } catch (RedisException $e) {
    122         }
    123     }
    124     if (extension_loaded('memcached') && class_exists('Memcached')) {
    125         try {
    126             $memcached = new Memcached();
    127             if ($memcached->addServer(function_exists('dfehc_get_memcached_server') ? dfehc_get_memcached_server() : '127.0.0.1', function_exists('dfehc_get_memcached_port') ? dfehc_get_memcached_port() : 11211)) {
    128                 $val = $memcached->get($key);
    129                 $memcached->quit();
    130                 if ($val !== false) {
    131                     return (int) $val;
    132                 }
    133             }
    134         } catch (Exception $e) {
    135         }
    136     }
    137     $cached = get_transient($key);
    138     return $cached !== false ? (int) $cached : 0;
    139 }
    140 
    141 function dfehc_safe_cache_delete(string $key, int $timeout = 1): void
    142 {
    143     if (extension_loaded('redis') && class_exists('Redis')) {
    144         try {
    145             $redis = new Redis();
    146             if (@$redis->connect(function_exists('dfehc_get_redis_server') ? dfehc_get_redis_server() : '127.0.0.1', function_exists('dfehc_get_redis_port') ? dfehc_get_redis_port() : 6379, $timeout) && $redis->ping()) {
    147                 $redis->del($key);
    148                 $redis->close();
    149             }
    150         } catch (RedisException $e) {
    151         }
    152     }
    153     if (extension_loaded('memcached') && class_exists('Memcached')) {
    154         try {
    155             $memcached = new Memcached();
    156             if ($memcached->addServer(function_exists('dfehc_get_memcached_server') ? dfehc_get_memcached_server() : '127.0.0.1', function_exists('dfehc_get_memcached_port') ? dfehc_get_memcached_port() : 11211)) {
    157                 $memcached->delete($key);
    158                 $memcached->quit();
    159             }
    160         } catch (Exception $e) {
    161         }
    162     }
    163     delete_transient($key);
    164 }
    165 
    166 function dfehc_get_website_visitors(): int
    167 {
    168     $cached = apply_filters('dfehc_safe_transient_get', get_transient('dfehc_total_visitors'), 'dfehc_total_visitors');
    169     if ($cached !== false) {
    170         return (int) $cached;
    171     }
    172     if (get_transient('dfehc_regenerating_cache')) {
    173         return (int) get_option('dfehc_stale_total_visitors', 0);
    174     }
    175     set_transient('dfehc_regenerating_cache', true, 60);
    176     try {
    177         $result = dfehc_safe_cache_get('dfehc_total_visitors');
    178         set_transient('dfehc_total_visitors', $result, 4 * MINUTE_IN_SECONDS);
    179         update_option('dfehc_stale_total_visitors', $result);
    180     } finally {
    181         delete_transient('dfehc_regenerating_cache');
    182     }
    183     return $result;
    184 }
    185 
    186 function dfehc_reset_total_visitors(): void
    187 {
    188     if (get_transient('dfehc_resetting_visitors')) {
    189         return;
    190     }
    191     set_transient('dfehc_resetting_visitors', true, 60);
    192     try {
    193         $load = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : 0;
    194         if ($load < 15) {
    195             delete_transient('dfehc_total_visitors');
    196             delete_option('dfehc_stale_total_visitors');
    197             dfehc_safe_cache_delete('dfehc_total_visitors');
    198         }
    199     } finally {
    200         delete_transient('dfehc_resetting_visitors');
     222        dfehc_release_lock($lock);
    201223    }
    202224}
    203225add_action('dfehc_reset_total_visitors_event', 'dfehc_reset_total_visitors');
    204226
    205 if (!wp_next_scheduled('dfehc_reset_total_visitors_event')) {
    206     wp_schedule_event(time(), 'hourly', 'dfehc_reset_total_visitors_event');
    207 }
     227function dfehc_on_activate(): void {
     228    if (!wp_next_scheduled('dfehc_reset_total_visitors_event')) {
     229        wp_schedule_event(time() + HOUR_IN_SECONDS, 'hourly', 'dfehc_reset_total_visitors_event');
     230    }
     231    dfehc_process_user_activity();
     232}
     233register_activation_hook(__FILE__, 'dfehc_on_activate');
     234
     235function dfehc_on_deactivate(): void {
     236    wp_clear_scheduled_hook('dfehc_process_user_activity');
     237    wp_clear_scheduled_hook('dfehc_reset_total_visitors_event');
     238    delete_option('dfehc_activity_cron_scheduled');
     239}
     240register_deactivation_hook(__FILE__, 'dfehc_on_deactivate');
    208241
    209242if (defined('WP_CLI') && WP_CLI) {
    210     WP_CLI::add_command('dfehc reset_visitors', function () {
     243    \WP_CLI::add_command('dfehc:reset_visitors', static function () {
    211244        dfehc_reset_total_visitors();
    212         WP_CLI::success('Visitor count reset triggered manually.');
     245        \WP_CLI::success('Visitor count reset triggered.');
    213246    });
    214247}
Note: See TracChangeset for help on using the changeset viewer.