Wednesday, February 6, 2019

No more calculation babysitting!

Let's imagine you work with a computer on a daily basis (which, as it were, is a rather common scenario these days), and let's imagine your work includes "relatively lengthy" computational tasks -- lengthy enough that it is not productive to just sit there staring at the proverbial hourglass cursor (or meditating over the progress bar, or praying over the build output console window that it compiles without errors, or whatever it is). So...

So, after some time waiting (depending on your boredom threshold, about 30 seconds for me, I'd guess under 2 minutes for most people) you decide to multitask and switch to another task (work-related or otherwise). Before long, that other task immerses you and you realize that your lengthy computation was actually done minutes ago and you could have, and should have, resumed your workflow earlier. So in an attempt to avoid doing nothing and increase productivity, you just dumped it down the tubes.

Or consider another scenario. Your computation is now lengthy enough (say, 10-20 minutes) so that you decide to grab a coffee from the kitchen, or grab a quick bite/smoke/chat, or whatever it is. It would be cool if you could get an alert that your task has finished, so you can timely return and resume work without having to check on your workstation multiple times.

Or yet another scenario. You need to run an even lengthier calculation, perhaps a few hours on end, so you leave it running after hours. And you have several of them to run after each other, and you want to run as many as you can before the next working day. So again it would be cool if you can get an alert once your calculation finishes, rather than having to log in and check on the computation  progress repeatedly. More importantly, if your task is aborted early due to some mishap (which happened to me due to my own banana fingers more times than I'd confess), you really want to be alerted at once, rather than after what you thing the task should have taken, had it completed normally.

The first scenario can partially be mitigated by running Windows Task Manager, minimizing it, and noticing when the CPU usage drops, indicating that your task has finished. But you still need to remain vigilant and keep watching that tiny indicator, and with current multicore CPUs, the drop may be from 13% to some 2%, which is not very noticeable visually. Not to mention that the two remaining scenarios cannot be worked around in this way.

Wouldn't it be nice to have an automated monitor which can do this for you?

Yeah it sure would.

So let's see how we can do it in Windows Powershell. You can determine the CPU usage of a process using this function (loosely adapted from here):

function get-excel-CPU ($avgs=1)
{
 $result=0
 for ($i=1; $i -le $avgs; $i++)
 {
  $cpuinfo = Get-WmiObject Win32_PerfFormattedData_PerfProc_Process -filter "Name LIKE '%EXCEL%'"
  $result=$result + ($cpuinfo.PercentProcessorTime |Measure -max).Maximum
  start-sleep -m 150
 }
 $result = $result/$avgs
 $result
}

Note the following:

  • I use EXCEL as an example because I use it most often. It is trivial to modify it to work with any other program.
  • The function takes into account that there can be multiple instances of your program, and will report the CPU usage of the most CPU intensive process. Usually, this is what you want, because only one of your instances will be doing computations anyway, but you can easily fine-tune it to be more instance-specific.
  • The function measures CPU usage several ($avgs) times with a short waiting period in between, and then averages the measurement. This is done because some computations, mostly ones heavily on local or network I/O, will have wildly fluctuating CPU usage, so taking one measurement may trick the script into falsely deciding that the computation has finished. You may need to fine-tune the number of measurements and the wait time between them to reflect your specific computation pattern.

Now that we have a way to automatically determine the CPU usage, we can easily wrap it in a control loop:

$poll = 10
$homethresh = 600
$sensitivity = 5
$fullout = new-timespan -Seconds 86400
$mailout = new-timespan -Seconds $homethresh
$sw = [diagnostics.stopwatch]::StartNew()

$thiscpu = get-excel-cpu 5

if ($thiscpu -lt $sensitivity) 
{
 write-host $thiscpu, ": Excel not running, exiting."
}
else
{
 write-host "Initial CPU is ", $thiscpu
 $cnt=0
 while ($sw.elapsed -lt $fullout)
 {
  start-sleep -s $poll
  $thiscpu = get-excel-cpu 3
  $lock = is-locked
  write-host "Elapsed", $sw.elapsed, " -- CPU is ", $thiscpu, "   ",$lock
  if ($thiscpu -lt $sensitivity) {$cnt++} else {$cnt=0}
  if ($cnt -ge 2)
   {
    write-host "FINISHED!!!!"
     if ($lock -eq "UNLOCKED") {show-splash} else {phone-home}
    return
   }
 }  
 write-host "Timed out!"
}

Note the line if (...) {show-splash} else {phone-home} . There are two ways to alert you that the computation has finished. One is to show you a big splash screen, borrowed from here:

function show-splash 
{
 Add-Type -AssemblyName System.Windows.Forms
 $Form = New-Object system.Windows.Forms.Form
  $Form.Text = "Finished"
  $Form.AutoSize = $True
  $Form.AutoSizeMode = "GrowAndShrink"
  $Form.BackColor = "Lime"
  $Font = New-Object System.Drawing.Font("Arial",96,[System.Drawing.FontStyle]::Bold)
  $Form.Font = $Font
  $Label = New-Object System.Windows.Forms.Label
  $Label.Text = "Calculation finished!"
  $Label.AutoSize = $True
  $Form.Controls.Add($Label)
  $Form.Topmost=$True
  # -- this ensures your splash screen appears on top of other windows!
 $Form.ShowDialog()
}

The other is to simply send you an email that gets pushed to your smartphone or smart watch, borrowed from here (using Outlook rather than Send-MailMessage so that your IT department can safely inspect your outgoing email and won't mistake your script for a trojan):

function phone-home
{
 $Outlook = New-Object -ComObject Outlook.Application
 $Mail = $Outlook.CreateItem(0)
  $Mail.To = "youremailaddress@mailserver.com"
  $Mail.Subject = "Calculation finished"
  $Mail.Body ="Your calculation has finished. If you need to start another one, go for it."
 $Mail.Send()
}

Now, how to choose between the two? You will want the splash screen if you are sitting in front of your screen, and the email otherwise. So we need a way of discriminating between the two. Following this idea, we can use
function is-locked
{
try {
$currentuser = gwmi -Class win32_computersystem | select -ExpandProperty username
$process = get-process logonui -ea silentlycontinue
if($currentuser -and $process){"LOCKED"}else{"UNLOCKED"}
return}
#Always return LOCKED if logged in remotely
catch{"LOCKED";return} }

Finally, here is a BAT-file one-liner wrapper, called ps.bat to run your PowerShell script on systems where execution of random scripts has been disallowed by default (for good reason). We cannot override this default without admin privileges, but we can by pass it temporarily by calling

@powershell -ExecutionPolicy RemoteSigned .\%1.ps1

You can then call your PowerShell script, e.g., poll.ps1, and simply type ps poll in your command prompt to invoke it quickly.

Enjoy!

No comments:

Post a Comment