5

How would i implement processing of an array with millisecond precision using python under linux (running on a single core Raspberry Pi).

I am trying to parse information from a MIDI file, which has been preprocessed to an array where each millisecond i check if the array has entries at the current timestamp and trigger some functions if it does.

Currently i am using time.time() and employ busy waiting (as concluded here). This eats up all the CPU, therefor i opt for a better solution.

# iterate through all milliseconds
for current_ms in xrange(0, last+1):
  start = time()

  # check if events are to be processed
  try:
    events = allEvents[current_ms]
    # iterate over all events for this millisecond
    for event in events:
      # check if event contains note information
      if 'note' in event:
        # check if mapping to pin exists
        if event['note'] in mapping:
          pin = mapping[event['note']]
          # check if event contains on/off information
          if 'mode' in event:
            if event['mode'] == 0:
              pin_off(pin)
            elif event['mode'] == 1:
              pin_on(pin)
            else:
              debug("unknown mode in event:"+event)
          else:
            debug("no mapping for note:" + event['note'])
  except:
    pass

  end = time()

  # fill the rest of the millisecond
  while (end-start) < (1.0/(1000.0)):
    end = time()

where last is the millisecond of the last event (known from preprocessing)

This is not a question about time() vs clock() more about sleep vs busy wait.

I cant really sleep in the "fill rest of millisecond" loop, because of the too low accuracy of sleep(). If i were to use ctypes, how would i go about it correctly?

Is there some Timer library which would call a callback each millisecond reliably?

My current implementation is on GitHub. With this approach i get a skew of around 4 or 5ms on the drum_sample, which is 3.7s total (with mock, so no real hardware attached). On a 30.7s sample, the skew is around 32ms (so its at least not linear!).

I have tried using time.sleep() and nanosleep() via ctypes with the following code

import time
import timeit
import ctypes
libc = ctypes.CDLL('libc.so.6')

class Timespec(ctypes.Structure):
  """ timespec struct for nanosleep, see:
      http://linux.die.net/man/2/nanosleep """
  _fields_ = [('tv_sec', ctypes.c_long),
              ('tv_nsec', ctypes.c_long)]

libc.nanosleep.argtypes = [ctypes.POINTER(Timespec),
                           ctypes.POINTER(Timespec)]
nanosleep_req = Timespec()
nanosleep_rem = Timespec()

def nsleep(us):
  #print('nsleep: {0:.9f}'.format(us)) 
  """ Delay microseconds with libc nanosleep() using ctypes. """
  if (us >= 1000000):
    sec = us/1000000
    us %= 1000000
  else: sec = 0
  nanosleep_req.tv_sec = sec
  nanosleep_req.tv_nsec = int(us * 1000)

  libc.nanosleep(nanosleep_req, nanosleep_rem)

LOOPS = 10000

def do_sleep(min_sleep):
  #print('try: {0:.9f}'.format(min_sleep))
  total = 0.0
  for i in xrange(0, LOOPS):
    start = timeit.default_timer()
    nsleep(min_sleep*1000*1000)
    #time.sleep(min_sleep)
    end = timeit.default_timer()
    total += end - start
  return (total / LOOPS)

iterations = 5
iteration = 1
min_sleep = 0.001
result = None
while True:
    result = do_sleep(min_sleep)
    #print('res: {0:.9f}'.format(result))
    if result > 1.5 * min_sleep:
      if iteration > iterations:
        break
      else:
        min_sleep = result
        iteration += 1
    else:
      min_sleep /= 2.0

print('FIN: {0:.9f}'.format(result))

The result on my i5 is

FIN: 0.000165443

while on the RPi it is

FIN: 0.000578617

which suggest a sleep period of about 0.1 or 0.5 milliseconds, with the given jitter (tend to sleep longer) that at most helps me reduce the load a little bit.

Community
  • 1
  • 1
x29a
  • 1,761
  • 1
  • 24
  • 43
  • Did you actually try using `sleep()`, or testing its performance on your system? [The second answer](http://stackoverflow.com/a/15967564/3004881) after the one you linked indicates it might have more than enough accuracy for you. – Dan Getz Jul 18 '15 at 19:46
  • yeah i did (see updated question), and the link suggests sleep times over 1ms, i am looking for solutions below (since 1ms will highly likely be overshot). Can not verify the 1µs accuracy, then again maybe its sleeping just fine and the measurement is just off? – x29a Jul 18 '15 at 22:54
  • You should know that if you are using a conventional MIDI interface, it takes 1ms to transmit a single (3 octet) "note on" message. You may be seeking an accuracy that the instrument can't satisfy. – msw Jul 18 '15 at 23:16
  • 1
    Thanks @msw, MIDI is only used to contain the information, the actual instrument is an MCP23017 with 9 solenoids, see the ceremony github for a picture :) but yes, they also inflict some delay (i2c), which should be below the 1ms, otherwise im screwed anyway. – x29a Jul 18 '15 at 23:33

1 Answers1

3

One possible solution, using the sched module:

import sched
import time

def f(t0):
    print 'Time elapsed since t0:', time.time() - t0
s = sched.scheduler(time.time, time.sleep)

for i in range(10):
    s.enterabs(t0 + 10 + i, 0, f, (t0,))
s.run()

Result:

Time elapsed since t0: 10.0058200359
Time elapsed since t0: 11.0022959709
Time elapsed since t0: 12.0017120838
Time elapsed since t0: 13.0022599697
Time elapsed since t0: 14.0022521019
Time elapsed since t0: 15.0015859604
Time elapsed since t0: 16.0023040771
Time elapsed since t0: 17.0023028851
Time elapsed since t0: 18.0023078918
Time elapsed since t0: 19.002286911

Apart from some constant offset of about 2 millisecond (which you could calibrate), the jitter seems to be on the order of 1 or 2 millisecond (as reported by time.time itself). Not sure if that is good enough for your application.

If you need to do some useful work in the meantime, you should look into multi-threading or multi-processing.

Note: a standard Linux distribution that runs on a RPi is not a hard real-time operating system. Also Python can show non-deterministic timing, e.g. when it starts a garbage collection. So your code might run fine with low jitter most of the time, but you might have occasional 'hickups', where there is a bit of delay.

Bas Swinckels
  • 18,095
  • 3
  • 45
  • 62
  • Thanks, i will give this a try. Since some of the MIDI songs are 20minutes, this would result in 1.2M scheduler entries, lets see if it can handle that. As for the "meantime", nope, all it should do is trigger the functions at the right time. The rest is operating system stuff. – x29a Jul 18 '15 at 13:03
  • Should not be a big problem, I guess. If you look at the [source code](https://hg.python.org/cpython/file/2.7/Lib/sched.py), it is pretty basic. It uses a [heap](https://docs.python.org/2/library/heapq.html) to store all events in a list and efficiency get the next one, a Python list of 1 million items is normally no problem if you have the memory. The timing is based on the `timefunc` and `delayfunc` you pass to `scheduler`. In my example, I use `time.time` and `time.sleep`. I don't have experience with a RPi, but you might be able to replace those with some hardware timers. – Bas Swinckels Jul 18 '15 at 13:19