Skip to content

Memory leak in jruby within openhab #9070

Description

@ninjaltd

Hi team,

I am running some very simple jruby automation scripts within openhab 4.1.3 on RPi 4 and am experiencing what appears to be some memory leaks within jruby as time goes on.

Linux hapi 5.10.103-v7l+ #1529 SMP Tue Mar 8 12:24:00 GMT 2022 armv7l GNU/Linux

jruby version is 9.4.9.0

I am not using any gems other than openhab-jrubyscripting to interface with openhab

If left unchecked, the JVM will run out of memory within a week in my RPi setup. Inspecting heap dumps, several possible leaks are identified which appear to be within jruby. Details below;

Problem Suspect 1
29,102 instances of org.jruby.RubyClass, loaded by org.openhab.automation.jrubyscripting occupy 195,511,120 (19.70%) bytes.
org.jruby.RubyClass
org.openhab.automation.jrubyscripting

Problem Suspect 2
71,150 instances of org.jruby.ir.IRMethod, loaded by org.openhab.automation.jrubyscripting occupy 148,220,520 (14.93%) bytes.
org.jruby.ir.IRMethod
org.openhab.automation.jrubyscripting

Problem Suspect 3
33 instances of org.jruby.Ruby, loaded by org.openhab.automation.jrubyscripting occupy 118,031,520 (11.89%) bytes.
org.jruby.Ruby
org.openhab.automation.jrubyscripting

I am happy to provide heap dumps privately for any jruby devs that would like to analyse.

Here is the script which I'm using:

# automation of HVAC based on astro, temp & solar forecasts
#

ITEM_DEFAULT_VALUES = {
  Hvac_Solar_Power_Threshold_Dusk    => 2000 | 'W',
  Hvac_Solar_Power_Threshold_Dawn    => 3000 | 'W',
  Hvac_Solar_Override_Trigger_Cool   => 9 | '°C',
  Hvac_Solar_Override_Trigger_Heat   => 34 | '°C',
  Hvac_Solar_Override_Normal_Cool    => 13 | '°C',
  Hvac_Solar_Override_Normal_Heat    => 28 | '°C'
}

DAWN_AVG_MINS = 10
DUSK_AVG_MINS = 10

shared_cache.compute_if_absent(:hvacstate) { OFF }
shared_cache.compute_if_absent(:hvacsolaroverride) { OFF }

def hvac_log(msg)
  logger.info "HVAC: UN(#{Airtouch_Unit_Fujitsu_Power.state}) AS(#{shared_cache[:hvacstate]}): #{msg}"
  Airtouch_Unit_Fujitsu_Automation_Message.update(msg)
end

def deploy_scenes(power_scene, setpoint_scene)
  hvac_log sprintf("deploy_scenes(#{power_scene}, #{setpoint_scene})")
  active_scenes = [power_scene, setpoint_scene]
  gAirtouchZones.members.each do |zone_group|
    zone_group.tags.sort.each do |tag|
      if tag =~ /Fujitsu_Scene_(\S+?)\[(\S+)\]\[(\S+)\]$/
        scene, tag, state = $1, $2, $3
        if active_scenes.include? scene
          zone_group.members.flatten.sort_by(&:name).tagged(tag).each do |item| 
            hvac_log sprintf("[Scene_Item_State] %-40s | %-10s | %-10s --> %s", item.name, scene, tag, state)
            item.ensure << state
          end
        end
      end
    end
  end
end

def determine_automation_scenes(mode)
  sunrise_time   = Sunrise_Time.state
  sunset_time    = Sunset_Time.state
  tnow           = Time.now

  # Determine POWER scene
  case tnow.wday
  when 5 # fri
    power_scene = "AllOn"
  when 6, 0 # sat, sun
    power_scene = "Weekend"
  else
    power_scene = "Workday"
  end
  power_scene = 'Night' if  Time.now < (sunrise_time - '1 hours') || Time.now > (sunset_time + '5 hours')

  # Determine SETPOINT scene
  case mode
  when 'HEAT'
    setpoint_scene = 'Winter'
  when 'COOL'
    setpoint_scene = 'Summer'
  end
  #case Season_Name.state
  #when 'WINTER','AUTUMN'
  #  setpoint_scene = 'Winter'
  #when 'SPRING','SUMMER'
  #  setpoint_scene = 'Summer'
  #end

  [power_scene, setpoint_scene]
end

rule "hvac_startup" do
  on_start at_level: 80
  run do
    for item, value in ITEM_DEFAULT_VALUES
      item.state = value if item.state.nil?
    end
  end
end

rule "hvac_scene_changed" do
  changed Airtouch_Unit_Fujitsu_Scene_Power
  changed Airtouch_Unit_Fujitsu_Scene_Setpoint
  run do
    deploy_scenes(Airtouch_Unit_Fujitsu_Scene_Power.state, Airtouch_Unit_Fujitsu_Scene_Setpoint.state)
  end
end

rule "hvac_event_off" do
  changed Airtouch_Unit_Fujitsu_Power, to: OFF
  run do
    shared_cache[:hvacstate] = OFF
    shared_cache[:hvacsolaroverride] = OFF
    logger.info "HVAC: was turned OFF"
  end
end

# Force the HVAC off at sunrise, assuming it was manually turned on beforehand.
rule "hvac_sunrise_force_off" do
	channel "astro:sun:home:rise#event", attach: "sunrise"   
  run do
    hvac_log "Turning unit off due to SUNRISE event"
    Airtouch_Unit_Fujitsu_Power.ensure.off
  end
end
#rule "hvac_morning_force_off" do
#  every :day, at: '07:00'
#  run do
#    hvac_log "Turning unit off due to reached 7am"
#    Airtouch_Unit_Fujitsu_Power.ensure.off
#  end
#end

rule "hvac_automation" do
  every 1.minutes
  run do |event|
    solar_remain   = ForecastSolar_9Hens_Remaining.state
    inlet_temp     = Airtouch_Unit_Fujitsu_Inlet_Temp.state
    max_temp_today = BOM_Day1_MaximumTemperature.state
    cur_temp       = BOM_Day1_AirTemperature.state
    apparent_temp  = BOM_Day1_ApparentTemperature.state

    # Use forecasts and inlet temp to determine if we should HEAT or COOL
    if max_temp_today <= 20 || apparent_temp <= 7 || (inlet_temp <= 12 | '°C')
      mode = 'HEAT'
    elsif max_temp_today >= 27 || apparent_temp >= 28 || (inlet_temp >= 28 | '°C')
      mode = 'COOL'
    else
      mode = nil
    end

    # Overrides for normal solar automation.  Use inlet temp as gauge of general comfort level
    if (inlet_temp <= Hvac_Solar_Override_Trigger_Cool.state | '°C') || (inlet_temp >= Hvac_Solar_Override_Trigger_Heat.state | '°C')
      shared_cache[:hvacsolaroverride] = ON
    end

    if Airtouch_Unit_Fujitsu_Automation_Override.state == ON
      hvac_log "Manual override active"
    else
      if Airtouch_Unit_Fujitsu_Power.state == ON
        if mode.nil?
          hvac_log "Environmentals outside of automation bounds"
          if shared_cache[:hvacstate] == ON
            hvac_log "Temperatures (cur:#{cur_temp} inlet:#{inlet_temp} max:#{max_temp_today} app:#{apparent_temp}) outside of automation bounds"
            hvac_log "turning unit OFF due to automation state being ON"
            Airtouch_Unit_Fujitsu_Power.ensure.off
          end
        else
          hvac_log "Sunset = #{Sunset_Time.state}, Season = #{Season_Name.state}, Mode = #{mode}, SolarRemain = #{solar_remain}"
          if shared_cache[:hvacstate] == ON
            if shared_cache[:hvacsolaroverride] == ON
              if ((mode == 'COOL' && inlet_temp <= Hvac_Solar_Override_Normal_Heat.state | '°C') || 
                  (mode == 'HEAT' && inlet_temp >= Hvac_Solar_Override_Normal_Cool.state | '°C'))
                hvac_log "turn unit off NOW:  SOLAR OVERRIDE IS ACTIVE AND INLET TEMP #{inlet_temp} IS NOW WITHIN COMFORTABLE RANGE"
                Airtouch_Unit_Fujitsu_Power.ensure.off
              end
            else
              sunset_avg_solar_power = Solar_Total_Gen_Power.average_since(DUSK_AVG_MINS.minutes.ago)
              if (solar_remain && (solar_remain < 3 | 'kWh')) || ((sunset_avg_solar_power < Hvac_Solar_Power_Threshold_Dusk.state | 'W') || (Time.now > Sunset_Time.state))
                hvac_log "Solar energy (#{sunset_avg_solar_power} / 10min) too low OR we're past sunset time:  TURN HVAC UNIT OFF NOW"
                Airtouch_Unit_Fujitsu_Power.ensure.off
              end
            end
          end
        end
      else
        if mode.nil?
          hvac_log "Temperatures (cur:#{cur_temp} inlet:#{inlet_temp} max:#{max_temp_today} app:#{apparent_temp}) outside of automation bounds"
        elsif shared_cache[:hvacsolaroverride] == ON || Time.now > Sunrise_Time.state
          sunrise_avg_solar_power = Solar_Total_Gen_Power.average_since(DAWN_AVG_MINS.minutes.ago)
          hvac_log "Sunrise = #{Sunrise_Time.state}, Season = #{Season_Name.state}, Mode = #{mode}, SolarGen(30mAVG) = #{sunrise_avg_solar_power}, SolarRemain = #{solar_remain}, Temp = cur:#{cur_temp} inlet:#{inlet_temp} max:#{max_temp_today} app:#{apparent_temp}"
          if shared_cache[:hvacsolaroverride] == ON || (sunrise_avg_solar_power > Hvac_Solar_Power_Threshold_Dawn.state | 'W') || ((sunrise_avg_solar_power > Hvac_Solar_Power_Threshold_Dawn.state | 'W') && solar_remain && (solar_remain > 20 | 'kWh'))
            if shared_cache[:hvacsolaroverride] == ON
              hvac_log "turn unit on NOW:  SOLAR OVERRIDE IS ACTIVE BECAUSE INLET TEMP #{inlet_temp} IS OUT OF COMFORTABLE RANGE !!!"
            else
              hvac_log "turn unit on NOW:  Average solar generation / solar forecast / temp forecast exceeded thresholds"
            end
            power_scene, setpoint_scene = determine_automation_scenes(mode)
            Airtouch_Unit_Fujitsu_Scene_Power.update(power_scene)
            Airtouch_Unit_Fujitsu_Scene_Setpoint.update(setpoint_scene)
            Airtouch_Unit_Fujitsu_Mode.ensure.command(mode)
            Airtouch_Unit_Fujitsu_Power.ensure.on
            hvac_log "Starting timer to set automation state" 
            sleep 10
            shared_cache[:hvacstate] = ON
            hvac_log "Mode is #{Airtouch_Unit_Fujitsu_Mode.state}, :hvacstate is #{shared_cache[:hvacstate]}, hvacsolaroverride is #{shared_cache[:hvacsolaroverride]}"
          end
        end
      end
    end
  end
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions