ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
lib/stats/stats.c
Go to the documentation of this file.
2#include <libplatform/atomic.h>
3#include <ascii-chat/common.h>
4#include <libplatform/mutex.h>
6#include <string.h>
7#include <stdatomic.h>
8#include <libplatform/thread.h>
9
10// Each duration recorder owns a shard. Readers use sequence checks to copy a
11// complete observation without making capture, conversion, and output contend.
12#define STATS_DURATION_SHARDS 16
13
19
31
51
68
69static atomic_t next_scope_id = {0};
70
72 if (!out_scope)
73 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing stats scope output");
74 *out_scope = NULL;
75 if (!capabilities)
76 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing stats capabilities");
77 stats_scope_t *scope = SAFE_CALLOC(1, sizeof(*scope), stats_scope_t *);
78 if (!scope)
79 return SET_ERRNO(ERROR_MEMORY, "Cannot allocate stats scope");
80 if (mutex_init(&scope->snapshot_mutex, "stats_snapshot") != 0) {
81 SAFE_FREE(scope);
82 return SET_ERRNO(ERROR_THREAD, "Cannot initialize stats snapshot mutex");
83 }
84 if (mutex_init(&scope->duration_mutex, "stats_duration") != 0) {
85 mutex_destroy(&scope->snapshot_mutex);
86 SAFE_FREE(scope);
87 return SET_ERRNO(ERROR_THREAD, "Cannot initialize stats duration mutex");
88 }
89 scope->capabilities = *capabilities;
90 // Bypass debug atomic instrumentation so recording never recursively collects or allocates.
91 scope->id = atomic_fetch_add_u64_impl(&next_scope_id, 1) + 1;
92 scope->started_ns = time_get_ns();
93 *out_scope = scope;
94 return ASCIICHAT_OK;
95}
96
98 if (!scope)
99 return;
100 mutex_destroy(&scope->duration_mutex);
101 mutex_destroy(&scope->snapshot_mutex);
102 SAFE_FREE(scope);
103}
104
105void stats_scope_set_observer(stats_scope_t *scope, stats_observer_fn observer, void *context) {
106 if (scope) {
107 scope->observer = observer;
108 scope->observer_context = context;
109 }
110}
111
113 if (scope && (unsigned)id < STATS_COUNTER_COUNT && scope->capabilities.counters[id]) {
114 atomic_fetch_add_u64_impl(&scope->counters[id], amount);
115 if (scope->observer)
116 scope->observer(STATS_RECORD_COUNTER, id, amount, scope->observer_context);
117 }
118}
119
121 if (scope && (unsigned)id < STATS_GAUGE_COUNT && scope->capabilities.gauges[id]) {
122 atomic_store_u64_impl(&scope->gauges[id], value);
123 if (scope->observer)
124 scope->observer(STATS_RECORD_GAUGE, id, value, scope->observer_context);
125 }
126}
127
128static uint64_t duration_add(uint64_t a, uint64_t b) {
129 return a > UINT64_MAX - b ? b - (UINT64_MAX - a) - 1 : a + b;
130}
131
132static stats_duration_shard_t *duration_shard(stats_scope_t *scope) {
133 static _Thread_local uint64_t cached_scope_id;
134 static _Thread_local stats_duration_shard_t *cached_shard;
135 if (cached_scope_id == scope->id)
136 return cached_shard;
137 uint64_t owner = thread_current_id();
138 cached_scope_id = scope->id;
139 cached_shard = NULL;
140 for (size_t i = 0; i < STATS_DURATION_SHARDS; ++i) {
141 uint64_t expected = 0;
142 if (atomic_load(&scope->shards[i].owner) == owner ||
143 atomic_compare_exchange_strong(&scope->shards[i].owner, &expected, owner)) {
144 cached_shard = &scope->shards[i];
145 break;
146 }
147 }
148 return cached_shard;
149}
150
152 if (!scope || (unsigned)id >= STATS_DURATION_COUNT || !scope->capabilities.durations[id])
153 return;
154 stats_duration_shard_t *shard = duration_shard(scope);
155 if (shard) {
156 stats_duration_cell_t *cell = &shard->durations[id];
157 uint64_t sequence = atomic_load(&cell->sequence);
158 atomic_store(&cell->sequence, duration_add(sequence, 1));
159 uint64_t observations = atomic_load(&cell->observations);
160 if (!observations || elapsed_ns < atomic_load(&cell->min_ns))
161 atomic_store(&cell->min_ns, elapsed_ns);
162 if (elapsed_ns > atomic_load(&cell->max_ns))
163 atomic_store(&cell->max_ns, elapsed_ns);
164 atomic_store(&cell->last_ns, elapsed_ns);
165 atomic_store(&cell->recorded_ns, time_get_ns());
166 atomic_store(&cell->observations, duration_add(observations, 1));
167 atomic_store(&cell->total_ns, duration_add(atomic_load(&cell->total_ns), elapsed_ns));
168 atomic_store(&cell->sequence, duration_add(sequence, 2));
169 if (scope->observer)
170 scope->observer(STATS_RECORD_DURATION, id, elapsed_ns, scope->observer_context);
171 return;
172 }
173 // Unusually many lifetime producers retain the bounded, coherent fallback.
174 if (mutex_lock(&scope->duration_mutex) != 0)
175 return;
176 stats_duration_snapshot_t *duration = &scope->durations[id];
177 duration->last_ns = elapsed_ns;
178 scope->duration_recorded_ns[id] = time_get_ns();
179 if (!duration->observations || elapsed_ns < duration->min_ns)
180 duration->min_ns = elapsed_ns;
181 if (elapsed_ns > duration->max_ns)
182 duration->max_ns = elapsed_ns;
183 duration->observations = duration->observations == UINT64_MAX ? 0 : duration->observations + 1;
184 // Unsigned wrap is intentional; an observed decrease causes samplers to rebaseline.
185 duration->total_ns = duration->total_ns > UINT64_MAX - elapsed_ns ? elapsed_ns - (UINT64_MAX - duration->total_ns) - 1
186 : duration->total_ns + elapsed_ns;
187 mutex_unlock(&scope->duration_mutex);
188 if (scope->observer)
189 scope->observer(STATS_RECORD_DURATION, id, elapsed_ns, scope->observer_context);
190}
191
193 if (!scope || !out)
194 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing stats scope or snapshot output");
195 // Logical constness: collection is unchanged; only snapshot synchronization is mutable.
196 stats_scope_t *mutable_scope = (stats_scope_t *)scope;
197 if (mutex_lock(&mutable_scope->snapshot_mutex) != 0)
198 return SET_ERRNO(ERROR_THREAD, "Cannot lock stats snapshot");
199 memset(out, 0, sizeof(*out));
200 out->scope_id = scope->id;
201 out->started_ns = scope->started_ns;
202 out->capabilities = scope->capabilities;
203 for (size_t i = 0; i < STATS_COUNTER_COUNT; ++i)
204 out->counters[i] = atomic_load_u64_impl(&scope->counters[i]);
205 for (size_t i = 0; i < STATS_GAUGE_COUNT; ++i)
206 out->gauges[i] = atomic_load_u64_impl(&scope->gauges[i]);
207 if (mutex_lock(&mutable_scope->duration_mutex) != 0) {
208 mutex_unlock(&mutable_scope->snapshot_mutex);
209 return SET_ERRNO(ERROR_THREAD, "Cannot lock stats durations");
210 }
211 memcpy(out->durations, scope->durations, sizeof(out->durations));
212 uint64_t recorded_ns[STATS_DURATION_COUNT];
213 memcpy(recorded_ns, scope->duration_recorded_ns, sizeof(recorded_ns));
214 mutex_unlock(&mutable_scope->duration_mutex);
215 for (size_t i = 0; i < STATS_DURATION_SHARDS; ++i) {
216 if (!atomic_load(&scope->shards[i].owner))
217 continue;
218 for (size_t id = 0; id < STATS_DURATION_COUNT; ++id) {
219 const stats_duration_cell_t *cell = &scope->shards[i].durations[id];
221 uint64_t stamp, before;
222 for (;;) {
223 before = atomic_load(&cell->sequence);
224 if (before & 1)
225 continue;
226 sample.observations = atomic_load(&cell->observations);
227 sample.total_ns = atomic_load(&cell->total_ns);
228 sample.min_ns = atomic_load(&cell->min_ns);
229 sample.max_ns = atomic_load(&cell->max_ns);
230 sample.last_ns = atomic_load(&cell->last_ns);
231 stamp = atomic_load(&cell->recorded_ns);
232 if (before == atomic_load(&cell->sequence))
233 break;
234 }
235 if (!sample.observations)
236 continue;
237 stats_duration_snapshot_t *total = &out->durations[id];
238 if (!total->observations || sample.min_ns < total->min_ns)
239 total->min_ns = sample.min_ns;
240 if (sample.max_ns > total->max_ns)
241 total->max_ns = sample.max_ns;
242 if (stamp >= recorded_ns[id]) {
243 recorded_ns[id] = stamp;
244 total->last_ns = sample.last_ns;
245 }
246 total->observations = duration_add(total->observations, sample.observations);
247 total->total_ns = duration_add(total->total_ns, sample.total_ns);
248 }
249 }
250 out->sampled_ns = time_get_ns();
251 mutex_unlock(&mutable_scope->snapshot_mutex);
252 return ASCIICHAT_OK;
253}
254
256 if (!out_sampler)
257 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing stats sampler output");
258 *out_sampler = NULL;
259 if (!config || !config->resolution_ns || config->window_ns < config->resolution_ns)
260 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid stats sampling interval");
261 uint64_t slots = config->window_ns / config->resolution_ns + (config->window_ns % config->resolution_ns != 0);
262 if (slots > 4096)
263 return SET_ERRNO(ERROR_INVALID_PARAM, "Stats sampler exceeds 4096 history intervals");
264 stats_sampler_t *sampler = SAFE_CALLOC(1, sizeof(*sampler), stats_sampler_t *);
265 if (!sampler)
266 return SET_ERRNO(ERROR_MEMORY, "Cannot allocate stats sampler");
267 sampler->capacity = (size_t)slots + 2;
268 sampler->history = SAFE_CALLOC(sampler->capacity, sizeof(*sampler->history), stats_snapshot_t *);
269 if (!sampler->history) {
270 SAFE_FREE(sampler);
271 return SET_ERRNO(ERROR_MEMORY, "Cannot allocate stats history");
272 }
273 sampler->config = *config;
274 *out_sampler = sampler;
275 return ASCIICHAT_OK;
276}
277
278static bool stats_baseline_changed(const stats_snapshot_t *old, const stats_snapshot_t *current) {
279 if (old->scope_id != current->scope_id || current->sampled_ns <= old->sampled_ns)
280 return true;
281 for (size_t i = 0; i < STATS_COUNTER_COUNT; ++i)
282 if (old->capabilities.counters[i] != current->capabilities.counters[i] || current->counters[i] < old->counters[i])
283 return true;
284 for (size_t i = 0; i < STATS_GAUGE_COUNT; ++i)
285 if (old->capabilities.gauges[i] != current->capabilities.gauges[i])
286 return true;
287 for (size_t i = 0; i < STATS_DURATION_COUNT; ++i)
288 if (old->capabilities.durations[i] != current->capabilities.durations[i] ||
289 current->durations[i].observations < old->durations[i].observations ||
290 current->durations[i].total_ns < old->durations[i].total_ns)
291 return true;
292 return false;
293}
294
296 stats_rates_t *out_rates) {
297 if (!sampler || !snapshot || !out_rates)
298 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing stats sampler, snapshot, or rates output");
299 memset(out_rates, 0, sizeof(*out_rates));
300 if (!sampler->count || stats_baseline_changed(&sampler->latest, snapshot)) {
301 sampler->head = 0;
302 sampler->count = 1;
303 sampler->history[0] = *snapshot;
304 sampler->latest = *snapshot;
305 return ASCIICHAT_OK;
306 }
307 uint64_t cutoff =
308 snapshot->sampled_ns > sampler->config.window_ns ? snapshot->sampled_ns - sampler->config.window_ns : 0;
309 while (sampler->count > 1) {
310 size_t next = (sampler->head + 1) % sampler->capacity;
311 if (sampler->history[next].sampled_ns > cutoff)
312 break;
313 sampler->head = next;
314 sampler->count--;
315 }
316 const stats_snapshot_t *baseline = &sampler->history[sampler->head];
317 out_rates->elapsed_ns = snapshot->sampled_ns - baseline->sampled_ns;
318 out_rates->ready = true;
319 for (size_t i = 0; i < STATS_COUNTER_COUNT; ++i)
320 if (snapshot->capabilities.counters[i])
321 out_rates->counters_per_second[i] =
322 (double)(snapshot->counters[i] - baseline->counters[i]) * 1e9 / (double)out_rates->elapsed_ns;
323 for (size_t i = 0; i < STATS_DURATION_COUNT; ++i) {
324 uint64_t observations = snapshot->durations[i].observations - baseline->durations[i].observations;
325 if (snapshot->capabilities.durations[i] && observations) {
326 out_rates->duration_valid[i] = true;
327 out_rates->duration_mean_ns[i] =
328 (double)(snapshot->durations[i].total_ns - baseline->durations[i].total_ns) / (double)observations;
329 }
330 }
331 size_t tail = (sampler->head + sampler->count - 1) % sampler->capacity;
332 if (snapshot->sampled_ns - sampler->history[tail].sampled_ns >= sampler->config.resolution_ns) {
333 size_t next = (tail + 1) % sampler->capacity;
334 sampler->history[next] = *snapshot;
335 if (sampler->count == sampler->capacity)
336 sampler->head = (sampler->head + 1) % sampler->capacity;
337 else
338 sampler->count++;
339 }
340 sampler->latest = *snapshot;
341 return ASCIICHAT_OK;
342}
343
345 if (!sampler)
346 return;
347 SAFE_FREE(sampler->history);
348 SAFE_FREE(sampler);
349}
#define SET_ERRNO(code,...)
⚙️ Common definitions, error codes, macros, and types shared throughout the application
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
unsigned long long uint64_t
Definition common.h:63
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
@ ERROR_THREAD
void(* stats_observer_fn)(stats_record_kind_t kind, unsigned id, uint64_t value, void *context)
struct stats_sampler stats_sampler_t
struct stats_scope stats_scope_t
void stats_scope_destroy(stats_scope_t *scope)
void stats_scope_set_observer(stats_scope_t *scope, stats_observer_fn observer, void *context)
void stats_counter_add(stats_scope_t *scope, stats_counter_id_t id, uint64_t amount)
void stats_duration_record(stats_scope_t *scope, stats_duration_id_t id, uint64_t elapsed_ns)
void stats_sampler_destroy(stats_sampler_t *sampler)
asciichat_error_t stats_scope_snapshot(const stats_scope_t *scope, stats_snapshot_t *out)
stats sampler config keeps the values that move through the subsystem: window_ns, resolution_ns....
#define STATS_DURATION_SHARDS
asciichat_error_t stats_scope_create(const stats_capabilities_t *capabilities, stats_scope_t **out_scope)
asciichat_error_t stats_sampler_update(stats_sampler_t *sampler, const stats_snapshot_t *snapshot, stats_rates_t *out_rates)
void stats_gauge_set(stats_scope_t *scope, stats_gauge_id_t id, uint64_t value)
asciichat_error_t stats_sampler_create(const stats_sampler_config_t *config, stats_sampler_t **out_sampler)
static bool size_t gotel_query_t * out
Definition queries.h:8
🔤 String Manipulation and Shell Escaping Utilities
stats descriptor is intentionally a small, concrete record. Fields such as name, unit,...
bool counters[STATS_COUNTER_COUNT]
bool durations[STATS_DURATION_COUNT]
_Atomic uint64_t min_ns
_Atomic uint64_t recorded_ns
_Atomic uint64_t max_ns
_Atomic uint64_t last_ns
_Atomic uint64_t observations
_Atomic uint64_t total_ns
_Atomic uint64_t sequence
For stats duration cell, this is the live record that ties sequence, observations,...
stats_duration_cell_t durations[STATS_DURATION_COUNT]
_Atomic uint64_t owner
double duration_mean_ns[STATS_DURATION_COUNT]
bool duration_valid[STATS_DURATION_COUNT]
double counters_per_second[STATS_COUNTER_COUNT]
This is the handoff point for stats scope. Callers fill observer, observer_context,...
stats_snapshot_t latest
stats_sampler_config_t config
stats_snapshot_t * history
stats duration shard keeps the values that move through the subsystem: owner. Some members are pointe...
stats_duration_shard_t shards[16]
mutex_t duration_mutex
mutex_t snapshot_mutex
uint64_t started_ns
atomic_t counters[STATS_COUNTER_COUNT]
stats_capabilities_t capabilities
stats_observer_fn observer
stats_duration_snapshot_t durations[STATS_DURATION_COUNT]
atomic_t gauges[STATS_GAUGE_COUNT]
void * observer_context
uint64_t duration_recorded_ns[STATS_DURATION_COUNT]
stats_capabilities_t capabilities
uint64_t counters[STATS_COUNTER_COUNT]
stats_duration_snapshot_t durations[STATS_DURATION_COUNT]
⏱️ High-precision timing utilities using sokol_time.h and uthash