Is Groovy the most native scripting language for OH?

There seems to be less conversation about this scripting language, but I have been using it more lately.

My understanding is that Javascript and Python are running their own engines/interpreters on top of Java, then have essentially a translation layer to interface with Java code.

In comparison Groovy is basically a superset of Java? If so, does it have the best performance of speed and latency?

Since it’s also an official scripting addon, it would seem to be the most native option for scripting. Is JS pushed as the default install just because of popularity, and considered to be “easier’“?

i can just say something about jsscripting and pythonscripting. Both engines are running on the top of graalvm which provides a way to embedd additional languages into java. Both allows direct access to native openhab java api.

Jsscripting provides its own openhab api to make the usebility easier. It wrappes nearly everthing, which results in a “stable” api which is independent of the openhab java api. This means, that also things like items are wrapped objects with their own js api. But you still can access the java api if you want.

Pythonscriptings focus is a bit different. It provides its own thin api, which just covers some basic aspects to access openhab objects like items. The objects itself are proxy objects to the native openhab java objects and their java api. The idea here is to work as close as possible with openhab java api.

Regarding the performance. Both languages are currently running in interpreter mode. This means they are not running at full speed currently, because the code is evaluated every time again. In the future it will be improved when we switch to just in time compiler. This will result in a major performance improvement. But i cant say when this will be done.

I think the performance is not a problem or a bottleneck. I myself have very extensive rules, which rarely run for longer than 1-20ms.

----- update -----

I looked into the groovy binding and it looks that it does not provide any kind of helper lib. This meany everything has to be done by yourself. e.g. there is no easy way to just annotate a function/class as a rule. Instead you have to create a rule object and manuall register it in the script manager…

1 Like

As I understand it that’s because it can’t. Groovy doesn’t support external libraries. At least not the way it’s implemented and brought into Karaf/openHAB. So there really is no feasible way to build such a helper library as a separate thing like Python, JS, and jRuby do.

Things may have changed in the years since I last looked at this.

  • no one had ever really volunteered to document well how to use Groovy in OH (to this day)
  • writing rules in the UI in Groovy or using the native OH Java API in general in any language (e.g Nashorn JS) is a miserable experience (almost half the code in every script ends up being imports just to be able to do anything)
  • Jython was the best candidate for awhile until it was abandoned in OH 3 (@holger_hees thank you again for picking it back up and creating GraalVM Python support). jRuby was the next candidate but at the time the developer wasn’t interested in making changes to improve the UI user’s experience (this has much improved since then). GraalVM JS was willing to make changes to improve the UI user’s experience so it became the only option. GraalVM Python came along much later. Lack of documentation and a way to help the user, particularly in the UI means Groovy was never a candidate.

The situation at the time was:

  • Rules DSL: it’s always been odd, doesn’t support libraries or even basic programming concepts like functions, half broken type system
  • Nashorn JS: not much of a helper library, deprecated, ancient version of JS
  • Groovy: no external libraries, never really documented, only supports the raw OH API s.
  • Jython: helper library was never part of the OH project itself, abandoned (only recently been picked back up for support), upstream development is exceptionally slow (still sick on Python 2.7 to this day)
  • jRuby: great helper library but requiredva good but of imports to use them in a script. Remember, in the UI reach script stands on it’s own meaning each and every script action and script condition needs to import everything it needs to use. You can’t just put them at the top of the file and reuse then across multiple rules. This has since changed.
  • GraalVM JS: provided a modern JS environment with a robust helper library and settings to ease it’s use for UI rules developers.

At the time there was only one clear choice. Only one had everything we need out of the default language. Today jRuby and GraalVM Python could meet all the criteria a default language would need to meet but GraalVM JS got there first. Groovy still does not.

Even though the performance of using Groovy might be measurably better than these alternatives, these alternatives are more than performant enough in a home automation context. And as long as the performance is good enough, it’s the least important criteria of them all.

Users already complain Blocky is too complicated. We’d completely loose them if we pushed Groovy instead.

4 Likes

No kidding about the documentation! I have mainly pieced together basic functionality between the pages Javadocs and JSR223, but higher level functions like JSON decoding might have to be left to JS or Python. Even then I think I stumbled upon many of the objects by trial and error.

Thanks Holger for the python addon. I can see there’s some more functionality in working with the native API, for one example it’s much easier to implement logic now based on what/where an item was commanded from, ie the UI, rule, script etc.

Previously I had trouble doing that with JS as I don’t think it had been implemented in the JS api. The addon itself is great, but I think I’m developing a distaste for the language itself…must be getting older.

That’s new in OH 5.1 and available in all the languages, even Rules DSL I think

I’m not sure if it’s been added to Blocky yet but if not I’m sure it will be soon. In JS I think it’s event.eventSource.

FYI I ran some test scripts across Groovy, JS and Python to get an idea of relative speed, even if a little contrived and not of much consequence to usual scripts.

I did some arithmetic and cache read/writes in a loop 100000 times, and averaged the result over 10 runs - excluding the initial outliers, as the scripts must have to do some caching/compilation on the first run making them about 2-3x slower.

JS: 270ms

Groovy 394ms

Python 3090ms

The most interesting thing is that I expected Groovy to be faster, but other scripts showed similar results.

1 Like

Can you share your script? Maybe I find the reason why python is so much slower. I dit a similar compare one year ago and the result was that js and python had ~ same speed.

The only known bottleneck currently is if you do a lot of things with java ZonedDateTime, because the proxy implementation is not very efficient.

Groovy test 1: 394ms average

import org.slf4j.LoggerFactory;
scriptExtension.importPreset("cache");

long elapsed_time;
long start_time = System.currentTimeMillis();
long end_time;
float counter = 0.2;
float rate = 3.54409;

privateCache.put('counter', counter);

for (int i = 0; i < 100000; i++) {
  counter = privateCache.get('counter');
  counter = rate*counter*(1 - counter);
  privateCache.put('counter', counter);
}

end_time = System.currentTimeMillis();
elapsed_time = end_time - start_time;

LoggerFactory.getLogger("org.openhab.core.automation.groovy").info("Performance Test: Groovy ran in ".concat(elapsed_time.toString()).concat(" milliseconds"));

JS Test 1: 270ms average

var counter = 0.2;
var rate = 3.54409;
var start_time, end_time, elapsed_time;
start_time = new Date().getTime();
cache.private.put('counter', counter);

for (let i = 0; i < 100000; i++) {
  counter = cache.private.get('counter');
  counter = rate*counter*(1 - counter);
  cache.private.put('counter', counter);
}
end_time = new Date().getTime();
elapsed_time = end_time - start_time;
console.info('Performance Test: JavaScript ran in '.concat(elapsed_time).concat(' milliseconds'));

Python Test 1: 3090ms average

from scope import cache
import time

counter = 0.2
rate = 3.54409
start_time = time.time_ns()

cache.privateCache.put('counter', counter)

for i in range(100000):
  counter = cache.privateCache.get('counter');
  counter = rate*counter*(1 - counter);
  cache.privateCache.put('counter', counter);

end_time = time.time_ns()
elapsed_time = (end_time - start_time) // 1000000
print(f'Performance Test: Python ran in {elapsed_time} milliseconds')

Running time increases linearly with the number of loops, so the cache read/writes were an obvious culprit. I removed the cache code and run purely off internal variables, plus I had to up the loops to 1,000,000 to more accurately see the relative difference.

Groovy test 2: 7ms average :open_mouth:

import org.slf4j.LoggerFactory;

long elapsed_time;
long start_time = System.currentTimeMillis();
long end_time;
float counter = 0.2;
float rate = 3.54409;

for (int i = 0; i < 1000000; i++) {
  counter = rate*counter*(1 - counter);
}

end_time = System.currentTimeMillis();
elapsed_time = end_time - start_time;

LoggerFactory.getLogger("org.openhab.core.automation.groovy").info("Performance Test: Groovy ran in ".concat(elapsed_time.toString()).concat(" milliseconds"));

JS Test 2: 98ms average

var counter = 0.2;
var rate = 3.54409;
var start_time, end_time, elapsed_time;
start_time = new Date().getTime();

for (let i = 0; i < 1000000; i++) {
  counter = rate*counter*(1 - counter);
}
end_time = new Date().getTime();
elapsed_time = end_time - start_time;
console.info('Performance Test: JavaScript ran in '.concat(elapsed_time).concat(' milliseconds'));

Python Test 2: 917ms average

import time

counter = 0.2
rate = 3.54409
start_time = time.time_ns()

for i in range(1000000):
  counter = rate*counter*(1 - counter);

end_time = time.time_ns()
elapsed_time = (end_time - start_time) // 1000000
print(f'Performance Test: Python ran in {elapsed_time} milliseconds')

So the relative difference is still quite similar between Python and JS, albeit in this very artificial case. But finally Groovy shows an interesting use case, if you have some extremely math heavy script.

In the first python test, can you try to assign cache.privateCache to a variable and then work with these? Otherwise proxy objects are involved more then necessary.

Like this? No difference sorry, but I wouldn’t expect it to make a difference?

from scope import cache
import time

counter = 0.2
rate = 3.54409
start_time = time.time_ns()

local_cache = cache.privateCache

local_cache.put('counter', counter)

for i in range(100000):
  counter = local_cache.get('counter');
  counter = rate*counter*(1 - counter);
  local_cache.put('counter', counter);

end_time = time.time_ns()
elapsed_time = (end_time - start_time) // 1000000
print(f'Performance Test: Python ran in {elapsed_time} milliseconds')

Not sure if it makes a difference, but I am running on a Raspberry Pi 5 in case cpu architecture comes into play somewhere.

It does make a difference.

  1. You should never use nested dict/object lookups in a loop, as this would result in 200.000 additional/avoidable calls in this particular case.

  2. Every call means crossing the boundery between Python and Java. I don’t really know how “expensive” that is, but it definitely costs resources. “cache” is a proxy Java object, and “privateCache” too. As if you call “cache.privateCache”, this means 2 “boundery crossings” 2 times per loop and 100.000 iterations.

You could also execute your code within a rule and set the profile_code flag to true. This would give you a complete debug trace, showing you in detail where the performance is being lost.

from openhab import rule
from openhab.triggers import SystemStartlevelTrigger

@rule(
    triggers = [
        SystemStartlevelTrigger(80)
    ], 
    profile_code=true
)
def test(module, input):
    print("your code")

I’m currently writing this from my phone and won’t be able to test it myself for another two days…

Here’s a JRuby script:

gemfile do
  source "https://rubygems.org"
  require "benchmark"
end

counter = 0.2
rate = 3.54409

result = Benchmark.measure { 1_000_000.times { counter = rate * counter * (1 - counter) } }

logger.info("Performance Test: JRuby ran in #{result.real * 1000} milliseconds")

Thanks, it runs an average of 137ms, with a higher std dev than the others. (min 109ms, max 187)

gemfile do
  source "https://rubygems.org"
  require "benchmark"
end

counter = 0.2
rate = 3.54409

times = []
10.times do
  times << (Benchmark.measure { 1_000_000.times { counter = rate * counter * (1 - counter) } }.real * 1000)
end

min = times.min
max = times.max
avg = times.sum / times.size
stddev = Math.sqrt(times.map { |t| (t - avg)**2 }.sum / (times.size - 1))

stats = "Min: #{min}, Max: #{max}, Avg: #{avg}, StdDev: #{stddev}"
logger.info("Performance Test: JRuby stats over 10 runs - #{stats}")

I ran it a few times:

  • Performance Test: JRuby stats over 10 runs - Min: 91.33904999998776, Max: 133.41516200034675, Avg: 106.21166789997005, StdDev: 13.653540495490226
  • Performance Test: JRuby stats over 10 runs - Min: 85.52839700041659, Max: 144.2851720003091, Avg: 100.92084220004836, StdDev: 17.46195095554317
  • Performance Test: JRuby stats over 10 runs - Min: 82.35407800020766, Max: 201.13105600012204, Avg: 99.34403880006357, StdDev: 36.624111805923356
  • Performance Test: JRuby stats over 10 runs - Min: 78.13432399962039, Max: 126.10484499964514, Avg: 89.02704499987522, StdDev: 15.462116008194972
  • Performance Test: JRuby stats over 10 runs - Min: 87.37422000012884, Max: 137.0086259994423, Avg: 97.13072089980415, StdDev: 16.122817010926735

Also another with 1000 runs:

  • Performance Test: JRuby stats over 1000 runs - Min: 79.65305200013972, Max: 160.2164650003033, Avg: 88.5783370549907, StdDev: 8.832689452556561
1 Like

Yes your right, but I found that assigning

local_cache = cache.privateCache

by itself didn’t really help the lookup speeds at all. I guess it still has to lookup the .put and .get methods. I tried putting the loop section in a function to maximise local variable access.

At first, accessing the cache through local_cache.put / local_cache.get was even worse than my initial test of 3090ms, this one taking ~4400ms. But if you do one lookup of the .put and .get methods, and assign them to a local variable it greatly speeds up, now measuring ~1200ms.

def run_loop_cache(loops, local_cache, initial, _rate):
  put = local_cache.put
  get = local_cache.get
  put('counter', initial)
  for i in range(loops):
    count = get('counter');
    count = _rate*count*(1 - count);
    put('counter', count);

run_loop_cache(100000, cache.privateCache, counter, rate)

@Johnno I think I found the reason.

In the past, I wrapped some functions to catch any possible exception, to simplify and improved graalpy foreign object related error messages. like:

"invalid instantiation of foreign object" => "java object function parameters are missing or does not match the required value type"
or
"foreign object has no attribute ...." => "java object has no attribute ..."

I also modified stack trace steps to keep focus on the real problem and avoid confusing of “normal” users by hiding “internal” wrapper logic"

I refactored this completely and simplified the logic massive without the need of this kind of wrappers. The error messages and stack traces are not 100% like before, but still understandable and easy enough. Additionally the complexity of the code is reduced and the visibility of the complete stack trace helps users to understand how everything works. I think it is now a good compromise.

maybe you can give it a try and check how the performance is now on your machine.

just run pythonscripting update install latest inside openhab cli to update to the latest version 1.0.16 and tell me the results.

1 Like

Have you looked into the docs? => JavaScript Scripting - Automation | openHAB

event.eventSource, as Rich posted as well.

1 Like

JRuby implemented a comprehensive set of helpers to deal with the event source, explained in detail here:

1 Like

Yes it did improve performance thanks!

Oddly I have noticed my script times have varied from day to day without changing any code. In my first examples where the JS was running 270ms average, today runs at 550ms average.

In that first test with unoptimised python code, today it ran between 3900-4300ms on version 1.0.15, then ran between 1550-1680ms on 1.0.16

The big improvement comes from what I mentioned here, namely that if you are doing multiple calls to external objects, the best practice may be to do a single lookup of the full object and assign it to a local variable, e.g.

put = cache.privateCache.put

whereas:

private_cache = cache.privateCache
private_cache.put('key','val')

wont give you the same performance benefit with multiple calls to private_cache.put()

So now, with version 1.0.16 and an optimised script, it today runs between 350-380ms (better than JS!). Admittedly this test is quite an extreme example of lookup overhead, but it could be beneficial in documentation to mention limiting calls to external objects in favour of assigning them to local variables (or script global).