What is an acceptable loop time? (performance)

I’m having an ESP32-C3 doing decibel measurements with an I2S microphone ( GitHub - stas-sl/esphome-sound-level-meter ).

The debug log loop time shows it takes 6000+ ms and the web interface is responding slowly. (15+seconds before all values are loaded)

Another board (same ESP32-C3 from Olimex) runs the bluetooth Proxy for home assistant and has a loop time of 30ms, the web interface is fast there.

Is I2S audio so heavy or is the ESP32-C3 not performant enough? Would a regular ESP32 or an ESP32-S3 be faster?

This is the YAML of the “fast” ESP32-C3 that only does the bluetooth proxy, loop time 30ms:

substitutions:
  display_name: bleproxy

esphome:
  name: ${display_name}
  name_add_mac_suffix: true
  project:
    name: "ble_proxy"
    version: "1.0.0"
  platformio_options:
    board_build.mcu: esp32c3
    board_build.variant: esp32c3  
  includes:
    # should contain single line: #include <esp_task_wdt.h>
    - wdt_include.h
  on_boot:
    then:
      - lambda: !lambda |-
          // increase watchdog timeout
          esp_task_wdt_init(90, false);

preferences:
    flash_write_interval: 10min      

esp32:
  variant: ESP32C3
  board: esp32dev
  framework:
    type: esp-idf
    sdkconfig_options:
      CONFIG_BT_BLE_50_FEATURES_SUPPORTED: y
      CONFIG_BT_BLE_42_FEATURES_SUPPORTED: y
      # CONFIG_COMPILER_OPTIMIZATION_PERF: y      
      CONFIG_ESP_TASK_WDT_TIMEOUT_S: "90"
      CONFIG_COMPILER_OPTIMIZATION_SIZE: y    


status_led:  
  pin: 
    number: GPIO8
    inverted: true

logger: 
  # logs:
  #   component: ERROR
  #level: INFO

debug:
  update_interval: 1s

api:

ota:
  safe_mode: true
  password: !secret ota_password  

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true
  reboot_timeout: 60s

  ap:
    ssid: !secret ap_ssid
    password: !secret ap_pass

esp32_ble_tracker:
  # scan_parameters:
    # interval: 1100ms
    # window: 1100ms
    # active: false
    # continuous: false

bluetooth_proxy:
  active: true

sensor:
  - platform: internal_temperature
    name: "${display_name} Internal Temperature" 
    update_interval: 1s 
  - platform: wifi_signal
    name: "${display_name} WiFi Signal"
    update_interval: 1s 
  - platform: uptime
    name: "${display_name} uptime"
    update_interval: 1s 
  - platform: debug
    free:
      name: "${display_name} Heap Free"     
    block:
      name: "${display_name} Heap Max Block"
    loop_time:
      name: "${display_name} Loop Time"

text_sensor:
  - platform: version
    hide_timestamp: true
    name: "${display_name} ESPHome Version"
  - platform: wifi_info
    ip_address:
      name: "${display_name} IP Address"
      icon: mdi:wifi      
    ssid:
      name: "${display_name} Connected SSID"
      icon: mdi:wifi-strength-2
    dns_address:
      name: "${display_name} DNS Address"    
  - platform: debug
    device:
      name: "${display_name} Device Info"
    reset_reason:
      name: "${display_name} Reset Reason"

button:
  - platform: safe_mode
    name: "${display_name} (Safe Mode)"

web_server:
  auth:
    username: !secret web_username
    password: !secret web_password

time:
  - platform: sntp
    id: sntp_time
    timezone: Europe/Amsterdam
    servers:
     - 0.pool.ntp.org
     - 1.pool.ntp.org
     - 2.pool.ntp.org    

This is the YAML of the decibel meter, the slow ESP32-C3 (6000+ms loop time):

substitutions:
  display_name: decibelMeter

esphome:
  name: ${display_name}
  name_add_mac_suffix: true
  project:
    name: "decibel_meter"
    version: "1.0.0"
  platformio_options:
    board_build.mcu: esp32c3
    board_build.variant: esp32c3  
  includes:
    # should contain single line: #include <esp_task_wdt.h>
    - wdt_include.h
  on_boot:
    then:
      - lambda: !lambda |-
          // increase watchdog timeout
          esp_task_wdt_init(90, false);

external_components:
  - source: github://stas-sl/esphome-sound-level-meter  # add @tag if you want to use a specific version (e.g @v1.0.0)


esp32:
  variant: ESP32C3
  board: esp32dev
  framework:
    type: esp-idf
    sdkconfig_options:
      CONFIG_BT_BLE_50_FEATURES_SUPPORTED: y
      CONFIG_BT_BLE_42_FEATURES_SUPPORTED: y
      CONFIG_COMPILER_OPTIMIZATION_PERF: y      
      CONFIG_ESP_TASK_WDT_TIMEOUT_S: "90"


status_led:  
  pin: 
    number: GPIO8
    inverted: true

logger: 
  logs:
    component: ERROR # filters out these logs: Component api took a long time for an operation (424 ms).
  #level: INFO

debug:
  update_interval: 120s

api:

ota:
  safe_mode: true
  password: !secret ota_password  

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true
  reboot_timeout: 60s

  ap:
    ssid: !secret ap_ssid
    password: !secret ap_pass

i2s:
  bck_pin: 4
  ws_pin: 5
  din_pin: 6
  sample_rate: 48000            # default: 48000
  bits_per_sample: 32           # default: 32
  dma_buf_count: 8              # default: 8
  dma_buf_len: 256              # default: 256
  use_apll: true                # default: false

  # right shift samples.
  # for example if mic has 24 bit resolution, and
  # i2s configured as 32 bits, then audio data will be aligned left (MSB)
  # and LSB will be padded with zeros, so you might want to shift them right by 8 bits
  bits_shift: 8                 # default: 0

sound_level_meter:
  id: sound_level_meter1

  # update_interval specifies over which interval to aggregate audio data
  # you can specify default update_interval on top level, but you can also override
  # it further by specifying it on sensor level
  update_interval: 5s           # default: 60s

  # you can disable (turn off) component by default (on boot)
  # and turn it on later when needed via sound_level_meter.turn_on/toggle actions;
  # when used with switch it might conflict/being overriden by
  # switch state restoration logic, so you have to either disable it in
  # switch config and then is_on property here will have effect, 
  # or completely rely on switch state restoration/initialization and 
  # any value set here will be ignored
  is_on: true                   # default: true

  # buffer_size is in samples (not bytes), so for float data type
  # number of bytes will be buffer_size * 4
  buffer_size: 1024             # default: 1024

  # ignore audio data at startup for this long
  warmup_interval: 500ms        # default: 500ms

  # audio processing runs in a separate task, you can change its settings below
  task_stack_size: 4096         # default: 4096
  task_priority: 2              # default: 2
  task_core: 1               # default: 1

  # see your mic datasheet to find sensitivity and reference SPL.
  # those are used to convert dB FS to db SPL
  mic_sensitivity: -26dB        # default: empty
  mic_sensitivity_ref: 94dB     # default: empty
  # additional offset if needed
  offset: 0dB                   # default: empty


  groups:
    - filters:
        # for now only SOS filter type is supported, see math/filter-design.ipynb
        # to learn how to create or convert other filter types to SOS
        - type: sos
          coeffs:
            # INMP441:
            #      b0            b1           b2          a1            a2
            - [ 1.0019784 , -1.9908513  , 0.9889158 , -1.9951786  , 0.99518436]

      # nested groups
      groups:
        # group 1.1 (no weighting)
        - sensors:
            # 'eq' type sensor calculates Leq (average) sound level over specified period
            - type: eq
              name: ${display_name}_LZeq_1min
              id: ${display_name}_LZeq_1min
              unit_of_measurement: dB(Z)

            # 'max' sensor type calculates Lmax with specified window_size.
            # for example, if update_interval is 60s and window_size is 1s
            # then it will calculate 60 Leq values for each second of audio data
            # and the result will be max of them
            - type: max
              name: ${display_name} LZmax_1s_1min
              id: ${display_name}_LZmax_1s_1min
              window_size: 1s
              unit_of_measurement: dB(Z)

            # same as 'max', but 'min'
            - type: min
              name: ${display_name} LZmin_1s_1min
              id: ${display_name}_LZmin_1s_1min
              window_size: 1s
              unit_of_measurement: dB(Z)

            # it finds max single sample over whole update_interval
            - type: peak
              name: ${display_name} LZpeak_1min
              id: ${display_name}_LZpeak_1min
              unit_of_measurement: dB(Z)

        # group 1.2 (A-weighting)
        - filters:
            # for now only SOS filter type is supported, see math/filter-design.ipynb
            # to learn how to create or convert other filter types to SOS
            - type: sos
              coeffs:
                # A-weighting:
                #       b0           b1            b2             a1            a2
                - [ 0.16999495 ,  0.741029   ,  0.52548885 , -0.11321865 , -0.056549273]
                - [ 1.         , -2.00027    ,  1.0002706  , -0.03433284 , -0.79215795 ]
                - [ 1.         , -0.709303   , -0.29071867 , -1.9822421  ,  0.9822986  ]
          sensors:
            - type: eq
              name: ${display_name} LAeq_1min_1s
              id: ${display_name}_LAeq_1min_1s
              unit_of_measurement: dB(A)
              update_interval: 1s
            - type: eq
              name: ${display_name} LAeq_1min
              id: ${display_name}_LAeq_1min
              unit_of_measurement: dB(A)
            - type: max
              name: ${display_name} LAmax_1s_1min
              id: ${display_name}_LAmax_1s_1min
              window_size: 1s
              unit_of_measurement: dB(A)
            - type: min
              name: ${display_name} LAmin_1s_1min
              id: ${display_name}_LAmin_1s_1min
              window_size: 1s
              unit_of_measurement: dB(A)
            - type: peak
              name: ${display_name} LApeak_1min
              id: ${display_name}_LApeak_1min
              unit_of_measurement: dB(A)

        # group 1.3 (C-weighting)
        - filters:
            # for now only SOS filter type is supported, see math/filter-design.ipynb
            # to learn how to create or convert other filter types to SOS
            - type: sos
              coeffs:
                # C-weighting:
                #       b0             b1             b2             a1             a2
                - [-0.49651518  , -0.12296628  , -0.0076134163, -0.37165618   , 0.03453208  ]
                - [ 1.          ,  1.3294908   ,  0.44188643  ,  1.2312505    , 0.37899444  ]
                - [ 1.          , -2.          ,  1.          , -1.9946145    , 0.9946217   ]
          sensors:
            - type: eq
              name: ${display_name} 4LCeq_1min
              id: ${display_name}_LCeq_1min
              unit_of_measurement: dB(C)
            - type: max
              name: ${display_name} LCmax_1s_1min
              id: ${display_name}_LCmax_1s_1min
              window_size: 1s
              unit_of_measurement: dB(C)
            - type: min
              name: ${display_name} LCmin_1s_1min
              id: ${display_name}_LCmin_1s_1min
              window_size: 1s
              unit_of_measurement: dB(C)
            - type: peak
              name: ${display_name} LCpeak_1min
              id: ${display_name}_LCpeak_1min
              unit_of_measurement: dB(C)

# automation
# available actions:
#   - sound_level_meter.turn_on
#   - sound_level_meter.turn_off
#   - sound_level_meter.toggle
switch:
  - platform: template
    name: "${display_name} Sound Level Meter Switch"
    # if you want is_on property on component to have effect, then set
    # restore_mode to DISABLED, or alternatively you can use other modes
    # (more on them in esphome docs), then is_on property on the component will
    # be overriden by the switch
    restore_mode: DISABLED # ALWAYS_OFF | ALWAYS_ON | RESTORE_DEFAULT_OFF | RESTORE_DEFAULT_ON
    lambda: |-
      return id(sound_level_meter1).is_on();
    turn_on_action:
      - sound_level_meter.turn_on
    turn_off_action:
      - sound_level_meter.turn_off
  - platform: restart
    name: "${display_name} Restart"      

sensor:
  - platform: internal_temperature
    name: "${display_name} Internal Temperature" 
    update_interval: 300s 
  - platform: wifi_signal
    name: "${display_name} WiFi Signal"
    update_interval: 300s 
  - platform: uptime
    name: "${display_name} uptime"
  - platform: debug
    free:
      name: "${display_name} Heap Free"   
    block:
      name: "${display_name} Heap Max Block"
    loop_time:
      name: "${display_name} Loop Time"

text_sensor:
  - platform: version
    hide_timestamp: true
    name: "${display_name} ESPHome Version"
  - platform: wifi_info
    ip_address:
      name: "${display_name} IP Address"
      icon: mdi:wifi      
    ssid:
      name: "${display_name} Connected SSID"
      icon: mdi:wifi-strength-2
    dns_address:
      name: "${display_name} DNS Address"
  - platform: debug
    device:
      name: "${display_name} Device Info"
    reset_reason:
      name: "${display_name} Reset Reason"

button:
  - platform: safe_mode
    name: "${display_name} (Safe Mode)"

web_server:
  auth:
    username: !secret web_username
    password: !secret web_password

time:
  - platform: sntp
    id: sntp_time
    timezone: Europe/Amsterdam
    servers:
     - 0.pool.ntp.org
     - 1.pool.ntp.org
     - 2.pool.ntp.org    

I can’t comment much on low level performance details but I did notice the docs suggests loop time should be about 16ms.

It seems like a pretty demanding application so it wouldn’t surprise me if beefing up to an esp32 or S3 resolves things.

Can’t offer much more as above my pay grade;)

1 Like

Thanks for the documentation link. 16ms is quite a bit faster than the times I get.

I do notice after some trial and error that setting the log level to NONE shaves of about 2000ms, then the loop time is between 3100 and 4000ms.

After some more tweaking, disabling OTA and the built in web server shaved off some more seconds, now the loop timer is around 1200 to 2000 seconds.