#include "common.h" #include #include #include #include #include #include #include // ── Helper to copy a file ──────────────────────────────────────────────── static int copy_file(const char *src, const char *dst) { int src_fd = open(src, O_RDONLY); if (src_fd < 0) return -1; int dst_fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (dst_fd < 0) { close(src_fd); return -1; } char buf[65536]; ssize_t n; while ((n = read(src_fd, buf, sizeof(buf))) > 0) { if (writen(dst_fd, buf, n) != n) { close(src_fd); close(dst_fd); return -1; } } close(src_fd); close(dst_fd); return 0; } // ── Simple fast hash for checksum-based skip ────────────────────────────── static uint64_t fast_hash_file(const char *path, uint64_t file_size) { // For small files (< 4KB), hash entire file // For large files, hash first 4KB + last 4KB + size FILE *f = fopen(path, "rb"); if (!f) return 0; uint64_t hash = file_size * 0x9e3779b97f4a7c15ULL; // Hash first chunk unsigned char buf[4096]; size_t n = fread(buf, 1, sizeof(buf), f); for (size_t i = 0; i < n; i++) { hash = hash * 0x880355f21e15d8c5ULL + buf[i]; } // For files > 8KB, also hash last chunk if (file_size > 8192) { if (fseeko(f, -(off_t)sizeof(buf), SEEK_END) == 0) { n = fread(buf, 1, sizeof(buf), f); for (size_t i = 0; i < n; i++) { hash = hash * 0x880355f21e15d8c5ULL + buf[i]; } } } fclose(f); return hash; } // ─── Per-connection context ──────────────────────────────────────────────── typedef struct { int tcp_fd; int use_udp; int udp_fd; struct sockaddr_in udp_server_addr; uint64_t udp_session_id; } sync_ctx_t; // ─── Global stats (atomic for lock-free updates) ────────────────────────── static atomic_uint_fast64_t g_bytes_sent = 0; static atomic_uint_fast32_t g_files_sent = 0; // ─── UDP session setup (called once per worker if -u) ───────────────────── static int setup_udp(sync_ctx_t *ctx, const char *host) { uint32_t req = MAGIC_UDP_REQ; if (writen(ctx->tcp_fd, &req, sizeof(req)) != sizeof(req)) return -1; uint32_t resp = 0; if (readn(ctx->tcp_fd, &resp, sizeof(resp)) != sizeof(resp)) return -1; if (resp != RESP_USE_UDP) { fprintf(stderr, "[thread] Server refused UDP, using TCP.\n"); ctx->use_udp = 0; return 0; } udp_handshake_t hs; if (readn(ctx->tcp_fd, &hs, sizeof(hs)) != sizeof(hs)) return -1; ctx->udp_session_id = hs.session_id; ctx->udp_fd = socket(AF_INET, SOCK_DGRAM, 0); if (ctx->udp_fd < 0) { perror("UDP socket"); return -1; } // Tune UDP socket for better throughput int bufsize = 4 * 1024 * 1024; // 4 MB setsockopt(ctx->udp_fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)); setsockopt(ctx->udp_fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)); memset(&ctx->udp_server_addr, 0, sizeof(ctx->udp_server_addr)); ctx->udp_server_addr.sin_family = AF_INET; ctx->udp_server_addr.sin_port = htons(hs.udp_port); inet_pton(AF_INET, host, &ctx->udp_server_addr.sin_addr); // Knock udp_packet_header_t knock; memset(&knock, 0, sizeof(knock)); knock.magic = MAGIC_UDP_KNOCK; knock.session_id = hs.session_id; sendto(ctx->udp_fd, &knock, sizeof(knock), 0, (struct sockaddr *)&ctx->udp_server_addr, sizeof(ctx->udp_server_addr)); uint32_t ack = 0; if (readn(ctx->tcp_fd, &ack, sizeof(ack)) != sizeof(ack) || ack != RESP_OK) { fprintf(stderr, "[thread] UDP knock failed\n"); return -1; } printf("[thread] UDP session ready (port %u)\n", hs.udp_port); return 0; } // ─── Batch file sending ──────────────────────────────────────────────────── static int send_batch_files(sync_ctx_t *ctx, file_task_t **tasks, int count) { // Collect and stat all files first (single pass to avoid double stat) // Also pre-open file descriptors for pipelining int *fds = malloc(count * sizeof(int)); struct stat *stats = malloc(count * sizeof(struct stat)); if (!fds || !stats) { perror("malloc"); free(fds); free(stats); return -1; } batch_meta_header_t batch_hdr; batch_hdr.magic = MAGIC_BATCH_META; batch_hdr.count = count; batch_hdr.total_name_len = 0; batch_hdr.total_size = 0; for (int i = 0; i < count; i++) { if (stat(tasks[i]->full_path, &stats[i]) < 0) { perror("stat"); for (int j = 0; j < i; j++) close(fds[j]); free(fds); free(stats); return -1; } batch_hdr.total_name_len += strlen(tasks[i]->rel_path); batch_hdr.total_size += stats[i].st_size; // Pre-open file descriptors fds[i] = open(tasks[i]->full_path, O_RDONLY); if (fds[i] < 0) { perror("open"); for (int j = 0; j < i; j++) close(fds[j]); free(fds); free(stats); return -1; } } // Send batch header if (writen(ctx->tcp_fd, &batch_hdr, sizeof(batch_hdr)) != sizeof(batch_hdr)) { for (int j = 0; j < count; j++) close(fds[j]); free(fds); free(stats); return -1; } // Send files with pipelining: meta+name+data for each file sequentially // This allows server to start writing file N while receiving meta for file N+1 for (int i = 0; i < count; i++) { file_meta_t meta; meta.magic = 0; meta.name_len = strlen(tasks[i]->rel_path); meta.file_size = stats[i].st_size; meta.mode = stats[i].st_mode; meta.checksum = tasks[i]->checksum; if (writen(ctx->tcp_fd, &meta, sizeof(meta)) != sizeof(meta)) { for (int j = 0; j < count; j++) close(fds[j]); free(fds); free(stats); return -1; } if (writen(ctx->tcp_fd, tasks[i]->rel_path, meta.name_len) != (ssize_t)meta.name_len) { for (int j = 0; j < count; j++) close(fds[j]); free(fds); free(stats); return -1; } // Send file data using sendfile() for zero-copy transfer off_t file_offset = 0; ssize_t total_sent = 0; while (file_offset < (off_t)stats[i].st_size) { ssize_t sent = sendfile(ctx->tcp_fd, fds[i], &file_offset, stats[i].st_size - file_offset); if (sent < 0) { if (errno == EINTR) continue; perror("sendfile"); for (int j = 0; j < count; j++) close(fds[j]); free(fds); free(stats); return -1; } if (sent == 0) break; // EOF total_sent += sent; } close(fds[i]); atomic_fetch_add(&g_bytes_sent, (uint64_t)total_sent); printf("[thread] Sent (pipelined+sendfile): %s\n", tasks[i]->rel_path); } free(fds); free(stats); // Update file count atomically atomic_fetch_add(&g_files_sent, count); return 0; } // ─── Worker thread: owns one TCP connection, drains work queue ───────────── static void *worker_thread(void *arg) { worker_arg_t *wa = (worker_arg_t *)arg; // Connect TCP sync_ctx_t ctx; memset(&ctx, 0, sizeof(ctx)); ctx.use_udp = wa->use_udp; ctx.tcp_fd = socket(AF_INET, SOCK_STREAM, 0); if (ctx.tcp_fd < 0) { perror("socket"); wa->result = -1; return NULL; } // TCP tuning for better throughput int optval = 1; setsockopt(ctx.tcp_fd, SOL_TCP, TCP_NODELAY, &optval, sizeof(optval)); setsockopt(ctx.tcp_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); // Large send/receive buffers int bufsize = 4 * 1024 * 1024; // 4 MB setsockopt(ctx.tcp_fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)); setsockopt(ctx.tcp_fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)); struct sockaddr_in sa; memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; sa.sin_port = htons(wa->port); inet_pton(AF_INET, wa->host, &sa.sin_addr); if (connect(ctx.tcp_fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { perror("connect"); close(ctx.tcp_fd); wa->result = -1; return NULL; } printf("[thread %d] TCP connected to %s:%d\n", wa->thread_id, wa->host, wa->port); if (wa->use_udp && setup_udp(&ctx, wa->host) < 0) { close(ctx.tcp_fd); wa->result = -1; return NULL; } // Drain queue with batching file_task_t *batch[BATCH_MAX_FILES]; int batch_count = 0; file_task_t *task; while ((task = wq_pop(wa->queue)) != NULL) { batch[batch_count++] = task; // If batch is full, send it if (batch_count >= BATCH_MAX_FILES) { if (send_batch_files(&ctx, batch, batch_count) < 0) { fprintf(stderr, "[thread %d] Batch send failed\n", wa->thread_id); wa->result = -1; } // Free all tasks in batch for (int i = 0; i < batch_count; i++) { free(batch[i]); } batch_count = 0; } } // Send remaining files in partial batch if (batch_count > 0) { if (send_batch_files(&ctx, batch, batch_count) < 0) { fprintf(stderr, "[thread %d] Final batch send failed\n", wa->thread_id); wa->result = -1; } for (int i = 0; i < batch_count; i++) { free(batch[i]); } } // Signal done to server file_meta_t done; memset(&done, 0, sizeof(done)); done.magic = MAGIC_DONE; writen(ctx.tcp_fd, &done, sizeof(done)); if (ctx.use_udp && ctx.udp_fd > 0) close(ctx.udp_fd); close(ctx.tcp_fd); printf("[thread %d] Done.\n", wa->thread_id); return NULL; } // ─── Scanner: recursively walks dir, collects files, sorts by size, pushes to queue ── typedef struct { work_queue_t *queue; char base_path[1024]; } scanner_arg_t; // Comparison function for sorting files by size (ascending: small files first) static int compare_files_by_size(const void *a, const void *b) { const file_task_t *fa = *(const file_task_t **)a; const file_task_t *fb = *(const file_task_t **)b; if (fa->file_size < fb->file_size) return -1; if (fa->file_size > fb->file_size) return 1; return 0; } // Collect all files recursively into a list static int collect_files(const char *base, const char *sub, file_task_t **list, int *count, int max_files) { char cur[2048]; if (sub && sub[0]) snprintf(cur, sizeof(cur), "%s/%s", base, sub); else snprintf(cur, sizeof(cur), "%s", base); DIR *dir = opendir(cur); if (!dir) { perror("opendir"); return -1; } struct dirent *e; while ((e = readdir(dir)) != NULL) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; char rel[1024], full[2048]; if (sub && sub[0]) { snprintf(rel, sizeof(rel), "%s/%s", sub, e->d_name); } else { snprintf(rel, sizeof(rel), "%s", e->d_name); } snprintf(full, sizeof(full), "%s/%s", base, rel); full[sizeof(full) - 1] = '\0'; rel[sizeof(rel) - 1] = '\0'; struct stat st; if (lstat(full, &st) < 0) { perror("lstat"); continue; } if (S_ISDIR(st.st_mode)) { if (collect_files(base, rel, list, count, max_files) < 0) { closedir(dir); return -1; } } else if (S_ISREG(st.st_mode)) { if (*count >= max_files) { fprintf(stderr, "Warning: Maximum file count reached, stopping scan\n"); closedir(dir); return 0; } list[*count] = malloc(sizeof(file_task_t)); if (!list[*count]) { perror("malloc"); closedir(dir); return -1; } snprintf(list[*count]->full_path, sizeof(list[*count]->full_path), "%s", full); snprintf(list[*count]->rel_path, sizeof(list[*count]->rel_path), "%s", rel); list[*count]->file_size = st.st_size; list[*count]->checksum = fast_hash_file(full, st.st_size); list[*count]->next = NULL; (*count)++; } } closedir(dir); return 0; } static void *scanner_thread(void *arg) { scanner_arg_t *sa = (scanner_arg_t *)arg; // First pass: count files to determine array size int file_count = 0; char cur[2048]; DIR *dir = opendir(sa->base_path); if (!dir) { perror("opendir"); wq_finish(sa->queue); return NULL; } struct dirent *e; while ((e = readdir(dir)) != NULL) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; snprintf(cur, sizeof(cur), "%s/%s", sa->base_path, e->d_name); struct stat st; if (lstat(cur, &st) < 0) continue; if (S_ISDIR(st.st_mode)) { // Recursively count DIR *subdir = opendir(cur); if (subdir) { struct dirent *se; while ((se = readdir(subdir)) != NULL) { if (!strcmp(se->d_name, ".") || !strcmp(se->d_name, "..")) continue; file_count++; } closedir(subdir); } } else if (S_ISREG(st.st_mode)) { file_count++; } } closedir(dir); // Allocate array for file tasks file_task_t **file_list = malloc(file_count * sizeof(file_task_t *)); if (!file_list) { perror("malloc"); wq_finish(sa->queue); return NULL; } int count = 0; if (collect_files(sa->base_path, "", file_list, &count, file_count) < 0) { for (int i = 0; i < count; i++) free(file_list[i]); free(file_list); wq_finish(sa->queue); return NULL; } // Sort files by size (smallest first for better pipelining) qsort(file_list, count, sizeof(file_task_t *), compare_files_by_size); // Push sorted files to queue for (int i = 0; i < count; i++) { wq_push(sa->queue, file_list[i]->full_path, file_list[i]->rel_path, file_list[i]->file_size, file_list[i]->checksum); free(file_list[i]); } free(file_list); wq_finish(sa->queue); /* broadcast to all waiting workers */ return NULL; } // ─── main ────────────────────────────────────────────────────────────────── static void print_usage(const char *prog) { fprintf(stderr, "Usage: %s -h [-p ] -s [-n ] [-u]\n" " -n number of parallel TCP connections (default: 4)\n" " -u enable UDP data channel\n", prog); } int main(int argc, char *argv[]) { char *host = NULL; int port = DEFAULT_PORT; char *source = NULL; int nconn = 4; int use_udp = 0; int opt; while ((opt = getopt(argc, argv, "h:p:s:n:u")) != -1) { switch (opt) { case 'h': host = optarg; break; case 'p': port = atoi(optarg); break; case 's': source = optarg; break; case 'n': nconn = atoi(optarg); break; case 'u': use_udp = 1; break; default: print_usage(argv[0]); exit(EXIT_FAILURE); } } if (!host || !source) { print_usage(argv[0]); exit(EXIT_FAILURE); } if (nconn < 1) nconn = 1; if (nconn > 16) nconn = 16; struct stat st; if (stat(source, &st) < 0) { perror("stat source"); exit(EXIT_FAILURE); } printf("Starting sync: %s → %s:%d (connections=%d, udp=%s)\n", source, host, port, nconn, use_udp ? "yes" : "no"); // ── Create temp dir for single files (to use directory scanning) ───────── char temp_dir[2048] = ""; int use_temp_dir = 0; char *final_source = (char *)source; if (S_ISREG(st.st_mode)) { // Create temp dir, copy file there with its name strncpy(temp_dir, "/tmp/fastsync_temp_XXXXXX", sizeof(temp_dir)); if (mkdtemp(temp_dir) == NULL) { perror("mkdtemp"); exit(EXIT_FAILURE); } use_temp_dir = 1; char *fname = strrchr(source, '/'); char new_path[2048]; snprintf(new_path, sizeof(new_path), "%s/%s", temp_dir, fname ? fname + 1 : source); // Copy file to temp dir if (copy_file(source, new_path) < 0) { perror("copy_file"); rmdir(temp_dir); exit(EXIT_FAILURE); } final_source = temp_dir; } // ── Init shared work queue ──────────────────────────────────────────── work_queue_t queue; wq_init(&queue); // ── Start scanner thread ────────────────────────────────────────────── scanner_arg_t sarg; sarg.queue = &queue; snprintf(sarg.base_path, sizeof(sarg.base_path), "%s", final_source); pthread_t stid; pthread_create(&stid, NULL, scanner_thread, &sarg); pthread_detach(stid); // ── Spawn N worker threads ──────────────────────────────────────────── pthread_t *tids = malloc(nconn * sizeof(pthread_t)); worker_arg_t *args = malloc(nconn * sizeof(worker_arg_t)); if (!tids || !args) { perror("malloc"); exit(EXIT_FAILURE); } // ── Start timer ─────────────────────────────────────────────────────── struct timespec t_start, t_end; clock_gettime(CLOCK_MONOTONIC, &t_start); for (int i = 0; i < nconn; i++) { args[i].thread_id = i; args[i].queue = &queue; args[i].host = host; args[i].port = port; args[i].use_udp = use_udp; args[i].result = 0; pthread_create(&tids[i], NULL, worker_thread, &args[i]); } // ── Wait for all workers ────────────────────────────────────────────── int overall = 0; for (int i = 0; i < nconn; i++) { pthread_join(tids[i], NULL); if (args[i].result != 0) overall = -1; } // ── Stop timer & print summary ──────────────────────────────────────── clock_gettime(CLOCK_MONOTONIC, &t_end); double elapsed_ms = (t_end.tv_sec - t_start.tv_sec) * 1000.0 + (t_end.tv_nsec - t_start.tv_nsec) / 1e6; uint64_t total_bytes = atomic_load(&g_bytes_sent); uint32_t total_files = atomic_load(&g_files_sent); double throughput = (total_bytes / 1048576.0) / (elapsed_ms / 1000.0); printf("\n=== Sync complete: %u files, %lu bytes, %.1f ms, %.2f MB/s ===\n", total_files, (unsigned long)total_bytes, elapsed_ms, throughput); /* Machine-readable line for benchmark.sh to parse */ printf("BENCH: connections=%d bytes=%lu ms=%.1f throughput_mbs=%.2f\n", nconn, (unsigned long)total_bytes, elapsed_ms, throughput); wq_destroy(&queue); free(tids); free(args); // Clean up temp directory for single file if (use_temp_dir) { // Remove the temp directory and its contents char cmd[2048]; snprintf(cmd, sizeof(cmd), "rm -rf %s", temp_dir); system(cmd); } return overall == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }