1 // SPDX-License-Identifier: GPL-2.0 2 /* Copyright (c) 2022 Meta Platforms, Inc. and affiliates. */ 3 #define _GNU_SOURCE 4 #include <argp.h> 5 #include <string.h> 6 #include <stdlib.h> 7 #include <linux/compiler.h> 8 #include <sched.h> 9 #include <pthread.h> 10 #include <dirent.h> 11 #include <signal.h> 12 #include <fcntl.h> 13 #include <unistd.h> 14 #include <sys/time.h> 15 #include <sys/sysinfo.h> 16 #include <sys/stat.h> 17 #include <bpf/libbpf.h> 18 #include <libelf.h> 19 #include <gelf.h> 20 #include <float.h> 21 22 enum stat_id { 23 VERDICT, 24 DURATION, 25 TOTAL_INSNS, 26 TOTAL_STATES, 27 PEAK_STATES, 28 MAX_STATES_PER_INSN, 29 MARK_READ_MAX_LEN, 30 31 FILE_NAME, 32 PROG_NAME, 33 34 ALL_STATS_CNT, 35 NUM_STATS_CNT = FILE_NAME - VERDICT, 36 }; 37 38 /* In comparison mode each stat can specify up to four different values: 39 * - A side value; 40 * - B side value; 41 * - absolute diff value; 42 * - relative (percentage) diff value. 43 * 44 * When specifying stat specs in comparison mode, user can use one of the 45 * following variant suffixes to specify which exact variant should be used for 46 * ordering or filtering: 47 * - `_a` for A side value; 48 * - `_b` for B side value; 49 * - `_diff` for absolute diff value; 50 * - `_pct` for relative (percentage) diff value. 51 * 52 * If no variant suffix is provided, then `_b` (control data) is assumed. 53 * 54 * As an example, let's say instructions stat has the following output: 55 * 56 * Insns (A) Insns (B) Insns (DIFF) 57 * --------- --------- -------------- 58 * 21547 20920 -627 (-2.91%) 59 * 60 * Then: 61 * - 21547 is A side value (insns_a); 62 * - 20920 is B side value (insns_b); 63 * - -627 is absolute diff value (insns_diff); 64 * - -2.91% is relative diff value (insns_pct). 65 * 66 * For verdict there is no verdict_pct variant. 67 * For file and program name, _a and _b variants are equivalent and there are 68 * no _diff or _pct variants. 69 */ 70 enum stat_variant { 71 VARIANT_A, 72 VARIANT_B, 73 VARIANT_DIFF, 74 VARIANT_PCT, 75 }; 76 77 struct verif_stats { 78 char *file_name; 79 char *prog_name; 80 81 long stats[NUM_STATS_CNT]; 82 }; 83 84 /* joined comparison mode stats */ 85 struct verif_stats_join { 86 char *file_name; 87 char *prog_name; 88 89 const struct verif_stats *stats_a; 90 const struct verif_stats *stats_b; 91 }; 92 93 struct stat_specs { 94 int spec_cnt; 95 enum stat_id ids[ALL_STATS_CNT]; 96 enum stat_variant variants[ALL_STATS_CNT]; 97 bool asc[ALL_STATS_CNT]; 98 int lens[ALL_STATS_CNT * 3]; /* 3x for comparison mode */ 99 }; 100 101 enum resfmt { 102 RESFMT_TABLE, 103 RESFMT_TABLE_CALCLEN, /* fake format to pre-calculate table's column widths */ 104 RESFMT_CSV, 105 }; 106 107 enum filter_kind { 108 FILTER_NAME, 109 FILTER_STAT, 110 }; 111 112 enum operator_kind { 113 OP_EQ, /* == or = */ 114 OP_NEQ, /* != or <> */ 115 OP_LT, /* < */ 116 OP_LE, /* <= */ 117 OP_GT, /* > */ 118 OP_GE, /* >= */ 119 }; 120 121 struct filter { 122 enum filter_kind kind; 123 /* FILTER_NAME */ 124 char *any_glob; 125 char *file_glob; 126 char *prog_glob; 127 /* FILTER_STAT */ 128 enum operator_kind op; 129 int stat_id; 130 enum stat_variant stat_var; 131 long value; 132 }; 133 134 static struct env { 135 char **filenames; 136 int filename_cnt; 137 bool verbose; 138 bool quiet; 139 int log_level; 140 enum resfmt out_fmt; 141 bool comparison_mode; 142 bool replay_mode; 143 144 struct verif_stats *prog_stats; 145 int prog_stat_cnt; 146 147 /* baseline_stats is allocated and used only in comparison mode */ 148 struct verif_stats *baseline_stats; 149 int baseline_stat_cnt; 150 151 struct verif_stats_join *join_stats; 152 int join_stat_cnt; 153 154 struct stat_specs output_spec; 155 struct stat_specs sort_spec; 156 157 struct filter *allow_filters; 158 struct filter *deny_filters; 159 int allow_filter_cnt; 160 int deny_filter_cnt; 161 162 int files_processed; 163 int files_skipped; 164 int progs_processed; 165 int progs_skipped; 166 } env; 167 168 static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) 169 { 170 if (!env.verbose) 171 return 0; 172 if (level == LIBBPF_DEBUG /* && !env.verbose */) 173 return 0; 174 return vfprintf(stderr, format, args); 175 } 176 177 const char *argp_program_version = "veristat"; 178 const char *argp_program_bug_address = "<bpf@vger.kernel.org>"; 179 const char argp_program_doc[] = 180 "veristat BPF verifier stats collection and comparison tool.\n" 181 "\n" 182 "USAGE: veristat <obj-file> [<obj-file>...]\n" 183 " OR: veristat -C <baseline.csv> <comparison.csv>\n"; 184 185 static const struct argp_option opts[] = { 186 { NULL, 'h', NULL, OPTION_HIDDEN, "Show the full help" }, 187 { "verbose", 'v', NULL, 0, "Verbose mode" }, 188 { "log-level", 'l', "LEVEL", 0, "Verifier log level (default 0 for normal mode, 1 for verbose mode)" }, 189 { "quiet", 'q', NULL, 0, "Quiet mode" }, 190 { "emit", 'e', "SPEC", 0, "Specify stats to be emitted" }, 191 { "sort", 's', "SPEC", 0, "Specify sort order" }, 192 { "output-format", 'o', "FMT", 0, "Result output format (table, csv), default is table." }, 193 { "compare", 'C', NULL, 0, "Comparison mode" }, 194 { "replay", 'R', NULL, 0, "Replay mode" }, 195 { "filter", 'f', "FILTER", 0, "Filter expressions (or @filename for file with expressions)." }, 196 {}, 197 }; 198 199 static int parse_stats(const char *stats_str, struct stat_specs *specs); 200 static int append_filter(struct filter **filters, int *cnt, const char *str); 201 static int append_filter_file(const char *path); 202 203 static error_t parse_arg(int key, char *arg, struct argp_state *state) 204 { 205 void *tmp; 206 int err; 207 208 switch (key) { 209 case 'h': 210 argp_state_help(state, stderr, ARGP_HELP_STD_HELP); 211 break; 212 case 'v': 213 env.verbose = true; 214 break; 215 case 'q': 216 env.quiet = true; 217 break; 218 case 'e': 219 err = parse_stats(arg, &env.output_spec); 220 if (err) 221 return err; 222 break; 223 case 's': 224 err = parse_stats(arg, &env.sort_spec); 225 if (err) 226 return err; 227 break; 228 case 'o': 229 if (strcmp(arg, "table") == 0) { 230 env.out_fmt = RESFMT_TABLE; 231 } else if (strcmp(arg, "csv") == 0) { 232 env.out_fmt = RESFMT_CSV; 233 } else { 234 fprintf(stderr, "Unrecognized output format '%s'\n", arg); 235 return -EINVAL; 236 } 237 break; 238 case 'l': 239 errno = 0; 240 env.log_level = strtol(arg, NULL, 10); 241 if (errno) { 242 fprintf(stderr, "invalid log level: %s\n", arg); 243 argp_usage(state); 244 } 245 break; 246 case 'C': 247 env.comparison_mode = true; 248 break; 249 case 'R': 250 env.replay_mode = true; 251 break; 252 case 'f': 253 if (arg[0] == '@') 254 err = append_filter_file(arg + 1); 255 else if (arg[0] == '!') 256 err = append_filter(&env.deny_filters, &env.deny_filter_cnt, arg + 1); 257 else 258 err = append_filter(&env.allow_filters, &env.allow_filter_cnt, arg); 259 if (err) { 260 fprintf(stderr, "Failed to collect program filter expressions: %d\n", err); 261 return err; 262 } 263 break; 264 case ARGP_KEY_ARG: 265 tmp = realloc(env.filenames, (env.filename_cnt + 1) * sizeof(*env.filenames)); 266 if (!tmp) 267 return -ENOMEM; 268 env.filenames = tmp; 269 env.filenames[env.filename_cnt] = strdup(arg); 270 if (!env.filenames[env.filename_cnt]) 271 return -ENOMEM; 272 env.filename_cnt++; 273 break; 274 default: 275 return ARGP_ERR_UNKNOWN; 276 } 277 return 0; 278 } 279 280 static const struct argp argp = { 281 .options = opts, 282 .parser = parse_arg, 283 .doc = argp_program_doc, 284 }; 285 286 287 /* Adapted from perf/util/string.c */ 288 static bool glob_matches(const char *str, const char *pat) 289 { 290 while (*str && *pat && *pat != '*') { 291 if (*str != *pat) 292 return false; 293 str++; 294 pat++; 295 } 296 /* Check wild card */ 297 if (*pat == '*') { 298 while (*pat == '*') 299 pat++; 300 if (!*pat) /* Tail wild card matches all */ 301 return true; 302 while (*str) 303 if (glob_matches(str++, pat)) 304 return true; 305 } 306 return !*str && !*pat; 307 } 308 309 static bool is_bpf_obj_file(const char *path) { 310 Elf64_Ehdr *ehdr; 311 int fd, err = -EINVAL; 312 Elf *elf = NULL; 313 314 fd = open(path, O_RDONLY | O_CLOEXEC); 315 if (fd < 0) 316 return true; /* we'll fail later and propagate error */ 317 318 /* ensure libelf is initialized */ 319 (void)elf_version(EV_CURRENT); 320 321 elf = elf_begin(fd, ELF_C_READ, NULL); 322 if (!elf) 323 goto cleanup; 324 325 if (elf_kind(elf) != ELF_K_ELF || gelf_getclass(elf) != ELFCLASS64) 326 goto cleanup; 327 328 ehdr = elf64_getehdr(elf); 329 /* Old LLVM set e_machine to EM_NONE */ 330 if (!ehdr || ehdr->e_type != ET_REL || (ehdr->e_machine && ehdr->e_machine != EM_BPF)) 331 goto cleanup; 332 333 err = 0; 334 cleanup: 335 if (elf) 336 elf_end(elf); 337 close(fd); 338 return err == 0; 339 } 340 341 static bool should_process_file_prog(const char *filename, const char *prog_name) 342 { 343 struct filter *f; 344 int i, allow_cnt = 0; 345 346 for (i = 0; i < env.deny_filter_cnt; i++) { 347 f = &env.deny_filters[i]; 348 if (f->kind != FILTER_NAME) 349 continue; 350 351 if (f->any_glob && glob_matches(filename, f->any_glob)) 352 return false; 353 if (f->any_glob && prog_name && glob_matches(prog_name, f->any_glob)) 354 return false; 355 if (f->file_glob && glob_matches(filename, f->file_glob)) 356 return false; 357 if (f->prog_glob && prog_name && glob_matches(prog_name, f->prog_glob)) 358 return false; 359 } 360 361 for (i = 0; i < env.allow_filter_cnt; i++) { 362 f = &env.allow_filters[i]; 363 if (f->kind != FILTER_NAME) 364 continue; 365 366 allow_cnt++; 367 if (f->any_glob) { 368 if (glob_matches(filename, f->any_glob)) 369 return true; 370 /* If we don't know program name yet, any_glob filter 371 * has to assume that current BPF object file might be 372 * relevant; we'll check again later on after opening 373 * BPF object file, at which point program name will 374 * be known finally. 375 */ 376 if (!prog_name || glob_matches(prog_name, f->any_glob)) 377 return true; 378 } else { 379 if (f->file_glob && !glob_matches(filename, f->file_glob)) 380 continue; 381 if (f->prog_glob && prog_name && !glob_matches(prog_name, f->prog_glob)) 382 continue; 383 return true; 384 } 385 } 386 387 /* if there are no file/prog name allow filters, allow all progs, 388 * unless they are denied earlier explicitly 389 */ 390 return allow_cnt == 0; 391 } 392 393 static struct { 394 enum operator_kind op_kind; 395 const char *op_str; 396 } operators[] = { 397 /* Order of these definitions matter to avoid situations like '<' 398 * matching part of what is actually a '<>' operator. That is, 399 * substrings should go last. 400 */ 401 { OP_EQ, "==" }, 402 { OP_NEQ, "!=" }, 403 { OP_NEQ, "<>" }, 404 { OP_LE, "<=" }, 405 { OP_LT, "<" }, 406 { OP_GE, ">=" }, 407 { OP_GT, ">" }, 408 { OP_EQ, "=" }, 409 }; 410 411 static bool parse_stat_id_var(const char *name, size_t len, int *id, enum stat_variant *var); 412 413 static int append_filter(struct filter **filters, int *cnt, const char *str) 414 { 415 struct filter *f; 416 void *tmp; 417 const char *p; 418 int i; 419 420 tmp = realloc(*filters, (*cnt + 1) * sizeof(**filters)); 421 if (!tmp) 422 return -ENOMEM; 423 *filters = tmp; 424 425 f = &(*filters)[*cnt]; 426 memset(f, 0, sizeof(*f)); 427 428 /* First, let's check if it's a stats filter of the following form: 429 * <stat><op><value, where: 430 * - <stat> is one of supported numerical stats (verdict is also 431 * considered numerical, failure == 0, success == 1); 432 * - <op> is comparison operator (see `operators` definitions); 433 * - <value> is an integer (or failure/success, or false/true as 434 * special aliases for 0 and 1, respectively). 435 * If the form doesn't match what user provided, we assume file/prog 436 * glob filter. 437 */ 438 for (i = 0; i < ARRAY_SIZE(operators); i++) { 439 enum stat_variant var; 440 int id; 441 long val; 442 const char *end = str; 443 const char *op_str; 444 445 op_str = operators[i].op_str; 446 p = strstr(str, op_str); 447 if (!p) 448 continue; 449 450 if (!parse_stat_id_var(str, p - str, &id, &var)) { 451 fprintf(stderr, "Unrecognized stat name in '%s'!\n", str); 452 return -EINVAL; 453 } 454 if (id >= FILE_NAME) { 455 fprintf(stderr, "Non-integer stat is specified in '%s'!\n", str); 456 return -EINVAL; 457 } 458 459 p += strlen(op_str); 460 461 if (strcasecmp(p, "true") == 0 || 462 strcasecmp(p, "t") == 0 || 463 strcasecmp(p, "success") == 0 || 464 strcasecmp(p, "succ") == 0 || 465 strcasecmp(p, "s") == 0 || 466 strcasecmp(p, "match") == 0 || 467 strcasecmp(p, "m") == 0) { 468 val = 1; 469 } else if (strcasecmp(p, "false") == 0 || 470 strcasecmp(p, "f") == 0 || 471 strcasecmp(p, "failure") == 0 || 472 strcasecmp(p, "fail") == 0 || 473 strcasecmp(p, "mismatch") == 0 || 474 strcasecmp(p, "mis") == 0) { 475 val = 0; 476 } else { 477 errno = 0; 478 val = strtol(p, (char **)&end, 10); 479 if (errno || end == p || *end != '\0' ) { 480 fprintf(stderr, "Invalid integer value in '%s'!\n", str); 481 return -EINVAL; 482 } 483 } 484 485 f->kind = FILTER_STAT; 486 f->stat_id = id; 487 f->stat_var = var; 488 f->op = operators[i].op_kind; 489 f->value = val; 490 491 *cnt += 1; 492 return 0; 493 } 494 495 /* File/prog filter can be specified either as '<glob>' or 496 * '<file-glob>/<prog-glob>'. In the former case <glob> is applied to 497 * both file and program names. This seems to be way more useful in 498 * practice. If user needs full control, they can use '/<prog-glob>' 499 * form to glob just program name, or '<file-glob>/' to glob only file 500 * name. But usually common <glob> seems to be the most useful and 501 * ergonomic way. 502 */ 503 f->kind = FILTER_NAME; 504 p = strchr(str, '/'); 505 if (!p) { 506 f->any_glob = strdup(str); 507 if (!f->any_glob) 508 return -ENOMEM; 509 } else { 510 if (str != p) { 511 /* non-empty file glob */ 512 f->file_glob = strndup(str, p - str); 513 if (!f->file_glob) 514 return -ENOMEM; 515 } 516 if (strlen(p + 1) > 0) { 517 /* non-empty prog glob */ 518 f->prog_glob = strdup(p + 1); 519 if (!f->prog_glob) { 520 free(f->file_glob); 521 f->file_glob = NULL; 522 return -ENOMEM; 523 } 524 } 525 } 526 527 *cnt += 1; 528 return 0; 529 } 530 531 static int append_filter_file(const char *path) 532 { 533 char buf[1024]; 534 FILE *f; 535 int err = 0; 536 537 f = fopen(path, "r"); 538 if (!f) { 539 err = -errno; 540 fprintf(stderr, "Failed to open filters in '%s': %d\n", path, err); 541 return err; 542 } 543 544 while (fscanf(f, " %1023[^\n]\n", buf) == 1) { 545 /* lines starting with # are comments, skip them */ 546 if (buf[0] == '\0' || buf[0] == '#') 547 continue; 548 /* lines starting with ! are negative match filters */ 549 if (buf[0] == '!') 550 err = append_filter(&env.deny_filters, &env.deny_filter_cnt, buf + 1); 551 else 552 err = append_filter(&env.allow_filters, &env.allow_filter_cnt, buf); 553 if (err) 554 goto cleanup; 555 } 556 557 cleanup: 558 fclose(f); 559 return err; 560 } 561 562 static const struct stat_specs default_output_spec = { 563 .spec_cnt = 7, 564 .ids = { 565 FILE_NAME, PROG_NAME, VERDICT, DURATION, 566 TOTAL_INSNS, TOTAL_STATES, PEAK_STATES, 567 }, 568 }; 569 570 static const struct stat_specs default_csv_output_spec = { 571 .spec_cnt = 9, 572 .ids = { 573 FILE_NAME, PROG_NAME, VERDICT, DURATION, 574 TOTAL_INSNS, TOTAL_STATES, PEAK_STATES, 575 MAX_STATES_PER_INSN, MARK_READ_MAX_LEN, 576 }, 577 }; 578 579 static const struct stat_specs default_sort_spec = { 580 .spec_cnt = 2, 581 .ids = { 582 FILE_NAME, PROG_NAME, 583 }, 584 .asc = { true, true, }, 585 }; 586 587 /* sorting for comparison mode to join two data sets */ 588 static const struct stat_specs join_sort_spec = { 589 .spec_cnt = 2, 590 .ids = { 591 FILE_NAME, PROG_NAME, 592 }, 593 .asc = { true, true, }, 594 }; 595 596 static struct stat_def { 597 const char *header; 598 const char *names[4]; 599 bool asc_by_default; 600 bool left_aligned; 601 } stat_defs[] = { 602 [FILE_NAME] = { "File", {"file_name", "filename", "file"}, true /* asc */, true /* left */ }, 603 [PROG_NAME] = { "Program", {"prog_name", "progname", "prog"}, true /* asc */, true /* left */ }, 604 [VERDICT] = { "Verdict", {"verdict"}, true /* asc: failure, success */, true /* left */ }, 605 [DURATION] = { "Duration (us)", {"duration", "dur"}, }, 606 [TOTAL_INSNS] = { "Insns", {"total_insns", "insns"}, }, 607 [TOTAL_STATES] = { "States", {"total_states", "states"}, }, 608 [PEAK_STATES] = { "Peak states", {"peak_states"}, }, 609 [MAX_STATES_PER_INSN] = { "Max states per insn", {"max_states_per_insn"}, }, 610 [MARK_READ_MAX_LEN] = { "Max mark read length", {"max_mark_read_len", "mark_read"}, }, 611 }; 612 613 static bool parse_stat_id_var(const char *name, size_t len, int *id, enum stat_variant *var) 614 { 615 static const char *var_sfxs[] = { 616 [VARIANT_A] = "_a", 617 [VARIANT_B] = "_b", 618 [VARIANT_DIFF] = "_diff", 619 [VARIANT_PCT] = "_pct", 620 }; 621 int i, j, k; 622 623 for (i = 0; i < ARRAY_SIZE(stat_defs); i++) { 624 struct stat_def *def = &stat_defs[i]; 625 size_t alias_len, sfx_len; 626 const char *alias; 627 628 for (j = 0; j < ARRAY_SIZE(stat_defs[i].names); j++) { 629 alias = def->names[j]; 630 if (!alias) 631 continue; 632 633 alias_len = strlen(alias); 634 if (strncmp(name, alias, alias_len) != 0) 635 continue; 636 637 if (alias_len == len) { 638 /* If no variant suffix is specified, we 639 * assume control group (just in case we are 640 * in comparison mode. Variant is ignored in 641 * non-comparison mode. 642 */ 643 *var = VARIANT_B; 644 *id = i; 645 return true; 646 } 647 648 for (k = 0; k < ARRAY_SIZE(var_sfxs); k++) { 649 sfx_len = strlen(var_sfxs[k]); 650 if (alias_len + sfx_len != len) 651 continue; 652 653 if (strncmp(name + alias_len, var_sfxs[k], sfx_len) == 0) { 654 *var = (enum stat_variant)k; 655 *id = i; 656 return true; 657 } 658 } 659 } 660 } 661 662 return false; 663 } 664 665 static bool is_asc_sym(char c) 666 { 667 return c == '^'; 668 } 669 670 static bool is_desc_sym(char c) 671 { 672 return c == 'v' || c == 'V' || c == '.' || c == '!' || c == '_'; 673 } 674 675 static int parse_stat(const char *stat_name, struct stat_specs *specs) 676 { 677 int id; 678 bool has_order = false, is_asc = false; 679 size_t len = strlen(stat_name); 680 enum stat_variant var; 681 682 if (specs->spec_cnt >= ARRAY_SIZE(specs->ids)) { 683 fprintf(stderr, "Can't specify more than %zd stats\n", ARRAY_SIZE(specs->ids)); 684 return -E2BIG; 685 } 686 687 if (len > 1 && (is_asc_sym(stat_name[len - 1]) || is_desc_sym(stat_name[len - 1]))) { 688 has_order = true; 689 is_asc = is_asc_sym(stat_name[len - 1]); 690 len -= 1; 691 } 692 693 if (!parse_stat_id_var(stat_name, len, &id, &var)) { 694 fprintf(stderr, "Unrecognized stat name '%s'\n", stat_name); 695 return -ESRCH; 696 } 697 698 specs->ids[specs->spec_cnt] = id; 699 specs->variants[specs->spec_cnt] = var; 700 specs->asc[specs->spec_cnt] = has_order ? is_asc : stat_defs[id].asc_by_default; 701 specs->spec_cnt++; 702 703 return 0; 704 } 705 706 static int parse_stats(const char *stats_str, struct stat_specs *specs) 707 { 708 char *input, *state = NULL, *next; 709 int err; 710 711 input = strdup(stats_str); 712 if (!input) 713 return -ENOMEM; 714 715 while ((next = strtok_r(state ? NULL : input, ",", &state))) { 716 err = parse_stat(next, specs); 717 if (err) 718 return err; 719 } 720 721 return 0; 722 } 723 724 static void free_verif_stats(struct verif_stats *stats, size_t stat_cnt) 725 { 726 int i; 727 728 if (!stats) 729 return; 730 731 for (i = 0; i < stat_cnt; i++) { 732 free(stats[i].file_name); 733 free(stats[i].prog_name); 734 } 735 free(stats); 736 } 737 738 static char verif_log_buf[64 * 1024]; 739 740 #define MAX_PARSED_LOG_LINES 100 741 742 static int parse_verif_log(char * const buf, size_t buf_sz, struct verif_stats *s) 743 { 744 const char *cur; 745 int pos, lines; 746 747 buf[buf_sz - 1] = '\0'; 748 749 for (pos = strlen(buf) - 1, lines = 0; pos >= 0 && lines < MAX_PARSED_LOG_LINES; lines++) { 750 /* find previous endline or otherwise take the start of log buf */ 751 for (cur = &buf[pos]; cur > buf && cur[0] != '\n'; cur--, pos--) { 752 } 753 /* next time start from end of previous line (or pos goes to <0) */ 754 pos--; 755 /* if we found endline, point right after endline symbol; 756 * otherwise, stay at the beginning of log buf 757 */ 758 if (cur[0] == '\n') 759 cur++; 760 761 if (1 == sscanf(cur, "verification time %ld usec\n", &s->stats[DURATION])) 762 continue; 763 if (6 == sscanf(cur, "processed %ld insns (limit %*d) max_states_per_insn %ld total_states %ld peak_states %ld mark_read %ld", 764 &s->stats[TOTAL_INSNS], 765 &s->stats[MAX_STATES_PER_INSN], 766 &s->stats[TOTAL_STATES], 767 &s->stats[PEAK_STATES], 768 &s->stats[MARK_READ_MAX_LEN])) 769 continue; 770 } 771 772 return 0; 773 } 774 775 static void fixup_obj(struct bpf_object *obj) 776 { 777 struct bpf_map *map; 778 779 bpf_object__for_each_map(map, obj) { 780 /* disable pinning */ 781 bpf_map__set_pin_path(map, NULL); 782 783 /* fix up map size, if necessary */ 784 switch (bpf_map__type(map)) { 785 case BPF_MAP_TYPE_SK_STORAGE: 786 case BPF_MAP_TYPE_TASK_STORAGE: 787 case BPF_MAP_TYPE_INODE_STORAGE: 788 case BPF_MAP_TYPE_CGROUP_STORAGE: 789 break; 790 default: 791 if (bpf_map__max_entries(map) == 0) 792 bpf_map__set_max_entries(map, 1); 793 } 794 } 795 } 796 797 static int process_prog(const char *filename, struct bpf_object *obj, struct bpf_program *prog) 798 { 799 const char *prog_name = bpf_program__name(prog); 800 size_t buf_sz = sizeof(verif_log_buf); 801 char *buf = verif_log_buf; 802 struct verif_stats *stats; 803 int err = 0; 804 void *tmp; 805 806 if (!should_process_file_prog(basename(filename), bpf_program__name(prog))) { 807 env.progs_skipped++; 808 return 0; 809 } 810 811 tmp = realloc(env.prog_stats, (env.prog_stat_cnt + 1) * sizeof(*env.prog_stats)); 812 if (!tmp) 813 return -ENOMEM; 814 env.prog_stats = tmp; 815 stats = &env.prog_stats[env.prog_stat_cnt++]; 816 memset(stats, 0, sizeof(*stats)); 817 818 if (env.verbose) { 819 buf_sz = 16 * 1024 * 1024; 820 buf = malloc(buf_sz); 821 if (!buf) 822 return -ENOMEM; 823 bpf_program__set_log_buf(prog, buf, buf_sz); 824 bpf_program__set_log_level(prog, env.log_level | 4); /* stats + log */ 825 } else { 826 bpf_program__set_log_buf(prog, buf, buf_sz); 827 bpf_program__set_log_level(prog, 4); /* only verifier stats */ 828 } 829 verif_log_buf[0] = '\0'; 830 831 /* increase chances of successful BPF object loading */ 832 fixup_obj(obj); 833 834 err = bpf_object__load(obj); 835 env.progs_processed++; 836 837 stats->file_name = strdup(basename(filename)); 838 stats->prog_name = strdup(bpf_program__name(prog)); 839 stats->stats[VERDICT] = err == 0; /* 1 - success, 0 - failure */ 840 parse_verif_log(buf, buf_sz, stats); 841 842 if (env.verbose) { 843 printf("PROCESSING %s/%s, DURATION US: %ld, VERDICT: %s, VERIFIER LOG:\n%s\n", 844 filename, prog_name, stats->stats[DURATION], 845 err ? "failure" : "success", buf); 846 } 847 848 if (verif_log_buf != buf) 849 free(buf); 850 851 return 0; 852 }; 853 854 static int process_obj(const char *filename) 855 { 856 struct bpf_object *obj = NULL, *tobj; 857 struct bpf_program *prog, *tprog, *lprog; 858 libbpf_print_fn_t old_libbpf_print_fn; 859 LIBBPF_OPTS(bpf_object_open_opts, opts); 860 int err = 0, prog_cnt = 0; 861 862 if (!should_process_file_prog(basename(filename), NULL)) { 863 if (env.verbose) 864 printf("Skipping '%s' due to filters...\n", filename); 865 env.files_skipped++; 866 return 0; 867 } 868 if (!is_bpf_obj_file(filename)) { 869 if (env.verbose) 870 printf("Skipping '%s' as it's not a BPF object file...\n", filename); 871 env.files_skipped++; 872 return 0; 873 } 874 875 if (!env.quiet && env.out_fmt == RESFMT_TABLE) 876 printf("Processing '%s'...\n", basename(filename)); 877 878 old_libbpf_print_fn = libbpf_set_print(libbpf_print_fn); 879 obj = bpf_object__open_file(filename, &opts); 880 if (!obj) { 881 /* if libbpf can't open BPF object file, it could be because 882 * that BPF object file is incomplete and has to be statically 883 * linked into a final BPF object file; instead of bailing 884 * out, report it into stderr, mark it as skipped, and 885 * proceed 886 */ 887 fprintf(stderr, "Failed to open '%s': %d\n", filename, -errno); 888 env.files_skipped++; 889 err = 0; 890 goto cleanup; 891 } 892 893 env.files_processed++; 894 895 bpf_object__for_each_program(prog, obj) { 896 prog_cnt++; 897 } 898 899 if (prog_cnt == 1) { 900 prog = bpf_object__next_program(obj, NULL); 901 bpf_program__set_autoload(prog, true); 902 process_prog(filename, obj, prog); 903 goto cleanup; 904 } 905 906 bpf_object__for_each_program(prog, obj) { 907 const char *prog_name = bpf_program__name(prog); 908 909 tobj = bpf_object__open_file(filename, &opts); 910 if (!tobj) { 911 err = -errno; 912 fprintf(stderr, "Failed to open '%s': %d\n", filename, err); 913 goto cleanup; 914 } 915 916 bpf_object__for_each_program(tprog, tobj) { 917 const char *tprog_name = bpf_program__name(tprog); 918 919 if (strcmp(prog_name, tprog_name) == 0) { 920 bpf_program__set_autoload(tprog, true); 921 lprog = tprog; 922 } else { 923 bpf_program__set_autoload(tprog, false); 924 } 925 } 926 927 process_prog(filename, tobj, lprog); 928 bpf_object__close(tobj); 929 } 930 931 cleanup: 932 bpf_object__close(obj); 933 libbpf_set_print(old_libbpf_print_fn); 934 return err; 935 } 936 937 static int cmp_stat(const struct verif_stats *s1, const struct verif_stats *s2, 938 enum stat_id id, bool asc) 939 { 940 int cmp = 0; 941 942 switch (id) { 943 case FILE_NAME: 944 cmp = strcmp(s1->file_name, s2->file_name); 945 break; 946 case PROG_NAME: 947 cmp = strcmp(s1->prog_name, s2->prog_name); 948 break; 949 case VERDICT: 950 case DURATION: 951 case TOTAL_INSNS: 952 case TOTAL_STATES: 953 case PEAK_STATES: 954 case MAX_STATES_PER_INSN: 955 case MARK_READ_MAX_LEN: { 956 long v1 = s1->stats[id]; 957 long v2 = s2->stats[id]; 958 959 if (v1 != v2) 960 cmp = v1 < v2 ? -1 : 1; 961 break; 962 } 963 default: 964 fprintf(stderr, "Unrecognized stat #%d\n", id); 965 exit(1); 966 } 967 968 return asc ? cmp : -cmp; 969 } 970 971 static int cmp_prog_stats(const void *v1, const void *v2) 972 { 973 const struct verif_stats *s1 = v1, *s2 = v2; 974 int i, cmp; 975 976 for (i = 0; i < env.sort_spec.spec_cnt; i++) { 977 cmp = cmp_stat(s1, s2, env.sort_spec.ids[i], env.sort_spec.asc[i]); 978 if (cmp != 0) 979 return cmp; 980 } 981 982 /* always disambiguate with file+prog, which are unique */ 983 cmp = strcmp(s1->file_name, s2->file_name); 984 if (cmp != 0) 985 return cmp; 986 return strcmp(s1->prog_name, s2->prog_name); 987 } 988 989 static void fetch_join_stat_value(const struct verif_stats_join *s, 990 enum stat_id id, enum stat_variant var, 991 const char **str_val, 992 double *num_val) 993 { 994 long v1, v2; 995 996 if (id == FILE_NAME) { 997 *str_val = s->file_name; 998 return; 999 } 1000 if (id == PROG_NAME) { 1001 *str_val = s->prog_name; 1002 return; 1003 } 1004 1005 v1 = s->stats_a ? s->stats_a->stats[id] : 0; 1006 v2 = s->stats_b ? s->stats_b->stats[id] : 0; 1007 1008 switch (var) { 1009 case VARIANT_A: 1010 if (!s->stats_a) 1011 *num_val = -DBL_MAX; 1012 else 1013 *num_val = s->stats_a->stats[id]; 1014 return; 1015 case VARIANT_B: 1016 if (!s->stats_b) 1017 *num_val = -DBL_MAX; 1018 else 1019 *num_val = s->stats_b->stats[id]; 1020 return; 1021 case VARIANT_DIFF: 1022 if (!s->stats_a || !s->stats_b) 1023 *num_val = -DBL_MAX; 1024 else if (id == VERDICT) 1025 *num_val = v1 == v2 ? 1.0 /* MATCH */ : 0.0 /* MISMATCH */; 1026 else 1027 *num_val = (double)(v2 - v1); 1028 return; 1029 case VARIANT_PCT: 1030 if (!s->stats_a || !s->stats_b) { 1031 *num_val = -DBL_MAX; 1032 } else if (v1 == 0) { 1033 if (v1 == v2) 1034 *num_val = 0.0; 1035 else 1036 *num_val = v2 < v1 ? -100.0 : 100.0; 1037 } else { 1038 *num_val = (v2 - v1) * 100.0 / v1; 1039 } 1040 return; 1041 } 1042 } 1043 1044 static int cmp_join_stat(const struct verif_stats_join *s1, 1045 const struct verif_stats_join *s2, 1046 enum stat_id id, enum stat_variant var, bool asc) 1047 { 1048 const char *str1 = NULL, *str2 = NULL; 1049 double v1, v2; 1050 int cmp = 0; 1051 1052 fetch_join_stat_value(s1, id, var, &str1, &v1); 1053 fetch_join_stat_value(s2, id, var, &str2, &v2); 1054 1055 if (str1) 1056 cmp = strcmp(str1, str2); 1057 else if (v1 != v2) 1058 cmp = v1 < v2 ? -1 : 1; 1059 1060 return asc ? cmp : -cmp; 1061 } 1062 1063 static int cmp_join_stats(const void *v1, const void *v2) 1064 { 1065 const struct verif_stats_join *s1 = v1, *s2 = v2; 1066 int i, cmp; 1067 1068 for (i = 0; i < env.sort_spec.spec_cnt; i++) { 1069 cmp = cmp_join_stat(s1, s2, 1070 env.sort_spec.ids[i], 1071 env.sort_spec.variants[i], 1072 env.sort_spec.asc[i]); 1073 if (cmp != 0) 1074 return cmp; 1075 } 1076 1077 /* always disambiguate with file+prog, which are unique */ 1078 cmp = strcmp(s1->file_name, s2->file_name); 1079 if (cmp != 0) 1080 return cmp; 1081 return strcmp(s1->prog_name, s2->prog_name); 1082 } 1083 1084 #define HEADER_CHAR '-' 1085 #define COLUMN_SEP " " 1086 1087 static void output_header_underlines(void) 1088 { 1089 int i, j, len; 1090 1091 for (i = 0; i < env.output_spec.spec_cnt; i++) { 1092 len = env.output_spec.lens[i]; 1093 1094 printf("%s", i == 0 ? "" : COLUMN_SEP); 1095 for (j = 0; j < len; j++) 1096 printf("%c", HEADER_CHAR); 1097 } 1098 printf("\n"); 1099 } 1100 1101 static void output_headers(enum resfmt fmt) 1102 { 1103 const char *fmt_str; 1104 int i, len; 1105 1106 for (i = 0; i < env.output_spec.spec_cnt; i++) { 1107 int id = env.output_spec.ids[i]; 1108 int *max_len = &env.output_spec.lens[i]; 1109 1110 switch (fmt) { 1111 case RESFMT_TABLE_CALCLEN: 1112 len = snprintf(NULL, 0, "%s", stat_defs[id].header); 1113 if (len > *max_len) 1114 *max_len = len; 1115 break; 1116 case RESFMT_TABLE: 1117 fmt_str = stat_defs[id].left_aligned ? "%s%-*s" : "%s%*s"; 1118 printf(fmt_str, i == 0 ? "" : COLUMN_SEP, *max_len, stat_defs[id].header); 1119 if (i == env.output_spec.spec_cnt - 1) 1120 printf("\n"); 1121 break; 1122 case RESFMT_CSV: 1123 printf("%s%s", i == 0 ? "" : ",", stat_defs[id].names[0]); 1124 if (i == env.output_spec.spec_cnt - 1) 1125 printf("\n"); 1126 break; 1127 } 1128 } 1129 1130 if (fmt == RESFMT_TABLE) 1131 output_header_underlines(); 1132 } 1133 1134 static void prepare_value(const struct verif_stats *s, enum stat_id id, 1135 const char **str, long *val) 1136 { 1137 switch (id) { 1138 case FILE_NAME: 1139 *str = s ? s->file_name : "N/A"; 1140 break; 1141 case PROG_NAME: 1142 *str = s ? s->prog_name : "N/A"; 1143 break; 1144 case VERDICT: 1145 if (!s) 1146 *str = "N/A"; 1147 else 1148 *str = s->stats[VERDICT] ? "success" : "failure"; 1149 break; 1150 case DURATION: 1151 case TOTAL_INSNS: 1152 case TOTAL_STATES: 1153 case PEAK_STATES: 1154 case MAX_STATES_PER_INSN: 1155 case MARK_READ_MAX_LEN: 1156 *val = s ? s->stats[id] : 0; 1157 break; 1158 default: 1159 fprintf(stderr, "Unrecognized stat #%d\n", id); 1160 exit(1); 1161 } 1162 } 1163 1164 static void output_stats(const struct verif_stats *s, enum resfmt fmt, bool last) 1165 { 1166 int i; 1167 1168 for (i = 0; i < env.output_spec.spec_cnt; i++) { 1169 int id = env.output_spec.ids[i]; 1170 int *max_len = &env.output_spec.lens[i], len; 1171 const char *str = NULL; 1172 long val = 0; 1173 1174 prepare_value(s, id, &str, &val); 1175 1176 switch (fmt) { 1177 case RESFMT_TABLE_CALCLEN: 1178 if (str) 1179 len = snprintf(NULL, 0, "%s", str); 1180 else 1181 len = snprintf(NULL, 0, "%ld", val); 1182 if (len > *max_len) 1183 *max_len = len; 1184 break; 1185 case RESFMT_TABLE: 1186 if (str) 1187 printf("%s%-*s", i == 0 ? "" : COLUMN_SEP, *max_len, str); 1188 else 1189 printf("%s%*ld", i == 0 ? "" : COLUMN_SEP, *max_len, val); 1190 if (i == env.output_spec.spec_cnt - 1) 1191 printf("\n"); 1192 break; 1193 case RESFMT_CSV: 1194 if (str) 1195 printf("%s%s", i == 0 ? "" : ",", str); 1196 else 1197 printf("%s%ld", i == 0 ? "" : ",", val); 1198 if (i == env.output_spec.spec_cnt - 1) 1199 printf("\n"); 1200 break; 1201 } 1202 } 1203 1204 if (last && fmt == RESFMT_TABLE) { 1205 output_header_underlines(); 1206 printf("Done. Processed %d files, %d programs. Skipped %d files, %d programs.\n", 1207 env.files_processed, env.files_skipped, env.progs_processed, env.progs_skipped); 1208 } 1209 } 1210 1211 static int parse_stat_value(const char *str, enum stat_id id, struct verif_stats *st) 1212 { 1213 switch (id) { 1214 case FILE_NAME: 1215 st->file_name = strdup(str); 1216 if (!st->file_name) 1217 return -ENOMEM; 1218 break; 1219 case PROG_NAME: 1220 st->prog_name = strdup(str); 1221 if (!st->prog_name) 1222 return -ENOMEM; 1223 break; 1224 case VERDICT: 1225 if (strcmp(str, "success") == 0) { 1226 st->stats[VERDICT] = true; 1227 } else if (strcmp(str, "failure") == 0) { 1228 st->stats[VERDICT] = false; 1229 } else { 1230 fprintf(stderr, "Unrecognized verification verdict '%s'\n", str); 1231 return -EINVAL; 1232 } 1233 break; 1234 case DURATION: 1235 case TOTAL_INSNS: 1236 case TOTAL_STATES: 1237 case PEAK_STATES: 1238 case MAX_STATES_PER_INSN: 1239 case MARK_READ_MAX_LEN: { 1240 long val; 1241 int err, n; 1242 1243 if (sscanf(str, "%ld %n", &val, &n) != 1 || n != strlen(str)) { 1244 err = -errno; 1245 fprintf(stderr, "Failed to parse '%s' as integer\n", str); 1246 return err; 1247 } 1248 1249 st->stats[id] = val; 1250 break; 1251 } 1252 default: 1253 fprintf(stderr, "Unrecognized stat #%d\n", id); 1254 return -EINVAL; 1255 } 1256 return 0; 1257 } 1258 1259 static int parse_stats_csv(const char *filename, struct stat_specs *specs, 1260 struct verif_stats **statsp, int *stat_cntp) 1261 { 1262 char line[4096]; 1263 FILE *f; 1264 int err = 0; 1265 bool header = true; 1266 1267 f = fopen(filename, "r"); 1268 if (!f) { 1269 err = -errno; 1270 fprintf(stderr, "Failed to open '%s': %d\n", filename, err); 1271 return err; 1272 } 1273 1274 *stat_cntp = 0; 1275 1276 while (fgets(line, sizeof(line), f)) { 1277 char *input = line, *state = NULL, *next; 1278 struct verif_stats *st = NULL; 1279 int col = 0; 1280 1281 if (!header) { 1282 void *tmp; 1283 1284 tmp = realloc(*statsp, (*stat_cntp + 1) * sizeof(**statsp)); 1285 if (!tmp) { 1286 err = -ENOMEM; 1287 goto cleanup; 1288 } 1289 *statsp = tmp; 1290 1291 st = &(*statsp)[*stat_cntp]; 1292 memset(st, 0, sizeof(*st)); 1293 1294 *stat_cntp += 1; 1295 } 1296 1297 while ((next = strtok_r(state ? NULL : input, ",\n", &state))) { 1298 if (header) { 1299 /* for the first line, set up spec stats */ 1300 err = parse_stat(next, specs); 1301 if (err) 1302 goto cleanup; 1303 continue; 1304 } 1305 1306 /* for all other lines, parse values based on spec */ 1307 if (col >= specs->spec_cnt) { 1308 fprintf(stderr, "Found extraneous column #%d in row #%d of '%s'\n", 1309 col, *stat_cntp, filename); 1310 err = -EINVAL; 1311 goto cleanup; 1312 } 1313 err = parse_stat_value(next, specs->ids[col], st); 1314 if (err) 1315 goto cleanup; 1316 col++; 1317 } 1318 1319 if (header) { 1320 header = false; 1321 continue; 1322 } 1323 1324 if (col < specs->spec_cnt) { 1325 fprintf(stderr, "Not enough columns in row #%d in '%s'\n", 1326 *stat_cntp, filename); 1327 err = -EINVAL; 1328 goto cleanup; 1329 } 1330 1331 if (!st->file_name || !st->prog_name) { 1332 fprintf(stderr, "Row #%d in '%s' is missing file and/or program name\n", 1333 *stat_cntp, filename); 1334 err = -EINVAL; 1335 goto cleanup; 1336 } 1337 1338 /* in comparison mode we can only check filters after we 1339 * parsed entire line; if row should be ignored we pretend we 1340 * never parsed it 1341 */ 1342 if (!should_process_file_prog(st->file_name, st->prog_name)) { 1343 free(st->file_name); 1344 free(st->prog_name); 1345 *stat_cntp -= 1; 1346 } 1347 } 1348 1349 if (!feof(f)) { 1350 err = -errno; 1351 fprintf(stderr, "Failed I/O for '%s': %d\n", filename, err); 1352 } 1353 1354 cleanup: 1355 fclose(f); 1356 return err; 1357 } 1358 1359 /* empty/zero stats for mismatched rows */ 1360 static const struct verif_stats fallback_stats = { .file_name = "", .prog_name = "" }; 1361 1362 static bool is_key_stat(enum stat_id id) 1363 { 1364 return id == FILE_NAME || id == PROG_NAME; 1365 } 1366 1367 static void output_comp_header_underlines(void) 1368 { 1369 int i, j, k; 1370 1371 for (i = 0; i < env.output_spec.spec_cnt; i++) { 1372 int id = env.output_spec.ids[i]; 1373 int max_j = is_key_stat(id) ? 1 : 3; 1374 1375 for (j = 0; j < max_j; j++) { 1376 int len = env.output_spec.lens[3 * i + j]; 1377 1378 printf("%s", i + j == 0 ? "" : COLUMN_SEP); 1379 1380 for (k = 0; k < len; k++) 1381 printf("%c", HEADER_CHAR); 1382 } 1383 } 1384 printf("\n"); 1385 } 1386 1387 static void output_comp_headers(enum resfmt fmt) 1388 { 1389 static const char *table_sfxs[3] = {" (A)", " (B)", " (DIFF)"}; 1390 static const char *name_sfxs[3] = {"_base", "_comp", "_diff"}; 1391 int i, j, len; 1392 1393 for (i = 0; i < env.output_spec.spec_cnt; i++) { 1394 int id = env.output_spec.ids[i]; 1395 /* key stats don't have A/B/DIFF columns, they are common for both data sets */ 1396 int max_j = is_key_stat(id) ? 1 : 3; 1397 1398 for (j = 0; j < max_j; j++) { 1399 int *max_len = &env.output_spec.lens[3 * i + j]; 1400 bool last = (i == env.output_spec.spec_cnt - 1) && (j == max_j - 1); 1401 const char *sfx; 1402 1403 switch (fmt) { 1404 case RESFMT_TABLE_CALCLEN: 1405 sfx = is_key_stat(id) ? "" : table_sfxs[j]; 1406 len = snprintf(NULL, 0, "%s%s", stat_defs[id].header, sfx); 1407 if (len > *max_len) 1408 *max_len = len; 1409 break; 1410 case RESFMT_TABLE: 1411 sfx = is_key_stat(id) ? "" : table_sfxs[j]; 1412 printf("%s%-*s%s", i + j == 0 ? "" : COLUMN_SEP, 1413 *max_len - (int)strlen(sfx), stat_defs[id].header, sfx); 1414 if (last) 1415 printf("\n"); 1416 break; 1417 case RESFMT_CSV: 1418 sfx = is_key_stat(id) ? "" : name_sfxs[j]; 1419 printf("%s%s%s", i + j == 0 ? "" : ",", stat_defs[id].names[0], sfx); 1420 if (last) 1421 printf("\n"); 1422 break; 1423 } 1424 } 1425 } 1426 1427 if (fmt == RESFMT_TABLE) 1428 output_comp_header_underlines(); 1429 } 1430 1431 static void output_comp_stats(const struct verif_stats_join *join_stats, 1432 enum resfmt fmt, bool last) 1433 { 1434 const struct verif_stats *base = join_stats->stats_a; 1435 const struct verif_stats *comp = join_stats->stats_b; 1436 char base_buf[1024] = {}, comp_buf[1024] = {}, diff_buf[1024] = {}; 1437 int i; 1438 1439 for (i = 0; i < env.output_spec.spec_cnt; i++) { 1440 int id = env.output_spec.ids[i], len; 1441 int *max_len_base = &env.output_spec.lens[3 * i + 0]; 1442 int *max_len_comp = &env.output_spec.lens[3 * i + 1]; 1443 int *max_len_diff = &env.output_spec.lens[3 * i + 2]; 1444 const char *base_str = NULL, *comp_str = NULL; 1445 long base_val = 0, comp_val = 0, diff_val = 0; 1446 1447 prepare_value(base, id, &base_str, &base_val); 1448 prepare_value(comp, id, &comp_str, &comp_val); 1449 1450 /* normalize all the outputs to be in string buffers for simplicity */ 1451 if (is_key_stat(id)) { 1452 /* key stats (file and program name) are always strings */ 1453 if (base) 1454 snprintf(base_buf, sizeof(base_buf), "%s", base_str); 1455 else 1456 snprintf(base_buf, sizeof(base_buf), "%s", comp_str); 1457 } else if (base_str) { 1458 snprintf(base_buf, sizeof(base_buf), "%s", base_str); 1459 snprintf(comp_buf, sizeof(comp_buf), "%s", comp_str); 1460 if (!base || !comp) 1461 snprintf(diff_buf, sizeof(diff_buf), "%s", "N/A"); 1462 else if (strcmp(base_str, comp_str) == 0) 1463 snprintf(diff_buf, sizeof(diff_buf), "%s", "MATCH"); 1464 else 1465 snprintf(diff_buf, sizeof(diff_buf), "%s", "MISMATCH"); 1466 } else { 1467 double p = 0.0; 1468 1469 if (base) 1470 snprintf(base_buf, sizeof(base_buf), "%ld", base_val); 1471 else 1472 snprintf(base_buf, sizeof(base_buf), "%s", "N/A"); 1473 if (comp) 1474 snprintf(comp_buf, sizeof(comp_buf), "%ld", comp_val); 1475 else 1476 snprintf(comp_buf, sizeof(comp_buf), "%s", "N/A"); 1477 1478 diff_val = comp_val - base_val; 1479 if (!base || !comp) { 1480 snprintf(diff_buf, sizeof(diff_buf), "%s", "N/A"); 1481 } else { 1482 if (base_val == 0) { 1483 if (comp_val == base_val) 1484 p = 0.0; /* avoid +0 (+100%) case */ 1485 else 1486 p = comp_val < base_val ? -100.0 : 100.0; 1487 } else { 1488 p = diff_val * 100.0 / base_val; 1489 } 1490 snprintf(diff_buf, sizeof(diff_buf), "%+ld (%+.2lf%%)", diff_val, p); 1491 } 1492 } 1493 1494 switch (fmt) { 1495 case RESFMT_TABLE_CALCLEN: 1496 len = strlen(base_buf); 1497 if (len > *max_len_base) 1498 *max_len_base = len; 1499 if (!is_key_stat(id)) { 1500 len = strlen(comp_buf); 1501 if (len > *max_len_comp) 1502 *max_len_comp = len; 1503 len = strlen(diff_buf); 1504 if (len > *max_len_diff) 1505 *max_len_diff = len; 1506 } 1507 break; 1508 case RESFMT_TABLE: { 1509 /* string outputs are left-aligned, number outputs are right-aligned */ 1510 const char *fmt = base_str ? "%s%-*s" : "%s%*s"; 1511 1512 printf(fmt, i == 0 ? "" : COLUMN_SEP, *max_len_base, base_buf); 1513 if (!is_key_stat(id)) { 1514 printf(fmt, COLUMN_SEP, *max_len_comp, comp_buf); 1515 printf(fmt, COLUMN_SEP, *max_len_diff, diff_buf); 1516 } 1517 if (i == env.output_spec.spec_cnt - 1) 1518 printf("\n"); 1519 break; 1520 } 1521 case RESFMT_CSV: 1522 printf("%s%s", i == 0 ? "" : ",", base_buf); 1523 if (!is_key_stat(id)) { 1524 printf("%s%s", i == 0 ? "" : ",", comp_buf); 1525 printf("%s%s", i == 0 ? "" : ",", diff_buf); 1526 } 1527 if (i == env.output_spec.spec_cnt - 1) 1528 printf("\n"); 1529 break; 1530 } 1531 } 1532 1533 if (last && fmt == RESFMT_TABLE) 1534 output_comp_header_underlines(); 1535 } 1536 1537 static int cmp_stats_key(const struct verif_stats *base, const struct verif_stats *comp) 1538 { 1539 int r; 1540 1541 r = strcmp(base->file_name, comp->file_name); 1542 if (r != 0) 1543 return r; 1544 return strcmp(base->prog_name, comp->prog_name); 1545 } 1546 1547 static bool is_join_stat_filter_matched(struct filter *f, const struct verif_stats_join *stats) 1548 { 1549 static const double eps = 1e-9; 1550 const char *str = NULL; 1551 double value = 0.0; 1552 1553 fetch_join_stat_value(stats, f->stat_id, f->stat_var, &str, &value); 1554 1555 switch (f->op) { 1556 case OP_EQ: return value > f->value - eps && value < f->value + eps; 1557 case OP_NEQ: return value < f->value - eps || value > f->value + eps; 1558 case OP_LT: return value < f->value - eps; 1559 case OP_LE: return value <= f->value + eps; 1560 case OP_GT: return value > f->value + eps; 1561 case OP_GE: return value >= f->value - eps; 1562 } 1563 1564 fprintf(stderr, "BUG: unknown filter op %d!\n", f->op); 1565 return false; 1566 } 1567 1568 static bool should_output_join_stats(const struct verif_stats_join *stats) 1569 { 1570 struct filter *f; 1571 int i, allow_cnt = 0; 1572 1573 for (i = 0; i < env.deny_filter_cnt; i++) { 1574 f = &env.deny_filters[i]; 1575 if (f->kind != FILTER_STAT) 1576 continue; 1577 1578 if (is_join_stat_filter_matched(f, stats)) 1579 return false; 1580 } 1581 1582 for (i = 0; i < env.allow_filter_cnt; i++) { 1583 f = &env.allow_filters[i]; 1584 if (f->kind != FILTER_STAT) 1585 continue; 1586 allow_cnt++; 1587 1588 if (is_join_stat_filter_matched(f, stats)) 1589 return true; 1590 } 1591 1592 /* if there are no stat allowed filters, pass everything through */ 1593 return allow_cnt == 0; 1594 } 1595 1596 static int handle_comparison_mode(void) 1597 { 1598 struct stat_specs base_specs = {}, comp_specs = {}; 1599 struct stat_specs tmp_sort_spec; 1600 enum resfmt cur_fmt; 1601 int err, i, j, last_idx; 1602 1603 if (env.filename_cnt != 2) { 1604 fprintf(stderr, "Comparison mode expects exactly two input CSV files!\n\n"); 1605 argp_help(&argp, stderr, ARGP_HELP_USAGE, "veristat"); 1606 return -EINVAL; 1607 } 1608 1609 err = parse_stats_csv(env.filenames[0], &base_specs, 1610 &env.baseline_stats, &env.baseline_stat_cnt); 1611 if (err) { 1612 fprintf(stderr, "Failed to parse stats from '%s': %d\n", env.filenames[0], err); 1613 return err; 1614 } 1615 err = parse_stats_csv(env.filenames[1], &comp_specs, 1616 &env.prog_stats, &env.prog_stat_cnt); 1617 if (err) { 1618 fprintf(stderr, "Failed to parse stats from '%s': %d\n", env.filenames[1], err); 1619 return err; 1620 } 1621 1622 /* To keep it simple we validate that the set and order of stats in 1623 * both CSVs are exactly the same. This can be lifted with a bit more 1624 * pre-processing later. 1625 */ 1626 if (base_specs.spec_cnt != comp_specs.spec_cnt) { 1627 fprintf(stderr, "Number of stats in '%s' and '%s' differs (%d != %d)!\n", 1628 env.filenames[0], env.filenames[1], 1629 base_specs.spec_cnt, comp_specs.spec_cnt); 1630 return -EINVAL; 1631 } 1632 for (i = 0; i < base_specs.spec_cnt; i++) { 1633 if (base_specs.ids[i] != comp_specs.ids[i]) { 1634 fprintf(stderr, "Stats composition differs between '%s' and '%s' (%s != %s)!\n", 1635 env.filenames[0], env.filenames[1], 1636 stat_defs[base_specs.ids[i]].names[0], 1637 stat_defs[comp_specs.ids[i]].names[0]); 1638 return -EINVAL; 1639 } 1640 } 1641 1642 /* Replace user-specified sorting spec with file+prog sorting rule to 1643 * be able to join two datasets correctly. Once we are done, we will 1644 * restore the original sort spec. 1645 */ 1646 tmp_sort_spec = env.sort_spec; 1647 env.sort_spec = join_sort_spec; 1648 qsort(env.prog_stats, env.prog_stat_cnt, sizeof(*env.prog_stats), cmp_prog_stats); 1649 qsort(env.baseline_stats, env.baseline_stat_cnt, sizeof(*env.baseline_stats), cmp_prog_stats); 1650 env.sort_spec = tmp_sort_spec; 1651 1652 /* Join two datasets together. If baseline and comparison datasets 1653 * have different subset of rows (we match by 'object + prog' as 1654 * a unique key) then assume empty/missing/zero value for rows that 1655 * are missing in the opposite data set. 1656 */ 1657 i = j = 0; 1658 while (i < env.baseline_stat_cnt || j < env.prog_stat_cnt) { 1659 const struct verif_stats *base, *comp; 1660 struct verif_stats_join *join; 1661 void *tmp; 1662 int r; 1663 1664 base = i < env.baseline_stat_cnt ? &env.baseline_stats[i] : &fallback_stats; 1665 comp = j < env.prog_stat_cnt ? &env.prog_stats[j] : &fallback_stats; 1666 1667 if (!base->file_name || !base->prog_name) { 1668 fprintf(stderr, "Entry #%d in '%s' doesn't have file and/or program name specified!\n", 1669 i, env.filenames[0]); 1670 return -EINVAL; 1671 } 1672 if (!comp->file_name || !comp->prog_name) { 1673 fprintf(stderr, "Entry #%d in '%s' doesn't have file and/or program name specified!\n", 1674 j, env.filenames[1]); 1675 return -EINVAL; 1676 } 1677 1678 tmp = realloc(env.join_stats, (env.join_stat_cnt + 1) * sizeof(*env.join_stats)); 1679 if (!tmp) 1680 return -ENOMEM; 1681 env.join_stats = tmp; 1682 1683 join = &env.join_stats[env.join_stat_cnt]; 1684 memset(join, 0, sizeof(*join)); 1685 1686 r = cmp_stats_key(base, comp); 1687 if (r == 0) { 1688 join->file_name = base->file_name; 1689 join->prog_name = base->prog_name; 1690 join->stats_a = base; 1691 join->stats_b = comp; 1692 i++; 1693 j++; 1694 } else if (comp == &fallback_stats || r < 0) { 1695 join->file_name = base->file_name; 1696 join->prog_name = base->prog_name; 1697 join->stats_a = base; 1698 join->stats_b = NULL; 1699 i++; 1700 } else { 1701 join->file_name = comp->file_name; 1702 join->prog_name = comp->prog_name; 1703 join->stats_a = NULL; 1704 join->stats_b = comp; 1705 j++; 1706 } 1707 env.join_stat_cnt += 1; 1708 } 1709 1710 /* now sort joined results accorsing to sort spec */ 1711 qsort(env.join_stats, env.join_stat_cnt, sizeof(*env.join_stats), cmp_join_stats); 1712 1713 /* for human-readable table output we need to do extra pass to 1714 * calculate column widths, so we substitute current output format 1715 * with RESFMT_TABLE_CALCLEN and later revert it back to RESFMT_TABLE 1716 * and do everything again. 1717 */ 1718 if (env.out_fmt == RESFMT_TABLE) 1719 cur_fmt = RESFMT_TABLE_CALCLEN; 1720 else 1721 cur_fmt = env.out_fmt; 1722 1723 one_more_time: 1724 output_comp_headers(cur_fmt); 1725 1726 for (i = 0; i < env.join_stat_cnt; i++) { 1727 const struct verif_stats_join *join = &env.join_stats[i]; 1728 1729 if (!should_output_join_stats(join)) 1730 continue; 1731 1732 if (cur_fmt == RESFMT_TABLE_CALCLEN) 1733 last_idx = i; 1734 1735 output_comp_stats(join, cur_fmt, i == last_idx); 1736 } 1737 1738 if (cur_fmt == RESFMT_TABLE_CALCLEN) { 1739 cur_fmt = RESFMT_TABLE; 1740 goto one_more_time; /* ... this time with feeling */ 1741 } 1742 1743 return 0; 1744 } 1745 1746 static bool is_stat_filter_matched(struct filter *f, const struct verif_stats *stats) 1747 { 1748 long value = stats->stats[f->stat_id]; 1749 1750 switch (f->op) { 1751 case OP_EQ: return value == f->value; 1752 case OP_NEQ: return value != f->value; 1753 case OP_LT: return value < f->value; 1754 case OP_LE: return value <= f->value; 1755 case OP_GT: return value > f->value; 1756 case OP_GE: return value >= f->value; 1757 } 1758 1759 fprintf(stderr, "BUG: unknown filter op %d!\n", f->op); 1760 return false; 1761 } 1762 1763 static bool should_output_stats(const struct verif_stats *stats) 1764 { 1765 struct filter *f; 1766 int i, allow_cnt = 0; 1767 1768 for (i = 0; i < env.deny_filter_cnt; i++) { 1769 f = &env.deny_filters[i]; 1770 if (f->kind != FILTER_STAT) 1771 continue; 1772 1773 if (is_stat_filter_matched(f, stats)) 1774 return false; 1775 } 1776 1777 for (i = 0; i < env.allow_filter_cnt; i++) { 1778 f = &env.allow_filters[i]; 1779 if (f->kind != FILTER_STAT) 1780 continue; 1781 allow_cnt++; 1782 1783 if (is_stat_filter_matched(f, stats)) 1784 return true; 1785 } 1786 1787 /* if there are no stat allowed filters, pass everything through */ 1788 return allow_cnt == 0; 1789 } 1790 1791 static void output_prog_stats(void) 1792 { 1793 const struct verif_stats *stats; 1794 int i, last_stat_idx = 0; 1795 1796 if (env.out_fmt == RESFMT_TABLE) { 1797 /* calculate column widths */ 1798 output_headers(RESFMT_TABLE_CALCLEN); 1799 for (i = 0; i < env.prog_stat_cnt; i++) { 1800 stats = &env.prog_stats[i]; 1801 if (!should_output_stats(stats)) 1802 continue; 1803 output_stats(stats, RESFMT_TABLE_CALCLEN, false); 1804 last_stat_idx = i; 1805 } 1806 } 1807 1808 /* actually output the table */ 1809 output_headers(env.out_fmt); 1810 for (i = 0; i < env.prog_stat_cnt; i++) { 1811 stats = &env.prog_stats[i]; 1812 if (!should_output_stats(stats)) 1813 continue; 1814 output_stats(stats, env.out_fmt, i == last_stat_idx); 1815 } 1816 } 1817 1818 static int handle_verif_mode(void) 1819 { 1820 int i, err; 1821 1822 if (env.filename_cnt == 0) { 1823 fprintf(stderr, "Please provide path to BPF object file!\n\n"); 1824 argp_help(&argp, stderr, ARGP_HELP_USAGE, "veristat"); 1825 return -EINVAL; 1826 } 1827 1828 for (i = 0; i < env.filename_cnt; i++) { 1829 err = process_obj(env.filenames[i]); 1830 if (err) { 1831 fprintf(stderr, "Failed to process '%s': %d\n", env.filenames[i], err); 1832 return err; 1833 } 1834 } 1835 1836 qsort(env.prog_stats, env.prog_stat_cnt, sizeof(*env.prog_stats), cmp_prog_stats); 1837 1838 output_prog_stats(); 1839 1840 return 0; 1841 } 1842 1843 static int handle_replay_mode(void) 1844 { 1845 struct stat_specs specs = {}; 1846 int err; 1847 1848 if (env.filename_cnt != 1) { 1849 fprintf(stderr, "Replay mode expects exactly one input CSV file!\n\n"); 1850 argp_help(&argp, stderr, ARGP_HELP_USAGE, "veristat"); 1851 return -EINVAL; 1852 } 1853 1854 err = parse_stats_csv(env.filenames[0], &specs, 1855 &env.prog_stats, &env.prog_stat_cnt); 1856 if (err) { 1857 fprintf(stderr, "Failed to parse stats from '%s': %d\n", env.filenames[0], err); 1858 return err; 1859 } 1860 1861 qsort(env.prog_stats, env.prog_stat_cnt, sizeof(*env.prog_stats), cmp_prog_stats); 1862 1863 output_prog_stats(); 1864 1865 return 0; 1866 } 1867 1868 int main(int argc, char **argv) 1869 { 1870 int err = 0, i; 1871 1872 if (argp_parse(&argp, argc, argv, 0, NULL, NULL)) 1873 return 1; 1874 1875 if (env.verbose && env.quiet) { 1876 fprintf(stderr, "Verbose and quiet modes are incompatible, please specify just one or neither!\n\n"); 1877 argp_help(&argp, stderr, ARGP_HELP_USAGE, "veristat"); 1878 return 1; 1879 } 1880 if (env.verbose && env.log_level == 0) 1881 env.log_level = 1; 1882 1883 if (env.output_spec.spec_cnt == 0) { 1884 if (env.out_fmt == RESFMT_CSV) 1885 env.output_spec = default_csv_output_spec; 1886 else 1887 env.output_spec = default_output_spec; 1888 } 1889 if (env.sort_spec.spec_cnt == 0) 1890 env.sort_spec = default_sort_spec; 1891 1892 if (env.comparison_mode && env.replay_mode) { 1893 fprintf(stderr, "Can't specify replay and comparison mode at the same time!\n\n"); 1894 argp_help(&argp, stderr, ARGP_HELP_USAGE, "veristat"); 1895 return 1; 1896 } 1897 1898 if (env.comparison_mode) 1899 err = handle_comparison_mode(); 1900 else if (env.replay_mode) 1901 err = handle_replay_mode(); 1902 else 1903 err = handle_verif_mode(); 1904 1905 free_verif_stats(env.prog_stats, env.prog_stat_cnt); 1906 free_verif_stats(env.baseline_stats, env.baseline_stat_cnt); 1907 free(env.join_stats); 1908 for (i = 0; i < env.filename_cnt; i++) 1909 free(env.filenames[i]); 1910 free(env.filenames); 1911 for (i = 0; i < env.allow_filter_cnt; i++) { 1912 free(env.allow_filters[i].any_glob); 1913 free(env.allow_filters[i].file_glob); 1914 free(env.allow_filters[i].prog_glob); 1915 } 1916 free(env.allow_filters); 1917 for (i = 0; i < env.deny_filter_cnt; i++) { 1918 free(env.deny_filters[i].any_glob); 1919 free(env.deny_filters[i].file_glob); 1920 free(env.deny_filters[i].prog_glob); 1921 } 1922 free(env.deny_filters); 1923 return -err; 1924 } 1925