Plugin Directory

Changeset 3461136


Ignore:
Timestamp:
02/14/2026 12:51:26 AM (6 weeks ago)
Author:
loghin
Message:

1.2.998.1

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

Legend:

Unmodified
Added
Removed
  • dynamic-front-end-heartbeat-control/trunk/admin/affix.php

    r3427256 r3461136  
    682682            <span style="padding-right:10px">'.esc_html__('Server','dfehc').'</span>
    683683            <input type="range" id="dfehc_priority_slider" name="dfehc_priority_slider" min="-3" max="3" step="1" value="'.esc_attr($slider_value).'" style="flex-grow:1" />
    684             <span style="padding-left:10px">'.esc_html__('Visitor','dfehc').'</span>
     684            <span style="padding-left:10px">'.esc_html__('User','dfehc').'</span>
    685685        </div>';
    686686
  • dynamic-front-end-heartbeat-control/trunk/admin/asset-manager.php

    r3427163 r3461136  
    251251        }
    252252
    253         $js = '
    254             jQuery(function($){
    255                 const overlay=$("<div>",{id:"dfehc-loader-overlay"}).addClass("dfehc-loader-overlay")
    256                     .append($("<div>",{class:"dfehc-loader-content"})
    257                     .append($("<div>",{class:"heartbeat-loader"}))
    258                     .append($("<p>").css({marginTop:"20px",fontSize:"1.2em"}).text("'.esc_js(__('Processing, please wait…','dfehc')).'")));
    259                 $("body").append(overlay);
    260 
    261                 $("#dfehc-optimizer-form").on("submit",function(e){
    262                     e.preventDefault();
    263                     let task=$(document.activeElement).val();
    264                     if(!task){alert("'.esc_js(__('Could not determine task. Please click a button to optimize.','dfehc')).'");return;}
    265                     overlay.show();
    266                     $.post(ajaxurl,{
    267                         action:"dfehc_optimize",
    268                         optimize_function:task,
    269                         _ajax_nonce:"'.wp_create_nonce('dfehc_optimize_action').'"
    270                     })
    271                     .done(()=>location.reload())
    272                     .fail(xhr=>{
    273                         overlay.hide();
    274                         alert(xhr.responseText||"'.esc_js(__('Unexpected error – check the PHP error log.','dfehc')).'");
    275                     });
    276                 });
    277             });
    278         ';
    279         wp_add_inline_script( 'dfhcsl-admin-js', $js );
     253$js = '
     254jQuery(function($){
     255    const overlay = $("<div>", { id: "dfehc-loader-overlay" }).addClass("dfehc-loader-overlay")
     256        .append(
     257            $("<div>", { class: "dfehc-loader-content" })
     258                .append($("<div>", { class: "heartbeat-loader" }))
     259                .append($("<p>").css({ marginTop: "20px", fontSize: "1.2em" }).text("'.esc_js(__('Processing, please wait…','dfehc')).'"))
     260                .append($("<p>", { id: "dfehc-loader-slow-note" }).css({ marginTop: "12px", fontSize: "1em", opacity: 0.9, display: "none" }).text("'.esc_js(__('This is taking longer than usual. The server might be busy or the task is large. To avoid heavy server load, the process runs gently, which can increase the time needed. Thanks for your patience.','dfehc')).'"))
     261        );
     262
     263    $("body").append(overlay);
     264
     265    let slowTimer = null;
     266    const startSlowTimer = () => {
     267        if (slowTimer) clearTimeout(slowTimer);
     268        $("#dfehc-loader-slow-note").hide();
     269        slowTimer = setTimeout(() => {
     270            $("#dfehc-loader-slow-note").fadeIn(200);
     271        }, 50000);
     272    };
     273    const stopSlowTimer = () => {
     274        if (slowTimer) clearTimeout(slowTimer);
     275        slowTimer = null;
     276        $("#dfehc-loader-slow-note").hide();
     277    };
     278
     279    $("#dfehc-optimizer-form").on("submit", function(e){
     280        e.preventDefault();
     281        let task = $(document.activeElement).val();
     282        if(!task){
     283            alert("'.esc_js(__('Could not determine task. Please click a button to optimize.','dfehc')).'");
     284            return;
     285        }
     286        overlay.show();
     287        startSlowTimer();
     288
     289        $.post(ajaxurl, {
     290            action: "dfehc_optimize",
     291            optimize_function: task,
     292            _ajax_nonce: "'.wp_create_nonce('dfehc_optimize_action').'"
     293        })
     294        .done(() => location.reload())
     295        .fail(xhr => {
     296            stopSlowTimer();
     297            overlay.hide();
     298            alert(xhr.responseText || "'.esc_js(__('Unexpected error – check the PHP error log.','dfehc')).'");
     299        });
     300    });
     301
     302    $(window).on("beforeunload", function(){
     303        stopSlowTimer();
     304    });
     305});
     306';
     307wp_add_inline_script('dfhcsl-admin-js', $js);
     308
    280309    }
    281310}
  • dynamic-front-end-heartbeat-control/trunk/defibrillator/load-estimator.php

    r3427163 r3461136  
    88}
    99
    10 class Dfehc_ServerLoadEstimator {
     10class Dfehc_ServerLoadEstimator
     11{
    1112    const BASELINE_TRANSIENT_PREFIX = 'dfehc_baseline_';
    1213    const LOAD_CACHE_TRANSIENT      = 'dfehc_last_known_load';
     
    1415    const BASELINE_RESET_CD_PREFIX  = 'dfehc_baseline_reset_cd_';
    1516
    16     public static function get_server_load(float $duration = 0.025) {
     17    private static $requestMemo = null;
     18    private static $scopeSuffixMemo = null;
     19    private static $hostnameKeyMemo = null;
     20    private static $blogIdMemo = null;
     21    private static $sapiTagMemo = null;
     22
     23    public static function get_server_load(float $duration = 0.025)
     24    {
     25        if (self::$requestMemo !== null) {
     26            return self::$requestMemo;
     27        }
     28
    1729        if (\apply_filters('dfehc_disable_loop_estimator', false)) {
    18             return false;
    19         }
     30            return self::$requestMemo = false;
     31        }
     32
    2033        if (!\function_exists('microtime') || (\defined('DFEHC_DISABLE_LOAD_ESTIMATION') && DFEHC_DISABLE_LOAD_ESTIMATION)) {
    21             return false;
    22         }
     34            return self::$requestMemo = false;
     35        }
     36
     37        if (!\apply_filters('dfehc_allow_estimator_on_request', true)) {
     38            return self::$requestMemo = false;
     39        }
     40
     41        $isCron = (\function_exists('wp_doing_cron') && \wp_doing_cron());
     42        $isAjax = (\function_exists('wp_doing_ajax') && \wp_doing_ajax());
     43        $isJson = (\function_exists('wp_is_json_request') && \wp_is_json_request());
     44        $isCli  = (\defined('WP_CLI') && WP_CLI);
     45
     46        if (!$isCron && ($isAjax || $isJson || $isCli)) {
     47            return self::$requestMemo = false;
     48        }
     49
    2350        $duration = (float) \apply_filters('dfehc_loop_duration', $duration);
    24         if (!\is_finite($duration) || $duration <= 0.0) {
    25             $duration = 0.025;
    26         }
    27         if ($duration < 0.01) {
    28             $duration = 0.01;
    29         }
    30         if ($duration > 0.5) {
    31             $duration = 0.5;
    32         }
     51        $duration = self::normalize_duration($duration, 0.025);
    3352
    3453        $suffix    = self::scope_suffix();
    3554        $baselineT = self::get_baseline_transient_name($suffix);
    36         $cacheTtl  = (int) \apply_filters('dfehc_load_cache_ttl', 90);
     55
     56        $cacheTtl = (int) \apply_filters('dfehc_load_cache_ttl', 90);
    3757        if ($cacheTtl < 1) {
    3858            $cacheTtl = 1;
    3959        }
    40         $cacheKey  = self::get_cache_key($suffix);
    41 
    42         $cached = self::get_scoped_transient($cacheKey);
    43         if ($cached !== false && $cached !== null && \is_numeric($cached)) {
    44             return (float) $cached;
    45         }
    46 
    47         $sysAvg = self::try_sys_getloadavg();
    48         if ($sysAvg !== null) {
    49             self::set_scoped_transient_noautoload($cacheKey, $sysAvg, $cacheTtl);
    50             return $sysAvg;
    51         }
    52 
    53         $baseline = self::get_baseline_value($baselineT);
    54         if ($baseline === false || $baseline === null || !\is_numeric($baseline) || (float) $baseline <= 0.0) {
    55             $baseline = self::maybe_calibrate($baselineT, $duration);
    56         }
    57         $baseline = (float) $baseline;
    58         if ($baseline <= 0.0) {
    59             $baseline = 1.0;
    60         }
    61 
    62         $loopsPerSec = self::run_loop_avg($duration);
    63         if ($loopsPerSec <= 0) {
    64             return false;
    65         }
    66 
    67         $loadRatio   = ($baseline * 0.125) / \max($loopsPerSec, 1);
    68         $loadPercent = \round(\min(100.0, \max(0.0, $loadRatio * 100.0)), 2);
    69         $loadPercent = (float) \apply_filters('dfehc_computed_load_percent', $loadPercent, $baseline, $loopsPerSec);
    70 
    71         self::update_spike_score($loadPercent, $suffix);
    72         self::set_scoped_transient_noautoload($cacheKey, $loadPercent, $cacheTtl);
    73 
    74         return $loadPercent;
    75     }
    76 
    77     public static function calibrate_baseline(float $duration = 0.025): float {
     60
     61        $staleTtl = (int) \apply_filters('dfehc_load_cache_stale_ttl', 1800);
     62        if ($staleTtl < $cacheTtl) {
     63            $staleTtl = $cacheTtl;
     64        }
     65
     66        $cacheKey = self::get_cache_key($suffix);
     67        $cached = self::get_cached_load_value($cacheKey, $cacheTtl, $staleTtl);
     68
     69        if ($cached['fresh'] && $cached['value'] !== null) {
     70            return self::$requestMemo = (float) $cached['value'];
     71        }
     72
     73        $allowSys = (bool) \apply_filters('dfehc_allow_sys_getloadavg', true);
     74        if ($allowSys) {
     75            $sysAvg = self::try_sys_getloadavg();
     76            if ($sysAvg !== null) {
     77                self::set_cached_load_value($cacheKey, $sysAvg, $staleTtl);
     78                return self::$requestMemo = (float) $sysAvg;
     79            }
     80        }
     81
     82        $loopAllowed = $isCron || self::is_idle_context();
     83        $loopAllowed = (bool) \apply_filters('dfehc_allow_loop_estimation', $loopAllowed, $suffix);
     84
     85        if (!$loopAllowed) {
     86            if ($cached['value'] !== null) {
     87                return self::$requestMemo = (float) $cached['value'];
     88            }
     89            return self::$requestMemo = false;
     90        }
     91
     92        $sampleRate = (float) \apply_filters('dfehc_loop_estimation_sample_rate', $isCron ? 1.0 : 0.05, $suffix);
     93        if (!\is_finite($sampleRate) || $sampleRate < 0.0) {
     94            $sampleRate = 0.0;
     95        } elseif ($sampleRate > 1.0) {
     96            $sampleRate = 1.0;
     97        }
     98
     99        if (!$isCron && $sampleRate < 1.0) {
     100            $r = self::rand_unit();
     101            if ($r > $sampleRate) {
     102                if ($cached['value'] !== null) {
     103                    return self::$requestMemo = (float) $cached['value'];
     104                }
     105                return self::$requestMemo = false;
     106            }
     107        }
     108
     109        $cooldownTtl = (int) \apply_filters('dfehc_loop_estimation_cooldown_ttl', $isCron ? 0 : 30, $suffix);
     110        if ($cooldownTtl < 0) {
     111            $cooldownTtl = 0;
     112        }
     113
     114        $cooldownKey = 'dfehc_loop_est_cd_' . $suffix;
     115        if ($cooldownTtl > 0 && self::get_scoped_transient($cooldownKey) !== false) {
     116            if ($cached['value'] !== null) {
     117                return self::$requestMemo = (float) $cached['value'];
     118            }
     119            return self::$requestMemo = false;
     120        }
     121
     122        $lockKey = 'dfehc_estimating_' . $suffix;
     123        $estLockTtl = (int) \apply_filters('dfehc_estimation_lock_ttl', 15);
     124        if ($estLockTtl < 1) {
     125            $estLockTtl = 1;
     126        }
     127
     128        $lock = self::acquire_lock($lockKey, $estLockTtl);
     129        if (!$lock) {
     130            if ($cached['value'] !== null) {
     131                return self::$requestMemo = (float) $cached['value'];
     132            }
     133            return self::$requestMemo = false;
     134        }
     135
     136        if ($cooldownTtl > 0) {
     137            self::set_scoped_transient_noautoload($cooldownKey, 1, $cooldownTtl);
     138        }
     139
     140        try {
     141            $baseline = self::get_baseline_value($baselineT);
     142            if ($baseline === false || $baseline === null || !\is_numeric($baseline) || (float) $baseline <= 0.0) {
     143                $baseline = self::maybe_calibrate($baselineT, $duration);
     144            }
     145            $baseline = (float) $baseline;
     146            if ($baseline <= 0.0) {
     147                $baseline = 1.0;
     148            }
     149
     150            $loopsPerSec = self::run_loop_avg($duration);
     151            if ($loopsPerSec <= 0) {
     152                if ($cached['value'] !== null) {
     153                    return self::$requestMemo = (float) $cached['value'];
     154                }
     155                return self::$requestMemo = false;
     156            }
     157
     158            $scale = (float) \apply_filters('dfehc_loop_to_percent_scale', 0.125);
     159            if (!\is_finite($scale) || $scale <= 0.0) {
     160                $scale = 0.125;
     161            }
     162
     163            $loadRatio   = ($baseline * $scale) / \max($loopsPerSec, 1);
     164            $loadPercent = \round(\min(100.0, \max(0.0, $loadRatio * 100.0)), 2);
     165            $loadPercent = (float) \apply_filters('dfehc_computed_load_percent', $loadPercent, $baseline, $loopsPerSec);
     166
     167            self::update_spike_score($loadPercent, $suffix);
     168            self::set_cached_load_value($cacheKey, $loadPercent, $staleTtl);
     169
     170            return self::$requestMemo = (float) $loadPercent;
     171        } finally {
     172            self::release_lock($lock);
     173        }
     174    }
     175
     176    public static function calibrate_baseline(float $duration = 0.025): float
     177    {
    78178        $duration = (float) \apply_filters('dfehc_loop_duration', $duration);
    79         if (!\is_finite($duration) || $duration <= 0.0) {
    80             $duration = 0.025;
    81         }
    82         if ($duration < 0.01) {
    83             $duration = 0.01;
    84         }
    85         if ($duration > 0.5) {
    86             $duration = 0.5;
    87         }
     179        $duration = self::normalize_duration($duration, 0.025);
    88180        return self::run_loop_avg($duration);
    89181    }
    90182
    91     public static function maybe_calibrate_if_idle(): void {
    92         if (\is_admin() || \is_user_logged_in()) {
    93             return;
    94         }
    95         if ((\function_exists('wp_doing_cron') && \wp_doing_cron()) ||
    96             (\function_exists('wp_doing_ajax') && \wp_doing_ajax()) ||
    97             (\function_exists('wp_is_json_request') && \wp_is_json_request()) ||
    98             (\defined('WP_CLI') && WP_CLI)) {
    99             return;
    100         }
    101 
     183    public static function maybe_calibrate_if_idle(): void
     184    {
     185        if (!self::is_idle_context()) {
     186            return;
     187        }
    102188        $suffix  = self::scope_suffix();
    103189        $seenKey = 'dfehc_seen_recently_' . $suffix;
     
    113199    }
    114200
    115     public static function maybe_calibrate_during_cron(): void {
     201    public static function maybe_calibrate_during_cron(): void
     202    {
    116203        if (!\function_exists('wp_doing_cron') || !\wp_doing_cron()) {
    117204            return;
     
    130217    }
    131218
    132     private static function try_sys_getloadavg(): ?float {
     219    public static function scheduled_recalibrate(string $suffix): void
     220    {
     221        $suffix = (string) $suffix;
     222        if ($suffix === '') {
     223            return;
     224        }
     225        $lock = self::acquire_lock('dfehc_sched_cal_' . $suffix, 30);
     226        if (!$lock) {
     227            return;
     228        }
     229        try {
     230            $baselineT = self::get_baseline_transient_name($suffix);
     231            self::delete_baseline_value($baselineT);
     232            self::ensure_baseline_for_suffix($suffix);
     233        } finally {
     234            self::release_lock($lock);
     235        }
     236    }
     237
     238    private static function normalize_duration(float $duration, float $fallback): float
     239    {
     240        if (!\is_finite($duration) || $duration <= 0.0) {
     241            $duration = $fallback;
     242        }
     243        if ($duration < 0.01) {
     244            $duration = 0.01;
     245        } elseif ($duration > 0.5) {
     246            $duration = 0.5;
     247        }
     248        return $duration;
     249    }
     250
     251    private static function rand_unit(): float
     252    {
     253        if (\function_exists('wp_rand')) {
     254            return (float) \wp_rand(0, 1000000) / 1000000.0;
     255        }
     256        return (float) \mt_rand(0, 1000000) / 1000000.0;
     257    }
     258
     259    private static function is_idle_context(): bool
     260    {
     261        if (\is_admin() || \is_user_logged_in()) {
     262            return false;
     263        }
     264        if ((\function_exists('wp_doing_cron') && \wp_doing_cron()) ||
     265            (\function_exists('wp_doing_ajax') && \wp_doing_ajax()) ||
     266            (\function_exists('wp_is_json_request') && \wp_is_json_request()) ||
     267            (\defined('WP_CLI') && WP_CLI)) {
     268            return false;
     269        }
     270        return true;
     271    }
     272
     273    private static function try_sys_getloadavg(): ?float
     274    {
    133275        if (!\function_exists('sys_getloadavg')) {
    134276            return null;
     
    152294        }
    153295
    154         return \min(100.0, \round(($raw / $cores) * 100.0, 2));
    155     }
    156 
    157     private static function now(): float {
    158         return \function_exists('hrtime') ? (hrtime(true) / 1e9) : \microtime(true);
    159     }
    160 
    161     private static function run_loop(float $duration): float {
    162         if ($duration <= 0.0) {
    163             return 0.0;
    164         }
    165         if ($duration < 0.01) {
    166             $duration = 0.01;
    167         }
    168         if ($duration > 0.5) {
    169             $duration = 0.5;
    170         }
     296        $pct = ($raw / $cores) * 100.0;
     297        if (!\is_finite($pct)) {
     298            return null;
     299        }
     300
     301        return \min(100.0, \round(\max(0.0, $pct), 2));
     302    }
     303
     304    private static function now(): float
     305    {
     306        if (\function_exists('hrtime')) {
     307            return (float) (\hrtime(true) / 1e9);
     308        }
     309        return (float) \microtime(true);
     310    }
     311
     312    private static function run_loop(float $duration): float
     313    {
     314        $duration = self::normalize_duration($duration, 0.025);
    171315        $duration += (float) \wp_rand(0, 2) * 0.001;
    172 
    173         $warm = self::now();
    174         for ($i = 0; $i < 1000; $i++) { $warm += 0; }
    175316
    176317        $start = self::now();
    177318        $end   = $start + $duration;
    178         $cnt   = 0;
    179         $cap   = (int) \apply_filters('dfehc_loop_iteration_cap', 10000000);
     319
     320        $cap = (int) \apply_filters('dfehc_loop_iteration_cap', 10000000);
    180321        if ($cap < 1000) {
    181322            $cap = 1000;
    182323        }
    183         $now   = $start;
     324
     325        $batch = (int) \apply_filters('dfehc_loop_timecheck_batch', 64);
     326        if ($batch < 8) {
     327            $batch = 8;
     328        } elseif ($batch > 2048) {
     329            $batch = 2048;
     330        }
     331
     332        $cnt = 0;
     333        $now = $start;
     334        $lastNow = $start;
     335        $x = 0;
    184336
    185337        while ($now < $end && $cnt < $cap) {
    186             ++$cnt;
     338            for ($i = 0; $i < $batch && $cnt < $cap; $i++) {
     339                $x = ($x + 1) & 0xFFFFFFFF;
     340                $cnt++;
     341            }
    187342            $now = self::now();
     343            if ($now < $lastNow) {
     344                $now = $lastNow;
     345            }
     346            $lastNow = $now;
    188347        }
    189348
     
    192351    }
    193352
    194     private static function run_loop_avg(float $duration): float {
     353    private static function run_loop_avg(float $duration): float
     354    {
    195355        $a = self::run_loop($duration);
    196356        $b = self::run_loop(\min(0.5, $duration * 1.5));
     
    198358            return 0.0;
    199359        }
    200         if ($a <= 0.0) return $b;
    201         if ($b <= 0.0) return $a;
     360        if ($a <= 0.0) {
     361            return $b;
     362        }
     363        if ($b <= 0.0) {
     364
     365            return $a;
     366        }
    202367        return ($a + $b) / 2.0;
    203368    }
    204369
    205     private static function maybe_calibrate(string $baselineT, float $duration): float {
     370    private static function maybe_calibrate(string $baselineT, float $duration): float
     371    {
    206372        $suffix   = self::scope_suffix();
    207373        $lockKey  = 'dfehc_calibrating_' . $suffix;
     
    231397    }
    232398
    233     private static function update_spike_score(float $loadPercent, string $suffix): void {
     399    private static function update_spike_score(float $loadPercent, string $suffix): void
     400    {
    234401        $spikeKey   = self::get_spike_key($suffix);
    235402        $scoreRaw   = self::get_scoped_transient($spikeKey);
     
    256423
    257424        if ($score >= $threshold) {
    258             if (\get_transient($resetCdKey) === false) {
     425            $isCron = (\function_exists('wp_doing_cron') && \wp_doing_cron());
     426            $isIdle = self::is_idle_context();
     427            $canReset = $isCron || $isIdle;
     428            $canReset = (bool) \apply_filters('dfehc_allow_baseline_reset', $canReset, $loadPercent, $suffix);
     429
     430            if ($canReset && \get_transient($resetCdKey) === false) {
    259431                $baselineName = self::get_baseline_transient_name($suffix);
    260432                self::delete_baseline_value($baselineName);
     
    263435                return;
    264436            }
     437
     438            if (\function_exists('wp_schedule_single_event') && \get_transient($resetCdKey) === false) {
     439                $scheduledKey = 'dfehc_sched_cal_cd_' . $suffix;
     440                $schedTtl = (int) \apply_filters('dfehc_scheduled_calibration_cooldown', 600, $suffix);
     441                if ($schedTtl < 60) {
     442                    $schedTtl = 60;
     443                }
     444                if (self::get_scoped_transient($scheduledKey) === false) {
     445                    self::set_scoped_transient_noautoload($scheduledKey, 1, $schedTtl);
     446                    \wp_schedule_single_event(\time() + 30, 'dfehc_calibrate_baseline_event', [$suffix]);
     447                }
     448            }
    265449        }
    266450
     
    268452    }
    269453
    270     private static function ensure_baseline(): void {
    271         $suffix    = self::scope_suffix();
     454    private static function ensure_baseline(): void
     455    {
     456        $suffix = self::scope_suffix();
     457        self::ensure_baseline_for_suffix($suffix);
     458    }
     459
     460    private static function ensure_baseline_for_suffix(string $suffix): void
     461    {
    272462        $baselineT = self::get_baseline_transient_name($suffix);
    273463        $existing  = self::get_baseline_value($baselineT);
     
    284474        try {
    285475            $duration = (float) \apply_filters('dfehc_loop_duration', 0.025);
    286             if (!\is_finite($duration) || $duration <= 0.0) {
    287                 $duration = 0.025;
    288             }
    289             if ($duration < 0.01) {
    290                 $duration = 0.01;
    291             }
    292             if ($duration > 0.5) {
    293                 $duration = 0.5;
    294             }
     476            $duration = self::normalize_duration($duration, 0.025);
    295477
    296478            $baseline = self::run_loop_avg($duration);
     
    309491    }
    310492
    311     private static function acquire_lock(string $key, int $ttl) {
     493    private static function acquire_lock(string $key, int $ttl)
     494    {
    312495        $ttl = $ttl < 1 ? 1 : $ttl;
    313496        $scopedKey = $key;
     
    317500            return $lock->acquire() ? $lock : null;
    318501        }
     502
    319503        if (\function_exists('wp_cache_add') && \wp_cache_add($scopedKey, 1, DFEHC_CACHE_GROUP, $ttl)) {
    320504            return (object) ['type' => 'cache', 'key' => $scopedKey];
    321505        }
     506
    322507        if (\get_transient($scopedKey) !== false) {
    323508            return null;
    324509        }
     510
    325511        if (\set_transient($scopedKey, 1, $ttl)) {
    326512            return (object) ['type' => 'transient', 'key' => $scopedKey];
    327513        }
     514
    328515        return null;
    329516    }
    330517
    331     private static function release_lock($lock): void {
     518    private static function release_lock($lock): void
     519    {
    332520        if ($lock instanceof \WP_Lock) {
    333521            $lock->release();
     
    348536    }
    349537
    350     private static function get_baseline_transient_name(string $suffix): string {
     538    private static function get_baseline_transient_name(string $suffix): string
     539    {
    351540        return self::BASELINE_TRANSIENT_PREFIX . $suffix;
    352541    }
    353542
    354     private static function get_hostname_key(): string {
    355         $host = @\php_uname('n');
    356         if (!$host) {
    357             $url = \defined('WP_HOME') && WP_HOME ? WP_HOME : (\function_exists('home_url') ? \home_url() : '');
    358             $parts = \wp_parse_url((string) $url);
    359             $host = \is_array($parts) ? ($parts['host'] ?? '') : '';
    360             if (!$host) {
    361                 $host = $url ?: 'unknown';
    362             }
    363         }
     543    private static function get_hostname_key(): string
     544    {
     545        if (self::$hostnameKeyMemo !== null) {
     546            return self::$hostnameKeyMemo;
     547        }
     548
     549        $url = \defined('WP_HOME') && WP_HOME ? WP_HOME : (\function_exists('home_url') ? \home_url() : '');
     550        $parts = \wp_parse_url((string) $url);
     551        $host = \is_array($parts) ? (string) ($parts['host'] ?? '') : '';
     552
     553        if ($host === '') {
     554            $host = (string) @\php_uname('n');
     555        }
     556        if ($host === '') {
     557            $host = $url !== '' ? (string) $url : 'unknown';
     558        }
     559
    364560        $salt = \defined('DB_NAME') ? (string) DB_NAME : '';
    365         return \substr(\md5((string) $host . $salt), 0, 10);
    366     }
    367 
    368     private static function get_blog_id(): int {
    369         return \function_exists('get_current_blog_id') ? (int) \get_current_blog_id() : 0;
    370     }
    371 
    372     private static function scope_suffix(): string {
    373         $sapi = \php_sapi_name();
    374         $sapiTag = $sapi ? \substr(\preg_replace('/[^a-z0-9]/i', '', strtolower($sapi)), 0, 6) : 'web';
    375         $suffix = self::get_hostname_key() . '_' . self::get_blog_id() . '_' . $sapiTag;
     561        self::$hostnameKeyMemo = \substr(\md5($host . $salt), 0, 10);
     562        return self::$hostnameKeyMemo;
     563    }
     564
     565    private static function get_blog_id(): int
     566    {
     567        if (self::$blogIdMemo !== null) {
     568            return (int) self::$blogIdMemo;
     569        }
     570        self::$blogIdMemo = \function_exists('get_current_blog_id') ? (int) \get_current_blog_id() : 0;
     571        return (int) self::$blogIdMemo;
     572    }
     573
     574    private static function scope_suffix(): string
     575    {
     576        if (self::$scopeSuffixMemo !== null) {
     577            return self::$scopeSuffixMemo;
     578        }
     579
     580        if (self::$sapiTagMemo === null) {
     581            $sapi = \php_sapi_name();
     582            $sapiTag = $sapi ? (string) \substr((string) \preg_replace('/[^a-z0-9]/i', '', \strtolower((string) $sapi)), 0, 6) : 'web';
     583            self::$sapiTagMemo = $sapiTag !== '' ? $sapiTag : 'web';
     584        }
     585
     586        $suffix = self::get_hostname_key() . '_' . self::get_blog_id() . '_' . self::$sapiTagMemo;
    376587        $override = \apply_filters('dfehc_baseline_scope_suffix', null, $suffix);
    377588        if (\is_string($override) && $override !== '') {
    378             return $override;
    379         }
    380         return $suffix;
    381     }
    382 
    383     private static function get_cache_key(string $suffix): string {
     589            self::$scopeSuffixMemo = $override;
     590            return self::$scopeSuffixMemo;
     591        }
     592
     593        self::$scopeSuffixMemo = $suffix;
     594        return self::$scopeSuffixMemo;
     595    }
     596
     597    private static function get_cache_key(string $suffix): string
     598    {
    384599        return self::LOAD_CACHE_TRANSIENT . '_' . $suffix;
    385600    }
    386601
    387     private static function get_spike_key(string $suffix): string {
     602    private static function get_spike_key(string $suffix): string
     603    {
    388604        return self::LOAD_SPIKE_TRANSIENT . '_' . $suffix;
    389605    }
    390606
    391     private static function get_baseline_value(string $name) {
     607    private static function get_baseline_value(string $name)
     608    {
    392609        return \is_multisite() ? \get_site_transient($name) : \get_transient($name);
    393610    }
    394611
    395     private static function set_baseline_value(string $name, $value, int $exp): void {
     612    private static function set_baseline_value(string $name, $value, int $exp): void
     613    {
    396614        if ($exp < 1) {
    397615            $exp = 1;
     
    404622    }
    405623
    406     private static function delete_baseline_value(string $name): void {
     624    private static function delete_baseline_value(string $name): void
     625    {
    407626        if (\is_multisite()) {
    408627            \delete_site_transient($name);
     
    412631    }
    413632
    414     private static function get_scoped_transient(string $key) {
     633    private static function get_scoped_transient(string $key)
     634    {
    415635        return \is_multisite() ? \get_site_transient($key) : \get_transient($key);
    416636    }
    417637
    418     private static function set_scoped_transient_noautoload(string $key, $value, int $ttl): void {
     638    private static function set_scoped_transient_noautoload(string $key, $value, int $ttl): void
     639    {
    419640        if ($ttl < 1) {
    420641            $ttl = 1;
     
    427648    }
    428649
    429     private static function delete_scoped_transient(string $key): void {
     650    private static function delete_scoped_transient(string $key): void
     651    {
    430652        if (\is_multisite()) {
    431653            \delete_site_transient($key);
     
    435657    }
    436658
    437     private static function set_transient_noautoload(string $key, $value, int $ttl): void {
     659    private static function get_cached_load_value(string $key, int $freshTtl, int $staleTtl): array
     660    {
     661        $raw = self::get_scoped_transient($key);
     662        $now = \time();
     663
     664        $value = null;
     665        $age = null;
     666
     667        if (\is_array($raw) && isset($raw['v'], $raw['t'])) {
     668            $t = (int) $raw['t'];
     669            if ($t > 0) {
     670                $age = $now - $t;
     671                if ($age < 0) {
     672                    $age = 0;
     673                }
     674                if ($age <= $staleTtl && \is_numeric($raw['v'])) {
     675                    $value = (float) $raw['v'];
     676                }
     677            }
     678        } elseif ($raw !== false && $raw !== null && \is_numeric($raw)) {
     679            $value = (float) $raw;
     680            $age = null;
     681        }
     682
     683        $fresh = ($value !== null) && ($age === null || $age <= $freshTtl);
     684
     685        return [
     686            'value' => $value,
     687            'fresh' => $fresh,
     688        ];
     689    }
     690
     691    private static function set_cached_load_value(string $key, float $value, int $ttl): void
     692    {
     693        self::set_scoped_transient_noautoload($key, ['v' => (float) $value, 't' => \time()], $ttl);
     694    }
     695
     696    private static function normalize_transient_key(string $key): string
     697    {
     698        $key = \trim($key);
     699        if ($key === '') {
     700            return '';
     701        }
     702        $key = \preg_replace('/[^a-zA-Z0-9_\-:]/', '_', $key) ?? $key;
     703        $key = \trim($key, "_ \t\n\r\0\x0B");
     704        if ($key === '') {
     705            return '';
     706        }
     707        if (\strlen($key) > 172) {
     708            $key = \substr($key, 0, 120) . '_' . \substr(\md5($key), 0, 16);
     709        }
     710        return $key;
     711    }
     712
     713    private static function set_transient_noautoload(string $key, $value, int $ttl): void
     714    {
     715        $key = self::normalize_transient_key($key);
     716        if ($key === '') {
     717            return;
     718        }
     719
    438720        $jitter = 0;
    439721        if (\function_exists('random_int')) {
     
    444726            }
    445727        }
     728        $ttl = (int) $ttl;
    446729        $ttl = \max(1, $ttl + $jitter);
    447730
    448731        if (\function_exists('wp_using_ext_object_cache') && \wp_using_ext_object_cache()) {
    449             \wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $ttl);
     732            if (\function_exists('wp_cache_set')) {
     733                \wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $ttl);
     734            }
    450735            return;
    451736        }
     
    453738        \set_transient($key, $value, $ttl);
    454739
     740        $minTtl = (int) \apply_filters('dfehc_noautoload_db_fix_min_ttl', 600);
     741        $doFix  = (bool) \apply_filters('dfehc_fix_transient_autoload', false, $key, $ttl);
     742
     743        if (!$doFix || $ttl < $minTtl) {
     744            return;
     745        }
     746
    455747        global $wpdb;
    456         if (!isset($wpdb) || !\is_object($wpdb) || !isset($wpdb->options)) {
    457             return;
    458         }
    459 
    460         $opt_key = "_transient_$key";
    461         $opt_key_to = "_transient_timeout_$key";
    462         $wpdb->suppress_errors(true);
     748        if (!($wpdb instanceof \wpdb) || empty($wpdb->options)) {
     749            return;
     750        }
     751
     752        $opt_key = '_transient_' . $key;
     753        $opt_key_to = '_transient_timeout_' . $key;
     754
     755        $prev_suppress = $wpdb->suppress_errors(true);
    463756        try {
    464             $autoload = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key));
    465             if ($autoload === 'yes') {
    466                 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key, 'autoload' => 'yes'], ['%s'], ['%s','%s']);
    467             }
    468             $autoload_to = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_to));
    469             if ($autoload_to === 'yes') {
    470                 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key_to, 'autoload' => 'yes'], ['%s'], ['%s','%s']);
    471             }
     757            $wpdb->query($wpdb->prepare(
     758                "UPDATE {$wpdb->options} SET autoload = 'no' WHERE option_name = %s AND autoload <> 'no' LIMIT 1",
     759                $opt_key
     760            ));
     761            $wpdb->query($wpdb->prepare(
     762                "UPDATE {$wpdb->options} SET autoload = 'no' WHERE option_name = %s AND autoload <> 'no' LIMIT 1",
     763                $opt_key_to
     764            ));
     765        } catch (\Throwable $e) {
    472766        } finally {
    473             $wpdb->suppress_errors(false);
    474         }
    475     }
    476 
    477     private static function set_site_transient_noautoload(string $key, $value, int $ttl): void {
     767            $wpdb->suppress_errors((bool) $prev_suppress);
     768        }
     769    }
     770
     771    private static function set_site_transient_noautoload(string $key, $value, int $ttl): void
     772    {
     773        $key = self::normalize_transient_key($key);
     774        if ($key === '') {
     775            return;
     776        }
     777
    478778        $jitter = 0;
    479779        if (\function_exists('random_int')) {
     
    484784            }
    485785        }
     786        $ttl = (int) $ttl;
    486787        $ttl = \max(1, $ttl + $jitter);
    487788
    488789        if (\function_exists('wp_using_ext_object_cache') && \wp_using_ext_object_cache()) {
    489             \wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $ttl);
     790            if (\function_exists('wp_cache_set')) {
     791                \wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $ttl);
     792            }
    490793            return;
    491794        }
     
    497800\add_action('init', [Dfehc_ServerLoadEstimator::class, 'maybe_calibrate_during_cron']);
    498801\add_action('template_redirect', [Dfehc_ServerLoadEstimator::class, 'maybe_calibrate_if_idle']);
     802\add_action('dfehc_calibrate_baseline_event', [Dfehc_ServerLoadEstimator::class, 'scheduled_recalibrate'], 10, 1);
    499803
    500804\add_filter('heartbeat_settings', function ($settings) {
  • dynamic-front-end-heartbeat-control/trunk/engine/interval-helper.php

    r3412283 r3461136  
    1919if (!defined('DFEHC_DEFAULT_SMA_WINDOW')) define('DFEHC_DEFAULT_SMA_WINDOW', 5);
    2020if (!defined('DFEHC_DEFAULT_MAX_DECREASE_RATE')) define('DFEHC_DEFAULT_MAX_DECREASE_RATE', 0.25);
    21 if (!defined('DFEHC_DEFAULT_EMA_TTL')) define('DFEHC_DEFAULT_EMA_TTL', 600);
     21if (!defined('DFEHC_DEFAULT_EMA_TTL')) define('DFEHC_DEFAULT_EMA_TTL', 900);
    2222
    2323if (!function_exists('dfehc_host_token')) {
    24     function dfehc_host_token(): string {
     24    function dfehc_host_token(): string
     25    {
    2526        static $t = '';
    2627        if ($t !== '') return $t;
    27         $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown'));
     28
     29        $url = '';
     30        if (defined('WP_HOME') && is_string(WP_HOME) && WP_HOME !== '') {
     31            $url = WP_HOME;
     32        } elseif (function_exists('home_url')) {
     33            $url = (string) home_url();
     34        }
     35
     36        $host = '';
     37        if ($url !== '' && function_exists('wp_parse_url')) {
     38            $parts = wp_parse_url($url);
     39            if (is_array($parts) && isset($parts['host']) && is_string($parts['host'])) {
     40                $host = $parts['host'];
     41            }
     42        }
     43
     44        if ($host === '') {
     45            $host = @php_uname('n') ?: 'unknown';
     46        }
     47
    2848        $salt = defined('DB_NAME') ? (string) DB_NAME : '';
    2949        return $t = substr(md5((string) $host . $salt), 0, 10);
     
    3252
    3353if (!function_exists('dfehc_blog_id')) {
    34     function dfehc_blog_id(): int {
     54    function dfehc_blog_id(): int
     55    {
    3556        return function_exists('get_current_blog_id') ? (int) get_current_blog_id() : 0;
    3657    }
     
    3859
    3960if (!function_exists('dfehc_scoped_key')) {
    40     function dfehc_scoped_key(string $base): string {
     61    function dfehc_scoped_key(string $base): string
     62    {
    4163        return "{$base}_" . dfehc_blog_id() . '_' . dfehc_host_token();
    4264    }
    4365}
    4466
     67if (!function_exists('dfehc_apply_filters')) {
     68    function dfehc_apply_filters(string $tag, $value, ...$args)
     69    {
     70        if (function_exists('apply_filters')) {
     71            return apply_filters($tag, $value, ...$args);
     72        }
     73        return $value;
     74    }
     75}
     76
     77if (!function_exists('dfehc_get_option')) {
     78    function dfehc_get_option(string $key, $default)
     79    {
     80        if (function_exists('get_option')) {
     81            return get_option($key, $default);
     82        }
     83        return $default;
     84    }
     85}
     86
     87if (!function_exists('dfehc_cache_get')) {
     88    function dfehc_cache_get(string $key)
     89    {
     90        $group = defined('DFEHC_CACHE_GROUP') ? (string) DFEHC_CACHE_GROUP : 'dfehc';
     91
     92        if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_get')) {
     93            $v = wp_cache_get($key, $group);
     94            return $v === false ? null : $v;
     95        }
     96
     97        if (function_exists('get_transient')) {
     98            $v = get_transient($key);
     99            return $v === false ? null : $v;
     100        }
     101
     102        return null;
     103    }
     104}
     105
    45106if (!function_exists('dfehc_store_lockfree')) {
    46     function dfehc_store_lockfree(string $key, $value, int $ttl): bool {
    47         $group = defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc';
    48         if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
     107    function dfehc_store_lockfree(string $key, $value, int $ttl): bool
     108    {
     109        $group = defined('DFEHC_CACHE_GROUP') ? (string) DFEHC_CACHE_GROUP : 'dfehc';
     110        $ttl = max(1, $ttl);
     111
     112        if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_set')) {
    49113            wp_cache_set($key, $value, $group, $ttl);
    50114            return true;
    51115        }
    52         return set_transient($key, $value, $ttl);
    53     }
    54 }
    55 
    56 if (!function_exists('dfehc_set_transient')) {
    57     function dfehc_set_transient(string $key, float $value, float $interval): void {
    58         $ttl = (int) apply_filters('dfehc_transient_ttl', max(60, (int) ceil($interval) * 2), $key, $value, $interval);
    59         $jitter = 0;
     116
     117        return function_exists('set_transient') ? (bool) set_transient($key, $value, $ttl) : false;
     118    }
     119}
     120
     121if (!function_exists('dfehc_clamp')) {
     122    function dfehc_clamp(float $v, float $lo, float $hi): float
     123    {
     124        if (!is_finite($v)) return $lo;
     125        return max($lo, min($hi, $v));
     126    }
     127}
     128
     129if (!function_exists('dfehc_abs')) {
     130    function dfehc_abs(float $v): float
     131    {
     132        return $v < 0 ? -$v : $v;
     133    }
     134}
     135
     136if (!function_exists('dfehc_rand_jitter')) {
     137    function dfehc_rand_jitter(int $min, int $max): int
     138    {
     139        $min = (int) $min;
     140        $max = (int) $max;
     141        if ($max < $min) { $t = $min; $min = $max; $max = $t; }
     142        if ($min === $max) return $min;
     143
    60144        if (function_exists('random_int')) {
    61             try {
    62                 $jitter = random_int(0, 5);
    63             } catch (\Throwable $e) {
    64                 $jitter = 0;
    65             }
    66         }
    67         $ttl += $jitter;
    68         dfehc_set_transient_noautoload($key, $value, $ttl);
    69     }
    70 }
    71 
    72 if (!function_exists('dfehc_clamp')) {
    73     function dfehc_clamp(float $v, float $lo, float $hi): float {
    74         return max($lo, min($hi, $v));
     145            try { return (int) random_int($min, $max); } catch (\Throwable $e) {}
     146        }
     147        if (function_exists('wp_rand')) return (int) wp_rand($min, $max);
     148        return (int) mt_rand($min, $max);
    75149    }
    76150}
    77151
    78152if (!function_exists('dfehc_weighted_sum')) {
    79     function dfehc_weighted_sum(array $factors, array $weights): float {
     153    function dfehc_weighted_sum(array $factors, array $weights): float
     154    {
    80155        $sum = 0.0;
    81156        foreach ($factors as $k => $v) {
    82             $sum += ((float) ($weights[$k] ?? 0.0)) * (float) $v;
    83         }
    84         return $sum;
     157            $w = isset($weights[$k]) ? (float) $weights[$k] : 0.0;
     158            $sum += $w * (float) $v;
     159        }
     160        return (float) $sum;
    85161    }
    86162}
    87163
    88164if (!function_exists('dfehc_normalize_weights')) {
    89     function dfehc_normalize_weights(array $weights): array {
    90         $total = array_sum($weights);
    91         if ($total <= 0) {
     165    function dfehc_normalize_weights(array $weights): array
     166    {
     167        $total = 0.0;
     168        foreach ($weights as $k => $w) {
     169            $weights[$k] = (float) $w;
     170            $total += $weights[$k];
     171        }
     172
     173        if ($total <= 0.0) {
    92174            $n = max(1, count($weights));
    93             $equal = 1 / $n;
     175            $equal = 1.0 / $n;
    94176            foreach ($weights as $k => $_) {
    95177                $weights[$k] = $equal;
     
    97179            return $weights;
    98180        }
     181
    99182        foreach ($weights as $k => $w) {
    100             $weights[$k] = $w / $total;
    101         }
     183            $weights[$k] = (float) ($w / $total);
     184        }
     185
    102186        return $weights;
    103187    }
    104188}
    105189
     190if (!function_exists('dfehc_should_write_value')) {
     191    function dfehc_should_write_value(?float $prev, float $next, float $eps): bool
     192    {
     193        if (!is_finite($next)) return false;
     194        if ($prev === null || !is_finite($prev)) return true;
     195        return dfehc_abs($next - $prev) >= max(0.0, $eps);
     196    }
     197}
     198
    106199if (!function_exists('dfehc_apply_exponential_moving_average')) {
    107     function dfehc_apply_exponential_moving_average(float $current): float {
    108         $alpha = dfehc_clamp((float) get_option(DFEHC_OPTION_EMA_ALPHA, DFEHC_DEFAULT_EMA_ALPHA), 0.01, 1.0);
    109         $key   = dfehc_scoped_key('dfehc_ema');
    110         $prev  = get_transient($key);
    111         $ema   = ($prev === false) ? $current : $alpha * $current + (1 - $alpha) * (float) $prev;
    112         $ttl_default = max(DFEHC_DEFAULT_EMA_TTL, (int) get_option(DFEHC_OPTION_MAX_INTERVAL, DFEHC_DEFAULT_MAX_INTERVAL) * 2);
    113         $ttl_default = (int) dfehc_clamp($ttl_default, 60, 86400);
    114         $ttl = (int) apply_filters('dfehc_ema_ttl', $ttl_default, $current, $ema);
    115         $jitter = 0;
    116         if (function_exists('random_int')) {
    117             try {
    118                 $jitter = random_int(0, 5);
    119             } catch (\Throwable $e) {
    120                 $jitter = 0;
    121             }
    122         }
    123         $ttl += $jitter;
    124         dfehc_set_transient_noautoload($key, $ema, $ttl);
    125         return $ema;
     200    function dfehc_apply_exponential_moving_average(float $current): float
     201    {
     202        static $memo = null;
     203
     204        $current = is_finite($current) ? $current : 0.0;
     205
     206        $alphaOpt = dfehc_get_option(DFEHC_OPTION_EMA_ALPHA, DFEHC_DEFAULT_EMA_ALPHA);
     207        $alpha = dfehc_clamp((float) $alphaOpt, 0.01, 1.0);
     208
     209        $key = dfehc_scoped_key('dfehc_ema');
     210
     211        $prev = null;
     212        if (is_array($memo) && array_key_exists($key, $memo)) {
     213            $prev = $memo[$key];
     214        } else {
     215            $pv = dfehc_cache_get($key);
     216            $prev = (is_numeric($pv) ? (float) $pv : null);
     217        }
     218
     219        $ema = ($prev === null) ? $current : ($alpha * $current + (1.0 - $alpha) * (float) $prev);
     220
     221        if (!is_finite($ema)) $ema = $current;
     222
     223        $ttlDefault = max((int) DFEHC_DEFAULT_EMA_TTL, (int) dfehc_get_option(DFEHC_OPTION_MAX_INTERVAL, DFEHC_DEFAULT_MAX_INTERVAL) * 2);
     224        $ttlDefault = (int) dfehc_clamp((float) $ttlDefault, 60.0, 86400.0);
     225        $ttl = (int) dfehc_apply_filters('dfehc_ema_ttl', $ttlDefault, $current, $ema);
     226        $ttl = max(30, $ttl + dfehc_rand_jitter(0, 5));
     227
     228        $eps = (float) dfehc_apply_filters('dfehc_ema_write_epsilon', 0.05, $current, $ema, $prev);
     229        $eps = max(0.0, $eps);
     230
     231        if (function_exists('dfehc_set_transient_noautoload') && dfehc_should_write_value($prev, $ema, $eps)) {
     232            dfehc_set_transient_noautoload($key, $ema, $ttl);
     233        }
     234
     235        if (!is_array($memo)) $memo = [];
     236        $memo[$key] = $ema;
     237
     238        return (float) $ema;
     239    }
     240}
     241
     242if (!function_exists('dfehc_defensive_stance')) {
     243    function dfehc_defensive_stance(float $proposed): float
     244    {
     245        static $memo = null;
     246
     247        $proposed = is_finite($proposed) ? $proposed : 0.0;
     248
     249        $key = dfehc_scoped_key('dfehc_prev_int');
     250
     251        $previous = null;
     252        if (is_array($memo) && array_key_exists($key, $memo)) {
     253            $previous = $memo[$key];
     254        } else {
     255            $pv = dfehc_cache_get($key);
     256            $previous = (is_numeric($pv) ? (float) $pv : null);
     257        }
     258
     259        if ($previous === null) {
     260            $ttl = (int) dfehc_apply_filters('dfehc_prev_interval_ttl', 1800);
     261            $ttl = max(60, $ttl + dfehc_rand_jitter(0, 5));
     262
     263            if (function_exists('dfehc_set_transient_noautoload')) {
     264                dfehc_set_transient_noautoload($key, $proposed, $ttl);
     265            }
     266
     267            if (!is_array($memo)) $memo = [];
     268            $memo[$key] = $proposed;
     269
     270            return (float) $proposed;
     271        }
     272
     273        $previous = (float) $previous;
     274
     275        $max_drop = dfehc_clamp((float) dfehc_get_option(DFEHC_OPTION_MAX_DECREASE_RATE, DFEHC_DEFAULT_MAX_DECREASE_RATE), 0.0, 0.95);
     276        $max_rise = dfehc_clamp((float) dfehc_apply_filters('dfehc_max_increase_rate', 0.5), 0.0, 5.0);
     277
     278        $lower = $previous * (1.0 - $max_drop);
     279        $upper = $previous * (1.0 + $max_rise);
     280
     281        $final = dfehc_clamp($proposed, $lower, $upper);
     282
     283        $ttl = (int) dfehc_apply_filters('dfehc_prev_interval_ttl', 1800);
     284        $ttl = max(60, $ttl + dfehc_rand_jitter(0, 5));
     285
     286        $eps = (float) dfehc_apply_filters('dfehc_prev_interval_write_epsilon', 0.5, $proposed, $final, $previous);
     287        $eps = max(0.0, $eps);
     288
     289        if (function_exists('dfehc_set_transient_noautoload') && dfehc_should_write_value($previous, $final, $eps)) {
     290            dfehc_set_transient_noautoload($key, $final, $ttl);
     291        }
     292
     293        if (!is_array($memo)) $memo = [];
     294        $memo[$key] = $final;
     295
     296        return (float) $final;
     297    }
     298}
     299
     300if (!function_exists('dfehc_set_transient')) {
     301    function dfehc_set_transient(string $key, float $value, float $interval): void
     302    {
     303        $ttlBase = max(60, (int) ceil(max(0.0, $interval)) * 2);
     304        $ttl = (int) dfehc_apply_filters('dfehc_transient_ttl', $ttlBase, $key, $value, $interval);
     305        $ttl = max(30, $ttl + dfehc_rand_jitter(0, 5));
     306
     307        if (function_exists('dfehc_set_transient_noautoload')) {
     308            dfehc_set_transient_noautoload($key, $value, $ttl);
     309        } else {
     310            dfehc_store_lockfree($key, $value, $ttl);
     311        }
     312    }
     313}
     314
     315if (!function_exists('dfehc_apply_factor_overrides')) {
     316    function dfehc_apply_factor_overrides(array $factors, float $time_elapsed, float $load_average, float $server_response_time): array
     317    {
     318        $factors = (array) dfehc_apply_filters('dfehc_interval_factors', $factors, $time_elapsed, $load_average, $server_response_time);
     319        foreach ($factors as $k => $v) {
     320            $factors[$k] = is_numeric($v) ? (float) $v : 0.0;
     321        }
     322        return $factors;
    126323    }
    127324}
    128325
    129326if (!function_exists('dfehc_calculate_recommended_interval')) {
    130     function dfehc_calculate_recommended_interval(float $time_elapsed, float $load_average, float $server_response_time): float {
    131         $min_interval      = max(1, (int) get_option(DFEHC_OPTION_MIN_INTERVAL, DFEHC_DEFAULT_MIN_INTERVAL));
    132         $max_interval      = max($min_interval, (int) get_option(DFEHC_OPTION_MAX_INTERVAL, DFEHC_DEFAULT_MAX_INTERVAL));
    133         $max_server_load   = max(0.1, (float) get_option(DFEHC_OPTION_MAX_SERVER_LOAD, DFEHC_DEFAULT_MAX_SERVER_LOAD));
    134         $max_response_time = max(0.1, (float) get_option(DFEHC_OPTION_MAX_RESPONSE_TIME, DFEHC_DEFAULT_MAX_RESPONSE_TIME));
    135 
    136         $custom_norm = apply_filters('dfehc_normalize_load', null, $load_average);
     327    function dfehc_calculate_recommended_interval(float $time_elapsed, float $load_average, float $server_response_time): float
     328    {
     329        $min_interval = max(1, (int) dfehc_get_option(DFEHC_OPTION_MIN_INTERVAL, DFEHC_DEFAULT_MIN_INTERVAL));
     330        $max_interval = max($min_interval, (int) dfehc_get_option(DFEHC_OPTION_MAX_INTERVAL, DFEHC_DEFAULT_MAX_INTERVAL));
     331
     332        $max_server_load = (float) dfehc_get_option(DFEHC_OPTION_MAX_SERVER_LOAD, DFEHC_DEFAULT_MAX_SERVER_LOAD);
     333        $max_server_load = max(0.1, $max_server_load);
     334
     335        $max_response_time = (float) dfehc_get_option(DFEHC_OPTION_MAX_RESPONSE_TIME, DFEHC_DEFAULT_MAX_RESPONSE_TIME);
     336        $max_response_time = max(0.1, $max_response_time);
     337
     338        $time_elapsed = max(0.0, $time_elapsed);
     339        $load_average = is_finite($load_average) ? $load_average : 0.0;
     340        $server_response_time = is_finite($server_response_time) ? $server_response_time : 0.0;
     341
     342        $custom_norm = dfehc_apply_filters('dfehc_normalize_load', null, $load_average);
    137343        if (is_numeric($custom_norm)) {
    138344            $la = dfehc_clamp((float) $custom_norm, 0.0, 1.0);
    139345        } else {
    140346            if ($load_average <= 1.0) {
    141                 $la = dfehc_clamp($load_average, 0.0, 1.0);
     347                $la = dfehc_clamp((float) $load_average, 0.0, 1.0);
    142348            } elseif ($load_average <= 100.0) {
    143                 $la = dfehc_clamp($load_average / 100.0, 0.0, 1.0);
     349                $la = dfehc_clamp((float) $load_average / 100.0, 0.0, 1.0);
    144350            } else {
    145                 $assumed_cores = (float) apply_filters('dfehc_assumed_cores_for_normalization', 8.0);
    146                 $la = dfehc_clamp($load_average / max(1.0, $assumed_cores), 0.0, 1.0);
     351                $assumed_cores = (float) dfehc_apply_filters('dfehc_assumed_cores_for_normalization', 8.0);
     352                $la = dfehc_clamp((float) $load_average / max(1.0, $assumed_cores), 0.0, 1.0);
    147353            }
    148354        }
    149355
    150356        $msl_ratio = $max_server_load > 1.0 ? ($max_server_load / 100.0) : $max_server_load;
    151         if ($msl_ratio <= 0) $msl_ratio = 1.0;
     357        if (!is_finite($msl_ratio) || $msl_ratio <= 0.0) $msl_ratio = 1.0;
     358
    152359        $server_load_factor = dfehc_clamp($la / $msl_ratio, 0.0, 1.0);
    153360
    154         $rt_units = apply_filters('dfehc_response_time_is_ms', null, $server_response_time);
     361        $rt_units = dfehc_apply_filters('dfehc_response_time_is_ms', null, $server_response_time);
    155362        $rt = (float) $server_response_time;
    156363        if ($rt_units === true) {
    157364            $rt = $rt / 1000.0;
    158365        } elseif ($rt_units === null) {
    159             if ($rt > ($max_response_time * 3) && $rt <= 60000.0) {
     366            if ($rt > ($max_response_time * 3.0) && $rt <= 60000.0) {
    160367                $rt = $rt / 1000.0;
    161368            }
    162369        }
    163         $rt = max(0.0, $rt);
    164         $response_time_factor = $rt > 0 ? dfehc_clamp($rt / $max_response_time, 0.0, 1.0) : 0.0;
     370        $rt = max(0.0, (float) $rt);
     371
     372        $response_time_factor = $rt > 0.0 ? dfehc_clamp($rt / $max_response_time, 0.0, 1.0) : 0.0;
    165373
    166374        $factors = [
    167             'user_activity' => dfehc_clamp($time_elapsed / $max_interval, 0.0, 1.0),
     375            'user_activity' => dfehc_clamp(($max_interval > 0 ? $time_elapsed / $max_interval : 0.0), 0.0, 1.0),
    168376            'server_load'   => $server_load_factor,
    169377            'response_time' => $response_time_factor,
    170378        ];
    171         $factors = (array) apply_filters('dfehc_interval_factors', $factors, $time_elapsed, $load_average, $server_response_time);
    172 
    173         $slider  = dfehc_clamp((float) get_option(DFEHC_OPTION_PRIORITY_SLIDER, 0.0), -1.0, 1.0);
     379        $factors = dfehc_apply_factor_overrides($factors, $time_elapsed, $load_average, $server_response_time);
     380
     381        $slider = dfehc_clamp((float) dfehc_get_option(DFEHC_OPTION_PRIORITY_SLIDER, 0.0), -1.0, 1.0);
     382
    174383        $weights = [
    175384            'user_activity' => 0.4 - 0.2 * $slider,
    176             'server_load'   => (0.6 + 0.2 * $slider) / 2,
    177             'response_time' => (0.6 + 0.2 * $slider) / 2,
     385            'server_load'   => (0.6 + 0.2 * $slider) / 2.0,
     386            'response_time' => (0.6 + 0.2 * $slider) / 2.0,
    178387        ];
    179         $weights = (array) apply_filters('dfehc_interval_weights', $weights, $slider);
     388        $weights = (array) dfehc_apply_filters('dfehc_interval_weights', $weights, $slider);
    180389        $weights = dfehc_normalize_weights($weights);
    181390
    182         $raw      = $min_interval + dfehc_weighted_sum($factors, $weights) * ($max_interval - $min_interval);
     391        $raw = (float) ($min_interval + dfehc_weighted_sum($factors, $weights) * ($max_interval - $min_interval));
     392        $raw = dfehc_clamp($raw, (float) $min_interval, (float) $max_interval);
     393
    183394        $smoothed = dfehc_apply_exponential_moving_average($raw);
    184         $lagged   = dfehc_defensive_stance($smoothed);
    185 
    186         $final = (float) apply_filters('dfehc_interval_snap', $lagged, $min_interval, $max_interval);
    187         $final = dfehc_clamp($final, (float) $min_interval, (float) $max_interval);
    188 
    189         return $final;
     395        $lagged = dfehc_defensive_stance($smoothed);
     396
     397        $final = (float) dfehc_apply_filters('dfehc_interval_snap', $lagged, $min_interval, $max_interval);
     398        $final = dfehc_clamp((float) $final, (float) $min_interval, (float) $max_interval);
     399
     400        return (float) $final;
    190401    }
    191402}
    192403
    193404if (!function_exists('dfehc_calculate_interval_based_on_duration')) {
    194     function dfehc_calculate_interval_based_on_duration(float $avg_duration, float $load_average): float {
    195         $min_interval = max(1, (int) get_option(DFEHC_OPTION_MIN_INTERVAL, DFEHC_DEFAULT_MIN_INTERVAL));
    196         $max_interval = max($min_interval, (int) get_option(DFEHC_OPTION_MAX_INTERVAL, DFEHC_DEFAULT_MAX_INTERVAL));
    197         if ($avg_duration <= $min_interval) return (float) $min_interval;
    198         if ($avg_duration >= $max_interval) return (float) $max_interval;
    199         $proposed = dfehc_calculate_recommended_interval($avg_duration, $load_average, 0.0);
    200         return dfehc_defensive_stance($proposed);
     405    function dfehc_calculate_interval_based_on_duration(float $avg_duration, float $load_average): float
     406    {
     407    $min_interval = max(1, (int) dfehc_get_option(DFEHC_OPTION_MIN_INTERVAL, DFEHC_DEFAULT_MIN_INTERVAL));
     408    $max_interval = max($min_interval, (int) dfehc_get_option(DFEHC_OPTION_MAX_INTERVAL, DFEHC_DEFAULT_MAX_INTERVAL));
     409
     410    if (!is_finite($avg_duration)) $avg_duration = 0.0;
     411    if (!is_finite($load_average)) $load_average = 0.0;
     412
     413    if ($avg_duration <= $min_interval) return (float) $min_interval;
     414    if ($avg_duration >= $max_interval) return (float) $max_interval;
     415
     416    return (float) dfehc_calculate_recommended_interval($avg_duration, $load_average, 0.0);
    201417    }
    202418}
    203419
    204420if (!function_exists('dfehc_smooth_moving')) {
    205     function dfehc_smooth_moving(array $values): float {
     421    function dfehc_smooth_moving(array $values): float
     422    {
    206423        if (!$values) return 0.0;
    207         $window = max(1, (int) get_option(DFEHC_OPTION_SMA_WINDOW, DFEHC_DEFAULT_SMA_WINDOW));
     424        $window = max(1, (int) dfehc_get_option(DFEHC_OPTION_SMA_WINDOW, DFEHC_DEFAULT_SMA_WINDOW));
    208425        $subset = array_slice($values, -$window);
    209426        if (!$subset) return 0.0;
    210         return array_sum($subset) / count($subset);
    211     }
    212 }
    213 
    214 if (!function_exists('dfehc_defensive_stance')) {
    215     function dfehc_defensive_stance(float $proposed): float {
    216         $key      = dfehc_scoped_key('dfehc_prev_int');
    217         $previous = get_transient($key);
    218         if ($previous === false) {
    219             $ttl = (int) apply_filters('dfehc_prev_interval_ttl', 1800);
    220             $jitter = 0;
    221             if (function_exists('random_int')) {
    222                 try {
    223                     $jitter = random_int(0, 5);
    224                 } catch (\Throwable $e) {
    225                     $jitter = 0;
    226                 }
    227             }
    228             $ttl += $jitter;
    229             dfehc_set_transient_noautoload($key, $proposed, $ttl);
    230             return $proposed;
    231         }
    232         $previous  = (float) $previous;
    233         $max_drop  = dfehc_clamp((float) get_option(DFEHC_OPTION_MAX_DECREASE_RATE, DFEHC_DEFAULT_MAX_DECREASE_RATE), 0.0, 0.95);
    234         $max_rise  = dfehc_clamp((float) apply_filters('dfehc_max_increase_rate', 0.5), 0.0, 5.0);
    235         $lower     = $previous * (1 - $max_drop);
    236         $upper     = $previous * (1 + $max_rise);
    237         $final     = dfehc_clamp($proposed, $lower, $upper);
    238         $ttl = (int) apply_filters('dfehc_prev_interval_ttl', 1800);
    239         $jitter = 0;
    240         if (function_exists('random_int')) {
    241             try {
    242                 $jitter = random_int(0, 5);
    243             } catch (\Throwable $e) {
    244                 $jitter = 0;
    245             }
    246         }
    247         $ttl += $jitter;
    248         dfehc_set_transient_noautoload($key, $final, $ttl);
    249         return $final;
    250     }
    251 }
     427
     428        $sum = 0.0;
     429        $cnt = 0;
     430        foreach ($subset as $v) {
     431            if (is_numeric($v) && is_finite((float) $v)) {
     432                $sum += (float) $v;
     433                $cnt++;
     434            }
     435        }
     436        return $cnt > 0 ? (float) ($sum / $cnt) : 0.0;
     437    }
     438}
  • dynamic-front-end-heartbeat-control/trunk/engine/server-load.php

    r3427163 r3461136  
    99}
    1010
    11 if (!defined('DFEHC_CACHE_GROUP')) {
    12     define('DFEHC_CACHE_GROUP', 'dfehc');
    13 }
    14 if (!defined('DFEHC_SERVER_LOAD_TTL')) {
    15     define('DFEHC_SERVER_LOAD_TTL', 180);
    16 }
    17 if (!defined('DFEHC_SERVER_LOAD_CACHE_KEY')) {
    18     define('DFEHC_SERVER_LOAD_CACHE_KEY', 'dfehc:server_load');
    19 }
    20 if (!defined('DFEHC_SERVER_LOAD_PAYLOAD_KEY')) {
    21     define('DFEHC_SERVER_LOAD_PAYLOAD_KEY', 'dfehc_server_load_payload');
    22 }
     11if (!defined('DFEHC_CACHE_GROUP')) define('DFEHC_CACHE_GROUP', 'dfehc');
     12if (!defined('DFEHC_SERVER_LOAD_TTL')) define('DFEHC_SERVER_LOAD_TTL', 180);
     13if (!defined('DFEHC_SERVER_LOAD_CACHE_KEY')) define('DFEHC_SERVER_LOAD_CACHE_KEY', 'dfehc:server_load');
     14if (!defined('DFEHC_SERVER_LOAD_PAYLOAD_KEY')) define('DFEHC_SERVER_LOAD_PAYLOAD_KEY', 'dfehc_server_load_payload');
     15if (!defined('DFEHC_SERVER_LOAD_SCALAR_KEY')) define('DFEHC_SERVER_LOAD_SCALAR_KEY', 'dfehc:server_load_scalar');
    2316
    2417if (!function_exists('dfehc_server_load_ttl')) {
    2518    function dfehc_server_load_ttl(): int
    2619    {
    27         return (int) apply_filters('dfehc_server_load_ttl', (int) DFEHC_SERVER_LOAD_TTL);
     20        $ttl = (int) apply_filters('dfehc_server_load_ttl', (int) DFEHC_SERVER_LOAD_TTL);
     21        return max(30, $ttl);
    2822    }
    2923}
     
    5852    {
    5953        static $t = '';
    60         if ($t !== '') return $t;
    61         $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown'));
     54        if ($t !== '') {
     55            return $t;
     56        }
     57        $host = @php_uname('n') ?: (defined('WP_HOME') ? (string) WP_HOME : (function_exists('home_url') ? (string) home_url() : 'unknown'));
    6258        $salt = defined('DB_NAME') ? (string) DB_NAME : '';
    6359        return $t = substr(md5((string) $host . $salt), 0, 10);
     
    9086        $is_valid_ip = static function (string $ip, bool $publicOnly): bool {
    9187            $ip = trim($ip);
    92             if ($ip === '') return false;
    93 
     88            if ($ip === '') {
     89                return false;
     90            }
    9491            if ($publicOnly) {
    9592                return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
     
    148145}
    149146
    150 if (!function_exists('dfehc_get_redis_server')) {
    151     function dfehc_get_redis_server(): string
    152     {
    153         $h = getenv('REDIS_HOST');
    154         return $h ? (string) $h : '127.0.0.1';
    155     }
    156 }
    157 if (!function_exists('dfehc_get_redis_port')) {
    158     function dfehc_get_redis_port(): int
    159     {
    160         $p = getenv('REDIS_PORT');
    161         return $p && ctype_digit((string) $p) ? (int) $p : 6379;
    162     }
    163 }
    164 if (!function_exists('dfehc_get_memcached_server')) {
    165     function dfehc_get_memcached_server(): string
    166     {
    167         $h = getenv('MEMCACHED_HOST');
    168         return $h ? (string) $h : '127.0.0.1';
    169     }
    170 }
    171 if (!function_exists('dfehc_get_memcached_port')) {
    172     function dfehc_get_memcached_port(): int
    173     {
    174         $p = getenv('MEMCACHED_PORT');
    175         return $p && ctype_digit((string) $p) ? (int) $p : 11211;
    176     }
    177 }
    178 
    179147function dfehc_get_cache_client(): array
    180148{
     
    182150    static $last_probe_ts = 0;
    183151
    184     $retryAfter = (int) apply_filters('dfehc_cache_retry_after', 60);
     152    $retryAfter = (int) apply_filters('dfehc_cache_retry_after', 90);
    185153    $retryAfter = max(0, $retryAfter);
    186154
     
    204172    }
    205173
    206     if (class_exists('Redis')) {
    207         try {
    208             $redis = new Redis();
    209             if ($redis->connect(dfehc_get_redis_server(), dfehc_get_redis_port(), 1.0)) {
    210                 $pass = apply_filters('dfehc_redis_auth', getenv('REDIS_PASSWORD') ?: null);
    211                 $user = apply_filters('dfehc_redis_user', getenv('REDIS_USERNAME') ?: null);
    212 
    213                 if (method_exists($redis, 'auth')) {
    214                     if ($user && $pass) {
    215                         $ok = $redis->auth([$user, $pass]);
    216                         if ($ok === false) {
    217                             throw new RuntimeException('Redis auth failed');
    218                         }
    219                     } elseif ($pass) {
    220                         $ok = $redis->auth($pass);
    221                         if ($ok === false) {
    222                             throw new RuntimeException('Redis auth failed');
    223                         }
    224                     }
    225                 }
    226 
    227                 return $cached = ['client' => $redis, 'type' => 'redis'];
    228             }
    229         } catch (Throwable $e) {
    230             dfehc_debug_log('DFEHC Redis connect error: ' . $e->getMessage());
    231         }
    232     }
    233 
    234     if (class_exists('Memcached')) {
    235         try {
    236             $mc = new Memcached('dfehc');
    237             if (!$mc->getServerList()) {
    238                 $mc->addServer(dfehc_get_memcached_server(), dfehc_get_memcached_port());
    239             }
    240 
    241             $user = getenv('MEMCACHED_USERNAME');
    242             $pass = getenv('MEMCACHED_PASSWORD');
    243             if ($user && $pass && method_exists($mc, 'setSaslAuthData')) {
    244                 $mc->setOption(Memcached::OPT_BINARY_PROTOCOL, true);
    245                 $mc->setSaslAuthData($user, $pass);
    246             }
    247 
    248             $versions = $mc->getVersion();
    249             $first = is_array($versions) ? reset($versions) : false;
    250             $ok = $first && $first !== '0.0.0' && $first !== '0.0.0.0';
    251             if ($ok) {
    252                 return $cached = ['client' => $mc, 'type' => 'memcached'];
    253             }
    254         } catch (Throwable $e) {
    255             dfehc_debug_log('DFEHC Memcached connect error: ' . $e->getMessage());
    256         }
    257     }
    258 
    259174    return $cached = ['client' => null, 'type' => 'none'];
    260175}
     
    262177function dfehc_cache_server_load(float $value): void
    263178{
    264     ['client' => $client, 'type' => $type] = dfehc_get_cache_client();
    265     if (!$client) {
    266         return;
    267     }
    268     $key = dfehc_key(DFEHC_SERVER_LOAD_CACHE_KEY);
     179    $value = max(0.0, (float) $value);
     180
    269181    $ttl = dfehc_server_load_ttl();
    270182    $jitter = 0;
    271183    if (function_exists('random_int')) {
    272184        try {
    273             $jitter = random_int(0, 5);
     185            $jitter = random_int(0, 8);
    274186        } catch (Throwable $e) {
    275187            $jitter = 0;
    276188        }
    277189    }
    278     $ttl += $jitter;
     190    $ttl = max(30, $ttl + $jitter);
     191
     192    $scalarKey = dfehc_key(DFEHC_SERVER_LOAD_SCALAR_KEY);
     193
     194    if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_set')) {
     195        wp_cache_set($scalarKey, $value, DFEHC_CACHE_GROUP, $ttl);
     196    }
     197
     198    $key = dfehc_key(DFEHC_SERVER_LOAD_CACHE_KEY);
     199
     200    if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_set')) {
     201        wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $ttl);
     202    }
     203
     204    $cc = dfehc_get_cache_client();
     205    $client = isset($cc['client']) ? $cc['client'] : null;
     206    $type = isset($cc['type']) ? (string) $cc['type'] : 'none';
     207
     208    if (!$client) {
     209        return;
     210    }
     211
    279212    try {
    280213        if ($type === 'redis') {
     
    291224{
    292225    $payloadKey = dfehc_key(DFEHC_SERVER_LOAD_PAYLOAD_KEY);
     226
    293227    $payload = null;
    294     if (wp_using_ext_object_cache()) {
     228    if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
    295229        $payload = wp_cache_get($payloadKey, DFEHC_CACHE_GROUP);
    296230        if ($payload === false) {
     
    300234        $payload = get_transient($payloadKey);
    301235    }
     236
    302237    if (!(is_array($payload) && isset($payload['raw'], $payload['cores'], $payload['source']))) {
    303238        if (!dfehc_load_acquire_lock()) {
     239            $fallback = dfehc_get_server_load_persistent();
     240            if ($fallback > 0.0) {
     241                return (float) $fallback;
     242            }
    304243            return dfehc_unknown_load();
    305244        }
     245
    306246        try {
    307247            $data = dfehc_detect_load_raw_with_source();
    308248            $payload = [
    309                 'raw' => (float) $data['load'],
    310                 'cores' => dfehc_get_cpu_cores(),
     249                'raw'    => (float) $data['load'],
     250                'cores'  => dfehc_get_cpu_cores(),
    311251                'source' => (string) $data['source'],
    312252            ];
     253
    313254            $ttl = dfehc_server_load_ttl();
    314255            $jitter = 0;
    315256            if (function_exists('random_int')) {
    316257                try {
    317                     $jitter = random_int(0, 5);
     258                    $jitter = random_int(0, 8);
    318259                } catch (Throwable $e) {
    319260                    $jitter = 0;
    320261                }
    321262            }
    322             $ttl += $jitter;
    323             if (wp_using_ext_object_cache()) {
     263            $ttl = max(30, $ttl + $jitter);
     264
     265            if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
    324266                wp_cache_set($payloadKey, $payload, DFEHC_CACHE_GROUP, $ttl);
    325267            } else {
     
    330272        }
    331273    }
    332     $raw = (float) $payload['raw'];
    333     $cores = (int) ($payload['cores'] ?: dfehc_get_cpu_cores());
     274
     275    $raw    = (float) $payload['raw'];
     276    $cores  = (int) ($payload['cores'] ?: dfehc_get_cpu_cores());
    334277    $source = (string) $payload['source'];
     278
    335279    $divide = (bool) apply_filters('dfehc_divide_cpu_load', true, $raw, $cores, $source);
    336     $load = ($source === 'cpu_load' && $divide && $cores > 0) ? $raw / $cores : $raw;
     280    $load = ($source === 'cpu_load' && $divide && $cores > 0) ? ($raw / $cores) : $raw;
     281
    337282    $load = max(0.0, (float) $load);
    338     return (float) apply_filters('dfehc_contextual_load_value', $load, $source);
     283
     284    $max = apply_filters('dfehc_server_load_max', null, $source);
     285    if (is_numeric($max)) {
     286        $load = min((float) $max, $load);
     287    }
     288
     289    $load = (float) apply_filters('dfehc_contextual_load_value', $load, $source);
     290
     291    if ($load > 0.0) {
     292        dfehc_cache_server_load($load);
     293    }
     294
     295    return (float) $load;
    339296}
    340297
     
    343300    if (function_exists('sys_getloadavg')) {
    344301        $arr = sys_getloadavg();
    345         if ($arr && isset($arr[0]) && $arr[0] >= 0) {
     302        if (is_array($arr) && isset($arr[0]) && is_numeric($arr[0]) && (float) $arr[0] >= 0.0) {
    346303            return ['load' => (float) $arr[0], 'source' => 'cpu_load'];
    347304        }
    348305    }
     306
    349307    if (is_readable('/proc/loadavg')) {
    350         $txt = file_get_contents('/proc/loadavg');
     308        $txt = @file_get_contents('/proc/loadavg');
    351309        if ($txt !== false) {
    352310            $parts = explode(' ', trim($txt));
    353             if (isset($parts[0])) {
     311            if (isset($parts[0]) && is_numeric($parts[0]) && (float) $parts[0] >= 0.0) {
    354312                return ['load' => (float) $parts[0], 'source' => 'cpu_load'];
    355313            }
    356314        }
    357315    }
    358     $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
    359     if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true) && !ini_get('open_basedir')) {
    360         $out = shell_exec('LANG=C uptime 2>&1');
    361         if ($out && preg_match('/load average[s]?:\s*([0-9.]+)/', $out, $m)) {
    362             return ['load' => (float) $m[1], 'source' => 'cpu_load'];
    363         }
    364     }
     316
     317    $allow_shell = (bool) apply_filters('dfehc_allow_shell_uptime', false);
     318    if ($allow_shell) {
     319        $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
     320        $can_shell = function_exists('shell_exec') && !in_array('shell_exec', $disabled, true) && !ini_get('open_basedir');
     321        if ($can_shell) {
     322            $out = @shell_exec('LANG=C uptime 2>&1');
     323            if (is_string($out) && $out !== '' && preg_match('/load average[s]?:\s*([0-9.]+)/', $out, $m)) {
     324                $v = (float) $m[1];
     325                if ($v >= 0.0) {
     326                    return ['load' => $v, 'source' => 'cpu_load'];
     327                }
     328            }
     329        }
     330    }
     331
     332    $estimator_loaded = false;
     333
    365334    if (defined('DFEHC_PLUGIN_PATH')) {
    366335        $est = rtrim((string) DFEHC_PLUGIN_PATH, "/\\") . '/defibrillator/load-estimator.php';
    367         if (file_exists($est)) {
     336        if (is_string($est) && $est !== '' && file_exists($est)) {
    368337            require_once $est;
    369             if (class_exists('DynamicHeartbeat\\Dfehc_ServerLoadEstimator')) {
    370                 $pct = DynamicHeartbeat\Dfehc_ServerLoadEstimator::get_server_load();
    371                 if (is_numeric($pct)) {
    372                     $cores = dfehc_get_cpu_cores();
    373                     $cpuLoad = ((float) $pct / 100.0) * max(1, $cores);
    374                     return ['load' => $cpuLoad, 'source' => 'cpu_load'];
    375                 }
    376             }
    377         }
    378     }
     338            $estimator_loaded = true;
     339        }
     340    }
     341
     342    if (!$estimator_loaded && class_exists('DynamicHeartbeat\\Dfehc_ServerLoadEstimator')) {
     343        $estimator_loaded = true;
     344    }
     345
     346    if ($estimator_loaded && class_exists('DynamicHeartbeat\\Dfehc_ServerLoadEstimator')) {
     347        $pct = DynamicHeartbeat\Dfehc_ServerLoadEstimator::get_server_load();
     348        if (is_numeric($pct)) {
     349            $cores = dfehc_get_cpu_cores();
     350            $raw = ((float) $pct / 100.0) * max(1, $cores);
     351            $raw = max(0.0, $raw);
     352            return ['load' => $raw, 'source' => 'cpu_load'];
     353        }
     354    }
     355
    379356    if (function_exists('do_action')) {
    380357        do_action('dfehc_load_detection_fell_back');
    381358    }
     359
    382360    return ['load' => dfehc_unknown_load(), 'source' => 'fallback'];
    383361}
     
    390368            return (int) $cores;
    391369        }
    392         $override = getenv('DFEHC_CPU_CORES');
    393         if ($override && ctype_digit((string) $override) && (int) $override > 0) {
     370
     371        $override = getenv('DFEHC_CPU_CORES') ?: (defined('DFEHC_CPU_CORES') ? DFEHC_CPU_CORES : null);
     372        if ($override !== null && is_numeric($override) && (int) $override > 0) {
    394373            $cores = (int) $override;
    395374            return (int) $cores;
    396375        }
     376
     377        $cores = 1;
     378
    397379        if (is_readable('/sys/fs/cgroup/cpu.max')) {
    398             $line = file_get_contents('/sys/fs/cgroup/cpu.max');
    399             if ($line !== false) {
    400                 [$quota, $period] = explode(' ', trim($line));
    401                 if ($quota !== 'max') {
    402                     $q = (int) $quota;
    403                     $p = (int) $period;
    404                     if ($q > 0 && $p > 0) {
    405                         $cores = max(1, (int) ceil($q / $p));
    406                         $cores = (int) apply_filters('dfehc_cpu_cores', (int) $cores);
    407                         return (int) $cores;
     380            $line = trim((string) @file_get_contents('/sys/fs/cgroup/cpu.max'));
     381            if ($line !== '') {
     382                $parts = preg_split('/\s+/', $line);
     383                if (is_array($parts) && count($parts) >= 2) {
     384                    $quota  = is_numeric($parts[0]) ? (float) $parts[0] : 0.0;
     385                    $period = is_numeric($parts[1]) ? (float) $parts[1] : 0.0;
     386                    if ($period > 0.0 && $quota > 0.0) {
     387                        $cores = max(1, (int) ceil($quota / $period));
    408388                    }
    409389                }
    410390            }
    411391        }
    412         if (is_readable('/proc/self/cgroup')) {
    413             $content = file_get_contents('/proc/self/cgroup');
    414             if ($content !== false && preg_match('/^[0-9]+:[^:]*cpu[^:]*:(.+)$/m', $content, $m)) {
    415                 $path = '/' . ltrim(trim($m[1]), '/');
    416                 $base = '/sys/fs/cgroup' . $path;
    417                 $quotaFile = "$base/cpu.cfs_quota_us";
    418                 $periodFile = "$base/cpu.cfs_period_us";
    419                 if (is_readable($quotaFile) && is_readable($periodFile)) {
    420                     $quota = (int) file_get_contents($quotaFile);
    421                     $period = (int) file_get_contents($periodFile);
    422                     if ($quota > 0 && $period > 0) {
    423                         $cores = max(1, (int) ceil($quota / $period));
    424                         $cores = (int) apply_filters('dfehc_cpu_cores', (int) $cores);
    425                         return (int) $cores;
     392
     393        if ($cores === 1 && is_readable('/proc/self/cgroup')) {
     394            $content = (string) @file_get_contents('/proc/self/cgroup');
     395            if ($content !== '') {
     396                if (preg_match('/0::\/(.+)$/m', $content, $m) || preg_match('/cpu[^:]*:(.+)$/m', $content, $m)) {
     397                    $rel_path = '/' . ltrim(trim($m[1]), '/');
     398                    $base = '/sys/fs/cgroup' . $rel_path;
     399                    $quotaFile = $base . '/cpu.max';
     400                    if (is_readable($quotaFile)) {
     401                        $line = trim((string) @file_get_contents($quotaFile));
     402                        if ($line !== '') {
     403                            $parts = preg_split('/\s+/', $line);
     404                            if (is_array($parts) && count($parts) >= 2) {
     405                                $quota  = is_numeric($parts[0]) ? (float) $parts[0] : 0.0;
     406                                $period = is_numeric($parts[1]) ? (float) $parts[1] : 0.0;
     407                                if ($period > 0.0 && $quota > 0.0) {
     408                                    $cores = max(1, (int) ceil($quota / $period));
     409                                }
     410                            }
     411                        }
    426412                    }
    427413                }
    428414            }
    429415        }
    430         if (is_readable('/sys/fs/cgroup/cpu/cpu.cfs_quota_us') && is_readable('/sys/fs/cgroup/cpu/cpu.cfs_period_us')) {
    431             $quota = (int) file_get_contents('/sys/fs/cgroup/cpu/cpu.cfs_quota_us');
    432             $period = (int) file_get_contents('/sys/fs/cgroup/cpu/cpu.cfs_period_us');
    433             if ($quota > 0 && $period > 0) {
    434                 $cores = max(1, (int) ceil($quota / $period));
    435                 $cores = (int) apply_filters('dfehc_cpu_cores', (int) $cores);
    436                 return (int) $cores;
    437             }
    438         }
    439         $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
    440         if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true) && !ini_get('open_basedir')) {
    441             $n = shell_exec('nproc 2>/dev/null');
    442             if ($n && ctype_digit(trim((string) $n))) {
    443                 $cores = max(1, (int) trim((string) $n));
    444                 $cores = (int) apply_filters('dfehc_cpu_cores', (int) $cores);
    445                 return (int) $cores;
    446             }
    447         }
    448         if (is_readable('/proc/cpuinfo')) {
    449             $info = file_get_contents('/proc/cpuinfo');
    450             if ($info !== false) {
    451                 $cnt = preg_match_all('/^processor/m', $info);
    452                 if ($cnt) {
    453                     $cores = (int) $cnt;
    454                     $cores = (int) apply_filters('dfehc_cpu_cores', (int) $cores);
    455                     return (int) $cores;
     416
     417        if ($cores === 1 && is_readable('/proc/cpuinfo')) {
     418            $info = (string) @file_get_contents('/proc/cpuinfo');
     419            if ($info !== '') {
     420                $cnt = preg_match_all('/^processor\s*:/m', $info);
     421                if ($cnt > 0) {
     422                    $cores = $cnt;
    456423                }
    457424            }
    458425        }
    459         $cores = 1;
    460         $cores = (int) apply_filters('dfehc_cpu_cores', (int) $cores);
     426
     427        $cores = (int) apply_filters('dfehc_cpu_cores', max(1, $cores));
    461428        return (int) $cores;
    462429    }
     
    465432function dfehc_log_server_load(): void
    466433{
     434    if (!apply_filters('dfehc_enable_load_logging', false)) {
     435        return;
     436    }
     437
    467438    $load = dfehc_get_server_load();
    468439    $optKey = 'dfehc_server_load_logs_' . dfehc_blog_id() . '_' . dfehc_host_token();
     440
     441    $max_entries = (int) apply_filters('dfehc_load_log_max_entries', 720);
     442    $max_entries = max(50, min(5000, $max_entries));
     443
     444    $retention = (int) apply_filters('dfehc_load_log_retention_seconds', DAY_IN_SECONDS);
     445    $retention = max(300, $retention);
     446
     447    $max_bytes = (int) apply_filters('dfehc_load_log_option_max_bytes', 1048576);
     448    $max_bytes = max(65536, $max_bytes);
     449
     450    global $wpdb;
     451    if ($wpdb instanceof wpdb && !empty($wpdb->options)) {
     452        $len = $wpdb->get_var($wpdb->prepare(
     453            "SELECT LENGTH(option_value) FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
     454            $optKey
     455        ));
     456        if (is_numeric($len) && (int) $len > $max_bytes) {
     457            update_option($optKey, [['timestamp' => time(), 'load' => (float) $load]], false);
     458            $wpdb->query($wpdb->prepare(
     459                "UPDATE {$wpdb->options} SET autoload='no' WHERE option_name=%s AND autoload<>'no' LIMIT 1",
     460                $optKey
     461            ));
     462            return;
     463        }
     464    }
     465
    469466    $logs = get_option($optKey, []);
    470467    if (!is_array($logs)) {
    471468        $logs = [];
    472469    }
     470
    473471    $now = time();
    474     $cutoff = $now - DAY_IN_SECONDS;
    475     $logs = array_filter(
    476         $logs,
    477         static function (array $row) use ($cutoff): bool {
    478             return isset($row['timestamp']) && $row['timestamp'] >= $cutoff;
    479         }
    480     );
    481     if (count($logs) > 2000) {
    482         $logs = array_slice($logs, -2000);
    483     }
    484     $logs[] = ['timestamp' => $now, 'load' => $load];
     472    $cutoff = $now - $retention;
     473
     474    $logs = array_filter($logs, static function ($row) use ($cutoff): bool {
     475        return is_array($row) && isset($row['timestamp']) && is_numeric($row['timestamp']) && (int) $row['timestamp'] >= $cutoff;
     476    });
     477
     478    if (count($logs) > $max_entries) {
     479        $logs = array_slice($logs, -$max_entries);
     480    }
     481
     482    $logs[] = ['timestamp' => $now, 'load' => (float) $load];
     483
     484    if (count($logs) > $max_entries) {
     485        $logs = array_slice($logs, -$max_entries);
     486    }
     487
    485488    update_option($optKey, array_values($logs), false);
     489
     490    if ($wpdb instanceof wpdb && !empty($wpdb->options)) {
     491        $wpdb->query($wpdb->prepare(
     492            "UPDATE {$wpdb->options} SET autoload='no' WHERE option_name=%s AND autoload<>'no' LIMIT 1",
     493            $optKey
     494        ));
     495    }
    486496}
    487497add_action('dfehc_log_server_load_hook', 'dfehc_log_server_load');
     
    491501    {
    492502        $allow_public = apply_filters('dfehc_allow_public_server_load', false);
     503
    493504        if (!$allow_public) {
    494505            $action = 'get_server_load';
     
    520531            $ip = dfehc_client_ip();
    521532            $rk = dfehc_key('dfehc_rl_' . md5($ip));
     533
     534            $limit = (int) apply_filters('dfehc_public_rate_limit', 30);
     535            $win   = (int) apply_filters('dfehc_public_rate_window', 60);
     536            $limit = max(1, $limit);
     537            $win   = max(1, $win);
     538
    522539            $cnt = (int) get_transient($rk);
    523             $limit = (int) apply_filters('dfehc_public_rate_limit', 60);
    524             $win   = (int) apply_filters('dfehc_public_rate_window', 60);
    525540            if ($cnt >= $limit) {
    526541                wp_send_json_error(['message' => 'rate_limited'], 429);
     
    528543            set_transient($rk, $cnt + 1, $win);
    529544        }
     545
    530546        nocache_headers();
    531547        wp_send_json_success(dfehc_get_server_load_persistent());
     
    534550add_action('wp_ajax_get_server_load', 'dfehc_get_server_load_ajax_handler');
    535551
    536 add_action('init', static function (): void {
     552add_action('init', function (): void {
    537553    if (apply_filters('dfehc_allow_public_server_load', false)) {
    538554        add_action('wp_ajax_nopriv_get_server_load', 'dfehc_get_server_load_ajax_handler');
     
    546562        return (float) $cached;
    547563    }
    548     ['client' => $client] = dfehc_get_cache_client();
     564
     565    $scalarKey = dfehc_key(DFEHC_SERVER_LOAD_SCALAR_KEY);
     566
     567    if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
     568        $val = wp_cache_get($scalarKey, DFEHC_CACHE_GROUP);
     569        if ($val !== false && $val !== '' && is_numeric($val)) {
     570            $cached = max(0.0, (float) $val);
     571            return (float) $cached;
     572        }
     573    }
     574
     575    $cc = dfehc_get_cache_client();
     576    $client = isset($cc['client']) ? $cc['client'] : null;
     577
    549578    $val = false;
    550     $key = dfehc_key(DFEHC_SERVER_LOAD_CACHE_KEY);
    551579    if ($client) {
    552580        try {
    553             $val = $client->get($key);
     581            $val = $client->get(dfehc_key(DFEHC_SERVER_LOAD_CACHE_KEY));
    554582        } catch (Throwable $e) {
    555583            dfehc_debug_log('DFEHC cache read error: ' . $e->getMessage());
    556584        }
    557585    }
    558     if ($val !== false && $val !== '') {
     586
     587    if ($val !== false && $val !== '' && is_numeric($val)) {
    559588        $cached = max(0.0, (float) $val);
    560589        return (float) $cached;
    561590    }
     591
    562592    $fresh = dfehc_get_server_load();
    563593    $fresh = max(0.0, (float) $fresh);
     594
    564595    dfehc_cache_server_load($fresh);
    565     if (wp_using_ext_object_cache()) {
    566         $ttl = dfehc_server_load_ttl();
    567         $jitter = 0;
    568         if (function_exists('random_int')) {
    569             try {
    570                 $jitter = random_int(0, 5);
    571             } catch (Throwable $e) {
    572                 $jitter = 0;
    573             }
    574         }
    575         $ttl += $jitter;
    576         wp_cache_set($key, $fresh, DFEHC_CACHE_GROUP, $ttl);
    577     }
     596
    578597    $cached = $fresh;
    579598    return (float) $cached;
     
    604623function dfehc_schedule_log_server_load(): void
    605624{
    606     if (!apply_filters('dfehc_enable_load_logging', true)) {
     625    if (!apply_filters('dfehc_enable_load_logging', false)) {
    607626        dfehc_clear_log_server_load_cron();
    608627        return;
    609628    }
    610     if (
    611         !function_exists('wp_get_schedules')
    612         || !function_exists('wp_next_scheduled')
    613         || !function_exists('wp_schedule_event')
    614     ) {
     629    if (!function_exists('wp_get_schedules') || !function_exists('wp_next_scheduled') || !function_exists('wp_schedule_event')) {
    615630        return;
    616631    }
     
    641656if (function_exists('register_activation_hook')) {
    642657    $plugin_file = defined('DFEHC_PLUGIN_FILE') ? DFEHC_PLUGIN_FILE : __FILE__;
    643     register_activation_hook($plugin_file, 'dfehc_schedule_log_server_load');
     658    register_activation_hook((string) $plugin_file, 'dfehc_schedule_log_server_load');
    644659}
    645660if (function_exists('register_deactivation_hook')) {
    646661    $plugin_file = defined('DFEHC_PLUGIN_FILE') ? DFEHC_PLUGIN_FILE : __FILE__;
    647     register_deactivation_hook($plugin_file, 'dfehc_deactivate_log_server_load');
     662    register_deactivation_hook((string) $plugin_file, 'dfehc_deactivate_log_server_load');
    648663}
    649664
     
    653668{
    654669    $key = dfehc_key('dfehc_load_lock');
     670
    655671    if (class_exists('WP_Lock')) {
    656672        $lock = new WP_Lock($key, 30);
     
    661677        return false;
    662678    }
     679
    663680    if (function_exists('wp_cache_add') && wp_cache_add($key, 1, DFEHC_CACHE_GROUP, 30)) {
    664681        $GLOBALS['dfehc_load_lock_cache_key'] = $key;
    665682        return true;
    666683    }
     684
    667685    if (false !== get_transient($key)) {
    668686        return false;
    669687    }
     688
    670689    if (set_transient($key, 1, 30)) {
    671690        $GLOBALS['dfehc_load_lock_transient_key'] = $key;
    672691        return true;
    673692    }
     693
    674694    return false;
    675695}
     
    682702        return;
    683703    }
     704
    684705    if (isset($GLOBALS['dfehc_load_lock_cache_key'])) {
    685         wp_cache_delete($GLOBALS['dfehc_load_lock_cache_key'], DFEHC_CACHE_GROUP);
     706        wp_cache_delete((string) $GLOBALS['dfehc_load_lock_cache_key'], DFEHC_CACHE_GROUP);
    686707        unset($GLOBALS['dfehc_load_lock_cache_key']);
    687708        return;
    688709    }
     710
    689711    if (isset($GLOBALS['dfehc_load_lock_transient_key'])) {
    690         delete_transient($GLOBALS['dfehc_load_lock_transient_key']);
     712        delete_transient((string) $GLOBALS['dfehc_load_lock_transient_key']);
    691713        unset($GLOBALS['dfehc_load_lock_transient_key']);
    692714    }
  • dynamic-front-end-heartbeat-control/trunk/engine/server-response.php

    r3427163 r3461136  
    99defined('DFEHC_CACHE_GROUP') || define('DFEHC_CACHE_GROUP', 'dfehc');
    1010
     11if (!function_exists('dfehc_ttl_jitter')) {
     12    function dfehc_ttl_jitter(int $max = 5): int
     13    {
     14        if ($max <= 0 || !function_exists('random_int')) {
     15            return 0;
     16        }
     17        try {
     18            return random_int(0, $max);
     19        } catch (\Throwable $e) {
     20            return 0;
     21        }
     22    }
     23}
     24
     25if (!function_exists('dfehc_transient_cache_get')) {
     26    function dfehc_transient_cache_get(string $key, bool &$found = null)
     27    {
     28        if (!isset($GLOBALS['dfehc_transient_cache']) || !is_array($GLOBALS['dfehc_transient_cache'])) {
     29            $GLOBALS['dfehc_transient_cache'] = [];
     30        }
     31        if (array_key_exists($key, $GLOBALS['dfehc_transient_cache'])) {
     32            $found = true;
     33            return $GLOBALS['dfehc_transient_cache'][$key];
     34        }
     35        $found = false;
     36        return null;
     37    }
     38}
     39
     40if (!function_exists('dfehc_transient_cache_set')) {
     41    function dfehc_transient_cache_set(string $key, $value): void
     42    {
     43        if (!isset($GLOBALS['dfehc_transient_cache']) || !is_array($GLOBALS['dfehc_transient_cache'])) {
     44            $GLOBALS['dfehc_transient_cache'] = [];
     45        }
     46        $GLOBALS['dfehc_transient_cache'][$key] = $value;
     47    }
     48}
     49
     50if (!function_exists('dfehc_transient_cache_del')) {
     51    function dfehc_transient_cache_del(string $key): void
     52    {
     53        if (isset($GLOBALS['dfehc_transient_cache']) && is_array($GLOBALS['dfehc_transient_cache'])) {
     54            unset($GLOBALS['dfehc_transient_cache'][$key]);
     55        }
     56    }
     57}
     58
     59if (!function_exists('dfehc_get_transient_cached')) {
     60    function dfehc_get_transient_cached(string $key)
     61    {
     62        $found = false;
     63        $v = dfehc_transient_cache_get($key, $found);
     64        if ($found) {
     65            return $v;
     66        }
     67        $v = get_transient($key);
     68        dfehc_transient_cache_set($key, $v);
     69        return $v;
     70    }
     71}
     72
     73if (!function_exists('dfehc_delete_transient_cached')) {
     74    function dfehc_delete_transient_cached(string $key): bool
     75    {
     76        dfehc_transient_cache_del($key);
     77        return delete_transient($key);
     78    }
     79}
     80
    1181if (!function_exists('dfehc_host_token')) {
    12     function dfehc_host_token(): string {
     82    function dfehc_host_token(): string
     83    {
    1384        static $t = '';
    14         if ($t !== '') return $t;
     85        if ($t !== '') {
     86            return $t;
     87        }
    1588        $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown'));
    1689        $salt = defined('DB_NAME') ? (string) DB_NAME : '';
     
    2093
    2194if (!function_exists('dfehc_blog_id')) {
    22     function dfehc_blog_id(): int {
     95    function dfehc_blog_id(): int
     96    {
    2397        return function_exists('get_current_blog_id') ? (int) get_current_blog_id() : 0;
    2498    }
     
    26100
    27101if (!function_exists('dfehc_key')) {
    28     function dfehc_key(string $base): string {
     102    function dfehc_key(string $base): string
     103    {
    29104        return $base . '_' . dfehc_blog_id() . '_' . dfehc_host_token();
    30105    }
     
    32107
    33108if (!function_exists('dfehc_store_lockfree')) {
    34     function dfehc_store_lockfree(string $key, $value, int $ttl): bool {
     109    function dfehc_store_lockfree(string $key, $value, int $ttl): bool
     110    {
    35111        if (function_exists('wp_cache_add') && wp_cache_add($key, $value, DFEHC_CACHE_GROUP, $ttl)) {
    36112            return true;
    37113        }
    38         return set_transient($key, $value, $ttl);
    39     }
    40 }
    41 
    42 if (!function_exists('dfehc_set_transient_noautoload')) {
    43     function dfehc_set_transient_noautoload(string $key, $value, int $expiration): void {
    44         if (wp_using_ext_object_cache()) {
    45             if (function_exists('wp_cache_add')) {
    46                 if (!wp_cache_add($key, $value, DFEHC_CACHE_GROUP, $expiration)) {
    47                     wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $expiration);
    48                 }
    49             } else {
    50                 wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $expiration);
    51             }
    52             return;
    53         }
    54         set_transient($key, $value, $expiration);
    55         global $wpdb;
    56         $opt_key = "_transient_$key";
    57         $opt_key_to = "_transient_timeout_$key";
    58         $wpdb->suppress_errors(true);
    59         $autoload = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key));
    60         if ($autoload === 'yes') {
    61             $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key, 'autoload' => 'yes'], ['%s'], ['%s','%s']);
    62         }
    63         $autoload_to = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_to));
    64         if ($autoload_to === 'yes') {
    65             $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key_to, 'autoload' => 'yes'], ['%s'], ['%s','%s']);
    66         }
    67         $wpdb->suppress_errors(false);
     114        $ok = set_transient($key, $value, $ttl);
     115        if ($ok) {
     116            dfehc_transient_cache_set($key, $value);
     117        }
     118        return (bool) $ok;
     119    }
     120}
     121
     122if (!function_exists('dfehc_home_host')) {
     123    function dfehc_home_host(): string
     124    {
     125        static $host = null;
     126        if ($host !== null) {
     127            return $host;
     128        }
     129        $host = '';
     130
     131        if (function_exists('home_url')) {
     132            $parsed = wp_parse_url((string) home_url());
     133            if (is_array($parsed) && isset($parsed['host'])) {
     134                $host = (string) $parsed['host'];
     135            }
     136        }
     137        return $host;
    68138    }
    69139}
     
    78148        $is_valid_ip = static function (string $ip, bool $publicOnly): bool {
    79149            $ip = trim($ip);
    80             if ($ip === '') return false;
     150            if ($ip === '') {
     151                return false;
     152            }
    81153            if ($publicOnly) {
    82154                return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
     
    107179
    108180                if ($h === 'HTTP_X_FORWARDED_FOR') {
    109                     $parts = array_map('trim', explode(',', $raw));
     181                    $parts_raw = array_map('trim', explode(',', $raw));
     182                    $parts = [];
     183                    foreach ($parts_raw as $p) {
     184                        $p = trim(sanitize_text_field($p));
     185                        if ($p !== '') {
     186                            $parts[] = $p;
     187                        }
     188                    }
    110189
    111190                    foreach ($parts as $cand) {
    112                         $cand = sanitize_text_field($cand);
    113191                        if ($is_valid_ip($cand, $public_only)) {
    114192                            return (string) apply_filters('dfehc_client_ip', $cand);
     
    116194                    }
    117195                    foreach ($parts as $cand) {
    118                         $cand = sanitize_text_field($cand);
    119196                        if ($is_valid_ip($cand, false)) {
    120197                            return (string) apply_filters('dfehc_client_ip', $cand);
     
    122199                    }
    123200                } else {
    124                     $cand = sanitize_text_field(trim($raw));
     201                    $cand = trim(sanitize_text_field($raw));
    125202                    if ($is_valid_ip($cand, $public_only) || $is_valid_ip($cand, false)) {
    126203                        return (string) apply_filters('dfehc_client_ip', $cand);
     
    156233
    157234    $cacheKey = dfehc_key('dfehc_cached_response_data');
    158     $cached = get_transient($cacheKey);
     235    $cached = dfehc_get_transient_cached($cacheKey);
    159236    if ($cached !== false && is_array($cached)) {
    160237        return array_merge($defaults, $cached);
     
    174251        ];
    175252        $ttl = (int) apply_filters('dfehc_high_traffic_cache_expiration', 300);
    176         $jitter = 0;
    177         if (function_exists('random_int')) {
    178             try {
    179                 $jitter = random_int(0, 5);
    180             } catch (\Throwable $e) {
    181                 $jitter = 0;
    182             }
    183         }
    184         $ttl += $jitter;
     253        $ttl += dfehc_ttl_jitter(5);
    185254        dfehc_set_transient_noautoload($cacheKey, $high, $ttl);
    186255        return $high;
     
    193262    try {
    194263        $results = dfehc_perform_response_measurements($default_ms);
     264
    195265        $baselineKey = dfehc_key('dfehc_baseline_response_data');
    196266        $spikeKey = dfehc_key('dfehc_spike_score');
    197         $baseline = get_transient($baselineKey);
    198         $spike = (float) get_transient($spikeKey);
     267
     268        $baseline = dfehc_get_transient_cached($baselineKey);
     269        $prev_spike = (float) dfehc_get_transient_cached($spikeKey);
     270        $spike = $prev_spike;
     271
    199272        $max_age = (int) apply_filters('dfehc_max_baseline_age', DFEHC_BASELINE_EXP);
    200273
     
    202275            $ts = isset($baseline['ts_unix']) && is_numeric($baseline['ts_unix']) ? (int) $baseline['ts_unix'] : strtotime($baseline['timestamp'] ?? 'now');
    203276            if ($now - (int) $ts > $max_age) {
    204                 delete_transient($baselineKey);
     277                dfehc_delete_transient_cached($baselineKey);
    205278                $baseline = false;
    206279            }
     
    211284            $results['timestamp'] = current_time('mysql');
    212285            $results['ts_unix'] = $now;
    213             $jitter = 0;
    214             if (function_exists('random_int')) {
    215                 try {
    216                     $jitter = random_int(0, 5);
    217                 } catch (\Throwable $e) {
    218                     $jitter = 0;
    219                 }
    220             }
    221             $exp += $jitter;
     286            $exp += dfehc_ttl_jitter(5);
    222287            dfehc_set_transient_noautoload($baselineKey, $results, $exp);
    223288            $baseline = $results;
     
    248313                $results['ts_unix'] = $now;
    249314                $exp = (int) apply_filters('dfehc_baseline_expiration', DFEHC_BASELINE_EXP);
    250                 $jitter = 0;
    251                 if (function_exists('random_int')) {
    252                     try {
    253                         $jitter = random_int(0, 5);
    254                     } catch (\Throwable $e) {
    255                         $jitter = 0;
    256                     }
    257                 }
    258                 $exp += $jitter;
     315                $exp += dfehc_ttl_jitter(5);
    259316                dfehc_set_transient_noautoload($baselineKey, $results, $exp);
    260317                $spike = 0.0;
     
    264321
    265322        $results['spike_score'] = $spike;
    266         $prev_spike = (float) get_transient($spikeKey);
    267323
    268324        if (abs($spike - $prev_spike) >= DFEHC_SPIKE_OPT_EPS) {
    269             $ttl = DFEHC_BASELINE_EXP;
    270             $jitter = 0;
    271             if (function_exists('random_int')) {
    272                 try {
    273                     $jitter = random_int(0, 5);
    274                 } catch (\Throwable $e) {
    275                     $jitter = 0;
    276                 }
    277             }
    278             $ttl += $jitter;
     325            $ttl = (int) DFEHC_BASELINE_EXP;
     326            $ttl += dfehc_ttl_jitter(5);
    279327            dfehc_set_transient_noautoload($spikeKey, $spike, $ttl);
    280328        }
    281329
    282330        $exp = (int) apply_filters('dfehc_cache_expiration', 3 * MINUTE_IN_SECONDS);
    283         $jitter = 0;
    284         if (function_exists('random_int')) {
    285             try {
    286                 $jitter = random_int(0, 5);
    287             } catch (\Throwable $e) {
    288                 $jitter = 0;
    289             }
    290         }
    291         $exp += $jitter;
     331        $exp += dfehc_ttl_jitter(5);
    292332        dfehc_set_transient_noautoload($cacheKey, $results, $exp);
    293333
     
    336376    $use_ajax_fallback = false;
    337377
    338     $home_host = '';
    339     if (function_exists('home_url')) {
    340         $parsed = wp_parse_url((string) home_url());
    341         if (is_array($parsed) && isset($parsed['host'])) {
    342             $home_host = (string) $parsed['host'];
    343         }
    344     }
     378    $home_host = dfehc_home_host();
    345379
    346380    $headers = (array) apply_filters('dfehc_probe_headers', [
     
    388422
    389423    $head_key = dfehc_key('dfehc_head_supported_' . md5($scheme . '|' . $url));
    390     $head_supported = get_transient($head_key);
     424    $head_supported = dfehc_get_transient_cached($head_key);
    391425    if ($head_supported === false) {
    392426        $head_supported = null;
     
    394428
    395429    $negKey = dfehc_key('dfehc_probe_fail');
    396     if (get_transient($negKey)) {
     430
     431    static $probe_blocked = null;
     432    if ($probe_blocked === null) {
     433        $probe_blocked = (bool) dfehc_get_transient_cached($negKey);
     434    }
     435    if ($probe_blocked) {
    397436        $r['method'] = 'failed';
    398437        $r['main_response_ms'] = $default_ms;
     
    432471                if ($head_supported === null) {
    433472                    $ttl = (int) apply_filters('dfehc_head_negative_ttl', DFEHC_HEAD_NEG_TTL);
    434                     $jitter = 0;
    435                     if (function_exists('random_int')) {
    436                         try {
    437                             $jitter = random_int(0, 5);
    438                         } catch (\Throwable $e) {
    439                             $jitter = 0;
    440                         }
    441                     }
    442                     $ttl += $jitter;
     473                    $ttl += dfehc_ttl_jitter(5);
    443474                    dfehc_set_transient_noautoload($head_key, 0, $ttl);
    444475                }
     
    447478                if ($head_supported === null) {
    448479                    $ttl = (int) apply_filters('dfehc_head_positive_ttl', DFEHC_HEAD_POS_TTL);
    449                     $jitter = 0;
    450                     if (function_exists('random_int')) {
    451                         try {
    452                             $jitter = random_int(0, 5);
    453                         } catch (\Throwable $e) {
    454                             $jitter = 0;
    455                         }
    456                     }
    457                     $ttl += $jitter;
     480                    $ttl += dfehc_ttl_jitter(5);
    458481                    dfehc_set_transient_noautoload($head_key, 1, $ttl);
    459482                }
     
    490513    } else {
    491514        $ttl = (int) apply_filters('dfehc_probe_fail_ttl', 60);
    492         $jitter = 0;
    493         if (function_exists('random_int')) {
    494             try {
    495                 $jitter = random_int(0, 5);
    496             } catch (\Throwable $e) {
    497                 $jitter = 0;
    498             }
    499         }
    500         $ttl += $jitter;
     515        $ttl += dfehc_ttl_jitter(5);
    501516        dfehc_set_transient_noautoload($negKey, 1, $ttl);
     517        $probe_blocked = true;
    502518        $r['method'] = 'failed';
    503519    }
     
    513529}
    514530
    515 function dfehc_ping_handler(): void {
     531if (!function_exists('dfehc_rl_increment')) {
     532    function dfehc_rl_increment(string $key, int $ttl): int
     533    {
     534        if (wp_using_ext_object_cache() && function_exists('wp_cache_incr')) {
     535            wp_cache_add($key, 0, DFEHC_CACHE_GROUP, $ttl);
     536            $v = wp_cache_incr($key, 1, DFEHC_CACHE_GROUP);
     537            if ($v === false) {
     538                $curr = (int) wp_cache_get($key, DFEHC_CACHE_GROUP);
     539                $curr++;
     540                wp_cache_set($key, $curr, DFEHC_CACHE_GROUP, $ttl);
     541                return $curr;
     542            }
     543            if ((int) $v === 1) {
     544                wp_cache_set($key, (int) $v, DFEHC_CACHE_GROUP, $ttl);
     545            }
     546            return (int) $v;
     547        }
     548
     549        $cnt = (int) dfehc_get_transient_cached($key);
     550        dfehc_set_transient_noautoload($key, $cnt + 1, $ttl);
     551        return $cnt + 1;
     552    }
     553}
     554
     555
     556function dfehc_ping_handler(): void
     557{
    516558    $ip = dfehc_client_ip();
    517559    $k = dfehc_key('dfehc_ping_rl_' . md5($ip));
    518560    $window = (int) apply_filters('dfehc_ping_rl_ttl', 2);
    519561    $limit  = (int) apply_filters('dfehc_ping_rl_limit', 2);
    520     $cnt = (int) get_transient($k);
    521     if ($cnt >= $limit) {
     562
     563    $cnt = dfehc_rl_increment($k, $window);
     564    if ($cnt > $limit) {
    522565        status_header(429);
    523566        nocache_headers();
    524567        wp_send_json_error('rate_limited', 429);
    525568    }
    526     dfehc_set_transient_noautoload($k, $cnt + 1, $window);
     569
    527570    nocache_headers();
    528571    wp_send_json_success('ok');
     
    553596        }
    554597
    555         if (false !== get_transient($key)) {
     598        $existing = get_transient($key);
     599        dfehc_transient_cache_set($key, $existing);
     600
     601        if (false !== $existing) {
    556602            return false;
    557603        }
    558604
    559605        if (set_transient($key, 1, 60)) {
     606            dfehc_transient_cache_set($key, 1);
    560607            $GLOBALS['dfehc_rt_lock_transient_key'] = $key;
    561608            return true;
     
    582629
    583630        if (isset($GLOBALS['dfehc_rt_lock_transient_key'])) {
    584             delete_transient($GLOBALS['dfehc_rt_lock_transient_key']);
     631            dfehc_delete_transient_cached($GLOBALS['dfehc_rt_lock_transient_key']);
    585632            unset($GLOBALS['dfehc_rt_lock_transient_key']);
    586633        }
     
    592639    {
    593640        $flag_key = dfehc_key('dfehc_high_traffic_flag');
    594         $flag = get_transient($flag_key);
     641        $flag = dfehc_get_transient_cached($flag_key);
    595642        if ($flag !== false) {
    596643            return (bool) $flag;
     
    599646        $threshold = (int) apply_filters('dfehc_high_traffic_threshold', 100);
    600647        $cnt_key = dfehc_key('dfehc_cached_visitor_cnt');
    601         $count = get_transient($cnt_key);
     648        $count = dfehc_get_transient_cached($cnt_key);
    602649        if ($count === false) {
    603650            $count = (int) apply_filters('dfehc_website_visitors', 0);
     
    614661        }
    615662
    616         $high = $count >= $threshold;
     663        $high = ((int) $count) >= $threshold;
    617664        dfehc_set_transient_noautoload($flag_key, $high ? 1 : 0, 60);
    618665
  • dynamic-front-end-heartbeat-control/trunk/engine/system-load-fallback.php

    r3427163 r3461136  
    33
    44defined('DFEHC_SERVER_LOAD_TTL') || define('DFEHC_SERVER_LOAD_TTL', 60);
    5 defined('DFEHC_SENTINEL_NO_LOAD') || define('DFEHC_SENTINEL_NO_LOAD', -1.0);
     5defined('DFEHC_SENTINEL_NO_LOAD') || define('DFEHC_SENTINEL_NO_LOAD', 0.404);
    66defined('DFEHC_SYSTEM_LOAD_KEY') || define('DFEHC_SYSTEM_LOAD_KEY', 'dfehc_system_load_avg');
    77defined('DFEHC_CACHE_GROUP') || define('DFEHC_CACHE_GROUP', 'dfehc');
    88
    99if (!function_exists('dfehc_host_token')) {
    10     function dfehc_host_token(): string {
     10    function dfehc_host_token(): string
     11    {
    1112        static $t = '';
    1213        if ($t !== '') return $t;
    13         $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown'));
     14
     15        $url = '';
     16        if (defined('WP_HOME') && is_string(WP_HOME) && WP_HOME !== '') {
     17            $url = WP_HOME;
     18        } elseif (function_exists('home_url')) {
     19            $url = (string) home_url();
     20        }
     21
     22        $host = '';
     23        if ($url !== '' && function_exists('wp_parse_url')) {
     24            $parts = wp_parse_url($url);
     25            if (is_array($parts) && isset($parts['host']) && is_string($parts['host'])) {
     26                $host = $parts['host'];
     27            }
     28        }
     29
     30        if ($host === '') {
     31            $host = @php_uname('n') ?: 'unknown';
     32        }
     33
    1434        $salt = defined('DB_NAME') ? (string) DB_NAME : '';
    15         return $t = substr(md5((string) $host . $salt), 0, 10);
     35        return $t = substr(md5($host . $salt), 0, 10);
    1636    }
    1737}
    1838
    1939if (!function_exists('dfehc_blog_id')) {
    20     function dfehc_blog_id(): int {
     40    function dfehc_blog_id(): int
     41    {
    2142        return function_exists('get_current_blog_id') ? (int) get_current_blog_id() : 0;
    2243    }
     
    2445
    2546if (!function_exists('dfehc_scoped_key')) {
    26     function dfehc_scoped_key(string $base): string {
     47    function dfehc_scoped_key(string $base): string
     48    {
    2749        return $base . '_' . dfehc_blog_id() . '_' . dfehc_host_token();
    2850    }
    2951}
    3052
    31 if (!function_exists('dfehc_set_transient_noautoload')) {
    32     function dfehc_set_transient_noautoload(string $key, $value, int $expiration): void {
    33         $jitter = 0;
     53if (!function_exists('dfehc_rand_jitter')) {
     54    function dfehc_rand_jitter(int $min, int $max): int
     55    {
     56        $min = (int) $min;
     57        $max = (int) $max;
     58        if ($max < $min) { $t = $min; $min = $max; $max = $t; }
     59        if ($min === $max) return $min;
    3460        if (function_exists('random_int')) {
    35             try {
    36                 $jitter = random_int(0, 5);
    37             } catch (\Throwable $e) {
    38                 $jitter = 0;
    39             }
    40         }
    41         $expiration = max(1, $expiration + $jitter);
    42 
    43         if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
    44             wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $expiration);
    45             return;
    46         }
    47 
    48         set_transient($key, $value, $expiration);
    49 
    50         if (!isset($GLOBALS['wpdb'])) {
    51             return;
    52         }
    53         global $wpdb;
    54         if (!isset($wpdb->options)) {
    55             return;
    56         }
    57 
    58         $opt_key = "_transient_$key";
    59         $opt_key_to = "_transient_timeout_$key";
    60 
    61         $wpdb->suppress_errors(true);
    62 
    63         $autoload = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key));
    64         if ($autoload === 'yes') {
    65             $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key, 'autoload' => 'yes'], ['%s'], ['%s', '%s']);
    66         }
    67 
    68         $autoload_to = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_to));
    69         if ($autoload_to === 'yes') {
    70             $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key_to, 'autoload' => 'yes'], ['%s'], ['%s', '%s']);
    71         }
    72 
    73         $wpdb->suppress_errors(false);
     61            try { return (int) random_int($min, $max); } catch (Throwable $e) {}
     62        }
     63        return (int) (function_exists('wp_rand') ? wp_rand($min, $max) : mt_rand($min, $max));
     64    }
     65}
     66
     67if (!function_exists('dfehc_cache_get_scalar')) {
     68    function dfehc_cache_get_scalar(string $key)
     69    {
     70        if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_get')) {
     71            $v = wp_cache_get($key, DFEHC_CACHE_GROUP);
     72            if ($v !== false && $v !== null) return $v;
     73        }
     74        if (function_exists('get_transient')) {
     75            $v = get_transient($key);
     76            if ($v !== false) return $v;
     77        }
     78        return null;
     79    }
     80}
     81
     82if (!function_exists('dfehc_allow_shell_fallback')) {
     83    function dfehc_allow_shell_fallback(): bool
     84    {
     85        $allow = (bool) apply_filters('dfehc_allow_shell_fallback', false);
     86        if (!$allow) return false;
     87        if (ini_get('open_basedir')) return false;
     88        $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
     89        if (!function_exists('shell_exec') || in_array('shell_exec', $disabled, true)) return false;
     90        return true;
    7491    }
    7592}
    7693
    7794if (!function_exists('dfehc_exec_with_timeout')) {
    78     function dfehc_exec_with_timeout(string $cmd, float $timeoutSec = 1.0): string {
    79         $timeoutSec = max(0.1, min(5.0, $timeoutSec));
    80 
    81         if (ini_get('open_basedir')) {
    82             return '';
    83         }
    84 
    85         $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
    86         $can_shell = function_exists('shell_exec') && !in_array('shell_exec', $disabled, true);
    87 
    88         if (!$can_shell) {
    89             return '';
    90         }
    91 
     95    function dfehc_exec_with_timeout(string $cmd, float $timeoutSec = 1.0): string
     96    {
     97        if (!dfehc_allow_shell_fallback()) return '';
     98
     99        $timeoutSec = max(0.15, min(3.0, $timeoutSec));
    92100        $cmd = trim($cmd);
    93         if ($cmd === '') {
    94             return '';
    95         }
     101        if ($cmd === '') return '';
    96102
    97103        if (PHP_OS_FAMILY !== 'Windows') {
    98             $has_timeout = trim((string) @shell_exec('command -v timeout 2>/dev/null'));
    99             if ($has_timeout !== '') {
    100                 $sec = max(1, (int) ceil($timeoutSec));
    101                 $cmd = 'timeout ' . $sec . ' ' . $cmd;
    102             }
    103         }
    104 
    105         return trim((string) @shell_exec($cmd));
     104            $sec = max(1, (int) ceil($timeoutSec));
     105            $pref = 'timeout ' . $sec . ' ';
     106            $cmd = $pref . $cmd;
     107        }
     108
     109        $out = @shell_exec($cmd);
     110        return is_string($out) ? trim($out) : '';
    106111    }
    107112}
    108113
    109114if (!function_exists('dfehc_get_cpu_cores')) {
    110     function dfehc_get_cpu_cores(): int {
     115    function dfehc_get_cpu_cores(): int
     116    {
    111117        static $cached = null;
    112118        if ($cached !== null) return (int) $cached;
     
    114120        $tkey = dfehc_scoped_key('dfehc_cpu_cores');
    115121
    116         if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
    117             $oc = wp_cache_get($tkey, DFEHC_CACHE_GROUP);
    118             if ($oc !== false && (int) $oc > 0) {
    119                 return $cached = (int) $oc;
    120             }
    121         }
    122 
    123         $tc = get_transient($tkey);
    124         if ($tc !== false && (int) $tc > 0) {
    125             return $cached = (int) $tc;
    126         }
    127 
    128         $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
    129 
    130         if (PHP_OS_FAMILY !== 'Windows' && function_exists('shell_exec') && !in_array('shell_exec', $disabled, true) && !ini_get('open_basedir')) {
    131             $val = trim((string) @shell_exec('getconf _NPROCESSORS_ONLN 2>/dev/null'));
    132             if (ctype_digit($val) && (int) $val > 0) {
    133                 $cores = (int) $val;
    134                 dfehc_set_transient_noautoload($tkey, $cores, (int) apply_filters('dfehc_cpu_cores_ttl', DAY_IN_SECONDS));
    135                 return $cached = $cores;
    136             }
    137         }
    138 
    139         if (PHP_OS_FAMILY === 'Windows' && !ini_get('open_basedir') && function_exists('shell_exec') && !in_array('shell_exec', $disabled, true)) {
    140             $wout = trim((string) @shell_exec('wmic cpu get NumberOfLogicalProcessors /value 2>NUL'));
    141             if ($wout && preg_match('/NumberOfLogicalProcessors=(\d+)/i', $wout, $m) && (int) $m[1] > 0) {
    142                 $cores = (int) $m[1];
    143                 dfehc_set_transient_noautoload($tkey, $cores, (int) apply_filters('dfehc_cpu_cores_ttl', DAY_IN_SECONDS));
    144                 return $cached = $cores;
    145             }
    146         }
    147 
    148         if (is_readable('/proc/cpuinfo')) {
    149             $cnt = preg_match_all('/^processor/m', (string) @file_get_contents('/proc/cpuinfo'));
    150             if ($cnt > 0) {
    151                 $cores = (int) $cnt;
    152                 dfehc_set_transient_noautoload($tkey, $cores, (int) apply_filters('dfehc_cpu_cores_ttl', DAY_IN_SECONDS));
    153                 return $cached = $cores;
    154             }
    155         }
    156 
    157         dfehc_set_transient_noautoload($tkey, 1, (int) apply_filters('dfehc_cpu_cores_ttl', DAY_IN_SECONDS));
    158         return $cached = 1;
     122        $v = dfehc_cache_get_scalar($tkey);
     123        if ($v !== null && is_numeric($v) && (int) $v > 0) {
     124            return $cached = (int) $v;
     125        }
     126
     127        $override = getenv('DFEHC_CPU_CORES');
     128        if ($override !== false && is_numeric($override) && (int) $override > 0) {
     129            $cores = (int) $override;
     130            dfehc_set_transient_noautoload($tkey, $cores, (int) apply_filters('dfehc_cpu_cores_ttl', DAY_IN_SECONDS));
     131            return $cached = $cores;
     132        }
     133        if (defined('DFEHC_CPU_CORES') && is_numeric(DFEHC_CPU_CORES) && (int) DFEHC_CPU_CORES > 0) {
     134            $cores = (int) DFEHC_CPU_CORES;
     135            dfehc_set_transient_noautoload($tkey, $cores, (int) apply_filters('dfehc_cpu_cores_ttl', DAY_IN_SECONDS));
     136            return $cached = $cores;
     137        }
     138
     139        $cores = 1;
     140
     141        if (is_readable('/sys/fs/cgroup/cpu.max')) {
     142            $line = trim((string) @file_get_contents('/sys/fs/cgroup/cpu.max'));
     143            if ($line !== '') {
     144                $parts = preg_split('/\s+/', $line);
     145                if (is_array($parts) && count($parts) >= 2) {
     146                    $quota  = is_numeric($parts[0]) ? (float) $parts[0] : 0.0;
     147                    $period = is_numeric($parts[1]) ? (float) $parts[1] : 0.0;
     148                    if ($period > 0.0 && $quota > 0.0) {
     149                        $cores = max(1, (int) ceil($quota / $period));
     150                    }
     151                }
     152            }
     153        }
     154
     155        if ($cores === 1 && is_readable('/proc/cpuinfo')) {
     156            $cnt = preg_match_all('/^processor\s*:/m', (string) @file_get_contents('/proc/cpuinfo'));
     157            if ($cnt > 0) $cores = (int) $cnt;
     158        }
     159
     160        $cores = (int) apply_filters('dfehc_cpu_cores', max(1, $cores));
     161        dfehc_set_transient_noautoload($tkey, $cores, (int) apply_filters('dfehc_cpu_cores_ttl', DAY_IN_SECONDS));
     162        return $cached = $cores;
    159163    }
    160164}
    161165
    162166if (!function_exists('dfehc_get_system_load_average')) {
    163     function dfehc_get_system_load_average(): float {
    164         $ttl = (int) apply_filters('dfehc_system_load_ttl', DFEHC_SERVER_LOAD_TTL);
     167    function dfehc_get_system_load_average(): float
     168    {
     169        static $memo = null;
     170        if ($memo !== null) return (float) $memo;
     171
     172        $ttl = (int) apply_filters('dfehc_system_load_ttl', (int) DFEHC_SERVER_LOAD_TTL);
     173        $ttl = max(5, min(600, $ttl));
     174        $ttl = $ttl + dfehc_rand_jitter(0, 6);
     175
    165176        $key = dfehc_scoped_key(DFEHC_SYSTEM_LOAD_KEY);
    166177
    167178        $as_percent = false;
    168179
    169         if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
    170             $vc = wp_cache_get($key, DFEHC_CACHE_GROUP);
    171             if ($vc !== false && $vc !== null) {
    172                 $ratio = (float) $vc;
    173                 $as_percent = (bool) apply_filters('dfehc_system_load_return_percent', false, $ratio);
    174                 return $as_percent ? (float) round($ratio * 100, 2) : $ratio;
    175             }
    176         }
    177 
    178         $cache = get_transient($key);
    179         if ($cache !== false) {
    180             $ratio = (float) $cache;
     180        $cached = dfehc_cache_get_scalar($key);
     181        if ($cached !== null && is_numeric($cached)) {
     182            $ratio = (float) $cached;
    181183            $as_percent = (bool) apply_filters('dfehc_system_load_return_percent', false, $ratio);
    182             return $as_percent ? (float) round($ratio * 100, 2) : $ratio;
     184            $memo = $as_percent ? (float) round($ratio * 100.0, 2) : (float) $ratio;
     185            return (float) $memo;
    183186        }
    184187
     
    186189        $source = '';
    187190        $normalized_ratio = false;
     191
     192        $strict = (bool) apply_filters('dfehc_system_load_strict_fallback', true);
    188193
    189194        if ($raw === null && function_exists('dfehc_get_server_load')) {
     
    207212        if ($raw === null && function_exists('sys_getloadavg')) {
    208213            $arr = @sys_getloadavg();
    209             if ($arr && isset($arr[0])) {
     214            if (is_array($arr) && isset($arr[0]) && is_numeric($arr[0]) && (float) $arr[0] >= 0.0) {
    210215                $raw = (float) $arr[0];
    211216                $source = 'sys_getloadavg';
     
    214219
    215220        if ($raw === null && PHP_OS_FAMILY !== 'Windows' && is_readable('/proc/loadavg')) {
    216             $parts = explode(' ', (string) @file_get_contents('/proc/loadavg'));
    217             if (isset($parts[0])) {
    218                 $raw = (float) $parts[0];
    219                 $source = 'proc_loadavg';
    220             }
    221         }
    222 
    223         if ($raw === null && PHP_OS_FAMILY !== 'Windows') {
    224             $out = dfehc_exec_with_timeout('LANG=C uptime 2>/dev/null', 1.0);
    225             if ($out && preg_match('/load average[s]?:\s*([0-9.]+)/', $out, $m)) {
    226                 $raw = (float) $m[1];
    227                 $source = 'uptime';
    228             }
    229         }
    230 
    231         if ($raw === null && PHP_OS_FAMILY === 'Windows' && !ini_get('open_basedir')) {
    232             $csv = dfehc_exec_with_timeout('typeperf -sc 1 "\Processor(_Total)\% Processor Time" 2>NUL', 2.0);
    233             if ($csv) {
    234                 $lines = array_values(array_filter(array_map('trim', explode("\n", $csv))));
    235                 $last = end($lines);
    236                 if ($last && preg_match('/,"?([0-9.]+)"?$/', (string) $last, $m)) {
     221            $txt = (string) @file_get_contents('/proc/loadavg');
     222            if ($txt !== '') {
     223                $parts = preg_split('/\s+/', trim($txt));
     224                if (is_array($parts) && isset($parts[0]) && is_numeric($parts[0]) && (float) $parts[0] >= 0.0) {
     225                    $raw = (float) $parts[0];
     226                    $source = 'proc_loadavg';
     227                }
     228            }
     229        }
     230
     231        if (!$strict) {
     232            if ($raw === null && PHP_OS_FAMILY !== 'Windows') {
     233                $out = dfehc_exec_with_timeout('LANG=C uptime 2>/dev/null', 1.0);
     234                if ($out !== '' && preg_match('/load average[s]?:\s*([0-9.]+)/', $out, $m)) {
     235                    $v = (float) $m[1];
     236                    if ($v >= 0.0) {
     237                        $raw = $v;
     238                        $source = 'uptime';
     239                    }
     240                }
     241            }
     242
     243            if ($raw === null && PHP_OS_FAMILY === 'Windows') {
     244                $out = dfehc_exec_with_timeout('wmic cpu get loadpercentage /value 2>NUL', 1.5);
     245                if ($out !== '' && preg_match('/loadpercentage=(\d+)/i', $out, $m)) {
    237246                    $pct = (float) $m[1];
    238                     $raw = ($pct / 100.0) * dfehc_get_cpu_cores();
    239                     $source = 'typeperf';
    240                 }
    241             }
    242         }
    243 
    244         if ($raw === null && PHP_OS_FAMILY === 'Windows' && !ini_get('open_basedir')) {
    245             $psCmd = "powershell -NoProfile -NonInteractive -Command \"\\\$v=(Get-Counter '\\Processor(_Total)\\% Processor Time').CounterSamples[0].CookedValue; [Console]::Out.WriteLine([Math]::Round(\\\$v,2))\" 2>NUL";
    246             $ps = dfehc_exec_with_timeout($psCmd, 2.0);
    247             if ($ps !== '' && is_numeric(trim($ps))) {
    248                 $pct = (float) trim($ps);
    249                 $raw = ($pct / 100.0) * dfehc_get_cpu_cores();
    250                 $source = 'powershell';
    251             }
    252         }
    253 
    254         if ($raw === null && PHP_OS_FAMILY === 'Windows' && !ini_get('open_basedir')) {
    255             $out = dfehc_exec_with_timeout('wmic cpu get loadpercentage /value 2>NUL', 1.5);
    256             if ($out && preg_match('/loadpercentage=(\d+)/i', $out, $m)) {
    257                 $pct = (float) $m[1];
    258                 $raw = ($pct / 100.0) * dfehc_get_cpu_cores();
    259                 $source = 'wmic';
     247                    if ($pct >= 0.0 && $pct <= 100.0) {
     248                        $raw = ($pct / 100.0) * dfehc_get_cpu_cores();
     249                        $source = 'wmic';
     250                    }
     251                }
    260252            }
    261253        }
     
    269261                    $normalized_ratio = true;
    270262                    $source = 'estimator_percent';
    271                 } else {
    272                     $raw = (float) $pct;
    273                     $source = 'estimator_raw';
    274263                }
    275264            }
     
    277266
    278267        if ($raw === null) {
    279             $sentinel_ttl = (int) apply_filters('dfehc_sentinel_ttl', 5);
    280             dfehc_set_transient_noautoload($key, (float) DFEHC_SENTINEL_NO_LOAD, $sentinel_ttl);
    281             $ratio = (float) DFEHC_SENTINEL_NO_LOAD;
    282             $as_percent = (bool) apply_filters('dfehc_system_load_return_percent', false, $ratio);
    283             return $as_percent ? (float) round($ratio * 100, 2) : $ratio;
     268            $memo = (float) DFEHC_SENTINEL_NO_LOAD;
     269            $as_percent = (bool) apply_filters('dfehc_system_load_return_percent', false, $memo);
     270            return $as_percent ? (float) round($memo * 100.0, 2) : (float) $memo;
    284271        }
    285272
     
    300287        }
    301288
    302         dfehc_set_transient_noautoload($key, $ratio, $ttl);
     289        $ratio = max((float) DFEHC_SENTINEL_NO_LOAD, (float) $ratio);
     290
     291        if ($ratio !== (float) DFEHC_SENTINEL_NO_LOAD) {
     292            dfehc_set_transient_noautoload($key, $ratio, $ttl);
     293        }
    303294
    304295        $as_percent = (bool) apply_filters('dfehc_system_load_return_percent', false, $ratio);
    305         return $as_percent ? (float) round($ratio * 100, 2) : (float) $ratio;
    306     }
    307 }
     296        $memo = $as_percent ? (float) round($ratio * 100.0, 2) : (float) $ratio;
     297        return (float) $memo;
     298    }
     299}
  • dynamic-front-end-heartbeat-control/trunk/heartbeat-async.php

    r3427163 r3461136  
    1010
    1111if (!function_exists('dfehc_max_server_load')) {
    12     function dfehc_max_server_load(): int { static $v; if ($v === null) { $v = (int) apply_filters('dfehc_max_server_load', 85); } return $v; }
    13 }
     12    function dfehc_max_server_load(): int
     13    {
     14        static $v;
     15        if ($v === null) {
     16            $v = (int) apply_filters('dfehc_max_server_load', 82);
     17            $v = max(1, min(100, $v));
     18        }
     19        return $v;
     20    }
     21}
     22
    1423if (!function_exists('dfehc_min_interval')) {
    15     function dfehc_min_interval(): int { static $v; if ($v === null) { $v = (int) apply_filters('dfehc_min_interval', 15); } return $v; }
    16 }
     24    function dfehc_min_interval(): int
     25    {
     26        static $v;
     27        if ($v === null) {
     28            $v = (int) apply_filters('dfehc_min_interval', 20);
     29            $v = max(5, $v);
     30        }
     31        return $v;
     32    }
     33}
     34
    1735if (!function_exists('dfehc_max_interval')) {
    18     function dfehc_max_interval(): int { static $v; if ($v === null) { $v = (int) apply_filters('dfehc_max_interval', 300); } return $v; }
    19 }
     36    function dfehc_max_interval(): int
     37    {
     38        static $v;
     39        if ($v === null) {
     40            $v = (int) apply_filters('dfehc_max_interval', 300);
     41            $v = max(dfehc_min_interval(), $v);
     42        }
     43        return $v;
     44    }
     45}
     46
    2047if (!function_exists('dfehc_fallback_interval')) {
    21     function dfehc_fallback_interval(): int { static $v; if ($v === null) { $v = (int) apply_filters('dfehc_fallback_interval', 60); } return $v; }
     48    function dfehc_fallback_interval(): int
     49    {
     50        static $v;
     51        if ($v === null) {
     52            $v = (int) apply_filters('dfehc_fallback_interval', 90);
     53            $v = max(dfehc_min_interval(), min(dfehc_max_interval(), $v));
     54        }
     55        return $v;
     56    }
    2257}
    2358
     
    2661    {
    2762        static $t = '';
    28         if ($t !== '') return $t;
    29         $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown'));
     63        if ($t !== '') {
     64            return $t;
     65        }
     66
     67        $url = '';
     68        if (defined('WP_HOME') && is_string(WP_HOME) && WP_HOME !== '') {
     69            $url = WP_HOME;
     70        } elseif (function_exists('home_url')) {
     71            $url = (string) home_url();
     72        }
     73
     74        $host = '';
     75        if ($url !== '' && function_exists('wp_parse_url')) {
     76            $parts = wp_parse_url($url);
     77            if (is_array($parts) && isset($parts['host']) && is_string($parts['host'])) {
     78                $host = $parts['host'];
     79            }
     80        }
     81
     82        if ($host === '') {
     83            $host = @php_uname('n') ?: 'unknown';
     84        }
     85
    3086        $salt = defined('DB_NAME') ? (string) DB_NAME : '';
    3187        return $t = substr(md5((string) $host . $salt), 0, 10);
     
    4197}
    4298
     99if (!function_exists('dfehc_rand_jitter')) {
     100    function dfehc_rand_jitter(int $min, int $max): int
     101    {
     102        $min = (int) $min;
     103        $max = (int) $max;
     104        if ($max < $min) {
     105            $t = $min;
     106            $min = $max;
     107            $max = $t;
     108        }
     109        if ($min === $max) {
     110            return $min;
     111        }
     112        if (function_exists('random_int')) {
     113            try {
     114                return (int) random_int($min, $max);
     115            } catch (Throwable $e) {
     116                return (int) wp_rand($min, $max);
     117            }
     118        }
     119        return (int) wp_rand($min, $max);
     120    }
     121}
     122
    43123if (!function_exists('dfehc_store_lockfree')) {
    44124    function dfehc_store_lockfree(string $key, $value, int $ttl): bool
    45125    {
    46         if (function_exists('wp_cache_add') && wp_cache_add($key, $value, DFEHC_CACHE_GROUP, $ttl)) {
    47             return true;
    48         }
    49         return set_transient($key, $value, $ttl);
    50     }
    51 }
    52 
    53 function dfehc_register_ajax(string $action, callable $callback): void
    54 {
    55     add_action("wp_ajax_$action", $callback);
    56     $allow_public = false;
    57     if ($action === 'get_server_load') {
    58         $allow_public = (bool) apply_filters('dfehc_allow_public_server_load', false);
    59     } elseif ($action === 'dfehc_async_heartbeat') {
    60         $allow_public = (bool) apply_filters('dfehc_allow_public_async', false);
    61     } else {
    62         $allow_public = (bool) apply_filters("dfehc_{$action}_allow_public", false);
    63     }
    64     if ($allow_public) {
    65         add_action("wp_ajax_nopriv_$action", $callback);
    66     }
    67 }
    68 
    69 if (!function_exists('dfehc_set_transient_noautoload')) {
    70     function dfehc_set_transient_noautoload(string $key, $value, int $expiration): void
    71     {
    72         if (wp_using_ext_object_cache()) {
    73             if (function_exists('wp_cache_add')) {
    74                 if (!wp_cache_add($key, $value, DFEHC_CACHE_GROUP, $expiration)) {
    75                     wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $expiration);
    76                 }
    77             } else {
    78                 wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $expiration);
     126        $ttl = max(10, $ttl);
     127        if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_add')) {
     128            if (wp_cache_add($key, $value, DFEHC_CACHE_GROUP, $ttl)) {
     129                return true;
     130            }
     131        }
     132        return (bool) set_transient($key, $value, $ttl);
     133    }
     134}
     135
     136if (!function_exists('dfehc_cache_get')) {
     137    function dfehc_cache_get(string $key)
     138    {
     139        if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
     140            $v = wp_cache_get($key, DFEHC_CACHE_GROUP);
     141            return $v === false ? null : $v;
     142        }
     143        $v = get_transient($key);
     144        return $v === false ? null : $v;
     145    }
     146}
     147
     148if (!function_exists('dfehc_register_ajax')) {
     149    function dfehc_register_ajax(string $action, callable $callback): void
     150    {
     151        add_action("wp_ajax_$action", $callback);
     152
     153        if ($action === 'get_server_load') {
     154            if ((bool) apply_filters('dfehc_allow_public_server_load', false)) {
     155                add_action("wp_ajax_nopriv_$action", $callback);
    79156            }
    80157            return;
    81158        }
    82         dfehc_store_lockfree($key, $value, $expiration);
    83         global $wpdb;
    84         if (!isset($wpdb) || !is_object($wpdb) || !isset($wpdb->options)) {
     159
     160        if ($action === 'dfehc_async_heartbeat') {
     161            if ((bool) apply_filters('dfehc_allow_public_async', false)) {
     162                add_action("wp_ajax_nopriv_$action", $callback);
     163            }
    85164            return;
    86165        }
    87         $opt_key = "_transient_$key";
    88         $opt_key_to = "_transient_timeout_$key";
    89         $wpdb->suppress_errors(true);
    90         $autoload = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key));
    91         if ($autoload === 'yes') {
    92             $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key, 'autoload' => 'yes'], ['%s'], ['%s','%s']);
    93         }
    94         $autoload_to = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_to));
    95         if ($autoload_to === 'yes') {
    96             $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key_to, 'autoload' => 'yes'], ['%s'], ['%s','%s']);
    97         }
    98         $wpdb->suppress_errors(false);
     166
     167        if ((bool) apply_filters("dfehc_{$action}_allow_public", false)) {
     168            add_action("wp_ajax_nopriv_$action", $callback);
     169        }
    99170    }
    100171}
     
    107178            return (int) $cached;
    108179        }
     180
     181        $override = getenv('DFEHC_CPU_CORES');
     182        if ($override !== false && is_numeric($override) && (int) $override > 0) {
     183            $cached = (int) $override;
     184            return (int) $cached;
     185        }
     186        if (defined('DFEHC_CPU_CORES') && is_numeric(DFEHC_CPU_CORES) && (int) DFEHC_CPU_CORES > 0) {
     187            $cached = (int) DFEHC_CPU_CORES;
     188            return (int) $cached;
     189        }
     190
    109191        $detected = 1;
    110         if (is_readable('/proc/cpuinfo')) {
    111             $cnt = preg_match_all('/^processor/m', (string) file_get_contents('/proc/cpuinfo'));
    112             if ($cnt > 0) {
    113                 $detected = $cnt;
    114             }
    115         } else {
    116             $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
    117             if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true)) {
    118                 $out = (string) (shell_exec('nproc 2>/dev/null') ?? '');
    119                 if (ctype_digit(trim($out)) && (int) $out > 0) {
    120                     $detected = (int) trim($out);
     192
     193        if (is_readable('/sys/fs/cgroup/cpu.max')) {
     194            $line = trim((string) @file_get_contents('/sys/fs/cgroup/cpu.max'));
     195            if ($line !== '') {
     196                $parts = preg_split('/\s+/', $line);
     197                if (is_array($parts) && count($parts) >= 2) {
     198                    $quota  = is_numeric($parts[0]) ? (float) $parts[0] : 0.0;
     199                    $period = is_numeric($parts[1]) ? (float) $parts[1] : 0.0;
     200                    if ($period > 0.0 && $quota > 0.0) {
     201                        $detected = max(1, (int) ceil($quota / $period));
     202                    }
    121203                }
    122204            }
    123205        }
    124         $env_override = getenv('DFEHC_CPU_CORES');
    125         if ($env_override !== false && ctype_digit((string) $env_override) && (int) $env_override > 0) {
    126             $detected = (int) $env_override;
    127         }
    128         $detected = (int) apply_filters('dfehc_cpu_cores', $detected);
    129         if ($detected <= 0) {
    130             $detected = 1;
    131         }
     206
     207        if ($detected === 1 && is_readable('/proc/cpuinfo')) {
     208            $content = (string) @file_get_contents('/proc/cpuinfo');
     209            if ($content !== '') {
     210                $cnt = preg_match_all('/^processor\s*:/m', $content);
     211                if ($cnt > 0) {
     212                    $detected = $cnt;
     213                }
     214            }
     215        }
     216
     217        $detected = (int) apply_filters('dfehc_cpu_cores', max(1, $detected));
    132218        $cached = $detected;
    133219        return (int) $cached;
     
    135221}
    136222
    137 function dfehc_get_or_calculate_server_load()
    138 {
    139     $key = dfehc_scoped_key(DFEHC_SERVER_LOAD);
    140     $load = get_transient($key);
    141     if ($load !== false) {
    142         return (float) $load;
    143     }
    144     $ttl = dfehc_server_load_ttl();
    145     $lock = dfehc_acquire_lock(DFEHC_LOAD_LOCK_BASE, $ttl + 5);
    146     if (!$lock) {
     223if (!function_exists('dfehc_should_attempt_load_calculation')) {
     224    function dfehc_should_attempt_load_calculation(): bool
     225    {
     226        $is_cron = function_exists('wp_doing_cron') && wp_doing_cron();
     227        $is_ajax = function_exists('wp_doing_ajax') && wp_doing_ajax();
     228        $is_json = function_exists('wp_is_json_request') && wp_is_json_request();
     229        $is_cli  = defined('WP_CLI') && WP_CLI;
     230
     231        if ($is_cron) {
     232            return (bool) apply_filters('dfehc_allow_load_calc_in_cron', true);
     233        }
     234
     235        if ($is_ajax || $is_json || $is_cli) {
     236            return (bool) apply_filters('dfehc_allow_load_calc_in_non_html', false);
     237        }
     238
     239        if (is_admin()) {
     240            return (bool) apply_filters('dfehc_allow_load_calc_in_admin', false);
     241        }
     242
     243        $rate = (float) apply_filters('dfehc_load_calc_sample_rate', 0.15);
     244        if (!is_finite($rate) || $rate < 0.0) $rate = 0.0;
     245        if ($rate > 1.0) $rate = 1.0;
     246
     247        if ($rate < 1.0) {
     248            $r = mt_rand(0, 1000000) / 1000000;
     249            if ($r > $rate) {
     250                return false;
     251            }
     252        }
     253
     254        $cd = (int) apply_filters('dfehc_load_calc_cooldown', 30);
     255        $cd = max(0, $cd);
     256        if ($cd > 0) {
     257            $cdKey = dfehc_scoped_key('dfehc_load_calc_cd');
     258            if (dfehc_cache_get($cdKey) !== null) {
     259                return false;
     260            }
     261            dfehc_set_transient_noautoload($cdKey, 1, $cd);
     262        }
     263
     264        return true;
     265    }
     266}
     267
     268if (!function_exists('dfehc_get_or_calculate_server_load')) {
     269    function dfehc_get_or_calculate_server_load()
     270    {
     271        static $requestMemo = null;
     272        if ($requestMemo !== null) {
     273            return $requestMemo;
     274        }
     275
     276        $key = dfehc_scoped_key(DFEHC_SERVER_LOAD);
     277        $cached = dfehc_cache_get($key);
     278
     279        $ttl = (int) apply_filters('dfehc_server_load_ttl', 120);
     280        $ttl = max(30, $ttl);
     281
     282        $staleTtl = (int) apply_filters('dfehc_server_load_stale_ttl', max(300, $ttl * 10));
     283        $staleTtl = max($ttl, $staleTtl);
     284
     285        $now = time();
     286        $cachedPayloadKey = dfehc_scoped_key('dfehc_server_load_payload');
     287        $payload = dfehc_cache_get($cachedPayloadKey);
     288
     289        if (is_array($payload) && isset($payload['v'], $payload['t']) && is_numeric($payload['v']) && is_numeric($payload['t'])) {
     290            $age = $now - (int) $payload['t'];
     291            if ($age < 0) $age = 0;
     292            if ($age <= $ttl) {
     293                $requestMemo = (float) $payload['v'];
     294                return $requestMemo;
     295            }
     296            if ($age <= $staleTtl && !$cached) {
     297                $cached = (float) $payload['v'];
     298            }
     299        }
     300
     301        if ($cached !== null && is_numeric($cached)) {
     302            $requestMemo = (float) $cached;
     303            if (!dfehc_should_attempt_load_calculation()) {
     304                return $requestMemo;
     305            }
     306        } else {
     307            if (!dfehc_should_attempt_load_calculation()) {
     308                $requestMemo = false;
     309                return $requestMemo;
     310            }
     311        }
     312
     313        $lock = dfehc_acquire_lock(DFEHC_LOAD_LOCK_BASE, $ttl + 10);
     314        if (!$lock) {
     315            if ($cached !== null && is_numeric($cached)) {
     316                $requestMemo = (float) $cached;
     317                return $requestMemo;
     318            }
     319            if (is_array($payload) && isset($payload['v'], $payload['t']) && is_numeric($payload['v']) && is_numeric($payload['t'])) {
     320                $age = $now - (int) $payload['t'];
     321                if ($age < 0) $age = 0;
     322                if ($age <= $staleTtl) {
     323                    $requestMemo = (float) $payload['v'];
     324                    return $requestMemo;
     325                }
     326            }
     327            $requestMemo = false;
     328            return $requestMemo;
     329        }
     330
     331        try {
     332            $raw = dfehc_calculate_server_load();
     333            if ($raw === false) {
     334                $requestMemo = false;
     335                return $requestMemo;
     336            }
     337
     338            $cores = max(1, dfehc_get_cpu_cores());
     339            $load_pct = min(100.0, round(((float) $raw / $cores) * 100.0, 2));
     340
     341            $payloadStore = ['v' => (float) $load_pct, 't' => (int) $now];
     342            dfehc_set_transient_noautoload($cachedPayloadKey, $payloadStore, $ttl + dfehc_rand_jitter(0, 10));
     343            dfehc_set_transient_noautoload($key, (float) $load_pct, $ttl);
     344
     345            $requestMemo = (float) $load_pct;
     346            return $requestMemo;
     347        } finally {
     348            dfehc_release_lock($lock);
     349        }
     350    }
     351}
     352
     353if (!function_exists('dfehc_calculate_server_load')) {
     354    function dfehc_calculate_server_load()
     355    {
     356        if (function_exists('sys_getloadavg')) {
     357            $load = sys_getloadavg();
     358            if (is_array($load) && isset($load[0]) && is_numeric($load[0]) && (float) $load[0] >= 0.0) {
     359                return (float) $load[0];
     360            }
     361        }
     362
     363        if (is_readable('/proc/loadavg')) {
     364            $content = (string) @file_get_contents('/proc/loadavg');
     365            $parts = explode(' ', trim($content));
     366            if (isset($parts[0]) && is_numeric($parts[0]) && (float) $parts[0] >= 0.0) {
     367                return (float) $parts[0];
     368            }
     369        }
     370
     371        if (class_exists(\DynamicHeartbeat\Dfehc_ServerLoadEstimator::class)) {
     372            $pct = \DynamicHeartbeat\Dfehc_ServerLoadEstimator::get_server_load();
     373            if ($pct !== false && is_numeric($pct) && (float) $pct >= 0.0) {
     374                $cores = max(1, dfehc_get_cpu_cores());
     375                return (float) ($cores * (((float) $pct) / 100.0));
     376            }
     377        }
     378
    147379        return false;
    148380    }
    149     $raw = dfehc_calculate_server_load();
    150     dfehc_release_lock($lock);
    151     if ($raw === false) {
    152         return false;
    153     }
    154     $cores = max(1, dfehc_get_cpu_cores());
    155     $load_pct = min(100.0, round($raw / $cores * 100, 2));
    156     dfehc_set_transient_noautoload($key, $load_pct, $ttl);
    157     return $load_pct;
    158 }
    159 
    160 function dfehc_calculate_server_load()
    161 {
    162     if (function_exists('sys_getloadavg')) {
    163         $load = sys_getloadavg();
    164         if (isset($load[0])) {
    165             return (float) $load[0];
    166         }
    167     }
    168     if (is_readable('/proc/loadavg')) {
    169         $parts = explode(' ', (string) file_get_contents('/proc/loadavg'));
    170         if (isset($parts[0])) {
    171             return (float) $parts[0];
    172         }
    173     }
    174     $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
    175     if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true)) {
    176         $out = (string) (shell_exec('LANG=C uptime 2>/dev/null') ?? '');
    177         if ($out && preg_match('/load average[s]?:\s*([0-9.]+)/', $out, $m)) {
    178             return (float) $m[1];
    179         }
    180     }
    181     if (defined('DFEHC_PLUGIN_PATH')) {
    182         $estimator = rtrim((string) DFEHC_PLUGIN_PATH, "/\\") . '/defibrillator/load-estimator.php';
    183         if (file_exists($estimator)) {
    184             require_once $estimator;
    185             if (class_exists(\DynamicHeartbeat\Dfehc_ServerLoadEstimator::class)) {
    186                 if (method_exists(\DynamicHeartbeat\Dfehc_ServerLoadEstimator::class, 'get_server_load')) {
    187                     $pct = \DynamicHeartbeat\Dfehc_ServerLoadEstimator::get_server_load();
    188                     if ($pct !== false && is_numeric($pct)) {
    189                         $cores = max(1, dfehc_get_cpu_cores());
    190                         return (float) ($cores * ((float) $pct) / 100.0);
    191                     }
    192                 } elseif (method_exists(\DynamicHeartbeat\Dfehc_ServerLoadEstimator::class, 'estimate')) {
    193                     $raw = \DynamicHeartbeat\Dfehc_ServerLoadEstimator::estimate();
    194                     if (is_numeric($raw)) {
    195                         return (float) $raw;
    196                     }
    197                 }
    198             }
    199         }
    200     }
    201     return false;
    202381}
    203382
     
    216395    public function maybe_schedule(): void
    217396    {
    218         if ($this->scheduled === true) {
     397        if ($this->scheduled) {
    219398            return;
    220399        }
    221400        $this->scheduled = true;
    222         $interval = absint(300);
     401
     402        if (!function_exists('wp_next_scheduled') || !function_exists('wp_schedule_event')) {
     403            return;
     404        }
     405
     406        $recurrence = (string) apply_filters('dfehc_async_cron_recurrence', 'dfehc_5_minutes');
     407        $schedules = function_exists('wp_get_schedules') ? (array) wp_get_schedules() : [];
     408
     409        if (!isset($schedules[$recurrence])) {
     410            $recurrence = 'dfehc_5_minutes';
     411        }
     412        if (!isset($schedules[$recurrence])) {
     413            $recurrence = 'hourly';
     414        }
     415
     416        $interval = isset($schedules[$recurrence]['interval']) ? (int) $schedules[$recurrence]['interval'] : 3600;
     417        $interval = max(60, $interval);
     418
    223419        $now = time();
    224420        $aligned = $now - ($now % $interval) + $interval;
    225         $schedules = function_exists('wp_get_schedules') ? wp_get_schedules() : [];
    226         if (!isset($schedules['dfehc_5_minutes'])) {
    227             return;
    228         }
     421
    229422        if (!wp_next_scheduled($this->action)) {
    230             wp_schedule_event($aligned, 'dfehc_5_minutes', $this->action);
    231         } else {
    232             $next = wp_next_scheduled($this->action);
    233             if ($next === false || ($next - $now) > ($interval * 2)) {
    234                 wp_schedule_single_event($now + $interval, $this->action);
    235             }
     423            wp_schedule_event($aligned, $recurrence, $this->action);
    236424        }
    237425    }
     
    239427    public function handle_async_request(): void
    240428    {
    241         $cap = apply_filters('dfehc_required_capability', DFEHC_CAPABILITY);
    242429        $allow_public = (bool) apply_filters('dfehc_allow_public_async', false);
    243 
    244430        $nonce_action = 'dfehc-' . $this->action;
    245         $nonce = '';
    246         if (isset($_REQUEST['nonce'])) {
    247             $nonce = sanitize_text_field(wp_unslash((string) $_REQUEST['nonce']));
    248         }
     431        $nonce = isset($_REQUEST['nonce']) ? sanitize_text_field(wp_unslash((string) $_REQUEST['nonce'])) : '';
    249432
    250433        if (!$allow_public) {
    251434            $valid = function_exists('check_ajax_referer')
    252                 ? check_ajax_referer($nonce_action, 'nonce', false)
    253                 : wp_verify_nonce($nonce, $nonce_action);
     435                ? (bool) check_ajax_referer($nonce_action, 'nonce', false)
     436                : (bool) wp_verify_nonce($nonce, $nonce_action);
    254437
    255438            if (!$valid) {
    256                 wp_send_json_error(['message' => 'dfehc/dfehc_async_heartbeat: invalid nonce'], 403);
    257             }
    258             if (!current_user_can($cap)) {
    259                 wp_send_json_error(['message' => 'dfehc/dfehc_async_heartbeat: not authorized'], 403);
     439                wp_send_json_error(['message' => 'invalid nonce'], 403);
     440            }
     441
     442            $cap = apply_filters('dfehc_required_capability', DFEHC_CAPABILITY);
     443            if (!current_user_can((string) $cap)) {
     444                wp_send_json_error(['message' => 'not authorized'], 403);
    260445            }
    261446        } else {
    262             $limit = absint(apply_filters('dfehc_public_rate_limit', 30));
    263             $window = absint(apply_filters('dfehc_public_rate_window', 60));
    264             $ip = dfehc_client_ip();
     447            $limit = (int) apply_filters('dfehc_public_rate_limit', 20);
     448            $window = (int) apply_filters('dfehc_public_rate_window', 60);
     449            $limit = max(1, $limit);
     450            $window = max(1, $window);
     451
     452            $ip = function_exists('dfehc_client_ip') ? (string) dfehc_client_ip() : '0.0.0.0';
    265453            $rl_key = dfehc_scoped_key('dfehc_rl_' . md5($ip));
    266454            $cnt = (int) get_transient($rl_key);
     455
    267456            if ($cnt >= $limit) {
    268                 wp_send_json_error(['message' => 'dfehc/dfehc_async_heartbeat: rate limited'], 429);
     457                wp_send_json_error(['message' => 'rate limited'], 429);
    269458            }
    270459            dfehc_set_transient_noautoload($rl_key, $cnt + 1, $window);
    271460        }
    272461
    273         try {
    274             $this->run_action();
    275             wp_send_json_success(true);
    276         } catch (\Throwable $e) {
    277             wp_send_json_error(['message' => 'dfehc/dfehc_async_heartbeat: internal error'], 500);
    278         }
    279 
    280         wp_die();
     462        $this->run_action();
     463        wp_send_json_success(true);
    281464    }
    282465
    283466    public function run_action(): void
    284467    {
    285         $lock = dfehc_acquire_lock('dfehc_async_run', 30);
     468        $lock_key = dfehc_scoped_key('dfehc_async_run');
     469        $lock_ttl = (int) apply_filters('dfehc_async_lock_ttl', 45);
     470        $lock_ttl = max(10, min(120, $lock_ttl));
     471
     472        $lock = dfehc_acquire_lock($lock_key, $lock_ttl);
    286473        if (!$lock) {
    287474            return;
    288475        }
     476
    289477        try {
    290             $last_activity = get_transient(dfehc_scoped_key('dfehc_last_user_activity'));
    291             if ($last_activity === false) {
    292                 dfehc_set_transient_noautoload(dfehc_scoped_key(DFEHC_RECOMMENDED_INTERVAL), dfehc_fallback_interval(), 300);
     478            $activity_key = dfehc_scoped_key('dfehc_last_user_activity');
     479            $last_activity = dfehc_cache_get($activity_key);
     480            if ($last_activity === null || !is_numeric($last_activity)) {
     481                dfehc_set_transient_noautoload(dfehc_scoped_key(DFEHC_RECOMMENDED_INTERVAL), dfehc_fallback_interval(), 600);
    293482                return;
    294483            }
     484
    295485            $now = time();
    296             $elapsed = $now - (int) $last_activity;
    297             if ($elapsed < 0) {
    298                 $elapsed = 0;
    299             }
    300             $load_raw = dfehc_calculate_server_load();
    301             if ($load_raw === false) {
    302                 dfehc_set_transient_noautoload(dfehc_scoped_key(DFEHC_RECOMMENDED_INTERVAL), dfehc_fallback_interval(), 300);
     486            $elapsed = max(0, $now - (int) $last_activity);
     487
     488            $load_pct = dfehc_get_or_calculate_server_load();
     489            if ($load_pct === false || !is_numeric($load_pct)) {
     490                dfehc_set_transient_noautoload(dfehc_scoped_key(DFEHC_RECOMMENDED_INTERVAL), dfehc_fallback_interval(), 600);
    303491                return;
    304492            }
    305             $cores = max(1, dfehc_get_cpu_cores());
    306             $load_pct = min(100.0, round($load_raw / $cores * 100, 2));
    307             dfehc_set_transient_noautoload(dfehc_scoped_key(DFEHC_SERVER_LOAD), $load_pct, dfehc_server_load_ttl());
    308             $samples = get_transient(dfehc_scoped_key(DFEHC_LOAD_AVERAGES));
     493            $load_pct = max(0.0, min(100.0, (float) $load_pct));
     494
     495            $ttl = (int) apply_filters('dfehc_server_load_ttl', 120);
     496            $ttl = max(30, $ttl);
     497            dfehc_set_transient_noautoload(dfehc_scoped_key(DFEHC_SERVER_LOAD), $load_pct, $ttl);
     498
     499            $samples_key = dfehc_scoped_key(DFEHC_LOAD_AVERAGES);
     500            $samples = dfehc_cache_get($samples_key);
    309501            if (!is_array($samples)) {
    310502                $samples = [];
    311503            }
     504
    312505            $samples[] = $load_pct;
    313             if (count($samples) > 5) {
    314                 array_shift($samples);
    315             }
    316             dfehc_set_transient_noautoload(dfehc_scoped_key(DFEHC_LOAD_AVERAGES), $samples, 1800);
    317             $weights_raw = apply_filters('dfehc_load_weights', [5, 4, 3, 2, 1]);
    318             $wr = array_values((array) $weights_raw);
    319             if (!$wr) {
    320                 $wr = [1];
    321             }
    322             $wr_count = count($wr);
    323             $weights = [];
     506
     507            $max_samples = (int) apply_filters('dfehc_load_avg_samples', 6);
     508            $max_samples = max(2, min(30, $max_samples));
     509
     510            if (count($samples) > $max_samples) {
     511                $samples = array_slice($samples, -$max_samples);
     512            }
     513
     514            dfehc_set_transient_noautoload($samples_key, array_values($samples), 3600);
     515
     516            $weights = (array) apply_filters('dfehc_load_weights', [4, 3, 2, 1]);
     517            $weights = array_values(array_filter($weights, static function ($v): bool {
     518                return is_numeric($v) && (float) $v > 0.0;
     519            }));
     520            if (!$weights) {
     521                $weights = [1];
     522            }
     523
    324524            $n = count($samples);
    325             for ($i = 0; $i < $n; $i++) {
    326                 $j = min($n - 1 - $i, $wr_count - 1);
    327                 $w = isset($wr[$j]) ? $wr[$j] : 1;
    328                 if (!is_numeric($w) || $w <= 0) {
    329                     $w = 1;
    330                 }
    331                 $weights[] = (float) $w;
    332             }
    333             $avg_load = dfehc_weighted_average($samples, $weights);
     525            $wCount = count($weights);
     526
     527            $weighted_sum = 0.0;
     528            $weight_sum = 0.0;
     529
     530            foreach ($samples as $i => $v) {
     531                $age = $n - 1 - (int) $i;
     532                $w = (float) ($weights[min($age, $wCount - 1)] ?? 1.0);
     533                $w = max(0.0001, $w);
     534                $weighted_sum += ((float) $v) * $w;
     535                $weight_sum += $w;
     536            }
     537
     538            $avg_load = $weight_sum > 0 ? round($weighted_sum / $weight_sum, 2) : 0.0;
     539            $avg_load = max(0.0, min(100.0, (float) $avg_load));
     540
    334541            $interval = $this->calculate_interval($elapsed, $avg_load);
    335             dfehc_set_transient_noautoload(dfehc_scoped_key(DFEHC_RECOMMENDED_INTERVAL), $interval, 300);
     542
     543            $rec_ttl = (int) apply_filters('dfehc_recommended_interval_ttl', 600);
     544            $rec_ttl = max(30, $rec_ttl + dfehc_rand_jitter(0, 10));
     545
     546            dfehc_set_transient_noautoload(dfehc_scoped_key(DFEHC_RECOMMENDED_INTERVAL), $interval, $rec_ttl);
    336547        } finally {
    337548            dfehc_release_lock($lock);
     
    341552    protected function calculate_interval(int $elapsed, float $load_pct): int
    342553    {
    343         if ($elapsed <= dfehc_min_interval() && $load_pct < dfehc_max_server_load()) {
    344             return dfehc_min_interval();
    345         }
    346         if ($elapsed >= dfehc_max_interval()) {
    347             return dfehc_max_interval();
    348         }
    349         $load_factor = min(1.0, $load_pct / dfehc_max_server_load());
    350         $activity_factor = ($elapsed - dfehc_min_interval()) / (dfehc_max_interval() - dfehc_min_interval());
     554        $min_i = dfehc_min_interval();
     555        $max_i = dfehc_max_interval();
     556        $max_load = (float) dfehc_max_server_load();
     557
     558        if ($max_i <= $min_i) {
     559            return $min_i;
     560        }
     561
     562        $load_pct = max(0.0, min(100.0, $load_pct));
     563        $elapsed = max(0, $elapsed);
     564
     565        $hard_load = (float) apply_filters('dfehc_hard_max_load_factor', 1.10);
     566        $hard_load = max(1.0, min(2.0, $hard_load));
     567
     568        if ($elapsed <= $min_i && $load_pct < $max_load) {
     569            return $min_i;
     570        }
     571        if ($elapsed >= $max_i || $load_pct >= ($max_load * $hard_load)) {
     572            return $max_i;
     573        }
     574
     575        $load_factor = $max_load > 0 ? min(1.0, $load_pct / $max_load) : 0.0;
     576
     577        $activity_factor = ($elapsed - $min_i) / ($max_i - $min_i);
    351578        $activity_factor = max(0.0, min(1.0, $activity_factor));
    352         $dominant = max($load_factor, $activity_factor);
    353         return (int) round(dfehc_min_interval() + $dominant * (dfehc_max_interval() - dfehc_min_interval()));
     579
     580        $act_w = (float) apply_filters('dfehc_activity_weight', 0.7);
     581        $load_w = (float) apply_filters('dfehc_load_weight', 0.3);
     582
     583        $act_w = max(0.0, min(1.0, $act_w));
     584        $load_w = max(0.0, min(1.0, $load_w));
     585
     586        $sum = $act_w + $load_w;
     587        if ($sum <= 0.0) {
     588            $act_w = 0.7;
     589            $load_w = 0.3;
     590            $sum = 1.0;
     591        }
     592        $act_w /= $sum;
     593        $load_w /= $sum;
     594
     595        $combined = max($load_factor, ($activity_factor * $act_w) + ($load_factor * $load_w));
     596        $combined = max(0.0, min(1.0, (float) $combined));
     597
     598        $interval = (int) round($min_i + $combined * ($max_i - $min_i));
     599        $interval = max($min_i, min($max_i, $interval));
     600
     601        $step = (int) apply_filters('dfehc_interval_step', 5);
     602        $step = max(0, $step);
     603        if ($step > 1) {
     604            $interval = (int) (round($interval / $step) * $step);
     605            $interval = max($min_i, min($max_i, $interval));
     606        }
     607
     608        return $interval;
    354609    }
    355610}
     
    363618    $tw = 0.0;
    364619    foreach ($values as $i => $v) {
    365         $w = isset($weights[$i]) ? $weights[$i] : 1;
    366         if (!is_numeric($w) || $w <= 0) {
    367             $w = 1;
    368         }
    369         $tv += (float) $v * (float) $w;
    370         $tw += (float) $w;
     620        $w = isset($weights[$i]) && is_numeric($weights[$i]) && (float) $weights[$i] > 0.0 ? (float) $weights[$i] : 1.0;
     621        $tv += (float) $v * $w;
     622        $tw += $w;
    371623    }
    372624    return $tw > 0 ? round($tv / $tw, 2) : 0.0;
     
    377629    {
    378630        $key = dfehc_scoped_key(DFEHC_RECOMMENDED_INTERVAL);
    379         $interval = get_transient($key);
    380         if ($interval !== false) {
     631        $interval = dfehc_cache_get($key);
     632        if ($interval !== null && is_numeric($interval)) {
    381633            return (float) $interval;
    382634        }
    383         return $current_load >= dfehc_max_server_load()
     635        $current_load = max(0.0, min(100.0, $current_load));
     636        return $current_load >= (float) dfehc_max_server_load()
    384637            ? (float) dfehc_max_interval()
    385638            : (float) dfehc_min_interval();
     
    390643{
    391644    if (!isset($s['dfehc_5_minutes'])) {
    392         $s['dfehc_5_minutes'] = ['interval' => 300, 'display' => __('Every 5 Minutes', 'dfehc')];
    393     }
    394     if (!isset($s['dfehc_daily'])) {
    395         $s['dfehc_daily'] = ['interval' => DAY_IN_SECONDS, 'display' => __('Once a day (DFEHC)', 'dfehc')];
     645        $s['dfehc_5_minutes'] = ['interval' => 300, 'display' => __('Every 5 Minutes (DFEHC)', 'dfehc')];
    396646    }
    397647    return $s;
    398648}
    399 add_filter('cron_schedules', 'dfehc_register_schedules', 1);
    400 
    401 function dfehc_prune_server_load_logs(): void
    402 {
    403     $max_age = absint(apply_filters('dfehc_log_retention_seconds', DAY_IN_SECONDS));
    404     $max_cnt = absint(apply_filters('dfehc_log_retention_max', 1440));
    405     $now = time();
    406     $all_ids = function_exists('get_sites')
    407         ? array_map('intval', get_sites(['fields' => 'ids']))
    408         : [get_current_blog_id()];
    409     $chunk_size = absint(apply_filters('dfehc_prune_chunk_size', 50));
    410     $offset_key = 'dfehc_prune_offset';
    411     $offset = (int) get_site_option($offset_key, 0);
    412     $chunk = array_slice($all_ids, $offset, $chunk_size);
    413     if ($chunk === []) {
    414         $offset = 0;
    415         $chunk = array_slice($all_ids, 0, $chunk_size);
    416     }
    417     foreach ($chunk as $id) {
    418         $did_switch = false;
    419         if (is_multisite()) {
    420             switch_to_blog($id);
    421             $did_switch = true;
    422         }
    423         try {
    424             $option = 'dfehc_server_load_logs_' . get_current_blog_id();
    425             $logs = get_option($option, []);
    426             if ($logs) {
    427                 $cutoff = $now - $max_age;
    428                 $logs = array_filter(
    429                     $logs,
    430                     static function ($row) use ($cutoff) {
    431                         return isset($row['timestamp']) && (int) $row['timestamp'] >= $cutoff;
    432                     }
    433                 );
    434                 if (count($logs) > $max_cnt) {
    435                     $logs = array_slice($logs, -$max_cnt);
    436                 }
    437                 update_option($option, array_values($logs), false);
    438             }
    439         } finally {
    440             if ($did_switch) {
    441                 restore_current_blog();
    442             }
    443         }
    444     }
    445     $offset += $chunk_size;
    446     update_site_option($offset_key, $offset);
    447 }
    448 
    449 add_action('dfehc_prune_logs_hook', 'dfehc_prune_server_load_logs');
    450 
    451 function dfehc_schedule_prune_logs(): void
    452 {
    453     if (!function_exists('wp_next_scheduled') || !function_exists('wp_schedule_event')) {
    454         return;
    455     }
    456     if (wp_next_scheduled('dfehc_prune_logs_hook')) {
    457         return;
    458     }
    459     $t = strtotime('today 03:00');
    460     if ($t < time()) {
    461         $t += DAY_IN_SECONDS;
    462     }
    463     wp_schedule_event($t, 'dfehc_daily', 'dfehc_prune_logs_hook');
    464 }
    465 
    466 add_action('init', 'dfehc_schedule_prune_logs', 1);
    467 
    468 add_filter('dfehc_required_capability', function () { return 'manage_options'; });
    469 add_filter('dfehc_server_load_ttl', function () { return 120; });
    470 add_filter('dfehc_load_weights', function () { return [3, 2, 1]; });
    471 add_filter('dfehc_async_retry', function () { return 1; });
    472 add_filter('dfehc_log_retention_seconds', function () { return 2 * DAY_IN_SECONDS; });
    473 add_filter('dfehc_log_retention_max', function () { return 3000; });
    474 add_filter('dfehc_allow_public_server_load', '__return_false');
    475 add_filter('dfehc_allow_public_async', '__return_false');
     649add_filter('cron_schedules', 'dfehc_register_schedules', 10);
    476650
    477651new Dfehc_Heartbeat_Async();
  • dynamic-front-end-heartbeat-control/trunk/heartbeat-controller.php

    r3427163 r3461136  
    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.998
     6Version: 1.2.998.1
    77Author: Codeloghin
    88Author URI: https://codeloghin.com
     
    7373}
    7474
    75 if (!function_exists('dfehc_set_transient_noautoload')) {
    76     function dfehc_set_transient_noautoload(string $key, $value, int $expiration): void
    77     {
    78         $jitter = 0;
    79         if (function_exists('random_int')) {
    80             try {
    81                 $jitter = random_int(0, 5);
    82             } catch (\Throwable $e) {
    83                 $jitter = 0;
    84             }
    85         }
    86         $expiration = max(1, $expiration + $jitter);
    87 
    88         if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
    89             wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $expiration);
    90             return;
    91         }
    92 
    93         set_transient($key, $value, $expiration);
    94 
    95         if (isset($GLOBALS['wpdb'])) {
    96             global $wpdb;
    97             if (!isset($wpdb) || !is_object($wpdb) || !isset($wpdb->options)) {
    98                 return;
    99             }
    100 
    101             $autoload_cache_key = 'dfehc_autoload_flag_' . md5((string) $key);
    102             $autoload_cache = wp_cache_get($autoload_cache_key, DFEHC_CACHE_GROUP);
    103             if ($autoload_cache === 'no') {
    104                 return;
    105             }
    106 
    107             $opt_key_val = '_transient_' . $key;
    108             $opt_key_to  = '_transient_timeout_' . $key;
    109 
    110             $wpdb->suppress_errors(true);
    111             try {
    112                 $autoload = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_val));
    113                 if ($autoload === 'yes') {
    114                     $wpdb->query($wpdb->prepare("UPDATE {$wpdb->options} SET autoload='no' WHERE option_name=%s AND autoload='yes' LIMIT 1", $opt_key_val));
    115                 }
    116                 $autoload_to = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_to));
    117                 if ($autoload_to === 'yes') {
    118                     $wpdb->query($wpdb->prepare("UPDATE {$wpdb->options} SET autoload='no' WHERE option_name=%s AND autoload='yes' LIMIT 1", $opt_key_to));
    119                 }
    120             } finally {
    121                 $wpdb->suppress_errors(false);
    122                 wp_cache_set($autoload_cache_key, 'no', DFEHC_CACHE_GROUP, DAY_IN_SECONDS);
    123             }
    124         }
    125     }
    126 }
    127 
    12875add_action('wp_default_scripts', function (WP_Scripts $scripts) {
    12976    if (is_admin() || wp_doing_ajax()) {
     
    13380    if (isset($scripts->registered['heartbeat'])) {
    13481        $scripts->registered['heartbeat']->src  = plugin_dir_url(__FILE__) . 'js/heartbeat.min.js';
    135         $scripts->registered['heartbeat']->ver  = '1.6.1';
     82        $scripts->registered['heartbeat']->ver  = '1.6.5';
    13683        $scripts->registered['heartbeat']->deps = array('jquery');
    13784    }
     
    14693    wp_enqueue_script('heartbeat');
    14794
     95    $site_key = function_exists('dfehc_scoped_key') ? (string) dfehc_scoped_key('site') : (string) wp_parse_url(home_url(), PHP_URL_HOST);
     96    $site_key = $site_key ?: (string) wp_parse_url(home_url(), PHP_URL_HOST);
     97    $site_key = $site_key ?: 'site';
     98
     99    $ver = defined('DFEHC_VERSION') ? (string) DFEHC_VERSION : (string) filemtime(__FILE__);
     100    $cache_duration_ms = (int) apply_filters('dfehc_js_cache_duration_ms', 10 * 60 * 1000);
     101    $cache_duration_ms = max(15000, min(60 * 60 * 1000, $cache_duration_ms));
     102
     103    $cache_bypass_rate = (float) apply_filters('dfehc_js_cache_bypass_rate', 0.05);
     104    $cache_bypass_rate = max(0.0, min(1.0, $cache_bypass_rate));
     105
     106    $leader_ttl_ms  = (int) apply_filters('dfehc_js_leader_ttl_ms', 8000);
     107    $leader_beat_ms = (int) apply_filters('dfehc_js_leader_beat_ms', 3000);
     108    $leader_ttl_ms  = max(2000, min(20000, $leader_ttl_ms));
     109    $leader_beat_ms = max(800, min(10000, $leader_beat_ms));
    148110    $load = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : null;
    149111    if ($load === false || $load === null) {
    150         $load = (float) DFEHC_MAX_SERVER_LOAD;
    151     }
     112    $load = (float) DFEHC_MAX_SERVER_LOAD;
     113    }
     114    $load = (float) $load;
    152115
    153116    $recommended = function_exists('dfehc_calculate_recommended_interval_user_activity')
    154         ? dfehc_calculate_recommended_interval_user_activity((float) $load, DFEHC_BATCH_SIZE)
    155         : 60.0;
    156 
    157     $host = wp_parse_url(home_url(), PHP_URL_HOST) ?: 'site';
    158     $ver  = defined('DFEHC_VERSION') ? (string) DFEHC_VERSION : (string) filemtime(__FILE__);
    159 
    160     $min_interval_option = defined('DFEHC_OPTION_MIN_INTERVAL') ? (int) get_option(DFEHC_OPTION_MIN_INTERVAL, defined('DFEHC_DEFAULT_MIN_INTERVAL') ? DFEHC_DEFAULT_MIN_INTERVAL : DFEHC_MIN_INTERVAL) : (int) DFEHC_MIN_INTERVAL;
    161     $max_interval_option = defined('DFEHC_OPTION_MAX_INTERVAL') ? (int) get_option(DFEHC_OPTION_MAX_INTERVAL, defined('DFEHC_DEFAULT_MAX_INTERVAL') ? DFEHC_DEFAULT_MAX_INTERVAL : DFEHC_MAX_INTERVAL) : (int) DFEHC_MAX_INTERVAL;
    162     $max_server_load_option = defined('DFEHC_OPTION_MAX_SERVER_LOAD') ? (float) get_option(DFEHC_OPTION_MAX_SERVER_LOAD, defined('DFEHC_DEFAULT_MAX_SERVER_LOAD') ? DFEHC_DEFAULT_MAX_SERVER_LOAD : DFEHC_MAX_SERVER_LOAD) : (float) DFEHC_MAX_SERVER_LOAD;
    163 
     117    ? dfehc_calculate_recommended_interval_user_activity($load, DFEHC_BATCH_SIZE)
     118    : 60.0;
    164119    wp_localize_script(
    165120        'heartbeat',
     
    207162{
    208163    $total_weighted_duration = 0.0;
    209     $total_weight            = 0;
    210     $offset                  = 0;
    211     $start_time              = microtime(true);
    212     $time_limit              = 1.5;
     164    $total_weight = 0;
     165
     166    $batch_size = max(1, min(75, $batch_size));
     167    $offset = 0;
     168
     169    if (is_admin() && !(function_exists('wp_doing_ajax') && wp_doing_ajax())) {
     170        $allow_admin = (bool) apply_filters('dfehc_allow_user_activity_scan_in_admin', false);
     171        if (!$allow_admin) {
     172            return ['total_duration' => 0.0, 'total_weight' => 0];
     173        }
     174    }
     175
     176    $start_time = function_exists('microtime') ? (float) microtime(true) : (float) time();
     177
     178    $time_limit = (float) apply_filters('dfehc_activity_summary_time_limit', 0.35);
     179    if (!is_finite($time_limit) || $time_limit <= 0.0) {
     180        $time_limit = 0.35;
     181    }
     182    $time_limit = max(0.10, min(1.50, $time_limit));
     183    $deadline = $start_time + $time_limit;
     184
     185    $max_users = (int) apply_filters('dfehc_activity_summary_max_users', 75);
     186    $max_users = max(1, min(75, $max_users));
     187
     188    $max_points_per_user = (int) apply_filters('dfehc_activity_summary_max_points_per_user', 40);
     189    $max_points_per_user = max(1, min(75, $max_points_per_user));
     190
     191    $processed = 0;
     192
    213193    while (true) {
    214         if (microtime(true) - $start_time > $time_limit) {
     194        $now = function_exists('microtime') ? (float) microtime(true) : (float) time();
     195        if ($now > $deadline) {
    215196            break;
    216197        }
     198        if ($processed >= $max_users) {
     199            break;
     200        }
     201
    217202        $userBatch = dfehc_get_users_in_batches($batch_size, $offset);
    218         if (!$userBatch) {
     203        if (!$userBatch || !is_array($userBatch)) {
    219204            break;
    220205        }
     206
    221207        foreach ($userBatch as $user) {
    222             $activity = get_user_meta($user->ID, 'dfehc_user_activity', true);
    223             if (empty($activity['durations']) || !is_array($activity['durations'])) {
     208            $processed++;
     209            if ($processed > $max_users) {
     210                break 2;
     211            }
     212
     213            $uid = 0;
     214            if (is_object($user) && isset($user->ID)) {
     215                $uid = (int) $user->ID;
     216            } elseif (is_numeric($user)) {
     217                $uid = (int) $user;
     218            }
     219
     220            if ($uid <= 0) {
    224221                continue;
    225222            }
    226             $weight = count($activity['durations']);
    227             if ($weight <= 0) {
     223
     224            $activity = get_user_meta($uid, 'dfehc_user_activity', true);
     225            if (!is_array($activity) || empty($activity['durations']) || !is_array($activity['durations'])) {
    228226                continue;
    229227            }
    230             $avg = array_sum($activity['durations']) / $weight;
    231             $total_weighted_duration += $weight * (float) $avg;
    232             $total_weight            += $weight;
    233         }
     228
     229            $durations = $activity['durations'];
     230            $cnt_all = count($durations);
     231            if ($cnt_all <= 0) {
     232                continue;
     233            }
     234
     235            if ($cnt_all > $max_points_per_user) {
     236                $durations = array_slice($durations, -$max_points_per_user);
     237            }
     238
     239            $sum = 0.0;
     240            $cnt = 0;
     241
     242            foreach ($durations as $d) {
     243                if (!is_numeric($d)) {
     244                    continue;
     245                }
     246                $v = (float) $d;
     247                if (!is_finite($v) || $v < 0.0) {
     248                    continue;
     249                }
     250                $sum += $v;
     251                $cnt++;
     252            }
     253
     254            if ($cnt <= 0) {
     255                continue;
     256            }
     257
     258            $avg = $sum / $cnt;
     259            $total_weighted_duration += $cnt * $avg;
     260            $total_weight += $cnt;
     261
     262            $now = function_exists('microtime') ? (float) microtime(true) : (float) time();
     263            if ($now > $deadline) {
     264                break 2;
     265            }
     266        }
     267
    234268        $offset += $batch_size;
    235269    }
     270
    236271    return ['total_duration' => $total_weighted_duration, 'total_weight' => $total_weight];
    237272}
     
    347382        $raw = isset($_SERVER['REMOTE_ADDR']) ? (string) wp_unslash($_SERVER['REMOTE_ADDR']) : '0.0.0.0';
    348383        $ip = sanitize_text_field($raw);
    349         if ($ip === '') {
    350             $ip = '0.0.0.0';
    351         }
     384        $ip = $ip !== '' ? $ip : '0.0.0.0';
    352385    }
    353386
     
    356389    $limit = (int) apply_filters('dfehc_recommend_rl_limit', 30);
    357390    $window = (int) apply_filters('dfehc_recommend_rl_window', 60);
     391    $limit = max(1, $limit);
     392    $window = max(1, $window);
     393
    358394    if ($cnt >= $limit) {
    359395        wp_send_json_error(['message' => 'rate_limited'], 429);
     
    361397    dfehc_set_transient_noautoload($rlk, $cnt + 1, $window);
    362398
     399    $now = time();
     400
     401    $snap_key = dfehc_scoped_tkey('dfehc_ri_snapshot');
     402    $snap_ttl = (int) apply_filters('dfehc_ri_snapshot_ttl', 15);
     403    $snap_ttl = max(5, min(120, $snap_ttl));
     404
     405    $stale_ttl = (int) apply_filters('dfehc_ri_snapshot_stale_ttl', 90);
     406    $stale_ttl = max($snap_ttl, min(600, $stale_ttl));
     407
     408    $snapshot = get_transient($snap_key);
     409    $fresh = false;
     410
     411    if (is_array($snapshot) && isset($snapshot['t'], $snapshot['interval'])) {
     412        $age = $now - (int) $snapshot['t'];
     413        if ($age < 0) {
     414            $age = 0;
     415        }
     416        if ($age <= $snap_ttl) {
     417            $fresh = true;
     418            $interval = (float) $snapshot['interval'];
     419            $load = isset($snapshot['load']) && is_numeric($snapshot['load']) ? (float) $snapshot['load'] : null;
     420
     421            wp_send_json_success([
     422                'interval' => $interval > 0 ? $interval : (float) DFEHC_MIN_INTERVAL,
     423                'load' => $load,
     424                'fresh' => 1,
     425                'next_poll_ms' => (int) apply_filters('dfehc_ajax_next_poll_ms_fresh', 45000),
     426            ]);
     427        }
     428        if ($age <= $stale_ttl) {
     429            $interval = (float) $snapshot['interval'];
     430            $load = isset($snapshot['load']) && is_numeric($snapshot['load']) ? (float) $snapshot['load'] : null;
     431
     432            $lock_key = dfehc_scoped_tkey('dfehc_ri_snapshot_lock');
     433            $lock_ttl = (int) apply_filters('dfehc_ri_snapshot_lock_ttl', 10);
     434            $lock_ttl = max(3, min(30, $lock_ttl));
     435
     436            $do_recompute = false;
     437
     438            $global_rl_key = dfehc_scoped_tkey('dfehc_ri_global_recompute_rl');
     439            $global_rl_win = (int) apply_filters('dfehc_ri_global_recompute_window', 10);
     440            $global_rl_lim = (int) apply_filters('dfehc_ri_global_recompute_limit', 4);
     441            $global_rl_win = max(2, min(60, $global_rl_win));
     442            $global_rl_lim = max(1, min(50, $global_rl_lim));
     443
     444            $global_cnt = (int) get_transient($global_rl_key);
     445            if ($global_cnt < $global_rl_lim) {
     446                $do_recompute = true;
     447            }
     448
     449            if ($do_recompute) {
     450                $got_lock = false;
     451
     452                if (function_exists('wp_cache_add') && function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
     453                    $got_lock = wp_cache_add($lock_key, 1, DFEHC_CACHE_GROUP, $lock_ttl);
     454                } else {
     455                    $got_lock = (get_transient($lock_key) === false) && set_transient($lock_key, 1, $lock_ttl);
     456                }
     457
     458                if ($got_lock) {
     459                    dfehc_set_transient_noautoload($global_rl_key, $global_cnt + 1, $global_rl_win);
     460
     461                    $interval_new = dfehc_get_recommended_heartbeat_interval_async();
     462                    if (!is_numeric($interval_new) || (float) $interval_new <= 0.0) {
     463                        $interval_new = $interval;
     464                    }
     465
     466                    $load_new = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : null;
     467                    $load_new = (is_numeric($load_new) && (float) $load_new >= 0.0) ? (float) $load_new : $load;
     468
     469                    $snapshot_new = [
     470                        't' => $now,
     471                        'interval' => (float) $interval_new,
     472                        'load' => $load_new,
     473                    ];
     474
     475                    dfehc_set_transient_noautoload($snap_key, $snapshot_new, $stale_ttl);
     476
     477                    if (function_exists('wp_cache_delete') && function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
     478                        wp_cache_delete($lock_key, DFEHC_CACHE_GROUP);
     479                    } else {
     480                        delete_transient($lock_key);
     481                    }
     482
     483                    wp_send_json_success([
     484                        'interval' => (float) $interval_new,
     485                        'load' => $load_new,
     486                        'fresh' => 1,
     487                        'next_poll_ms' => (int) apply_filters('dfehc_ajax_next_poll_ms_fresh', 45000),
     488                    ]);
     489                }
     490            }
     491
     492            wp_send_json_success([
     493                'interval' => $interval > 0 ? $interval : (float) DFEHC_MIN_INTERVAL,
     494                'load' => $load,
     495                'fresh' => 0,
     496                'next_poll_ms' => (int) apply_filters('dfehc_ajax_next_poll_ms_stale', 90000),
     497            ]);
     498        }
     499    }
     500
    363501    $interval = dfehc_get_recommended_heartbeat_interval_async();
    364     if (!is_numeric($interval) || $interval <= 0) {
    365         $interval = (float) dfehc_calculate_recommended_interval_user_activity();
    366     }
    367     wp_send_json_success(['interval' => (float) $interval]);
    368 }
     502    if (!is_numeric($interval) || (float) $interval <= 0.0) {
     503        $interval = (float) DFEHC_MIN_INTERVAL;
     504    }
     505
     506    $load = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : null;
     507    $load = (is_numeric($load) && (float) $load >= 0.0) ? (float) $load : null;
     508
     509    dfehc_set_transient_noautoload($snap_key, [
     510        't' => $now,
     511        'interval' => (float) $interval,
     512        'load' => $load,
     513    ], $stale_ttl);
     514
     515    wp_send_json_success([
     516        'interval' => (float) $interval,
     517        'load' => $load,
     518        'fresh' => 1,
     519        'next_poll_ms' => (int) apply_filters('dfehc_ajax_next_poll_ms_fresh', 45000),
     520    ]);
     521}
     522
    369523add_action('wp_ajax_dfehc_update_heartbeat_interval', 'dfehc_get_recommended_intervals');
    370524add_action('wp_ajax_nopriv_dfehc_update_heartbeat_interval', 'dfehc_get_recommended_intervals');
     
    372526function dfehc_override_heartbeat_interval(array $settings): array
    373527{
    374     $interval             = (int) ($settings['interval'] ?? DFEHC_MIN_INTERVAL);
    375     $interval             = min(max($interval, (int) DFEHC_MIN_INTERVAL), (int) DFEHC_MAX_INTERVAL);
     528    if (is_admin() || (function_exists('wp_doing_ajax') && wp_doing_ajax())) {
     529        return $settings;
     530    }
     531
     532    if (get_option('dfehc_disable_heartbeat')) {
     533        $settings['interval'] = (int) DFEHC_MAX_INTERVAL;
     534        return $settings;
     535    }
     536
     537    $enabled = (string) get_option('dfehc_heartbeat_control_enabled', '1');
     538    if ($enabled !== '1') {
     539        return $settings;
     540    }
     541
     542    $interval = 0.0;
     543
     544    $snap_key = dfehc_scoped_tkey('dfehc_ri_snapshot');
     545    $snapshot = get_transient($snap_key);
     546    if (is_array($snapshot) && isset($snapshot['interval']) && is_numeric($snapshot['interval'])) {
     547        $interval = (float) $snapshot['interval'];
     548    }
     549
     550    if ($interval <= 0.0) {
     551        $interval = function_exists('dfehc_get_recommended_heartbeat_interval_async')
     552            ? (float) dfehc_get_recommended_heartbeat_interval_async()
     553            : (float) dfehc_calculate_recommended_interval_user_activity();
     554    }
     555
     556    $interval = (int) round($interval);
     557    $interval = min(max($interval, (int) DFEHC_MIN_INTERVAL), (int) DFEHC_MAX_INTERVAL);
     558
    376559    $settings['interval'] = $interval;
    377560    return $settings;
    378561}
    379 add_filter('heartbeat_settings', 'dfehc_override_heartbeat_interval');
     562add_filter('heartbeat_settings', 'dfehc_override_heartbeat_interval', 99);
     563
    380564
    381565function dfehc_get_server_health_status(float $load): string
  • dynamic-front-end-heartbeat-control/trunk/js/heartbeat.js

    r3412283 r3461136  
    11((wp) => {
    22  const intervals = {
    3     low:    [15, 30, 60, 120, 180, 240, 300],
    4     medium: [30, 60, 120, 180, 240, 300],
    5     high:   [60, 120, 180, 240, 300]
    6   };
    7 
    8   const vars = typeof window.dfehc_heartbeat_vars === 'object' ? window.dfehc_heartbeat_vars : {};
     3    low: [20, 40, 60, 90, 120, 180, 240],
     4    medium: [40, 60, 90, 120, 180, 240],
     5    high: [60, 90, 120, 180, 240]
     6  };
     7
     8  const vars = typeof window.dfehc_heartbeat_vars === 'object' && window.dfehc_heartbeat_vars ? window.dfehc_heartbeat_vars : {};
    99
    1010  const clampNumber = (v, lo, hi, d) => {
     
    1414  };
    1515
    16   const DEFAULT_MIN = 15;
    17   const DEFAULT_MAX = 300;
    18   const ABS_MIN = 1;
    19   const ABS_MAX = 3600;
     16  const DEFAULT_MIN = 20;
     17  const DEFAULT_MAX = 240;
     18  const ABS_MIN = 15;
     19  const ABS_MAX = 300;
    2020
    2121  let MIN = clampNumber(vars.min_interval, ABS_MIN, ABS_MAX, DEFAULT_MIN);
    2222  let MAX = clampNumber(vars.max_interval, MIN, ABS_MAX, DEFAULT_MAX);
    23   if (MIN > MAX) { const t = MIN; MIN = MAX; MAX = t; }
    24 
    25   const LOAD_CAP = Number.isFinite(vars.max_server_load) ? Number(vars.max_server_load) : 85;
     23  if (MIN > MAX) {
     24    const t = MIN;
     25    MIN = MAX;
     26    MAX = t;
     27  }
     28
     29  const LOAD_CAP = clampNumber(vars.max_server_load, 10, 100, 85);
    2630
    2731  const msOrAuto = (v, d) => {
     
    3034    return n <= 600 ? n * 1000 : n;
    3135  };
    32   const cacheTimeout = msOrAuto(vars.cache_duration, 5 * 60 * 1000);
    33   const cacheBypassRate = Math.min(1, Math.max(0, Number(vars.cache_bypass_rate) || 0.05));
     36
     37  const cacheTimeout = Math.max(15000, msOrAuto(vars.cache_duration, 10 * 60 * 1000));
     38  const cacheBypassRate = Math.min(1, Math.max(0, Number(vars.cache_bypass_rate) || 0.03));
    3439
    3540  const sanitizeKey = (s) => String(s || '').replace(/[^a-z0-9_.-]/gi, '_');
    3641  const siteKey = sanitizeKey(vars.site_key || location.host);
    37   const localCacheKey = `dfehc_heartbeat_server_load:${siteKey}:${vars.ver || '1'}`;
     42  const versionKey = sanitizeKey(vars.ver || '1');
     43  const localCacheKey = `dfehc_heartbeat_server_load:${siteKey}:${versionKey}`;
     44  const leaderKey = `${localCacheKey}:leader`;
     45
    3846  const memoryCache = Object.create(null);
    3947
    4048  const SUPPORTS_BC = typeof window.BroadcastChannel === 'function';
    4149  const bc = SUPPORTS_BC ? new BroadcastChannel('dfehc-heartbeat') : null;
    42   const TAB_ID = Math.random().toString(36).slice(2) + Date.now().toString(36);
     50
     51  const TAB_ID = (Math.random().toString(36).slice(2) + Date.now().toString(36));
    4352  let isLeader = false;
    4453  let lastLeaderBeat = 0;
    45   const LEADER_TTL = 5000;
    46   const leaderKey = `${localCacheKey}:leader`;
    47 
    48   function readLeader() {
     54
     55  const LEADER_TTL = Math.max(4000, clampNumber(vars.leader_ttl_ms, 2000, 20000, 8000));
     56  const LEADER_BEAT = Math.max(1200, clampNumber(vars.leader_beat_ms, 800, 10000, 3000));
     57
     58  const jitter = (ms) => Math.floor(Math.random() * ms);
     59  const safeJsonParse = (s) => {
    4960    try {
    50       const raw = localStorage.getItem(leaderKey);
    51       if (!raw) return null;
    52       const obj = JSON.parse(raw);
    53       if (!obj || typeof obj !== 'object') return null;
    54       const ts = Number(obj.ts);
    55       const tab = String(obj.tab || '');
    56       if (!Number.isFinite(ts) || !tab) return null;
    57       return { ts, tab };
     61      return JSON.parse(s);
    5862    } catch {
    5963      return null;
    6064    }
     65  };
     66
     67  function readLeader() {
     68    const raw = (() => { try { return localStorage.getItem(leaderKey); } catch { return null; } })();
     69    if (!raw) return null;
     70    const obj = safeJsonParse(raw);
     71    if (!obj || typeof obj !== 'object') return null;
     72    const ts = Number(obj.ts);
     73    const tab = String(obj.tab || '');
     74    if (!Number.isFinite(ts) || !tab) return null;
     75    return { ts, tab };
    6176  }
    6277
     
    88103      return true;
    89104    }
     105    isLeader = false;
    90106    return false;
    91107  }
     
    94110    if (!isLeader) return;
    95111    const now = Date.now();
    96     if (now - lastLeaderBeat > 2000) {
     112    if (now - lastLeaderBeat > LEADER_BEAT) {
    97113      writeLeader(now, TAB_ID);
    98114      lastLeaderBeat = now;
     
    108124  const getLocalCache = (key) => {
    109125    try {
    110       const raw = window.localStorage.getItem(key);
     126      const raw = localStorage.getItem(key);
    111127      if (!raw) return null;
    112       const parsed = JSON.parse(raw);
     128      const parsed = safeJsonParse(raw);
    113129      if (!parsed || typeof parsed !== 'object') return null;
    114130      const timestamp = Number(parsed.timestamp);
    115       const data = parsed.data;
    116131      if (!Number.isFinite(timestamp)) return null;
    117       return Date.now() - timestamp < cacheTimeout ? data : null;
     132      const age = Date.now() - timestamp;
     133      if (age < 0 || age > cacheTimeout) return null;
     134      return parsed.data;
    118135    } catch {
    119       try { window.localStorage.removeItem(key); } catch {}
     136      try { localStorage.removeItem(key); } catch {}
    120137      return null;
    121138    }
     
    124141  const setLocalCache = (key, data) => {
    125142    try {
    126       const jitterMs = Math.floor(Math.random() * 5000);
    127       window.localStorage.setItem(key, JSON.stringify({ timestamp: Date.now() - jitterMs, data }));
     143      localStorage.setItem(key, JSON.stringify({ timestamp: Date.now() - jitter(8000), data }));
    128144    } catch {}
    129145  };
    130146
    131   const nearestFrom = (value, list) =>
    132     list.reduce((best, v) => (Math.abs(v - value) < Math.abs(best - value) ? v : best), list[0]);
     147  const nearestFrom = (value, list) => {
     148    if (!Array.isArray(list) || list.length === 0) return value;
     149    let best = list[0];
     150    let bestD = Math.abs(best - value);
     151    for (let i = 1; i < list.length; i++) {
     152      const v = list[i];
     153      const d = Math.abs(v - value);
     154      if (d < bestD) {
     155        bestD = d;
     156        best = v;
     157      }
     158    }
     159    return best;
     160  };
    133161
    134162  const trafficLevel = (load) => (load <= 50 ? 'low' : load <= 75 ? 'medium' : 'high');
     
    137165    const bucket = Math.max(0, Math.min(100, Math.round(load)));
    138166    if (Object.prototype.hasOwnProperty.call(memoryCache, bucket)) return memoryCache[bucket];
     167
    139168    const level = trafficLevel(load);
    140     const opts = intervals[level];
     169    const opts = intervals[level] || intervals.low;
     170
    141171    const min = Math.max(MIN, opts[0]);
    142172    const max = Math.min(MAX, opts[opts.length - 1]);
     173
    143174    const ratio = Math.max(0, Math.min(1, load / LOAD_CAP));
    144175    const raw = min + (max - min) * ratio;
     176
    145177    const clamped = Math.min(Math.max(raw, MIN), MAX);
    146178    const snapped = nearestFrom(clamped, opts);
     179
    147180    memoryCache[bucket] = snapped;
    148181    return snapped;
     
    162195    if (Number.isFinite(Number(val))) {
    163196      const n = Number(val);
    164       const mode = (vars.server_payload || 'auto').toLowerCase();
     197      const mode = String(vars.server_payload || 'auto').toLowerCase();
    165198      if (mode === 'interval') return { interval: n, load: null };
    166199      if (mode === 'load') return { interval: null, load: n };
     
    184217    if (!hb || typeof hb.interval !== 'function') return;
    185218    if (typeof secs !== 'number' || !Number.isFinite(secs)) return;
    186     if (lastInterval !== null && Math.round(lastInterval) === Math.round(secs)) return;
    187     const preset = secs <= 20 ? 'fast' : secs <= 60 ? 'standard' : 'slow';
     219    const rounded = Math.round(secs);
     220    if (lastInterval !== null && Math.round(lastInterval) === rounded) return;
     221    const preset = secs <= 30 ? 'fast' : secs <= 90 ? 'standard' : 'slow';
    188222    try { hb.interval(preset); hb.interval(secs); } catch {}
    189223    lastInterval = secs;
     
    192226  const setHeartbeatInterval = (secs) => {
    193227    let s = secs;
     228    if (!Number.isFinite(s)) return;
     229    s = Math.max(MIN, Math.min(MAX, s));
    194230    if (s >= 60) {
    195       const jitter = Math.floor(Math.random() * 3) - 1;
    196       s = Math.max(MIN, Math.min(MAX, s + jitter));
     231      const j = (Math.floor(Math.random() * 5) - 2);
     232      s = Math.max(MIN, Math.min(MAX, s + j));
    197233    }
    198234    applyPresetThenExact(s);
    199235  };
    200236
    201   const debouncedSetInterval = debounce(setHeartbeatInterval, 150);
     237  const debouncedSetInterval = debounce(setHeartbeatInterval, 200);
    202238
    203239  let failureCount = 0;
    204240  let nextRetryAt = 0;
    205   const backoffMs = () => Math.min(120000, 2000 * Math.pow(2, Math.min(failureCount, 5)));
     241  const backoffMs = () => Math.min(180000, 3000 * Math.pow(2, Math.min(failureCount, 6)));
    206242
    207243  const fetchWithTimeout = (url, opts, ms) => {
     
    219255  let reqSeq = 0;
    220256
     257  const fallbackPayload = (rid) => {
     258    const base = Number(vars.fallback_interval) || 90;
     259    const j = Math.floor(Math.random() * 10) - 5;
     260    return { interval: Math.max(MIN, Math.min(MAX, base + j)), load: null, rid };
     261  };
     262
    221263  const fetchServerData = async (nonce, rid) => {
    222     if (Date.now() < nextRetryAt) {
    223       const base = Number(vars.fallback_interval) || 60;
    224       const jitter = Math.floor(Math.random() * 7) - 3;
    225       return { interval: Math.max(MIN, Math.min(MAX, base + jitter)), load: null, rid };
    226     }
     264    if (Date.now() < nextRetryAt) return fallbackPayload(rid);
    227265
    228266    if (Math.random() >= cacheBypassRate) {
     
    233271    const I_AM_LEADER = tryBecomeLeader();
    234272    if (!I_AM_LEADER) {
    235       const wait = Math.min(1500, Math.max(150, Math.floor(Math.random() * 600)));
    236       await new Promise(r => setTimeout(r, wait));
     273      const wait = Math.min(2000, Math.max(200, 200 + jitter(600)));
     274      await new Promise((r) => setTimeout(r, wait));
    237275      const cached = getLocalCache(localCacheKey);
    238276      if (cached !== null) return { ...coerceServerPayload(cached), rid };
    239277    }
    240278
    241     if (navigator.onLine === false) {
    242       const base = Number(vars.fallback_interval) || 60;
    243       const jitter = Math.floor(Math.random() * 7) - 3;
    244       return { interval: Math.max(MIN, Math.min(MAX, base + jitter)), load: null, rid };
    245     }
     279    if (navigator.onLine === false) return fallbackPayload(rid);
    246280
    247281    const url =
     
    252286      const body = new URLSearchParams({ action: 'dfehc_update_heartbeat_interval' });
    253287      if (nonce) body.append('nonce', nonce);
     288
    254289      const res = await fetchWithTimeout(url, {
    255290        method: 'POST',
     
    260295          'Cache-Control': 'no-store'
    261296        }
    262       }, 6000);
    263       if (!res.ok) throw new Error(String(res.status));
     297      }, 8000);
     298
     299      if (!res || !res.ok) throw new Error(res ? String(res.status) : 'no_response');
     300
    264301      const json = await res.json();
    265       if (!json || json.success !== true) throw new Error('bad payload');
     302      if (!json || json.success !== true) throw new Error('bad_payload');
     303
    266304      setLocalCache(localCacheKey, json.data);
    267305      if (bc) bc.postMessage({ t: 'payload', data: json.data });
     306
    268307      failureCount = 0;
    269308      nextRetryAt = 0;
     309
    270310      renewLeadership();
    271311      return { ...coerceServerPayload(json.data), rid };
     
    273313      failureCount += 1;
    274314      nextRetryAt = Date.now() + backoffMs();
    275       const base = Number(vars.fallback_interval) || 60;
    276       const jitter = Math.floor(Math.random() * 7) - 3;
    277315      relinquishLeadership();
    278       return { interval: Math.max(MIN, Math.min(MAX, base + jitter)), load: null, rid };
    279     }
     316      return fallbackPayload(rid);
     317    }
     318  };
     319
     320  const resolveIntervalFromPayload = (payload) => {
     321    const interval = payload && typeof payload.interval === 'number' ? payload.interval : null;
     322    const load = payload && typeof payload.load === 'number' ? payload.load : null;
     323
     324    const snapList = intervals[trafficLevel(typeof load === 'number' ? load : LOAD_CAP)] || intervals.low;
     325
     326    if (typeof interval === 'number' && Number.isFinite(interval)) {
     327      const clamped = Math.min(Math.max(interval, MIN), MAX);
     328      return nearestFrom(clamped, snapList);
     329    }
     330
     331    return calcRecommendedIntervalFromLoad(typeof load === 'number' ? load : 60);
    280332  };
    281333
    282334  const heartbeat = {
    283     update(interval) { debouncedSetInterval(interval); },
     335    update(interval) {
     336      debouncedSetInterval(interval);
     337    },
    284338    updateUI(interval) {
    285339      const sel = document.querySelector('#dfehc-heartbeat-interval');
     
    290344      const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
    291345      const et = conn && conn.effectiveType ? String(conn.effectiveType) : '';
     346
    292347      if ('deviceMemory' in navigator && navigator.deviceMemory < 2) return;
    293348      if ((conn && conn.saveData) || et.startsWith('2g') || et.startsWith('slow-2g')) return;
     349
    294350      const myRid = ++reqSeq;
    295351      const payload = await fetchServerData(nonce, myRid);
    296       if (payload.rid !== reqSeq) return;
    297       const interval = payload.interval;
    298       const load = payload.load;
    299       const snapList =
    300         intervals[trafficLevel(typeof load === 'number' ? load : LOAD_CAP)] || intervals.low;
    301       const finalInterval =
    302         typeof interval === 'number'
    303           ? nearestFrom(Math.min(Math.max(interval, MIN), MAX), snapList)
    304           : calcRecommendedIntervalFromLoad(typeof load === 'number' ? load : 60);
     352      if (!payload || payload.rid !== reqSeq) return;
     353
     354      const finalInterval = resolveIntervalFromPayload(payload);
    305355      this.updateUI(finalInterval);
    306356    }
     
    309359  if (bc) {
    310360    bc.onmessage = (e) => {
    311       const m = e.data || {};
     361      const m = (e && e.data) ? e.data : {};
    312362      if (m.t === 'leader') {
    313363        const tab = String(m.tab || '');
     
    315365      }
    316366      if (m.t === 'payload') {
    317         try { setLocalCache(localCacheKey, m.data); } catch {}
    318         Object.keys(memoryCache).forEach(k => delete memoryCache[k]);
    319         const coerced = coerceServerPayload(m.data);
    320         const interval = coerced.interval;
    321         const load = coerced.load;
    322         const snapList =
    323           intervals[trafficLevel(typeof load === 'number' ? load : LOAD_CAP)] || intervals.low;
    324         const finalInterval =
    325           typeof interval === 'number'
    326             ? nearestFrom(Math.min(Math.max(interval, MIN), MAX), snapList)
    327             : calcRecommendedIntervalFromLoad(typeof load === 'number' ? load : 60);
    328         debouncedSetInterval(finalInterval);
     367        setLocalCache(localCacheKey, m.data);
     368        Object.keys(memoryCache).forEach((k) => delete memoryCache[k]);
     369        debouncedSetInterval(resolveIntervalFromPayload(coerceServerPayload(m.data)));
    329370      }
    330371    };
     
    332373
    333374  window.addEventListener('storage', (e) => {
    334     if (e.key !== localCacheKey || !e.newValue) return;
    335     try {
    336       const v = JSON.parse(e.newValue);
    337       if (!v || typeof v !== 'object' || !('data' in v)) return;
    338       Object.keys(memoryCache).forEach(k => delete memoryCache[k]);
    339       const coerced = coerceServerPayload(v.data);
    340       const interval = coerced.interval;
    341       const load = coerced.load;
    342       const snapList =
    343         intervals[trafficLevel(typeof load === 'number' ? load : LOAD_CAP)] || intervals.low;
    344       const finalInterval =
    345         typeof interval === 'number'
    346           ? nearestFrom(Math.min(Math.max(interval, MIN), MAX), snapList)
    347           : calcRecommendedIntervalFromLoad(typeof load === 'number' ? load : 60);
    348       debouncedSetInterval(finalInterval);
    349     } catch {}
     375    if (!e || e.key !== localCacheKey || !e.newValue) return;
     376    const v = safeJsonParse(e.newValue);
     377    if (!v || typeof v !== 'object' || !('data' in v)) return;
     378    Object.keys(memoryCache).forEach((k) => delete memoryCache[k]);
     379    debouncedSetInterval(resolveIntervalFromPayload(coerceServerPayload(v.data)));
    350380  });
    351381
    352   let memoVersion = String(vars.ver || '1');
     382  let memoVersion = String(versionKey);
    353383  function maybeResetMemo() {
    354     const v = String(vars.ver || '1');
     384    const v = String(sanitizeKey(vars.ver || '1'));
    355385    if (v !== memoVersion) {
    356386      memoVersion = v;
    357       Object.keys(memoryCache).forEach(k => delete memoryCache[k]);
     387      Object.keys(memoryCache).forEach((k) => delete memoryCache[k]);
    358388    }
    359389  }
     
    370400  });
    371401  window.addEventListener('online', () => { nextRetryAt = 0; });
    372   window.addEventListener('pageshow', (e) => { if (e.persisted) nextRetryAt = 0; });
     402  window.addEventListener('pageshow', (e) => { if (e && e.persisted) nextRetryAt = 0; });
    373403  window.addEventListener('pagehide', cleanup, { once: true });
    374404  window.addEventListener('beforeunload', cleanup, { once: true });
     
    376406  document.addEventListener('DOMContentLoaded', () => {
    377407    maybeResetMemo();
    378     if ((vars.heartbeat_control_enabled || '') !== '1') return;
     408    if (String(vars.heartbeat_control_enabled || '') !== '1') return;
     409
    379410    const nonce = vars.nonce;
     411
    380412    if ('requestIdleCallback' in window) {
    381       window.requestIdleCallback(() => heartbeat.init(nonce));
     413      window.requestIdleCallback(() => heartbeat.init(nonce), { timeout: 500 });
    382414    } else {
    383       setTimeout(() => heartbeat.init(nonce), 100);
    384     }
     415      setTimeout(() => heartbeat.init(nonce), 150);
     416    }
     417
    385418    const sel = document.querySelector('#dfehc-heartbeat-interval');
    386419    if (sel) {
    387420      sel.addEventListener('change', function () {
    388         const val = parseInt(this.value, 10);
     421        const val = parseInt(String(this.value), 10);
    389422        if (!Number.isNaN(val)) heartbeat.update(Math.min(Math.max(val, MIN), MAX));
    390423      });
  • dynamic-front-end-heartbeat-control/trunk/js/heartbeat.min.js

    r3412283 r3461136  
    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]};const vars=typeof window.dfehc_heartbeat_vars==="object"?window.dfehc_heartbeat_vars:{};const clampNumber=(v,lo,hi,d)=>{const n=Number(v);if(!Number.isFinite(n))return d;return Math.min(hi,Math.max(lo,n))};const DEFAULT_MIN=15;const DEFAULT_MAX=300;const ABS_MIN=1;const ABS_MAX=3600;let MIN=clampNumber(vars.min_interval,ABS_MIN,ABS_MAX,DEFAULT_MIN);let MAX=clampNumber(vars.max_interval,MIN,ABS_MAX,DEFAULT_MAX);if(MIN>MAX){const t=MIN;MIN=MAX;MAX=t}const LOAD_CAP=Number.isFinite(vars.max_server_load)?Number(vars.max_server_load):85;const msOrAuto=(v,d)=>{const n=Number(v);if(!Number.isFinite(n))return d;return n<=600?n*1e3:n};const cacheTimeout=msOrAuto(vars.cache_duration,5*60*1e3);const cacheBypassRate=Math.min(1,Math.max(0,Number(vars.cache_bypass_rate)||.05));const sanitizeKey=s=>String(s||"").replace(/[^a-z0-9_.-]/gi,"_");const siteKey=sanitizeKey(vars.site_key||location.host);const localCacheKey=`dfehc_heartbeat_server_load:${siteKey}:${vars.ver||"1"}`;const memoryCache=Object.create(null);const SUPPORTS_BC=typeof window.BroadcastChannel==="function";const bc=SUPPORTS_BC?new BroadcastChannel("dfehc-heartbeat"):null;const TAB_ID=Math.random().toString(36).slice(2)+Date.now().toString(36);let isLeader=false;let lastLeaderBeat=0;const LEADER_TTL=5e3;const leaderKey=`${localCacheKey}:leader`;function readLeader(){try{const raw=localStorage.getItem(leaderKey);if(!raw)return null;const obj=JSON.parse(raw);if(!obj||typeof obj!=="object")return null;const ts=Number(obj.ts);const tab=String(obj.tab||"");if(!Number.isFinite(ts)||!tab)return null;return{ts,tab}}catch{return null}}function writeLeader(ts,tab){try{localStorage.setItem(leaderKey,JSON.stringify({ts,tab}))}catch{}}function clearLeader(tab){try{const cur=readLeader();if(!cur||cur.tab===tab)localStorage.removeItem(leaderKey)}catch{}}function tryBecomeLeader(){if(document.hidden)return false;const now=Date.now();const cur=readLeader();if(!cur||now-cur.ts>LEADER_TTL){writeLeader(now,TAB_ID);isLeader=true;lastLeaderBeat=now;if(bc)bc.postMessage({t:"leader",ts:now,tab:TAB_ID});return true}if(cur.tab===TAB_ID){isLeader=true;lastLeaderBeat=cur.ts;return true}return false}function renewLeadership(){if(!isLeader)return;const now=Date.now();if(now-lastLeaderBeat>2e3){writeLeader(now,TAB_ID);lastLeaderBeat=now}}function relinquishLeadership(){if(!isLeader)return;clearLeader(TAB_ID);isLeader=false}const getLocalCache=key=>{try{const raw=window.localStorage.getItem(key);if(!raw)return null;const parsed=JSON.parse(raw);if(!parsed||typeof parsed!=="object")return null;const timestamp=Number(parsed.timestamp);const data=parsed.data;if(!Number.isFinite(timestamp))return null;return Date.now()-timestamp<cacheTimeout?data:null}catch{try{window.localStorage.removeItem(key)}catch{}return null}};const setLocalCache=(key,data)=>{try{const jitterMs=Math.floor(Math.random()*5e3);window.localStorage.setItem(key,JSON.stringify({timestamp:Date.now()-jitterMs,data}))}catch{}};const nearestFrom=(value,list)=>list.reduce((best,v)=>Math.abs(v-value)<Math.abs(best-value)?v:best,list[0]);const trafficLevel=load=>load<=50?"low":load<=75?"medium":"high";const calcRecommendedIntervalFromLoad=load=>{const bucket=Math.max(0,Math.min(100,Math.round(load)));if(Object.prototype.hasOwnProperty.call(memoryCache,bucket))return memoryCache[bucket];const level=trafficLevel(load);const opts=intervals[level];const min=Math.max(MIN,opts[0]);const max=Math.min(MAX,opts[opts.length-1]);const ratio=Math.max(0,Math.min(1,load/LOAD_CAP));const raw=min+(max-min)*ratio;const clamped=Math.min(Math.max(raw,MIN),MAX);const snapped=nearestFrom(clamped,opts);memoryCache[bucket]=snapped;return snapped};const toNum=v=>{const n=Number(v);return Number.isFinite(n)?n:null};const coerceServerPayload=val=>{if(val&&typeof val==="object"){const interval=toNum(val.interval);const load=toNum(val.load);return{interval,load}}if(Number.isFinite(Number(val))){const n=Number(val);const mode=(vars.server_payload||"auto").toLowerCase();if(mode==="interval")return{interval:n,load:null};if(mode==="load")return{interval:null,load:n};if(n>=0&&n<=100)return{interval:null,load:n};if(n>100)return{interval:n,load:null}}return{interval:null,load:null}};const debounce=(fn,wait)=>{let t;return(...args)=>{clearTimeout(t);t=setTimeout(()=>fn(...args),wait)}};let lastInterval=null;const applyPresetThenExact=secs=>{const hb=wp&&wp.heartbeat;if(!hb||typeof hb.interval!=="function")return;if(typeof secs!=="number"||!Number.isFinite(secs))return;if(lastInterval!==null&&Math.round(lastInterval)===Math.round(secs))return;const preset=secs<=20?"fast":secs<=60?"standard":"slow";try{hb.interval(preset);hb.interval(secs)}catch{}lastInterval=secs};const setHeartbeatInterval=secs=>{let s=secs;if(s>=60){const jitter=Math.floor(Math.random()*3)-1;s=Math.max(MIN,Math.min(MAX,s+jitter))}applyPresetThenExact(s)};const debouncedSetInterval=debounce(setHeartbeatInterval,150);let failureCount=0;let nextRetryAt=0;const backoffMs=()=>Math.min(12e4,2e3*Math.pow(2,Math.min(failureCount,5)));const fetchWithTimeout=(url,opts,ms)=>{if("AbortController"in window){const ctrl=new AbortController;const t=setTimeout(()=>ctrl.abort(),ms);return fetch(url,{...opts,signal:ctrl.signal}).finally(()=>clearTimeout(t))}return Promise.race([fetch(url,opts),new Promise((_,rej)=>setTimeout(()=>rej(new Error("timeout")),ms))])};let reqSeq=0;const fetchServerData=async(nonce,rid)=>{if(Date.now()<nextRetryAt){const base=Number(vars.fallback_interval)||60;const jitter=Math.floor(Math.random()*7)-3;return{interval:Math.max(MIN,Math.min(MAX,base+jitter)),load:null,rid}}if(Math.random()>=cacheBypassRate){const cached=getLocalCache(localCacheKey);if(cached!==null)return{...coerceServerPayload(cached),rid}}const I_AM_LEADER=tryBecomeLeader();if(!I_AM_LEADER){const wait=Math.min(1500,Math.max(150,Math.floor(Math.random()*600)));await new Promise(r=>setTimeout(r,wait));const cached=getLocalCache(localCacheKey);if(cached!==null)return{...coerceServerPayload(cached),rid}}if(navigator.onLine===false){const base=Number(vars.fallback_interval)||60;const jitter=Math.floor(Math.random()*7)-3;return{interval:Math.max(MIN,Math.min(MAX,base+jitter)),load:null,rid}}const url=vars.ajax_url||typeof window.ajaxurl!=="undefined"?window.ajaxurl:`${location.origin.replace(/\/$/,"")}/wp-admin/admin-ajax.php`;try{const body=new URLSearchParams({action:"dfehc_update_heartbeat_interval"});if(nonce)body.append("nonce",nonce);const res=await fetchWithTimeout(url,{method:"POST",body,credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8","Cache-Control":"no-store"}},6e3);if(!res.ok)throw new Error(String(res.status));const json=await res.json();if(!json||json.success!==true)throw new Error("bad payload");setLocalCache(localCacheKey,json.data);if(bc)bc.postMessage({t:"payload",data:json.data});failureCount=0;nextRetryAt=0;renewLeadership();return{...coerceServerPayload(json.data),rid}}catch{failureCount+=1;nextRetryAt=Date.now()+backoffMs();const base=Number(vars.fallback_interval)||60;const jitter=Math.floor(Math.random()*7)-3;relinquishLeadership();return{interval:Math.max(MIN,Math.min(MAX,base+jitter)),load:null,rid}}};const heartbeat={update(interval){debouncedSetInterval(interval)},updateUI(interval){const sel=document.querySelector("#dfehc-heartbeat-interval");if(sel)sel.value=String(interval);debouncedSetInterval(interval)},async init(nonce){const conn=navigator.connection||navigator.mozConnection||navigator.webkitConnection;const et=conn&&conn.effectiveType?String(conn.effectiveType):"";if("deviceMemory"in navigator&&navigator.deviceMemory<2)return;if(conn&&conn.saveData||et.startsWith("2g")||et.startsWith("slow-2g"))return;const myRid=++reqSeq;const payload=await fetchServerData(nonce,myRid);if(payload.rid!==reqSeq)return;const interval=payload.interval;const load=payload.load;const snapList=intervals[trafficLevel(typeof load==="number"?load:LOAD_CAP)]||intervals.low;const finalInterval=typeof interval==="number"?nearestFrom(Math.min(Math.max(interval,MIN),MAX),snapList):calcRecommendedIntervalFromLoad(typeof load==="number"?load:60);this.updateUI(finalInterval)}};if(bc){bc.onmessage=e=>{const m=e.data||{};if(m.t==="leader"){const tab=String(m.tab||"");if(tab&&tab!==TAB_ID)isLeader=false}if(m.t==="payload"){try{setLocalCache(localCacheKey,m.data)}catch{}Object.keys(memoryCache).forEach(k=>delete memoryCache[k]);const coerced=coerceServerPayload(m.data);const interval=coerced.interval;const load=coerced.load;const snapList=intervals[trafficLevel(typeof load==="number"?load:LOAD_CAP)]||intervals.low;const finalInterval=typeof interval==="number"?nearestFrom(Math.min(Math.max(interval,MIN),MAX),snapList):calcRecommendedIntervalFromLoad(typeof load==="number"?load:60);debouncedSetInterval(finalInterval)}}}window.addEventListener("storage",e=>{if(e.key!==localCacheKey||!e.newValue)return;try{const v=JSON.parse(e.newValue);if(!v||typeof v!=="object"||!("data"in v))return;Object.keys(memoryCache).forEach(k=>delete memoryCache[k]);const coerced=coerceServerPayload(v.data);const interval=coerced.interval;const load=coerced.load;const snapList=intervals[trafficLevel(typeof load==="number"?load:LOAD_CAP)]||intervals.low;const finalInterval=typeof interval==="number"?nearestFrom(Math.min(Math.max(interval,MIN),MAX),snapList):calcRecommendedIntervalFromLoad(typeof load==="number"?load:60);debouncedSetInterval(finalInterval)}catch{}});let memoVersion=String(vars.ver||"1");function maybeResetMemo(){const v=String(vars.ver||"1");if(v!==memoVersion){memoVersion=v;Object.keys(memoryCache).forEach(k=>delete memoryCache[k])}}function cleanup(){relinquishLeadership();if(bc&&typeof bc.close==="function"){try{bc.close()}catch{}}}document.addEventListener("visibilitychange",()=>{if(!document.hidden)nextRetryAt=0});window.addEventListener("online",()=>{nextRetryAt=0});window.addEventListener("pageshow",e=>{if(e.persisted)nextRetryAt=0});window.addEventListener("pagehide",cleanup,{once:true});window.addEventListener("beforeunload",cleanup,{once:true});document.addEventListener("DOMContentLoaded",()=>{maybeResetMemo();if((vars.heartbeat_control_enabled||"")!=="1")return;const nonce=vars.nonce;if("requestIdleCallback"in window){window.requestIdleCallback(()=>heartbeat.init(nonce))}else{setTimeout(()=>heartbeat.init(nonce),100)}const sel=document.querySelector("#dfehc-heartbeat-interval");if(sel){sel.addEventListener("change",function(){const val=parseInt(this.value,10);if(!Number.isNaN(val))heartbeat.update(Math.min(Math.max(val,MIN),MAX))})}})})(window.wp||{});
     1((wp)=>{const intervals={low:[20,40,60,90,120,180,240],medium:[40,60,90,120,180,240],high:[60,90,120,180,240]};const vars=typeof window.dfehc_heartbeat_vars==="object"&&window.dfehc_heartbeat_vars?window.dfehc_heartbeat_vars:{};const clampNumber=(v,lo,hi,d)=>{const n=Number(v);if(!Number.isFinite(n))return d;return Math.min(hi,Math.max(lo,n))};const DEFAULT_MIN=20,DEFAULT_MAX=240,ABS_MIN=15,ABS_MAX=300;let MIN=clampNumber(vars.min_interval,ABS_MIN,ABS_MAX,DEFAULT_MIN);let MAX=clampNumber(vars.max_interval,MIN,ABS_MAX,DEFAULT_MAX);if(MIN>MAX){const t=MIN;MIN=MAX;MAX=t}const LOAD_CAP=clampNumber(vars.max_server_load,10,100,85);const msOrAuto=(v,d)=>{const n=Number(v);if(!Number.isFinite(n))return d;return n<=600?n*1000:n};const cacheTimeout=Math.max(15000,msOrAuto(vars.cache_duration,600000));const cacheBypassRate=Math.min(1,Math.max(0,Number(vars.cache_bypass_rate)||0.03));const sanitizeKey=s=>String(s||"").replace(/[^a-z0-9_.-]/gi,"_");const siteKey=sanitizeKey(vars.site_key||location.host);const versionKey=sanitizeKey(vars.ver||"1");const localCacheKey=`dfehc_heartbeat_server_load:${siteKey}:${versionKey}`;const leaderKey=`${localCacheKey}:leader`;const memoryCache=Object.create(null);const SUPPORTS_BC=typeof window.BroadcastChannel==="function";const bc=SUPPORTS_BC?new BroadcastChannel("dfehc-heartbeat"):null;const TAB_ID=Math.random().toString(36).slice(2)+Date.now().toString(36);let isLeader=false,lastLeaderBeat=0;const LEADER_TTL=Math.max(4000,clampNumber(vars.leader_ttl_ms,2000,20000,8000));const LEADER_BEAT=Math.max(1200,clampNumber(vars.leader_beat_ms,800,10000,3000));const jitter=ms=>Math.floor(Math.random()*ms);const safeJsonParse=s=>{try{return JSON.parse(s)}catch{return null}};function readLeader(){let raw=null;try{raw=localStorage.getItem(leaderKey)}catch{}if(!raw)return null;const obj=safeJsonParse(raw);if(!obj||typeof obj!=="object")return null;const ts=Number(obj.ts);const tab=String(obj.tab||"");if(!Number.isFinite(ts)||!tab)return null;return{ts,tab}}function writeLeader(ts,tab){try{localStorage.setItem(leaderKey,JSON.stringify({ts,tab}))}catch{}}function clearLeader(tab){try{const cur=readLeader();if(!cur||cur.tab===tab)localStorage.removeItem(leaderKey)}catch{}}function tryBecomeLeader(){if(document.hidden)return false;const now=Date.now();const cur=readLeader();if(!cur||now-cur.ts>LEADER_TTL){writeLeader(now,TAB_ID);isLeader=true;lastLeaderBeat=now;if(bc)bc.postMessage({t:"leader",ts:now,tab:TAB_ID});return true}if(cur.tab===TAB_ID){isLeader=true;lastLeaderBeat=cur.ts;return true}isLeader=false;return false}function renewLeadership(){if(!isLeader)return;const now=Date.now();if(now-lastLeaderBeat>LEADER_BEAT){writeLeader(now,TAB_ID);lastLeaderBeat=now}}function relinquishLeadership(){if(!isLeader)return;clearLeader(TAB_ID);isLeader=false}const getLocalCache=key=>{try{const raw=localStorage.getItem(key);if(!raw)return null;const parsed=safeJsonParse(raw);if(!parsed||typeof parsed!=="object")return null;const ts=Number(parsed.timestamp);if(!Number.isFinite(ts))return null;const age=Date.now()-ts;if(age<0||age>cacheTimeout)return null;return parsed.data}catch{try{localStorage.removeItem(key)}catch{}return null}};const setLocalCache=(key,data)=>{try{localStorage.setItem(key,JSON.stringify({timestamp:Date.now()-jitter(8000),data}))}catch{}};const nearestFrom=(value,list)=>{if(!Array.isArray(list)||!list.length)return value;let best=list[0],bestD=Math.abs(best-value);for(let i=1;i<list.length;i++){const v=list[i],d=Math.abs(v-value);if(d<bestD){bestD=d;best=v}}return best};const trafficLevel=load=>load<=50?"low":load<=75?"medium":"high";const calcRecommendedIntervalFromLoad=load=>{const bucket=Math.max(0,Math.min(100,Math.round(load)));if(Object.prototype.hasOwnProperty.call(memoryCache,bucket))return memoryCache[bucket];const opts=intervals[trafficLevel(load)]||intervals.low;const min=Math.max(MIN,opts[0]);const max=Math.min(MAX,opts[opts.length-1]);const ratio=Math.max(0,Math.min(1,load/LOAD_CAP));const raw=min+(max-min)*ratio;const clamped=Math.min(Math.max(raw,MIN),MAX);const snapped=nearestFrom(clamped,opts);memoryCache[bucket]=snapped;return snapped};const toNum=v=>{const n=Number(v);return Number.isFinite(n)?n:null};const coerceServerPayload=val=>{if(val&&typeof val==="object"){return{interval:toNum(val.interval),load:toNum(val.load)}}if(Number.isFinite(Number(val))){const n=Number(val);const mode=String(vars.server_payload||"auto").toLowerCase();if(mode==="interval")return{interval:n,load:null};if(mode==="load")return{interval:null,load:n};if(n>=0&&n<=100)return{interval:null,load:n};if(n>100)return{interval:n,load:null}}return{interval:null,load:null}};const debounce=(fn,wait)=>{let t;return(...args)=>{clearTimeout(t);t=setTimeout(()=>fn(...args),wait)}};let lastInterval=null;const applyPresetThenExact=secs=>{const hb=wp&&wp.heartbeat;if(!hb||typeof hb.interval!=="function")return;if(typeof secs!=="number"||!Number.isFinite(secs))return;const rounded=Math.round(secs);if(lastInterval!==null&&Math.round(lastInterval)===rounded)return;const preset=secs<=30?"fast":secs<=90?"standard":"slow";try{hb.interval(preset);hb.interval(secs)}catch{}lastInterval=secs};const setHeartbeatInterval=secs=>{if(!Number.isFinite(secs))return;let s=Math.max(MIN,Math.min(MAX,secs));if(s>=60){s=Math.max(MIN,Math.min(MAX,s+(Math.floor(Math.random()*5)-2)))}applyPresetThenExact(s)};const debouncedSetInterval=debounce(setHeartbeatInterval,200);let failureCount=0,nextRetryAt=0;const backoffMs=()=>Math.min(180000,3000*Math.pow(2,Math.min(failureCount,6)));const fetchWithTimeout=(url,opts,ms)=>{"AbortController"in window?(()=>{const ctrl=new AbortController(),t=setTimeout(()=>ctrl.abort(),ms);return fetch(url,{...opts,signal:ctrl.signal}).finally(()=>clearTimeout(t))})():Promise.race([fetch(url,opts),new Promise((_,rej)=>setTimeout(()=>rej(new Error("timeout")),ms))])};let reqSeq=0;const fallbackPayload=rid=>{const base=Number(vars.fallback_interval)||90;const j=Math.floor(Math.random()*10)-5;return{interval:Math.max(MIN,Math.min(MAX,base+j)),load:null,rid}};const fetchServerData=async(nonce,rid)=>{if(Date.now()<nextRetryAt)return fallbackPayload(rid);if(Math.random()>=cacheBypassRate){const cached=getLocalCache(localCacheKey);if(cached!==null)return{...coerceServerPayload(cached),rid}}const I_AM_LEADER=tryBecomeLeader();if(!I_AM_LEADER){await new Promise(r=>setTimeout(r,Math.min(2000,Math.max(200,200+jitter(600)))));const cached=getLocalCache(localCacheKey);if(cached!==null)return{...coerceServerPayload(cached),rid}}if(navigator.onLine===false)return fallbackPayload(rid);const url=vars.ajax_url||(typeof window.ajaxurl!=="undefined"?window.ajaxurl:`${location.origin.replace(/\/$/,"")}/wp-admin/admin-ajax.php`);try{const body=new URLSearchParams({action:"dfehc_update_heartbeat_interval"});if(nonce)body.append("nonce",nonce);const res=await fetchWithTimeout(url,{method:"POST",body,credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8","Cache-Control":"no-store"}},8000);if(!res||!res.ok)throw 0;const json=await res.json();if(!json||json.success!==true)throw 0;setLocalCache(localCacheKey,json.data);if(bc)bc.postMessage({t:"payload",data:json.data});failureCount=0;nextRetryAt=0;renewLeadership();return{...coerceServerPayload(json.data),rid}}catch{failureCount++;nextRetryAt=Date.now()+backoffMs();relinquishLeadership();return fallbackPayload(rid)}};const resolveIntervalFromPayload=payload=>{const interval=payload&&typeof payload.interval==="number"?payload.interval:null;const load=payload&&typeof payload.load==="number"?payload.load:null;const snapList=intervals[trafficLevel(typeof load==="number"?load:LOAD_CAP)]||intervals.low;if(typeof interval==="number"&&Number.isFinite(interval))return nearestFrom(Math.min(Math.max(interval,MIN),MAX),snapList);return calcRecommendedIntervalFromLoad(typeof load==="number"?load:60)};const heartbeat={update:i=>debouncedSetInterval(i),updateUI:i=>{const sel=document.querySelector("#dfehc-heartbeat-interval");if(sel)sel.value=String(i);debouncedSetInterval(i)},init:async nonce=>{const conn=navigator.connection||navigator.mozConnection||navigator.webkitConnection;const et=conn&&conn.effectiveType?String(conn.effectiveType):"";if("deviceMemory"in navigator&&navigator.deviceMemory<2)return;if((conn&&conn.saveData)||et.startsWith("2g")||et.startsWith("slow-2g"))return;const myRid=++reqSeq;const payload=await fetchServerData(nonce,myRid);if(!payload||payload.rid!==reqSeq)return;heartbeat.updateUI(resolveIntervalFromPayload(payload))}};if(bc){bc.onmessage=e=>{const m=e&&e.data?e.data:{};if(m.t==="leader"){const tab=String(m.tab||"");if(tab&&tab!==TAB_ID)isLeader=false}if(m.t==="payload"){setLocalCache(localCacheKey,m.data);Object.keys(memoryCache).forEach(k=>delete memoryCache[k]);debouncedSetInterval(resolveIntervalFromPayload(coerceServerPayload(m.data)))}}}window.addEventListener("storage",e=>{if(!e||e.key!==localCacheKey||!e.newValue)return;const v=safeJsonParse(e.newValue);if(!v||typeof v!=="object"||!("data"in v))return;Object.keys(memoryCache).forEach(k=>delete memoryCache[k]);debouncedSetInterval(resolveIntervalFromPayload(coerceServerPayload(v.data)))});let memoVersion=String(versionKey);function maybeResetMemo(){const v=String(sanitizeKey(vars.ver||"1"));if(v!==memoVersion){memoVersion=v;Object.keys(memoryCache).forEach(k=>delete memoryCache[k])}}function cleanup(){relinquishLeadership();if(bc&&typeof bc.close==="function"){try{bc.close()}catch{}}}document.addEventListener("visibilitychange",()=>{if(!document.hidden)nextRetryAt=0});window.addEventListener("online",()=>{nextRetryAt=0});window.addEventListener("pageshow",e=>{if(e&&e.persisted)nextRetryAt=0});window.addEventListener("pagehide",cleanup,{once:true});window.addEventListener("beforeunload",cleanup,{once:true});document.addEventListener("DOMContentLoaded",()=>{maybeResetMemo();if(String(vars.heartbeat_control_enabled||"")!=="1")return;const nonce=vars.nonce;if("requestIdleCallback"in window){window.requestIdleCallback(()=>heartbeat.init(nonce),{timeout:500})}else{setTimeout(()=>heartbeat.init(nonce),150)}const sel=document.querySelector("#dfehc-heartbeat-interval");if(sel){sel.addEventListener("change",function(){const val=parseInt(String(this.value),10);if(!Number.isNaN(val))heartbeat.update(Math.min(Math.max(val,MIN),MAX))})}})})(window.wp||{});
  • dynamic-front-end-heartbeat-control/trunk/readme.txt

    r3441926 r3461136  
    33Tested up to:      6.9
    44Requires PHP:      7.2
    5 Stable tag:        1.2.998
     5Stable tag:        1.2.998.1
    66License:           GPLv2 or later
    77License URI:       https://www.gnu.org/licenses/gpl-2.0.html
     
    2020✅ Full control when you want it — adjust priority settings, set custom intervals for the Backend and Editor, or disable Heartbeat entirely with a single click.
    2121✅ Highly advanced real-time decision making done on your behalf. Works with minimal server resources and completely prioritises user experience and website speed.
     22✅ Cleans up database clutter with 1 click.
    2223✅ Install, activate, done — performance improvements begin immediately with no configuration required in most cases.
    2324
     
    6162
    6263== Changelog ==
     64
     65= 1.2.998.1 =
     66
     67* Performance upgrade.
    6368
    6469= 1.2.998 =
  • dynamic-front-end-heartbeat-control/trunk/visitor/cookie-helper.php

    r3427163 r3461136  
    22declare(strict_types=1);
    33
    4 function dfehc_get_bot_pattern(): string
    5 {
    6     static $pattern = null;
    7     if ($pattern !== null) {
     4if (!function_exists('dfehc_get_bot_pattern')) {
     5    function dfehc_get_bot_pattern(): string
     6    {
     7        static $pattern = null;
     8        if (is_string($pattern) && $pattern !== '') {
     9            return $pattern;
     10        }
     11
     12        $sigs = (array) apply_filters('dfehc_bot_signatures', [
     13            'bot','crawl','crawler','slurp','spider','mediapartners','bingpreview',
     14            'yandex','duckduckbot','baiduspider','sogou','exabot',
     15            'facebot','facebookexternalhit','ia_archiver',
     16            'GPTBot','ChatGPT-User','OAI-SearchBot',
     17            'ClaudeBot','Claude-Web','anthropic-ai',
     18            'PerplexityBot','Perplexity-User',
     19            'Meta-ExternalAgent','DuckAssistBot',
     20            'Bytespider','Google-Extended','GoogleOther',
     21            'Applebot-Extended','Amazonbot','CCBot',
     22            'cohere-ai','ImagesiftBot','Diffbot','YouBot','GeminiBot',
     23            'MistralAI-User','xAI-GrokBot','AI2Bot',
     24        ]);
     25
     26        $clean = [];
     27        foreach ($sigs as $s) {
     28            if (!is_string($s)) {
     29                continue;
     30            }
     31            $s = trim($s);
     32            if ($s === '') {
     33                continue;
     34            }
     35            $clean[$s] = true;
     36        }
     37
     38        if (!$clean) {
     39            $pattern = '/$^/';
     40            return $pattern;
     41        }
     42
     43        $tokens = [];
     44        foreach (array_keys($clean) as $s) {
     45            $tokens[] = preg_quote($s, '/');
     46        }
     47
     48        $pattern = '/(' . implode('|', $tokens) . ')/i';
    849        return $pattern;
    950    }
    10     $sigs = (array) apply_filters('dfehc_bot_signatures', [
    11         'bot','crawl','crawler','slurp','spider','mediapartners','bingpreview',
    12         'yandex','duckduckbot','baiduspider','sogou','exabot',
    13         'facebot','facebookexternalhit','ia_archiver',
    14     ]);
    15     $tokens = array_map(
    16         static function (string $s): string {
    17             return '(?<![A-Za-z0-9])' . preg_quote($s, '/') . '(?![A-Za-z0-9])';
    18         },
    19         $sigs
    20     );
    21     return $pattern = '/(' . implode('|', $tokens) . ')/i';
    2251}
    2352
     
    3362    {
    3463        static $t = '';
    35         if ($t !== '') return $t;
    36         $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown'));
     64        if ($t !== '') {
     65            return $t;
     66        }
     67
     68        $url = '';
     69        if (defined('WP_HOME') && is_string(WP_HOME) && WP_HOME !== '') {
     70            $url = WP_HOME;
     71        } elseif (function_exists('home_url')) {
     72            $url = (string) home_url();
     73        }
     74
     75        $host = '';
     76        if ($url !== '' && function_exists('wp_parse_url')) {
     77            $parts = wp_parse_url($url);
     78            if (is_array($parts) && isset($parts['host']) && is_string($parts['host'])) {
     79                $host = $parts['host'];
     80            }
     81        }
     82
     83        if ($host === '') {
     84            $host = @php_uname('n') ?: 'unknown';
     85        }
     86
    3787        $salt = defined('DB_NAME') ? (string) DB_NAME : '';
    38         return $t = substr(md5((string) $host . $salt), 0, 10);
     88        $t = substr(md5($host . $salt), 0, 10);
     89        return $t;
    3990    }
    4091}
     
    53104            return (bool) filter_var($ip, FILTER_VALIDATE_IP) && $ip === $cidr;
    54105        }
    55         [$subnet, $mask] = explode('/', $cidr, 2);
    56         $mask = (int) $mask;
    57 
    58         $ip_bin  = @inet_pton($ip);
     106
     107        $parts = explode('/', $cidr, 2);
     108        if (count($parts) !== 2) {
     109            return false;
     110        }
     111
     112        $subnet = (string) $parts[0];
     113        $mask = (int) $parts[1];
     114
     115        $ip_bin = @inet_pton($ip);
    59116        $sub_bin = @inet_pton($subnet);
    60117        if ($ip_bin === false || $sub_bin === false) {
     
    65122        }
    66123
    67         $len      = strlen($ip_bin);
     124        $len = strlen($ip_bin);
    68125        $max_bits = $len * 8;
    69126        if ($mask < 0 || $mask > $max_bits) {
     
    72129
    73130        $bytes = intdiv($mask, 8);
    74         $bits  = $mask % 8;
     131        $bits = $mask % 8;
    75132
    76133        if ($bytes && substr($ip_bin, 0, $bytes) !== substr($sub_bin, 0, $bytes)) {
     
    80137            return true;
    81138        }
    82         $ip_byte   = ord($ip_bin[$bytes]);
    83         $sub_byte  = ord($sub_bin[$bytes]);
     139
     140        $ip_byte = ord($ip_bin[$bytes]);
     141        $sub_byte = ord($sub_bin[$bytes]);
    84142        $mask_byte = (0xFF << (8 - $bits)) & 0xFF;
     143
    85144        return ($ip_byte & $mask_byte) === ($sub_byte & $mask_byte);
    86145    }
     
    90149    function dfehc_select_client_ip_from_xff(string $xff, array $trustedCidrs): ?string
    91150    {
    92         $candidates   = array_filter(array_map('trim', explode(',', $xff)), 'strlen');
     151        $parts = explode(',', $xff);
     152        $candidates = [];
     153        foreach ($parts as $p) {
     154            $p = trim($p);
     155            if ($p !== '') {
     156                $candidates[] = $p;
     157            }
     158        }
     159
    93160        $ipNonTrusted = null;
    94161
    95162        foreach ($candidates as $ip) {
    96             $ip = preg_replace('/%[0-9A-Za-z.\-]+$/', '', $ip);
    97             $ip = trim((string) $ip);
     163            $ip = (string) preg_replace('/%[0-9A-Za-z.\-]+$/', '', $ip);
     164            $ip = trim($ip);
     165
    98166            if ($ip === '' || !filter_var($ip, FILTER_VALIDATE_IP)) {
    99167                continue;
     
    124192        }
    125193
    126         $last = trim((string) end($candidates));
    127         return ($last !== '' && filter_var($last, FILTER_VALIDATE_IP)) ? $last : null;
     194        if ($candidates) {
     195            $last = trim((string) end($candidates));
     196            if ($last !== '' && filter_var($last, FILTER_VALIDATE_IP)) {
     197                return $last;
     198            }
     199        }
     200
     201        return null;
    128202    }
    129203}
     
    132206    function dfehc_client_ip(): string
    133207    {
     208        static $memo = null;
     209        if (is_string($memo) && $memo !== '') {
     210            return $memo;
     211        }
     212
    134213        $remote_raw = isset($_SERVER['REMOTE_ADDR']) ? (string) wp_unslash($_SERVER['REMOTE_ADDR']) : '';
    135214        $remote_raw = trim($remote_raw);
    136         $remote     = ($remote_raw !== '' && filter_var($remote_raw, FILTER_VALIDATE_IP)) ? $remote_raw : '';
     215        $remote = ($remote_raw !== '' && filter_var($remote_raw, FILTER_VALIDATE_IP)) ? $remote_raw : '';
    137216
    138217        $trustedCidrs = (array) apply_filters('dfehc_trusted_proxies', []);
    139         $trustedCidrs = array_values(array_filter(array_map(
    140             static function ($v): string { return is_string($v) ? trim($v) : ''; },
    141             $trustedCidrs
    142         ), 'strlen'));
     218        $trusted = [];
     219        foreach ($trustedCidrs as $v) {
     220            if (!is_string($v)) {
     221                continue;
     222            }
     223            $v = trim($v);
     224            if ($v !== '') {
     225                $trusted[] = $v;
     226            }
     227        }
    143228
    144229        $isTrustedRemote = false;
    145         if ($remote !== '' && $trustedCidrs) {
    146             foreach ($trustedCidrs as $cidr) {
     230        if ($remote !== '' && $trusted) {
     231            foreach ($trusted as $cidr) {
    147232                if (dfehc_ip_in_cidr($remote, $cidr)) {
    148233                    $isTrustedRemote = true;
     
    155240
    156241        if ($isTrustedRemote) {
    157             $headers = (array) apply_filters('dfehc_proxy_ip_headers', ['HTTP_X_FORWARDED_FOR', 'HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP']);
    158             $headers = array_values(array_filter(array_map(
    159                 static function ($h): string { return is_string($h) ? trim($h) : ''; },
    160                 $headers
    161             ), 'strlen'));
    162 
     242            $headers = (array) apply_filters('dfehc_proxy_ip_headers', [
     243                'HTTP_CF_CONNECTING_IP',
     244                'HTTP_X_FORWARDED_FOR',
     245                'HTTP_X_REAL_IP',
     246            ]);
     247
     248            $hdrs = [];
    163249            foreach ($headers as $h) {
     250                if (!is_string($h)) {
     251                    continue;
     252                }
     253                $h = trim($h);
     254                if ($h !== '') {
     255                    $hdrs[] = $h;
     256                }
     257            }
     258
     259            foreach ($hdrs as $h) {
    164260                if (empty($_SERVER[$h])) {
    165261                    continue;
    166262                }
     263
    167264                $raw = trim((string) wp_unslash($_SERVER[$h]));
    168265                if ($raw === '') {
     
    171268
    172269                if ($h === 'HTTP_X_FORWARDED_FOR') {
    173                     $picked = dfehc_select_client_ip_from_xff($raw, $trustedCidrs);
     270                    $picked = dfehc_select_client_ip_from_xff($raw, $trusted);
    174271                    if ($picked !== null) {
    175272                        $cand = $picked;
     
    179276                }
    180277
    181                 $raw = preg_replace('/%[0-9A-Za-z.\-]+$/', '', $raw);
     278                $raw = (string) preg_replace('/%[0-9A-Za-z.\-]+$/', '', $raw);
     279                $raw = trim($raw);
     280
    182281                if ($raw !== '' && filter_var($raw, FILTER_VALIDATE_IP)) {
    183282                    $cand = $raw;
     
    188287
    189288        if ($cand === null) {
    190             $cand = $remote !== '' ? $remote : '0.0.0.0';
    191         }
    192 
    193         return (string) apply_filters('dfehc_client_ip', $cand);
    194     }
    195 }
    196 
    197 if (!function_exists('dfehc_set_transient_noautoload')) {
    198     function dfehc_set_transient_noautoload(string $key, $value, int $expiration): void
    199     {
    200         $group = defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc';
    201 
    202         if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
     289            $cand = ($remote !== '' ? $remote : '0.0.0.0');
     290        }
     291
     292        $memo = (string) apply_filters('dfehc_client_ip', $cand);
     293        if ($memo === '') {
     294            $memo = '0.0.0.0';
     295        }
     296        return $memo;
     297    }
     298}
     299
     300if (!function_exists('dfehc_is_request_bot')) {
     301    function dfehc_is_request_bot(): bool
     302    {
     303        static $cached = null;
     304        static $server_ctx = null;
     305
     306        if ($server_ctx === null) {
     307            $server_ctx = [
     308                'REMOTE_ADDR'     => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field((string) wp_unslash($_SERVER['REMOTE_ADDR'])) : '',
     309                'HTTP_USER_AGENT' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field((string) wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '',
     310                'HTTP_ACCEPT'     => isset($_SERVER['HTTP_ACCEPT']) ? sanitize_text_field((string) wp_unslash($_SERVER['HTTP_ACCEPT'])) : '',
     311                'HTTP_SEC_CH_UA'  => isset($_SERVER['HTTP_SEC_CH_UA']) ? sanitize_text_field((string) wp_unslash($_SERVER['HTTP_SEC_CH_UA'])) : '',
     312                'REQUEST_URI'     => isset($_SERVER['REQUEST_URI']) ? sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_URI'])) : '',
     313                'REQUEST_METHOD'  => isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_METHOD'])) : '',
     314            ];
     315        }
     316
     317        if ($cached !== null) {
     318            return (bool) apply_filters('dfehc_is_request_bot', (bool) $cached, $server_ctx);
     319        }
     320
     321        $ua = (string) ($server_ctx['HTTP_USER_AGENT'] ?? '');
     322
     323        $treatEmptyUaAsBot = (bool) apply_filters('dfehc_empty_ua_is_bot', true, $server_ctx);
     324        if ($ua === '') {
     325            $cached = $treatEmptyUaAsBot;
     326            return (bool) apply_filters('dfehc_is_request_bot', (bool) $cached, $server_ctx);
     327        }
     328
     329        if (preg_match(dfehc_get_bot_pattern(), $ua)) {
     330            $cached = true;
     331            return (bool) apply_filters('dfehc_is_request_bot', true, $server_ctx);
     332        }
     333
     334        $accept = (string) ($server_ctx['HTTP_ACCEPT'] ?? '');
     335        $sec_ch = (string) ($server_ctx['HTTP_SEC_CH_UA'] ?? '');
     336
     337        $strictHeuristic = (bool) apply_filters('dfehc_bot_accept_heuristic_enabled', true, $server_ctx);
     338        if ($strictHeuristic && ($accept === '' || stripos($accept, 'text/html') === false) && $sec_ch === '') {
     339            $cached = true;
     340            return (bool) apply_filters('dfehc_is_request_bot', true, $server_ctx);
     341        }
     342
     343        $ip = dfehc_client_ip();
     344        $group = (string) apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc');
     345
     346        $badBase = (string) apply_filters('dfehc_bad_ip_key_base', 'dfehc_bad_ip_');
     347        $badKey = dfehc_scoped_key($badBase) . hash('sha256', $ip ?: 'none');
     348        $badKeyLocal = $badKey . '_t';
     349
     350        if ($ip && function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_get')) {
     351            if (wp_cache_get($badKey, $group)) {
     352                $cached = true;
     353                return (bool) apply_filters('dfehc_is_request_bot', true, $server_ctx);
     354            }
     355        }
     356
     357        if ($ip && get_transient($badKeyLocal)) {
     358            $cached = true;
     359            return (bool) apply_filters('dfehc_is_request_bot', true, $server_ctx);
     360        }
     361
     362        $cached = false;
     363        return (bool) apply_filters('dfehc_is_request_bot', false, $server_ctx);
     364    }
     365}
     366
     367if (!function_exists('dfehc_should_set_cookie')) {
     368    function dfehc_should_set_cookie(): bool
     369    {
     370        if (defined('DOING_CRON') && DOING_CRON) {
     371            return false;
     372        }
     373        if (defined('WP_CLI') && WP_CLI) {
     374            return false;
     375        }
     376        if (function_exists('is_admin') && is_admin()) {
     377            return false;
     378        }
     379        if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
     380            return false;
     381        }
     382        if (function_exists('wp_is_json_request') && wp_is_json_request()) {
     383            return false;
     384        }
     385
     386        if (function_exists('rest_get_url_prefix')) {
     387            $p = rest_get_url_prefix();
     388            $uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '';
     389            $uri = (string) preg_replace('#\?.*$#', '', $uri);
     390            $uri = ltrim($uri, '/');
     391
     392            $p = is_string($p) ? trim($p, " \t\n\r\0\x0B/") : '';
     393            if ($p && strpos($uri, $p . '/') === 0) {
     394                return false;
     395            }
     396        }
     397
     398        if (function_exists('is_feed') && is_feed()) {
     399            return false;
     400        }
     401        if (function_exists('is_robots') && is_robots()) {
     402            return false;
     403        }
     404
     405        $method = isset($_SERVER['REQUEST_METHOD']) ? (string) wp_unslash($_SERVER['REQUEST_METHOD']) : '';
     406        $method = strtoupper(trim($method));
     407        if ($method !== '' && $method !== 'GET') {
     408            return false;
     409        }
     410
     411        return true;
     412    }
     413}
     414
     415if (!function_exists('dfehc_cache_increment')) {
     416    function dfehc_cache_increment(string $key, string $group, int $ttl): int
     417    {
     418        static $memo = [];
     419
     420        $ttl = max(10, (int) $ttl);
     421
     422        if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_incr')) {
    203423            if (function_exists('wp_cache_add')) {
    204                 if (!wp_cache_add($key, $value, $group, $expiration)) {
    205                     wp_cache_set($key, $value, $group, $expiration);
    206                 }
     424                wp_cache_add($key, 0, $group, $ttl);
     425            }
     426            $value = wp_cache_incr($key, 1, $group);
     427            if ($value === false) {
     428                if (function_exists('wp_cache_set')) {
     429                    wp_cache_set($key, 1, $group, $ttl);
     430                }
     431                return 1;
     432            }
     433            return (int) $value;
     434        }
     435
     436        if (isset($memo[$key]) && is_numeric($memo[$key])) {
     437            $memo[$key] = (int) $memo[$key] + 1;
     438            if (function_exists('dfehc_set_transient_noautoload')) {
     439                dfehc_set_transient_noautoload($key, (int) $memo[$key], $ttl);
    207440            } else {
    208                 wp_cache_set($key, $value, $group, $expiration);
    209             }
     441                set_transient($key, (int) $memo[$key], $ttl);
     442            }
     443            return (int) $memo[$key];
     444        }
     445
     446        $cur = get_transient($key);
     447        $cur = is_numeric($cur) ? (int) $cur : 0;
     448        $cur++;
     449        $memo[$key] = $cur;
     450
     451        if (function_exists('dfehc_set_transient_noautoload')) {
     452            dfehc_set_transient_noautoload($key, $cur, $ttl);
     453        } else {
     454            set_transient($key, $cur, $ttl);
     455        }
     456
     457        return (int) $cur;
     458    }
     459}
     460
     461if (!function_exists('dfehc_should_count_visitor_now')) {
     462    function dfehc_should_count_visitor_now(bool $shouldRefresh, bool $hadCookie, array $ctx): bool
     463    {
     464        $mode = (string) apply_filters('dfehc_visitor_count_mode', 'refresh_only', $ctx);
     465        $mode = strtolower(trim($mode));
     466
     467        if ($mode === 'always') {
     468            return true;
     469        }
     470
     471        if ($mode === 'refresh_only') {
     472            return $shouldRefresh || !$hadCookie;
     473        }
     474
     475        if ($mode === 'sample') {
     476            $rate = (float) apply_filters('dfehc_visitor_count_sample_rate', 0.05, $ctx);
     477            if (!is_finite($rate) || $rate <= 0.0) {
     478                return false;
     479            }
     480            if ($rate >= 1.0) {
     481                return true;
     482            }
     483            $roll = function_exists('wp_rand') ? (int) wp_rand(0, 1000000) : (int) (time() % 1000001);
     484            return ((float) $roll / 1000000.0) < $rate;
     485        }
     486
     487        return $shouldRefresh || !$hadCookie;
     488    }
     489}
     490
     491if (!function_exists('dfehc_set_user_cookie')) {
     492    function dfehc_set_user_cookie(): void
     493    {
     494        if (!dfehc_should_set_cookie()) {
    210495            return;
    211496        }
    212 
    213         set_transient($key, $value, $expiration);
    214 
    215         global $wpdb;
    216         if (!isset($wpdb) || !is_object($wpdb) || !isset($wpdb->options)) {
     497        if (dfehc_is_request_bot()) {
    217498            return;
    218499        }
    219         $opt_key    = "_transient_$key";
    220         $opt_key_to = "_transient_timeout_$key";
    221         $wpdb->suppress_errors(true);
    222         try {
    223             $autoload = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key));
    224             if ($autoload === 'yes') {
    225                 $wpdb->update(
    226                     $wpdb->options,
    227                     ['autoload' => 'no'],
    228                     ['option_name' => $opt_key, 'autoload' => 'yes'],
    229                     ['%s'],
    230                     ['%s', '%s']
    231                 );
    232             }
    233             $autoload_to = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_to));
    234             if ($autoload_to === 'yes') {
    235                 $wpdb->update(
    236                     $wpdb->options,
    237                     ['autoload' => 'no'],
    238                     ['option_name' => $opt_key_to, 'autoload' => 'yes'],
    239                     ['%s'],
    240                     ['%s', '%s']
    241                 );
    242             }
    243         } finally {
    244             $wpdb->suppress_errors(false);
    245         }
    246     }
    247 }
    248 
    249 function dfehc_trusted_proxy_request(): bool
    250 {
    251     $remote_raw = isset($_SERVER['REMOTE_ADDR']) ? (string) wp_unslash($_SERVER['REMOTE_ADDR']) : '';
    252     $remote_raw = trim($remote_raw);
    253     if ($remote_raw === '' || !filter_var($remote_raw, FILTER_VALIDATE_IP)) return false;
    254 
    255     $trusted = (array) apply_filters('dfehc_trusted_proxies', []);
    256     foreach ($trusted as $cidr) {
    257         $cidr = is_string($cidr) ? trim($cidr) : '';
    258         if ($cidr !== '' && dfehc_ip_in_cidr($remote_raw, $cidr)) {
    259             return true;
    260         }
    261     }
    262     return false;
    263 }
    264 
    265 function dfehc_is_request_bot(): bool
    266 {
    267     static $cached = null;
    268 
    269     $server_ctx = [
    270         'REMOTE_ADDR'     => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field((string) wp_unslash($_SERVER['REMOTE_ADDR'])) : '',
    271         'HTTP_USER_AGENT' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field((string) wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '',
    272         'HTTP_ACCEPT'     => isset($_SERVER['HTTP_ACCEPT']) ? sanitize_text_field((string) wp_unslash($_SERVER['HTTP_ACCEPT'])) : '',
    273         'HTTP_SEC_CH_UA'  => isset($_SERVER['HTTP_SEC_CH_UA']) ? sanitize_text_field((string) wp_unslash($_SERVER['HTTP_SEC_CH_UA'])) : '',
    274         'REQUEST_URI'     => isset($_SERVER['REQUEST_URI']) ? sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_URI'])) : '',
    275         'REQUEST_METHOD'  => isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_METHOD'])) : '',
    276     ];
    277 
    278     if ($cached !== null) {
    279         return (bool) apply_filters('dfehc_is_request_bot', $cached, $server_ctx);
    280     }
    281 
    282     $ua = $server_ctx['HTTP_USER_AGENT'];
    283     if ($ua === '' || preg_match(dfehc_get_bot_pattern(), $ua)) {
    284         $cached = true;
    285         return (bool) apply_filters('dfehc_is_request_bot', $cached, $server_ctx);
    286     }
    287 
    288     $accept = $server_ctx['HTTP_ACCEPT'];
    289     $sec_ch = $server_ctx['HTTP_SEC_CH_UA'];
    290 
    291     if (($accept === '' || stripos($accept, 'text/html') === false) && $sec_ch === '') {
    292         $cached = true;
    293         return (bool) apply_filters('dfehc_is_request_bot', $cached, $server_ctx);
    294     }
    295 
    296     $ip          = dfehc_client_ip();
    297     $ipKeyScoped = dfehc_scoped_key('dfehc_bad_ip_') . md5($ip ?: 'none');
    298     $group       = apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc');
    299 
    300     if ($ip && function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_get')) {
    301         if (wp_cache_get($ipKeyScoped, $group)) {
    302             $cached = true;
    303             return (bool) apply_filters('dfehc_is_request_bot', $cached, $server_ctx);
    304         }
    305     } else {
    306         $tkey = $ipKeyScoped . '_t';
    307         if ($ip && get_transient($tkey)) {
    308             $cached = true;
    309             return (bool) apply_filters('dfehc_is_request_bot', $cached, $server_ctx);
    310         }
    311     }
    312 
    313     $cached = false;
    314     return (bool) apply_filters('dfehc_is_request_bot', $cached, $server_ctx);
    315 }
    316 
    317 function dfehc_should_set_cookie(): bool
    318 {
    319     if (defined('DOING_CRON') && DOING_CRON) return false;
    320     if (defined('WP_CLI') && WP_CLI) return false;
    321     if (function_exists('is_admin') && is_admin()) return false;
    322     if (function_exists('wp_doing_ajax') && wp_doing_ajax()) return false;
    323     if (function_exists('wp_is_json_request') && wp_is_json_request()) return false;
    324 
    325     if (function_exists('rest_get_url_prefix')) {
    326         $p   = rest_get_url_prefix();
    327         $uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '';
    328         $uri = (string) preg_replace('#\?.*$#', '', $uri);
    329         $uri = ltrim($uri, '/');
    330         $p   = is_string($p) ? trim($p, " \t\n\r\0\x0B/") : '';
    331         if ($p && strpos($uri, $p . '/') === 0) return false;
    332     }
    333 
    334     if (function_exists('is_feed') && is_feed()) return false;
    335     if (function_exists('is_robots') && is_robots()) return false;
    336 
    337     $method = isset($_SERVER['REQUEST_METHOD']) ? (string) wp_unslash($_SERVER['REQUEST_METHOD']) : '';
    338     $method = strtoupper(trim($method));
    339     if ($method !== '' && $method !== 'GET') return false;
    340 
    341     return true;
    342 }
    343 
    344 function dfehc_set_user_cookie(): void
    345 {
    346     if (!dfehc_should_set_cookie()) {
    347         return;
    348     }
    349     if (dfehc_is_request_bot()) {
    350         return;
    351     }
    352 
    353     $ip    = dfehc_client_ip();
    354     $group = apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc');
    355     $maxRPM   = (int) apply_filters('dfehc_max_rpm', 120);
    356     $badIpTtl = (int) apply_filters('dfehc_bad_ip_ttl', HOUR_IN_SECONDS);
    357     $scopedVisitorKey = dfehc_scoped_key('dfehc_total_visitors');
    358 
    359     if ($ip && function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_incr')) {
    360         $rpmKey = dfehc_scoped_key('dfehc_iprpm_') . md5($ip);
    361         $rpmTtl = 60;
    362 
    363         if (function_exists('wp_rand')) {
    364             $rpmTtl += (int) wp_rand(0, 5);
    365         }
    366 
    367         if (false === wp_cache_add($rpmKey, 0, $group, $rpmTtl)) {
    368             wp_cache_set($rpmKey, (int) (wp_cache_get($rpmKey, $group) ?: 0), $group, $rpmTtl);
    369         }
    370         $rpm = wp_cache_incr($rpmKey, 1, $group);
    371         if ($rpm === false) {
    372             wp_cache_set($rpmKey, 1, $group, $rpmTtl);
    373             $rpm = 1;
    374         } else {
    375             wp_cache_set($rpmKey, (int) $rpm, $group, $rpmTtl);
    376         }
    377 
    378         if ($rpm > $maxRPM) {
    379             $badKey = dfehc_scoped_key('dfehc_bad_ip_') . md5($ip);
    380             wp_cache_set($badKey, 1, $group, $badIpTtl);
    381             $tkey = $badKey . '_t';
    382             dfehc_set_transient_noautoload($tkey, 1, $badIpTtl);
    383             wp_cache_delete($rpmKey, $group);
    384             return;
    385         }
    386     }
    387 
    388     $name     = (string) apply_filters('dfehc_cookie_name', 'dfehc_user');
    389     $lifetime = (int) apply_filters('dfehc_cookie_lifetime', 400);
    390     $path     = (string) apply_filters('dfehc_cookie_path', defined('COOKIEPATH') ? COOKIEPATH : '/');
    391 
    392     $home = function_exists('home_url') ? (string) home_url('/') : '';
    393     $host = '';
    394     if ($home !== '') {
    395         $parsed = function_exists('wp_parse_url') ? (array) wp_parse_url($home) : [];
    396         $host = !empty($parsed['host']) ? (string) $parsed['host'] : '';
    397     }
    398 
    399     $isIpHost = $host && (filter_var($host, FILTER_VALIDATE_IP) !== false);
    400     $domainDefault = $isIpHost ? '' : ($host ?: (defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : ''));
    401     $domain   = (string) apply_filters('dfehc_cookie_domain', $domainDefault);
    402 
    403     $sameSite = (string) apply_filters('dfehc_cookie_samesite', 'Lax');
    404     $map = ['lax' => 'Lax', 'strict' => 'Strict', 'none' => 'None'];
    405     $sameSiteUpper = $map[strtolower($sameSite)] ?? 'Lax';
    406 
    407     $secure   = (function_exists('is_ssl') && is_ssl()) || $sameSiteUpper === 'None';
    408     if ($sameSiteUpper === 'None' && !$secure) {
    409         $sameSiteUpper = 'Lax';
    410     }
    411     $httpOnly = true;
    412 
    413     $nowTs = isset($_SERVER['REQUEST_TIME']) ? (int) wp_unslash($_SERVER['REQUEST_TIME']) : time();
    414 
    415     $existing = isset($_COOKIE[$name]) ? (string) wp_unslash($_COOKIE[$name]) : '';
    416     $existing = sanitize_text_field($existing);
    417 
    418     $val = $existing;
    419 
    420     if (!preg_match('/^[A-Fa-f0-9]{32,64}$/', (string) $val)) {
    421         if (function_exists('random_bytes')) {
    422             try {
    423                 $val = bin2hex(random_bytes(16));
    424             } catch (\Throwable $e) {
    425                 $val = bin2hex(wp_hash((string) wp_rand() . '|' . $nowTs . '|' . dfehc_host_token(), 'nonce'));
    426                 $val = substr(preg_replace('/[^A-Fa-f0-9]/', '', (string) $val), 0, 32);
    427             }
    428         } else {
    429             $val = bin2hex(wp_hash((string) wp_rand() . '|' . $nowTs . '|' . dfehc_host_token(), 'nonce'));
    430             $val = substr(preg_replace('/[^A-Fa-f0-9]/', '', (string) $val), 0, 32);
    431         }
    432     }
    433 
    434     $refreshPct = (int) apply_filters('dfehc_cookie_refresh_percent', 5);
    435     $refreshPct = max(0, min(99, $refreshPct));
    436     $shouldRefresh = !isset($_COOKIE[$name]) || ((int) wp_rand(0, 99) < $refreshPct);
    437 
    438     if ($shouldRefresh) {
    439         if (!headers_sent()) {
     500
     501        $ip = dfehc_client_ip();
     502        $group = (string) apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc');
     503
     504        $maxRPM = (int) apply_filters('dfehc_max_rpm', 300);
     505        $maxRPM = max(1, $maxRPM);
     506
     507        $badIpTtl = (int) apply_filters('dfehc_bad_ip_ttl', defined('HOUR_IN_SECONDS') ? HOUR_IN_SECONDS : 3600);
     508        $badIpTtl = max(60, $badIpTtl);
     509
     510        $name = (string) apply_filters('dfehc_cookie_name', 'dfehc_user');
     511        $lifetime = (int) apply_filters('dfehc_cookie_lifetime', 400);
     512        $lifetime = max(60, min(86400, $lifetime));
     513
     514        $path = (string) apply_filters('dfehc_cookie_path', defined('COOKIEPATH') ? COOKIEPATH : '/');
     515
     516        $home = function_exists('home_url') ? (string) home_url('/') : '';
     517        $host = '';
     518        if ($home !== '' && function_exists('wp_parse_url')) {
     519            $parsed = (array) wp_parse_url($home);
     520            $host = !empty($parsed['host']) ? (string) $parsed['host'] : '';
     521        }
     522
     523        $isIpHost = $host && (filter_var($host, FILTER_VALIDATE_IP) !== false);
     524        $domainDefault = $isIpHost ? '' : ($host ?: (defined('COOKIE_DOMAIN') ? (string) COOKIE_DOMAIN : ''));
     525        $domain = (string) apply_filters('dfehc_cookie_domain', $domainDefault);
     526
     527        $sameSite = (string) apply_filters('dfehc_cookie_samesite', 'Lax');
     528        $map = ['lax' => 'Lax', 'strict' => 'Strict', 'none' => 'None'];
     529        $sameSiteUpper = $map[strtolower($sameSite)] ?? 'Lax';
     530
     531        $secure = (function_exists('is_ssl') && is_ssl()) || $sameSiteUpper === 'None';
     532        if ($sameSiteUpper === 'None' && !$secure) {
     533            $sameSiteUpper = 'Lax';
     534        }
     535
     536        $httpOnly = true;
     537
     538        $nowTs = isset($_SERVER['REQUEST_TIME']) ? (int) wp_unslash($_SERVER['REQUEST_TIME']) : time();
     539
     540        $existing = isset($_COOKIE[$name]) ? (string) wp_unslash($_COOKIE[$name]) : '';
     541        $existing = sanitize_text_field($existing);
     542
     543        $val = $existing;
     544        if (!preg_match('/^[A-Fa-f0-9]{32}$/', (string) $val)) {
     545            $val = '';
     546            if (function_exists('random_bytes')) {
     547                try {
     548                    $val = bin2hex(random_bytes(16));
     549                } catch (\Throwable $e) {
     550                    $val = '';
     551                }
     552            }
     553            if ($val === '' || !preg_match('/^[A-Fa-f0-9]{32}$/', $val)) {
     554                $seed = (string) (function_exists('wp_rand') ? wp_rand() : mt_rand()) . '|' . $nowTs . '|' . dfehc_host_token() . '|' . uniqid('', true);
     555                $val = md5($seed);
     556            }
     557        }
     558
     559        $refreshPct = (int) apply_filters('dfehc_cookie_refresh_percent', 5);
     560        $refreshPct = max(0, min(99, $refreshPct));
     561
     562        $roll = function_exists('wp_rand') ? (int) wp_rand(0, 99) : (int) (time() % 100);
     563        $hadCookie = isset($_COOKIE[$name]);
     564        $shouldRefresh = !$hadCookie || ($roll < $refreshPct);
     565
     566        $ctx = [
     567            'ip' => $ip,
     568            'cookie_name' => $name,
     569            'had_cookie' => $hadCookie ? 1 : 0,
     570            'should_refresh' => $shouldRefresh ? 1 : 0,
     571            'lifetime' => $lifetime,
     572        ];
     573
     574        if ($ip) {
     575            $rpmGateMode = (string) apply_filters('dfehc_rpm_mode', 'refresh_only', $ctx);
     576            $rpmGateMode = strtolower(trim($rpmGateMode));
     577            $doRpm = ($rpmGateMode === 'always') ? true : ($shouldRefresh || !$hadCookie);
     578
     579            if ($doRpm) {
     580                $rpmKeyBase = (string) apply_filters('dfehc_ip_rpm_key_base', 'dfehc_iprpm_', $ctx);
     581                $rpmKey = dfehc_scoped_key($rpmKeyBase) . hash('sha256', $ip);
     582                $rpmTtl = 60 + (function_exists('wp_rand') ? (int) wp_rand(0, 29) : (int) (time() % 30));
     583                $rpm = dfehc_cache_increment($rpmKey, $group, $rpmTtl);
     584
     585                if ($rpm > $maxRPM) {
     586                    $badBase = (string) apply_filters('dfehc_bad_ip_key_base', 'dfehc_bad_ip_', $ctx);
     587                    $badKey = dfehc_scoped_key($badBase) . hash('sha256', $ip);
     588                    if (function_exists('wp_cache_set')) {
     589                        wp_cache_set($badKey, 1, $group, $badIpTtl);
     590                    }
     591                    if (function_exists('dfehc_set_transient_noautoload')) {
     592                        dfehc_set_transient_noautoload($badKey . '_t', 1, $badIpTtl);
     593                    } else {
     594                        set_transient($badKey . '_t', 1, $badIpTtl);
     595                    }
     596                    if (function_exists('wp_cache_delete')) {
     597                        wp_cache_delete($rpmKey, $group);
     598                    }
     599                    return;
     600                }
     601            }
     602        }
     603
     604        if ($shouldRefresh && !headers_sent()) {
    440605            $expires = $nowTs + $lifetime;
     606
    441607            if (PHP_VERSION_ID >= 70300) {
    442608                setcookie($name, $val, [
     
    461627            }
    462628        }
    463     }
    464 
    465     if (isset($_COOKIE[$name])) {
    466         return;
    467     }
    468 
    469     if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_incr')) {
    470         $vTtl = $lifetime;
    471         $vTtl += (int) wp_rand(0, 5);
    472 
    473         if (false === wp_cache_add($scopedVisitorKey, 0, $group, $vTtl)) {
    474             wp_cache_set($scopedVisitorKey, (int) (wp_cache_get($scopedVisitorKey, $group) ?: 0), $group, $vTtl);
    475         }
    476         $valInc = wp_cache_incr($scopedVisitorKey, 1, $group);
    477         if ($valInc === false) {
    478             wp_cache_set($scopedVisitorKey, 1, $group, $vTtl);
    479         } else {
    480             wp_cache_set($scopedVisitorKey, (int) $valInc, $group, $vTtl);
    481         }
    482         return;
    483     }
    484 
    485     $allowDirectClients = (bool) apply_filters('dfehc_enable_direct_cache_clients', false);
    486 
    487     static $client = null;
    488     if ($allowDirectClients && !$client && extension_loaded('redis') && class_exists('Redis')) {
    489         try {
    490             $client = new \Redis();
    491             $sock = function_exists('get_option') ? (string) get_option('dfehc_redis_socket', '') : '';
    492             $sock = is_string($sock) ? trim($sock) : '';
    493 
    494             $ok = $sock
    495                 ? $client->pconnect($sock)
    496                 : $client->pconnect(
    497                     function_exists('dfehc_get_redis_server') ? dfehc_get_redis_server() : '127.0.0.1',
    498                     function_exists('dfehc_get_redis_port')   ? dfehc_get_redis_port()   : 6379,
    499                     1.0
    500                 );
    501 
    502             if ($ok) {
    503                 $pass = apply_filters('dfehc_redis_auth', getenv('REDIS_PASSWORD') ?: null);
    504                 $user = apply_filters('dfehc_redis_user', getenv('REDIS_USERNAME') ?: null);
    505                 if ($user && $pass && method_exists($client, 'auth')) {
    506                     $client->auth([$user, $pass]);
    507                 } elseif ($pass && method_exists($client, 'auth')) {
    508                     $client->auth($pass);
    509                 }
    510                 $pong = $client->ping();
    511                 if (!in_array($pong, ['+PONG','PONG',true], true)) {
    512                     $client = null;
    513                 }
    514             } else {
    515                 $client = null;
    516             }
    517         } catch (\Throwable $e) {
    518             $client = null;
    519         }
    520     }
    521 
    522     if ($client) {
    523         try {
    524             $client->incr($scopedVisitorKey);
    525             $client->expire($scopedVisitorKey, $lifetime);
    526             return;
    527         } catch (\Throwable $e) {
    528         }
    529     }
    530 
    531     static $mem = null;
    532     if ($allowDirectClients && !$mem && extension_loaded('memcached') && class_exists('Memcached')) {
    533         $mem = new \Memcached('dfehc-cookie');
    534         if (!$mem->getServerList()) {
    535             $mem->addServer(
    536                 function_exists('dfehc_get_memcached_server') ? dfehc_get_memcached_server() : '127.0.0.1',
    537                 function_exists('dfehc_get_memcached_port') ? dfehc_get_memcached_port() : 11211
    538             );
    539         }
    540         if (empty($mem->getStats())) {
    541             $mem = null;
    542         }
    543     }
    544 
    545     if ($mem) {
    546         try {
    547             $valInc = $mem->increment($scopedVisitorKey, 1);
    548             if ($valInc === false) {
    549                 $mem->set($scopedVisitorKey, 1, $lifetime);
    550             } else {
    551                 if (method_exists($mem, 'touch')) {
    552                     $mem->touch($scopedVisitorKey, $lifetime);
    553                 } else {
    554                     $mem->set($scopedVisitorKey, (int) $valInc, $lifetime);
    555                 }
    556             }
    557             return;
    558         } catch (\Throwable $e) {
    559         }
    560     }
    561 
    562     $cnt  = (int) get_transient($scopedVisitorKey);
    563     $vTtl = $lifetime + (int) wp_rand(0, 5);
    564     dfehc_set_transient_noautoload($scopedVisitorKey, $cnt + 1, $vTtl);
     629
     630        $jitterMax = (int) apply_filters('dfehc_visitor_ttl_jitter', 30);
     631        $jitterMax = max(0, min(300, $jitterMax));
     632        $jitter = $jitterMax > 0 ? (function_exists('wp_rand') ? (int) wp_rand(0, $jitterMax) : (int) (time() % ($jitterMax + 1))) : 0;
     633
     634        $visitorKeyBase = (string) apply_filters('dfehc_total_visitors_key_base', 'dfehc_total_visitors', $ctx);
     635        $visitorKey = dfehc_scoped_key($visitorKeyBase);
     636
     637        $shouldCount = dfehc_should_count_visitor_now($shouldRefresh, $hadCookie, $ctx);
     638        $shouldCount = (bool) apply_filters('dfehc_should_count_visitor', $shouldCount, $ctx);
     639
     640        if ($shouldCount) {
     641            dfehc_cache_increment($visitorKey, $group, $lifetime + $jitter);
     642        }
     643    }
    565644}
    566645
  • dynamic-front-end-heartbeat-control/trunk/visitor/manager.php

    r3427163 r3461136  
    22declare(strict_types=1);
    33
     4defined('ABSPATH') || exit;
     5
    46if (!function_exists('dfehc_blog_id')) {
    5     function dfehc_blog_id(): int {
     7    function dfehc_blog_id(): int
     8    {
    69        return function_exists('get_current_blog_id') ? (int) get_current_blog_id() : 0;
    710    }
    811}
     12
    913if (!function_exists('dfehc_host_token')) {
    10     function dfehc_host_token(): string {
     14    function dfehc_host_token(): string
     15    {
    1116        static $t = '';
    12         if ($t !== '') return $t;
    13         $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown'));
     17        if ($t !== '') {
     18            return $t;
     19        }
     20
     21        $url = '';
     22        if (defined('WP_HOME') && is_string(WP_HOME) && WP_HOME !== '') {
     23            $url = WP_HOME;
     24        } elseif (function_exists('home_url')) {
     25            $url = (string) home_url('/');
     26        }
     27
     28        $host = '';
     29        if ($url !== '' && function_exists('wp_parse_url')) {
     30            $p = wp_parse_url($url);
     31            if (is_array($p) && isset($p['host']) && is_string($p['host'])) {
     32                $host = $p['host'];
     33            }
     34        }
     35
     36        if ($host === '') {
     37            $host = @php_uname('n') ?: 'unknown';
     38        }
     39
    1440        $salt = defined('DB_NAME') ? (string) DB_NAME : '';
    15         return $t = substr(md5((string) $host . $salt), 0, 10);
    16     }
    17 }
     41        $t = substr(md5($host . $salt), 0, 10);
     42        return $t;
     43    }
     44}
     45
    1846if (!function_exists('dfehc_scoped_key')) {
    19     function dfehc_scoped_key(string $base): string {
     47    function dfehc_scoped_key(string $base): string
     48    {
    2049        return $base . '_' . dfehc_blog_id() . '_' . dfehc_host_token();
    21     }
    22 }
    23 if (!function_exists('dfehc_set_transient_noautoload')) {
    24     function dfehc_set_transient_noautoload(string $key, $value, int $expiration): void {
    25         $group = defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc';
    26 
    27         if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
    28             if (function_exists('wp_cache_add')) {
    29                 if (!wp_cache_add($key, $value, $group, $expiration)) {
    30                     wp_cache_set($key, $value, $group, $expiration);
    31                 }
    32             } else {
    33                 wp_cache_set($key, $value, $group, $expiration);
    34             }
    35             return;
    36         }
    37 
    38         set_transient($key, $value, $expiration);
    39 
    40         global $wpdb;
    41         if (!isset($wpdb) || !is_object($wpdb) || !isset($wpdb->options)) return;
    42         $opt_key = "_transient_$key";
    43         $opt_key_to = "_transient_timeout_$key";
    44         $wpdb->suppress_errors(true);
    45         try {
    46             $autoload = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key));
    47             if ($autoload === 'yes') {
    48                 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key, 'autoload' => 'yes'], ['%s'], ['%s','%s']);
    49             }
    50             $autoload_to = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_to));
    51             if ($autoload_to === 'yes') {
    52                 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key_to, 'autoload' => 'yes'], ['%s'], ['%s','%s']);
    53             }
    54         } finally {
    55             $wpdb->suppress_errors(false);
    56         }
    5750    }
    5851}
     
    6659            $max = $t;
    6760        }
    68 
    69         if (function_exists('wp_rand')) {
    70             return (int) wp_rand($min, $max);
    71         }
    72 
    73         try {
    74             return (int) random_int($min, $max);
    75         } catch (\Throwable $e) {
     61        if ($min === $max) {
    7662            return $min;
    7763        }
    78     }
    79 }
    80 
    81 if (!function_exists('dfehc_get_redis_server')) {
    82     function dfehc_get_redis_server(): string {
    83         $h = getenv('REDIS_HOST');
    84         return $h ? (string) $h : '127.0.0.1';
    85     }
    86 }
    87 if (!function_exists('dfehc_get_redis_port')) {
    88     function dfehc_get_redis_port(): int {
    89         $p = getenv('REDIS_PORT');
    90         return $p && ctype_digit((string) $p) ? (int) $p : 6379;
    91     }
    92 }
    93 if (!function_exists('dfehc_get_memcached_server')) {
    94     function dfehc_get_memcached_server(): string {
    95         $h = getenv('MEMCACHED_HOST');
    96         return $h ? (string) $h : '127.0.0.1';
    97     }
    98 }
    99 if (!function_exists('dfehc_get_memcached_port')) {
    100     function dfehc_get_memcached_port(): int {
    101         $p = getenv('MEMCACHED_PORT');
    102         return $p && ctype_digit((string) $p) ? (int) $p : 11211;
    103     }
    104 }
     64        if (function_exists('random_int')) {
     65            try {
     66                return (int) random_int($min, $max);
     67            } catch (\Throwable $e) {
     68            }
     69        }
     70        return function_exists('wp_rand') ? (int) wp_rand($min, $max) : $min;
     71    }
     72}
     73
    10574if (!defined('DFEHC_SENTINEL_NO_LOAD')) {
    106     define('DFEHC_SENTINEL_NO_LOAD', -1);
     75    define('DFEHC_SENTINEL_NO_LOAD', 0.404);
     76}
     77
     78if (!function_exists('dfehc_set_transient_noautoload')) {
     79    function dfehc_set_transient_noautoload(string $key, $value, int $expiration): void
     80    {
     81        $expiration = (int) $expiration;
     82        if ($expiration < 1) {
     83            $expiration = 1;
     84        }
     85        if (!function_exists('set_transient')) {
     86            return;
     87        }
     88        set_transient($key, $value, $expiration);
     89    }
    10790}
    10891
    10992if (!function_exists('dfehc_acquire_lock')) {
    110     function dfehc_acquire_lock(string $key, int $ttl = 60) {
    111         $group = apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc');
     93    function dfehc_acquire_lock(string $key, int $ttl = 60)
     94    {
     95        $ttl = max(5, (int) $ttl);
     96        $group = (string) apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? (string) DFEHC_CACHE_GROUP : 'dfehc');
    11297        $scoped = dfehc_scoped_key($key);
     98
    11399        if (class_exists('WP_Lock')) {
    114100            $lock = new WP_Lock($scoped, $ttl);
    115101            return $lock->acquire() ? $lock : null;
    116102        }
    117         if (function_exists('wp_cache_add') && wp_cache_add($scoped, 1, $group, $ttl)) {
    118             return (object) ['cache_key' => $scoped, 'cache_group' => $group];
    119         }
    120         if (false !== get_transient($scoped)) {
     103
     104        if (function_exists('wp_cache_add') && function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) {
     105            if (wp_cache_add($scoped, 1, $group, $ttl)) {
     106                return (object) ['cache_key' => $scoped, 'cache_group' => $group];
     107            }
    121108            return null;
    122109        }
    123         if (set_transient($scoped, 1, $ttl)) {
     110
     111        if (function_exists('get_transient') && false !== get_transient($scoped)) {
     112            return null;
     113        }
     114
     115        if (function_exists('set_transient') && set_transient($scoped, 1, $ttl)) {
    124116            return (object) ['transient_key' => $scoped];
    125117        }
     118
    126119        return null;
    127120    }
    128121}
     122
    129123if (!function_exists('dfehc_release_lock')) {
    130     function dfehc_release_lock($lock): void {
     124    function dfehc_release_lock($lock): void
     125    {
    131126        if ($lock instanceof WP_Lock) {
    132127            $lock->release();
    133128            return;
    134129        }
     130
    135131        if (is_object($lock) && isset($lock->cache_key)) {
    136             $group = $lock->cache_group ?? apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc');
     132            $group = isset($lock->cache_group)
     133                ? (string) $lock->cache_group
     134                : (string) apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? (string) DFEHC_CACHE_GROUP : 'dfehc');
     135
    137136            if (function_exists('wp_cache_delete')) {
    138                 wp_cache_delete($lock->cache_key, $group);
    139             }
    140             return;
    141         }
    142         if (is_object($lock) && isset($lock->transient_key)) {
    143             delete_transient($lock->transient_key);
    144         }
    145     }
    146 }
    147 
    148 function dfehc_set_default_last_activity_time(int $user_id): void {
    149     $meta_key = (string) apply_filters('dfehc_last_activity_meta_key', 'last_activity_time');
    150     update_user_meta($user_id, $meta_key, time());
     137                wp_cache_delete((string) $lock->cache_key, $group);
     138            }
     139            return;
     140        }
     141
     142        if (is_object($lock) && isset($lock->transient_key) && function_exists('delete_transient')) {
     143            delete_transient((string) $lock->transient_key);
     144        }
     145    }
     146}
     147
     148if (!function_exists('dfehc_set_default_last_activity_time')) {
     149    function dfehc_set_default_last_activity_time(int $user_id): void
     150    {
     151        if (!function_exists('update_user_meta')) {
     152            return;
     153        }
     154        $meta_key = (string) apply_filters('dfehc_last_activity_meta_key', 'last_activity_time');
     155        update_user_meta($user_id, $meta_key, time());
     156    }
    151157}
    152158add_action('user_register', 'dfehc_set_default_last_activity_time');
    153159
    154 function dfehc_add_intervals(array $s): array {
    155     if (!isset($s['dfehc_5_minutes'])) {
    156         $s['dfehc_5_minutes'] = ['interval' => 300, 'display' => __('Every 5 minutes (DFEHC)', 'dfehc')];
    157     }
    158     if (!isset($s['dfehc_daily'])) {
    159         $s['dfehc_daily'] = ['interval' => DAY_IN_SECONDS, 'display' => __('Daily (DFEHC)', 'dfehc')];
    160     }
    161     return $s;
     160if (!function_exists('dfehc_add_intervals')) {
     161    function dfehc_add_intervals(array $s): array
     162    {
     163        if (!isset($s['dfehc_5_minutes'])) {
     164            $s['dfehc_5_minutes'] = [
     165                'interval' => 300,
     166                'display'  => __('Every 5 minutes (DFEHC)', 'dfehc'),
     167            ];
     168        }
     169        if (!isset($s['dfehc_daily'])) {
     170            $s['dfehc_daily'] = [
     171                'interval' => defined('DAY_IN_SECONDS') ? (int) DAY_IN_SECONDS : 86400,
     172                'display'  => __('Daily (DFEHC)', 'dfehc'),
     173            ];
     174        }
     175        return $s;
     176    }
    162177}
    163178add_filter('cron_schedules', 'dfehc_add_intervals');
    164179
    165 function dfehc_schedule_user_activity_processing(): void {
    166     $lock = dfehc_acquire_lock('dfehc_cron_sched_lock', 15);
    167     if (!$lock) {
    168         return;
    169     }
    170 
    171     $aligned = time() - time() % 300 + 300;
    172 
    173     try {
    174         if (!get_option('dfehc_activity_cron_scheduled') && !wp_next_scheduled('dfehc_process_user_activity')) {
    175             $ok = wp_schedule_event($aligned, 'dfehc_5_minutes', 'dfehc_process_user_activity');
    176             if ($ok && !is_wp_error($ok)) {
    177                 update_option('dfehc_activity_cron_scheduled', 1, false);
    178             } else {
    179                 wp_schedule_single_event($aligned, 'dfehc_process_user_activity');
    180             }
    181         }
    182 
    183         if (!wp_next_scheduled('dfehc_cleanup_user_activity')) {
    184             $args = [0, (int) apply_filters('dfehc_cleanup_batch_size', 75)];
    185             $ok2 = wp_schedule_event($aligned + 300, 'dfehc_daily', 'dfehc_cleanup_user_activity', $args);
    186             if (!$ok2 || is_wp_error($ok2)) {
    187                 wp_schedule_single_event($aligned + 300, 'dfehc_cleanup_user_activity', $args);
    188             }
    189         }
    190     } finally {
    191         dfehc_release_lock($lock);
    192     }
    193 }
    194 add_action('init', 'dfehc_schedule_user_activity_processing');
    195 
    196 function dfehc_throttled_user_activity_handler(): void {
    197     $lock = dfehc_acquire_lock('dfehc_recent_user_processing', 300);
    198     if (!$lock) {
    199         return;
    200     }
    201 
    202     $prev = (bool) ignore_user_abort(true);
    203 
    204     try {
    205         dfehc_process_user_activity();
    206     } finally {
    207         ignore_user_abort((bool) $prev);
    208         dfehc_release_lock($lock);
     180if (!function_exists('dfehc_activity_scheduled_option_key')) {
     181    function dfehc_activity_scheduled_option_key(): string
     182    {
     183        return dfehc_scoped_key('dfehc_activity_cron_scheduled');
     184    }
     185}
     186
     187if (!function_exists('dfehc_schedule_user_activity_processing')) {
     188    function dfehc_schedule_user_activity_processing(): void
     189    {
     190        static $ran = false;
     191        if ($ran) {
     192            return;
     193        }
     194        $ran = true;
     195
     196        $fast_key = dfehc_scoped_key('dfehc_sched_fast_guard');
     197        if (function_exists('get_transient') && get_transient($fast_key) !== false) {
     198            return;
     199        }
     200        dfehc_set_transient_noautoload($fast_key, 1, (int) apply_filters('dfehc_schedule_fast_guard_ttl', 300));
     201
     202        $lock = dfehc_acquire_lock('dfehc_cron_sched_lock', 15);
     203        if (!$lock) {
     204            return;
     205        }
     206
     207        $aligned = time() - (time() % 300) + 300;
     208
     209        try {
     210            $schedOpt = dfehc_activity_scheduled_option_key();
     211
     212            $needs_primary = true;
     213            if (function_exists('get_option')) {
     214                $needs_primary = !get_option($schedOpt);
     215            }
     216
     217            if ($needs_primary && function_exists('wp_next_scheduled') && !wp_next_scheduled('dfehc_process_user_activity')) {
     218                $ok = function_exists('wp_schedule_event') ? wp_schedule_event($aligned, 'dfehc_5_minutes', 'dfehc_process_user_activity') : false;
     219                if ($ok && !is_wp_error($ok)) {
     220                    if (function_exists('update_option')) {
     221                        update_option($schedOpt, 1, false);
     222                    }
     223                } else {
     224                    if (function_exists('wp_schedule_single_event')) {
     225                        wp_schedule_single_event($aligned, 'dfehc_process_user_activity');
     226                    }
     227                }
     228            }
     229
     230            $cleanupArgs = [0, (int) apply_filters('dfehc_cleanup_batch_size', 75)];
     231            if (function_exists('wp_next_scheduled') && !wp_next_scheduled('dfehc_cleanup_user_activity', $cleanupArgs)) {
     232                $ok2 = function_exists('wp_schedule_event') ? wp_schedule_event($aligned + 300, 'dfehc_daily', 'dfehc_cleanup_user_activity', $cleanupArgs) : false;
     233                if (!$ok2 || is_wp_error($ok2)) {
     234                    if (function_exists('wp_schedule_single_event')) {
     235                        wp_schedule_single_event($aligned + 300, 'dfehc_cleanup_user_activity', $cleanupArgs);
     236                    }
     237                }
     238            }
     239        } finally {
     240            dfehc_release_lock($lock);
     241        }
     242    }
     243}
     244add_action('init', 'dfehc_schedule_user_activity_processing', 10);
     245
     246if (!function_exists('dfehc_get_users_in_batches')) {
     247    function dfehc_get_users_in_batches(int $batch_size = 75, int $offset = 0): array
     248    {
     249        $batch_size = max(1, min(1000, $batch_size));
     250        $offset = max(0, $offset);
     251
     252        if (!class_exists('WP_User_Query')) {
     253            return [];
     254        }
     255
     256        $q = new WP_User_Query([
     257            'number' => $batch_size,
     258            'offset' => $offset,
     259            'fields' => 'ID',
     260        ]);
     261
     262        $res = $q->get_results();
     263        return is_array($res) ? $res : [];
     264    }
     265}
     266
     267if (!function_exists('dfehc_throttled_user_activity_handler')) {
     268    function dfehc_throttled_user_activity_handler(): void
     269    {
     270        $lock = dfehc_acquire_lock('dfehc_recent_user_processing', 300);
     271        if (!$lock) {
     272            return;
     273        }
     274
     275        $prev = function_exists('ignore_user_abort') ? (bool) ignore_user_abort(true) : false;
     276        try {
     277            dfehc_process_user_activity();
     278        } finally {
     279            if (function_exists('ignore_user_abort')) {
     280                ignore_user_abort((bool) $prev);
     281            }
     282            dfehc_release_lock($lock);
     283        }
    209284    }
    210285}
    211286add_action('dfehc_process_user_activity', 'dfehc_throttled_user_activity_handler');
    212287
    213 function dfehc_process_user_activity(): void {
    214     $flag_opt = dfehc_scoped_key('dfehc_activity_backfill_done');
    215     if (get_option($flag_opt)) {
    216         return;
    217     }
    218     global $wpdb;
    219     if (!isset($wpdb) || !is_object($wpdb) || !isset($wpdb->users)) {
    220         return;
    221     }
    222     $meta_key = (string) apply_filters('dfehc_last_activity_meta_key', 'last_activity_time');
    223     $batch = (int) apply_filters('dfehc_activity_processing_batch_size', 75);
    224     $batch = max(1, min(500, $batch));
    225     $last_id_opt = dfehc_scoped_key('dfehc_activity_last_id');
    226     $last_id = (int) get_option($last_id_opt, 0);
    227     $ids = $wpdb->get_col($wpdb->prepare(
    228         "SELECT ID FROM $wpdb->users WHERE ID > %d ORDER BY ID ASC LIMIT %d",
    229         $last_id, $batch
    230     ));
    231     if (!$ids) {
    232         update_option($flag_opt, 1, false);
    233         delete_option($last_id_opt);
    234         update_option(dfehc_scoped_key('dfehc_last_activity_cron'), time(), false);
    235         return;
    236     }
    237     $now = time();
    238     $written = 0;
    239     $max_writes = (int) apply_filters('dfehc_activity_max_writes_per_run', 500);
    240     foreach ($ids as $id) {
    241         if (!get_user_meta((int) $id, $meta_key, true)) {
    242             update_user_meta((int) $id, $meta_key, $now);
    243             $written++;
    244             if ($written >= $max_writes) {
    245                 break;
    246             }
    247         }
    248     }
    249     $last = end($ids);
    250     update_option($last_id_opt, $last ? (int) $last : (int) $last_id, false);
    251     update_option(dfehc_scoped_key('dfehc_last_activity_cron'), time(), false);
    252 }
    253 
    254 function dfehc_record_user_activity(): void {
    255     if (!function_exists('is_user_logged_in') || !is_user_logged_in()) {
    256         return;
    257     }
    258     static $cache = [];
    259     $meta_key = (string) apply_filters('dfehc_last_activity_meta_key', 'last_activity_time');
    260     $uid = (int) get_current_user_id();
    261     if ($uid <= 0) {
    262         return;
    263     }
    264     $now = time();
    265     $interval = (int) apply_filters('dfehc_activity_update_interval', 900);
    266     $interval = max(60, $interval);
    267     $last = $cache[$uid] ?? (int) get_user_meta($uid, $meta_key, true);
    268     if ($now - $last >= $interval) {
    269         update_user_meta($uid, $meta_key, $now);
    270         $cache[$uid] = $now;
    271     }
    272 }
    273 add_action('wp', 'dfehc_record_user_activity');
    274 
    275 function dfehc_cleanup_user_activity(int $last_id = 0, int $batch_size = 75): void {
    276     $lock = dfehc_acquire_lock('dfehc_cleanup_lock', 600);
    277     if (!$lock) {
    278         return;
    279     }
    280 
    281     $prev = (bool) ignore_user_abort(true);
    282 
    283     try {
     288if (!function_exists('dfehc_process_user_activity')) {
     289    function dfehc_process_user_activity(): void
     290    {
     291        static $memo_done = null;
     292        static $memo_flag_opt = null;
     293        static $memo_last_id_opt = null;
     294
     295        $flag_opt = $memo_flag_opt ?? ($memo_flag_opt = dfehc_scoped_key('dfehc_activity_backfill_done'));
     296        if (function_exists('get_option')) {
     297            if ($memo_done === true) {
     298                return;
     299            }
     300            $done = (bool) get_option($flag_opt);
     301            if ($done) {
     302                $memo_done = true;
     303                return;
     304            }
     305        }
     306
    284307        global $wpdb;
    285308        if (!isset($wpdb) || !is_object($wpdb) || !isset($wpdb->users)) {
    286309            return;
    287310        }
     311
    288312        $meta_key = (string) apply_filters('dfehc_last_activity_meta_key', 'last_activity_time');
    289         $batch_size = (int) apply_filters('dfehc_cleanup_batch_size', $batch_size);
    290         $batch_size = max(1, min(500, $batch_size));
    291 
    292         $ids = $wpdb->get_col(
    293             $wpdb->prepare(
    294                 "SELECT ID FROM $wpdb->users WHERE ID > %d ORDER BY ID ASC LIMIT %d",
     313        $batch = (int) apply_filters('dfehc_activity_processing_batch_size', 75);
     314        $batch = max(1, min(500, $batch));
     315
     316        $last_id_opt = $memo_last_id_opt ?? ($memo_last_id_opt = dfehc_scoped_key('dfehc_activity_last_id'));
     317        $last_id = function_exists('get_option') ? (int) get_option($last_id_opt, 0) : 0;
     318
     319        $ids = $wpdb->get_col($wpdb->prepare(
     320            "SELECT ID FROM {$wpdb->users} WHERE ID > %d ORDER BY ID ASC LIMIT %d",
     321            $last_id,
     322            $batch
     323        ));
     324
     325        if (!$ids) {
     326            if (function_exists('update_option')) {
     327                update_option($flag_opt, 1, false);
     328                $memo_done = true;
     329                delete_option($last_id_opt);
     330                update_option(dfehc_scoped_key('dfehc_last_activity_cron'), time(), false);
     331            }
     332            return;
     333        }
     334
     335        $now = time();
     336        $written = 0;
     337        $max_writes = (int) apply_filters('dfehc_activity_max_writes_per_run', 500);
     338        $max_writes = max(1, min(2000, $max_writes));
     339
     340        foreach ($ids as $id) {
     341            $id = (int) $id;
     342            $has = function_exists('get_user_meta') ? get_user_meta($id, $meta_key, true) : null;
     343            if (!$has) {
     344                if (function_exists('update_user_meta')) {
     345                    update_user_meta($id, $meta_key, $now);
     346                    $written++;
     347                    if ($written >= $max_writes) {
     348                        break;
     349                    }
     350                }
     351            }
     352        }
     353
     354        $last = end($ids);
     355        if (function_exists('update_option')) {
     356            update_option($last_id_opt, $last ? (int) $last : (int) $last_id, false);
     357            update_option(dfehc_scoped_key('dfehc_last_activity_cron'), time(), false);
     358        }
     359    }
     360}
     361
     362if (!function_exists('dfehc_record_user_activity')) {
     363    function dfehc_record_user_activity(): void
     364    {
     365        if (!function_exists('is_user_logged_in') || !is_user_logged_in()) {
     366            return;
     367        }
     368
     369        $uid = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
     370        if ($uid <= 0) {
     371            return;
     372        }
     373
     374        static $cache = [];
     375        $meta_key = (string) apply_filters('dfehc_last_activity_meta_key', 'last_activity_time');
     376
     377        $now = time();
     378        $interval = (int) apply_filters('dfehc_activity_update_interval', 900);
     379        $interval = max(60, $interval);
     380
     381        $last = isset($cache[$uid]) ? (int) $cache[$uid] : (int) (function_exists('get_user_meta') ? get_user_meta($uid, $meta_key, true) : 0);
     382
     383        if ($last <= 0) {
     384            if (function_exists('update_user_meta')) {
     385                update_user_meta($uid, $meta_key, $now);
     386            }
     387            $cache[$uid] = $now;
     388            return;
     389        }
     390
     391        $delta = $now - $last;
     392        if ($delta < 0) {
     393            $delta = 0;
     394        }
     395
     396        if ($delta >= $interval) {
     397            if (function_exists('update_user_meta')) {
     398                update_user_meta($uid, $meta_key, $now);
     399            }
     400            $cache[$uid] = $now;
     401
     402            $k = 'dfehc_user_activity';
     403            $activity = function_exists('get_user_meta') ? get_user_meta($uid, $k, true) : null;
     404            if (!is_array($activity)) {
     405                $activity = [];
     406            }
     407            $dur = isset($activity['durations']) && is_array($activity['durations']) ? $activity['durations'] : [];
     408
     409            $delta_f = (float) $delta;
     410            if (is_finite($delta_f) && $delta_f >= 0.0) {
     411                $dur[] = $delta_f;
     412            }
     413
     414            $max_points = (int) apply_filters('dfehc_activity_max_points_per_user', 40);
     415            $max_points = max(5, min(150, $max_points));
     416            if (count($dur) > $max_points) {
     417                $dur = array_slice($dur, -$max_points);
     418            }
     419
     420            $activity['durations'] = $dur;
     421            $activity['t'] = $now;
     422
     423            if (function_exists('update_user_meta')) {
     424                update_user_meta($uid, $k, $activity);
     425            }
     426        }
     427    }
     428}
     429add_action('wp', 'dfehc_record_user_activity', 10);
     430
     431if (!function_exists('dfehc_cleanup_user_activity')) {
     432    function dfehc_cleanup_user_activity(int $last_id = 0, int $batch_size = 75): void
     433    {
     434        $lock = dfehc_acquire_lock('dfehc_cleanup_lock', 600);
     435        if (!$lock) {
     436            return;
     437        }
     438
     439        $prev = function_exists('ignore_user_abort') ? (bool) ignore_user_abort(true) : false;
     440        try {
     441            global $wpdb;
     442            if (!isset($wpdb) || !is_object($wpdb) || !isset($wpdb->users)) {
     443                return;
     444            }
     445
     446            $meta_key = (string) apply_filters('dfehc_last_activity_meta_key', 'last_activity_time');
     447            $batch_size = (int) apply_filters('dfehc_cleanup_batch_size', $batch_size);
     448            $batch_size = max(1, min(500, $batch_size));
     449
     450            $ids = $wpdb->get_col($wpdb->prepare(
     451                "SELECT ID FROM {$wpdb->users} WHERE ID > %d ORDER BY ID ASC LIMIT %d",
    295452                (int) $last_id,
    296453                (int) $batch_size
    297             )
    298         );
    299 
    300         if (!$ids) {
    301             update_option(dfehc_scoped_key('dfehc_last_cleanup_cron'), time(), false);
    302             return;
    303         }
    304 
    305         $cutoff = time() - (int) apply_filters('dfehc_activity_expiration', WEEK_IN_SECONDS);
    306 
    307         foreach ($ids as $id) {
    308             $id = (int) $id;
    309             $ts = (int) get_user_meta($id, $meta_key, true);
    310             if ($ts && $ts < $cutoff) {
    311                 delete_user_meta($id, $meta_key);
    312             }
    313         }
    314 
    315         if (count($ids) === $batch_size) {
    316             $delay = 15 + dfehc_rand_int(0, 5);
    317             wp_schedule_single_event(
    318                 time() + $delay,
    319                 'dfehc_cleanup_user_activity',
    320                 [(int) end($ids), (int) $batch_size]
    321             );
    322         }
    323 
    324         update_option(dfehc_scoped_key('dfehc_last_cleanup_cron'), time(), false);
    325 
    326     } finally {
    327         ignore_user_abort((bool) $prev);
    328         dfehc_release_lock($lock);
     454            ));
     455
     456            if (!$ids) {
     457                if (function_exists('update_option')) {
     458                    update_option(dfehc_scoped_key('dfehc_last_cleanup_cron'), time(), false);
     459                }
     460                return;
     461            }
     462
     463            $cutoff = time() - (int) apply_filters('dfehc_activity_expiration', defined('WEEK_IN_SECONDS') ? (int) WEEK_IN_SECONDS : 604800);
     464
     465            foreach ($ids as $id) {
     466                $id = (int) $id;
     467                $ts = function_exists('get_user_meta') ? (int) get_user_meta($id, $meta_key, true) : 0;
     468                if ($ts && $ts < $cutoff) {
     469                    if (function_exists('delete_user_meta')) {
     470                        delete_user_meta($id, $meta_key);
     471                    }
     472                }
     473            }
     474
     475            if (count($ids) === $batch_size && function_exists('wp_schedule_single_event')) {
     476                $delay = 15 + dfehc_rand_int(0, 5);
     477                wp_schedule_single_event(time() + $delay, 'dfehc_cleanup_user_activity', [(int) end($ids), (int) $batch_size]);
     478            }
     479
     480            if (function_exists('update_option')) {
     481                update_option(dfehc_scoped_key('dfehc_last_cleanup_cron'), time(), false);
     482            }
     483        } finally {
     484            if (function_exists('ignore_user_abort')) {
     485                ignore_user_abort((bool) $prev);
     486            }
     487            dfehc_release_lock($lock);
     488        }
    329489    }
    330490}
    331491add_action('dfehc_cleanup_user_activity', 'dfehc_cleanup_user_activity', 10, 2);
    332492
    333 function dfehc_increment_total_visitors(): void {
    334     $key = dfehc_scoped_key('dfehc_total_visitors');
    335     $grp = apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc');
    336     $ttl = (int) apply_filters('dfehc_total_visitors_ttl', HOUR_IN_SECONDS);
    337     if (function_exists('random_int')) {
     493if (!function_exists('dfehc_total_visitors_storage_key')) {
     494    function dfehc_total_visitors_storage_key(): string
     495    {
     496        return dfehc_scoped_key('dfehc_total_visitors');
     497    }
     498}
     499
     500if (!function_exists('dfehc_increment_total_visitors')) {
     501    function dfehc_increment_total_visitors(): void
     502    {
     503        $key = dfehc_total_visitors_storage_key();
     504        $grp = (string) apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? (string) DFEHC_CACHE_GROUP : 'dfehc');
     505        $ttl = (int) apply_filters('dfehc_total_visitors_ttl', defined('HOUR_IN_SECONDS') ? (int) HOUR_IN_SECONDS : 3600);
     506        $ttl = max(60, $ttl + dfehc_rand_int(0, 5));
     507
     508        if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_incr')) {
     509            if (function_exists('wp_cache_add')) {
     510                wp_cache_add($key, 0, $grp, $ttl);
     511            }
     512            $val = wp_cache_incr($key, 1, $grp);
     513            if ($val === false && function_exists('wp_cache_set')) {
     514                wp_cache_set($key, 1, $grp, $ttl);
     515            }
     516            return;
     517        }
     518
     519        $cur = function_exists('get_transient') ? get_transient($key) : false;
     520        $cur = is_numeric($cur) ? (int) $cur : 0;
     521        $cur++;
     522        dfehc_set_transient_noautoload($key, $cur, $ttl);
     523    }
     524}
     525
     526if (!function_exists('dfehc_safe_cache_get_raw')) {
     527    function dfehc_safe_cache_get_raw(string $rawKey): int
     528    {
     529        $grp = (string) apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? (string) DFEHC_CACHE_GROUP : 'dfehc');
     530        if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_get')) {
     531            $v = wp_cache_get($rawKey, $grp);
     532            return is_numeric($v) ? (int) $v : 0;
     533        }
     534        $v = function_exists('get_transient') ? get_transient($rawKey) : false;
     535        return is_numeric($v) ? (int) $v : 0;
     536    }
     537}
     538
     539if (!function_exists('dfehc_safe_cache_delete_raw')) {
     540    function dfehc_safe_cache_delete_raw(string $rawKey): void
     541    {
     542        $grp = (string) apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? (string) DFEHC_CACHE_GROUP : 'dfehc');
     543        if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_delete')) {
     544            wp_cache_delete($rawKey, $grp);
     545        }
     546        if (function_exists('delete_transient')) {
     547            delete_transient($rawKey);
     548        }
     549    }
     550}
     551
     552if (!function_exists('dfehc_get_website_visitors')) {
     553    function dfehc_get_website_visitors(): int
     554    {
     555        static $request_memo = null;
     556        if ($request_memo !== null) {
     557            return (int) $request_memo;
     558        }
     559
     560        $cache_key = dfehc_scoped_key('dfehc_total_visitors_cache');
     561        $regen_key = dfehc_scoped_key('dfehc_regenerating_cache');
     562        $stale_opt = dfehc_scoped_key('dfehc_stale_total_visitors');
     563
     564        $cache = function_exists('get_transient') ? get_transient($cache_key) : false;
     565        if ($cache !== false && is_numeric($cache)) {
     566            $request_memo = (int) $cache;
     567            return (int) $request_memo;
     568        }
     569
     570        if (function_exists('get_transient') && get_transient($regen_key)) {
     571            $stale = function_exists('get_option') ? get_option($stale_opt, 0) : 0;
     572            $request_memo = is_numeric($stale) ? (int) $stale : 0;
     573            return (int) $request_memo;
     574        }
     575
     576        $regen_ttl = (int) (defined('MINUTE_IN_SECONDS') ? (int) MINUTE_IN_SECONDS : 60) + dfehc_rand_int(0, 5);
     577        dfehc_set_transient_noautoload($regen_key, 1, $regen_ttl);
     578
     579        $rawKey = dfehc_total_visitors_storage_key();
     580        $total = dfehc_safe_cache_get_raw($rawKey);
     581
     582        $ttl = (int) apply_filters('dfehc_visitors_cache_ttl', 10 * (defined('MINUTE_IN_SECONDS') ? (int) MINUTE_IN_SECONDS : 60));
     583        $ttl = max(30, $ttl + dfehc_rand_int(0, 5));
     584
     585        dfehc_set_transient_noautoload($cache_key, (int) $total, $ttl);
     586        if (function_exists('update_option')) {
     587            update_option($stale_opt, (int) $total, false);
     588        }
     589        if (function_exists('delete_transient')) {
     590            delete_transient($regen_key);
     591        }
     592
     593        $request_memo = (int) apply_filters('dfehc_get_website_visitors_result', (int) $total);
     594        return (int) $request_memo;
     595    }
     596}
     597
     598if (!function_exists('dfehc_reset_total_visitors')) {
     599    function dfehc_reset_total_visitors(): void
     600    {
     601        $start = microtime(true);
     602        $lock = dfehc_acquire_lock('dfehc_resetting_visitors', 60);
     603        if (!$lock) {
     604            return;
     605        }
     606
    338607        try {
    339             $ttl += random_int(0, 5);
    340         } catch (\Throwable $e) {
    341         }
    342     }
    343 
    344     if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_incr')) {
    345         if (false === wp_cache_add($key, 0, $grp, $ttl)) {
    346             $existing = wp_cache_get($key, $grp);
    347             wp_cache_set($key, (int) ($existing ?: 0), $grp, $ttl);
    348         }
    349         $val = wp_cache_incr($key, 1, $grp);
    350         if ($val === false) {
    351             wp_cache_set($key, 1, $grp, $ttl);
    352         }
    353         return;
    354     }
    355 
    356     $allowDirectClients = (bool) apply_filters('dfehc_enable_direct_cache_clients', false);
    357 
    358     static $redis = null;
    359     if ($allowDirectClients && !$redis && extension_loaded('redis') && class_exists('Redis')) {
    360         try {
    361             $redis = new \Redis();
    362             $ok = $redis->pconnect(dfehc_get_redis_server(), dfehc_get_redis_port(), 1.0);
    363             if ($ok) {
    364                 $pass = apply_filters('dfehc_redis_auth', getenv('REDIS_PASSWORD') ?: null);
    365                 $user = apply_filters('dfehc_redis_user', getenv('REDIS_USERNAME') ?: null);
    366                 if ($user && $pass && method_exists($redis, 'auth')) {
    367                     $redis->auth([$user, $pass]);
    368                 } elseif ($pass && method_exists($redis, 'auth')) {
    369                     $redis->auth($pass);
     608            $threshold = (float) apply_filters('dfehc_reset_load_threshold', 15.0);
     609            $load = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : (defined('DFEHC_SENTINEL_NO_LOAD') ? (float) DFEHC_SENTINEL_NO_LOAD : 0.404);
     610            if (!is_numeric($load)) {
     611                return;
     612            }
     613
     614            $load = (float) $load;
     615            if ($load === (defined('DFEHC_SENTINEL_NO_LOAD') ? (float) DFEHC_SENTINEL_NO_LOAD : 0.404) || $load >= $threshold) {
     616                return;
     617            }
     618
     619            $rawKey = dfehc_total_visitors_storage_key();
     620            dfehc_safe_cache_delete_raw($rawKey);
     621
     622            if (function_exists('delete_transient')) {
     623                delete_transient(dfehc_scoped_key('dfehc_total_visitors_cache'));
     624                delete_transient(dfehc_scoped_key('dfehc_regenerating_cache'));
     625            }
     626            if (function_exists('delete_option')) {
     627                delete_option(dfehc_scoped_key('dfehc_stale_total_visitors'));
     628            }
     629
     630            if ((microtime(true) - $start) > 5.0) {
     631                return;
     632            }
     633        } finally {
     634            dfehc_release_lock($lock);
     635        }
     636    }
     637}
     638add_action('dfehc_reset_total_visitors_event', 'dfehc_reset_total_visitors');
     639
     640if (!function_exists('dfehc_on_activate')) {
     641    function dfehc_on_activate(): void
     642    {
     643        $aligned = time() - (time() % 300) + 300;
     644
     645        if (function_exists('wp_next_scheduled') && function_exists('wp_schedule_event') && !wp_next_scheduled('dfehc_reset_total_visitors_event')) {
     646            $ts = $aligned + (defined('HOUR_IN_SECONDS') ? (int) HOUR_IN_SECONDS : 3600);
     647            $ok = wp_schedule_event($ts, 'hourly', 'dfehc_reset_total_visitors_event');
     648            if (!$ok || is_wp_error($ok)) {
     649                if (function_exists('wp_schedule_single_event')) {
     650                    wp_schedule_single_event($ts, 'dfehc_reset_total_visitors_event');
    370651                }
    371                 $pong = $redis->ping();
    372                 if (!in_array($pong, ['+PONG','PONG', true], true)) {
    373                     $redis = null;
    374                 }
    375             } else {
    376                 $redis = null;
    377             }
    378         } catch (\Throwable $e) {
    379             $redis = null;
    380         }
    381     }
    382     if ($redis) {
    383         try {
    384             $redis->incr($key);
    385             $redis->expire($key, $ttl);
    386             return;
    387         } catch (\Throwable $e) {
    388         }
    389     }
    390 
    391     static $mem = null;
    392     if ($allowDirectClients && !$mem && extension_loaded('memcached') && class_exists('Memcached')) {
    393         $mem = new \Memcached('dfehc-visitors');
    394         if (!$mem->getServerList()) {
    395             $mem->addServer(dfehc_get_memcached_server(), dfehc_get_memcached_port());
    396         }
    397         if (empty($mem->getStats())) {
    398             $mem = null;
    399         }
    400     }
    401     if ($mem) {
    402         try {
    403             $inc = $mem->increment($key, 1);
    404             if ($inc === false) {
    405                 $mem->set($key, 1, $ttl);
    406             } else {
    407                 if (method_exists($mem, 'touch')) {
    408                     $mem->touch($key, $ttl);
    409                 } else {
    410                     $mem->set($key, (int) $inc, $ttl);
    411                 }
    412             }
    413             return;
    414         } catch (\Throwable $e) {
    415         }
    416     }
    417 
    418     $cnt = (int) get_transient($key);
    419     dfehc_set_transient_noautoload($key, $cnt + 1, $ttl);
    420 }
    421 function dfehc_increment_total_visitors_fallback(): void {
    422     dfehc_increment_total_visitors();
    423 }
    424 
    425 function dfehc_safe_cache_get(string $key): int {
    426     $scoped = dfehc_scoped_key($key);
    427     $grp = apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc');
    428     if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_get')) {
    429         $v = wp_cache_get($scoped, $grp);
    430         return is_numeric($v) ? (int) $v : 0;
    431     }
    432     $v = get_transient($scoped);
    433     return is_numeric($v) ? (int) $v : 0;
    434 }
    435 
    436 function dfehc_safe_cache_delete(string $key): void {
    437     $scoped = dfehc_scoped_key($key);
    438     $grp = apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc');
    439     if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_delete')) {
    440         wp_cache_delete($scoped, $grp);
    441     }
    442     delete_transient($scoped);
    443 }
    444 
    445 function dfehc_get_website_visitors(): int {
    446     $cache_key = dfehc_scoped_key('dfehc_total_visitors_cache');
    447     $regen_key = dfehc_scoped_key('dfehc_regenerating_cache');
    448     $stale_opt = dfehc_scoped_key('dfehc_stale_total_visitors');
    449     $cache = get_transient($cache_key);
    450     if ($cache !== false) {
    451         return (int) $cache;
    452     }
    453     if (get_transient($regen_key)) {
    454         $stale = get_option($stale_opt, 0);
    455         return is_numeric($stale) ? (int) $stale : 0;
    456     }
    457 
    458     $regen_ttl = (int) MINUTE_IN_SECONDS;
    459     if (function_exists('random_int')) {
    460         try {
    461             $regen_ttl += random_int(0, 5);
    462         } catch (\Throwable $e) {
    463         }
    464     }
    465     dfehc_set_transient_noautoload($regen_key, true, $regen_ttl);
    466 
    467     $total = dfehc_safe_cache_get('dfehc_total_visitors');
    468 
    469     $ttl = (int) apply_filters('dfehc_visitors_cache_ttl', 10 * MINUTE_IN_SECONDS);
    470     if (function_exists('random_int')) {
    471         try {
    472             $ttl += random_int(0, 5);
    473         } catch (\Throwable $e) {
    474         }
    475     }
    476     dfehc_set_transient_noautoload($cache_key, (int) $total, $ttl);
    477     update_option($stale_opt, (int) $total, false);
    478     delete_transient($regen_key);
    479 
    480     return (int) apply_filters('dfehc_get_website_visitors_result', (int) $total);
    481 }
    482 
    483 function dfehc_get_users_in_batches(int $batch_size, int $offset): array {
    484     $batch_size = max(1, min(1000, $batch_size));
    485     $offset = max(0, $offset);
    486     $query = new WP_User_Query([
    487         'number' => $batch_size,
    488         'offset' => $offset,
    489         'fields' => ['ID'],
    490     ]);
    491     $res = $query->get_results();
    492     return is_array($res) ? $res : [];
    493 }
    494 
    495 function dfehc_reset_total_visitors(): void {
    496     $start = microtime(true);
    497     $lock = dfehc_acquire_lock('dfehc_resetting_visitors', 60);
    498     if (!$lock) {
    499         return;
    500     }
    501     try {
    502         $threshold = (float) apply_filters('dfehc_reset_load_threshold', 15.0);
    503         $load = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : DFEHC_SENTINEL_NO_LOAD;
    504         if (!is_numeric($load)) {
    505             return;
    506         }
    507         $load = (float) $load;
    508         if ($load === (float) DFEHC_SENTINEL_NO_LOAD || $load >= $threshold) {
    509             return;
    510         }
    511         dfehc_safe_cache_delete('dfehc_total_visitors');
    512         delete_option(dfehc_scoped_key('dfehc_stale_total_visitors'));
    513         delete_transient(dfehc_scoped_key('dfehc_total_visitors'));
    514         delete_transient(dfehc_scoped_key('dfehc_regenerating_cache'));
    515         if (microtime(true) - $start > 5) {
    516             return;
    517         }
    518     } finally {
    519         dfehc_release_lock($lock);
    520     }
    521 }
    522 add_action('dfehc_reset_total_visitors_event', 'dfehc_reset_total_visitors');
    523 
    524 function dfehc_on_activate(): void {
    525     $aligned = time() - time() % 300 + 300;
    526     if (!wp_next_scheduled('dfehc_reset_total_visitors_event')) {
    527         $ok = wp_schedule_event($aligned + HOUR_IN_SECONDS, 'hourly', 'dfehc_reset_total_visitors_event');
     652            }
     653        }
     654
     655        dfehc_process_user_activity();
     656        dfehc_schedule_user_activity_processing();
     657    }
     658}
     659
     660if (!function_exists('dfehc_ensure_reset_visitors_schedule')) {
     661    function dfehc_ensure_reset_visitors_schedule(): void
     662    {
     663        if (!function_exists('wp_next_scheduled') || !function_exists('wp_schedule_event')) {
     664            return;
     665        }
     666        if (wp_next_scheduled('dfehc_reset_total_visitors_event')) {
     667            return;
     668        }
     669
     670        $aligned = time() - (time() % 300) + 300;
     671        $ts = $aligned + (defined('HOUR_IN_SECONDS') ? (int) HOUR_IN_SECONDS : 3600);
     672
     673        $ok = wp_schedule_event($ts, 'hourly', 'dfehc_reset_total_visitors_event');
    528674        if (!$ok || is_wp_error($ok)) {
    529             wp_schedule_single_event($aligned + HOUR_IN_SECONDS, 'dfehc_reset_total_visitors_event');
    530         }
    531     }
    532     dfehc_process_user_activity();
    533 }
     675            if (function_exists('wp_schedule_single_event')) {
     676                wp_schedule_single_event($ts, 'dfehc_reset_total_visitors_event');
     677            }
     678        }
     679    }
     680}
     681add_action('init', 'dfehc_ensure_reset_visitors_schedule', 10);
     682
     683if (!function_exists('dfehc_unschedule_all')) {
     684    function dfehc_unschedule_all(string $hook, array $args = []): void
     685    {
     686        if (!function_exists('wp_next_scheduled') || !function_exists('wp_unschedule_event')) {
     687            return;
     688        }
     689        while ($ts = wp_next_scheduled($hook, $args)) {
     690            wp_unschedule_event($ts, $hook, $args);
     691        }
     692    }
     693}
     694
     695if (!function_exists('dfehc_on_deactivate')) {
     696    function dfehc_on_deactivate(): void
     697    {
     698        dfehc_unschedule_all('dfehc_process_user_activity');
     699        dfehc_unschedule_all('dfehc_reset_total_visitors_event');
     700        $cleanupArgs = [0, (int) apply_filters('dfehc_cleanup_batch_size', 75)];
     701        dfehc_unschedule_all('dfehc_cleanup_user_activity', $cleanupArgs);
     702
     703        if (function_exists('delete_option')) {
     704            delete_option(dfehc_activity_scheduled_option_key());
     705            delete_option(dfehc_scoped_key('dfehc_activity_backfill_done'));
     706            delete_option(dfehc_scoped_key('dfehc_activity_last_id'));
     707        }
     708    }
     709}
     710
    534711if (function_exists('register_activation_hook')) {
    535     register_activation_hook(__FILE__, 'dfehc_on_activate');
    536 }
    537 
    538 function dfehc_ensure_reset_visitors_schedule(): void {
    539     if (!function_exists('wp_next_scheduled') || !function_exists('wp_schedule_event')) {
    540         return;
    541     }
    542     if (wp_next_scheduled('dfehc_reset_total_visitors_event')) {
    543         return;
    544     }
    545     $aligned = time() - time() % 300 + 300;
    546     $ts = $aligned + HOUR_IN_SECONDS;
    547     $ok = wp_schedule_event($ts, 'hourly', 'dfehc_reset_total_visitors_event');
    548     if (!$ok || is_wp_error($ok)) {
    549         wp_schedule_single_event($ts, 'dfehc_reset_total_visitors_event');
    550     }
    551 }
    552 add_action('init', 'dfehc_ensure_reset_visitors_schedule', 10);
    553 
    554 function dfehc_on_deactivate(): void {
    555     wp_clear_scheduled_hook('dfehc_process_user_activity');
    556     wp_clear_scheduled_hook('dfehc_reset_total_visitors_event');
    557     wp_clear_scheduled_hook('dfehc_cleanup_user_activity');
    558     delete_option('dfehc_activity_cron_scheduled');
    559     delete_option(dfehc_scoped_key('dfehc_activity_backfill_done'));
    560 }
     712    $plugin_file = defined('DFEHC_PLUGIN_FILE') ? (string) DFEHC_PLUGIN_FILE : __FILE__;
     713    register_activation_hook($plugin_file, 'dfehc_on_activate');
     714}
     715
    561716if (function_exists('register_deactivation_hook')) {
    562     register_deactivation_hook(__FILE__, 'dfehc_on_deactivate');
    563 }
    564 
    565 if (defined('WP_CLI') && WP_CLI) {
     717    $plugin_file = defined('DFEHC_PLUGIN_FILE') ? (string) DFEHC_PLUGIN_FILE : __FILE__;
     718    register_deactivation_hook($plugin_file, 'dfehc_on_deactivate');
     719}
     720
     721if (defined('WP_CLI') && WP_CLI && class_exists('\WP_CLI')) {
    566722    \WP_CLI::add_command('dfehc:reset_visitors', static function () {
    567723        dfehc_reset_total_visitors();
  • dynamic-front-end-heartbeat-control/trunk/widget.php

    r3427163 r3461136  
    11<?php
    22function dfehc_enqueue_chart_js() {
     3    if (!is_admin()) {
     4        return;
     5    }
     6
    37    $load_logs = get_option('dfehc_server_load_logs', []);
    48    if (!is_array($load_logs)) {
     
    2933add_action('admin_enqueue_scripts', 'dfehc_enqueue_chart_js');
    3034
     35function dfehc_logs_max_entries(): int {
     36    $n = (int) apply_filters('dfehc_server_load_logs_max_entries', 1500);
     37    return max(50, min(20000, $n));
     38}
     39
     40function dfehc_trim_load_logs(array $logs): array {
     41    $max = dfehc_logs_max_entries();
     42    $count = count($logs);
     43    if ($count <= $max) return $logs;
     44    return array_slice($logs, $count - $max);
     45}
     46
     47function dfehc_update_load_logs_option(array $logs): void {
     48    $logs = array_values($logs);
     49    $logs = dfehc_trim_load_logs($logs);
     50    update_option('dfehc_server_load_logs', $logs, false);
     51}
     52
    3153function dfehc_heartbeat_health_dashboard_widget_function() {
     54    static $memo = null;
     55    if (is_array($memo)) {
     56        echo $memo['html'];
     57        return;
     58    }
     59
     60    $now = time();
     61
    3262    $heartbeat_status = get_transient('dfehc_heartbeat_health_status');
     63
    3364    $server_load = dfehc_get_server_load();
    3465    $server_response_time = dfehc_get_server_response_time();
     
    4576    if (!is_array($load_logs)) $load_logs = [];
    4677
    47     $load_logs[] = ['timestamp' => time(), 'load' => $server_load];
    48 
    49     $now = time();
     78    $log_enabled = (bool) apply_filters('dfehc_widget_log_enabled', true);
     79    $log_sample_rate = (float) apply_filters('dfehc_widget_log_sample_rate', 1.0);
     80    if (!is_finite($log_sample_rate) || $log_sample_rate < 0.0) $log_sample_rate = 0.0;
     81    if ($log_sample_rate > 1.0) $log_sample_rate = 1.0;
     82
     83    $should_log = $log_enabled;
     84    if ($should_log && $log_sample_rate < 1.0) {
     85        $r = function_exists('wp_rand') ? (int) wp_rand(0, 1000000) : (int) (mt_rand(0, 1000000));
     86        $should_log = ((float) $r / 1000000.0) <= $log_sample_rate;
     87    }
     88
     89    if ($should_log) {
     90        $load_logs[] = ['timestamp' => $now, 'load' => $server_load];
     91    }
     92
     93    $snap_key = 'dfehc_server_load_logs_snapshot';
     94    $snap = get_transient($snap_key);
     95    $snap_ttl = (int) apply_filters('dfehc_widget_logs_snapshot_ttl', 60);
     96    if ($snap_ttl < 5) $snap_ttl = 5;
     97    if ($snap_ttl > 600) $snap_ttl = 600;
     98
     99    $save_interval = (int) apply_filters('dfehc_widget_logs_save_interval', 120);
     100    if ($save_interval < 10) $save_interval = 10;
     101    if ($save_interval > 1800) $save_interval = 1800;
     102
     103    $save_lock_key = 'dfehc_server_load_logs_save_lock';
     104    $save_lock_ttl = (int) apply_filters('dfehc_widget_logs_save_lock_ttl', 15);
     105    if ($save_lock_ttl < 5) $save_lock_ttl = 5;
     106    if ($save_lock_ttl > 120) $save_lock_ttl = 120;
     107
     108    $has_snapshot = is_array($snap) && isset($snap['t'], $snap['logs']) && is_array($snap['logs']);
     109    $snap_time = $has_snapshot ? (int) $snap['t'] : 0;
     110    $use_snapshot = $has_snapshot && ($now - $snap_time) >= 0 && ($now - $snap_time) <= $snap_ttl;
     111
     112    $logs_for_chart = $use_snapshot ? $snap['logs'] : $load_logs;
     113
    50114    $clean_logs = [];
    51     foreach ($load_logs as $log) {
     115    $cutoff = $now - 86400;
     116    $upper = $now + 60;
     117
     118    foreach ($logs_for_chart as $log) {
    52119        if (!is_array($log)) continue;
    53120        $ts = isset($log['timestamp']) ? (int) $log['timestamp'] : 0;
    54         if ($ts < ($now - 86400) || $ts > ($now + 60) || $ts <= 0) continue;
     121        if ($ts < $cutoff || $ts > $upper || $ts <= 0) continue;
    55122        $ld = $log['load'] ?? null;
    56123        $ld = is_numeric($ld) ? (float) $ld : 0.0;
    57124        $clean_logs[] = ['timestamp' => $ts, 'load' => $ld];
    58125    }
    59     $load_logs = array_values($clean_logs);
    60     update_option('dfehc_server_load_logs', $load_logs);
     126
     127    $logs_for_chart = array_values($clean_logs);
     128
     129    if (!$use_snapshot) {
     130        set_transient($snap_key, ['t' => $now, 'logs' => $logs_for_chart], $snap_ttl);
     131    }
     132
     133    $should_save = false;
     134    $last_saved = (int) get_option('dfehc_server_load_logs_last_saved', 0);
     135    if ($last_saved < 0) $last_saved = 0;
     136
     137    if ($should_log) {
     138        if ($last_saved === 0 || ($now - $last_saved) >= $save_interval) {
     139            $should_save = true;
     140        }
     141    }
     142
     143    if ($should_save) {
     144        $got_lock = false;
     145        if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_add')) {
     146            $got_lock = wp_cache_add($save_lock_key, 1, 'dfehc', $save_lock_ttl);
     147        } else {
     148            $got_lock = (get_transient($save_lock_key) === false) && set_transient($save_lock_key, 1, $save_lock_ttl);
     149        }
     150
     151        if ($got_lock) {
     152            update_option('dfehc_update_load_logs_option', $logs_for_chart, false);
     153            update_option('dfehc_server_load_logs_last_saved', $now, false);
     154
     155            if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_delete')) {
     156                wp_cache_delete($save_lock_key, 'dfehc');
     157            } else {
     158                delete_transient($save_lock_key);
     159            }
     160        }
     161    }
    61162
    62163    $status = is_string($heartbeat_status) ? $heartbeat_status : 'Stopped';
     
    127228    $ajax_url = function_exists('admin_url') ? (string) admin_url('admin-ajax.php') : '';
    128229    $ajax_nonce = function_exists('wp_create_nonce') ? (string) wp_create_nonce('dfehc_widget_stats') : '';
     230
     231    ob_start();
    129232
    130233    echo "<style>
     
    539642    });
    540643    </script>';
     644
     645    $memo = ['html' => (string) ob_get_clean()];
     646    echo $memo['html'];
    541647}
    542648
     
    546652    }
    547653    check_ajax_referer('dfehc_widget_stats');
     654
     655    $cache_ttl = (int) apply_filters('dfehc_widget_refresh_cache_ttl', 3);
     656    if ($cache_ttl < 0) $cache_ttl = 0;
     657    if ($cache_ttl > 30) $cache_ttl = 30;
     658
     659    $cache_key = 'dfehc_widget_refresh_cache';
     660    if ($cache_ttl > 0) {
     661        $cached = get_transient($cache_key);
     662        if (is_array($cached) && isset($cached['server_load'], $cached['server_response_time'], $cached['recommended_interval'])) {
     663            wp_send_json_success($cached);
     664        }
     665    }
    548666
    549667    $server_load = dfehc_get_server_load();
     
    577695    if (!is_numeric($safe_load) && !is_string($safe_load)) $safe_load = '';
    578696
    579     wp_send_json_success([
     697    $payload = [
    580698        'server_load' => $safe_load,
    581699        'server_response_time' => (float) $response_seconds,
    582700        'recommended_interval' => (float) $recommended_interval,
    583     ]);
     701    ];
     702
     703    if ($cache_ttl > 0) {
     704        set_transient($cache_key, $payload, $cache_ttl);
     705    }
     706
     707    wp_send_json_success($payload);
    584708});
    585709
    586710function dfehc_add_heartbeat_health_dashboard_widget() {
    587711    wp_add_dashboard_widget('heartbeat_health_dashboard_widget', 'Dynamic Heartbeat Health Check', 'dfehc_heartbeat_health_dashboard_widget_function');
    588 
    589712}
    590713add_action('wp_dashboard_setup', 'dfehc_add_heartbeat_health_dashboard_widget');
Note: See TracChangeset for help on using the changeset viewer.