Re: Call function no more than every 0.1 seconds
by choroba (Cardinal) on Aug 11, 2020 at 23:29 UTC
|
#! /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]
| [reply] [d/l] [select] |
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
| [reply] [d/l] [select] |
Re: Call function no more than every 0.1 seconds
by Tux (Canon) on Aug 12, 2020 at 08:49 UTC
|
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
| [reply] [d/l] [select] |
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.
| [reply] [d/l] |
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.
| [reply] |
|
|
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.
| [reply] |
|
|
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.
| [reply] |
Re: Call function no more than every 0.1 seconds
by haukex (Archbishop) on Aug 12, 2020 at 20:12 UTC
|
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. | [reply] [d/l] [select] |
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.
| [reply] |
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...
| [reply] |
|
|
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.
| [reply] |
|
|
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...
| [reply] |
|
|
|
|
|