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.
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
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 seeMY_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
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 Event
s and Error
s 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()