From c9a4b7f85d4c7a479c81eb1bb80a988001bb76ec Mon Sep 17 00:00:00 2001 From: andreastaliad Date: Tue, 26 May 2026 17:18:48 +0300 Subject: [PATCH] added chunked Pi calculations test for fork vs thread --- .gitignore | 1 + README.md | 1 + pi_digits_benchmark/README.md | 33 + pi_digits_benchmark/pi_digits_benchmark | Bin 0 -> 31016 bytes pi_digits_benchmark/pi_digits_benchmark.c | 765 ++++++++++++++++++ .../run_pi_digits_benchmark.sh | 65 ++ run_all_experiments.sh | 29 + 7 files changed, 894 insertions(+) create mode 100644 pi_digits_benchmark/README.md create mode 100755 pi_digits_benchmark/pi_digits_benchmark create mode 100644 pi_digits_benchmark/pi_digits_benchmark.c create mode 100755 pi_digits_benchmark/run_pi_digits_benchmark.sh diff --git a/.gitignore b/.gitignore index cffc250..858440c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ creation_time_experiment/creation_time_results.txt thread_recursive_scaling/runs/ +pi_digits_benchmark/runs/ diff --git a/README.md b/README.md index cc9514a..9dbf5f3 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Run individual experiments (examples): ```sh bash creation_time_experiment/bash_test_iter.sh bash thread_recursive_scaling/run_thread_recursive.sh +bash pi_digits_benchmark/run_pi_digits_benchmark.sh ``` Enable the dangerous stress test (timeboxed): diff --git a/pi_digits_benchmark/README.md b/pi_digits_benchmark/README.md new file mode 100644 index 0000000..55f8ea1 --- /dev/null +++ b/pi_digits_benchmark/README.md @@ -0,0 +1,33 @@ +# Pi Digits Benchmark + +This benchmark compares pthread-based parallelism and fork()-based parallelism +for computing digits of pi in independent chunks. + +## Configuration (environment variables) + +- `THREAD_COUNT` : number of worker threads +- `PROCESS_COUNT` : number of worker processes +- `PI_DIGITS` : total digits of pi to compute +- `CHUNK_SIZE` : digits per worker + +Defaults: `THREAD_COUNT=4`, `PROCESS_COUNT=4`, `PI_DIGITS=2000`, `CHUNK_SIZE=500` + +## Run + +```sh +# Run both modes sequentially +bash run_pi_digits_benchmark.sh + +# Run a single mode +bash run_pi_digits_benchmark.sh thread +bash run_pi_digits_benchmark.sh process +``` + +## Outputs + +Per-run outputs are stored under `runs/`: + +- `pi_digits.txt` — final concatenated digits +- `workers.csv` — per-worker durations and chunk metadata +- `results.csv` — per-run summary metrics (includes aggregated RSS/VSZ and snapshot overhead) +- `stdout.log` / `stderr.log` — benchmark logs diff --git a/pi_digits_benchmark/pi_digits_benchmark b/pi_digits_benchmark/pi_digits_benchmark new file mode 100755 index 0000000000000000000000000000000000000000..d41562f46a2748b3ea1417a2fae571b10630d292 GIT binary patch literal 31016 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef enum { + MODE_THREAD = 0, + MODE_PROCESS = 1 +} run_mode_t; + +typedef struct { + long thread_count; + long process_count; + long pi_digits; + long chunk_size; +} bench_config_t; + +typedef struct { + int worker_id; + long chunk_start; + long chunk_count; + double start_time; + double end_time; + int exit_code; + char output_path[PATH_MAX]; + char stats_path[PATH_MAX]; + pid_t pid; +} worker_info_t; + +typedef struct { + double wall_s; + double cpu_user_s; + double cpu_sys_s; + double cpu_total_s; + double cpu_pct; + long max_rss_kb; + long max_vsz_kb; + long agg_rss_kb; + long agg_vsz_kb; + double mem_snapshot_s; + size_t output_bytes; + int exit_code; +} run_metrics_t; + +static double now_wall(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec + ts.tv_nsec / 1e9; +} + +static void iso_timestamp(char *out, size_t size) { + time_t now = time(NULL); + struct tm tm_now; + localtime_r(&now, &tm_now); + strftime(out, size, "%Y-%m-%dT%H:%M:%S%z", &tm_now); +} + +static void log_event(const char *fmt, ...) { + char ts[64]; + iso_timestamp(ts, sizeof(ts)); + fprintf(stdout, "[%s] ", ts); + + va_list args; + va_start(args, fmt); + vfprintf(stdout, fmt, args); + va_end(args); + + fputc('\n', stdout); + fflush(stdout); +} + +static long read_env_long(const char *name, long def_value) { + const char *val = getenv(name); + if (!val || !*val) { + return def_value; + } + char *end = NULL; + long v = strtol(val, &end, 10); + if (end == val || v <= 0) { + return def_value; + } + return v; +} + +static int ensure_dir(const char *path) { + if (mkdir(path, 0755) == 0) { + return 0; + } + if (errno == EEXIST) { + return 0; + } + return -1; +} + +static int ensure_dir_recursive(const char *path) { + char tmp[PATH_MAX]; + size_t len = strlen(path); + if (len == 0 || len >= sizeof(tmp)) { + return -1; + } + strncpy(tmp, path, sizeof(tmp)); + tmp[sizeof(tmp) - 1] = '\0'; + if (tmp[len - 1] == '/') { + tmp[len - 1] = '\0'; + } + + for (char *p = tmp + 1; *p; ++p) { + if (*p == '/') { + *p = '\0'; + if (ensure_dir(tmp) != 0) { + return -1; + } + *p = '/'; + } + } + return ensure_dir(tmp); +} + +static int remove_dir_tree(const char *path) { + DIR *dir = opendir(path); + if (!dir) { + return 0; + } + struct dirent *ent; + while ((ent = readdir(dir)) != NULL) { + if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) { + continue; + } + char child[PATH_MAX]; + snprintf(child, sizeof(child), "%s/%s", path, ent->d_name); + unlink(child); + } + closedir(dir); + rmdir(path); + return 0; +} + +static void reap_zombies(void) { + int status = 0; + while (waitpid(-1, &status, WNOHANG) > 0) { + } +} + +static void kill_orphan_pids(const char *pid_file) { + FILE *f = fopen(pid_file, "r"); + if (!f) { + return; + } + pid_t pid = 0; + while (fscanf(f, "%d", &pid) == 1) { + if (pid > 1) { + kill(pid, SIGTERM); + } + } + fclose(f); + usleep(200000); + f = fopen(pid_file, "r"); + if (!f) { + return; + } + while (fscanf(f, "%d", &pid) == 1) { + if (pid > 1) { + kill(pid, SIGKILL); + } + } + fclose(f); +} + +static void cleanup_outputs(const char *out_dir, const char *tmp_dir, const char *pid_file, const char *final_path, const char *workers_csv, const char *results_csv) { + (void)out_dir; + log_event("Cleanup start"); + kill_orphan_pids(pid_file); + reap_zombies(); + remove_dir_tree(tmp_dir); + unlink(final_path); + unlink(workers_csv); + unlink(results_csv); + unlink(pid_file); + log_event("Cleanup complete"); +} + +typedef struct { + long start; + long end; + long index; + FILE *out; + char buffer[4096]; + size_t buf_len; +} emit_ctx_t; + +static int emit_digit(emit_ctx_t *ctx, int digit) { + ctx->index++; + if (ctx->index == 0) { + return 0; + } + long pos = ctx->index - 1; + if (pos < ctx->start) { + return 0; + } + if (pos >= ctx->end) { + return 1; + } + if (digit < 0 || digit > 9) { + return 1; + } + ctx->buffer[ctx->buf_len++] = (char)('0' + digit); + if (ctx->buf_len == sizeof(ctx->buffer)) { + fwrite(ctx->buffer, 1, ctx->buf_len, ctx->out); + ctx->buf_len = 0; + } + return 0; +} + +static int flush_emit(emit_ctx_t *ctx) { + if (ctx->buf_len > 0) { + if (fwrite(ctx->buffer, 1, ctx->buf_len, ctx->out) != ctx->buf_len) { + return -1; + } + } + return 0; +} + +static int write_pi_chunk(FILE *out, long start, long count) { + if (count <= 0) { + return 0; + } + + long end = start + count; + if (end <= 0) { + return 0; + } + + long total = end; + long len_calc = (total * 10) / 3 + 1; + if (len_calc <= 0) { + return -1; + } + + size_t len = (size_t)len_calc; + int *a = calloc(len, sizeof(int)); + if (!a) { + return -1; + } + for (size_t i = 0; i < len; ++i) { + a[i] = 2; + } + + int nines = 0; + int predigit = 0; + emit_ctx_t ctx = {start, end, -1, out, {0}, 0}; + + for (long i = 0; i < total; ++i) { + int q = 0; + for (long j = (long)len - 1; j >= 0; --j) { + int x = 10 * a[j] + q * (int)(j + 1); + a[j] = x % (int)(2 * j + 1); + q = x / (int)(2 * j + 1); + } + a[0] = q % 10; + q = q / 10; + + if (q == 9) { + nines++; + continue; + } + if (q == 10) { + if (emit_digit(&ctx, predigit + 1)) { + break; + } + for (int k = 0; k < nines; ++k) { + if (emit_digit(&ctx, 0)) { + break; + } + } + predigit = 0; + nines = 0; + } else { + if (emit_digit(&ctx, predigit)) { + break; + } + for (int k = 0; k < nines; ++k) { + if (emit_digit(&ctx, 9)) { + break; + } + } + predigit = q; + nines = 0; + } + + if (ctx.index - 1 >= end) { + break; + } + } + + if (ctx.index - 1 < end) { + emit_digit(&ctx, predigit); + } + + int rc = flush_emit(&ctx); + free(a); + return rc; +} + +static int compute_chunk_to_file(const worker_info_t *info) { + FILE *out = fopen(info->output_path, "w"); + if (!out) { + return -1; + } + int rc = write_pi_chunk(out, info->chunk_start, info->chunk_count); + fclose(out); + return rc; +} + +static int read_self_status_kb(long *rss_kb, long *vsz_kb) { + *rss_kb = 0; + *vsz_kb = 0; + FILE *f = fopen("/proc/self/status", "r"); + if (!f) { + return -1; + } + char line[256]; + while (fgets(line, sizeof(line), f)) { + if (sscanf(line, "VmRSS: %ld", rss_kb) == 1) { + continue; + } + if (sscanf(line, "VmSize: %ld", vsz_kb) == 1) { + continue; + } + } + fclose(f); + return 0; +} + +static int write_worker_stats(const char *path) { + long rss_kb = 0; + long vsz_kb = 0; + if (read_self_status_kb(&rss_kb, &vsz_kb) != 0) { + return -1; + } + FILE *f = fopen(path, "w"); + if (!f) { + return -1; + } + fprintf(f, "%ld,%ld\n", rss_kb, vsz_kb); + fclose(f); + return 0; +} + +static void *thread_worker(void *arg) { + worker_info_t *info = (worker_info_t *)arg; + int rc = compute_chunk_to_file(info); + info->exit_code = rc == 0 ? 0 : 1; + return NULL; +} + +static int run_thread_mode(const bench_config_t *cfg, const char *tmp_dir, const char *workers_csv, worker_info_t *workers, run_metrics_t *metrics) { + pthread_t *threads = calloc((size_t)cfg->thread_count, sizeof(pthread_t)); + if (!threads) { + return 1; + } + bool *created = calloc((size_t)cfg->thread_count, sizeof(bool)); + if (!created) { + free(threads); + return 1; + } + + log_event("Threaded benchmark start (workers=%ld)", cfg->thread_count); + + for (long i = 0; i < cfg->thread_count; ++i) { + workers[i].worker_id = (int)i; + workers[i].chunk_start = i * cfg->chunk_size; + workers[i].chunk_count = cfg->chunk_size; + if (workers[i].chunk_start >= cfg->pi_digits) { + workers[i].chunk_count = 0; + } else if (workers[i].chunk_start + workers[i].chunk_count > cfg->pi_digits) { + workers[i].chunk_count = cfg->pi_digits - workers[i].chunk_start; + } + snprintf(workers[i].output_path, sizeof(workers[i].output_path), "%s/worker_%06ld.txt", tmp_dir, i); + workers[i].stats_path[0] = '\0'; + + log_event("Worker start (thread id=%ld chunk_start=%ld chunk_size=%ld)", i, workers[i].chunk_start, workers[i].chunk_count); + workers[i].start_time = now_wall(); + int rc = pthread_create(&threads[i], NULL, thread_worker, &workers[i]); + if (rc != 0) { + errno = rc; + perror("pthread_create"); + workers[i].exit_code = 1; + created[i] = false; + } else { + created[i] = true; + } + } + + for (long i = 0; i < cfg->thread_count; ++i) { + if (created[i]) { + pthread_join(threads[i], NULL); + } + workers[i].end_time = now_wall(); + log_event("Worker complete (thread id=%ld duration_s=%.6f exit_code=%d)", i, workers[i].end_time - workers[i].start_time, workers[i].exit_code); + } + + FILE *wcsv = fopen(workers_csv, "w"); + if (wcsv) { + fprintf(wcsv, "worker_id,mode,chunk_start,chunk_size,duration_s,exit_code\n"); + for (long i = 0; i < cfg->thread_count; ++i) { + fprintf(wcsv, "%ld,thread,%ld,%ld,%.6f,%d\n", + i, workers[i].chunk_start, workers[i].chunk_count, + workers[i].end_time - workers[i].start_time, workers[i].exit_code); + } + fclose(wcsv); + } + + free(created); + free(threads); + metrics->exit_code = 0; + log_event("Threaded benchmark complete"); + return 0; +} + +static int run_process_mode(const bench_config_t *cfg, const char *tmp_dir, const char *workers_csv, const char *pid_file, worker_info_t *workers, run_metrics_t *metrics) { + log_event("Process benchmark start (workers=%ld)", cfg->process_count); + + FILE *pf = fopen(pid_file, "w"); + if (!pf) { + perror("fopen pid_file"); + return 1; + } + + for (long i = 0; i < cfg->process_count; ++i) { + workers[i].worker_id = (int)i; + workers[i].chunk_start = i * cfg->chunk_size; + workers[i].chunk_count = cfg->chunk_size; + if (workers[i].chunk_start >= cfg->pi_digits) { + workers[i].chunk_count = 0; + } else if (workers[i].chunk_start + workers[i].chunk_count > cfg->pi_digits) { + workers[i].chunk_count = cfg->pi_digits - workers[i].chunk_start; + } + snprintf(workers[i].output_path, sizeof(workers[i].output_path), "%s/worker_%06ld.txt", tmp_dir, i); + snprintf(workers[i].stats_path, sizeof(workers[i].stats_path), "%s/worker_%06ld.stat", tmp_dir, i); + + log_event("Worker start (process id=%ld chunk_start=%ld chunk_size=%ld)", i, workers[i].chunk_start, workers[i].chunk_count); + workers[i].start_time = now_wall(); + pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + workers[i].exit_code = 1; + continue; + } + if (pid == 0) { + int rc = compute_chunk_to_file(&workers[i]); + if (rc == 0) { + if (write_worker_stats(workers[i].stats_path) != 0) { + rc = 1; + } + } + _exit(rc == 0 ? 0 : 1); + } + workers[i].pid = pid; + fprintf(pf, "%d\n", pid); + fflush(pf); + } + fclose(pf); + + for (long i = 0; i < cfg->process_count; ++i) { + if (workers[i].pid <= 0) { + continue; + } + int status = 0; + pid_t waited = waitpid(workers[i].pid, &status, 0); + workers[i].end_time = now_wall(); + if (waited < 0) { + perror("waitpid"); + workers[i].exit_code = 1; + } else if (WIFEXITED(status)) { + workers[i].exit_code = WEXITSTATUS(status); + } else { + workers[i].exit_code = 1; + } + log_event("Worker complete (process id=%ld duration_s=%.6f exit_code=%d)", i, workers[i].end_time - workers[i].start_time, workers[i].exit_code); + } + + FILE *wcsv = fopen(workers_csv, "w"); + if (wcsv) { + fprintf(wcsv, "worker_id,mode,chunk_start,chunk_size,duration_s,exit_code\n"); + for (long i = 0; i < cfg->process_count; ++i) { + fprintf(wcsv, "%ld,process,%ld,%ld,%.6f,%d\n", + i, workers[i].chunk_start, workers[i].chunk_count, + workers[i].end_time - workers[i].start_time, workers[i].exit_code); + } + fclose(wcsv); + } + + metrics->exit_code = 0; + log_event("Process benchmark complete"); + return 0; +} + +static int concatenate_outputs(const bench_config_t *cfg, const char *tmp_dir, const char *final_path, long worker_count) { + log_event("Aggregation start"); + + FILE *out = fopen(final_path, "w"); + if (!out) { + perror("fopen final output"); + return 1; + } + + char buf[4096]; + for (long i = 0; i < worker_count; ++i) { + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%s/worker_%06ld.txt", tmp_dir, i); + FILE *in = fopen(path, "r"); + if (!in) { + continue; + } + size_t n = 0; + while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { + fwrite(buf, 1, n, out); + } + fclose(in); + } + + fclose(out); + log_event("Aggregation complete"); + return 0; +} + +static size_t file_size(const char *path) { + struct stat st; + if (stat(path, &st) != 0) { + return 0; + } + return (size_t)st.st_size; +} + +static void collect_metrics(run_metrics_t *metrics, double start_wall, double end_wall) { + struct rusage self_ru; + struct rusage child_ru; + memset(&self_ru, 0, sizeof(self_ru)); + memset(&child_ru, 0, sizeof(child_ru)); + getrusage(RUSAGE_SELF, &self_ru); + getrusage(RUSAGE_CHILDREN, &child_ru); + + double self_user = self_ru.ru_utime.tv_sec + self_ru.ru_utime.tv_usec / 1e6; + double self_sys = self_ru.ru_stime.tv_sec + self_ru.ru_stime.tv_usec / 1e6; + double child_user = child_ru.ru_utime.tv_sec + child_ru.ru_utime.tv_usec / 1e6; + double child_sys = child_ru.ru_stime.tv_sec + child_ru.ru_stime.tv_usec / 1e6; + + metrics->wall_s = end_wall - start_wall; + metrics->cpu_user_s = self_user + child_user; + metrics->cpu_sys_s = self_sys + child_sys; + metrics->cpu_total_s = metrics->cpu_user_s + metrics->cpu_sys_s; + metrics->cpu_pct = metrics->wall_s > 0.0 ? (100.0 * metrics->cpu_total_s / metrics->wall_s) : 0.0; + metrics->max_rss_kb = self_ru.ru_maxrss; + + metrics->max_vsz_kb = 0; + read_self_status_kb(&metrics->max_rss_kb, &metrics->max_vsz_kb); +} + +static int read_worker_stats(const char *path, long *rss_kb, long *vsz_kb) { + *rss_kb = 0; + *vsz_kb = 0; + FILE *f = fopen(path, "r"); + if (!f) { + return -1; + } + if (fscanf(f, "%ld,%ld", rss_kb, vsz_kb) != 2) { + fclose(f); + return -1; + } + fclose(f); + return 0; +} + +static void aggregate_worker_memory(const bench_config_t *cfg, run_mode_t mode, const worker_info_t *workers, run_metrics_t *metrics) { + double t0 = now_wall(); + long total_rss = 0; + long total_vsz = 0; + + long self_rss = 0; + long self_vsz = 0; + read_self_status_kb(&self_rss, &self_vsz); + total_rss += self_rss; + total_vsz += self_vsz; + + if (mode == MODE_PROCESS) { + for (long i = 0; i < cfg->process_count; ++i) { + if (workers[i].stats_path[0] == '\0') { + continue; + } + long rss_kb = 0; + long vsz_kb = 0; + if (read_worker_stats(workers[i].stats_path, &rss_kb, &vsz_kb) == 0) { + total_rss += rss_kb; + total_vsz += vsz_kb; + } + } + } + + metrics->agg_rss_kb = total_rss; + metrics->agg_vsz_kb = total_vsz; + metrics->mem_snapshot_s = now_wall() - t0; +} + +static void write_results_csv(const char *results_csv, const char *run_id, const char *mode, const bench_config_t *cfg, const run_metrics_t *metrics) { + FILE *csv = fopen(results_csv, "w"); + if (!csv) { + perror("fopen results_csv"); + return; + } + char ts[64]; + iso_timestamp(ts, sizeof(ts)); + char host[128] = "unknown"; + char kernel[128] = "unknown"; + gethostname(host, sizeof(host) - 1); + struct utsname uts; + if (uname(&uts) == 0) { + strncpy(kernel, uts.release, sizeof(kernel) - 1); + kernel[sizeof(kernel) - 1] = '\0'; + } + + fprintf(csv, "run_id,timestamp,host,kernel,mode,worker_count,pi_digits,chunk_size,exit_code,wall_s,cpu_user_s,cpu_sys_s,cpu_pct,max_rss_kb,max_vsz_kb,agg_rss_kb,agg_vsz_kb,mem_snapshot_s,final_output_bytes\n"); + long workers = (strcmp(mode, "thread") == 0) ? cfg->thread_count : cfg->process_count; + fprintf(csv, "%s,%s,%s,%s,%s,%ld,%ld,%ld,%d,%.6f,%.6f,%.6f,%.2f,%ld,%ld,%ld,%ld,%.6f,%zu\n", + run_id, ts, host, kernel, mode, workers, cfg->pi_digits, cfg->chunk_size, + metrics->exit_code, metrics->wall_s, metrics->cpu_user_s, metrics->cpu_sys_s, + metrics->cpu_pct, metrics->max_rss_kb, metrics->max_vsz_kb, + metrics->agg_rss_kb, metrics->agg_vsz_kb, metrics->mem_snapshot_s, metrics->output_bytes); + fclose(csv); +} + +static void usage(const char *prog) { + fprintf(stderr, "Usage: %s --mode thread|process [--out-dir PATH]\n", prog); +} + +int main(int argc, char **argv) { + run_mode_t mode = MODE_THREAD; + const char *mode_name = "thread"; + const char *out_dir = "./pi_digits_run"; + + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--mode") == 0 && i + 1 < argc) { + mode_name = argv[++i]; + } else if (strncmp(argv[i], "--mode=", 7) == 0) { + mode_name = argv[i] + 7; + } else if (strcmp(argv[i], "--out-dir") == 0 && i + 1 < argc) { + out_dir = argv[++i]; + } else if (strncmp(argv[i], "--out-dir=", 10) == 0) { + out_dir = argv[i] + 10; + } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + usage(argv[0]); + return 1; + } else { + usage(argv[0]); + return 1; + } + } + + if (strcmp(mode_name, "thread") == 0) { + mode = MODE_THREAD; + } else if (strcmp(mode_name, "process") == 0) { + mode = MODE_PROCESS; + } else { + usage(argv[0]); + return 1; + } + + bench_config_t cfg; + cfg.thread_count = read_env_long("THREAD_COUNT", 4); + cfg.process_count = read_env_long("PROCESS_COUNT", 4); + cfg.pi_digits = read_env_long("PI_DIGITS", 2000); + cfg.chunk_size = read_env_long("CHUNK_SIZE", 500); + + if (cfg.chunk_size <= 0) { + cfg.chunk_size = cfg.pi_digits; + } + + log_event("Benchmark start (mode=%s)", mode_name); + + if (ensure_dir_recursive(out_dir) != 0) { + perror("mkdir out_dir"); + return 1; + } + + char tmp_dir[PATH_MAX]; + char final_path[PATH_MAX]; + char workers_csv[PATH_MAX]; + char results_csv[PATH_MAX]; + char pid_file[PATH_MAX]; + char run_id[64]; + + snprintf(run_id, sizeof(run_id), "%ld", (long)time(NULL)); + snprintf(tmp_dir, sizeof(tmp_dir), "%s/worker_tmp", out_dir); + snprintf(final_path, sizeof(final_path), "%s/pi_digits.txt", out_dir); + snprintf(workers_csv, sizeof(workers_csv), "%s/workers.csv", out_dir); + snprintf(results_csv, sizeof(results_csv), "%s/results.csv", out_dir); + snprintf(pid_file, sizeof(pid_file), "%s/workers.pids", out_dir); + + cleanup_outputs(out_dir, tmp_dir, pid_file, final_path, workers_csv, results_csv); + + if (ensure_dir_recursive(tmp_dir) != 0) { + perror("mkdir tmp_dir"); + return 1; + } + + long worker_count = (mode == MODE_THREAD) ? cfg.thread_count : cfg.process_count; + worker_info_t *workers = calloc((size_t)worker_count, sizeof(worker_info_t)); + if (!workers) { + perror("calloc workers"); + return 1; + } + + run_metrics_t metrics; + memset(&metrics, 0, sizeof(metrics)); + + double t0 = now_wall(); + int rc = 0; + if (mode == MODE_THREAD) { + rc = run_thread_mode(&cfg, tmp_dir, workers_csv, workers, &metrics); + } else { + rc = run_process_mode(&cfg, tmp_dir, workers_csv, pid_file, workers, &metrics); + } + + if (rc != 0) { + metrics.exit_code = 1; + } + + int agg_rc = concatenate_outputs(&cfg, tmp_dir, final_path, worker_count); + if (agg_rc != 0) { + metrics.exit_code = 1; + } + + aggregate_worker_memory(&cfg, mode, workers, &metrics); + + remove_dir_tree(tmp_dir); + unlink(pid_file); + + double t1 = now_wall(); + collect_metrics(&metrics, t0, t1); + metrics.output_bytes = file_size(final_path); + + write_results_csv(results_csv, run_id, mode_name, &cfg, &metrics); + + log_event("Benchmark end (mode=%s wall_s=%.6f cpu_pct=%.2f output_bytes=%zu)", + mode_name, metrics.wall_s, metrics.cpu_pct, metrics.output_bytes); + + free(workers); + return metrics.exit_code; +} diff --git a/pi_digits_benchmark/run_pi_digits_benchmark.sh b/pi_digits_benchmark/run_pi_digits_benchmark.sh new file mode 100755 index 0000000..1c47527 --- /dev/null +++ b/pi_digits_benchmark/run_pi_digits_benchmark.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail +LC_ALL=C + +DIR=$(cd "$(dirname "$0")" && pwd) +cd "$DIR" + +gcc -O2 -pthread -o pi_digits_benchmark pi_digits_benchmark.c + +echo "Compiled pi_digits_benchmark" + +THREAD_COUNT=${THREAD_COUNT:-4} +PROCESS_COUNT=${PROCESS_COUNT:-4} +PI_DIGITS=${PI_DIGITS:-2000} +CHUNK_SIZE=${CHUNK_SIZE:-500} + +RUNS_DIR="$DIR/runs" +mkdir -p "$RUNS_DIR" +RUN_ID=$(date +"%Y%m%d_%H%M%S") + +RESULTS_CSV="$DIR/results.csv" +if [ ! -f "$RESULTS_CSV" ]; then + echo "run_id,timestamp,host,kernel,mode,worker_count,pi_digits,chunk_size,exit_code,wall_s,cpu_user_s,cpu_sys_s,cpu_pct,max_rss_kb,max_vsz_kb,agg_rss_kb,agg_vsz_kb,mem_snapshot_s,final_output_bytes" > "$RESULTS_CSV" +fi + +run_mode() { + local mode="$1" + local out_dir="$RUNS_DIR/${RUN_ID}_${mode}" + local stdout_log="$out_dir/stdout.log" + local stderr_log="$out_dir/stderr.log" + + mkdir -p "$out_dir" + + echo "Running mode=$mode out_dir=$out_dir" + THREAD_COUNT="$THREAD_COUNT" PROCESS_COUNT="$PROCESS_COUNT" PI_DIGITS="$PI_DIGITS" CHUNK_SIZE="$CHUNK_SIZE" \ + ./pi_digits_benchmark --mode "$mode" --out-dir "$out_dir" >"$stdout_log" 2>"$stderr_log" + + if [ -f "$out_dir/results.csv" ]; then + tail -n 1 "$out_dir/results.csv" >> "$RESULTS_CSV" + fi + + echo " stdout: $stdout_log" + echo " stderr: $stderr_log" + echo " results: $out_dir/results.csv" + echo " workers: $out_dir/workers.csv" + echo " pi output: $out_dir/pi_digits.txt" +} + +MODE=${1:-both} +case "$MODE" in + thread) + run_mode thread + ;; + process) + run_mode process + ;; + both) + run_mode thread + run_mode process + ;; + *) + echo "Usage: $0 [thread|process|both]" + exit 1 + ;; +esac diff --git a/run_all_experiments.sh b/run_all_experiments.sh index c7d3350..e030d4f 100644 --- a/run_all_experiments.sh +++ b/run_all_experiments.sh @@ -156,9 +156,16 @@ compile_thread_recursive() { "$ROOT_DIR/thread_recursive_scaling/thread_recursive.c" -lm } +compile_pi_digits_benchmark() { + log "Compiling pi_digits_benchmark" + gcc -O2 -pthread -o "$ROOT_DIR/pi_digits_benchmark/pi_digits_benchmark" \ + "$ROOT_DIR/pi_digits_benchmark/pi_digits_benchmark.c" +} + compile_creation_time compile_fork_bomb compile_thread_recursive +compile_pi_digits_benchmark # Defaults (override by env vars) ALLOW_DANGEROUS=${ALLOW_DANGEROUS:-0} @@ -166,6 +173,10 @@ FORK_BOMB_SECONDS=${FORK_BOMB_SECONDS:-5} CREATION_TIME_ITERS=${CREATION_TIME_ITERS:-"100 1000 10000"} CREATION_TIME_SECONDS=${CREATION_TIME_SECONDS:-60} PROC_SAMPLE_STEP=${PROC_SAMPLE_STEP:-50} +THREAD_COUNT=${THREAD_COUNT:-4} +PROCESS_COUNT=${PROCESS_COUNT:-4} +PI_DIGITS=${PI_DIGITS:-2000} +CHUNK_SIZE=${CHUNK_SIZE:-500} CREATION_TIME_STDOUT="$OUT_DIR/creation_time_stdout.log" CREATION_TIME_STDERR="$OUT_DIR/creation_time_stderr.log" @@ -200,5 +211,23 @@ run_with_sampling \ "$OUT_DIR/thread_recursive_scaling.csv" \ 0 +PI_BENCH_OUT_BASE="$OUT_DIR/pi_digits_benchmark" +PI_THREAD_STDOUT="$OUT_DIR/pi_digits_thread_stdout.log" +PI_THREAD_STDERR="$OUT_DIR/pi_digits_thread_stderr.log" +PI_PROC_STDOUT="$OUT_DIR/pi_digits_process_stdout.log" +PI_PROC_STDERR="$OUT_DIR/pi_digits_process_stderr.log" + +run_with_sampling \ + "pi_digits_benchmark_thread" \ + "cd '$ROOT_DIR/pi_digits_benchmark' && THREAD_COUNT='$THREAD_COUNT' PROCESS_COUNT='$PROCESS_COUNT' PI_DIGITS='$PI_DIGITS' CHUNK_SIZE='$CHUNK_SIZE' ./pi_digits_benchmark --mode thread --out-dir '$PI_BENCH_OUT_BASE/thread' >'$PI_THREAD_STDOUT' 2>'$PI_THREAD_STDERR'" \ + "$OUT_DIR/pi_digits_thread.csv" \ + 0 + +run_with_sampling \ + "pi_digits_benchmark_process" \ + "cd '$ROOT_DIR/pi_digits_benchmark' && THREAD_COUNT='$THREAD_COUNT' PROCESS_COUNT='$PROCESS_COUNT' PI_DIGITS='$PI_DIGITS' CHUNK_SIZE='$CHUNK_SIZE' ./pi_digits_benchmark --mode process --out-dir '$PI_BENCH_OUT_BASE/process' >'$PI_PROC_STDOUT' 2>'$PI_PROC_STDERR'" \ + "$OUT_DIR/pi_digits_process.csv" \ + 0 + log "All experiments finished" log "Run directory: $OUT_DIR"