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