Code Size vs Code Speed

At times the mentioning of Driver and App code Size comes up as a goal in and off itself when developing for HE. Remember, this is about code running in HE, other platforms have other concerns and/or constraints.

As a developer who doesn't write fewer lines of code just to keep code size down, but instead focuses on separating the code in re-usable blocks and writing the code using as little as possible of language features known to be slow and inefficient, I can say with certainty that you can very well write an App or Driver that outperforms another app or driver written to be much smaller in code size but doesn't take these other consideration into account. Since the only time there is a "penalty" for large code is when loading it, those few moments are easily recovered in other areas. Yes, ideally there shouldn't be a, measurable, load time when a parent device calls a method in a child device, but that is a system design thing.

I have a parent driver that is 141kB in size, using that driver to turn on lights triggered by my motion sensor driver of 46kB, I have a delay of a maximum of 200ms from trigger event entering the hub to the lights reporting back as on. Using a different and much smaller driver for my motion sensor, I have about the same speed. Using an older, smaller, and not as optimized (still rather optimized) version of my light parent driver it can add 50ms to the total time.

Writing code this way is not always easy when there are no native tools to measure performance and run unit tests, but it is possible.

What I want to say to those that look for a smaller piece of code over a larger one, check what it does and how it does it. Try to measure the time it takes to do what you need it to, the results will probably not be what you expect. Sure, you could write super-efficient and really compact code which outperforms anything else in terms of speed, but how easy do you make changes in such code? What if much of that code needs to be used in 30 different drivers or apps but needs some minor variations to fit?

I know many will have opinions about this, and feel free to have them, but what I talk about above is based on measurable data, not opinion. This is more a reminder, don't judge the code based on the size. Small code size might outperform software with a large code size, but large code size software can also outperform software with a small code size. It is content, not size, that matters.

10 Likes

Things I have found from webCoRE, NST and other development

  • in Groovy, specifying types (Integer, Boolean, Map, List, Double) etc vs. def does improve execution of groovy code

    • this can avoid a lot of box, unbox code, and type conversions. -> smaller code

      • this also means for Map references, you need this: (Boolean)rtD.myBoolean, or (Integer)rtD.myInteger, etc
      • remember HE has to have code in memory to run, so smaller code == less memory needed
    • for small apps, probably not worth the effort, but if you have more than 200 lines of code in a groovy script, you should be doing this. (app or device)

  • use these types or void properly for methods that return values (or do not)

  • set large Maps, List, @Field static to null when you are done with them. Give the java GC a break (and HE Linux OS)

  • Avoid state, and atomicState as much as you can. These are expensive and use the db in HE

    • related to this is when you use them smaller, and less accesses is better.
    • note that think of settings, getDataValue, attributes etc as state. Treat them the same - less is more.
    • the database is a shared resource, so your garbage becomes everyone's garbage.
  • An alternative many times is @Field static ....

    • I find for state, folks have a few different use cases

      • you need to store something for a later run (but it does not matter if this persists across a reboot)

        • @Field static is good for this. When you don't need it any more set the @Field static variable to null
      • you need to store something that has to persist (across executions and reboots)

        • so this should be in state/atomicState, but you may still be able to cache with @Field static and write back changes to state/atomicState
      • you need shared state across multiple children (think shared memory)

        • again @Field static can do this with above view.
    • For me this is where SmartThings in the current (and perhaps new architecture) mis-understand the use cases for state and persistence.

      • this is why their current architecture is so expensive, and their new architecture with micro-services is going to end up relying on a db for all state again - especially when sharing is required.
  • If your groovy code can get multiple events / threads operating, there are many better and faster ways to synchronize/semaphore these threads than atomicState. This is a larger discussion, but if this happens a lot, try to avoid atomicState.

    • Typically thread synchronization does not require across reboot persistence (ie use @Field static with a semaphore array / hash).
  • logging is your friend, until it is not.

    • use it to debug, monitor, but quiet it down once stable - less is more.
      • auto shutoff is a polite feature.
      • it is also more polite for users to not see a bunch of logs
  • in device handlers, don't set isStateChange to true (in sendEvent, et al). just leave it alone and let HE do the right thing. Setting it to true further garbages up the db, etc.

  • if you schedule things, you have choices (depending on your need)

    • runIn does a one time schedule (so you repeatedly keep scheduling things)
    • schedule creates a schedule once and it just runs
      • it also works better across reboots or other exceptions - it is still scheduled.

I think @chuck.schwer is working on things to make responsiveness better, hopefully these will come out soon.

8 Likes

There are many things to keep in mind to optimize Groovy code, on top of the above one important thing to consider is to avoid magic unboxing in if statements and many other locations where you could be explicit instead of implicit. The general guiding principle is that in, almost, all cases explicit is faster than implicit.

If you want to experiment and learn more about the impact of things like magic unboxing, transpiling Groovy into bytecode on your local machine can show you how much work is really done "behind the scenes". It is also important to consider putting more time and effort with optimizing areas of your code that runs frequently, like parse() in a driver, vs methods with infrequent use like installed().

All of this can be discussed a lot, and in much greater detail, I'm sure there's many who would want to know more but don't have the background to know what to look for and to focus on. @nh.schottfam has a great start above. If anyone has specific questions regarding code optimization the collective knowledge here probably cover most areas.

An example or link to or about "magic unboxing", would help the totally clueless like me. I did find this link, but still clueless. I went from PHP to Groovy without coding Java..... :exploding_head:

1 Like

To truly know exactly what runs faster than something else, you would need to use a profiler. Most literature talking about Groovy optimization would refer to using a profiler, but that is not something easily done with Groovy written for HE. With that said, this article explains rather well the basics on the why and what to think about. Some of it can't be done on HE, but the section under "DefaultTypeTransformation.booleanUnbox()" explains what I mean with explicit being better than implicit.

This is an example of implicit and "slow" code (it would be even slower without declaring as String):

String myData = null
...other code...
myData = "some data"
if(myData) {
    ...do something with myData...
}

This would be a more optimized version:

String myData = null
...other code...
myData = "some data"
if(myData != null && myData != "") {
    ...do something with myData...
}

This very same thing goes for most places where you could get away with "if(someVar) ...do stuff...". What you should do for better speed is to be more clear of what you're checking for, like "if(someVar != null && someVar > 0) ...do stuff...". This would be extra important if you had defined "someVar" with just "def" and first set it to contain a String and then an Integer, the extra code needed to handle that is rather significant. The "magic" of Groovy makes it easy to write in, but it also makes it easy to write rather slow code.
These are not complete examples, but hope they give an idea of what I mean.

2 Likes

I know this is late, but I just stumbled upon it...

auto-boxing in Java is when the java runtime implicitly converts data types. With Groovy, there is a lot more support for the conversion that in Java. But here goes. The easiest was to describe auto-boxing is with int and Integer (or any other native type vs object).

// Autobox Integer to int
int value = Integer(10); 
int value = Integer(10).intValue()

// Autobox int to Integer
Integer val2 = 7; 
Integer val2 = Integer.valueOf(7)

// The same works with Strings.
String text = "The first value is " + value + " and other is " + val2
String text = "The first value is " + value.toString() + and other is " + val2.toString()

Since java has standard methods for type conversion, auto-boxing leverages these calls. Groovy is a dialect that sits on-top of Java. This means groovy benefits from auto boxing too. But groovy goes even further with more data types. Declaring a variable of type def, the variable is loosely typed, and can be of any type -- like a Variant in Visual Basic. When you convert from def as the variable type, to a static typed variable, like String or int, float, BigInteger, etc.. you can no longer use auto-boxing. Now you must use the proper java type assignments... although Java auto-boxing still applies.

in Groovy, you can do the following:

def add(a, b) {
    a + b
}
def s = add('1', 2) // result = 3

But, if you define the function as:

int add(a, b) {
    a + b
}
int s = add('1', 2) // type-cast conversion error, or failure call toInt() on String

This is the issue with converting code from loosely typed to strongly typed code. You need to be more precise in the data being passed around. But, you can still leverage groovy to make the conversions between types easier (at a cost of performance), you just can't use groovy specific auto-boxing on strongly typed groovy.

1 Like

In interpreted groovy, every type is the wrapper type. You don't get int unless you get compile static, which is not available on HE.

1 Like

Yes. I understand that this is the case in HE — everything is an Object — no primitives. I was explained auto boxing, as was asked. Perhaps my way of understanding behind the scenes how things work is overkill for this thread.

Download the Hubitat app