From e76c174f4c12e6afb4d778e74b5a741ae84a7477 Mon Sep 17 00:00:00 2001 From: Andress Barajas Date: Sat, 10 May 2025 07:25:39 -0700 Subject: [PATCH] Add gcov implementation that supports GCC 13+ gcov file specs --- .gitignore | 4 + addons/include/gcov/gcov.h | 152 ++++++++++++++++++++++++ addons/libgcov/Makefile | 9 ++ addons/libgcov/dump.c | 191 ++++++++++++++++++++++++++++++ addons/libgcov/filepath.c | 81 +++++++++++++ addons/libgcov/gcov.c | 97 +++++++++++++++ addons/libgcov/gcov.h | 165 ++++++++++++++++++++++++++ addons/libgcov/kos/dreamcast.cnf | 0 addons/libgcov/merge.c | 108 +++++++++++++++++ addons/libgcov/profiler.c | 99 ++++++++++++++++ addons/libgcov/reset.c | 39 ++++++ addons/libgcov/topn.c | 196 +++++++++++++++++++++++++++++++ 12 files changed, 1141 insertions(+) create mode 100644 addons/include/gcov/gcov.h create mode 100644 addons/libgcov/Makefile create mode 100644 addons/libgcov/dump.c create mode 100644 addons/libgcov/filepath.c create mode 100644 addons/libgcov/gcov.c create mode 100644 addons/libgcov/gcov.h create mode 100644 addons/libgcov/kos/dreamcast.cnf create mode 100644 addons/libgcov/merge.c create mode 100644 addons/libgcov/profiler.c create mode 100644 addons/libgcov/reset.c create mode 100644 addons/libgcov/topn.c diff --git a/.gitignore b/.gitignore index d00910dbe5..689261b5bc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,13 +6,17 @@ *.exe *.dll *.map +*.gcda +*.gcno *~ .*.swp .DS_Store +≈ .vscode/ doc/reference/ environ.sh romdisk.img +coverage.info kernel/stubs/*.c kernel/exports/kernel_exports.c kernel/arch/dreamcast/kernel/arch_exports.c diff --git a/addons/include/gcov/gcov.h b/addons/include/gcov/gcov.h new file mode 100644 index 0000000000..6a11affed4 --- /dev/null +++ b/addons/include/gcov/gcov.h @@ -0,0 +1,152 @@ +/* KallistiOS ##version## + + gcov/gcov.h + Copyright (C) 2025 Andy Barajas +*/ + +/** \file gcov/gcov.h + \brief Minimal GCOV runtime implementation. + \ingroup gcov + + This file defines the public interface for the GCOV profiling runtime. + It enables GCC's `--coverage` and `-fprofile-generate` functionality. The + implementation is compatible with GCC 13+ and supports generation and manual + dumping of `.gcda` coverage files, including full support for all standard + counter types. + + \author Andy Barajas +*/ + +#ifndef __GCOV_GCOV_H +#define __GCOV_GCOV_H + +#include +__BEGIN_DECLS + +/** \defgroup gcov GCOV + \brief Lightweight GCOV profiling runtime for KOS + \ingroup debugging + + This file provides runtime support for GCOV, a coverage analysis tool built + into GCC. GCOV allows you to determine which parts of your code were executed, + how often, and which branches were taken. This is especially helpful for + analyzing test coverage or identifying unused code paths. + + Supported Profiling Modes: + + 1. `-fprofile-generate`: + + Enables profile data generation. This inserts instrumentation into your + code to collect execution counts for functions, branches, and arcs during + runtime. The results are written to `.gcda` files when `__gcov_exit()` or + `__gcov_dump()` is called. + + Example: + ```sh + CFLAGS += -fprofile-generate + ``` + + 2. `-fprofile-use`: + + Recompiles your code using previously collected `.gcda` files (from + `-fprofile-generate`) to guide optimizations. This can improve performance + by reordering code based on actual runtime behavior. + + Example: + ```sh + CFLAGS += -fprofile-use + ``` + + 3. `-ftest-coverage` (or `--coverage`): + + Enables both `-fprofile-arcs` and `-ftest-coverage`, which insert + instrumentation and generate `.gcno` metadata files during compilation. + These `.gcno` files are required to interpret `.gcda` data later. + + Example: + ```sh + CFLAGS += --coverage + ``` + + Collecting and Analyzing Coverage: + + 1. **Compile with coverage support:** + + ```sh + CFLAGS += --coverage + ``` + + This generates `.gcno` files alongside your object files. + + 2. **Run your program on Dreamcast (with `-fprofile-generate`):** + + Coverage data will be collected in memory during execution. You can manually + trigger a dump at any point by calling: + + ```c + __gcov_dump(); // or __gcov_exit(); + ``` + + This writes `.gcda` files to the filesystem, redirected to `/pc` by default. + + 3. **Generate an HTML report with LCOV:** + + Use this Makefile target to capture and visualize results: + + ```make + lcov: + lcov \ + --gcov-tool=/opt/toolchains/dc/sh-elf/bin/sh-elf-gcov \ + --directory . \ + --base-directory . \ + --capture \ + --output-file coverage.info + genhtml coverage.info --output-directory html + open html/index.html + ``` + + This generates a full HTML report with annotated source code in the `html/` directory. + + \author Andy Barajas + + @{ +*/ + +/** \brief Environment variable to set the output directory for `.gcda` files. + \ingroup gcov + + If set, this value is prepended to the stripped source path to form the + final output path. +*/ +#define GCOV_PREFIX "GCOV_PREFIX" + +/** \brief Environment variable to control path stripping. + \ingroup gcov + + Specifies how many leading directory components should be removed from the + source file path before generating the `.gcda` output file. +*/ +#define GCOV_PREFIX_STRIP "GCOV_PREFIX_STRIP" + +/** \brief Clears all collected runtime coverage counters. + \ingroup gcov + + This function resets all counters in memory without writing them out. + Useful for restarting coverage collection mid-run. +*/ +void __gcov_reset(void); + +/** \brief Writes all registered coverage data to `.gcda` files. + \ingroup gcov + + This function flushes all registered coverage counters to disk using the + current GCOV output rules. Called automatically on exit, but can also be + called manually for intermediate coverage snapshots. +*/ +void __gcov_dump(void); + +/** @} */ + +__END_DECLS + +#endif /* !__GCOV_GCOV_H */ diff --git a/addons/libgcov/Makefile b/addons/libgcov/Makefile new file mode 100644 index 0000000000..d4f6310afa --- /dev/null +++ b/addons/libgcov/Makefile @@ -0,0 +1,9 @@ +# libgcov Makefile +# +# Copyright (C) 2025 Andy Barajas +# + +TARGET = libgcov.a +OBJS = gcov.o profiler.o merge.o dump.o reset.o topn.o filepath.o + +include $(KOS_BASE)/addons/Makefile.prefab diff --git a/addons/libgcov/dump.c b/addons/libgcov/dump.c new file mode 100644 index 0000000000..e7ca267454 --- /dev/null +++ b/addons/libgcov/dump.c @@ -0,0 +1,191 @@ +/* KallistiOS ##version## + + dump.c + Copyright (C) 2025 Andy Barajas + +*/ + +#include + +#include "gcov.h" + +#define GCOV_TAG_EOF 0 + +#define GCOV_MAGIC ((uint32_t)0x67636461) /* "gcda" */ +#define GCOV_TAG_FUNCTION ((uint32_t)0x01000000) +#define GCOV_TAG_COUNTER_BASE 0x01a10000 +#define GCOV_TAG_FOR_COUNTER(CNT) (GCOV_TAG_COUNTER_BASE + ((uint32_t)(CNT) << 17)) +#define GCOV_COUNTER_FOR_TAG(TAG) (((TAG) - GCOV_TAG_COUNTER_BASE) >> 17) + +/* So merge function pointers can have access to data we want to merge */ +gcov_type *merge_temp_buf = NULL; + +static inline bool are_all_counters_zero(const gcov_ctr_info_t *ci_ptr) { + for(uint32_t i = 0; i < ci_ptr->num; i++) { + if(ci_ptr->values[i] != 0) + return false; + } + + return true; +} + +/* Reads a .gcda file and merges its counters into the current gcov_info_t */ +static void merge_existing_gcda(const gcov_info_t *info) { + FILE *f; + int32_t length; + uint32_t tag; + uint32_t fn_idx = 0; + uint32_t ctr_idx = 0; + char out_path[GCOV_FILEPATH_MAX]; + + gcov_build_filepath(info->filepath, out_path); + + f = fopen(out_path, "rb"); + if(!f) + return; + + /* Check MAGIC */ + if(fread(&tag, 4, 1, f) != 1 || tag != GCOV_MAGIC) + goto done; + + /* Skip version, stamp, and checksum */ + fseek(f, 12, SEEK_CUR); + + /* Read TAG blocks */ + while(fread(&tag, 4, 1, f) == 1) { + if(tag == GCOV_TAG_EOF) + break; + + /* Read TAG length (in bytes) */ + if(fread(&length, 4, 1, f) != 1) + break; + + if(tag == GCOV_TAG_FUNCTION) { + /* Skip ident, lineno_checksum, and cfg_checksum */ + fseek(f, length, SEEK_CUR); + fn_idx++; + /* Reset counters index for new function */ + ctr_idx = 0; + } + else if(tag >= GCOV_TAG_FOR_COUNTER(0) && + tag < GCOV_TAG_FOR_COUNTER(GCOV_COUNTERS)) { + + /* Skip if negative - Counters are all zero */ + if(length < 0) { + ctr_idx++; + continue; + } + + int mrg_idx = GCOV_COUNTER_FOR_TAG(tag); + const gcov_fn_info_t *fn = info->functions[fn_idx - 1]; + + if(fn && info->merge[mrg_idx]) { + gcov_type *buffer = NULL; + if(posix_memalign((void **)&buffer, 8, length)) + break; + + if(fread(buffer, length, 1, f) != 1) { + free(buffer); + break; + } + + uint32_t num_counters = length / sizeof(gcov_type); + merge_temp_buf = buffer; + info->merge[mrg_idx](fn->ctrs[ctr_idx].values, num_counters); + merge_temp_buf = NULL; + free(buffer); + } + else { + /* This shouldnt happen */ + fseek(f, length, SEEK_CUR); + } + + ctr_idx++; + } + else { + /* Skip unknown tag block */ + fseek(f, length, SEEK_CUR); + } + } + dbglog(DBG_ERROR, "GCOV: Done merging file%s\n", out_path); +done: + fclose(f); +} + +static void write_gdca(const gcov_info_t *info) { + FILE *f; + bool all_zero; + uint32_t i, j, k, tmp, length; + char out_path[GCOV_FILEPATH_MAX]; + + gcov_build_filepath(info->filepath, out_path); + + f = fopen(out_path, "wb"); + if(!f) { + dbglog(DBG_ERROR, "GCOV: Failed to open %s\n", out_path); + return; + } + + /* File header */ + tmp = GCOV_MAGIC; + fwrite(&tmp, 1, 4, f); + fwrite(&info->version, 1, 4, f); + fwrite(&info->stamp, 1, 4, f); + fwrite(&info->checksum, 1, 4, f); + + /* Each function */ + for(i = 0; i < info->num_functions; ++i) { + const gcov_fn_info_t *fn = info->functions[i]; + length = (fn && fn->key == info) ? 12 : 0; + + /* Tag: Function header */ + tmp = GCOV_TAG_FUNCTION; + fwrite(&tmp, 1, 4, f); + fwrite(&length, 1, 4, f); /* # of following bytes */ + if(!length) continue; + + fwrite(&fn->ident, 1, 4, f); + fwrite(&fn->lineno_checksum, 1, 4, f); + fwrite(&fn->cfg_checksum, 1, 4, f); + + /* Each Counter type */ + const gcov_ctr_info_t *ci = fn->ctrs; + for(j = 0; j < GCOV_COUNTERS; ++j) { + if(!info->merge[j]) + continue; + + tmp = GCOV_TAG_FOR_COUNTER(j); + fwrite(&tmp, 1, 4, f); + + if(j == GCOV_COUNTER_V_TOPN || j == GCOV_COUNTER_V_INDIR) + topn_write(f, ci); + else { + all_zero = are_all_counters_zero(ci); + + tmp = sizeof(gcov_type) * (all_zero ? -ci->num : ci->num); + fwrite(&tmp, 1, 4, f); + + if(!all_zero) { + for(k = 0; k < ci->num; ++k) { + fwrite(&ci->values[k], 1, sizeof(gcov_type), f); + } + } + } + + ci++; + } + } + + /* Terminator */ + tmp = GCOV_TAG_EOF; + fwrite(&tmp, 1, 4, f); + + fclose(f); + + dbglog(DBG_NOTICE, "GCOV: dumped %s\n", out_path); +} + +void dump_info(const gcov_info_t *info) { + merge_existing_gcda(info); + write_gdca(info); +} diff --git a/addons/libgcov/filepath.c b/addons/libgcov/filepath.c new file mode 100644 index 0000000000..747d44af09 --- /dev/null +++ b/addons/libgcov/filepath.c @@ -0,0 +1,81 @@ +/* KallistiOS ##version## + + filepath.c + Copyright (C) 2025 Andy Barajas + +*/ + +#include + +#include +#include "gcov.h" + +/* + Strips the first `strip_count` components from a given file path, + normalizes redundant slashes, and writes the result into `out`. +*/ +void strip_leading_dirs(const char *path, int strip_count, char *out, size_t out_size) { + const char *p = path; + size_t i; + bool last_was_slash = false; // Flag to track consecutive slashes + + /* Skip any leading slashes */ + while(*p == '/') p++; + + /* Strip the specified number of path components */ + while(strip_count-- > 0) { + /* Skip over one component (up to the next '/') */ + while(*p && *p != '/') p++; + + /* Skip the slash separator(s) */ + while(*p == '/') p++; + } + + /* Copy the remaining path to `out`, collapsing multiple slashes into one */ + for(i = 0; *p && i < out_size - 1; ++p) { + if(*p == '/') { + if(last_was_slash) continue; /* Skip redundant slashes */ + last_was_slash = true; + } + else + last_was_slash = false; + + out[i++] = *p; + } + + /* Null terminate */ + out[i] = '\0'; +} + +/* + Builds a final output path by combining a prefix (from GCOV_PREFIX) and + a stripped version of the source path (based on GCOV_PREFIX_STRIP). +*/ +void gcov_build_filepath(const char *src_path, char *out_path) { + char *stripped = (char *)alloca(GCOV_FILEPATH_MAX); + char *prefix_clean = (char *)alloca(GCOV_FILEPATH_MAX); + + const char *strip_str = getenv(GCOV_PREFIX_STRIP); + int prefix_strip = strip_str ? atoi(strip_str) : 0; + + /* Strip path components from the source file path */ + strip_leading_dirs(src_path, prefix_strip, stripped, GCOV_FILEPATH_MAX); + + const char *prefix = getenv(GCOV_PREFIX); + if(!prefix) + prefix = ""; + + /* Remove trailing slash from the prefix if it exists */ + size_t prefix_len = strlen(prefix); + if(prefix_len > 0 && prefix[prefix_len - 1] == '/') { + /* Copy all but the trailing slash into prefix_clean */ + snprintf(prefix_clean, GCOV_FILEPATH_MAX, "%.*s", (int)(prefix_len - 1), prefix); + prefix = prefix_clean; + } + + /* Final output path: prefix + '/' + stripped, or just stripped */ + if(*prefix) + snprintf(out_path, GCOV_FILEPATH_MAX, "%s/%s", prefix, stripped); + else + snprintf(out_path, GCOV_FILEPATH_MAX, "%s", stripped); +} diff --git a/addons/libgcov/gcov.c b/addons/libgcov/gcov.c new file mode 100644 index 0000000000..6ed35827da --- /dev/null +++ b/addons/libgcov/gcov.c @@ -0,0 +1,97 @@ +/* KallistiOS ##version## + + gcov.c + Copyright (C) 2025 Andy Barajas + +*/ + +#include +#include + +#include +#include "gcov.h" + +/* Head of the linked list of registered coverage data. */ +static gcov_info_t *__gcov_info_head = NULL; + +/* Flag to ensure GCOV environment setup only happens once. */ +static bool set_default_env = false; + +static mutex_t gcov_mutex = MUTEX_INITIALIZER; + +/* Init the memory pool */ +void gprof_init(void) { + topn_pool_init(__gcov_info_head); +} + +/* Register a new gcov_info block with the runtime. */ +void __gcov_init(gcov_info_t *info) { + if(!info->version || !info->num_functions) + return; + + info->next = __gcov_info_head; + __gcov_info_head = info; + + /* Set a default output prefix if none is already set. */ + if(!set_default_env) { + set_default_env = true; + setenv(GCOV_PREFIX, "/pc", true); + } + + // gcov_fn_info_t *fn = NULL; + // gcov_ctr_info_t *ci = NULL; + + // for(uint32_t f_ix = 0; f_ix < info->num_functions; f_ix++) { + // fn = info->functions[f_ix]; + // dbglog(DBG_NOTICE, " functions[%lu] = %p\n", f_ix, (void *)fn); + // dbglog(DBG_NOTICE, " fn[%lu]: ident=0x%08lx, checksum=0x%08lx\n", + // f_ix, fn->ident, fn->cfg_checksum); + + // ci = fn->ctrs; + // for(uint32_t t_ix = 0; t_ix < GCOV_COUNTERS; t_ix++) { + // if(!info->merge[t_ix]) + // continue; + + // dbglog(DBG_NOTICE, + // " CTR[%lu]: num=%lu, values=%p\n", + // t_ix, ci->num, (void *)ci->values); + + // ci++; // move to next active counter + // } + // } +} + +/* Reset all coverage counters to zero. */ +void __gcov_reset(void) { + gcov_info_t *info = __gcov_info_head; + + mutex_lock_scoped(&gcov_mutex); + + while(info) { + reset_info(info); + info = info->next; + } +} + +/* Write out .gcda data for all registered objects. */ +void __gcov_dump(void) { + gcov_info_t *info = __gcov_info_head; + + mutex_lock_scoped(&gcov_mutex); + + while(info) { + dump_info(info); + info = info->next; + } +} + +/* Called on exit */ +void __gcov_exit(void) { + //topn_dump_text(__gcov_info_head); + + /* Flush all coverage data */ + __gcov_dump(); + + /* Free the memory pool */ + topn_pool_shutdown(); +} diff --git a/addons/libgcov/gcov.h b/addons/libgcov/gcov.h new file mode 100644 index 0000000000..ababdd90e6 --- /dev/null +++ b/addons/libgcov/gcov.h @@ -0,0 +1,165 @@ +/* KallistiOS ##version## + + gcov.h + Copyright (C) 2025 Andy Barajas + +*/ + +#ifndef __GCOV_H +#define __GCOV_H + +#include +#include +#include +#include + +#define GCOV_FILEPATH_MAX 1024 +#define TOPN_MAX_SLOT_SIZE 8 + +/* + GCOV counter types used for profiling. + + These represent various runtime statistics collected during execution. + Only a subset may be used for any given function. +*/ +enum { + GCOV_COUNTER_ARCS, /* Edge (branch) execution counts */ + GCOV_COUNTER_V_INTERVAL, /* Value profiling: interval distribution */ + GCOV_COUNTER_V_POW2, /* Value profiling: power-of-2 histogram */ + GCOV_COUNTER_V_TOPN, /* Value profiling: top-N most frequent values */ + GCOV_COUNTER_V_INDIR, /* Indirect call target profiling */ + GCOV_COUNTER_AVERAGE, /* Average value tracking (e.g., loop iterations) */ + GCOV_COUNTER_IOR, /* Bitwise OR of all observed values */ + GCOV_TIME_PROFILER, /* Time-based profiling (first-time execution) */ + GCOV_COUNTER_CONDS, /* Conditional branch tracking */ + GCOV_COUNTER_PATHS, /* Execution path profiling */ + GCOV_COUNTERS /* Total number of counter types */ +}; + +typedef int64_t gcov_type; + +/* A key-value pair used for TopN profiling. */ +typedef struct topn_ctr { + gcov_type value; /* Observed value */ + gcov_type count; /* Number of times observed */ +} topn_ctr_t; + +/* A TopN counter used per function site. */ +typedef struct topn_counter { + gcov_type total; /* Total observations */ + gcov_type list_size; /* Current number of tracked values */ + gcov_type list_idx; /* Index into the topn_table */ +} topn_counter_t; + +/* Represents an indirect call record. */ +typedef struct indirect_call { + void *callee; /* Called function address */ + topn_counter_t *ctr; /* Associated TopN counter */ +} indirect_call_t; + +/* Holds all counter values for a specific counter type. */ +typedef struct gcov_ctr_info { + uint32_t num; /* Number of values in this counter */ + gcov_type *values; /* Pointer to the counter data */ +} gcov_ctr_info_t; + +/* Contains metadata and counters for one instrumented function. */ +typedef struct gcov_fn_info { + const struct gcov_info *key; /* COMDAT deduplication key */ + uint32_t ident; /* Function ID */ + uint32_t lineno_checksum; /* Checksum of source line numbers */ + uint32_t cfg_checksum; /* Checksum of control flow graph */ + gcov_ctr_info_t ctrs[1]; /* One entry per used counter type */ +} gcov_fn_info_t; + +typedef void (*gcov_merge_fn_t)(gcov_type *counters, uint32_t num_counters); + +/* + Holds profiling data for an entire object file. + + One of these is emitted per object file when built with --coverage, + and registered at runtime with __gcov_init(). + + This structure links together all functions and merge routines. +*/ +typedef struct gcov_info { + uint32_t version; /* GCOV format version */ + struct gcov_info *next; /* Next entry in global list */ + uint32_t stamp; /* Timestamp of object file */ + uint32_t checksum; /* CRC of associated source file */ + const char *filepath; /* Path to the source file */ + gcov_merge_fn_t merge[GCOV_COUNTERS]; /* Merge functions per counter type */ + uint32_t num_functions; /* Number of instrumented functions */ + const gcov_fn_info_t *const *functions; /* Pointer to array of function info structs */ +} gcov_info_t; + +/* + Builds a .gcda output path from the source file path. + + This uses the GCOV_PREFIX and GCOV_PREFIX_STRIP environment variables, + removes the source extension, and appends ".gcda". + + Example: + Input: "/src/game.c" + Output: "/pc/src/game.gcda" +*/ +void gcov_build_filepath(const char *src_path, char *out_path); + +/* + Writes profiling data for a single gcov_info object to its .gcda file. + + This function serializes all counter data (e.g., arcs, value profiles, TopN) + for a given object file and writes it to the appropriate .gcda file on disk. + + It handles: + - Opening the file (using filepath, GCOV_PREFIX, etc.) + - Writing GCDA file headers and tags + - Calling the appropriate write routines for each counter type +*/ +void dump_info(const gcov_info_t *info); + +/* + Resets all counters in a gcov_info object to zero. + + It is used after flushing data, or if you want to restart profiling + mid-run (e.g., between levels or frames). +*/ +void reset_info(gcov_info_t *info); + +/* + Allocates and initializes the TopN profiling pool for all counters. + + This function traverses all functions in the gcov_info list and: + - Detects TopN and indirect call counters + - Initializes each TopN counter block (total, list_size, list_idx) + - Calculates the number of total slots needed + - Allocates a single aligned memory pool (topn_table) + + This must be called before any TopN counters are used or merged. +*/ +void topn_pool_init(const gcov_info_t *head); + +/* Frees the global memory pool allocated for TopN profiling. */ +void topn_pool_shutdown(void); + +/* Adds a new value observation to a TopN counter. + + If the value already exists, its count is incremented. + If not: + - The value is inserted if there is room + - If full, the lowest-scoring value may be evicted based on + count. +*/ +void topn_add_value(topn_counter_t *ctr, gcov_type value, gcov_type count); + +/* + Writes a gcov_ctr_info_t containing TopN counters to a .gcda file. + + This function serializes: + - The total observation count + - The number of entries in the TopN list + - Each value/count pair currently tracked +*/ +void topn_write(FILE *f, const gcov_ctr_info_t *ci_ptr); + +#endif /* __GCOV_H */ diff --git a/addons/libgcov/kos/dreamcast.cnf b/addons/libgcov/kos/dreamcast.cnf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/addons/libgcov/merge.c b/addons/libgcov/merge.c new file mode 100644 index 0000000000..abb12ab296 --- /dev/null +++ b/addons/libgcov/merge.c @@ -0,0 +1,108 @@ +/* KallistiOS ##version## + + merge.c + Copyright (C) 2025 Andy Barajas + +*/ + +#include + +#include +#include "gcov.h" + +extern gcov_type *merge_temp_buf; + +/* + Merges profiling data for TopN value counters. Each counter block tracks + the most frequently observed values (key-value pairs) with a max count of + TOPN_MAX_SLOT_SIZE. + + Serialized memory layout in merge_temp_buf: + - counters[0]: Total observations (optional; currently skipped) + - counters[1]: Number of entries (N) + - counters[2..]: N pairs of (value, count) + + Destination counters[] layout: + - counters[0]: Total observations + - counters[1]: Number of entries in the linked list + - counters[2]: Index of the head entry in the topn_table + + IMPORTANT: + - num_counters: Must be a multiple of 3 for valid TopN counter blocks. +*/ +void __gcov_merge_topn(gcov_type *counters, uint32_t num_counters) { + if(!merge_temp_buf || (num_counters % 3) != 0) { + dbglog(DBG_ERROR, "GCOV: __gcov_merge_topn: invalid state\n"); + return; + } + + uint32_t i, j; + gcov_type *buf_ptr = merge_temp_buf; + gcov_type value, count, num_entries; + + /* Interpret counters as an array of topn_counter_t structs */ + topn_counter_t *counter = (topn_counter_t *)counters; + + /* topn_counter_t structs (3 gcov_type each) */ + num_counters /= 3; + + for(i = 0; i < num_counters; i++) { + /* Skip Total; Handled in topn_add_value */ + buf_ptr++; + + num_entries = *buf_ptr++; + + for(j = 0; j < num_entries; j++) { + value = *buf_ptr++; + count = *buf_ptr++; + topn_add_value(counter, value, count); + } + + counter++; + } +} + +/* + Merges profiling counters that track the timestamp of the first time a + function was entered. This merge keeps the earliest time (smallest value) + between existing and new. +*/ +void __gcov_merge_time_profile(gcov_type *counters, uint32_t num_counters) { + uint32_t i; + + if(!merge_temp_buf) return; + + for(i = 0; i < num_counters; ++i) { + if(merge_temp_buf[i] && + (counters[i] == 0 || merge_temp_buf[i] < counters[i])) { + counters[i] = merge_temp_buf[i]; + } + } +} + +/* + Merges profiling counters that performs element-wise addition of + counters. +*/ +void __gcov_merge_add(gcov_type *counters, uint32_t num_counters) { + uint32_t i; + + if(!merge_temp_buf) return; + + for(i = 0; i < num_counters; ++i) + counters[i] += merge_temp_buf[i]; +} + +/* + Merges profiling counters that track the bitwise OR of all values + observed during execution. Each counter records a single gcov_type value + representing the union of flags or bits. +*/ +void __gcov_merge_ior(gcov_type *counters, uint32_t num_counters) { + uint32_t i; + + if(!merge_temp_buf) return; + + for(i = 0; i < num_counters; ++i) + counters[i] |= merge_temp_buf[i]; +} diff --git a/addons/libgcov/profiler.c b/addons/libgcov/profiler.c new file mode 100644 index 0000000000..af564620bc --- /dev/null +++ b/addons/libgcov/profiler.c @@ -0,0 +1,99 @@ +/* KallistiOS ##version## + + profiler.c + Copyright (C) 2025 Andy Barajas + +*/ + +#include +#include + +#include +#include "gcov.h" + +/* Required when compiling with -fprofile-generate for time and indirect call profiling. */ +gcov_type __gcov_time_profiler_counter = 0; +_Thread_local indirect_call_t __gcov_indirect_call; + +/* + Tracks the frequency of indirect function calls (e.g., virtual calls or function pointers). + + - Before a call, instrumentation stores the expected callee in `__gcov_indirect_call.callee` + and the associated counters in `__gcov_indirect_call.counters`. + - This function is called after the indirect call executes. + - If the actual callee matches the expected one, it records the callee_id in the TopN set. + + Use case: + Enables profile-guided devirtualization and indirect call promotion. + GCC can then optimize common indirect calls as direct branches. +*/ +void __gcov_indirect_call_profiler_v4(gcov_type callee_id, void *callee) { + if(callee == __gcov_indirect_call.callee && __gcov_indirect_call.ctr) { + topn_add_value(__gcov_indirect_call.ctr, callee_id, 1); + } + __gcov_indirect_call.callee = NULL; +} + +/* + Tracks the most frequently observed values at runtime. + + - Called when a profiled site (e.g., a switch case) executes with a particular value. + - Adds the value to the corresponding TopN counter set. + + Use case: + Helps compilers optimize for hot values, like common branches or loop bounds. +*/ +void __gcov_topn_values_profiler(topn_counter_t *counter, gcov_type value) { + topn_add_value(counter, value, 1); +} + +/* + Tracks the average of a series of values. + - counters[0] stores the running sum + - counters[1] stores the number of samples + */ +void __gcov_average_profiler(gcov_type *counters, gcov_type value) { + counters[0] += value; + counters[1] += 1; +} + +/* + Tracks which interval (bucket) a given value falls into. + - Useful for histograms (e.g., loop trip counts, size classes). + - Values < start increment underflow bucket: counters[steps + 1] + - Values ≥ start + steps increment overflow bucket: counters[steps] + - Values in range [start, start + steps - 1] go to their corresponding bucket +*/ +void __gcov_interval_profiler(gcov_type *counters, gcov_type value, int start, uint32_t steps) { + gcov_type delta = value - start; + + if(delta < 0) + counters[steps + 1] += 1; /* Underflow */ + else if(delta >= steps) + counters[steps] += 1; /* Overflow */ + else + counters[delta] += 1; /* Valid bucket */ +} + +/* + Tracks whether a given value is a power of two. + - counters[0] counts non-power-of-2 values + - counters[1] counts power-of-2 values + + Useful in analyzing whether runtime values are power-of-two aligned, + which has implications for bitmasking, fast division, and vectorization. +*/ +void __gcov_pow2_profiler(gcov_type *counters, gcov_type value) { + if(value == 0 || (value & (value - 1)) != 0) + counters[0] += 1; /* Not a power of 2 */ + else + counters[1] += 1; /* Power of 2 */ +} + +/* + Tracks a bitwise OR of all values seen. + - counters[0] |= value for every call +*/ +void __gcov_ior_profiler(gcov_type *counters, gcov_type value) { + counters[0] |= value; +} diff --git a/addons/libgcov/reset.c b/addons/libgcov/reset.c new file mode 100644 index 0000000000..b536e863b0 --- /dev/null +++ b/addons/libgcov/reset.c @@ -0,0 +1,39 @@ +/* KallistiOS ##version## + + reset.c + Copyright (C) 2025 Andy Barajas + +*/ + +#include "gcov.h" + +/* + Zeroes out all active counters associated with each function in + the provided gcov_info_t structure. + */ +void reset_info(gcov_info_t *info) { + uint32_t i, j, k; + + /* Each function */ + for(i = 0; i < info->num_functions; ++i) { + const gcov_fn_info_t *fn = info->functions[i]; + if(!fn) continue; + + /* Each Counter type */ + const gcov_ctr_info_t *ctr = fn->ctrs; + for(j = 0; j < GCOV_COUNTERS; ++j) { + /* Skip counter type if not present. + 'merge[j]' is only set for active counters */ + if(!info->merge[j]) + continue; + + if(ctr->num > 0 && ctr->values) { + for(k = 0; k < ctr->num; ++k) { + ctr->values[k] = 0; + } + } + + ctr++; + } + } +} diff --git a/addons/libgcov/topn.c b/addons/libgcov/topn.c new file mode 100644 index 0000000000..2432b61892 --- /dev/null +++ b/addons/libgcov/topn.c @@ -0,0 +1,196 @@ +/* KallistiOS ##version## + + topn.c + Copyright (C) 2025 Andy Barajas + +*/ + +#include + +#include + +#include +#include "gcov.h" + +static topn_ctr_t *topn_table = NULL; + +// void topn_dump_text(const gcov_info_t *head) { +// for (const gcov_info_t *info = head; info; info = info->next) { +// for (uint32_t f = 0; f < info->num_functions; f++) { +// const gcov_fn_info_t *fn = info->functions[f]; +// if (!fn) continue; + +// const gcov_ctr_info_t *ctrs = fn->ctrs; +// uint32_t ctr_index = 0; + +// for (uint32_t c = 0; c < GCOV_COUNTERS; c++) { +// if (!info->merge[c]) continue; + +// if ((c == GCOV_COUNTER_V_TOPN || c == GCOV_COUNTER_V_INDIR) && +// ctrs[ctr_index].num > 0) { + +// uint32_t num = ctrs[ctr_index].num / 3; +// topn_counter_t *counters = (topn_counter_t *)ctrs[ctr_index].values; + +// for (uint32_t i = 0; i < num; i++) { +// dbglog(DBG_NOTICE, "\n[GCOV][TopN] Counter #%lu (slot base %llu):\n", i, counters[i].list_idx); + +// for (uint32_t j = 0; j < counters[i].list_size; j++) { +// topn_ctr_t *e = &topn_table[counters[i].list_idx + j]; +// dbglog(DBG_NOTICE, " [%02lu] value=%llu count=%llu\n", +// j, e->value, e->count); +// } +// } +// } + +// ctr_index++; +// } +// } +// } +// } + +void topn_pool_init(const gcov_info_t *head) { + uint32_t fn_idx, ctr_idx, ent_idx; + uint32_t total_entries = 0; + uint32_t active_ctr_index; + uint32_t topn_table_size = 0; + topn_counter_t *counter; + + for(const gcov_info_t *info = head; info; info = info->next) { + for(fn_idx = 0; fn_idx < info->num_functions; fn_idx++) { + const gcov_ctr_info_t *ctrs = info->functions[fn_idx]->ctrs; + active_ctr_index = 0; + + for(ctr_idx = 0; ctr_idx < GCOV_COUNTERS; ctr_idx++) { + if(!info->merge[ctr_idx]) + continue; + + const gcov_ctr_info_t *ci = &ctrs[active_ctr_index]; + if((ctr_idx == GCOV_COUNTER_V_TOPN || ctr_idx == GCOV_COUNTER_V_INDIR) && + ci->num > 0) { + + uint32_t num_counters = ci->num / 3; + + for(ent_idx = 0; ent_idx < num_counters; ent_idx++) { + counter = (topn_counter_t *)(&ci->values[ent_idx * 3]); + counter->total = 0; + counter->list_size = 0; + counter->list_idx = total_entries * TOPN_MAX_SLOT_SIZE; + total_entries++; + } + } + + active_ctr_index++; + } + } + } + + /* Allocate memory for topn_ctr_t table */ + topn_table_size = total_entries * TOPN_MAX_SLOT_SIZE * sizeof(topn_ctr_t); + + if(posix_memalign((void**)&topn_table, 32, topn_table_size)) + dbglog(DBG_ERROR, "[GCOV] Failed to allocate %lu memory for TopN table\n", topn_table_size); + else { + dbglog(DBG_NOTICE, "[GCOV] Allocated %lu bytes for TopN table\n", topn_table_size); + memset(topn_table, 0, topn_table_size); + } +} + +void topn_pool_shutdown(void) { + free(topn_table); + topn_table = NULL; +} + +static __always_inline void swap(topn_ctr_t *a, topn_ctr_t *b) { + topn_ctr_t temp = *a; + *a = *b; + *b = temp; +} + +void topn_add_value(topn_counter_t *ctr, gcov_type value, gcov_type count) { + bool found = false; + uint32_t i; + topn_ctr_t *entries = &topn_table[ctr->list_idx]; + topn_ctr_t *entry = entries; + + /* Track replacement candidate */ + topn_ctr_t *replace_candidate = entry; + + /* List empty? */ + if(ctr->list_size == 0) { + ctr->total = count; + ctr->list_size = 1; + entry->value = value; + entry->count = count; + return; + } + + /* Search list for match and determine worst candidate */ + for(i = 0; i < ctr->list_size; i++) { + if(!found && entry->value == value) { + entry->count += count; + ctr->total += count; + found = true; + } + + /* Track lowest-count for replacement */ + if (entry->count < replace_candidate->count || + (entry->count == replace_candidate->count)) { + replace_candidate = entry; + } + + entry++; + } + + if(found) + return; + + /* List full? Try to evict worst */ + if(ctr->list_size == TOPN_MAX_SLOT_SIZE) { + if(count > replace_candidate->count) { + ctr->total += count - replace_candidate->count; + replace_candidate->value = value; + replace_candidate->count = count; + } + } + else { + /* Room to add new entry */ + ctr->list_size++; + ctr->total += count; + entry->value = value; + entry->count = count; + } +} + +void topn_write(FILE *f, const gcov_ctr_info_t *ci_ptr) { + uint32_t i, j, length; + uint32_t num_pairs = 0; + uint32_t num_counters = ci_ptr->num / 3; + topn_counter_t *counters = (topn_counter_t *)ci_ptr->values; + + /* First pass: calculate number of pairs */ + for(i = 0;i < num_counters; i++) + num_pairs += counters[i].list_size; + + /* Write byte length for all the following data */ + length = (num_counters * 2 + num_pairs * 2) * sizeof(gcov_type); + fwrite(&length, 1, 4, f); + + for(i = 0;i < num_counters; i++) { + topn_counter_t *ctr = &counters[i]; + + /* Write the total */ + fwrite(&ctr->total, 1, 8, f); + + /* Write the amount of pairs */ + fwrite(&ctr->list_size, 1, 8, f); + + /* Write value-count pairs for this counter */ + topn_ctr_t *entries = &topn_table[counters[i].list_idx]; + + for(j = 0;j < counters[i].list_size; j++) { + fwrite(&entries[j].value, 1, 8, f); + fwrite(&entries[j].count, 1, 8, f); + } + } +}