/* StarForth — Steady-State Virtual Machine Runtime Copyright (c) 2023–2025 Robert A. James All rights reserved. This file is part of the StarForth project. Licensed under the StarForth License, Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://github.com/star.4th@proton.me/StarForth/LICENSE.txt This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. See the License for the specific language governing permissions and limitations under the License. StarForth — Steady-State Virtual Machine Runtime Copyright (c) 2023–2025 Robert A. James All rights reserved. This file is part of the StarForth project. Licensed under the StarForth License, Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://github.com/star.4th@proton.me/StarForth/LICENSE.txt This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. See the License for the specific language governing permissions and limitations under the License. */ /** * @file doe_metrics.c * @brief Design of Experiments metrics collection implementation */ #include "doe_metrics.h" #include "physics_hotwords_cache.h" #include "rolling_window_of_truth.h" #include "rolling_window_knobs.h" #include "inference_engine.h" #include "platform_time.h" #include #include #include #include #ifdef __unix__ #include #endif /* Forward declarations from physics system */ extern struct { uint64_t total_lookups; uint64_t cache_hits; uint64_t bucket_hits; } physics_global_stats; /** * Get current CPU temperature in Celsius */ int32_t metrics_get_cpu_temp_c(void) { #ifdef __unix__ FILE *f = fopen("/sys/class/thermal/thermal_zone0/temp", "r"); if (!f) return 0; int temp_millidegrees = 0; if (fscanf(f, "%d", &temp_millidegrees) != 1) { fclose(f); return 0; } fclose(f); return (int32_t)(temp_millidegrees / 1000); #else return 0; #endif } /** * Get current CPU frequency in MHz */ int32_t metrics_get_cpu_freq_mhz(void) { #ifdef __unix__ /* Try scaling_cur_freq first */ FILE *f = fopen("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq", "r"); if (f) { int freq_khz = 0; if (fscanf(f, "%d", &freq_khz) == 1) { fclose(f); return (int32_t)(freq_khz / 1000); } fclose(f); } /* Fallback to /proc/cpuinfo */ f = fopen("/proc/cpuinfo", "r"); if (f) { char line[256]; while (fgets(line, sizeof(line), f)) { if (strncmp(line, "cpu MHz", 7) == 0) { float mhz = 0.0f; if (sscanf(line, "cpu MHz : %f", &mhz) == 1) { fclose(f); return (int32_t)mhz; } } } fclose(f); } #endif return 0; } /** * Get current timestamp as ISO 8601 string */ void metrics_get_timestamp(char *buf, size_t bufsize) { if (bufsize < 32) return; time_t now = time(NULL); struct tm *tm_info = localtime(&now); strftime(buf, bufsize, "%Y-%m-%dT%H:%M:%S", tm_info); } /** * Extract metrics from VM hotwords cache stats */ static void extract_cache_metrics(const HotwordsCache *cache, DoeMetrics *metrics) { if (!cache) { metrics->cache_hits = 0; metrics->cache_hit_percent = 0.0; metrics->bucket_hits = 0; metrics->bucket_hit_percent = 0.0; metrics->cache_hit_latency_ns = 0; metrics->cache_hit_stddev_ns = 0; metrics->bucket_search_latency_ns = 0; metrics->bucket_search_stddev_ns = 0; return; } const HotwordsStats *stats = &cache->stats; /* Cache hits */ metrics->cache_hits = stats->cache_hits; if (stats->total_lookups > 0) { metrics->cache_hit_percent = 100.0 * (double)stats->cache_hits / (double)stats->total_lookups; } else { metrics->cache_hit_percent = 0.0; } /* Bucket hits */ metrics->bucket_hits = stats->bucket_hits; if (stats->total_lookups > 0) { metrics->bucket_hit_percent = 100.0 * (double)stats->bucket_hits / (double)stats->total_lookups; } else { metrics->bucket_hit_percent = 0.0; } /* Cache hit latency (convert from Q48.16 to ns) */ if (stats->cache_hit_samples > 0) { int64_t avg_q48 = stats->cache_hit_total_ns_q48 / (int64_t)stats->cache_hit_samples; metrics->cache_hit_latency_ns = avg_q48 >> 16; /* Convert from Q48.16 to nanoseconds */ /* StdDev calculation from variance sum */ if (stats->cache_hit_samples > 1) { /* Simplified: use variance sum for estimation */ int64_t variance_q48 = stats->cache_hit_variance_sum_q48 / (int64_t)stats->cache_hit_samples; metrics->cache_hit_stddev_ns = (int64_t)(variance_q48 >> 16); } else { metrics->cache_hit_stddev_ns = 0; } } else { metrics->cache_hit_latency_ns = 0; metrics->cache_hit_stddev_ns = 0; } /* Bucket search latency (convert from Q48.16 to ns) */ if (stats->bucket_search_samples > 0) { int64_t avg_q48 = stats->bucket_search_total_ns_q48 / (int64_t)stats->bucket_search_samples; metrics->bucket_search_latency_ns = avg_q48 >> 16; /* Convert from Q48.16 to nanoseconds */ /* StdDev calculation from variance sum */ if (stats->bucket_search_samples > 1) { int64_t variance_q48 = stats->bucket_search_variance_sum_q48 / (int64_t)stats->bucket_search_samples; metrics->bucket_search_stddev_ns = (int64_t)(variance_q48 >> 16); } else { metrics->bucket_search_stddev_ns = 0; } } else { metrics->bucket_search_latency_ns = 0; metrics->bucket_search_stddev_ns = 0; } } /** * Extract metrics from entire VM */ DoeMetrics metrics_from_vm(VM *vm, uint64_t workload_duration_ns, int32_t cpu_temp_delta_c, int32_t cpu_freq_delta_mhz) { DoeMetrics metrics = {0}; /* Lookups */ metrics.total_lookups = vm->hotwords_cache ? vm->hotwords_cache->stats.total_lookups : 0; /* Cache metrics */ if (ENABLE_HOTWORDS_CACHE && vm->hotwords_cache) { extract_cache_metrics(vm->hotwords_cache, &metrics); metrics.enable_hotwords_cache = vm->hotwords_cache->enabled ? 1 : 0; } else { metrics.enable_hotwords_cache = 0; } /* Pipelining metrics - extract from global pipeline metrics (Loop #4) */ metrics.context_predictions_total = vm->pipeline_metrics.prefetch_attempts; metrics.context_correct = vm->pipeline_metrics.prefetch_hits; metrics.context_accuracy_percent = 0.0; if (vm->pipeline_metrics.prefetch_attempts > 0) { metrics.context_accuracy_percent = 100.0 * (double)vm->pipeline_metrics.prefetch_hits / (double)vm->pipeline_metrics.prefetch_attempts; } /* === Rolling Window Metrics (Loop #2) === */ metrics.window_diversity_percent = 0.0; metrics.window_final_size_bytes = 4096; metrics.rolling_window_width = (uint32_t)vm->rolling_window.effective_window_size; metrics.actual_window_size = (uint32_t)(vm->rolling_window.total_executions < ROLLING_WINDOW_SIZE ? vm->rolling_window.total_executions : ROLLING_WINDOW_SIZE); metrics.total_executions = vm->rolling_window.total_executions; /* Protect access to last_inference_outputs against heartbeat thread race */ sf_mutex_lock(&vm->tuning_lock); metrics.window_variance_q48 = vm->last_inference_outputs ? vm->last_inference_outputs->window_variance_q48 : 0; sf_mutex_unlock(&vm->tuning_lock); /* === Heat Dynamics (Loop #1 & #3) === */ metrics.decay_slope = (double)vm->decay_slope_q48 / 65536.0; /* Collect snapshot of current dictionary state */ { uint64_t hot_word_count = 0; uint64_t stale_word_count = 0; uint64_t total_heat = 0; uint32_t word_count = 0; sf_mutex_lock(&vm->dict_lock); for (DictEntry *e = vm->latest; e != NULL; e = e->link) { if (e->execution_heat > HOTWORDS_EXECUTION_HEAT_THRESHOLD) hot_word_count++; else if (e->execution_heat > 0 && e->execution_heat < 10) stale_word_count++; total_heat += e->execution_heat; word_count++; } sf_mutex_unlock(&vm->dict_lock); metrics.total_heat = total_heat; metrics.hot_word_count = hot_word_count; metrics.stale_word_count = stale_word_count; metrics.stale_word_ratio = (word_count > 0) ? (double)stale_word_count / (double)word_count : 0.0; metrics.avg_word_heat = (word_count > 0) ? (double)total_heat / (double)word_count : 0.0; } /* === Heartbeat & Timing (Loop #7) === */ metrics.tick_count = vm->heartbeat.tick_count; metrics.tick_target_ns = vm->heartbeat.tick_target_ns; metrics.inference_run_count = vm->heartbeat.inference_run_count; metrics.early_exit_count = vm->heartbeat.early_exit_count; /* === Cache Promotions/Demotions (Loop #4) === */ if (vm->hotwords_cache) { metrics.cache_promotions = vm->hotwords_cache->stats.promotions; metrics.cache_demotions = vm->hotwords_cache->stats.evictions; /* evictions = demotions */ } else { metrics.cache_promotions = 0; metrics.cache_demotions = 0; } /* === Window & Decay Inference (Loop #5 & #6) === */ metrics.prefetch_accuracy_percent = 0.0; metrics.prefetch_attempts = vm->pipeline_metrics.prefetch_attempts; metrics.prefetch_hits = vm->pipeline_metrics.prefetch_hits; metrics.window_tuning_checks = vm->pipeline_metrics.window_tuning_checks; metrics.final_effective_window_size = (uint32_t)vm->rolling_window.effective_window_size; if (vm->pipeline_metrics.prefetch_attempts > 0) { metrics.prefetch_accuracy_percent = 100.0 * (double)vm->pipeline_metrics.prefetch_hits / (double)vm->pipeline_metrics.prefetch_attempts; } /* === Performance counters === */ metrics.words_executed = vm->heartbeat.words_executed; metrics.dictionary_lookups = vm->heartbeat.dictionary_lookups; /* Performance - workload duration as Q48.16 */ metrics.vm_workload_duration_ns_q48 = (int64_t)workload_duration_ns << 16; metrics.total_runtime_ms = 0; metrics.memory_allocated_bytes = 0; metrics.speedup_vs_baseline = 1.0; /* Statistical (defaults) */ metrics.ci_lower_95 = 0.0; metrics.ci_upper_95 = 0.0; /* System state deltas - convert to Q48.16 * UBSan fix: Use multiplication instead of left-shift for signed values (2025-12-09) * Left-shifting negative values is undefined behavior */ metrics.cpu_temp_delta_c_q48 = (int64_t)cpu_temp_delta_c * 65536; metrics.cpu_freq_delta_mhz_q48 = (int64_t)cpu_freq_delta_mhz * 65536; /* Tuning knobs */ metrics.decay_rate_q16 = DECAY_RATE_PER_US_Q16; metrics.decay_min_interval_ns = DECAY_MIN_INTERVAL; metrics.rolling_window_size = ROLLING_WINDOW_SIZE; metrics.adaptive_shrink_rate = 75; metrics.heat_cache_demotion_threshold = 10; /* === Loop Enable Flags (2^7 factorial) === */ /* L8 FINAL INTEGRATION: All loops always-on, controlled by L8 policy */ metrics.enable_loop_1_heat_tracking = 1; /* Always-on (internal signal) */ metrics.enable_loop_2_rolling_window = 1; /* Always-on (L8-controlled) */ metrics.enable_loop_3_linear_decay = 1; /* Always-on */ metrics.enable_loop_4_pipelining = 1; /* Always-on (internal signal) */ metrics.enable_loop_5_window_inference = 1; /* Always-on */ metrics.enable_loop_6_decay_inference = 1; /* Always-on */ metrics.enable_loop_7_adaptive_heartrate = 1; /* Always-on */ /* Legacy configuration */ metrics.enable_hotwords_cache = ENABLE_HOTWORDS_CACHE; metrics.enable_pipelining = ENABLE_PIPELINING; return metrics; } /** * Write CSV header */ void metrics_write_csv_header(FILE *out) { fprintf(out, /* Loop enable flags (2^7 factorial) - FIRST for easy filtering */ "L1_heat,L2_window,L3_decay,L4_pipeline,L5_win_inf,L6_decay_inf,L7_heartrate," /* Cache stats */ "total_lookups,cache_hits,cache_hit_pct,bucket_hits,bucket_hit_pct," "cache_lat_ns,cache_lat_std,bucket_lat_ns,bucket_lat_std," /* Pipelining (Loop #4) */ "ctx_pred_total,ctx_correct,ctx_acc_pct,cache_promos,cache_demos," /* Rolling window (Loop #2) */ "win_diversity_pct,win_final_bytes,win_width,actual_win_size,win_total_exec,win_var_q48," /* Heat dynamics (Loop #1 & #3) */ "decay_slope,total_heat,hot_words,stale_words,stale_ratio,avg_heat," /* Heartbeat & timing (Loop #7) */ "tick_count,tick_target_ns,infer_runs,early_exits," /* Window & decay inference (Loop #5 & #6) */ "prefetch_acc_pct,prefetch_attempts,prefetch_hits,win_tune_checks,final_win_size," /* Performance */ "workload_ns_q48,runtime_ms,words_exec,dict_lookups,mem_bytes,speedup," /* Statistical */ "ci_lower_95,ci_upper_95," /* System deltas */ "cpu_temp_delta_q48,cpu_freq_delta_q48," /* Tuning knobs */ "decay_rate_q16,decay_min_ns,roll_win_size,shrink_rate,demo_thresh," /* Legacy */ "hotwords_cache,pipelining\n"); } /** * Write CSV row - MUST match header column order exactly */ void metrics_write_csv_row(FILE *out, const DoeMetrics *metrics) { fprintf(out, /* Loop enable flags (2^7 factorial) - FIRST for easy filtering */ "%d,%d,%d,%d,%d,%d,%d," /* Cache stats */ "%u,%lu,%.2f,%lu,%.2f," "%ld,%ld,%ld,%ld," /* Pipelining (Loop #4) */ "%lu,%lu,%.2f,%lu,%lu," /* Rolling window (Loop #2) */ "%.2f,%u,%u,%u,%lu,%lu," /* Heat dynamics (Loop #1 & #3) */ "%.6f,%lu,%lu,%lu,%.6f,%.6f," /* Heartbeat & timing (Loop #7) */ "%lu,%lu,%lu,%lu," /* Window & decay inference (Loop #5 & #6) */ "%.2f,%lu,%lu,%lu,%u," /* Performance */ "%ld,%lu,%lu,%lu,%lu,%.4f," /* Statistical */ "%.6f,%.6f," /* System deltas */ "%ld,%ld," /* Tuning knobs */ "%u,%u,%u,%u,%u," /* Legacy */ "%d,%d\n", /* Loop enable flags */ metrics->enable_loop_1_heat_tracking, metrics->enable_loop_2_rolling_window, metrics->enable_loop_3_linear_decay, metrics->enable_loop_4_pipelining, metrics->enable_loop_5_window_inference, metrics->enable_loop_6_decay_inference, metrics->enable_loop_7_adaptive_heartrate, /* Cache stats */ metrics->total_lookups, metrics->cache_hits, metrics->cache_hit_percent, metrics->bucket_hits, metrics->bucket_hit_percent, metrics->cache_hit_latency_ns, metrics->cache_hit_stddev_ns, metrics->bucket_search_latency_ns, metrics->bucket_search_stddev_ns, /* Pipelining (Loop #4) */ metrics->context_predictions_total, metrics->context_correct, metrics->context_accuracy_percent, metrics->cache_promotions, metrics->cache_demotions, /* Rolling window (Loop #2) */ metrics->window_diversity_percent, metrics->window_final_size_bytes, metrics->rolling_window_width, metrics->actual_window_size, metrics->total_executions, metrics->window_variance_q48, /* Heat dynamics (Loop #1 & #3) */ metrics->decay_slope, metrics->total_heat, metrics->hot_word_count, metrics->stale_word_count, metrics->stale_word_ratio, metrics->avg_word_heat, /* Heartbeat & timing (Loop #7) */ metrics->tick_count, metrics->tick_target_ns, metrics->inference_run_count, metrics->early_exit_count, /* Window & decay inference (Loop #5 & #6) */ metrics->prefetch_accuracy_percent, metrics->prefetch_attempts, metrics->prefetch_hits, metrics->window_tuning_checks, metrics->final_effective_window_size, /* Performance */ metrics->vm_workload_duration_ns_q48, metrics->total_runtime_ms, metrics->words_executed, metrics->dictionary_lookups, metrics->memory_allocated_bytes, metrics->speedup_vs_baseline, /* Statistical */ metrics->ci_lower_95, metrics->ci_upper_95, /* System deltas */ metrics->cpu_temp_delta_c_q48, metrics->cpu_freq_delta_mhz_q48, /* Tuning knobs */ metrics->decay_rate_q16, metrics->decay_min_interval_ns, metrics->rolling_window_size, metrics->adaptive_shrink_rate, metrics->heat_cache_demotion_threshold, /* Legacy */ metrics->enable_hotwords_cache, metrics->enable_pipelining); } /** * Print metrics as human-readable text */ void metrics_print_text(FILE *out, const DoeMetrics *metrics) { fprintf(out, "\n=== DoE Metrics ===\n"); fprintf(out, "Lookups: %u\n", metrics->total_lookups); fprintf(out, "Cache Hits: %lu (%.2f%%)\n", metrics->cache_hits, metrics->cache_hit_percent); fprintf(out, "Bucket Hits: %lu (%.2f%%)\n", metrics->bucket_hits, metrics->bucket_hit_percent); fprintf(out, "Hit Latency: %ld ns (±%ld)\n", metrics->cache_hit_latency_ns, metrics->cache_hit_stddev_ns); fprintf(out, "Search Latency: %ld ns (±%ld)\n", metrics->bucket_search_latency_ns, metrics->bucket_search_stddev_ns); fprintf(out, "Predictions: %lu / %lu (%.2f%% accurate)\n", metrics->context_correct, metrics->context_predictions_total, metrics->context_accuracy_percent); fprintf(out, "Window Width: %u bytes\n", metrics->rolling_window_width); fprintf(out, "Decay Slope: %.2f\n", metrics->decay_slope); fprintf(out, "Workload Time: %ld ns (Q48.16)\n", metrics->vm_workload_duration_ns_q48); fprintf(out, "CPU Temp Delta: %ld°C (Q48.16)\n", metrics->cpu_temp_delta_c_q48); fprintf(out, "CPU Freq Delta: %ld MHz (Q48.16)\n", metrics->cpu_freq_delta_mhz_q48); } /** * Integer square root using Newton's method (pure C99, no math.h) */ static uint64_t isqrt(uint64_t n) { if (n == 0) return 0; if (n < 4) return 1; uint64_t x = n; uint64_t y = (x + 1) / 2; while (y < x) { x = y; y = (x + n / x) / 2; } return x; } /** * Write reduced James Law CSV row (20 columns for window scaling experiment) * * This outputs only the subset of metrics needed for the James Law validation experiment. * Format matches the header in run_window_sweep.sh (minus the 5 script-prepended columns). * * Expected columns (20): * workload_duration_ns, l8_mode_final, lambda_effective, K_statistic, K_deviation, * entropy, cv, temporal_decay, stability_score, ns_per_word, total_runtime_ms, * window_final, hot_word_count, avg_heat, bucket_mean_K, bucket_cv_K, * bucket_window_var, bucket_heat_var, bucket_mode_transitions, bucket_collapse_flag */ void metrics_write_james_law_csv_row(FILE *out, const DoeMetrics *metrics, VM *vm) { /* Extract rolling window size for calculations */ uint32_t rolling_window_size = metrics->rolling_window_size; if (rolling_window_size == 0) rolling_window_size = 1; /* Avoid division by zero */ /* Calculate James Law metrics */ uint32_t window_width = metrics->rolling_window_width; double lambda_effective = (double)window_width; double K_statistic = (window_width > 0) ? ((double)window_width / (double)rolling_window_size) : 1.0; double K_deviation = K_statistic - 1.0; /* Calculate derived metrics */ double entropy = metrics->window_diversity_percent / 100.0; /* Normalize to 0-1 */ /* Coefficient of variation: use cache hit latency stddev / mean as proxy */ double cv_metric = (metrics->cache_hit_latency_ns > 0) ? ((double)metrics->cache_hit_stddev_ns / (double)metrics->cache_hit_latency_ns) : 1.0; double temporal_decay = metrics->decay_slope; /* Stability score: inverse of stale word ratio (higher = more stable) */ double stability_score = (metrics->stale_word_ratio < 1.0) ? (1.0 - metrics->stale_word_ratio) * 10000.0 /* Scale to match expected range */ : 0.0; /* Performance: nanoseconds per word executed */ double ns_per_word = (metrics->words_executed > 0) ? ((double)metrics->vm_workload_duration_ns_q48 / (double)metrics->words_executed) : 0.0; /* Get L8 mode from VM heartbeat state */ uint8_t l8_mode_final = 1; /* Default to mode 1 if unavailable */ if (vm && vm->heartbeat.tick_buffer_write_index > 0) { /* Get last tick's L8 mode */ uint32_t last_idx = (vm->heartbeat.tick_buffer_write_index - 1) % HEARTBEAT_TICK_BUFFER_SIZE; l8_mode_final = vm->heartbeat.tick_buffer[last_idx].l8_mode; } /* Calculate bucket aggregates from heartbeat state */ double bucket_mean_K = 0.0; double bucket_cv_K = 0.0; double bucket_window_var = 0.0; double bucket_heat_var = 0.0; uint32_t bucket_mode_transitions = 0; int bucket_collapse_flag = 0; if (vm && vm->heartbeat.bucket_tick_count > 0) { uint32_t n = vm->heartbeat.bucket_tick_count; /* Mean K */ bucket_mean_K = vm->heartbeat.bucket_sum_K / (double)n; /* CV of K: sqrt(variance) / mean using integer approximation */ double mean_K_squared = bucket_mean_K * bucket_mean_K; double variance_K = (vm->heartbeat.bucket_sum_K_squared / (double)n) - mean_K_squared; if (variance_K > 0.0 && bucket_mean_K > 0.0) { /* Scale variance to avoid precision loss, compute integer sqrt, then scale back */ uint64_t variance_scaled = (uint64_t)(variance_K * 1000000.0); uint64_t stddev_scaled = isqrt(variance_scaled); double stddev_K = (double)stddev_scaled / 1000.0; bucket_cv_K = (stddev_K / bucket_mean_K) * 100.0; /* As percentage */ } /* Window and heat variance (using accumulated sums as proxy) */ bucket_window_var = vm->heartbeat.bucket_sum_window_variance; bucket_heat_var = vm->heartbeat.bucket_sum_heat_variance; bucket_mode_transitions = vm->heartbeat.bucket_mode_transitions; bucket_collapse_flag = vm->heartbeat.bucket_collapse_flag; } /* Write CSV row: 20 columns */ fprintf(out, "%ld,%u,%.6f,%.6f,%.6f," /* workload_duration_ns, l8_mode_final, lambda_effective, K_statistic, K_deviation */ "%.6f,%.6f,%.6f,%.6f,%.6f," /* entropy, cv, temporal_decay, stability_score, ns_per_word */ "%lu,%u,%lu,%.6f," /* total_runtime_ms, window_final, hot_word_count, avg_heat */ "%.6f,%.6f,%.6f,%.6f,%u,%d\n", /* bucket_mean_K, bucket_cv_K, bucket_window_var, bucket_heat_var, bucket_mode_transitions, bucket_collapse_flag */ /* Row 1-5 */ metrics->vm_workload_duration_ns_q48, l8_mode_final, lambda_effective, K_statistic, K_deviation, /* Row 6-10 */ entropy, cv_metric, temporal_decay, stability_score, ns_per_word, /* Row 11-14 */ metrics->total_runtime_ms, metrics->final_effective_window_size, metrics->hot_word_count, metrics->avg_word_heat, /* Row 15-20 */ bucket_mean_K, bucket_cv_K, bucket_window_var, bucket_heat_var, bucket_mode_transitions, bucket_collapse_flag ); }