#!/usr/bin/perl

=encoding utf8

=head1 NAME

    sleep_under_load

=head1 DESCRIPTION

    Скрипт, делающий sleep на время, зависящее от нагрузки на машину.
    Чем выше нагрузка (больше la1, меньше свободной оперативной памяти),
        тем на бОльшее время скрипт сделает уснет.

    Базовое максимальное время сна и доля "разброса" регулируются константами.

=head1 SYNOPSIS

    /usr/local/bin/sleep_under_load ; command_to_execute

=cut


use warnings;
use strict;

use List::Util qw/max min/;
use Time::HiRes 'sleep';

=head2 CONSTS

=over 4

=item SLEEP_BASE

    Базовое время сна при максимальной нагрузке.

=item STDDEV_COEFF

    Доля от вычисленного времени сна, на которую будет рандомизован результат.

=back

=cut

use constant {
    MEDIAN => 0,
    PI => 4 * atan2(1, 1),
    SLEEP_BASE => 20,
    STDDEV_COEFF => 0.1,
};

my $VERBOSE = $ENV{DEBUG} ? 1 : 0;

sleep(get_sleep_time());
exit 0;

sub _str {
    return (shift // 'undef');
}

sub _get_free_mem {
    my $mem_free;

    if(open(my $fh, '<', '/proc/meminfo')) {
        my %meminfo;
        while (my $line = <$fh>) {
            if ($line =~ m/^(MemTotal|MemFree|Buffers|Cached):\s+(\d+)\s+kB$/) {
                $meminfo{$1} = $2;
            }
        }
        close($fh) or print STDERR "Error closing fh for /proc/meminfo: $!\n";

        if (defined $meminfo{MemTotal} && $meminfo{MemTotal}
            && defined $meminfo{MemFree}
            && defined $meminfo{Buffers}
            && defined $meminfo{Cached}
        ) {
            $mem_free = min(1, ($meminfo{MemFree} + $meminfo{Cached} + $meminfo{Buffers}) / $meminfo{MemTotal});
        } else {
            printf STDERR "Error parsing meminfo! MemTotal: %s; MemFree: %s; Buffers: %s; Cached: %s.\n",
                _str($meminfo{MemTotal}), _str($meminfo{MemFree}), _str($meminfo{Buffers}), _str($meminfo{Cached});
        }
    } else {
        print STDERR "Error opening /proc/meminfo: $!\n";
    }

    $mem_free //= 0;

    if ($VERBOSE) {
        printf STDERR "_get_free_mem: mem_free: %.2f\n", $mem_free;
    }

    return $mem_free;
}

sub _get_cpu_cnt {
    my $cpu_cnt;

    if(open(my $fh, '<', '/proc/cpuinfo')) {
        while (my $line = <$fh>) {
            if ($line =~ m/^processor\s+:/) {
                $cpu_cnt++;
            }
        }
        close($fh) or print STDERR "Error closing fh for /proc/cpuinfo: $!\n";
    } else {
        print STDERR "Error opening /proc/cpuinfo: $!\n";
    }

    return $cpu_cnt;
}

sub _get_la_1 {
    my $la1;

    if(open(my $fh, '<', '/proc/loadavg')) {
        if (<$fh> =~ m/^(\d+\.\d{2})\s/) {
            $la1 = $1;
        } else {
            print STDERR "Error parsing /proc/loadavg\n";
        }
        close($fh) or print STDERR "Error closing fh for /proc/loadavg: $!\n";
    } else {
        print STDERR "Error opening /proc/loadavg: $!\n";
    }

    return $la1;
}

sub _get_free_cpu {
    my $cpu_free;
    my $cpu_cnt = _get_cpu_cnt();
    my $la1 = _get_la_1();
    if (defined $cpu_cnt && $cpu_cnt > 0
        && defined $la1
    ) {
        $cpu_free = max(0, 1 - $la1 / $cpu_cnt);
    } else {
        printf STDERR "Error parsing cpu info. CPUs: %s; la1: %s.\n",
            _str($cpu_cnt), _str($la1);
    }

    $cpu_free //= 0;

    if ($VERBOSE) {
        printf STDERR "_get_free_cpu: la1: %s, cpu_cnt: %s, cpu_free: %.2f\n",
            _str($la1), _str($cpu_cnt), $cpu_free;
    }

    return $cpu_free;
}

sub _get_normal_distributed_random {
    my ($mean, $stddev) = @_;
    $mean //= 0;
    $stddev //= 1;

    # используем преобразование Бокса-Мюллера для получения
    # нормально распределенных случайных чисел из равномерно распределенных
    return $mean + $stddev * cos(2 * PI * (1 - rand())) * sqrt(-2 * log(1 - rand()));
}

sub get_sleep_time {
    my $base_time = SLEEP_BASE * (1 - _get_free_mem() * _get_free_cpu());
    my $rand_time = _get_normal_distributed_random(MEDIAN, STDDEV_COEFF * $base_time);

    # ограничиваем случайную составляющую времени максимальным базовым значением
    if ($rand_time > SLEEP_BASE) {
        $rand_time = SLEEP_BASE;
    }

    my $sleep_time = $base_time + $rand_time;
    if ($sleep_time < 0) {
        $sleep_time = 0;
    }

    if ($VERBOSE) {
        printf STDERR "get_sleep_time: base_time: %2.2f, rand_time: %2.2f, sleep_time: %2.2f\n", $base_time, $rand_time, $sleep_time;
    }

    return $sleep_time;
}
