[5GB DB] Slowness on first startup

Hello everyone, hello @pierre-gilles,

I’m talking about it on the forum before defining together what you want to see on GitHub :wink:
I’ve been feeling this for a while, but I was able to confirm it now by recording my screen.

Starting Gladys
If the Gladys container is stopped and then started again (after an update for example), the first loading of the graphs takes a lot of time.
This is certainly partly due to the size of the DB 4.5Go! But not only, since you explained that you only take a very small part of the data to speed up the display.

Demo video (1m30)
Note:

  1. The Gladys page is open at the beginning, but do not take it into account
  2. I (re) launch Gladys, then I refresh the gladys web page
  3. This is where we see the slowness and the high use of the « disk » (SD card at my place)

Interesting @lmilcent, thanks for the feedback with the video :slight_smile:

First, I see that all your graphs are « last hours » displays, and that’s LIVE data (not aggregated), so it’s possible that there are thousands of lines/hundreds of thousands depending on whether your sensors are verbose or not.

Could you:

  • Export your DB
  • Open it on your computer (with a tool like TablePlus)
  • Then, try to filter your table t_device_feature_state to see how many lines there are per sensor and per hour, the idea is to understand the volume of data we’re talking about.

Then, we’ll see if it’s a « normal » problem (in the sense that we just need to see how to reduce your data flow if it’s too large), or if on the Gladys side there are DB optimizations to look for :slight_smile:

Running your last query to check what’s wrong.

As before, the electrical outlets have the most events:

However, I wonder what the value at the very top is:

And for information, the aggregation seems very very very verbose for me (1-300 of ~8,510,037), possibly due to the history (the NaN bug itself):

It looks like a date!!

It’s been a long time since I suggested adding an option for each feature to specify whether you want to record all values or only state changes. And with that, update only the last_value of the device_feature table, which would allow you to have the date of the last value received to then be able to set an alert if no data is received for x seconds/minutes/etc.
If you select « Record only state changes » when a state change occurs, you send the old value before to clearly specify on the graphs that you remained at the same value for a certain period of time.

For info, this is what I did on the Netatmo integration by @damalgos and it works very well. No unnecessary values stored.

In the meantime, it’s a shame, but you can always go through Node-red first to sort the values to be recorded or not?

Ah, aren’t these outlets part of the devices that are way too verbose? I think some Zigbee devices send a value every 15-20 seconds even without a state change, which could explain why it’s so full when I imagine you haven’t turned your outlet on 375,000 times :stuck_out_tongue:

Feel free to do some SQL cleaning on your DB while waiting for a feature in Gladys that allows you to do it in the UI ^^

Oho, interesting, I didn’t think there were NaN values in the aggregated data too! That’s not normal!

I’ll add a clean in the next Gladys update, in the meantime, same as last time, you can clean these aggregated data that are clearly useless.

That’s true, I remember! Is there a feature request for that?

Mmmh I thought so but I’m not sure!!^^ I’ll double-check!!

Edit:
@pierre-gilles, I found it: Case à cochée "Ne pas enregistrer les nouvelles valeurs identiques" + Choix de durée de backup indépendant pour chaque Device Features - #2 par pierre-gilles

I’d be happy to take care of it as soon as possible (I did it for the Netatmo service that runs at home and it works perfectly). However, you would need to validate the operating principle so as not to start something and have to redo everything.

  • Front-end:

    • a checkbox on each feature « Record only state changes »,
    • this box is unchecked by default to remain as it is now,
  • Server-side:

    • if the « Record only state changes » box is checked, then:
      • add a column « only_keep_value_changes » in the t_device_feature table, this is 0 by default,
      • or when checked, add a parameter in device_param:
{
  "name": "[feature id]:only_keep_value_changes",
  "value": true
}
  • if the value of this parameter is true and the new value is the same as the previous one, we go through a new file server/lib/device/device.saveLastStateChanged.js to save in the t_device_feature table the saveLastStateChanged as you asked me at the time of the Netatmo dev:
const db = require('../../models');
const logger = require('../../utils/logger');
const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../utils/constants');

/**
 * @description Save new device feature state in DB.
 * @param {Object} deviceFeature - A DeviceFeature object.
 * @example
 * saveLastValueChanged({
 *   id: 'fc235c88-b10d-4706-8b59-fef92a7119b2',
 *   selector: 'my-light'
 * });
 */
async function saveLastStateChanged(deviceFeature) {
  // logger.debug(`device.saveLastStateChanged of deviceFeature ${deviceFeature.selector}`);
  const now = new Date();
  // save local state in RAM
  this.stateManager.setState('deviceFeature', deviceFeature.selector, {
    last_value_changed: now,
  });
  await db.sequelize.transaction(async (t) => {
    // update deviceFeature lastValue in DB
    await db.DeviceFeature.update(
      {
        last_value_changed: now,
      },
      {
        where: {
          id: deviceFeature.id,
        },
      },
      {
        transaction: t,
      },
    );
  });

  // send websocket event
  this.eventManager.emit(EVENTS.WEBSOCKET.SEND_ALL, {
    type: WEBSOCKET_MESSAGE_TYPES.DEVICE.NEW_STATE_NO_CHANGED,
    payload: {
      device_feature_selector: deviceFeature.selector,
      last_value_changed: now,
    },
  });
}

module.exports = {
  saveLastStateChanged,
};
  • When the value changes again, we go back through the function of the file server/lib/device/device.saveState.js that we must modify to re-record, before the new value, the old value already in the database, to have an adjusted curve:
const db = require('../../models');
const logger = require('../../utils/logger');
const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../utils/constants');
const { BadParameters } = require('../../utils/coreErrors');

const DEFAULT_OPTIONS = {
  skip: 0,
  order_dir: 'DESC',
  order_by: 'created_at',
}

/**
 * @description Save new device feature state in DB.
 * @param {Object} deviceFeature - A DeviceFeature object.
 * @param {number} newValue - The new value of the deviceFeature to save.
 * @example
 * saveState({
 *   id: 'fc235c88-b10d-4706-8b59-fef92a7119b2',
 *   selector: 'my-light'
 * }, 12);
 */
async function saveState(deviceFeature, newValue) {
  if (Number.isNaN(newValue)) {
    throw new BadParameters(`device.saveState of NaN value on ${deviceFeature.selector}`);
  }
  const optionsWithDefault = Object.assign({}, DEFAULT_OPTIONS);

  // logger.debug(`device.saveState of deviceFeature ${deviceFeature.selector}`);
  const now = new Date();
  const previousDeviceFeature = this.stateManager.get('deviceFeature', deviceFeature.selector);
  const previousDeviceFeatureValue = previousDeviceFeature ? previousDeviceFeature.last_value : null;

  const previousDeviceFeatureLastValueChanged = previousDeviceFeature ? previousDeviceFeature.last_value_changed : null;

  const deviceFeaturesState = await db.DeviceFeatureState.findOne({
    attributes: ['device_feature_id', 'value', 'created_at'],
    order: [[optionsWithDefault.order_by, optionsWithDefault.order_dir]],
    where: {
      device_feature_id: deviceFeature.id,
    },
  });
  const previousDeviceFeatureStateLastValueChanged = deviceFeaturesState ? deviceFeaturesState.created_at : 0;

  // save local state in RAM
  this.stateManager.setState('deviceFeature', deviceFeature.selector, {
    last_value: newValue,
    last_value_changed: now,
  });
  // update deviceFeature lastValue in DB
  await db.DeviceFeature.update(
    {
      last_value: newValue,
      last_value_changed: now,
    },
    {
      where: {
        id: deviceFeature.id,
      },
    },
  );
  // if the deviceFeature should keep history, we save a new deviceFeatureState
  if (deviceFeature.keep_history) {
    // if the previous created deviceFeatureState is different of deviceFeature
    // last value changed, we save a new deviceFeatureState of old state
    if (previousDeviceFeatureLastValueChanged - previousDeviceFeatureStateLastValueChanged > 0) {
      await db.DeviceFeatureState.create({
        device_feature_id: deviceFeature.id,
        value: previousDeviceFeatureValue,
        created_at: previousDeviceFeatureLastValueChanged,
      });
    }
    await db.DeviceFeatureState.create({
      device_feature_id: deviceFeature.id,
      value: newValue,
    });
  }
  // });

  // send websocket event
  this.eventManager.emit(EVENTS.WEBSOCKET.SEND_ALL, {
    type: WEBSOCKET_MESSAGE_TYPES.DEVICE.NEW_STATE,
    payload: {
      device_feature_selector: deviceFeature.selector,
      last_value: newValue,
      last_value_changed: now,
    },
  });

  // check if there is a trigger matching
  this.eventManager.emit(EVENTS.TRIGGERS.CHECK, {
    type: EVENTS.DEVICE.NEW_STATE,
    device_feature: deviceFeature.selector,
    previous_value: previousDeviceFeatureValue,
    last_value: newValue,
    last_value_changed: now,
  });
}

module.exports = {
  saveState,
}

For example, if we receive for a consumed power:

{
  value: 1014.7,
  created_at: 2022-04-19 04:20:13.277 +00:00
},
{
  value: 1014.9,
  created_at: 2022-04-19 04:30:13.277 +00:00
},
{
  value: 1014.9,
  created_at: 2022-04-19 04:40:13.277 +00:00
},
{
  value: 1014.9,
  created_at: 2022-04-19 04:50:13.277 +00:00
},
{
  value: 1014.9,
  created_at: 2022-04-19 05:00:13.277 +00:00
},
{
  value: 1014.9,
  created_at: 2022-04-19 05:10:13.277 +00:00
},
{
  value: 78.0,
  created_at: 2022-04-19 05:20:13.277 +00:00
}

We will only find in the DB

{
  value: 1014.7,
  created_at: 2022-04-19 04:20:13.277 +00:00
},
{
  value: 1014.9,
  created_at: 2022-04-19 04:30:13.277 +00:00
},
// here we did not save the identical values
{
  value: 1014.9,
  created_at: 2022-04-19 05:10:13.277 +00:00
},
// After writing the value below, we sent
{
  value: 78.0,
  created_at: 2022-04-19 05:20:13.277 +00:00
}

Do you see it differently? In that case, would you roughly have the line to follow?

For the second point on the history,

  • Front side:

    • a selection field on each feature « Keep the history of the feature states » offering the same choices as in the Settings/Systems tab,
    • The field is selected by default on the general configuration of Gladys (except for cameras)
  • Server side:

    • Either:
      • we add a « delay_keep_history » column to the t_device_feature table by default on the general configuration of Gladys?
      • or we modify the « keep_history » column to be able to put the value directly?
      • or we use a parameter, if it exists then we take this parameter into account for the feature values?

I’m modifying the db to avoid storing everything in history, for example, the aqara sensors. In my opinion, it’s pointless to historize atmospheric pressure in each room since the values will be identical.

My DB is 2GB.

But it’s true that we’ve already had this discussion somewhere on the forum.

And finally a PR for the scenes to be able to send an alert in case of non-receipt of a value during a time period on a device.
Example:
Trigger: « Every 10 minutes » (Existing)
Action: « Retrieve the last update date of the (feature(s)) » (last_value_changed) with selection of one feature per device, for example the battery (To be developed)
Action: « Continue only if more than 10 minutes » (To be developed - subtraction)
Action: « Send a message » (with variable(s)) (last_value_changed will probably need to be added)

On my side, I found the feature request, you indeed voted for it as well ^^ But no answer on it. I don’t find any other similar topics …

If you have time to read the 2 previous posts and give your opinion on the proposal, it will be with pleasure ^^

Thank you all for your responses. I’ll leave you to it and thank you for working on this :blush:

I am on vacation without easy access to my PC or the database, so I will do all that when I get back.

Hi @lmilcent, a small gift for when you get back from vacation, I made a quick PR to clean the t_device_feature_state_aggregate table of NaN values :slight_smile:

It’s not included in the release I’m going to make today, it will go into the next one.

To start, let’s keep it simple and do it well, I think there’s nothing to do on the server side.

We just need to add in the interface a button « Keep the history of this device’s values »: checked or not.

This boolean will modify the keep_history attribute which already exists and already works on the server side. It’s 2 hours of work all in, the time to make the PR, and there’s no specification to write because the functionality already exists :slight_smile:

Adding per-device history complicates the product quite a bit and needs thought (what about aggregation? Which takes precedence: the global setting or the per-device setting?). In short, in my opinion let’s stay focused on the essentials for now.

Great, thanks :slight_smile:
Nice surprise to come back to after vacation!

So, no aggregation of values over 24h, since only the last value is kept, right ?

There is no history at all then, only one value is kept, the last one :slight_smile:

How do you do that?

I open the DB with DB Browser and edit the keep_history column in the device_feature table

@lmilcent DB fix deployed!

I noticed that Gladys worked for a very long time, so I infer that it cleaned the data, although I haven’t checked yet.
I manually ran a VACUUM which freed up 100 MB.

Great :slight_smile: We’ll see how it looks at the next save