I am trying to perform a ping-speed test using PHP PEAR and im not sure how to add a duration for the ping-speed to end in the CLI you press Q to end the test just not sure how to perform a similar action using the API…
Best,
Patrick.
I am trying to perform a ping-speed test using PHP PEAR and im not sure how to add a duration for the ping-speed to end in the CLI you press Q to end the test just not sure how to perform a similar action using the API…
Best,
Patrick.
You’d need to use $client->sendAsync(), and then cancel the command once you’re ready.
To use a “duration” in the sense of “time”, you can use $client->loop($timeout), but keep in mind that all gathered replies are buffered in the server’s memory until you later read them. You can specify the “interval” argument on “ping-speed” in order to make sure you don’t get more than “interval times timeout” replies.
e.g.
//interval * timeout = ~8 replies
$client->sendAsync(new RouterOS\Request('/tool ping-speed address=192.168.88.100 interval=0.5', null, 'p'));
//Gather everything for 4 seconds
$client->loop(4);
//Cancel the request afterwards... Note that the replies will feature one last !trap due to the cancel, and !done reply
$client->cancelRequest('p');
//Get all data replies for the request
$responses = $client->extractNewResponses('p')->getAllOfType(RouterOS\Response::TYPE_DATA);
//In the case of ping-speed, you probably want the average speed at the end, so...
$avg = $responses->end()->getArgument('average');
//Otherwise, process $responses as you see fit
To use a “duration” in the sense of “number of replies” or some other more artificial factor, you can use a callback inside sendAsync(), and make it return true when you’re ready to cancel. The callback is invoked for each reply and is not buffered, so you can stop processing at any time, as long as you keep track of how long you’ve been calling the callback. Similarly to before, if using interval, you can artificially limit the time if you keep track of the count or vice-versa.
e.g.
$avg = '';
$count = 0;
$client->sendAsync(
new RouterOS\Request('/tool ping-speed address=192.168.88.100 interval=0.5', null, 'p'),
function ($response) use (&$count, &$avg) {
if ($response->getType() === RouterOS\Response::TYPE_DATA) {
$avg = $response->getArgument('average');
return 100 === ++$count;
}
}
);
$client->completeRequest('p');
That did work just how I had wanted… What I am trying to do is perform a speedtest from the mikrotik and return these results using PHP, the ping-speed isn’t giving me results accurate/consistent… im thinking a way to do this would be to fetch a file of 10mb and record how long it takes to fetch the file?
Any input on this would be great and thank you for your help so far.
Patrick.
In my experience, ping-speed is… adequate.
I don’t think you’ll get any significantly more accurate results by clocking a fetch. But if you insist… You can run an exec() for a script that would do that, saves the output in a global variable, that you can then read.
e.g.
//Doing the test and fetching the result
$util = new RouterOS\Util($client);
$util->exec('
:local timezone [/system clock get time-zone-name]
:local startDate [/system clock get date]
:local startTime [/system clock get time]
/tool fetch url=$url
:local endTime [/system clock get time]
:local endDate [/system clock get date]
:global speedTestResults ($timezone . ";" . $startDate . " " . $startTime . ";" . $endDate . " " . $endTime)
',
array(
'url' => 'http://example.com/testfile.dat'
)
);
$util->changeMenu('/system script environment');
$speedTestResults = explode(';', $util->get('speedTestResults', 'value'));
$util->remove('speedTestResults');
//Processing the results
$timezone = new DateTimeZone($speedTestResults[0]);
$startDateTime = DateTime::createFromFormat('M/d/Y H:i:s', ucfirst($speedTestResults[1]), $timezone);
$endDateTime = DateTime::createFromFormat('M/d/Y H:i:s', ucfirst($speedTestResults[2]), $timezone);
$duration = $endDateTime->diff($startDateTime);
//Formatting the result
$seconds = $duration->s + ($duration->i * 60) + ($duration->h * (60 * 60)) + ($duration->a * (24 * 60 * 60));
//Assuming we already know the size in megabytes, and have it in $size
echo 'Average speed is ' . ($size / $seconds) . 'Mbps';
Any input on this would be great and thank you for your help so far.
My pleasure .
Im getting this error?
Fatal error: Uncaught exception 'Exception' with message 'DateTimeZone::__construct() [<a href='datetimezone.--construct'>datetimezone.--construct</a>]: Unknown or bad timezone ()' in
public_html/system/speedtest1.php:39 Stack trace: #0
public_html/system/speedtest1.php(39): DateTimeZone->__construct('') #1 {main} thrown in public_html/system/speedtest1.php on line 39
This looks like exactly the right sort of thing though.
I think the problem with ping speed is finding a consistent IP to ping to? seems to work fine where ever we have a fibre connection but not anywhere else.
Odd… do you have your time zone set at the router? If not, that may be what’s causing issues… All right… To make the code more agnostic towards the timezone, replace
$timezone = new DateTimeZone($speedTestResults[0]);
with
try {
$timezone = new DateTimeZone($speedTestResults[0]);
} catch (Exception $e) {
$timezone = new DateTimeZone('UTC');
}
Could be… I have MikroTik in two networks, but both of them receive internet via a fiber optic connection, so that may be skewing my personal perception of ping-speed’s accuracy.
Think the problem is with the return $speedTestResults getting a null array Array ( [0] => ) which means the return value isn’t good?
OK, I did some more throughout tests, and yes, the problem is that the global variable is not created, but what’s weird is just that - it SHOULD be created but isn’t.
That’s most probably a RouterOS bug… To work around it in the meantime, you can add the script as a scheduler, which I’ve also come to notice doesn’t accept variables from the outer scope, meaning that escaping the URL is also slightly more cumbersome. Also, since the script is executed in the background, and the variable is only created at the end, the PHP script needs to keep sleeping (or whatever…) until the variable is created, i.e. the results as in.
So:
//BTW, ensure we don't halt the test in the middle of it all
set_time_limit(0);
//Do the test
$util = new RouterOS\Util($client);
$util->exec('
/system scheduler add name="SPEED_TEST" interval=2 on-event={
/system scheduler remove "SPEED_TEST"
:local url ' . RouterOS\Util::escapeValue('http://example.com/testfile.dat') . '
:local timezone [/system clock get time-zone-name]
:local startDate [/system clock get date]
:local startTime [/system clock get time]
/tool fetch url=$url
:local endTime [/system clock get time]
:local endDate [/system clock get date]
:global speedTestResults ($timezone . ";" . $startDate . " " . $startTime . ";" . $endDate . " " . $endTime)
}
'
);
//Wait for the results
$util->changeMenu('/system script environment');
do {
sleep(2);
} while ('' == ($speedTestResultsValue = $util->get('speedTestResults', 'value')));
$util->remove('speedTestResults');
//Processing the results
$speedTestResults = explode(';', $speedTestResultsValue);
$timezone = new DateTimeZone('(manual)' === $speedTestResults[0] ? 'UTC' : $speedTestResults[0]);
$startDateTime = DateTime::createFromFormat('M/d/Y H:i:s', ucfirst($speedTestResults[1]), $timezone);
$endDateTime = DateTime::createFromFormat('M/d/Y H:i:s', ucfirst($speedTestResults[2]), $timezone);
$duration = $endDateTime->diff($startDateTime);
//Formatting the result
$seconds = $duration->s + ($duration->i * 60) + ($duration->h * (60 * 60)) + ($duration->days * (24 * 60 * 60));
//Assuming we already know the size in megabytes, and have it in $size
echo 'Average speed is ' . ($size / $seconds) . 'Mbps';
(You’ll also notice two other small errors… $duration->a should be “->days” instead, and the only “real” timezone to worry about is “(manual)” that I’ve now special cased)
Been testing this out for several hours now, found a few issues and possibly improvements.
Getting the date / timezone seemed un needed as all we want is the difference in seconds so I’ve modified your original method…
//BTW, ensure we don't halt the test in the middle of it all
set_time_limit(0);
//Do the test
$util = new RouterOS\Util($client);
$util->exec('
/system scheduler add name="SPEED_TEST" interval=2 on-event={
/system scheduler remove "SPEED_TEST"
:local url ' . RouterOS\Util::escapeValue('http://flux.realmcloud.com/system/speedtest.dat') . '
:local startTime [/system clock get time]
/tool fetch url=$url
:local endTime [/system clock get time]
:global speedTestResults ($startTime . ";" . $endTime)
}
'
);
//Wait for the results
$util->changeMenu('/system script environment');
do {
sleep(2);
} while ('' == ($speedTestResultsValue = $util->get('speedTestResults', 'value')));
$util->remove('speedTestResults');
//Processing the results
$speedTestResults = explode(';', $speedTestResultsValue);
$startTime = strtotime($speedTestResults[0]);
$endTime = strtotime($speedTestResults[1]);
$seconds = ($endTime - $startTime) -1;
$size = 14680064;
$value = ($size / $seconds);
echo formatBytes($value, 0);
I also realised that the value I was getting was MBps now Mbps so have wrote a function to get round this. This does however mean you have to give your file “$size” in bytes. And that the file takes about a second to initiate (API request, mikrotik responding to fetch etc…) so i took a second off the difference.
function formatBytes($bytes, $precision) {
$units = array('bps', 'Kbps', 'Mbps', 'Gbps', 'Tbps');
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision)*8 . ' ' . $units[$pow];
}
I think the biggest issue I’m having is to get a consistent result you either need to have a very large file or a more precise time returned in (ms).
i.e. if you have a 100mbps fibre line and a 5MB file its always going to download in a second which returns 5/1 = 5.
Not really sure if there is something obvious I’m missing or not. Keen to hear your thoughts.
Very impressive PHP API didn’t realise it was capable of doing such awesome stuff, great work!
The primary reason I included the date was if you happen to run the test near midnight (as, in fact, I actually did my tests on…) and I included the timezone in case you run the test when the timezone is on the edge of a DST shifting hour (e.g. you start it at 3:59:58, but it passed over to 4:00:00, which forces the clock to 3:00:00, so in turn the test “ends” in 3:00:01) - the DateTime class handles all of these quirks. Otherwise, the seconds you get are totally messed up, in turn leading to obviously false results.
And that the file takes about a second to initiate (API request, mikrotik responding to fetch etc…) so i took a second off the difference.
I don’t think the inaccuracy is about the API request - after all, the test itself, as well as both measures, run outside of it. That’s in fact why both time measures are inside the script - to prevent this offset.
The inaccuracy may be due to the DNS request (redeemable by doing a “:resolve” before the test), coupled with the file’s HTTP headers, which are a few additional bytes (sometimes adding up even to a kilobyte) that the router needs to download in addition to the file itself. Sadly, RouterOS doesn’t support HEAD HTTP requests… If it did, you could measure THAT request, and subtract the difference from the GET request to get the time that’s solely for the file itself.
a very large file or a more precise time returned in (ms).
Yeah, I don’t know of any way to do that in RouterOS. And as mentioned, doing that on PHP’s side, while possible, will yield even worse results due to the API request itself being a source of inaccuracy.
i.e. if you have a 100mbps fibre line and a 5MB file its always going to download in a second which returns 5/1 = 5.
You can check if the speed is greater than or equal to the size, and report to the user that their speed is “at least” that.
Not really sure if there is something obvious I’m missing or not. Keen to hear your thoughts.
Here’s an idea… How about using ping-speed, but instead of taking the average, you take the median? I mean, it seems to me the reason ping-speed’s average feels off is due to fluxuations from the speed that you experience “most often” during the test i.e. the median.
To get the median, use the very first approach in this topic, but process $responses as follows:
$speeds = array();
foreach ($responses as $response) {
$speeds[] = $response('current');
}
//The speeds need to be sorted... natsort should do nicely, unless perhaps there's both Mbps and Gbps during the test...
natsort($speeds);
$medianPos = (int) (count($speeds) / 2);
if (count($speeds) % 2 === 0) {
//No exact median; we take the average of the nearest two samples
$median = ($speeds[$medianPos] + $speeds[$medianPos - 1]) / 2;
} else {
$median = $speeds[$medianPos];
}
echo 'Median speed is ' . $median;
You can tweak the median calculations a bit to take an average out of the values around the median even when an exact median is available. Of course, the more values you take, the closer you become to the total average, so the ideal will be a “trial & error” exercise.
Very impressive PHP API didn’t realise it was capable of doing such awesome stuff, great work!
Util in particular was made to give off that “I feel like a ninja” (read: awesome) smell , hehe. Thanks.
The primary reason I included the date was if you happen to run the test near midnight (as, in fact, I actually did my tests on…) and I included the timezone in case you run the test when the timezone is on the edge of a DST shifting hour (e.g. you start it at 3:59:58, but it passed over to 4:00:00, which forces the clock to 3:00:00, so in turn the test “ends” in 3:00:01) - the DateTime class handles all of these quirks. Otherwise, the seconds you get are totally messed up, in turn leading to obviously false results.
My bad, ill add it back in.
DNS request (redeemable by doing a “:resolve” before the test)
I think this is a really valid point. As the test works fantastically for our fibre lines which resolve addresses in no time but our copper lines there seems to be a bit of lag on.
You can check if the speed is greater than or equal to the size, and report to the user that their speed is “at least” that.
If they are running a line that can download 11MB in under a second then 100mbps or more.
Here’s an idea… How about using ping-speed, but instead of taking the average, you take the median? I mean, it seems to me the reason ping-speed’s average feels off is due to fluxuations from the speed that you experience “most often” during the test i.e. the median.
I did try this with mixed results… unfortunately the ping-speed tool doesn’t work when a mikrotiks behind certain routers. Also i don’t know how it actually works (process) despite having read the documentation.
Util in particular was made to give off that “I feel like a ninja” (read: awesome) smell >
> , hehe. Thanks.
You give it off by the boat load!
Feel like were extremely close to this making the news. haha
I think I have found another possible way.
The Max ether1 traffic when the file is downloaded is almost always the speed of the line… If its doable to get median of 3 highest values on the ethernet port? (which is always ether1)…
maybe…
This would also include connected clients which is ideal.
I did try this with mixed results… unfortunately the ping-speed tool doesn’t work when a mikrotiks behind certain routers. Also i don’t know how it actually works (process) despite having read the documentation.
AFAIK, ping-speed works just like a normal ping, in that it sends ICMP echo messages… But unlike “ping”, which waits for a packet’s return before sending the next one, ping-speed sends different ICMP echo messages at once, and measures the round trip for the total data, rather than individual packet’s delay.
The Max ether1 traffic when the file is downloaded is almost always the speed of the line… If its doable to get median of 3 highest values on the ethernet port? (which is always ether1)…
A median out of a sample of 3?!? That’s hardly a median… You may as well average those…
And you could do that easily like:
$speeds = array();
foreach ($responses as $response) {
$speeds[] = $response('current');
}
natsort($speeds);
$highestAvg = array_sum(array_slice($speeds, -3)) / 3;
I’m not certain if the results would be any better, but I guess it’s worth a shot.
I don’t know how to get the ethernet data using the API once the file download has started.
almost too simple
Eh? What do you mean by that? What would be the console equivalent? Or… wait, what?
Im not sure i know how to get the info in webfig /interface ethernet ether1…
Get what info? The link speed (10Mbps, 100Mbps, 1Gbps)? That’s trivial:
$util->changeMenu('/interface ethernet');
$linkSpeed = $util->get('ether1', 'speed');
But that only tells you the maximum speed that can go through that interface, not the actual one.
Or do you… aaahhhh… I get it now… you want to start the download and during the download, monitor the current speed on the interface?
Right… To get the current speed, it’s best to use “/interface monitor interface=ether1”, and collect its responses until the test is completed (as indicated by the presence of the global variable).
So… after the exec(), something like:
$client->sendAsync(new RouterOS\Request('/interface monitor interface=ether1 interval=0.25', null, 'm'));
//Wait for the results
$util->changeMenu('/system script environment');
do {
//Collect some speed samples during the wait
$client->loop(2);
} while ('' == ($speedTestResultsValue = $util->get('speedTestResults', 'value')));
$responses = $client->extractNewResponses('m');
//Clean up
$util->remove('speedTestResults');
$client->cancelRequest('m');
$client->completeRequest('m');
//$responses will then contain the download speeds in the... I believe "rx-bits-per-second" property, so to get the max of those
$speeds = array();
foreach ($responses as $response) {
$speeds[] = $response('rx-bits-per-second');
}
natsort($speeds);
$max = $speeds[count($speeds)-1];
But keep in mind that this approach may give overestimates if the download of the particular file is slow but the full link (i.e. the download, plus another one that may be occurring in the mean time) has a higher limit.
(We use the interval argument to prevent the web server’s memory from going berserk… I think 4 samples per second should be more than enough, hence 0.25 interval)
Im getting this error, not too sure how to go about debugging…
Fatal error: Uncaught exception 'PEAR2\Net\RouterOS\DataFlowException' with message 'No such request. Canceling aborted.' in phar:///var/sites/f/url/public_html/system/PEAR2_Net_RouterOS-1.0.0b4.phar/PEAR2_Net_RouterOS-1.0.0b4/src/PEAR2/Net/RouterOS/Client.php:635 Stack trace: #0 /var/sites/f/url/public_html/system/testbed.php(47): PEAR2\Net\RouterOS\Client->cancelRequest('m') #1 {main} thrown in phar:///var/sites/f/url/public_html/system/PEAR2_Net_RouterOS-1.0.0b4.phar/PEAR2_Net_RouterOS-1.0.0b4/src/PEAR2/Net/RouterOS/Client.php on line 635
guess its something to do with this? the command works fine in ssh so must just syntax.
$client->cancelRequest('m');
$client->completeRequest('m');
That’s kind’a odd… I thought I had made it so that a cancel keeps responses collected thus far, and that they remain until you extract the remaining ones, either with extractNewResponses() or completeRequest(). I guess I’ll have to re-test those edge cases.
Oh well… I guess just remove the line
$client->completeRequest('m');
then.
EDIT: OK, I re-read the reference docs (that I wrote… talk about a note to self…), and it appears this was the intended behavior. Come to think of it, I made it that way exactly in order to avoid the somewhat unnecessary call to completeRequest(). On the one hand, if there were additional replies between the last extractNewResponse() and cancelRequest(), you’re loosing them, but on the other hand, if you’re doing a cancelRequest(), you probably don’t care for them anyway (if you did, you’d extract them prior to the cancel call), so cancelRequest() was made to clear the buffer, and free the tag, so that you could then possibly create a new request with that same tag.