1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400
|
/* ========================================================================= */
/**
* @file wlmnetgraph.c
*
* Network usage graph dock-app for wlmaker.
*
* @copyright
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "wlm_graph_shared.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libbase/libbase.h>
/** Application name. */
static const char _app_name[] = "wlmnetgraph";
/** Application help. */
static const char _app_help[] =
"Displays network activity as a scrolling graph.\n"
"\n"
"Shows three activity categories:\n"
" - Receive: incoming traffic (blue)\n"
" - Transmit: outgoing traffic (cyan)\n"
" - Bidirectional: combined traffic (red)\n"
"\n"
"The label displays current throughput. Scale auto-adjusts to peak rate.";
/* == Definitions ========================================================== */
/** Number of network categories tracked. */
#define NET_CATEGORY_COUNT 3
/** Minimum peak rate for scaling (1 MB/s in bytes). */
#define PEAK_RATE_MIN (1024ULL * 1024ULL)
/** Peak rate decay divisor (~1% decay per sample). */
#define PEAK_DECAY_DIVISOR 128
/** Threshold below which peak rate doesn't decay. */
#define PEAK_DECAY_THRESHOLD 1024
/** Line buffer size for /proc/net/dev parsing. */
#define LINE_BUFFER_SIZE 512
/** Number of header lines to skip in /proc/net/dev. */
#define HEADER_LINE_COUNT 2
/** Maximum length of the label string. */
#define LABEL_BUFFER_SIZE 16
/** Bytes per kilobyte. */
#define BYTES_PER_KB 1024ULL
/** Bytes per megabyte. */
#define BYTES_PER_MB (1024ULL * 1024ULL)
/** Bytes per gigabyte. */
#define BYTES_PER_GB (1024ULL * 1024ULL * 1024ULL)
/** Network category indices (maps to heat-map colors). */
enum {
/** Receive activity. */
NET_CATEGORY_IN = 0,
/** Transmit activity. */
NET_CATEGORY_OUT = 1,
/** Bidirectional activity. */
NET_CATEGORY_IN_OUT = 2,
};
/** Scale entry pairing a byte value with its display label. */
typedef struct {
/** Byte threshold for this scale. */
unsigned long long bytes;
/** Display label (e.g., "1 MB/s"). */
const char *label;
} scale_entry_t;
/** Available scale values (1/10/100 × KB/MB/GB). */
static const scale_entry_t _scales[] = {
{ 1ULL * BYTES_PER_KB, "1 KB/s" },
{ 10ULL * BYTES_PER_KB, "10 KB/s" },
{ 100ULL * BYTES_PER_KB, "100 KB/s" },
{ 1ULL * BYTES_PER_MB, "1 MB/s" },
{ 10ULL * BYTES_PER_MB, "10 MB/s" },
{ 100ULL * BYTES_PER_MB, "100 MB/s" },
{ 1ULL * BYTES_PER_GB, "1 GB/s" },
{ 10ULL * BYTES_PER_GB, "10 GB/s" },
{ 100ULL * BYTES_PER_GB, "100 GB/s" },
};
/** Number of scale entries. */
#define SCALE_COUNT (sizeof(_scales) / sizeof(_scales[0]))
/** Raw rate history entry for regeneration. */
typedef struct {
/** Receive rate (bytes per interval). */
unsigned long long rx_rate;
/** Transmit rate (bytes per interval). */
unsigned long long tx_rate;
} rate_history_t;
/** State for the network graph (mutable runtime data). */
typedef struct {
/** Open file handle to /proc/net/dev. */
FILE *proc_fp;
/** Previous absolute RX byte count for computing rate. */
unsigned long long prev_rx_bytes;
/** Previous absolute TX byte count for computing rate. */
unsigned long long prev_tx_bytes;
/** Peak observed rate (for auto-scaling). */
unsigned long long peak_rate;
/** Index into _scales for current display scale. */
size_t scale_index;
/** Raw rate history for regeneration (ring buffer, newest first). */
rate_history_t history[WLM_GRAPH_REGENERATE_HISTORY_MAX];
/** Current write position in history (next slot to write). */
uint32_t history_index;
/** Number of valid entries in history. */
uint32_t history_num;
} netgraph_state_t;
/* == Label ================================================================ */
/* ------------------------------------------------------------------------- */
/**
* Finds index of smallest scale >= val.
*
* @param val Value to find ceiling scale for.
* @return Index into _scales array.
*/
static size_t _scale_index_ceil(const unsigned long long val)
{
for (size_t i = 0; i < SCALE_COUNT; i++) {
if (_scales[i].bytes >= val) {
return i;
}
}
// Beyond largest scale, return last index.
return SCALE_COUNT - 1;
}
/* ------------------------------------------------------------------------- */
/** Returns the scale label. */
static const char *_label_fn(void *app_state)
{
netgraph_state_t *state = app_state;
return _scales[state->scale_index].label;
}
/* == Cleanup ============================================================== */
/* ------------------------------------------------------------------------- */
/** Frees all allocated state. */
static void _state_free(void *app_state)
{
netgraph_state_t *state = app_state;
if (NULL != state->proc_fp) {
fclose(state->proc_fp);
state->proc_fp = NULL;
}
}
/* == Network statistics =================================================== */
/* ------------------------------------------------------------------------- */
/**
* Reads network statistics from /proc/net/dev.
*
* Parses all interfaces (excluding loopback) and sums RX/TX bytes.
* Computes rate since last read and scales to 0-255 based on peak rate.
*
* @param app_state App state (netgraph_state_t pointer).
* @param values Buffer to fill (may reallocate data/num).
*
* @return WLM_GRAPH_READ_OK on success, WLM_GRAPH_READ_ERROR on error.
*/
static wlm_graph_read_result_t _stats_read_fn(void *app_state, wlm_graph_values_t *values)
{
netgraph_state_t * const state = app_state;
FILE * const fp = state->proc_fp;
if (NULL == fp) {
return WLM_GRAPH_READ_ERROR;
}
// Reallocate buffer if size doesn't match.
if (NET_CATEGORY_COUNT != values->num) {
uint8_t *new_buf = realloc(values->data, NET_CATEGORY_COUNT);
if (NULL == new_buf) {
return WLM_GRAPH_READ_ERROR;
}
values->data = new_buf;
values->num = NET_CATEGORY_COUNT;
}
rewind(fp);
unsigned long long total_rx_bytes = 0;
unsigned long long total_tx_bytes = 0;
// Skip header lines in /proc/net/dev.
char line[LINE_BUFFER_SIZE];
for (int i = 0; i < HEADER_LINE_COUNT; i++) {
if (NULL == fgets(line, sizeof(line), fp)) {
return WLM_GRAPH_READ_ERROR;
}
}
while (NULL != fgets(line, sizeof(line), fp)) {
// Find interface name (ends with ':').
char * const colon = strchr(line, ':');
if (NULL == colon) {
continue;
}
// Extract interface name and skip loopback.
*colon = '\0';
char *iface = line;
while (' ' == *iface) iface++; // Trim leading spaces.
if (0 == strcmp(iface, "lo")) {
continue;
}
// Parse stats after colon.
// Format: rx_bytes rx_packets rx_errs rx_drop rx_fifo rx_frame
// rx_compressed rx_multicast tx_bytes tx_packets ...
unsigned long long rx_bytes, tx_bytes, skip;
const int parsed = sscanf(colon + 1,
"%llu %llu %llu %llu %llu %llu %llu %llu %llu",
&rx_bytes, &skip, &skip, &skip, &skip, &skip, &skip, &skip,
&tx_bytes);
if (9 == parsed) {
total_rx_bytes += rx_bytes;
total_tx_bytes += tx_bytes;
}
}
// Compute rate (bytes since last read).
unsigned long long rx_rate = 0;
unsigned long long tx_rate = 0;
if (total_rx_bytes >= state->prev_rx_bytes) {
rx_rate = total_rx_bytes - state->prev_rx_bytes;
}
if (total_tx_bytes >= state->prev_tx_bytes) {
tx_rate = total_tx_bytes - state->prev_tx_bytes;
}
state->prev_rx_bytes = total_rx_bytes;
state->prev_tx_bytes = total_tx_bytes;
// Store raw rates in history for regeneration.
state->history[state->history_index].rx_rate = rx_rate;
state->history[state->history_index].tx_rate = tx_rate;
state->history_index = (state->history_index + 1) % WLM_GRAPH_REGENERATE_HISTORY_MAX;
if (state->history_num < WLM_GRAPH_REGENERATE_HISTORY_MAX) {
state->history_num++;
}
// Update peak rate for auto-scaling (with decay to adapt to lower rates).
const unsigned long long total_rate = rx_rate + tx_rate;
const size_t prev_scale_index = state->scale_index;
if (total_rate > state->peak_rate) {
state->peak_rate = total_rate;
state->scale_index = _scale_index_ceil(state->peak_rate);
} else if (state->peak_rate > PEAK_DECAY_THRESHOLD) {
state->peak_rate -= state->peak_rate / PEAK_DECAY_DIVISOR;
state->scale_index = _scale_index_ceil(state->peak_rate);
}
// Ensure minimum peak to avoid division by zero.
if (state->peak_rate < PEAK_RATE_MIN) {
state->peak_rate = PEAK_RATE_MIN;
state->scale_index = _scale_index_ceil(PEAK_RATE_MIN);
}
// Scale rates to 0-255 based on peak (clamped to peak).
// IN_OUT uses min to show bidirectional activity (both directions active).
const unsigned long long peak = state->peak_rate;
values->data[NET_CATEGORY_IN] = (uint8_t)((BS_MIN(rx_rate, peak) * 255) / peak);
values->data[NET_CATEGORY_OUT] = (uint8_t)((BS_MIN(tx_rate, peak) * 255) / peak);
values->data[NET_CATEGORY_IN_OUT] = (uint8_t)((BS_MIN(rx_rate, tx_rate) * 255) / peak);
// Request regeneration if scale changed.
if (state->scale_index != prev_scale_index) {
return WLM_GRAPH_READ_OK_AND_REGENERATE;
}
return WLM_GRAPH_READ_OK;
}
/* ------------------------------------------------------------------------- */
/**
* Regenerates historical samples at the current scale.
*
* @param app_state App state (netgraph_state_t pointer).
* @param samples Array of sample buffers to regenerate (newest first).
* @param samples_num Number of samples to regenerate.
*/
static void _regenerate_fn(void *app_state, wlm_graph_values_t *samples, const uint32_t samples_num)
{
netgraph_state_t * const state = app_state;
const unsigned long long peak = state->peak_rate;
uint32_t samples_num_regenerate = 0;
if (state->history_num > 1) {
// Determine how many samples have history available (excludes current).
const uint32_t samples_num_available = state->history_num - 1;
samples_num_regenerate = BS_MIN(samples_num, samples_num_available);
// Regenerate samples that have history.
// samples[0] = newest historical (before current), samples[N-1] = oldest.
// History index wraps at array size (WLM_GRAPH_REGENERATE_HISTORY_MAX),
// not history_num. The samples_num_available limit ensures we don't
// read invalid entries.
// -2: convert from next-write to most-recent (-1), skip current sample (-1).
// +WLM_GRAPH_REGENERATE_HISTORY_MAX: ensures modulo works when subtracting.
const uint32_t history_offset =
(state->history_index + WLM_GRAPH_REGENERATE_HISTORY_MAX) - 2;
for (uint32_t i = 0; i < samples_num_regenerate; i++) {
const uint32_t hist_index =
(history_offset - i) % WLM_GRAPH_REGENERATE_HISTORY_MAX;
const rate_history_t *h = &state->history[hist_index];
// Regenerate scaled values at current peak.
samples[i].data[NET_CATEGORY_IN] =
(uint8_t)((BS_MIN(h->rx_rate, peak) * 255) / peak);
samples[i].data[NET_CATEGORY_OUT] =
(uint8_t)((BS_MIN(h->tx_rate, peak) * 255) / peak);
samples[i].data[NET_CATEGORY_IN_OUT] =
(uint8_t)((BS_MIN(h->rx_rate, h->tx_rate) * 255) / peak);
}
}
// Clear samples without available history.
for (uint32_t i = samples_num_regenerate; i < samples_num; i++) {
memset(samples[i].data, 0, samples[i].num);
}
}
/* == Main program ========================================================= */
/** Main program. */
int main(const int argc, const char **argv)
{
netgraph_state_t state = {};
state.proc_fp = fopen("/proc/net/dev", "r");
if (NULL == state.proc_fp) {
bs_log(BS_ERROR | BS_ERRNO, "Failed to open /proc/net/dev");
return EXIT_FAILURE;
}
// Prime prev values so first real sample computes proper delta.
// Reset peak and history after priming (first read sets peak to total bytes).
{
wlm_graph_values_t values = {};
_stats_read_fn(&state, &values);
free(values.data);
state.peak_rate = 0;
state.history_index = 0;
state.history_num = 0;
}
const wlm_graph_app_config_t config = {
.app_name = _app_name,
.app_help = _app_help,
.accumulate_mode = WLM_GRAPH_ACCUMULATE_MODE_INDEPENDENT,
.stats_read_fn = _stats_read_fn,
.regenerate_fn = _regenerate_fn,
.app_state = &state,
.state_free_fn = _state_free,
.label_fn = _label_fn,
};
return wlm_graph_app_run(argc, argv, &config);
}
/* == End of wlmnetgraph.c ================================================= */
|