zapoi has asked for the wisdom of the Perl Monks concerning the following question:

Dear gracious ones,

I'm trying to write a loop that will call a function no more frequently than every 0.1 seconds. The function has an embedded API call so I can't be certain how long it will take to return but typically about 0.03 seconds. I currently do this by waiting 0.07 seconds but this isn't what I require. I want my loop to wait until exactly 0.1 seconds since last execution. I require the data from the last execution so if the last execution took 0.2s then I need to wait for it to complete, I can't concurrently call the function

Any ideas?

Thank you

Zapoi
  • Comment on Call function no more than every 0.1 seconds

Replies are listed 'Best First'.
Re: Call function no more than every 0.1 seconds
by choroba (Cardinal) on Aug 11, 2020 at 23:29 UTC
    A similar problem was discussed in The most precise second (timer).

    If you don't need to be almost precise, something like this might be good enough:

    #! /usr/bin/perl use warnings; use strict; use feature qw{ say }; use Time::HiRes qw{ time sleep }; sub func { sleep rand 0.05 } my $t0 = 0; while (1) { if (time - $t0 >= 0.1) { say $t0 = time; func(); } }
    On my machine, it shows:
    1597188470.0211 1597188470.1211 1597188470.2211 1597188470.3211 1597188470.4211 1597188470.52111 1597188470.62111 1597188470.72111 1597188470.82111 1597188470.92111 1597188471.02111 1597188471.12111 1597188471.22112 1597188471.32112 1597188471.42112 1597188471.52112 1597188471.62112 1597188471.72112 1597188471.82113

    map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]
    A reply falls below the community's threshold of quality. You may see it by logging in.
Re: Call function no more than every 0.1 seconds
by marioroy (Prior) on Aug 12, 2020 at 07:12 UTC

    Greetings, zapoi

    I tried serial and parallel demonstrations that doesn't involve a busy CPU loop while waiting for the next time period. The resolution is near 1 millisecond. The monotonic clock is used if available.

    Serial code:

    use strict; use warnings; package Interval { use Time::HiRes; use constant HAS_CLOCK_MONOTONIC => eval { # use monotonic clock if available Time::HiRes::clock_gettime( Time::HiRes::CLOCK_MONOTONIC() ); 1; }; sub new { my ($class, $delay) = @_; return bless [ $delay, undef ], $class; } sub yield { my ($self) = @_; my ($delay, $lapse) = @{ $self }; my $time = HAS_CLOCK_MONOTONIC ? Time::HiRes::clock_gettime( Time::HiRes::CLOCK_MONOTONIC() ) : Time::HiRes::time(); if (!$delay || !defined $lapse) { $lapse = $time; } elsif ($lapse + $delay - $time < 0) { $lapse += int(abs($time - $lapse) / $delay + 0.5) * $delay; } $self->[1] = ($lapse += $delay); Time::HiRes::usleep(($lapse - $time) * 1e6); return; } } use Time::HiRes qw{ sleep time }; my $interval = Interval->new(0.1); my $last_value = 0; sub func { my ($last_value, $seq) = @_; printf "%0.3f $last_value $seq\n", time; sleep 0.1 if $seq == 3; # simulate extra time return $seq; } for my $seq (1..20) { # delay until the next time period $interval->yield; my $value = func($last_value, $seq); $last_value = $value; } __END__ 1597215036.077 0 1 1597215036.177 1 2 1597215036.277 2 3 # 0.1 gap between sequence 3 and 4 due to 3 taking +extra time 1597215036.477 3 4 1597215036.577 4 5 1597215036.677 5 6 1597215036.777 6 7 1597215036.877 7 8 1597215036.978 8 9 1597215037.078 9 10 1597215037.177 10 11 1597215037.277 11 12 1597215037.377 12 13 1597215037.476 13 14 1597215037.578 14 15 1597215037.677 15 16 1597215037.777 16 17 1597215037.878 17 18 1597215037.977 18 19 1597215038.077 19 20

    Parallel code:

    MCE::Relay makes it possible to obtain the value for the last execution. There is MCE->yield but was not able to use successfully with MCE::relay due to yield being computed by worker ID and relay by chunk ID internally. I will resolve this in the next MCE release. The MCE interval option (not shown here) and MCE->yield is how one throttles workers.

    Here, the Interval class is updated to support time intervals among many workers (i.e. $lapse is sent and received using a channel).

    use strict; use warnings; package Interval { use Time::HiRes; use MCE::Channel; use constant HAS_CLOCK_MONOTONIC => eval { # use monotonic clock if available Time::HiRes::clock_gettime( Time::HiRes::CLOCK_MONOTONIC() ); 1; }; sub new { my ($class, $delay) = @_; my $chnl = MCE::Channel->new; $chnl->send( undef ); return bless [ $delay, $chnl ], $class; } sub yield { my ($delay, $chnl) = @{ $_[0] }; my $lapse = $chnl->recv; my $time = HAS_CLOCK_MONOTONIC ? Time::HiRes::clock_gettime( Time::HiRes::CLOCK_MONOTONIC() ) : Time::HiRes::time(); if (!$delay || !defined $lapse) { $lapse = $time; } elsif ($lapse + $delay - $time < 0) { $lapse += int(abs($time - $lapse) / $delay + 0.5) * $delay; } $chnl->send($lapse += $delay); Time::HiRes::usleep(($lapse - $time) * 1e6); return; } } use MCE; use Time::HiRes qw{ sleep time }; my $interval = Interval->new(0.1); sub func { my ($last_value, $seq) = @_; printf "%0.3f $last_value $seq\n", time; sleep 0.1 if $seq == 3; # simulate extra time return $seq; } MCE->new( max_workers => 4, chunk_size => 1, sequence => [ 1, 20 ], init_relay => 0, user_func => sub { my $seq = $_; # wait for data from the last execution # this value will be 0 for the worker processing MCE->chunk_id 1 my $last_value = MCE->relay_recv; # delay until the next time period $interval->yield; my $value = func($last_value, $seq); # relay data to the next worker MCE::relay { $_ = $value }; } )->run; __END__ 1597215117.883 0 1 1597215117.983 1 2 1597215118.083 2 3 # 0.1 gap between sequence 3 and 4 due to 3 taking +extra time 1597215118.283 3 4 1597215118.382 4 5 1597215118.482 5 6 1597215118.583 6 7 1597215118.683 7 8 1597215118.783 8 9 1597215118.882 9 10 1597215118.983 10 11 1597215119.082 11 12 1597215119.182 12 13 1597215119.283 13 14 1597215119.383 14 15 1597215119.483 15 16 1597215119.583 16 17 1597215119.683 17 18 1597215119.782 18 19 1597215119.883 19 20

    Regards, Mario

Re: Call function no more than every 0.1 seconds
by Tux (Canon) on Aug 12, 2020 at 08:49 UTC

    Using only CORE functionality:

    use 5.14.0; use warnings; use Time::HiRes qw( ualarm gettimeofday usleep ); my $delay = 100_000; # us my $x = 0; sub foo { my $duration = rand 1200; # Some will be longer than the max durat +ion my ($sec, $msec) = gettimeofday; printf "%4d %12d.%03d %6d\n", $x++, $sec, int ($msec / 1000), $dur +ation; vec (my $v, 13, 1) = 0; select ($v, $v, $v, $duration / 10000.); } # foo my $running = 0; $SIG{ALRM} = sub { ualarm ($delay); unless ($running) { $running = 1; foo (); $running = 0; } }; ualarm (10); usleep (10) while 1;

    -->

    $ perl test.pl 0 1597222014.930 430 1 1597222015.030 543 2 1597222015.131 362 3 1597222015.231 1059 4 1597222015.337 929 5 1597222015.437 611 6 1597222015.537 1027 7 1597222015.640 396 8 1597222015.740 243 9 1597222015.840 1095 10 1597222015.950 710 11 1597222016.050 674 12 1597222016.150 695 13 1597222016.250 842 14 1597222016.350 901 15 1597222016.450 468 16 1597222016.550 568 17 1597222016.650 322 18 1597222016.750 633 19 1597222016.850 194 20 1597222016.950 1141 21 1597222017.064 365 22 1597222017.165 226 23 1597222017.265 1162 24 1597222017.381 998 25 1597222017.481 543 26 1597222017.581 184 27 1597222017.681 197 28 1597222017.781 431 29 1597222017.881 1081 30 1597222017.990 644 31 1597222018.090 49 32 1597222018.190 500 33 1597222018.290 1137 34 1597222018.404 171 35 1597222018.504 995 36 1597222018.604 847 37 1597222018.704 998 38 1597222018.804 262 39 1597222018.904 1043 40 1597222019.009 292 41 1597222019.109 668 42 1597222019.209 805 43 1597222019.309 886 44 1597222019.409 361 45 1597222019.509 676 ^C

    Enjoy, Have FUN! H.Merijn
Re: Call function no more than every 0.1 seconds
by Fletch (Bishop) on Aug 12, 2020 at 04:16 UTC

    Possibly overkill for this situation but in a more general case an event loop like POE (or AnyEvent or EV or Mojo::IOLoop or . . .) might be useful (especially if you're going to need to handle other timing, or react to outside IO events (network traffic, IO from files, . . .). You'd call your routine in an event handler, then after it returns request to be delivered the same event to yourself after the requisite delay (starting things off immediately sending yourself the event).

    Edit: Mojo version.

    #!/usr/bin/env perl use 5.032; use Mojo; use Mojo::IOLoop; use Mojo::Log; my $log = Mojo::Log->new; my $interval = 0.25; my $ctr = 1; sub callback { my $loop = shift; $log->debug( qq{: Timer, counter } . $ctr++ ); $loop->timer( $interval => \&callback ) if $ctr <= 5; } Mojo::IOLoop->timer( $interval => \&callback ); Mojo::IOLoop->start unless Mojo::IOLoop->is_running; exit 0; __END__

    The cake is a lie.
    The cake is a lie.
    The cake is a lie.

Re: Call function no more than every 0.1 seconds
by jcb (Parson) on Aug 12, 2020 at 02:48 UTC

    The simple solution:

    • read the clock before calling the function, add your desired interval to this value and call it "goaltime"
    • call the function, wait for it to return
    • read the clock again, compute the time remaining between "now" and "goaltime"
    • delay for that amount of time

    What to do with "missed" samples is up to you. The obvious choices are to start the next sample immediately and to calculate the next desired sampling time and resume on an interval boundary. Which of these you want depends on what you are doing with the data.

      I like this idea. This Is like a "throttle" to reduce number of requests per second.
      Sounds pretty good to me. But be aware that execution will not happen on .1 sec intervals.

        It will get very close. The delay is calculated after the work for "this" cycle has been done. The accuracy is limited by the accuracy of whatever you use to implement the delay, of course, and the system scheduler might preempt your process, but this can be limited by keeping the load reasonable. As I understand, a load average less than the number of processors means that all tasks are (on average) being run whenever they are not blocked.

Re: Call function no more than every 0.1 seconds
by haukex (Archbishop) on Aug 12, 2020 at 20:12 UTC

    From "The most precise second (timer)", which choroba linked to, see my post here which demonstrates clock_nanosleep from Time::HiRes.

    You don't mention what's supposed to happen when your function happens to take longer to execute than 0.1 seconds?

    use warnings; use strict; use Time::HiRes qw/ clock_gettime clock_nanosleep CLOCK_REALTIME TIMER_ABSTIME /; die "Don't have nanosleep" unless Time::HiRes::d_nanosleep(); use Time::HiRes qw/usleep/; # just for this fake "myfunc" sub myfunc { printf "fired %.2f\n", clock_gettime(CLOCK_REALTIME); my $delay_us = int(rand 100000)+10000; # 110000us = 0.11s printf "delay %.2fs\n", $delay_us/1e6; usleep $delay_us; } my $next_s = int(clock_gettime(CLOCK_REALTIME))+1; my $interval_s = 0.1; my $run = 1; $SIG{INT} = sub { $run = 0 }; while ($run) { my $now_s = clock_gettime(CLOCK_REALTIME); $next_s += $interval_s while $next_s < $now_s; clock_nanosleep(CLOCK_REALTIME, $next_s*1e9, TIMER_ABSTIME); myfunc(); }

    Update: Note that since the above code uses floating-point numbers there's a small chance for errors there. If that's a concern, you can switch to nanoseconds, since that's what clock_nanosleep takes anway.

Re: Call function no more than every 0.1 seconds
by zapoi (Novice) on Aug 12, 2020 at 21:27 UTC
    Thank you for your most detailed replies and explanations. I've implemented Choroba's version as I can handle slight slippageas the most important thing is not to over-throttle the api I'm calling.
Re: Call function no more than every 0.1 seconds
by jmlynesjr (Deacon) on Aug 12, 2020 at 01:21 UTC

    I'm not sure how you do it in Perl, but in the Arduino world you read the system time, add the desired time period, call the function, and loop until the system time matches the saved time.

    This extends the Setup()/Loop() abstraction with pseudo tasking.

    James

    There's never enough time to do it right, but always enough time to do it over...

      in the Arduino world you read the system time, add the desired time period, call the function, and loop until the system time matches the saved time.

      Well, that's one way to do it, but IMHO not the most efficient. The generic way to do this with a microcontroller would be to use one of the uC's timers, typically using a 32.768 kHz crystal as the clock source, set up one of the timer's compare units to trigger an interrupt when the timer value matches the user-configured interval (in other words, have the hardware do what you're describing doing in software), and put the uC to sleep or do other things until the interrupt fires. For example, on the Arduino Uno, one could use the ATmega328P's Timer2.

      Minor edits.

        Since I came out of the real-time software world, I would use a processor timer interrupt as you pointed out. However, most of the Arduino world wouldn't understand event driven code if they tripped over it. They use delay() which is blocking - a bad idea.

        I participated in the Open Source Ventilator Project in connection with the University of Florida. The Delay() loop people won out and ran off a contributor that had developed an event driven design because they didn't understand it and didn't want to learn. The end result was a working $200 ventilator, out for manufacture, with some really nasty delay() loops. Good luck with maintenance...

        James

        There's never enough time to do it right, but always enough time to do it over...