Skip to content

fix(rate): initialise a stat that is new since the previous sample - #3721

Merged
nicolargo merged 1 commit into
nicolargo:developfrom
ntdatt812:fix/rate-new-stat-in-list
Sep 10, 2026
Merged

nicolargo merged 1 commit into
nicolargo:developfrom
ntdatt812:fix/rate-new-stat-in-list

Conversation

@ntdatt812

Copy link
Copy Markdown
Contributor

compute_rate_on_list() looks up each stat's previous sample by key and skips the stat when there is none:

for stat in stats:
    old = previous_by_key.get(stat[key])
    if old is not None:
        compute_rate(self, stat, old)

The dict path treats a missing previous as a first sample and initialises the stat. The list path skipped it. Interfaces, disks and VMs appear while Glances is running — a VPN comes up, a container starts, a disk is plugged in — so this is not only a start-up case. network, diskio and vms all carry rate fields on a list.

A skipped stat keeps the raw psutil counter in the delta field and gets no _gauge, no _rate_per_sec and no time_since_update:

round 2  eth0  {'bytes_recv': 0, 'time_since_update': 3.0e-05, 'bytes_recv_gauge': 1500, 'bytes_recv_rate_per_sec': 0}
round 2  wg0   {'bytes_recv': 8000000000}

Two consequences

The refresh publishes the lifetime total as one refresh of traffic. bytes_recv is documented as the delta, and for one sample it holds the since-boot counter instead. On a quiet link that first sample reads in gigabytes.

NetworkPlugin.update_views() raises. It assigns views/<iface>/bytes_recv_rate_per_sec directly, and update_views() in the base class builds views only from listkeys(i) — the fields a stat actually carries. With no rate field there is no view to decorate:

glances/plugins/network/__init__.py:237: KeyError: 'bytes_recv_rate_per_sec'

Driving the real plugin from a fake psutil.net_io_counters, one interface appearing is enough to reach it. diskio happens to survive because it guards with if f'{field}_rate_per_sec' in disk_views; network does not.

The change

One line: pass an empty previous rather than skipping.

compute_rate(self, stat, previous_by_key.get(stat[key], {}))

compute_rate already handles an empty previous — it is the branch a dict stat takes on its own first sample. The gauge is recorded, the rate reads 0, and the next sample measures a real delta. That also gets the stat to its second sample one refresh sooner, since the gauge it needs now exists the moment it appears.

I did not add a guard in network/update_views() instead, because the missing views are a symptom: the stat was left in a shape no other stat is ever in, and vms and diskio were publishing wrong numbers from the same cause without raising.

Verification

tests/test_rate_on_list.py, 5 tests: four on the shared rate code through a minimal list plugin, one end-to-end through NetworkPlugin for the KeyError.

Checked by breaking it — reverting the one changed line back to the skip:

test on the mutation
new stat does not publish its lifetime counter as a delta fails
new stat is given the same fields as every other fails
new stat measures from its own gauge on the next sample fails
network update_views survives an interface appearing fails, KeyError
a stat already present still measures its own delta passes — it is the regression guard

Full suite on Windows, before and after: the set of failing test names is byte-identical (63 pre-existing environment failures — the xmlrpc and browser-restful servers). ruff check and ruff format --check clean.

compute_rate_on_list() looked up each stat's previous sample by key and
skipped the stat entirely when there was none. Interfaces, disks and VMs
appear while Glances is running -- a VPN comes up, a container starts, a
disk is plugged in -- so a stat with no previous sample is not only a
start-up case, and the plugins that carry rate fields on a list are
network, diskio and vms.

A skipped stat kept the raw psutil counter in the delta field and got no
_gauge, no _rate_per_sec and no time_since_update at all. Two consequences:

  * That refresh published the whole since-boot total as if it were one
    refresh of traffic. On a quiet link the first sample of a new
    interface reads in gigabytes.

  * NetworkPlugin.update_views() assigns
    views/<iface>/bytes_recv_rate_per_sec directly. Views are built only
    from the fields a stat actually carries, so there was no view to
    decorate and the refresh raised KeyError: 'bytes_recv_rate_per_sec'.

Give compute_rate an empty previous instead of skipping, which is the
path a dict stat already takes on its own first sample: record the gauge,
publish a rate of 0, and measure a real delta on the next sample. The
stat then also reaches its second sample one refresh sooner, because the
gauge it needs is now recorded the moment it appears.

Verified on Windows with the full suite: the set of failing test names is
byte-identical before and after (63 pre-existing environment failures,
xmlrpc and browser-restful servers). Reverting the one changed line turns
4 of the 5 new tests red, including the KeyError one; the fifth is the
guard that the neighbouring stat is undisturbed, so it stays green.
@nicolargo
nicolargo merged commit 2db5e73 into nicolargo:develop Sep 10, 2026
17 of 19 checks passed
@nicolargo nicolargo added the bug label Sep 10, 2026
@nicolargo nicolargo added this to the Glances 4.5.7 milestone Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants