OXYGENHUB
# 9120e87480a6 enhanced-x16-emulator / src / perf_budget.c
24.6 KiB · C · last changed chore(): initial commit 25 days ago source blame history raw
1
// Commander X16 Emulator — guest performance budget accounting.
2
// See perf_budget.h for what this measures and why it has to classify cycles
3
// rather than merely count them.
4
5
#include "perf_budget.h"
6
7
#include <stdlib.h>
8
#include <string.h>
9
#include <stdio.h>
10
11
// Rounding and absolute value, done by hand rather than with <math.h>.
12
//
13
// perf_budget.c is linked into unit tests that link nothing else -- that is the
14
// whole point of it taking the machine as arguments -- and it is also linked
15
// into memory.c's test targets. lrintf() and fabsf() are intrinsics on MSVC but
16
// real libm symbols on glibc, so using them meant every one of those targets
17
// had to grow a -lm it does not otherwise need. Both values rounded here are
18
// positive, so the naive form is exact and obviously correct.
19
static uint32_t
20
round_pos(float v)
21
{
22
	return (uint32_t)(v + 0.5f);
23
}
24
25
static float
26
abs_f(float v)
27
{
28
	return v < 0.0f ? -v : v;
29
}
30
31
bool perf_budget_enabled = false;
32
33
// ─── Configuration ──────────────────────────────────────────────────────────
34
35
static uint32_t cfg_machine_khz      = 8000;
36
static uint32_t cfg_cycles_per_frame = 134400; // 8 MHz, VGA
37
static uint16_t cfg_scanlines        = 525;
38
static float    cfg_target_fps       = 60.0f;
39
40
static perf_idle_mode_t cfg_idle_mode = PERF_IDLE_AUTO;
41
42
// Derived from the above by recompute_budget().
43
static int      budget_frames = 1;      // vsyncs the budget spans
44
static uint32_t budget_cycles = 134400;
45
46
// ─── The ring ───────────────────────────────────────────────────────────────
47
48
static perf_frame_t *ring     = NULL;
49
static uint32_t     *scratch  = NULL; // percentile workspace, sized with the ring
50
static int           capacity = PERF_DEFAULT_CAPACITY;
51
static int           head     = 0; // next slot to write
52
static int           count    = 0; // valid samples
53
static uint32_t      total_frames = 0;
54
55
// ─── The frame in progress ──────────────────────────────────────────────────
56
57
static perf_frame_t cur;
58
static uint32_t     cur_zone[PERF_MAX_ZONES];
59
60
// Trailing work history, for budgets that span more than one frame. Only the
61
// last `budget_frames` entries matter and budget_frames is small, so a tiny
62
// ring of its own is cheaper and simpler than reaching back into the main one
63
// (which may be empty, or may have been resized, exactly when this is needed).
64
#define PERF_PERIOD_MAX 16
65
static uint32_t period_hist[PERF_PERIOD_MAX];
66
static int      period_pos = 0;
67
68
// ─── Spin detection ─────────────────────────────────────────────────────────
69
70
static uint32_t store_count = 0;
71
72
static struct {
73
	uint16_t anchor;
74
	uint16_t lo, hi;
75
	uint16_t last_pc;
76
	uint32_t stores_at_anchor;
77
	uint32_t pending; // cycles in the pass currently being accumulated
78
	uint8_t  iters;   // clean passes completed
79
	bool     valid;
80
	bool     have_last;
81
} spin;
82
83
// ─── Markers ────────────────────────────────────────────────────────────────
84
85
static bool    marker_mode = false; // a marker has been seen this session
86
static bool    marker_idle = false; // $F1 seen; work resumes at $F0
87
static int8_t  marker_stack[PERF_MARKER_STACK];
88
static int     marker_depth = 0;
89
90
// ─── Zones ──────────────────────────────────────────────────────────────────
91
92
static perf_zone_t zones[PERF_MAX_ZONES];
93
static uint8_t    *zone_of = NULL; // PC -> zone index + 1; 0 means none
94
95
// ─── Overrun accumulator ────────────────────────────────────────────────────
96
97
static perf_overrun_report_t pending_overruns;
98
99
// ────────────────────────────────────────────────────────────────────────────
100
101
static void
102
recompute_budget(void)
103
{
104
	if (cfg_cycles_per_frame == 0)
105
		cfg_cycles_per_frame = 1;
106
107
	const float vsync = perf_budget_vsync_hz();
108
	if (cfg_target_fps <= 0.0f || vsync <= 0.0f) {
109
		budget_frames = 1;
110
		budget_cycles = cfg_cycles_per_frame;
111
		return;
112
	}
113
114
	// Vsyncs per unit of work. Above 1 the work may span several frames; below
115
	// 1 the caller is asking for a routine to fit inside part of one.
116
	const float ratio = vsync / cfg_target_fps;
117
	int         n     = (int)round_pos(ratio);
118
119
	if (n >= 1 && abs_f(ratio - (float)n) <= 0.05f * (float)n) {
120
		// Close enough to a whole number of vsyncs to be what was meant.
121
		if (n > PERF_PERIOD_MAX)
122
			n = PERF_PERIOD_MAX;
123
		budget_frames = n;
124
		budget_cycles = cfg_cycles_per_frame * (uint32_t)n;
125
	} else {
126
		// A fraction of a frame, or an awkward multiple of one. Sum over as
127
		// many whole frames as the ratio covers, and scale the budget exactly.
128
		if (n < 1)
129
			n = 1;
130
		if (n > PERF_PERIOD_MAX)
131
			n = PERF_PERIOD_MAX;
132
		budget_frames = n;
133
		budget_cycles = round_pos((float)cfg_cycles_per_frame * ratio);
134
		if (budget_cycles == 0)
135
			budget_cycles = 1;
136
	}
137
}
138
139
// What a period's work is actually compared against: the budget, plus the
140
// boundary granularity described at PERF_BOUNDARY_SLACK.
141
static uint32_t
142
overrun_threshold(void)
143
{
144
	return budget_cycles + (uint32_t)budget_frames * PERF_BOUNDARY_SLACK;
145
}
146
147
static void
148
spin_reset(uint16_t pc, bool valid)
149
{
150
	spin.anchor           = pc;
151
	spin.lo               = pc;
152
	spin.hi               = pc;
153
	spin.stores_at_anchor = store_count;
154
	spin.pending          = 0;
155
	spin.iters            = 0;
156
	spin.valid            = valid;
157
}
158
159
static void
160
clear_current(void)
161
{
162
	memset(&cur, 0, sizeof cur);
163
	memset(cur_zone, 0, sizeof cur_zone);
164
	spin_reset(0, false);
165
	spin.have_last = false;
166
}
167
168
void
169
perf_budget_configure(uint32_t machine_khz, uint32_t cycles_per_frame, uint16_t scanlines_per_frame)
170
{
171
	if (machine_khz)
172
		cfg_machine_khz = machine_khz;
173
	if (cycles_per_frame)
174
		cfg_cycles_per_frame = cycles_per_frame;
175
	if (scanlines_per_frame)
176
		cfg_scanlines = scanlines_per_frame;
177
	recompute_budget();
178
}
179
180
void
181
perf_budget_set_target_fps(float fps)
182
{
183
	if (fps > 0.0f)
184
		cfg_target_fps = fps;
185
	recompute_budget();
186
}
187
188
float
189
perf_budget_get_target_fps(void)
190
{
191
	return cfg_target_fps;
192
}
193
194
float
195
perf_budget_vsync_hz(void)
196
{
197
	if (cfg_cycles_per_frame == 0)
198
		return 0.0f;
199
	return (float)cfg_machine_khz * 1000.0f / (float)cfg_cycles_per_frame;
200
}
201
202
uint32_t
203
perf_budget_cycles_per_frame(void)
204
{
205
	return cfg_cycles_per_frame;
206
}
207
208
uint32_t
209
perf_budget_cycles_per_scanline(void)
210
{
211
	return cfg_scanlines ? cfg_cycles_per_frame / cfg_scanlines : 0;
212
}
213
214
uint32_t
215
perf_budget_budget_cycles(void)
216
{
217
	return budget_cycles;
218
}
219
220
int
221
perf_budget_frames_per_budget(void)
222
{
223
	return budget_frames;
224
}
225
226
void
227
perf_budget_set_idle_mode(perf_idle_mode_t mode)
228
{
229
	cfg_idle_mode = mode;
230
}
231
232
perf_idle_mode_t
233
perf_budget_get_idle_mode(void)
234
{
235
	return cfg_idle_mode;
236
}
237
238
// ─── Enable / capacity ──────────────────────────────────────────────────────
239
240
static bool
241
alloc_ring(int frames)
242
{
243
	perf_frame_t *nr = (perf_frame_t *)calloc((size_t)frames, sizeof(perf_frame_t));
244
	if (!nr)
245
		return false;
246
	uint32_t *ns = (uint32_t *)calloc((size_t)frames, sizeof(uint32_t));
247
	if (!ns) {
248
		free(nr);
249
		return false;
250
	}
251
	free(ring);
252
	free(scratch);
253
	ring     = nr;
254
	scratch  = ns;
255
	capacity = frames;
256
	head     = 0;
257
	count    = 0;
258
	return true;
259
}
260
261
bool
262
perf_budget_set_enabled(bool enabled)
263
{
264
	if (!enabled) {
265
		perf_budget_enabled = false;
266
		free(ring);
267
		free(scratch);
268
		ring    = NULL;
269
		scratch = NULL;
270
		head = count = 0;
271
		clear_current();
272
		return true;
273
	}
274
275
	if (!ring && !alloc_ring(capacity))
276
		return false;
277
278
	clear_current();
279
	memset(period_hist, 0, sizeof period_hist);
280
	period_pos = 0;
281
	memset(&pending_overruns, 0, sizeof pending_overruns);
282
	perf_budget_enabled = true;
283
	return true;
284
}
285
286
bool
287
perf_budget_is_enabled(void)
288
{
289
	return perf_budget_enabled;
290
}
291
292
static bool arm_want[PERF_OWNER_COUNT];
293
294
bool
295
perf_budget_arm(perf_owner_t owner, bool want)
296
{
297
	if ((int)owner < 0 || (int)owner >= PERF_OWNER_COUNT)
298
		return false;
299
	arm_want[owner] = want;
300
301
	bool any = false;
302
	for (int i = 0; i < PERF_OWNER_COUNT; i++)
303
		any = any || arm_want[i];
304
305
	// Only act on a real change: perf_budget_set_enabled(true) restarts the
306
	// accumulator, so calling it every frame from a panel that is merely still
307
	// open would keep resetting the frame in progress.
308
	if (any == perf_budget_enabled)
309
		return true;
310
	return perf_budget_set_enabled(any);
311
}
312
313
bool
314
perf_budget_owner_wants(perf_owner_t owner)
315
{
316
	if ((int)owner < 0 || (int)owner >= PERF_OWNER_COUNT)
317
		return false;
318
	return arm_want[owner];
319
}
320
321
bool
322
perf_budget_set_capacity(int frames)
323
{
324
	if (frames < 2)
325
		frames = 2;
326
	if (frames == capacity && ring)
327
		return true;
328
	if (!ring) { // not allocated yet; remember it for when profiling starts
329
		capacity = frames;
330
		return true;
331
	}
332
	return alloc_ring(frames);
333
}
334
335
int
336
perf_budget_capacity(void)
337
{
338
	return capacity;
339
}
340
341
void
342
perf_budget_reset(void)
343
{
344
	head = count = 0;
345
	total_frames = 0;
346
	clear_current();
347
	memset(period_hist, 0, sizeof period_hist);
348
	period_pos = 0;
349
	memset(&pending_overruns, 0, sizeof pending_overruns);
350
	store_count = 0;
351
352
	// Marker state goes too. Marker mode suppresses the heuristic entirely, so
353
	// leaving it set would mean a program that used markers once could never
354
	// get automatic detection back -- not by resetting, not by reloading, not
355
	// by anything short of restarting the emulator. Since a guest that is still
356
	// driving markers re-asserts the mode on its very next write, clearing it
357
	// costs that guest nothing and gives every other one its detector back.
358
	marker_mode  = false;
359
	marker_idle  = false;
360
	marker_depth = 0;
361
}
362
363
// ─── The hot path ───────────────────────────────────────────────────────────
364
365
void
366
perf_budget_note_store_(void)
367
{
368
	store_count++;
369
}
370
371
// Track a candidate spin loop, and credit whole passes of a confirmed one to
372
// idle.
373
//
374
// A loop is identified by the BACKWARD JUMP that closes it, not by watching the
375
// PC drift within some window. That distinction matters: keying on drift means
376
// whatever address the walk happens to start at becomes the anchor, so a
377
// compact routine whose whole body fits inside the window is treated as one
378
// enormous "loop" that stores -- and the tight, store-free poll nested inside
379
// it is never seen at all. Anchoring on the branch target instead means each
380
// loop is recognised as itself, however tightly the code around it is packed.
381
//
382
// Classification happens at pass boundaries rather than per instruction so that
383
// a loop is never credited before it has proved itself; the cost is that the
384
// first confirmed pass is not counted, which is a handful of cycles against a
385
// frame of six figures.
386
static void
387
spin_track(uint32_t clocks, uint16_t pc, int irq_depth)
388
{
389
	// A handler is work by definition, and it also perturbs the loop it
390
	// interrupted, so re-anchor rather than trying to resume the pass.
391
	if (irq_depth > 0) {
392
		spin_reset(pc, false);
393
		spin.last_pc   = pc;
394
		spin.have_last = true;
395
		return;
396
	}
397
398
	if (!spin.valid)
399
		spin_reset(pc, true);
400
401
	const bool backward = spin.have_last && pc < spin.last_pc;
402
403
	if (backward) {
404
		if (pc == spin.anchor) {
405
			// The loop being watched has closed another pass.
406
			const bool clean = (spin.stores_at_anchor == store_count) &&
407
			                   ((uint16_t)(spin.hi - spin.lo) <= PERF_SPIN_SPAN);
408
			if (clean) {
409
				if (spin.iters >= 1)
410
					cur.idle += spin.pending;
411
				if (spin.iters < 255)
412
					spin.iters++;
413
			} else {
414
				spin.iters = 0;
415
			}
416
			spin.pending          = 0;
417
			spin.lo               = pc;
418
			spin.hi               = pc;
419
			spin.stores_at_anchor = store_count;
420
		} else {
421
			// A different backward jump: that target is the loop head now.
422
			spin_reset(pc, true);
423
		}
424
	} else {
425
		if (pc < spin.lo)
426
			spin.lo = pc;
427
		if (pc > spin.hi)
428
			spin.hi = pc;
429
	}
430
431
	spin.pending += clocks;
432
	spin.last_pc   = pc;
433
	spin.have_last = true;
434
}
435
436
void
437
perf_budget_step_(uint32_t clocks, uint16_t pc, uint8_t pbank, bool waiting, int irq_depth)
438
{
439
	cur.total += clocks;
440
	cur.instructions++;
441
	if (irq_depth > 0)
442
		cur.irq += clocks;
443
444
	// Attribution. An open marker region wins over an address zone: the guest
445
	// saying "this is the sprite update" is better evidence than a range
446
	// somebody drew around what they thought the sprite update was.
447
	int  zone   = -1;
448
	bool z_idle = false;
449
	if (marker_depth > 0) {
450
		zone = marker_stack[marker_depth - 1];
451
	} else if (zone_of) {
452
		const uint8_t t = zone_of[pc];
453
		if (t) {
454
			const perf_zone_t *z = &zones[t - 1];
455
			if (z->bank < 0 || z->bank == (int16_t)pbank)
456
				zone = t - 1;
457
		}
458
	}
459
	if (zone >= 0 && zone < PERF_MAX_ZONES) {
460
		cur_zone[zone] += clocks;
461
		z_idle = zones[zone].is_idle;
462
	}
463
464
	// Idle classification, highest precedence first.
465
	if (marker_mode) {
466
		// The guest is telling us. Do not also guess: an explicit statement
467
		// that beats the heuristic is the whole point of the marker layer.
468
		if (marker_idle || z_idle)
469
			cur.idle += clocks;
470
		spin_reset(pc, false);
471
	} else if (z_idle) {
472
		cur.idle += clocks;
473
		spin_reset(pc, false);
474
	} else if (waiting && cfg_idle_mode != PERF_IDLE_NONE) {
475
		cur.idle += clocks; // WAI: unambiguous, and free to detect
476
		spin_reset(pc, false);
477
	} else if (cfg_idle_mode == PERF_IDLE_AUTO) {
478
		spin_track(clocks, pc, irq_depth);
479
	} else {
480
		spin_reset(pc, false);
481
	}
482
483
	// A headless run has no video_step() to raise a frame, so nothing would
484
	// ever close the accumulator and the module would report one ever-growing
485
	// frame. Close it synthetically once it has run well past a frame's worth.
486
	// The threshold is deliberately generous: a machine that does produce
487
	// frames must never reach it, however badly it is overrunning its budget.
488
	if (cur.total >= cfg_cycles_per_frame * 4u)
489
		perf_budget_frame_end(0);
490
}
491
492
void
493
perf_budget_frame_end(uint32_t host_us)
494
{
495
	if (!perf_budget_enabled || !ring)
496
		return;
497
498
	// A region left open by the guest is closed here rather than carried, so a
499
	// missing leave costs one frame instead of every frame after it.
500
	marker_depth = 0;
501
502
	const uint32_t work = cur.total - cur.idle;
503
504
	period_hist[period_pos] = work;
505
	period_pos              = (period_pos + 1) % PERF_PERIOD_MAX;
506
507
	uint32_t period = 0;
508
	for (int i = 0; i < budget_frames; i++) {
509
		const int idx = (period_pos - 1 - i + 2 * PERF_PERIOD_MAX) % PERF_PERIOD_MAX;
510
		period += period_hist[idx];
511
	}
512
513
	cur.frame       = total_frames;
514
	cur.host_us     = host_us;
515
	cur.period_work = period;
516
	for (int i = 0; i < PERF_RING_ZONES; i++)
517
		cur.zone[i] = cur_zone[i];
518
519
	ring[head] = cur;
520
	head       = (head + 1) % capacity;
521
	if (count < capacity)
522
		count++;
523
	total_frames++;
524
525
	if (period > overrun_threshold()) {
526
		const uint32_t over = period - budget_cycles;
527
		pending_overruns.overruns++;
528
		if (over > pending_overruns.worst_overrun) {
529
			pending_overruns.worst_overrun = over;
530
			pending_overruns.worst_frame   = cur.frame;
531
			pending_overruns.worst_utilization =
532
			    budget_cycles ? 100.0f * (float)period / (float)budget_cycles : 0.0f;
533
		}
534
	}
535
	pending_overruns.frames++;
536
537
	clear_current();
538
}
539
540
// ─── Markers ────────────────────────────────────────────────────────────────
541
542
// Find (or claim) the zone slot standing for marker region `id`.
543
static int
544
marker_zone(uint8_t id)
545
{
546
	int free_slot = -1;
547
	for (int i = 0; i < PERF_MAX_ZONES; i++) {
548
		if (zones[i].used && zones[i].is_marker && zones[i].marker_id == id)
549
			return i;
550
		if (!zones[i].used && free_slot < 0)
551
			free_slot = i;
552
	}
553
	if (free_slot < 0)
554
		return -1; // table full: the region still counts as work, just unnamed
555
556
	perf_zone_t *z = &zones[free_slot];
557
	memset(z, 0, sizeof *z);
558
	snprintf(z->name, sizeof z->name, "region %u", (unsigned)id);
559
	z->bank      = -1;
560
	z->is_marker = true;
561
	z->marker_id = id;
562
	z->used      = true;
563
	return free_slot;
564
}
565
566
void
567
perf_budget_marker_write(uint8_t value)
568
{
569
	marker_mode = true;
570
571
	if (value == 0xFE) {
572
		perf_budget_reset();
573
		marker_mode = true; // the reset cleared it; the guest is still driving
574
		marker_idle = false;
575
		return;
576
	}
577
	if (value == 0xF0) {
578
		marker_idle  = false;
579
		marker_depth = 0;
580
		return;
581
	}
582
	if (value == 0xF1) {
583
		marker_idle  = true;
584
		marker_depth = 0;
585
		return;
586
	}
587
	if (value == 0x00) {
588
		if (marker_depth > 0)
589
			marker_depth--;
590
		return;
591
	}
592
	if (value & 0x80) {
593
		const uint8_t id = value & 0x3F;
594
		// Unwind to just below the named region. Leaving a region that is not
595
		// open is ignored rather than treated as an error.
596
		for (int d = marker_depth - 1; d >= 0; d--) {
597
			const int zi = marker_stack[d];
598
			if (zi >= 0 && zones[zi].is_marker && zones[zi].marker_id == id) {
599
				marker_depth = d;
600
				return;
601
			}
602
		}
603
		return;
604
	}
605
	if (value <= 0x3F) {
606
		if (marker_depth < PERF_MARKER_STACK)
607
			marker_stack[marker_depth++] = (int8_t)marker_zone(value);
608
		return;
609
	}
610
}
611
612
uint8_t
613
perf_budget_marker_read(void)
614
{
615
	return PERF_MARKER_PROTOCOL;
616
}
617
618
// ─── Zones ──────────────────────────────────────────────────────────────────
619
620
static void
621
rebuild_zone_map(void)
622
{
623
	bool any = false;
624
	for (int i = 0; i < PERF_MAX_ZONES; i++) {
625
		if (zones[i].used && !zones[i].is_marker) {
626
			any = true;
627
			break;
628
		}
629
	}
630
	if (!any) {
631
		free(zone_of);
632
		zone_of = NULL;
633
		return;
634
	}
635
	if (!zone_of) {
636
		zone_of = (uint8_t *)calloc(65536, 1);
637
		if (!zone_of)
638
			return;
639
	}
640
	memset(zone_of, 0, 65536);
641
642
	// Later zones win where two overlap, which makes "draw a tighter zone
643
	// inside a looser one" work the way anybody would expect.
644
	for (int i = 0; i < PERF_MAX_ZONES; i++) {
645
		const perf_zone_t *z = &zones[i];
646
		if (!z->used || z->is_marker || z->end <= z->start)
647
			continue;
648
		for (uint32_t a = z->start; a < z->end; a++)
649
			zone_of[a] = (uint8_t)(i + 1);
650
	}
651
}
652
653
int
654
perf_budget_zone_add(const char *name, uint16_t start, uint16_t end, int16_t bank, bool is_idle)
655
{
656
	if (end <= start)
657
		return -1;
658
	for (int i = 0; i < PERF_MAX_ZONES; i++) {
659
		if (zones[i].used)
660
			continue;
661
		perf_zone_t *z = &zones[i];
662
		memset(z, 0, sizeof *z);
663
		snprintf(z->name, sizeof z->name, "%s", name ? name : "zone");
664
		z->start   = start;
665
		z->end     = end;
666
		z->bank    = bank;
667
		z->is_idle = is_idle;
668
		z->used    = true;
669
		rebuild_zone_map();
670
		return i;
671
	}
672
	return -1;
673
}
674
675
bool
676
perf_budget_zone_remove(int index)
677
{
678
	if (index < 0 || index >= PERF_MAX_ZONES || !zones[index].used)
679
		return false;
680
	memset(&zones[index], 0, sizeof zones[index]);
681
	// Anything still pointing at the slot would attribute cycles to a zone that
682
	// no longer exists.
683
	for (int d = 0; d < marker_depth; d++)
684
		if (marker_stack[d] == index)
685
			marker_stack[d] = -1;
686
	rebuild_zone_map();
687
	return true;
688
}
689
690
void
691
perf_budget_zone_clear(void)
692
{
693
	memset(zones, 0, sizeof zones);
694
	memset(cur_zone, 0, sizeof cur_zone);
695
	marker_depth = 0;
696
	rebuild_zone_map();
697
}
698
699
int
700
perf_budget_zone_count(void)
701
{
702
	int n = 0;
703
	for (int i = 0; i < PERF_MAX_ZONES; i++)
704
		if (zones[i].used)
705
			n = i + 1;
706
	return n;
707
}
708
709
bool
710
perf_budget_zone_get(int index, perf_zone_t *out)
711
{
712
	if (index < 0 || index >= PERF_MAX_ZONES || !zones[index].used)
713
		return false;
714
	if (out)
715
		*out = zones[index];
716
	return true;
717
}
718
719
// ─── Queries ────────────────────────────────────────────────────────────────
720
721
static int
722
cmp_u32(const void *a, const void *b)
723
{
724
	const uint32_t x = *(const uint32_t *)a, y = *(const uint32_t *)b;
725
	return (x > y) - (x < y);
726
}
727
728
// Nearest-rank percentile over an ascending array.
729
static uint32_t
730
percentile(const uint32_t *v, int n, int p)
731
{
732
	if (n <= 0)
733
		return 0;
734
	int idx = (int)(((int64_t)p * n + 99) / 100) - 1;
735
	if (idx < 0)
736
		idx = 0;
737
	if (idx >= n)
738
		idx = n - 1;
739
	return v[idx];
740
}
741
742
// Consumes `scratch[0..n-1]` (unsorted on entry, sorted on exit).
743
static void
744
stat_from_scratch(int n, perf_stat_t *out)
745
{
746
	memset(out, 0, sizeof *out);
747
	if (n <= 0)
748
		return;
749
750
	uint64_t sum = 0;
751
	for (int i = 0; i < n; i++)
752
		sum += scratch[i];
753
754
	qsort(scratch, (size_t)n, sizeof(uint32_t), cmp_u32);
755
756
	out->min  = scratch[0];
757
	out->max  = scratch[n - 1];
758
	out->mean = (uint32_t)(sum / (uint64_t)n);
759
	out->p50  = percentile(scratch, n, 50);
760
	out->p95  = percentile(scratch, n, 95);
761
	out->p99  = percentile(scratch, n, 99);
762
}
763
764
// How many of the newest samples fall in `seconds`.
765
static int
766
window_frames(float seconds)
767
{
768
	if (count <= 0)
769
		return 0;
770
	if (seconds <= 0.0f)
771
		return count;
772
	const float hz = perf_budget_vsync_hz();
773
	if (hz <= 0.0f)
774
		return count;
775
	int n = (int)(seconds * hz + 0.5f);
776
	if (n < 1)
777
		n = 1;
778
	if (n > count)
779
		n = count;
780
	return n;
781
}
782
783
// Index of the i'th newest sample (i == 0 is the newest).
784
static int
785
newest(int i)
786
{
787
	return (head - 1 - i + 2 * capacity) % capacity;
788
}
789
790
bool
791
perf_budget_last_frame(perf_frame_t *out)
792
{
793
	if (!ring || count <= 0)
794
		return false;
795
	if (out)
796
		*out = ring[newest(0)];
797
	return true;
798
}
799
800
bool
801
perf_budget_current_frame(perf_frame_t *out)
802
{
803
	if (!out)
804
		return false;
805
	*out = cur;
806
	out->frame = total_frames;
807
	for (int i = 0; i < PERF_RING_ZONES; i++)
808
		out->zone[i] = cur_zone[i];
809
	out->period_work = cur.total - cur.idle;
810
	return true;
811
}
812
813
int
814
perf_budget_frame_count(void)
815
{
816
	return count;
817
}
818
819
bool
820
perf_budget_frame_at(int newest_index, perf_frame_t *out)
821
{
822
	if (!ring || newest_index < 0 || newest_index >= count)
823
		return false;
824
	if (out)
825
		*out = ring[newest(newest_index)];
826
	return true;
827
}
828
829
uint32_t
830
perf_budget_total_frames(void)
831
{
832
	return total_frames;
833
}
834
835
bool
836
perf_budget_window(float seconds, perf_window_t *out)
837
{
838
	if (!out || !ring)
839
		return false;
840
	const int n = window_frames(seconds);
841
	if (n <= 0)
842
		return false;
843
844
	memset(out, 0, sizeof *out);
845
	out->seconds       = seconds;
846
	out->frames        = n;
847
	out->budget_cycles = budget_cycles;
848
849
	for (int i = 0; i < n; i++)
850
		scratch[i] = perf_frame_work(&ring[newest(i)]);
851
	stat_from_scratch(n, &out->work);
852
853
	for (int i = 0; i < n; i++)
854
		scratch[i] = ring[newest(i)].total;
855
	stat_from_scratch(n, &out->total);
856
857
	for (int i = 0; i < n; i++)
858
		scratch[i] = ring[newest(i)].irq;
859
	stat_from_scratch(n, &out->irq);
860
861
	for (int i = 0; i < n; i++)
862
		scratch[i] = ring[newest(i)].host_us;
863
	stat_from_scratch(n, &out->host_us);
864
865
	// Budget-period work carries the overrun answer, so the scan for the worst
866
	// frame rides along with the copy rather than making a second pass.
867
	int overruns = 0;
868
	const uint32_t threshold = overrun_threshold();
869
	for (int i = 0; i < n; i++) {
870
		const perf_frame_t *f = &ring[newest(i)];
871
		scratch[i]            = f->period_work;
872
		if (f->period_work > threshold) {
873
			overruns++;
874
			const uint32_t over = f->period_work - budget_cycles;
875
			if (over > out->worst_overrun) {
876
				out->worst_overrun = over;
877
				out->worst_frame   = f->frame;
878
			}
879
		}
880
	}
881
	stat_from_scratch(n, &out->budget_work);
882
883
	out->overruns    = overruns;
884
	out->overrun_pct = 100.0f * (float)overruns / (float)n;
885
886
	if (budget_cycles) {
887
		const float b        = (float)budget_cycles;
888
		out->mean_utilization = 100.0f * (float)out->budget_work.mean / b;
889
		out->p95_utilization  = 100.0f * (float)out->budget_work.p95 / b;
890
		out->max_utilization  = 100.0f * (float)out->budget_work.max / b;
891
	}
892
	return true;
893
}
894
895
bool
896
perf_budget_zone_window(int zone, float seconds, perf_stat_t *out)
897
{
898
	if (!out || zone < 0 || zone >= PERF_MAX_ZONES || !zones[zone].used)
899
		return false;
900
901
	memset(out, 0, sizeof *out);
902
903
	// Zones past the ring's per-frame slots have no history to take
904
	// percentiles over. Report the live frame flat rather than nothing, so the
905
	// caller still sees a number and can tell it apart by its equal fields.
906
	if (zone >= PERF_RING_ZONES) {
907
		const uint32_t v = cur_zone[zone];
908
		out->min = out->max = out->mean = out->p50 = out->p95 = out->p99 = v;
909
		return true;
910
	}
911
	if (!ring)
912
		return false;
913
914
	const int n = window_frames(seconds);
915
	if (n <= 0)
916
		return false;
917
	for (int i = 0; i < n; i++)
918
		scratch[i] = ring[newest(i)].zone[zone];
919
	stat_from_scratch(n, out);
920
	return true;
921
}
922
923
bool
924
perf_budget_drain_overruns(perf_overrun_report_t *out)
925
{
926
	if (pending_overruns.overruns == 0) {
927
		pending_overruns.frames = 0;
928
		return false;
929
	}
930
	if (out)
931
		*out = pending_overruns;
932
	memset(&pending_overruns, 0, sizeof pending_overruns);
933
	return true;
934
}