Files
scriptc/packages/runtime/src/scr_perf_loop.c
T
Chris Tate 4004d4c9d0 Answer 511 for an empty event-loop-delay histogram's percentiles
Node creates monitorEventLoopDelay's histogram with a lowest discernible
value of 1000 ns, so an empty histogram's percentile() returns 511, the
highest value its first bucket holds. The runtime returned 0.

6397-event-loop-metrics.ts now skips a sampling window that has no
recorded delta. A delta needs two samples after reset(), and a loop that
wakes late runs only one sample in a window, so on a loaded machine the
program printed an empty window under Node and compiled alike. The
program also prints percentile(50) after each reset.
2026-10-21 00:06:54 -05:00

334 lines
12 KiB
C

/* node:perf_hooks event-loop metrics: performance.eventLoopUtilization()
* and monitorEventLoopDelay()'s IntervalHistogram.
*
* Event-loop utilization. Node measures idle time as the time its loop
* spends blocked in the poll phase. A compiled program's main thread is
* busy exactly while it runs on the CPU (or blocks in a synchronous call),
* so the thread's CPU time stands in for Node's active time: idle is the
* wall time since the loop started minus that CPU time. The loop start is
* marked by the emitted main just before the loop runs, and only in
* programs that ask for the metric; before it, utilization is all zeros,
* like Node during the main module's synchronous evaluation.
*
* Event-loop delay. Each histogram samples on an unref'd interval at its
* resolution and records the time between samples in nanoseconds, like
* Node's ELDHistogram (Histogram::RecordDelta). Values land in an
* HdrHistogram-shaped counts array (lowest 1, three significant figures:
* 2048 sub-buckets per power of two), so min, max, mean, stddev and
* percentiles answer the bucket values Node's hdr_histogram answers. */
#include "scr_runtime.h"
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
#endif
/* ── event-loop utilization ─────────────────────────────────────────── */
static SCR_TL double scr_perf_loop_start_ms = 0; /* performance.now() at loop start */
static SCR_TL double scr_perf_loop_cpu0_ms = 0; /* thread CPU time at loop start */
/* The calling thread's CPU time in milliseconds; 0 where unavailable. */
static double scr_perf_thread_cpu_ms(void) {
#if defined(_WIN32)
FILETIME created, exited, kernel, user;
if (!GetThreadTimes(GetCurrentThread(), &created, &exited, &kernel, &user)) return 0;
ULARGE_INTEGER k, u;
k.LowPart = kernel.dwLowDateTime;
k.HighPart = kernel.dwHighDateTime;
u.LowPart = user.dwLowDateTime;
u.HighPart = user.dwHighDateTime;
return (double)(k.QuadPart + u.QuadPart) / 1e4; /* 100ns units */
#elif defined(CLOCK_THREAD_CPUTIME_ID) && !defined(__wasi__)
struct timespec ts;
if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts) != 0) return 0;
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
#else
return 0;
#endif
}
void scr_perf_loop_mark(void) {
if (scr_perf_loop_start_ms > 0) return;
scr_perf_loop_cpu0_ms = scr_perf_thread_cpu_ms();
double now = scr_perf_now();
/* Node's loopStart is positive once set; a mark at the very first
* microsecond still counts as started. */
scr_perf_loop_start_ms = now > 0 ? now : 1e-6;
}
double scr_perf_loop_start(void) { return scr_perf_loop_start_ms; }
double scr_perf_idle_time(void) {
if (scr_perf_loop_start_ms <= 0) return 0;
double wall = scr_perf_now() - scr_perf_loop_start_ms;
double busy = scr_perf_thread_cpu_ms() - scr_perf_loop_cpu0_ms;
double idle = wall - busy;
if (idle < 0) idle = 0;
if (idle > wall) idle = wall;
return idle;
}
/* ── HdrHistogram-shaped counts (lowest 1, 3 significant figures) ───── */
#define SCR_HDR_SUB_BUCKET_HALF_MAG 10
#define SCR_HDR_SUB_BUCKET_HALF (1 << SCR_HDR_SUB_BUCKET_HALF_MAG) /* 1024 */
#define SCR_HDR_SUB_BUCKET_COUNT (SCR_HDR_SUB_BUCKET_HALF * 2) /* 2048 */
#define SCR_HDR_SUB_BUCKET_MASK ((int64_t)SCR_HDR_SUB_BUCKET_COUNT - 1)
typedef struct {
int64_t *counts; /* owned, grown on demand */
size_t counts_len;
int64_t total;
int64_t min_value; /* INT64_MAX while empty */
int64_t max_value; /* 0 while empty */
double count; /* Node's count_ (successful records) */
uint64_t prev_ns; /* RecordDelta's previous sample; 0 = none */
double resolution; /* ms */
double timer; /* the sampling interval's handle while enabled, else 0 */
ScrClosure *tick; /* owned: the sampling callback, identifies this histogram */
bool enabled;
} ScrEld;
static int scr_hdr_bucket_index(int64_t value) {
int pow2ceiling = 64 - __builtin_clzll((uint64_t)(value | SCR_HDR_SUB_BUCKET_MASK));
return pow2ceiling - (SCR_HDR_SUB_BUCKET_HALF_MAG + 1);
}
static size_t scr_hdr_counts_index(int64_t value) {
int bucket = scr_hdr_bucket_index(value);
int64_t sub = value >> bucket;
return (size_t)(((int64_t)(bucket + 1) << SCR_HDR_SUB_BUCKET_HALF_MAG) + (sub - SCR_HDR_SUB_BUCKET_HALF));
}
static int64_t scr_hdr_value_at_index(size_t index) {
int bucket = (int)(index >> SCR_HDR_SUB_BUCKET_HALF_MAG) - 1;
int64_t sub = (int64_t)(index & (SCR_HDR_SUB_BUCKET_HALF - 1)) + SCR_HDR_SUB_BUCKET_HALF;
if (bucket < 0) {
sub -= SCR_HDR_SUB_BUCKET_HALF;
bucket = 0;
}
return sub << bucket;
}
static int64_t scr_hdr_range_size(int64_t value) {
int bucket = scr_hdr_bucket_index(value);
int64_t sub = value >> bucket;
int adjusted = sub >= SCR_HDR_SUB_BUCKET_COUNT ? bucket + 1 : bucket;
return (int64_t)1 << adjusted;
}
static int64_t scr_hdr_lowest_equivalent(int64_t value) {
int bucket = scr_hdr_bucket_index(value);
int64_t sub = value >> bucket;
return sub << bucket;
}
static int64_t scr_hdr_highest_equivalent(int64_t value) {
return scr_hdr_lowest_equivalent(value) + scr_hdr_range_size(value) - 1;
}
static int64_t scr_hdr_median_equivalent(int64_t value) {
return scr_hdr_lowest_equivalent(value) + (scr_hdr_range_size(value) >> 1);
}
/* Newton's square root: keeps this unit free of a libm reference. */
static double scr_eld_sqrt(double x) {
if (!(x > 0)) return x == 0 ? 0 : NAN;
double r = x >= 1 ? x : 1;
for (int i = 0; i < 64; i++) {
double next = 0.5 * (r + x / r);
if (next == r) break;
r = next;
}
return r;
}
static void scr_eld_record(ScrEld *h, int64_t value) {
if (value < 0) return;
size_t index = scr_hdr_counts_index(value);
if (index >= h->counts_len) {
size_t len = h->counts_len ? h->counts_len : SCR_HDR_SUB_BUCKET_COUNT;
while (len <= index) len *= 2;
int64_t *grown = realloc(h->counts, len * sizeof(int64_t));
if (grown == NULL) return;
for (size_t i = h->counts_len; i < len; i++) grown[i] = 0;
h->counts = grown;
h->counts_len = len;
}
h->counts[index]++;
h->total++;
if (value != 0 && value < h->min_value) h->min_value = value;
if (value > h->max_value) h->max_value = value;
h->count += 1;
}
static void scr_eld_clear(ScrEld *h) {
for (size_t i = 0; i < h->counts_len; i++) h->counts[i] = 0;
h->total = 0;
h->min_value = INT64_MAX;
h->max_value = 0;
h->count = 0;
h->prev_ns = 0;
}
/* ── monitorEventLoopDelay ──────────────────────────────────────────── */
static SCR_TL ScrEld **scr_elds;
static SCR_TL size_t scr_elds_len;
static ScrEld *scr_eld_of(double handle) {
if (!(handle >= 1) || handle > (double)scr_elds_len) return NULL;
return scr_elds[(size_t)handle - 1];
}
static void scr_eld_tick(ScrClosure *self) {
for (size_t i = 0; i < scr_elds_len; i++) {
ScrEld *h = scr_elds[i];
if (h->tick != self) continue;
uint64_t now = scr_hrtime_ns();
if (h->prev_ns > 0 && now > h->prev_ns) scr_eld_record(h, (int64_t)(now - h->prev_ns));
h->prev_ns = now;
return;
}
}
/* Histograms live for the thread's lifetime (Node frees them with their
* JS object); release them when the script ends so their callbacks and
* counts do not outlive it. */
static void scr_eld_cleanup(void) {
for (size_t i = 0; i < scr_elds_len; i++) {
ScrEld *h = scr_elds[i];
scr_closure_release(h->tick);
free(h->counts);
free(h);
}
free(scr_elds);
scr_elds = NULL;
scr_elds_len = 0;
}
/* Node's validateInteger(resolution, 'options.resolution', 1): a catchable
* RangeError (ERR_OUT_OF_RANGE) for a non-integer or a value out of range,
* with the value as util.inspect renders it ("-0" included). */
static bool scr_eld_resolution_ok(double resolution) {
bool integer = isfinite(resolution) && trunc(resolution) == resolution;
if (integer && resolution >= 1 && resolution <= 9007199254740991.0) return true;
char received[48];
if (resolution == 0 && signbit(resolution))
memcpy(received, "-0", 3);
else
scr_num_received(resolution, received);
char msg[192];
int len = snprintf(msg, sizeof msg,
"The value of \"options.resolution\" is out of range. It must be %s. Received %s",
integer ? ">= 1 && <= 9007199254740991" : "an integer", received);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
}
double scr_eld_new(double resolution) {
if (!scr_eld_resolution_ok(resolution)) return 0;
if (scr_elds_len == 0 && scr_elds == NULL) scr_atexit(scr_eld_cleanup);
ScrEld *h = calloc(1, sizeof(ScrEld));
ScrEld **grown = realloc(scr_elds, (scr_elds_len + 1) * sizeof(ScrEld *));
if (h == NULL || grown == NULL) {
free(h);
if (grown != NULL) scr_elds = grown;
return 0;
}
scr_elds = grown;
h->min_value = INT64_MAX;
h->resolution = resolution;
h->tick = scr_closure_new((void *)scr_eld_tick, 0);
scr_elds[scr_elds_len++] = h;
return (double)scr_elds_len;
}
bool scr_eld_enable(double handle) {
ScrEld *h = scr_eld_of(handle);
if (h == NULL || h->enabled) return false;
h->enabled = true;
h->timer = scr_set_interval(scr_closure_retain(h->tick), h->resolution);
scr_timer_unref(h->timer);
return true;
}
bool scr_eld_disable(double handle) {
ScrEld *h = scr_eld_of(handle);
if (h == NULL || !h->enabled) return false;
h->enabled = false;
scr_clear_interval(h->timer);
h->timer = 0;
return true;
}
void scr_eld_reset(double handle) {
ScrEld *h = scr_eld_of(handle);
if (h != NULL) scr_eld_clear(h);
}
/* Node's event-loop-delay histogram has a lowest discernible value of 1000 ns
* (Histogram::Options { 1000 }): its smallest bucket spans 0..511, so an empty
* histogram answers 511 for every percentile. Recorded deltas are at least the
* resolution (>= 1 ms), where that layout and this one share bucket bounds. */
#define SCR_ELD_EMPTY_PERCENTILE 511
/* hdr_value_at_percentile; the compiler checked Node's 0 < p <= 100 range. */
double scr_eld_percentile(double handle, double percentile) {
ScrEld *h = scr_eld_of(handle);
if (h == NULL || h->total == 0) return SCR_ELD_EMPTY_PERCENTILE;
double requested = percentile < 100.0 ? percentile : 100.0;
int64_t wanted = (int64_t)((requested / 100.0) * (double)h->total + 0.5);
if (wanted < 1) wanted = 1;
int64_t seen = 0;
for (size_t i = 0; i < h->counts_len; i++) {
seen += h->counts[i];
if (seen >= wanted) return (double)scr_hdr_highest_equivalent(scr_hdr_value_at_index(i));
}
return 0;
}
/* which: 0 min, 1 max, 2 mean, 3 stddev, 4 count, 5 exceeds. */
double scr_eld_stat(double handle, double which) {
ScrEld *h = scr_eld_of(handle);
int w = (int)which;
if (h == NULL) return w == 0 ? (double)INT64_MAX : w >= 2 && w <= 3 ? NAN : 0;
switch (w) {
case 0:
if (h->counts_len > 0 && h->counts[0] > 0) return 0;
return h->min_value == INT64_MAX ? (double)INT64_MAX
: (double)scr_hdr_lowest_equivalent(h->min_value);
case 1:
return h->max_value == 0 ? 0 : (double)scr_hdr_highest_equivalent(h->max_value);
case 2:
case 3: {
if (h->total == 0) return NAN;
double sum = 0;
for (size_t i = 0; i < h->counts_len; i++) {
if (h->counts[i] == 0) continue;
sum += (double)h->counts[i] * (double)scr_hdr_median_equivalent(scr_hdr_value_at_index(i));
}
double mean = sum / (double)h->total;
if (w == 2) return mean;
double geo = 0;
for (size_t i = 0; i < h->counts_len; i++) {
if (h->counts[i] == 0) continue;
double dev = (double)scr_hdr_median_equivalent(scr_hdr_value_at_index(i)) - mean;
geo += dev * dev * (double)h->counts[i];
}
return scr_eld_sqrt(geo / (double)h->total);
}
case 4:
return h->count;
default:
return 0;
}
}