[SOLVED] Floating point operations in rules causes severe performance issues on hard float capable hardware/software

  • Platform information:
    • Hardware: Raspberry Pi 3 B
    • OS: Rasbian (openHABian v1.4)
    • Java Runtime Environment: Zulu Embedded 8.25.0.76-linux-aarch32hf
    • openHAB version: 2.4.0-1 Stable
    • Storage: 64GB Sandisk Extreme Go USB3.1

Hi,
After searching the forum and online I’m now trying my luck here. :slight_smile:
I’m having some performance issues using floating point numbers in rules.
I know computers are bad at floating point operations but what I’m seeing now feels ridiculous.
The following code takes about 2 minutes for openhab to refresh, and then 2.5 minutes to run.
During this time the processor load is over 100%.

val logID = "math.rules"
// val X = newArrayList(66f, 2, 5, 6)
// val Y = newArrayList(66f, 2, 5, 6)
// 	X.get(1)
val float Y2 = 17f
val float Y1 = 1f
val float X2 = 100f
val float X1 = 0f
var float Xtrigger = 3.4f
var float y
var float oldAqi = -100f
var int setPoint = 300
// --------------- air quality changed ---------------
rule "air quality changed"
when
    // Item aqiHome changed
    Item Test_switch_4 received command ON
then
    logInfo(logID, "button pushed")
    val newAqi = (aqiHome.state as DecimalType).floatValue
    if (Math::abs(newAqi-oldAqi) >= Xtrigger) {
        logInfo(logID, "aqi changed more than: " + Xtrigger)
        y = ((Y2-Y1)/(X2-X1)) * newAqi + Y2 - ((Y2-Y1)/(X2-X1)) * X2
        // setPoint = Math::round(((Y2-Y1)/(X2-X1))*newAqi + Y2 - ((Y2-Y1)/(X2-X1))*X2)
        setPoint = Math::round(y)
        // setPoint = Math::round(newAqi)
    // purifier_1_favoritelevel.sendCommand(setPoint)
    logInfo(logID, "purifier setPoint: " + setPoint)
    }
end

I found it’s all about this expression:

((Y2-Y1)/(X2-X1)) * newAqi + Y2 - ((Y2-Y1)/(X2-X1)) * X2

Without that it refreshes and runs in 2-3 seconds.

As i understood it the Zulu JVM shipped with openhabian utilizes hard float so the performance penalty for using floats should not be so severe.
Does anyone have any ideas?

Sincerely,
Carl

PS.

Adding the output of: java -XshowSettings:properties -version
for reference

Property settings:
    awt.toolkit = sun.awt.X11.XToolkit
    file.encoding = UTF-8
    file.encoding.pkg = sun.io
    file.separator = /
    java.awt.graphicsenv = sun.awt.X11GraphicsEnvironment
    java.awt.printerjob = sun.print.PSPrinterJob
    java.class.path = .
    java.class.version = 52.0
    java.endorsed.dirs = /usr/lib/jvm/zulu-embedded-8-armhf/jre/lib/endorsed
    java.ext.dirs = /usr/lib/jvm/zulu-embedded-8-armhf/jre/lib/ext
        /usr/java/packages/lib/ext
    java.home = /usr/lib/jvm/zulu-embedded-8-armhf/jre
    java.io.tmpdir = /tmp
    java.library.path = /usr/java/packages/lib/aarch32
        /lib
        /usr/lib
    java.runtime.name = OpenJDK Runtime Environment
    java.runtime.version = 1.8.0_152-b76
    java.specification.name = Java Platform API Specification
    java.specification.vendor = Oracle Corporation
    java.specification.version = 1.8
    java.vendor = Azul Systems, Inc.
    java.vendor.url = http://www.azulsystems.com/
    java.vendor.url.bug = http://www.azulsystems.com/support/
    java.version = 1.8.0_152
    java.vm.info = mixed mode, Evaluation
    java.vm.name = OpenJDK Client VM
    java.vm.specification.name = Java Virtual Machine Specification
    java.vm.specification.vendor = Oracle Corporation
    java.vm.specification.version = 1.8
    java.vm.vendor = Azul Systems, Inc.
    java.vm.version = 25.152-b76
    line.separator = \n
    os.arch = arm
    os.name = Linux
    os.version = 4.14.79-v7+
    path.separator = :
    sun.arch.abi = gnueabihf
    sun.arch.data.model = 32
    sun.boot.class.path = /usr/lib/jvm/zulu-embedded-8-armhf/jre/lib/resources.jar
        /usr/lib/jvm/zulu-embedded-8-armhf/jre/lib/rt.jar
        /usr/lib/jvm/zulu-embedded-8-armhf/jre/lib/sunrsasign.jar
        /usr/lib/jvm/zulu-embedded-8-armhf/jre/lib/jsse.jar
        /usr/lib/jvm/zulu-embedded-8-armhf/jre/lib/jce.jar
        /usr/lib/jvm/zulu-embedded-8-armhf/jre/lib/charsets.jar
        /usr/lib/jvm/zulu-embedded-8-armhf/jre/lib/jfr.jar
        /usr/lib/jvm/zulu-embedded-8-armhf/jre/classes
    sun.boot.library.path = /usr/lib/jvm/zulu-embedded-8-armhf/jre/lib/aarch32
    sun.cpu.endian = little
    sun.cpu.isalist =
    sun.io.unicode.encoding = UnicodeLittle
    sun.java.launcher = SUN_STANDARD
    sun.jnu.encoding = UTF-8
    sun.management.compiler = HotSpot Client Compiler
    sun.os.patch.level = unknown
    user.country = US
    user.dir = /home/openhabian
    user.home = /home/openhabian
    user.language = en
    user.name = openhabian
    user.timezone =

openjdk version "1.8.0_152"
OpenJDK Runtime Environment (Zulu Embedded 8.25.0.76-linux-aarch32hf) (build 1.8.0_152-b76)
OpenJDK Client VM (Zulu Embedded 8.25.0.76-linux-aarch32hf) (build 25.152-b76, mixed mode, Evaluation)

It is recommend not to use primitives (int, float…) in the rules script

Use Number type instead

val logID = "math.rules"
// val X = newArrayList(66f, 2, 5, 6)
// val Y = newArrayList(66f, 2, 5, 6)
// 	X.get(1)
val Number Y2 = 17
val Number  Y1 = 1
val Number X2 = 100
val Number X1 = 0
var Number Xtrigger = 3.4
var Number y
var Number oldAqi = -100
var Number setPoint = 300
// --------------- air quality changed ---------------
rule "air quality changed"
when
    // Item aqiHome changed
    Item Test_switch_4 received command ON
then
    logInfo(logID, "button pushed")
    val newAqi = aqiHome.state as Number
    if (Math::abs(newAqi-oldAqi) >= Xtrigger) {
        logInfo(logID, "aqi changed more than: " + Xtrigger)
        y = ((Y2-Y1)/(X2-X1)) * newAqi + Y2 - ((Y2-Y1)/(X2-X1)) * X2
        // setPoint = Math::round(((Y2-Y1)/(X2-X1))*newAqi + Y2 - ((Y2-Y1)/(X2-X1))*X2)
        setPoint = Math::round(y)
        // setPoint = Math::round(newAqi)
    // purifier_1_favoritelevel.sendCommand(setPoint)
    logInfo(logID, "purifier setPoint: " + setPoint.toString)
    }
end
2 Likes

Yes it should but you should double check it’s the right JVM installed.

But I second @vzorglub, it’s likely to be the primitives.

I see, I assumed I had to use DecinalType since the xtend docummentation says “There are only decimal floating-point literals.”. I guess I don’t understand that sentence fully :slight_smile:
Thank you for the code suggestion, running it however gives me this error.

2019-01-10 19:36:35.327 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'air quality changed': An error occurred during the script execution: Could not invoke method: java.lang.Math.abs(int) on instance: null

I checked newAqi which is the only thing not declared in the rule and it definately has a state.

aqiHome (Type=GroupItem, BaseType=NumberItem, Members=1, State=13, Label=aqiHome MAX, Category=null)

However the Category is null, but I have no idea what a Category is for an Item.
Any ideas why I get this error?

I thought thats what the hf stands for, or does the mixed mode thing mean it might not use it?

You don’t seem to be feeding that an int, though? Maybe it defaults to integer version of abs if fed null.

Investigate with logInfo

val newAqi = aqiHome.state as Number
   logInfo("test", "values " + newAqi.toString + " " + oldAqi.toString)
    if (Math::abs(newAqi-oldAqi) >= Xtrigger) {

This

    logInfo(logID, "aqiHome: "+ aqiHome.state.toString)                                      │To interact with openHAB on the command line, execute: 'openhab-cli --help'
    val newAqi = aqiHome.state as Number                                                     │
    logInfo(logID, "oldAqi: "+ oldAqi.toString)                                              │[19:10:48] openhabian@Marvin:/etc/openhab2/items$ cd..
    logInfo(logID, "newAqi: "+ newAqi.toString)

outputs this

2019-01-10 20:18:50.723 [INFO ] [se.smarthome.model.script.math.rules] - aqiHome: 7
2019-01-10 20:18:50.728 [INFO ] [se.smarthome.model.script.math.rules] - oldAqi: -100
2019-01-10 20:18:50.733 [INFO ] [se.smarthome.model.script.math.rules] - newAqi: 7

Edit: Sorry about strange post, apparently found some shortcut to click post :slight_smile:

Might help


but not eit drives you to primitives again for abs().

It might be more effecient to write your own if-then abs comparison for Numbers

Just realized it’s 7–100, can it be that the negative of a negative is undefined?

That’s fine. Isn’t that what your abs() is supposed to deal with?

I was thinking if it got confused by the double negative, but I just tested with other number and it does not affect it.

Could avoid abs() by rewrite as

    if ( ( (newAqi-oldAqi) >= 0 && >= Xtrigger) ||     // postive result
          ( 0 - (newAqi-oldAqi)) >= Xtrigger )  {         // negative result

I think my logic is correct … please check, causes headaches :crazy_face:

2 Likes

I have something working fairly fast now.
Using:

setPoint = Math::round(y.floatValue())

to round and I’ll write my own abs with if

1 Like

very nice, thank you! :smile:

Edit:
It threw and error but with this small edit it works nicely :slight_smile:

if ( ( (newAqi-oldAqi) >= 0 && (newAqi-oldAqi) >= Xtrigger) ||     // postive result         │
          ( 0 - (newAqi-oldAqi)) >= Xtrigger )  {                                 // negative result

As this seems a pretty severe example manifestation of performance issue, I’ve referenced this post in an existing issue

1 Like

Brilliant, right logic wrong brackets :crazy_face:

1 Like

Thank you :slight_smile:

Back in the thread
I thought that the abs function might cause a problem but I wasn’t sure
Is the rule faster now?

hf is the right one

It’s a known problem with no known solution in site. For some reason (I have unsubstantiated theories) the Rules parser has a really hard time parsing .rules files that use primitives.

Luckily you almost never have to use primitives.

The problem isn’t in actually performing the operation. The problem is with the parser. My theory of what is going on is that Xtend, as a weakly typed language, prefers to put off until run time type checking. But when you use primitives it cannot put off the type checking so it has to do a whole lot more work to verify that all the types check out at parse time.

By default without forcing it, Xtend will use the BigDecimal class to represent all Numbers. By forcing your variables to be primitives you are bypassing this.

So, there are some places where a primitive must be used. These are usually in calls to library methods like abs. You can use a primitive in just these places though. For example:

Math::abs((newAqi-oldAqi).intValue)

For one or two calls to a library like this I see no reason to avoid the calls to intValue or doubleValue where needed. That shouldn’t impact performance too much. Typically there will be only one or two such calls in a given Rule anyway.

1 Like