diff --git a/CHANGELOG.md b/CHANGELOG.md index d72d692..7d72922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ in the `title` particle. * network: request link stats and expose under tags `dl-speed` and `ul-speed` when `poll-interval` is set. +* new module: disk-io. [153]: https://codeberg.org/dnkl/yambar/issues/153 diff --git a/doc/meson.build b/doc/meson.build index e882f52..81188bf 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -6,6 +6,7 @@ scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) foreach man_src : ['yambar.1.scd', 'yambar.5.scd', 'yambar-decorations.5.scd', 'yambar-modules-alsa.5.scd', 'yambar-modules-backlight.5.scd', 'yambar-modules-battery.5.scd', 'yambar-modules-clock.5.scd', + 'yambar-modules-disk-io.5.scd', 'yambar-modules-foreign-toplevel.5.scd', 'yambar-modules-i3.5.scd', 'yambar-modules-label.5.scd', 'yambar-modules-mpd.5.scd', 'yambar-modules-network.5.scd', diff --git a/doc/yambar-modules-disk-io.5.scd b/doc/yambar-modules-disk-io.5.scd new file mode 100644 index 0000000..92ce449 --- /dev/null +++ b/doc/yambar-modules-disk-io.5.scd @@ -0,0 +1,85 @@ +yambar-modules-disk-io(5) + +# NAME +disk-io - This module keeps track of the amount of bytes being +read/written from/to disk. It can distinguish between all partitions +currently present in the machine. + +# TAGS + +[[ *Name* +:[ *Type* +:[ *Description* +| device +: string +: Name of the device being tracked (use the command *lsblk* to see these). + There is a special device, "Total", that reports the total stats + for the machine +| is_disk +: boolean +: whether or not the device is a disk (e.g. sda, sdb) or a partition + (e.g. sda1, sda2, ...). "Total" is advertised as a disk. +| read_speed +: int +: bytes read, in bytes/s +| write_speed +: int +: bytes written, in bytes/s +| ios_in_progress +: int +: number of ios that are happening at the time of polling + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:[ *Description* +| interval +: int +: no +: Refresh interval of disk's stats in ms (default=500). + Cannot be less then 500 ms + +# EXAMPLES + +This reports the total amount of bytes being read and written every second, +formatting in b/s, kb/s, mb/s, or gb/s, as appropriate. + +``` +bar: + left: + - disk-io: + interval: 1000 + content: + map: + conditions: + device == Total: + list: + items: + - string: {text: "Total read: "} + - map: + default: {string: {text: "{read_speed} B/s"}} + conditions: + read_speed > 1073741824: + string: {text: "{read_speed:gib} GB/s"} + read_speed > 1048576: + string: {text: "{read_speed:mib} MB/s"} + read_speed > 1024: + string: {text: "{read_speed:kib} KB/s"} + - string: {text: " | "} + - string: {text: "Total written: "} + - map: + default: {string: {text: "{write_speed} B/s"}} + conditions: + write_speed > 1073741824: + string: {text: "{write_speed:gib} GB/s"} + write_speed > 1048576: + string: {text: "{write_speed:mib} MB/s"} + write_speed > 1024: + string: {text: "{write_speed:kib} KB/s"} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) diff --git a/modules/disk-io.c b/modules/disk-io.c new file mode 100644 index 0000000..ee6da8a --- /dev/null +++ b/modules/disk-io.c @@ -0,0 +1,355 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include "../particles/dynlist.h" +#include "../bar/bar.h" +#include "../config-verify.h" +#include "../config.h" +#include "../log.h" +#include "../plugin.h" + +#define LOG_MODULE "disk-io" +#define LOG_ENABLE_DBG 0 +#define SMALLEST_INTERVAL 500 + +struct device_stats { + char *name; + bool is_disk; + + uint64_t prev_sectors_read; + uint64_t cur_sectors_read; + + uint64_t prev_sectors_written; + uint64_t cur_sectors_written; + + uint32_t ios_in_progress; + + bool exists; +}; + +struct private { + struct particle *label; + uint16_t interval; + tll(struct device_stats *) devices; +}; + +static bool +is_disk(char const *name) +{ + DIR *dir = opendir("/sys/block"); + if (dir == NULL) { + LOG_ERRNO("failed to read /sys/block directory"); + return false; + } + + struct dirent *entry; + + bool found = false; + while ((entry = readdir(dir)) != NULL) { + if (strcmp(name, entry->d_name) == 0) { + found = true; + break; + } + } + + closedir(dir); + return found; +} + +static struct device_stats* +new_device_stats(char const *name) +{ + struct device_stats *dev = malloc(sizeof(*dev)); + dev->name = strdup(name); + dev->is_disk = is_disk(name); + return dev; +} + +static void +free_device_stats(struct device_stats *dev) +{ + free(dev->name); + free(dev); +} + +static void +destroy(struct module *mod) +{ + struct private *m = mod->private; + m->label->destroy(m->label); + tll_foreach(m->devices, it) { + free_device_stats(it->item); + } + tll_free(m->devices); + free(m); + module_default_destroy(mod); +} + +static const char * +description(struct module *mod) +{ + return "disk-io"; +} + +static void +refresh_device_stats(struct private *m) +{ + FILE *fp = NULL; + char *line = NULL; + size_t len = 0; + ssize_t read; + + fp = fopen("/proc/diskstats", "r"); + if (NULL == fp) { + LOG_ERRNO("unable to open /proc/diskstats"); + return; + } + + /* + * Devices may be added or removed during the bar's lifetime, as external + * block devices are connected or disconnected from the machine. /proc/diskstats + * reports data only for the devices that are currently connected. + * + * This means that if we have a device that ISN'T in /proc/diskstats, it was + * disconnected, and we need to remove it from the list. + * + * On the other hand, if a device IS in /proc/diskstats, but not in our list, we + * must create a new device_stats struct and add it to the list. + * + * The 'exists' variable is what keep tracks of whether or not /proc/diskstats + * is still reporting the device (i.e., it is still connected). + */ + tll_foreach(m->devices, it) { + it->item->exists = false; + } + + while ((read = getline(&line, &len, fp)) != -1) { + /* + * For an explanation of the fields bellow, see + * https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats + */ + uint8_t major_number = 0; + uint8_t minor_number = 0; + char *device_name = NULL; + uint32_t completed_reads = 0; + uint32_t merged_reads = 0; + uint64_t sectors_read = 0; + uint32_t reading_time = 0; + uint32_t completed_writes = 0; + uint32_t merged_writes = 0; + uint64_t sectors_written = 0; + uint32_t writting_time = 0; + uint32_t ios_in_progress = 0; + uint32_t io_time = 0; + uint32_t io_weighted_time = 0; + uint32_t completed_discards = 0; + uint32_t merged_discards = 0; + uint32_t sectors_discarded = 0; + uint32_t discarding_time = 0; + uint32_t completed_flushes = 0; + uint32_t flushing_time = 0; + if (!sscanf(line, + " %" SCNu8 " %" SCNu8 " %ms %" SCNu32 " %" SCNu32 " %" SCNu64 " %" SCNu32 + " %" SCNu32 " %" SCNu32 " %" SCNu64 " %" SCNu32 " %" SCNu32 " %" SCNu32 + " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 + " %" SCNu32, + &major_number, &minor_number, &device_name, &completed_reads, + &merged_reads, §ors_read, &reading_time, &completed_writes, + &merged_writes, §ors_written, &writting_time, &ios_in_progress, + &io_time, &io_weighted_time, &completed_discards, &merged_discards, + §ors_discarded, &discarding_time, &completed_flushes, &flushing_time)) + { + LOG_ERR("unable to parse /proc/diskstats line"); + free(device_name); + goto exit; + } + + bool found = false; + tll_foreach(m->devices, it) { + struct device_stats *dev = it->item; + if (strcmp(dev->name, device_name) == 0){ + dev->prev_sectors_read = dev->cur_sectors_read; + dev->prev_sectors_written = dev->cur_sectors_written; + dev->ios_in_progress = ios_in_progress; + dev->cur_sectors_read = sectors_read; + dev->cur_sectors_written = sectors_written; + dev->exists = true; + found = true; + break; + } + } + + if (!found) { + struct device_stats *new_dev = new_device_stats(device_name); + new_dev->ios_in_progress = ios_in_progress; + new_dev->prev_sectors_read = sectors_read; + new_dev->cur_sectors_read = sectors_read; + new_dev->prev_sectors_written = sectors_written; + new_dev->cur_sectors_written = sectors_written; + new_dev->exists = true; + tll_push_back(m->devices, new_dev); + } + + free(device_name); + } + + tll_foreach(m->devices, it) { + if (!it->item->exists){ + free_device_stats(it->item); + tll_remove(m->devices, it); + } + } +exit: + fclose(fp); + free(line); +} + +static struct exposable * +content(struct module *mod) +{ + const struct private *p = mod->private; + uint64_t total_bytes_read = 0; + uint64_t total_bytes_written = 0; + uint32_t total_ios_in_progress = 0; + mtx_lock(&mod->lock); + struct exposable *tag_parts[p->devices.length + 1]; + int i = 0; + tll_foreach(p->devices, it) { + struct device_stats *dev = it->item; + uint64_t bytes_read = (dev->cur_sectors_read - dev->prev_sectors_read) * 512; + uint64_t bytes_written = (dev->cur_sectors_written - dev->prev_sectors_written) * 512; + + if (dev->is_disk){ + total_bytes_read += bytes_read; + total_bytes_written += bytes_written; + total_ios_in_progress += dev->ios_in_progress; + } + + struct tag_set tags = { + .tags = (struct tag *[]) { + tag_new_string(mod, "device", dev->name), + tag_new_bool(mod, "is_disk", dev->is_disk), + tag_new_int(mod, "read_speed", (bytes_read * 1000) / p->interval), + tag_new_int(mod, "write_speed", (bytes_written * 1000) / p->interval), + tag_new_int(mod, "ios_in_progress", dev->ios_in_progress), + }, + .count = 5, + }; + tag_parts[i++] = p->label->instantiate(p->label, &tags); + tag_set_destroy(&tags); + } + struct tag_set tags = { + .tags = (struct tag *[]) { + tag_new_string(mod, "device", "Total"), + tag_new_bool(mod, "is_disk", true), + tag_new_int(mod, "read_speed", (total_bytes_read * 1000) / p->interval), + tag_new_int(mod, "write_speed", (total_bytes_written * 1000) / p->interval), + tag_new_int(mod, "ios_in_progress", total_ios_in_progress), + }, + .count = 5, + }; + tag_parts[i] = p->label->instantiate(p->label, &tags); + tag_set_destroy(&tags); + mtx_unlock(&mod->lock); + + return dynlist_exposable_new(tag_parts, p->devices.length + 1, 0, 0); +} + +static int +run(struct module *mod) +{ + const struct bar *bar = mod->bar; + bar->refresh(bar); + struct private *p = mod->private; + while (true) { + struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}}; + + int res = poll(fds, sizeof(fds) / sizeof(*fds), p->interval); + + if (res < 0) { + if (EINTR == errno) + continue; + LOG_ERRNO("unable to poll abort fd"); + return -1; + } + + if (fds[0].revents & POLLIN) + break; + + mtx_lock(&mod->lock); + refresh_device_stats(p); + mtx_unlock(&mod->lock); + bar->refresh(bar); + } + + return 0; +} + +static struct module * +disk_io_new(uint16_t interval, struct particle *label) +{ + struct private *p = calloc(1, sizeof(*p)); + p->label = label; + p->interval = interval; + + struct module *mod = module_common_new(); + mod->private = p; + mod->run = &run; + mod->destroy = &destroy; + mod->content = &content; + mod->description = &description; + return mod; +} + +static struct module * +from_conf(const struct yml_node *node, struct conf_inherit inherited) +{ + const struct yml_node *interval = yml_get_value(node, "interval"); + const struct yml_node *c = yml_get_value(node, "content"); + + return disk_io_new( + interval == NULL ? SMALLEST_INTERVAL : yml_value_as_int(interval), + conf_to_particle(c, inherited)); +} + +static bool +conf_verify_interval(keychain_t *chain, const struct yml_node *node) +{ + if (!conf_verify_unsigned(chain, node)) + return false; + + if (yml_value_as_int(node) < SMALLEST_INTERVAL) { + LOG_ERR( + "%s: interval value cannot be less than %d ms", + conf_err_prefix(chain, node), SMALLEST_INTERVAL); + return false; + } + + return true; +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static const struct attr_info attrs[] = { + {"interval", false, &conf_verify_interval}, + MODULE_COMMON_ATTRS, + }; + + return conf_verify_dict(chain, node, attrs); +} + +const struct module_iface module_disk_io_iface = { + .verify_conf = &verify_conf, + .from_conf = &from_conf, +}; + +#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) +extern const struct module_iface iface __attribute__((weak, alias("module_disk_io_iface"))); +#endif diff --git a/modules/meson.build b/modules/meson.build index 192b094..ada5ab6 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -18,6 +18,7 @@ mod_data = { 'battery': [[], [udev]], 'clock': [[], []], 'cpu': [[], []], + 'disk-io': [[], [dynlist]], 'mem': [[], []], 'i3': [['i3-common.c', 'i3-common.h'], [dynlist, json]], 'label': [[], []], diff --git a/plugin.c b/plugin.c index 43e8672..06304d8 100644 --- a/plugin.c +++ b/plugin.c @@ -36,6 +36,7 @@ EXTERN_MODULE(alsa); EXTERN_MODULE(backlight); EXTERN_MODULE(battery); EXTERN_MODULE(clock); +EXTERN_MODULE(disk_io); EXTERN_MODULE(foreign_toplevel); EXTERN_MODULE(i3); EXTERN_MODULE(label); @@ -118,6 +119,7 @@ init(void) REGISTER_CORE_MODULE(backlight, backlight); REGISTER_CORE_MODULE(battery, battery); REGISTER_CORE_MODULE(clock, clock); + REGISTER_CORE_MODULE(disk-io, disk_io); #if defined(HAVE_PLUGIN_foreign_toplevel) REGISTER_CORE_MODULE(foreign-toplevel, foreign_toplevel); #endif