| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "plugin_proc.h" |
| 4 | |
| 5 | #define PLUGIN_PROC_MODULE_INTERRUPTS_NAME "/proc/interrupts" |
| 6 | #define CONFIG_SECTION_PLUGIN_PROC_INTERRUPTS "plugin:" PLUGIN_PROC_CONFIG_NAME ":" PLUGIN_PROC_MODULE_INTERRUPTS_NAME |
| 7 | |
| 8 | #define MAX_INTERRUPT_NAME 50 |
| 9 | |
| 10 | struct cpu_interrupt { |
| 11 | unsigned long long value; |
| 12 | RRDDIM *rd; |
| 13 | }; |
| 14 | |
| 15 | struct interrupt { |
| 16 | int used; |
| 17 | char *id; |
| 18 | char name[MAX_INTERRUPT_NAME + 1]; |
| 19 | RRDDIM *rd; |
| 20 | unsigned long long total; |
| 21 | struct cpu_interrupt cpu[]; |
| 22 | }; |
| 23 | |
| 24 | // since each interrupt is variable in size |
| 25 | // we use this to calculate its record size |
| 26 | #define recordsize(cpus) (sizeof(struct interrupt) + ((cpus) * sizeof(struct cpu_interrupt))) |
| 27 | |
| 28 | // given a base, get a pointer to each record |
| 29 | #define irrindex(base, line, cpus) ((struct interrupt *)&((char *)(base))[(line) * recordsize(cpus)]) |
| 30 | |
| 31 | static inline void proc_interrupts_keep_colon_in_words(procfile *ff) { |
| 32 | if(ff) |
| 33 | ff->separators[(uint8_t)':'] = PF_CHAR_IS_WORD; |
| 34 | } |
| 35 | |
| 36 | static inline char *proc_interrupts_lineword(procfile *ff, size_t line, size_t word) { |
| 37 | if(!ff) |
| 38 | return NULL; |
| 39 | |
| 40 | if(!ff->lines) |
| 41 | return NULL; |
| 42 | |
| 43 | if(!ff->words) |
| 44 | return NULL; |
| 45 | |
| 46 | if(line >= ff->lines->len) |
| 47 | return NULL; |
| 48 | |
| 49 | ffline *pfline = &ff->lines->lines[line]; |
| 50 | if(word >= pfline->words) |
| 51 | return NULL; |
| 52 | |
| 53 | size_t word_index = (size_t)pfline->first + word; |
| 54 | if(word_index >= ff->words->len) |
| 55 | return NULL; |
| 56 | |
| 57 | return ff->words->words[word_index]; |
| 58 | } |
| 59 | |
| 60 | static inline bool proc_interrupts_word_is_number(const char *word) { |
| 61 | if(word == NULL) |
| 62 | return false; |
| 63 | |
| 64 | const unsigned char *p = (const unsigned char *)word; |
| 65 | if(*p == '\0') |
| 66 | return false; |
| 67 | |
| 68 | while(*p) { |
| 69 | if(!isdigit(*p)) |
| 70 | return false; |
| 71 | |
| 72 | p++; |
| 73 | } |
| 74 | |
| 75 | return true; |
| 76 | } |
| 77 | |
| 78 | static inline bool proc_interrupts_word_is_trigger(const char *word) { |
| 79 | if(!word) |
| 80 | return false; |
| 81 | |
| 82 | return ( |
| 83 | strcasecmp(word, "edge") == 0 || |
| 84 | strcasecmp(word, "level") == 0 || |
| 85 | strcasecmp(word, "fasteoi") == 0); |
| 86 | } |
| 87 | |
| 88 | static inline bool proc_interrupts_word_has_trigger_suffix(const char *word) { |
| 89 | const char *separator = word ? strrchr(word, '-') : NULL; |
| 90 | return separator && proc_interrupts_word_is_trigger(separator + 1); |
| 91 | } |
| 92 | |
| 93 | static size_t proc_interrupts_name_first_word(procfile *ff, size_t line, int cpus, size_t words) { |
| 94 | size_t first = (size_t)cpus + 1; |
| 95 | if(unlikely(first >= words)) |
| 96 | return words; |
| 97 | |
| 98 | // Skip the interrupt controller/chip column. |
| 99 | first++; |
| 100 | |
| 101 | while(first < words) { |
| 102 | const char *word = proc_interrupts_lineword(ff, line, first); |
| 103 | if(proc_interrupts_word_is_number(word) || |
| 104 | proc_interrupts_word_is_trigger(word) || |
| 105 | proc_interrupts_word_has_trigger_suffix(word)) |
| 106 | first++; |
| 107 | else |
| 108 | break; |
| 109 | } |
| 110 | |
| 111 | return first; |
| 112 | } |
| 113 | |
| 114 | static inline char proc_interrupts_name_char(char c) { |
| 115 | return (c == ':' || isspace((uint8_t)c)) ? '_' : c; |
| 116 | } |
| 117 | |
| 118 | static size_t proc_interrupts_strnlen(const char *s, size_t max) { |
| 119 | size_t len = 0; |
| 120 | |
| 121 | if(!s) |
| 122 | return 0; |
| 123 | |
| 124 | while(len < max && s[len]) |
| 125 | len++; |
| 126 | |
| 127 | return len; |
| 128 | } |
| 129 | |
| 130 | static inline void proc_interrupts_append_char(char *dst, size_t *len, char c) { |
| 131 | if(*len < MAX_INTERRUPT_NAME) { |
| 132 | dst[*len] = c; |
| 133 | (*len)++; |
| 134 | dst[*len] = '\0'; |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | static void proc_interrupts_append_id(char *name, const char *id, size_t idlen) { |
| 139 | if(!id) |
| 140 | return; |
| 141 | |
| 142 | if(!idlen) |
| 143 | return; |
| 144 | |
| 145 | if(unlikely(idlen >= MAX_INTERRUPT_NAME)) { |
| 146 | strncpyz(name, id, MAX_INTERRUPT_NAME); |
| 147 | return; |
| 148 | } |
| 149 | |
| 150 | size_t nlen = proc_interrupts_strnlen(name, MAX_INTERRUPT_NAME); |
| 151 | if(likely(nlen + 1 + idlen <= MAX_INTERRUPT_NAME)) { |
| 152 | name[nlen] = '_'; |
| 153 | strncpyz(&name[nlen + 1], id, MAX_INTERRUPT_NAME - nlen - 1); |
| 154 | } |
| 155 | else { |
| 156 | name[MAX_INTERRUPT_NAME - idlen - 1] = '_'; |
| 157 | strncpyz(&name[MAX_INTERRUPT_NAME - idlen], id, idlen); |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | static void proc_interrupts_build_name( |
| 162 | char *dst, |
| 163 | procfile *ff, |
| 164 | size_t line, |
| 165 | size_t first_name_word, |
| 166 | size_t words, |
| 167 | const char *id, |
| 168 | size_t idlen) { |
| 169 | dst[0] = '\0'; |
| 170 | |
| 171 | size_t len = 0; |
| 172 | for(size_t w = first_name_word; w < words ;w++) { |
| 173 | const char *word = proc_interrupts_lineword(ff, line, w); |
| 174 | if(!word) |
| 175 | continue; |
| 176 | |
| 177 | if(!*word) |
| 178 | continue; |
| 179 | |
| 180 | if(len && dst[len - 1] != '_') |
| 181 | proc_interrupts_append_char(dst, &len, '_'); |
| 182 | |
| 183 | while(*word) { |
| 184 | char c = proc_interrupts_name_char(*word++); |
| 185 | if(c != '_' || !len || dst[len - 1] != '_') |
| 186 | proc_interrupts_append_char(dst, &len, c); |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | if(likely(len)) |
| 191 | proc_interrupts_append_id(dst, id, idlen); |
| 192 | else |
| 193 | strncpyz(dst, id, MAX_INTERRUPT_NAME); |
| 194 | } |
| 195 | |
| 196 | static inline struct interrupt *get_interrupts_array(size_t lines, int cpus) { |
| 197 | static struct interrupt *irrs = NULL; |
| 198 | static size_t allocated = 0; |
| 199 | |
| 200 | if(unlikely(lines != allocated)) { |
| 201 | size_t l; |
| 202 | int c; |
| 203 | |
| 204 | irrs = (struct interrupt *)reallocz(irrs, lines * recordsize(cpus)); |
| 205 | |
| 206 | // reset all interrupt RRDDIM pointers as any line could have shifted |
| 207 | for(l = 0; l < lines ;l++) { |
| 208 | struct interrupt *irr = irrindex(irrs, l, cpus); |
| 209 | irr->rd = NULL; |
| 210 | irr->name[0] = '\0'; |
| 211 | for(c = 0; c < cpus ;c++) |
| 212 | irr->cpu[c].rd = NULL; |
| 213 | } |
| 214 | |
| 215 | allocated = lines; |
| 216 | } |
| 217 | |
| 218 | return irrs; |
| 219 | } |
| 220 | |
| 221 | int do_proc_interrupts(int update_every, usec_t dt) { |
| 222 | (void)dt; |
| 223 | static procfile *ff = NULL; |
| 224 | static int cpus = -1, do_per_core = CONFIG_BOOLEAN_INVALID; |
| 225 | struct interrupt *irrs = NULL; |
| 226 | |
| 227 | if(unlikely(do_per_core == CONFIG_BOOLEAN_INVALID)) |
| 228 | do_per_core = inicfg_get_boolean_ondemand(&netdata_config, CONFIG_SECTION_PLUGIN_PROC_INTERRUPTS, "interrupts per core", CONFIG_BOOLEAN_NO); |
| 229 | |
| 230 | if(unlikely(!ff)) { |
| 231 | char filename[FILENAME_MAX + 1]; |
| 232 | snprintfz(filename, FILENAME_MAX, "%s%s", netdata_configured_host_prefix, "/proc/interrupts"); |
| 233 | ff = procfile_open(inicfg_get(&netdata_config, CONFIG_SECTION_PLUGIN_PROC_INTERRUPTS, "filename to monitor", filename), " \t:", PROCFILE_FLAG_DEFAULT); |
| 234 | proc_interrupts_keep_colon_in_words(ff); |
| 235 | } |
| 236 | if(unlikely(!ff)) |
| 237 | return 1; |
| 238 | |
| 239 | ff = procfile_readall(ff); |
| 240 | if(unlikely(!ff)) |
| 241 | return 0; // we return 0, so that we will retry to open it next time |
| 242 | |
| 243 | size_t lines = procfile_lines(ff), l; |
| 244 | size_t words = procfile_linewords(ff, 0); |
| 245 | |
| 246 | if(unlikely(!lines)) { |
| 247 | collector_error("Cannot read /proc/interrupts, zero lines reported."); |
| 248 | return 1; |
| 249 | } |
| 250 | |
| 251 | // find how many CPUs are there |
| 252 | if(unlikely(cpus == -1)) { |
| 253 | uint32_t w; |
| 254 | cpus = 0; |
| 255 | for(w = 0; w < words ; w++) { |
| 256 | const char *word = proc_interrupts_lineword(ff, 0, w); |
| 257 | if(!word) |
| 258 | continue; |
| 259 | |
| 260 | if(likely(strncmp(word, "CPU", 3) == 0)) |
| 261 | cpus++; |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | if(unlikely(!cpus)) { |
| 266 | collector_error("PLUGIN: PROC_INTERRUPTS: Cannot find the number of CPUs in /proc/interrupts"); |
| 267 | return 1; |
| 268 | } |
| 269 | |
| 270 | // allocate the size we need; |
| 271 | irrs = get_interrupts_array(lines, cpus); |
| 272 | irrs[0].used = 0; |
| 273 | |
| 274 | // loop through all lines |
| 275 | for(l = 1; l < lines ;l++) { |
| 276 | struct interrupt *irr = irrindex(irrs, l, cpus); |
| 277 | irr->used = 0; |
| 278 | irr->total = 0; |
| 279 | |
| 280 | words = procfile_linewords(ff, l); |
| 281 | if(unlikely(!words)) continue; |
| 282 | |
| 283 | irr->id = proc_interrupts_lineword(ff, l, 0); |
| 284 | if(unlikely(!irr->id)) continue; |
| 285 | if(unlikely(!irr->id[0])) continue; |
| 286 | |
| 287 | size_t idlen = strlen(irr->id); |
| 288 | if(irr->id[idlen - 1] == ':') |
| 289 | irr->id[--idlen] = '\0'; |
| 290 | |
| 291 | int c; |
| 292 | for(c = 0; c < cpus ;c++) { |
| 293 | char *word = NULL; |
| 294 | if(likely((c + 1) < (int)words)) |
| 295 | word = proc_interrupts_lineword(ff, l, (uint32_t)(c + 1)); |
| 296 | |
| 297 | irr->cpu[c].value = word ? str2ull(word, NULL) : 0; |
| 298 | |
| 299 | irr->total += irr->cpu[c].value; |
| 300 | } |
| 301 | |
| 302 | if(unlikely(isdigit(irr->id[0]))) { |
| 303 | size_t first_name_word = proc_interrupts_name_first_word(ff, l, cpus, words); |
| 304 | proc_interrupts_build_name(irr->name, ff, l, first_name_word, words, irr->id, idlen); |
| 305 | } |
| 306 | else { |
| 307 | strncpyz(irr->name, irr->id, MAX_INTERRUPT_NAME); |
| 308 | } |
| 309 | |
| 310 | irr->used = 1; |
| 311 | } |
| 312 | |
| 313 | static RRDSET *st_system_interrupts = NULL; |
| 314 | if(unlikely(!st_system_interrupts)) |
| 315 | st_system_interrupts = rrdset_create_localhost( |
| 316 | "system" |
| 317 | , "interrupts" |
| 318 | , NULL |
| 319 | , "interrupts" |
| 320 | , NULL |
| 321 | , "System interrupts" |
| 322 | , "interrupts/s" |
| 323 | , PLUGIN_PROC_NAME |
| 324 | , PLUGIN_PROC_MODULE_INTERRUPTS_NAME |
| 325 | , NETDATA_CHART_PRIO_SYSTEM_INTERRUPTS |
| 326 | , update_every |
| 327 | , RRDSET_TYPE_STACKED |
| 328 | ); |
| 329 | |
| 330 | for(l = 0; l < lines ;l++) { |
| 331 | struct interrupt *irr = irrindex(irrs, l, cpus); |
| 332 | if(irr->used && irr->total) { |
| 333 | // some interrupt may have changed without changing the total number of lines |
| 334 | // if the same number of interrupts have been added and removed between two |
| 335 | // calls of this function. |
| 336 | if(unlikely(!irr->rd || strncmp(rrddim_name(irr->rd), irr->name, MAX_INTERRUPT_NAME) != 0)) { |
| 337 | irr->rd = rrddim_add(st_system_interrupts, irr->id, irr->name, 1, 1, RRD_ALGORITHM_INCREMENTAL); |
| 338 | rrddim_reset_name(st_system_interrupts, irr->rd, irr->name); |
| 339 | |
| 340 | // also reset per cpu RRDDIMs to avoid repeating strncmp() in the per core loop |
| 341 | if(likely(do_per_core != CONFIG_BOOLEAN_NO)) { |
| 342 | int c; |
| 343 | for(c = 0; c < cpus; c++) irr->cpu[c].rd = NULL; |
| 344 | } |
| 345 | } |
| 346 | |
| 347 | rrddim_set_by_pointer(st_system_interrupts, irr->rd, irr->total); |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | rrdset_done(st_system_interrupts); |
| 352 | |
| 353 | if(likely(do_per_core != CONFIG_BOOLEAN_NO)) { |
| 354 | static RRDSET **core_st = NULL; |
| 355 | static int old_cpus = 0; |
| 356 | |
| 357 | if(old_cpus < cpus) { |
| 358 | core_st = reallocz(core_st, sizeof(RRDSET *) * cpus); |
| 359 | memset(&core_st[old_cpus], 0, sizeof(RRDSET *) * (cpus - old_cpus)); |
| 360 | old_cpus = cpus; |
| 361 | } |
| 362 | |
| 363 | int c; |
| 364 | |
| 365 | for(c = 0; c < cpus ;c++) { |
| 366 | if(unlikely(!core_st[c])) { |
| 367 | char id[50+1]; |
| 368 | snprintfz(id, sizeof(id) - 1, "cpu%d_interrupts", c); |
| 369 | |
| 370 | char title[100+1]; |
| 371 | snprintfz(title, sizeof(title) - 1, "CPU Interrupts"); |
| 372 | core_st[c] = rrdset_create_localhost( |
| 373 | "cpu" |
| 374 | , id |
| 375 | , NULL |
| 376 | , "interrupts" |
| 377 | , "cpu.interrupts" |
| 378 | , title |
| 379 | , "interrupts/s" |
| 380 | , PLUGIN_PROC_NAME |
| 381 | , PLUGIN_PROC_MODULE_INTERRUPTS_NAME |
| 382 | , NETDATA_CHART_PRIO_INTERRUPTS_PER_CORE + c |
| 383 | , update_every |
| 384 | , RRDSET_TYPE_STACKED |
| 385 | ); |
| 386 | |
| 387 | char core[50+1]; |
| 388 | snprintfz(core, sizeof(core) - 1, "cpu%d", c); |
| 389 | rrdlabels_add(core_st[c]->rrdlabels, "cpu", core, RRDLABEL_SRC_AUTO); |
| 390 | } |
| 391 | |
| 392 | for(l = 0; l < lines ;l++) { |
| 393 | struct interrupt *irr = irrindex(irrs, l, cpus); |
| 394 | if(irr->used && (do_per_core == CONFIG_BOOLEAN_YES || irr->cpu[c].value)) { |
| 395 | if(unlikely(!irr->cpu[c].rd)) { |
| 396 | irr->cpu[c].rd = rrddim_add(core_st[c], irr->id, irr->name, 1, 1, RRD_ALGORITHM_INCREMENTAL); |
| 397 | rrddim_reset_name(core_st[c], irr->cpu[c].rd, irr->name); |
| 398 | } |
| 399 | |
| 400 | rrddim_set_by_pointer(core_st[c], irr->cpu[c].rd, irr->cpu[c].value); |
| 401 | } |
| 402 | } |
| 403 | |
| 404 | rrdset_done(core_st[c]); |
| 405 | } |
| 406 | } |
| 407 | |
| 408 | return 0; |
| 409 | } |
| 410 | |
| 411 | int proc_interrupts_unittest(void) { |
| 412 | #if !defined(__NR_memfd_create) || !defined(MFD_CLOEXEC) |
| 413 | fprintf(stderr, "%s skipped, memfd_create() is not available.\n", __FUNCTION__); |
| 414 | return 0; |
| 415 | #else |
| 416 | int fd = (int)syscall(__NR_memfd_create, "netdata-proc-interrupts", MFD_CLOEXEC); |
| 417 | if(fd == -1) { |
| 418 | fprintf(stderr, "Cannot create in-memory /proc/interrupts fixture: %s\n", strerror(errno)); |
| 419 | return 1; |
| 420 | } |
| 421 | |
| 422 | static const char fixture[] = |
| 423 | " CPU0 CPU1\n" |
| 424 | "240: 1 2 PCI-MSI 1572864-edge mlx5_comp40@pci:0000:86:00.0\n" |
| 425 | "250: 3 4 PCI-MSI 5242880-edge nvme 0 io5\n" |
| 426 | " 0: 5 6 IO-APIC 2-edge timer\n" |
| 427 | " 1: 7 8 XT-PIC-XT keyboard\n" |
| 428 | " 27: 9 10 GICv3 27 Level arch_timer\n"; |
| 429 | |
| 430 | if(write(fd, fixture, sizeof(fixture) - 1) != (ssize_t)sizeof(fixture) - 1) { |
| 431 | fprintf(stderr, "Cannot write in-memory /proc/interrupts fixture: %s\n", strerror(errno)); |
| 432 | close(fd); |
| 433 | return 1; |
| 434 | } |
| 435 | |
| 436 | if(lseek(fd, 0, SEEK_SET) == -1) { |
| 437 | fprintf(stderr, "Cannot rewind in-memory /proc/interrupts fixture: %s\n", strerror(errno)); |
| 438 | close(fd); |
| 439 | return 1; |
| 440 | } |
| 441 | |
| 442 | char filename[FILENAME_MAX + 1]; |
| 443 | snprintfz(filename, FILENAME_MAX, "/proc/self/fd/%d", fd); |
| 444 | |
| 445 | procfile *ff = procfile_open(filename, " \t:", PROCFILE_FLAG_DEFAULT); |
| 446 | proc_interrupts_keep_colon_in_words(ff); |
| 447 | close(fd); |
| 448 | |
| 449 | if(!ff) |
| 450 | return 1; |
| 451 | |
| 452 | ff = procfile_readall(ff); |
| 453 | if(!ff) |
| 454 | return 1; |
| 455 | |
| 456 | size_t words = procfile_linewords(ff, 0); |
| 457 | int cpus = 0; |
| 458 | for(uint32_t w = 0; w < words ;w++) { |
| 459 | const char *word = proc_interrupts_lineword(ff, 0, w); |
| 460 | if(!word) |
| 461 | continue; |
| 462 | |
| 463 | if(strncmp(word, "CPU", 3) == 0) |
| 464 | cpus++; |
| 465 | } |
| 466 | |
| 467 | static const char *expected[] = { |
| 468 | "mlx5_comp40@pci_0000_86_00.0_240", |
| 469 | "nvme_0_io5_250", |
| 470 | "timer_0", |
| 471 | "keyboard_1", |
| 472 | "arch_timer_27", |
| 473 | }; |
| 474 | |
| 475 | int rc = 0; |
| 476 | size_t expected_idx = 0; |
| 477 | for(size_t line = 1; line < procfile_lines(ff) ;line++) { |
| 478 | words = procfile_linewords(ff, line); |
| 479 | if(!words) |
| 480 | continue; |
| 481 | |
| 482 | if(expected_idx >= _countof(expected)) { |
| 483 | fprintf(stderr, "proc_interrupts_unittest found unexpected parsed line %zu\n", line); |
| 484 | rc = 1; |
| 485 | break; |
| 486 | } |
| 487 | |
| 488 | char id[MAX_INTERRUPT_NAME + 1]; |
| 489 | const char *id_word = proc_interrupts_lineword(ff, line, 0); |
| 490 | if(!id_word) { |
| 491 | rc = 1; |
| 492 | break; |
| 493 | } |
| 494 | |
| 495 | strncpyz(id, id_word, MAX_INTERRUPT_NAME); |
| 496 | size_t idlen = proc_interrupts_strnlen(id, MAX_INTERRUPT_NAME); |
| 497 | if(idlen && id[idlen - 1] == ':') |
| 498 | id[--idlen] = '\0'; |
| 499 | |
| 500 | size_t first_name_word = proc_interrupts_name_first_word(ff, line, cpus, words); |
| 501 | |
| 502 | char name[MAX_INTERRUPT_NAME + 1]; |
| 503 | proc_interrupts_build_name(name, ff, line, first_name_word, words, id, idlen); |
| 504 | |
| 505 | if(strcmp(name, expected[expected_idx]) != 0) { |
| 506 | fprintf( |
| 507 | stderr, |
| 508 | "proc_interrupts_unittest line %zu expected '%s', got '%s'\n", |
| 509 | line, |
| 510 | expected[expected_idx], |
| 511 | name); |
| 512 | rc = 1; |
| 513 | break; |
| 514 | } |
| 515 | |
| 516 | expected_idx++; |
| 517 | } |
| 518 | |
| 519 | if(!rc && expected_idx != _countof(expected)) { |
| 520 | fprintf( |
| 521 | stderr, |
| 522 | "proc_interrupts_unittest expected %zu parsed lines, got %zu\n", |
| 523 | _countof(expected), |
| 524 | expected_idx); |
| 525 | rc = 1; |
| 526 | } |
| 527 | |
| 528 | procfile_close(ff); |
| 529 | |
| 530 | return rc; |
| 531 | #endif |
| 532 | } |