1 /*- 2 * BSD LICENSE 3 * 4 * Copyright (c) Intel Corporation. 5 * All rights reserved. 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * * Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * * Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in 15 * the documentation and/or other materials provided with the 16 * distribution. 17 * * Neither the name of Intel Corporation nor the names of its 18 * contributors may be used to endorse or promote products derived 19 * from this software without specific prior written permission. 20 * 21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 */ 33 34 #include "spdk/stdinc.h" 35 36 #include "spdk/env.h" 37 #include "spdk/string.h" 38 #include "spdk/trace.h" 39 #include "spdk/util.h" 40 #include "spdk/barrier.h" 41 42 #define TRACE_FILE_COPY_SIZE (32 * 1024) 43 #define TRACE_PATH_MAX 2048 44 45 static char *g_exe_name; 46 static int g_verbose = 1; 47 static uint64_t g_tsc_rate; 48 static uint64_t g_utsc_rate; 49 static bool g_shutdown = false; 50 static uint64_t g_histories_size; 51 52 struct lcore_trace_record_ctx { 53 char lcore_file[TRACE_PATH_MAX]; 54 int fd; 55 struct spdk_trace_history *in_history; 56 struct spdk_trace_history *out_history; 57 58 /* Recorded next entry index in record */ 59 uint64_t rec_next_entry; 60 61 /* Record tsc for report */ 62 uint64_t first_entry_tsc; 63 uint64_t last_entry_tsc; 64 65 /* Total number of entries in lcore trace file */ 66 uint64_t num_entries; 67 }; 68 69 struct aggr_trace_record_ctx { 70 const char *out_file; 71 int out_fd; 72 int shm_fd; 73 struct lcore_trace_record_ctx lcore_ports[SPDK_TRACE_MAX_LCORE]; 74 struct spdk_trace_histories *trace_histories; 75 }; 76 77 static int 78 input_trace_file_mmap(struct aggr_trace_record_ctx *ctx, const char *shm_name) 79 { 80 void *history_ptr; 81 int i; 82 83 ctx->shm_fd = shm_open(shm_name, O_RDONLY, 0); 84 if (ctx->shm_fd < 0) { 85 fprintf(stderr, "Could not open %s.\n", shm_name); 86 return -1; 87 } 88 89 /* Map the header of trace file */ 90 history_ptr = mmap(NULL, sizeof(struct spdk_trace_histories), PROT_READ, MAP_SHARED, ctx->shm_fd, 91 0); 92 if (history_ptr == MAP_FAILED) { 93 fprintf(stderr, "Could not mmap shm %s.\n", shm_name); 94 close(ctx->shm_fd); 95 return -1; 96 } 97 98 ctx->trace_histories = (struct spdk_trace_histories *)history_ptr; 99 100 g_tsc_rate = ctx->trace_histories->flags.tsc_rate; 101 g_utsc_rate = g_tsc_rate / 1000; 102 if (g_tsc_rate == 0) { 103 fprintf(stderr, "Invalid tsc_rate %ju\n", g_tsc_rate); 104 munmap(history_ptr, sizeof(struct spdk_trace_histories)); 105 close(ctx->shm_fd); 106 return -1; 107 } 108 109 if (g_verbose) { 110 printf("TSC Rate: %ju\n", g_tsc_rate); 111 } 112 113 /* Remap the entire trace file */ 114 g_histories_size = spdk_get_trace_histories_size(ctx->trace_histories); 115 munmap(history_ptr, sizeof(struct spdk_trace_histories)); 116 history_ptr = mmap(NULL, g_histories_size, PROT_READ, MAP_SHARED, ctx->shm_fd, 0); 117 if (history_ptr == MAP_FAILED) { 118 fprintf(stderr, "Could not remmap shm %s.\n", shm_name); 119 close(ctx->shm_fd); 120 return -1; 121 } 122 123 ctx->trace_histories = (struct spdk_trace_histories *)history_ptr; 124 for (i = 0; i < SPDK_TRACE_MAX_LCORE; i++) { 125 ctx->lcore_ports[i].in_history = spdk_get_per_lcore_history(ctx->trace_histories, i); 126 127 if (g_verbose) { 128 printf("Number of trace entries for lcore (%d): %ju\n", i, 129 ctx->lcore_ports[i].in_history->num_entries); 130 } 131 } 132 133 return 0; 134 } 135 136 static int 137 output_trace_files_prepare(struct aggr_trace_record_ctx *ctx, const char *aggr_path) 138 { 139 int flags = O_CREAT | O_EXCL | O_RDWR; 140 struct lcore_trace_record_ctx *port_ctx; 141 int name_len; 142 int i, rc; 143 144 /* Assign file names for related trace files */ 145 ctx->out_file = aggr_path; 146 for (i = 0; i < SPDK_TRACE_MAX_LCORE; i++) { 147 port_ctx = &ctx->lcore_ports[i]; 148 149 /* Get the length of trace file name for each lcore with format "%s-%d" */ 150 name_len = snprintf(port_ctx->lcore_file, TRACE_PATH_MAX, "%s-%d", ctx->out_file, i); 151 if (name_len >= TRACE_PATH_MAX) { 152 fprintf(stderr, "Length of file path (%s) exceeds limitation for lcore file.\n", 153 aggr_path); 154 goto err; 155 } 156 } 157 158 /* If output trace file already exists, try to unlink it together with its temporary files */ 159 if (access(ctx->out_file, F_OK) == 0) { 160 rc = unlink(ctx->out_file); 161 if (rc) { 162 goto err; 163 } 164 165 for (i = 0; i < SPDK_TRACE_MAX_LCORE; i++) { 166 port_ctx = &ctx->lcore_ports[i]; 167 if (access(port_ctx->lcore_file, F_OK) == 0) { 168 rc = unlink(port_ctx->lcore_file); 169 if (rc) { 170 goto err; 171 } 172 } 173 } 174 175 } 176 177 for (i = 0; i < SPDK_TRACE_MAX_LCORE; i++) { 178 port_ctx = &ctx->lcore_ports[i]; 179 180 port_ctx->fd = open(port_ctx->lcore_file, flags, 0600); 181 if (port_ctx->fd < 0) { 182 fprintf(stderr, "Could not open lcore file %s.\n", port_ctx->lcore_file); 183 goto err; 184 } 185 186 if (g_verbose) { 187 printf("Create tmp lcore trace file %s for lcore %d\n", port_ctx->lcore_file, i); 188 } 189 190 port_ctx->out_history = calloc(1, sizeof(struct spdk_trace_history)); 191 if (port_ctx->out_history == NULL) { 192 fprintf(stderr, "Failed to allocate memory for out_history.\n"); 193 goto err; 194 } 195 } 196 197 return 0; 198 199 err: 200 for (i = 0; i < SPDK_TRACE_MAX_LCORE; i++) { 201 port_ctx = &ctx->lcore_ports[i]; 202 free(port_ctx->out_history); 203 204 if (port_ctx->fd > 0) { 205 close(port_ctx->fd); 206 } 207 } 208 209 return -1; 210 } 211 212 static void 213 output_trace_files_finish(struct aggr_trace_record_ctx *ctx) 214 { 215 struct lcore_trace_record_ctx *port_ctx; 216 int i; 217 218 for (i = 0; i < SPDK_TRACE_MAX_LCORE; i++) { 219 port_ctx = &ctx->lcore_ports[i]; 220 221 free(port_ctx->out_history); 222 close(port_ctx->fd); 223 unlink(port_ctx->lcore_file); 224 225 if (g_verbose) { 226 printf("Remove tmp lcore trace file %s for lcore %d\n", port_ctx->lcore_file, i); 227 } 228 } 229 } 230 231 static int 232 cont_write(int fildes, const void *buf, size_t nbyte) 233 { 234 int rc; 235 int _nbyte = nbyte; 236 237 while (_nbyte) { 238 rc = write(fildes, buf, _nbyte); 239 if (rc < 0) { 240 if (errno != EINTR) { 241 return -1; 242 } 243 244 continue; 245 } 246 247 _nbyte -= rc; 248 } 249 250 return nbyte; 251 } 252 253 static int 254 cont_read(int fildes, void *buf, size_t nbyte) 255 { 256 int rc; 257 int _nbyte = nbyte; 258 259 while (_nbyte) { 260 rc = read(fildes, buf, _nbyte); 261 if (rc == 0) { 262 return nbyte - _nbyte; 263 } else if (rc < 0) { 264 if (errno != EINTR) { 265 return -1; 266 } 267 268 continue; 269 } 270 271 _nbyte -= rc; 272 } 273 274 return nbyte; 275 } 276 277 static int 278 lcore_trace_last_entry_idx(struct spdk_trace_history *in_history, int cir_next_idx) 279 { 280 int last_idx; 281 282 if (cir_next_idx == 0) { 283 last_idx = in_history->num_entries - 1; 284 } else { 285 last_idx = cir_next_idx - 1; 286 } 287 288 return last_idx; 289 } 290 291 static int 292 circular_buffer_padding_backward(int fd, struct spdk_trace_history *in_history, 293 int cir_start, int cir_end) 294 { 295 int rc; 296 297 if (cir_end <= cir_start) { 298 fprintf(stderr, "Wrong using of circular_buffer_padding_back\n"); 299 return -1; 300 } 301 302 rc = cont_write(fd, &in_history->entries[cir_start], 303 sizeof(struct spdk_trace_entry) * (cir_end - cir_start)); 304 if (rc < 0) { 305 fprintf(stderr, "Failed to append entries into lcore file\n"); 306 return rc; 307 } 308 309 return 0; 310 } 311 312 static int 313 circular_buffer_padding_across(int fd, struct spdk_trace_history *in_history, 314 int cir_start, int cir_end) 315 { 316 int rc; 317 int num_entries = in_history->num_entries; 318 319 if (cir_end > cir_start) { 320 fprintf(stderr, "Wrong using of circular_buffer_padding_across\n"); 321 return -1; 322 } 323 324 rc = cont_write(fd, &in_history->entries[cir_start], 325 sizeof(struct spdk_trace_entry) * (num_entries - cir_start)); 326 if (rc < 0) { 327 fprintf(stderr, "Failed to append entries into lcore file backward\n"); 328 return rc; 329 } 330 331 if (cir_end == 0) { 332 return 0; 333 } 334 335 rc = cont_write(fd, &in_history->entries[0], sizeof(struct spdk_trace_entry) * cir_end); 336 if (rc < 0) { 337 fprintf(stderr, "Failed to append entries into lcore file forward\n"); 338 return rc; 339 } 340 341 return 0; 342 } 343 344 static int 345 circular_buffer_padding_all(int fd, struct spdk_trace_history *in_history, 346 int cir_end) 347 { 348 return circular_buffer_padding_across(fd, in_history, cir_end, cir_end); 349 } 350 351 static int 352 lcore_trace_record(struct lcore_trace_record_ctx *lcore_port) 353 { 354 struct spdk_trace_history *in_history = lcore_port->in_history; 355 uint64_t rec_next_entry = lcore_port->rec_next_entry; 356 uint64_t rec_num_entries = lcore_port->num_entries; 357 int fd = lcore_port->fd; 358 uint64_t shm_next_entry; 359 uint64_t num_cir_entries; 360 uint64_t shm_cir_next; 361 uint64_t rec_cir_next; 362 int rc; 363 int last_idx; 364 365 shm_next_entry = in_history->next_entry; 366 367 /* Ensure all entries of spdk_trace_history are latest to next_entry */ 368 spdk_smp_rmb(); 369 370 if (shm_next_entry == rec_next_entry) { 371 /* There is no update */ 372 return 0; 373 } else if (shm_next_entry < rec_next_entry) { 374 /* Error branch */ 375 fprintf(stderr, "Trace porting error in lcore %d, trace rollback occurs.\n", in_history->lcore); 376 fprintf(stderr, "shm_next_entry is %ju, record_next_entry is %ju.\n", shm_next_entry, 377 rec_next_entry); 378 return -1; 379 } 380 381 num_cir_entries = in_history->num_entries; 382 shm_cir_next = shm_next_entry & (num_cir_entries - 1); 383 384 /* Record first entry's tsc and corresponding entries when recording first time. */ 385 if (lcore_port->first_entry_tsc == 0) { 386 if (shm_next_entry < num_cir_entries) { 387 /* Updates haven't been across circular buffer yet. 388 * The first entry in shared memory is the eldest one. 389 */ 390 lcore_port->first_entry_tsc = in_history->entries[0].tsc; 391 392 lcore_port->num_entries += shm_cir_next; 393 rc = circular_buffer_padding_backward(fd, in_history, 0, shm_cir_next); 394 } else { 395 /* Updates have already been across circular buffer. 396 * The eldest entry in shared memory is pointed by shm_cir_next. 397 */ 398 lcore_port->first_entry_tsc = in_history->entries[shm_cir_next].tsc; 399 400 lcore_port->num_entries += num_cir_entries; 401 rc = circular_buffer_padding_all(fd, in_history, shm_cir_next); 402 } 403 404 goto out; 405 } 406 407 if (shm_next_entry - rec_next_entry > num_cir_entries) { 408 /* There must be missed updates */ 409 fprintf(stderr, "Trace-record missed %ju trace entries\n", 410 shm_next_entry - rec_next_entry - num_cir_entries); 411 412 lcore_port->num_entries += num_cir_entries; 413 rc = circular_buffer_padding_all(fd, in_history, shm_cir_next); 414 } else if (shm_next_entry - rec_next_entry == num_cir_entries) { 415 /* All circular buffer is updated */ 416 lcore_port->num_entries += num_cir_entries; 417 rc = circular_buffer_padding_all(fd, in_history, shm_cir_next); 418 } else { 419 /* Part of circular buffer is updated */ 420 rec_cir_next = rec_next_entry & (num_cir_entries - 1); 421 422 if (shm_cir_next > rec_cir_next) { 423 /* Updates are not across circular buffer */ 424 lcore_port->num_entries += shm_cir_next - rec_cir_next; 425 rc = circular_buffer_padding_backward(fd, in_history, rec_cir_next, shm_cir_next); 426 } else { 427 /* Updates are across circular buffer */ 428 lcore_port->num_entries += num_cir_entries - rec_cir_next + shm_cir_next; 429 rc = circular_buffer_padding_across(fd, in_history, rec_cir_next, shm_cir_next); 430 } 431 } 432 433 out: 434 if (rc) { 435 return rc; 436 } 437 438 if (g_verbose) { 439 printf("Append %ju trace_entry for lcore %d\n", lcore_port->num_entries - rec_num_entries, 440 in_history->lcore); 441 } 442 443 /* Update tpoint_count info */ 444 memcpy(lcore_port->out_history, lcore_port->in_history, sizeof(struct spdk_trace_history)); 445 446 /* Update last_entry_tsc to align with appended entries */ 447 last_idx = lcore_trace_last_entry_idx(in_history, shm_cir_next); 448 lcore_port->last_entry_tsc = in_history->entries[last_idx].tsc; 449 lcore_port->rec_next_entry = shm_next_entry; 450 451 return rc; 452 } 453 454 static int 455 trace_files_aggregate(struct aggr_trace_record_ctx *ctx) 456 { 457 int flags = O_CREAT | O_EXCL | O_RDWR; 458 struct lcore_trace_record_ctx *lcore_port; 459 char copy_buff[TRACE_FILE_COPY_SIZE]; 460 uint64_t lcore_offsets[SPDK_TRACE_MAX_LCORE + 1]; 461 int rc, i; 462 ssize_t len = 0; 463 uint64_t len_sum; 464 465 ctx->out_fd = open(ctx->out_file, flags, 0600); 466 if (ctx->out_fd < 0) { 467 fprintf(stderr, "Could not open aggregation file %s.\n", ctx->out_file); 468 return -1; 469 } 470 471 if (g_verbose) { 472 printf("Create trace file %s for output\n", ctx->out_file); 473 } 474 475 /* Write flags of histories into head of converged trace file, except num_entriess */ 476 rc = cont_write(ctx->out_fd, ctx->trace_histories, 477 sizeof(struct spdk_trace_histories) - sizeof(lcore_offsets)); 478 if (rc < 0) { 479 fprintf(stderr, "Failed to write trace header into trace file\n"); 480 goto out; 481 } 482 483 /* Update and append lcore offsets converged trace file */ 484 lcore_offsets[0] = sizeof(struct spdk_trace_flags); 485 for (i = 1; i < (int)SPDK_COUNTOF(lcore_offsets); i++) { 486 lcore_offsets[i] = spdk_get_trace_history_size(ctx->lcore_ports[i - 1].num_entries) + 487 lcore_offsets[i - 1]; 488 } 489 490 rc = cont_write(ctx->out_fd, lcore_offsets, sizeof(lcore_offsets)); 491 if (rc < 0) { 492 fprintf(stderr, "Failed to write lcore offsets into trace file\n"); 493 goto out; 494 } 495 496 /* Append each lcore trace file into converged trace file */ 497 for (i = 0; i < SPDK_TRACE_MAX_LCORE; i++) { 498 lcore_port = &ctx->lcore_ports[i]; 499 500 lcore_port->out_history->num_entries = lcore_port->num_entries; 501 rc = cont_write(ctx->out_fd, lcore_port->out_history, sizeof(struct spdk_trace_history)); 502 if (rc < 0) { 503 fprintf(stderr, "Failed to write lcore trace header into trace file\n"); 504 goto out; 505 } 506 507 /* Move file offset to the start of trace_entries */ 508 rc = lseek(lcore_port->fd, 0, SEEK_SET); 509 if (rc != 0) { 510 fprintf(stderr, "Failed to lseek lcore trace file\n"); 511 goto out; 512 } 513 514 len_sum = 0; 515 while ((len = cont_read(lcore_port->fd, copy_buff, TRACE_FILE_COPY_SIZE)) > 0) { 516 len_sum += len; 517 rc = cont_write(ctx->out_fd, copy_buff, len); 518 if (rc != len) { 519 fprintf(stderr, "Failed to write lcore trace entries into trace file\n"); 520 goto out; 521 } 522 } 523 524 if (len_sum != lcore_port->num_entries * sizeof(struct spdk_trace_entry)) { 525 fprintf(stderr, "Len of lcore trace file doesn't match number of entries for lcore\n"); 526 } 527 } 528 529 printf("All lcores trace entries are aggregated into trace file %s\n", ctx->out_file); 530 531 out: 532 close(ctx->out_fd); 533 534 return rc; 535 } 536 537 static void 538 __shutdown_signal(int signo) 539 { 540 g_shutdown = true; 541 } 542 543 static int 544 setup_exit_signal_handler(void) 545 { 546 struct sigaction sigact; 547 int rc; 548 549 memset(&sigact, 0, sizeof(sigact)); 550 sigemptyset(&sigact.sa_mask); 551 /* Install the same handler for SIGINT and SIGTERM */ 552 sigact.sa_handler = __shutdown_signal; 553 554 rc = sigaction(SIGINT, &sigact, NULL); 555 if (rc < 0) { 556 fprintf(stderr, "sigaction(SIGINT) failed\n"); 557 558 return rc; 559 } 560 561 rc = sigaction(SIGTERM, &sigact, NULL); 562 if (rc < 0) { 563 fprintf(stderr, "sigaction(SIGTERM) failed\n"); 564 } 565 566 return rc; 567 } 568 569 static void usage(void) 570 { 571 printf("\n%s is used to record all SPDK generated trace entries\n", g_exe_name); 572 printf("from SPDK trace shared-memory to specified file.\n\n"); 573 printf("usage:\n"); 574 printf(" %s <option>\n", g_exe_name); 575 printf(" option = '-q' to disable verbose mode\n"); 576 printf(" '-s' to specify spdk_trace shm name for a\n"); 577 printf(" currently running process\n"); 578 printf(" '-i' to specify the shared memory ID\n"); 579 printf(" '-p' to specify the trace PID\n"); 580 printf(" (one of -i or -p must be specified)\n"); 581 printf(" '-f' to specify output trace file name\n"); 582 printf(" '-h' to print usage information\n"); 583 } 584 585 int main(int argc, char **argv) 586 { 587 const char *app_name = NULL; 588 const char *file_name = NULL; 589 int op; 590 char shm_name[64]; 591 int shm_id = -1, shm_pid = -1; 592 int rc = 0; 593 int i; 594 struct aggr_trace_record_ctx ctx = {}; 595 struct lcore_trace_record_ctx *lcore_port; 596 597 g_exe_name = argv[0]; 598 while ((op = getopt(argc, argv, "f:i:p:qs:h")) != -1) { 599 switch (op) { 600 case 'i': 601 shm_id = spdk_strtol(optarg, 10); 602 break; 603 case 'p': 604 shm_pid = spdk_strtol(optarg, 10); 605 break; 606 case 'q': 607 g_verbose = 0; 608 break; 609 case 's': 610 app_name = optarg; 611 break; 612 case 'f': 613 file_name = optarg; 614 break; 615 case 'h': 616 default: 617 usage(); 618 exit(1); 619 } 620 } 621 622 if (file_name == NULL) { 623 fprintf(stderr, "-f must be specified\n"); 624 usage(); 625 exit(1); 626 } 627 628 if (app_name == NULL) { 629 fprintf(stderr, "-s must be specified\n"); 630 usage(); 631 exit(1); 632 } 633 634 if (shm_id == -1 && shm_pid == -1) { 635 fprintf(stderr, "-i or -p must be specified\n"); 636 usage(); 637 exit(1); 638 } 639 640 if (shm_id >= 0) { 641 snprintf(shm_name, sizeof(shm_name), "/%s_trace.%d", app_name, shm_id); 642 } else { 643 snprintf(shm_name, sizeof(shm_name), "/%s_trace.pid%d", app_name, shm_pid); 644 } 645 646 rc = setup_exit_signal_handler(); 647 if (rc) { 648 exit(1); 649 } 650 651 rc = input_trace_file_mmap(&ctx, shm_name); 652 if (rc) { 653 exit(1); 654 } 655 656 rc = output_trace_files_prepare(&ctx, file_name); 657 if (rc) { 658 exit(1); 659 } 660 661 printf("Start to poll trace shm file %s\n", shm_name); 662 while (!g_shutdown && rc == 0) { 663 for (i = 0; i < SPDK_TRACE_MAX_LCORE; i++) { 664 lcore_port = &ctx.lcore_ports[i]; 665 666 rc = lcore_trace_record(lcore_port); 667 if (rc) { 668 break; 669 } 670 } 671 } 672 673 if (rc) { 674 exit(1); 675 } 676 677 printf("Start to aggregate lcore trace files\n"); 678 rc = trace_files_aggregate(&ctx); 679 if (rc) { 680 exit(1); 681 } 682 683 /* Summary report */ 684 printf("TSC Rate: %ju\n", g_tsc_rate); 685 for (i = 0; i < SPDK_TRACE_MAX_LCORE; i++) { 686 lcore_port = &ctx.lcore_ports[i]; 687 688 if (lcore_port->num_entries == 0) { 689 continue; 690 } 691 692 printf("Port %ju trace entries for lcore (%d) in %ju usec\n", 693 lcore_port->num_entries, i, 694 (lcore_port->last_entry_tsc - lcore_port->first_entry_tsc) / g_utsc_rate); 695 696 } 697 698 munmap(ctx.trace_histories, g_histories_size); 699 close(ctx.shm_fd); 700 701 output_trace_files_finish(&ctx); 702 703 return 0; 704 } 705