/* * bench_v3.cpp — Benchmark tinygltf v3 parser: parse speed & memory. * * Measures: * - File read time * - JSON parse + model build time * - Peak arena memory usage * - Throughput (MB/s) * * Usage: * bench_v3 [--iterations N] [--warmup N] [--quiet] * bench_v3 --batch ... [--iterations N] */ #define TINYGLTF3_IMPLEMENTATION #define TINYGLTF3_ENABLE_FS #include "tiny_gltf_v3.h" #include #include #include #include #include #include #include #include #if defined(__linux__) #include #endif /* ------------------------------------------------------------------ */ /* Timing helpers */ /* ------------------------------------------------------------------ */ using Clock = std::chrono::high_resolution_clock; using TimePoint = Clock::time_point; static double elapsed_ms(TimePoint start, TimePoint end) { return std::chrono::duration(end - start).count(); } /* ------------------------------------------------------------------ */ /* Memory tracking allocator */ /* ------------------------------------------------------------------ */ struct MemTracker { size_t current; size_t peak; size_t total_allocs; size_t total_frees; }; static void *tracked_alloc(size_t size, void *ud) { MemTracker *mt = (MemTracker *)ud; void *ptr = malloc(size); if (ptr) { mt->current += size; if (mt->current > mt->peak) mt->peak = mt->current; mt->total_allocs++; } return ptr; } static void *tracked_realloc(void *ptr, size_t old_size, size_t new_size, void *ud) { MemTracker *mt = (MemTracker *)ud; void *new_ptr = realloc(ptr, new_size); if (new_ptr) { mt->current -= old_size; mt->current += new_size; if (mt->current > mt->peak) mt->peak = mt->current; } return new_ptr; } static void tracked_free(void *ptr, size_t size, void *ud) { MemTracker *mt = (MemTracker *)ud; if (ptr) { mt->current -= size; mt->total_frees++; free(ptr); } } /* ------------------------------------------------------------------ */ /* RSS measurement (Linux) */ /* ------------------------------------------------------------------ */ static size_t get_rss_bytes() { #if defined(__linux__) FILE *f = fopen("/proc/self/statm", "r"); if (!f) return 0; long pages = 0; if (fscanf(f, "%*s %ld", &pages) != 1) pages = 0; fclose(f); return (size_t)pages * 4096; #else return 0; #endif } /* ------------------------------------------------------------------ */ /* Benchmark result */ /* ------------------------------------------------------------------ */ struct BenchResult { std::string filename; uint64_t file_size; int iterations; /* Parse timing (ms) */ double parse_min; double parse_max; double parse_avg; double parse_median; /* Memory */ size_t arena_peak; /* Peak arena allocation */ size_t rss_before; size_t rss_after; /* Model stats */ uint32_t meshes; uint32_t nodes; uint32_t accessors; uint32_t materials; uint32_t animations; uint32_t buffers; uint32_t buffer_views; uint32_t images; uint32_t textures; /* Derived */ double throughput_mbs; /* MB/s based on median */ }; /* ------------------------------------------------------------------ */ /* Run benchmark for a single file */ /* ------------------------------------------------------------------ */ static BenchResult bench_file(const char *filename, int iterations, int warmup, bool quiet, int float32_mode = 0, int skip_extras_values = 0, int borrow_input_buffers = 0) { BenchResult r = {}; r.filename = filename; r.iterations = iterations; /* Read file into memory */ FILE *f = fopen(filename, "rb"); if (!f) { fprintf(stderr, "ERROR: Cannot open '%s'\n", filename); return r; } fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); if (sz <= 0) { fclose(f); return r; } std::vector data((size_t)sz); size_t rd = fread(data.data(), 1, (size_t)sz, f); fclose(f); if ((long)rd != sz) { return r; } r.file_size = (uint64_t)sz; /* Extract base dir */ std::string path(filename); std::string base_dir; size_t sep = path.find_last_of("/\\"); if (sep != std::string::npos) base_dir = path.substr(0, sep); /* Warmup iterations (not measured) */ for (int i = 0; i < warmup; ++i) { tg3_model model; tg3_error_stack errors; tg3_error_stack_init(&errors); tg3_parse_auto(&model, &errors, data.data(), data.size(), base_dir.c_str(), (uint32_t)base_dir.size(), NULL); tg3_model_free(&model); tg3_error_stack_free(&errors); } /* Benchmark iterations */ std::vector times; times.reserve(iterations); MemTracker tracker_best; memset(&tracker_best, 0, sizeof(tracker_best)); r.rss_before = get_rss_bytes(); for (int i = 0; i < iterations; ++i) { MemTracker tracker; memset(&tracker, 0, sizeof(tracker)); tg3_parse_options opts; tg3_parse_options_init(&opts); opts.memory.allocator.alloc = tracked_alloc; opts.memory.allocator.realloc = tracked_realloc; opts.memory.allocator.free = tracked_free; opts.memory.allocator.user_data = &tracker; opts.parse_float32 = float32_mode; opts.skip_extras_values = skip_extras_values; opts.borrow_input_buffers = borrow_input_buffers; tg3_model model; tg3_error_stack errors; tg3_error_stack_init(&errors); TimePoint t0 = Clock::now(); tg3_error_code err = tg3_parse_auto(&model, &errors, data.data(), data.size(), base_dir.c_str(), (uint32_t)base_dir.size(), &opts); TimePoint t1 = Clock::now(); double ms = elapsed_ms(t0, t1); times.push_back(ms); /* Capture model stats on first successful iteration */ if (i == 0 && err == TG3_OK) { r.meshes = model.meshes_count; r.nodes = model.nodes_count; r.accessors = model.accessors_count; r.materials = model.materials_count; r.animations = model.animations_count; r.buffers = model.buffers_count; r.buffer_views = model.buffer_views_count; r.images = model.images_count; r.textures = model.textures_count; } if (tracker.peak > tracker_best.peak) { tracker_best = tracker; } tg3_model_free(&model); tg3_error_stack_free(&errors); if (err != TG3_OK && !quiet) { fprintf(stderr, " Parse error on iteration %d: code %d\n", i, (int)err); } } r.rss_after = get_rss_bytes(); r.arena_peak = tracker_best.peak; /* Compute stats */ std::sort(times.begin(), times.end()); r.parse_min = times.front(); r.parse_max = times.back(); double sum = 0; for (double t : times) sum += t; r.parse_avg = sum / times.size(); r.parse_median = times[times.size() / 2]; /* Throughput: file_size / median_time */ if (r.parse_median > 0) { r.throughput_mbs = ((double)r.file_size / (1024.0 * 1024.0)) / (r.parse_median / 1000.0); } return r; } /* ------------------------------------------------------------------ */ /* Print results */ /* ------------------------------------------------------------------ */ static const char *human_bytes(size_t bytes, char *buf, size_t buf_sz) { if (bytes >= 1024ULL * 1024 * 1024) snprintf(buf, buf_sz, "%.2f GB", (double)bytes / (1024.0 * 1024 * 1024)); else if (bytes >= 1024 * 1024) snprintf(buf, buf_sz, "%.2f MB", (double)bytes / (1024.0 * 1024)); else if (bytes >= 1024) snprintf(buf, buf_sz, "%.2f KB", (double)bytes / 1024.0); else snprintf(buf, buf_sz, "%zu B", bytes); return buf; } static void print_result(const BenchResult &r) { char buf1[64], buf2[64]; printf("┌─────────────────────────────────────────────────────────────────┐\n"); printf("│ %-63s │\n", r.filename.c_str()); printf("├─────────────────────────────────────────────────────────────────┤\n"); printf("│ File size: %-47s │\n", human_bytes((size_t)r.file_size, buf1, sizeof(buf1))); printf("│ Iterations: %-47d │\n", r.iterations); printf("│ │\n"); printf("│ Parse time (ms): │\n"); printf("│ min: %10.3f │\n", r.parse_min); printf("│ max: %10.3f │\n", r.parse_max); printf("│ avg: %10.3f │\n", r.parse_avg); printf("│ median: %10.3f │\n", r.parse_median); printf("│ │\n"); printf("│ Throughput: %-47s │\n", (snprintf(buf1, sizeof(buf1), "%.2f MB/s", r.throughput_mbs), buf1)); printf("│ Arena peak: %-47s │\n", human_bytes(r.arena_peak, buf1, sizeof(buf1))); if (r.rss_before > 0) { printf("│ RSS before: %-47s │\n", human_bytes(r.rss_before, buf1, sizeof(buf1))); printf("│ RSS after: %-47s │\n", human_bytes(r.rss_after, buf2, sizeof(buf2))); } printf("│ │\n"); printf("│ Model: %u meshes, %u nodes, %u accessors, %u materials", r.meshes, r.nodes, r.accessors, r.materials); printf(" │\n"); printf("│ %u animations, %u buffers, %u images", r.animations, r.buffers, r.images); printf(" │\n"); printf("└─────────────────────────────────────────────────────────────────┘\n"); } static void print_csv_header() { printf("file,size_bytes,iterations,parse_min_ms,parse_max_ms,parse_avg_ms," "parse_median_ms,throughput_mbs,arena_peak_bytes," "meshes,nodes,accessors,materials,animations\n"); } static void print_csv_row(const BenchResult &r) { printf("%s,%lu,%d,%.3f,%.3f,%.3f,%.3f,%.2f,%zu,%u,%u,%u,%u,%u\n", r.filename.c_str(), (unsigned long)r.file_size, r.iterations, r.parse_min, r.parse_max, r.parse_avg, r.parse_median, r.throughput_mbs, r.arena_peak, r.meshes, r.nodes, r.accessors, r.materials, r.animations); } /* ------------------------------------------------------------------ */ /* Main */ /* ------------------------------------------------------------------ */ static void usage() { fprintf(stderr, "Usage:\n" " bench_v3 [--iterations N] [--warmup N] [--csv] [--quiet]\n" " bench_v3 --batch [file2] ... [--iterations N] [--csv]\n" "\n" "Options:\n" " --iterations N Number of timed parse iterations (default: 10)\n" " --warmup N Number of warmup iterations (default: 2)\n" " --csv Output in CSV format\n" " --quiet Suppress per-iteration error messages\n" " --batch Benchmark multiple files\n" " --float32 Parse JSON floats as float32 (faster, less precise)\n" " --skip-extras-values\n" " Skip materializing extras/unknown extension values\n" " --borrow-input-buffers\n" " Let GLB buffers reference caller-owned input bytes\n"); } int main(int argc, char **argv) { if (argc < 2) { usage(); return 1; } int iterations = 10; int warmup = 2; bool csv = false; bool quiet = false; int float32_mode = 0; int skip_extras_values = 0; int borrow_input_buffers = 0; std::vector files; for (int i = 1; i < argc; ++i) { if (strcmp(argv[i], "--iterations") == 0 && i + 1 < argc) { iterations = atoi(argv[++i]); } else if (strcmp(argv[i], "--warmup") == 0 && i + 1 < argc) { warmup = atoi(argv[++i]); } else if (strcmp(argv[i], "--csv") == 0) { csv = true; } else if (strcmp(argv[i], "--quiet") == 0) { quiet = true; } else if (strcmp(argv[i], "--float32") == 0) { float32_mode = 1; } else if (strcmp(argv[i], "--skip-extras-values") == 0) { skip_extras_values = 1; } else if (strcmp(argv[i], "--borrow-input-buffers") == 0) { borrow_input_buffers = 1; } else if (strcmp(argv[i], "--batch") == 0) { /* batch mode: just collect files */ } else if (argv[i][0] != '-') { files.push_back(argv[i]); } } if (files.empty()) { usage(); return 1; } if (csv) print_csv_header(); for (const auto &file : files) { if (!csv && !quiet) { printf("Benchmarking: %s (%d iterations, %d warmup%s)\n", file.c_str(), iterations, warmup, float32_mode ? ", float32" : skip_extras_values ? ", skip extras" : borrow_input_buffers ? ", borrow buffers" : ""); } BenchResult r = bench_file(file.c_str(), iterations, warmup, quiet, float32_mode, skip_extras_values, borrow_input_buffers); if (csv) { print_csv_row(r); } else { print_result(r); printf("\n"); } } return 0; }