Events and Errors in uTensor

In a previous life, I did some artifactual work around improving logging capabilities for IoT deployments. These systems often have soft real-time guarantees and RAM on the order of tens-of-kilobytes. Some of these general learnings are going to be in the upcoming release of uTensor.

First pass, c strings

In this regard printf and snprintf are like the flat head screwdrivers of the debugging world, you can make five hundred billion hacked-up tools out of them, but it’s pretty far from optimal.

First off, there’s the close to linear time cost for each function call and on top of that there’s generally several syscall hops within the RTOS which probably only have simple buffering capabilities. And for small embedded systems, c strings take up a non-trivial amount of storage. To show this, I ran a simple random sampling experiment where for each iteration I would randomly select 100 Mbed OS 5 projects from https://os.mbed.com/code/ and https://github.com/armmbed/, compile them, scrape the strings from the top-level module compiled objects, and compute their basic statistics. The plot below shows the distribution of sample means for each iteration which for the most part you can ignore, the moral of the story is that for a given module the average length of a c string is likely to between 25 and 29 bytes long. Times the number of strings in that module. And this doesn’t even count strings that get composed! It doesn’t seem like a lot, but a few extra kilobytes for just c strings means less working code that gets deployed.

pinto-compress-strings.png

Event Codes

On the other end of the spectrum is the good old fashioned event encoding system. This generally involves a council of wise ancients declaring a set of laws to govern the structure of integer types. "THOU shalt be -12, for the runes describe an invalid parameter has been passed". While memory optimal, these encodings can collide, which is again fine since collisions are rare, or worse get convolved in #define hell. This problem gets even more annoying when you start using multiple modules with multiple different encoding schemes.

Events and Errors in uTensor

Example of Errors getting thrown with current API. Don’t worry, the error handler will clean up any dangling pointers on return, if the errors get arbitrated properly

Example of Errors getting thrown with current API. Don’t worry, the error handler will clean up any dangling pointers on return, if the errors get arbitrated properly

One of my favorite parts of C++11 is wonderful constexpr, which basically allows a given expression to be evaluated at compile time. As an embedded research engineer, the more stuff we can do at compile means less crap we actually need to put on the board! What if we can use constexpr to build a hybrid system that is as human readable/decodable as c strings, but as compact as event codes?

In general RTTI, run time type information, is expensive for tiny systems. Rather than forcing users to compile their code with RTTI enabled and eat that cost, we found a neat way to give uTensor the ability to identify events dynamically at runtime with configurable cost! Basically using only C++11 language features, we just hash the static signature of an event type at compile time and store this as a, mostly, unique ID inside the resulting Event objects.

There are a few nice byproducts from this method:

  • The generated IDs remain the same across builds and machines, unless someone explicitly changes the signature of an Event
  • Users dont need to manually specify some magic number associated with each event
  • At compile time, each event is a unique type so in debug builds you actually get extra debug symbols. If you #define MY_ERROR 5 you just see the number 5 in a gdb session (super helpful, I know), where as here you would see MY_ERROR{id: 5} or something similar.
  • You can configure the exact size of the Event object because it is the same as the integer type used to represent the IDs, for example: 1 byte, 2 bytes, or 4 bytes depending on how many unique event types you need, even though the 4 byte version is probably way overkill for small devices.
  • Composing events, i.e. adding additional attributes, can now be done either per event or per software module

https://github.com/uTensor/uTensor/blob/re-arch-rc1/src/uTensor/core/errorHandler.hpp

Screen Shot 2020-05-15 at 12.15.27 PM.png

These IDs are pretty useful when debugging remote deployments. All you have to do is grep your source code for DECLARE_EVENT() or DECLARE_ERROR(), run the same hash function on the s, and store this in a simple map(hash() => ). Then if an Event or Error occurs you just have to query this map. I keep mine in a sorted text map so I can track even IDs in git.

Alternatively, this Python snippet will scrape codebases for all basically defined Events and Errors and store them in a Python dictionary:

import numpy as np
import glob
import re

from collections import defaultdict
from pprint import pprint


def mHash_fnv1a(mStr):
  np.seterr(over='ignore')
  val_32_const = np.uint32(0x811c9dc5)
  prime_32_const = np.uint32(0x1000193)
  value = val_32_const
  for c in mStr:
      value = (value ^ np.uint32(ord(c))) * prime_32_const
  return value

def get_target_files():
  x = glob.glob('**/*.[ch]pp', recursive=True)
  return x

def get_event_map():
  tgts = get_target_files()
  event_names = []
  event_map = defaultdict(list)
  for f in tgts:
    with open(f) as fp:
      for line in fp:
        m = re.match("\s*DECLARE_\w+\((\w+)\)", line)
        if m:
          #print(m)
          event_names.append(m.group(1))

  for evt in event_names:
    x = mHash_fnv1a(evt)
    event_map[x].append(evt)
  pprint(event_map)
  return event_map

if __name__ == "__main__":
  x = get_event_map()
Previous
Previous

uTensor: A Tale of Two Allocators

Next
Next

Hello World!