repo: merge the 60 fps present batch (perf-60)

Busy desktop windows ran at ~50 copies/s because WindowServer, holding at most two swaps, got one refresh per frame while the kext completed each swap a tick after its copy, and the kext's per-IB cache maintenance (3.4 ms over ~98 MiB) sat on WindowServer's path. The GPU is not the limit: 94% of the time at 1700 MHz, ~7% busy. ADRENO_PRESENT_EARLY_DONE (--present-early-done, adaptive mode 2) completes a layer-0 swap at its scanout copy after a late frame; the desktop-early-done profile adds it to the coherent backlight build so --gpu-coherent can drop the maintenance. Replay: busy 49.8 -> 56.3 copies/s, 57.9 with coherent.
This commit is contained in:
Acelogic committed 2026-10-24 21:59:20 -04:00
commit 822226cfed
12 files changed
+849 -4

No files matched your search

+22 -3
View File
@@ -147,7 +147,7 @@ The flags fall into these groups:
| --- | --- |
| Console and early stop | `--console`, `--single-user`, `--early-stop-delay`, `--let-panic`, `--panic-serve` |
| Input | `--input`, `--input-touchpad`, `--input-ptp`, `--input-active-us`, `--input-fnkeys` |
| Present path | `--present-dartpool`, `--present-resident`, `--present-beam`, `--present-va-reclaim`, `--present-maint`, `--present-latch`, `--present-pipeline` |
| Present path | `--present-dartpool`, `--present-resident`, `--present-beam`, `--present-va-reclaim`, `--present-maint`, `--present-latch`, `--present-pipeline`, `--present-early-done` |
| GPU | `--ib-async`, `--gpu-recover`, `--gpu-va-wide`, `--gpu-coherent`, `--resident-mib`, `--adreno-fill-br`, `--adreno-fence-br`, `--adreno-cs-samplers`, `--gpu-x290-survey` |
| Panel brightness | `--edp-backlight`, `--edp-backlight-mode` |
| Clocks and bandwidth | `--thermal-guard`, `--gpu-dvfs`, `--gpu-dvfs-ceiling`, `--cpu-dvfs`, `--ddr-vote`, `--llcc`, `--bw-probe` |
@@ -214,12 +214,31 @@ nits); either boot-arg moves the curve without rebuilding the monitor. The
collection is built with
`KEXT_PROFILE=desktop-next I2C_PROFILE=desktop-fnkeys tools/build-auxkc-desktop.sh
NAME` for a monitor without `--gpu-coherent` (`desktop-next` is
`desktop-nocoherent` plus the service); a `--gpu-coherent` run needs a
`KEXT_PROFILE=desktop-backlight` collection pinned there instead. `--edp-backlight-mode` lets the first change switch a panel left in
`desktop-nocoherent` plus the service). With `--gpu-coherent` or
`--present-early-done` the run takes the collection pinned as
`INPUT_AUXKC_DESKTOP_EARLY_DONE` instead (`KEXT_PROFILE=desktop-early-done
I2C_PROFILE=desktop-fnkeys tools/build-auxkc-desktop.sh NAME`; `desktop-early-done` is
`desktop-backlight`, the coherent `desktop` plus the service, with early
completion), and is refused until that collection is pinned. `--edp-backlight-mode` lets the first change switch a panel left in
PWM-pin mode to DPCD control. `tools/survey-edp-aux.py` checks, read-only from
q1n1, that the clocks and the AUX controller the call needs are on.
`tools/test-edp-backlight.py` is the offline test.
`--present-early-done MODE` (needs `--edp-backlight` and its chain) adds the
guest boot-arg `adreno_present_early_done=MODE` for the `desktop-early-done`
AdrenoX2 (`src/AdrenoDisplayEarlyDone.h` in the kext). AdrenoDisplay then
completes a layer-0 swap right after its scanout copy instead of at the next
frame tick. WindowServer keeps at most two swaps outstanding, so completing at
the copy frees one of its entries a refresh earlier and a frame that runs past
one refresh can still make the next latch; WindowServer may then run up to one
frame further ahead. `1` completes at every copy, `2` only while WindowServer
is late (for `--present-early-hold-ms`, default 2000, after each late frame),
`0` only measures. Every mode adds one `AdrenoDisplay: perf done` line per perf
window: completions at the copy, late frames, engagements, and histograms of
WindowServer's arrivals after each tick and of each late frame's lateness. It
excludes `--present-hold on`. `tools/test-present-early-done.py` is the offline
test (with the presentation replay's early-done scenarios).
`--view-reclaim` gives the monitor `SNAP_VIEW_RECLAIM`. The kernel's per-CPU
private page-table copies (`shadow/sprr-view.h`) hold 256 tables instead of
128, and when a protecting RETYPE (read-only zone, TXM) finds them full, the
+10
View File
@@ -295,6 +295,16 @@ def parse(argv=None):
help='with --present-latch: guest boot-arg adreno_hold (default off since b12v11: on completes the swap '
'on screen just before the next latch and copies the pending one early; b12v11 showed early '
'copies of frames whose IB had not finished)')
p.add_argument('--present-early-done', type=int, choices=(0, 1, 2), default=None,
help='with --edp-backlight and its chain: the AuxKC INPUT_AUXKC_DESKTOP_EARLY_DONE and guest '
'boot-arg adreno_present_early_done: AdrenoDisplay completes a layer-0 swap right after its '
'scanout copy instead of at the next tick, so WindowServer\'s two swap entries free a refresh '
'earlier (1 always; 2 only while WindowServer is late, for --present-early-hold-ms after each late '
'frame; 0 measures only). Every mode adds the perf done line (WindowServer\'s arrivals against '
'the ticks). Not with --present-hold on; give it to every mode of the attempt')
p.add_argument('--present-early-hold-ms', type=int, default=None,
help='with --present-early-done 2: guest boot-arg adreno_present_early_hold_ms, how long a late frame '
'keeps early completion on (default 2000; 100..10000)')
p.add_argument('--present-gate-us', type=int, default=None,
help='with --present-latch: guest boot-arg adreno_gate_us, the age under which a layer-0 swap is not '
'copied while a GPU submission is in the kext (default 4000; 0 = no gate)')
+36
View File
@@ -244,6 +244,17 @@ INPUT_AUXKC_PTP_DARTPOOL_RESIDENT_BEAM_VA_WIDE_LATCH_SHA = 'ccf0c8f4da4f63fbe25c
# (57965a84); without --edp-backlight the latch collection above (org-desktop-nocoherent-20261023, ccf0c8f4) is used.
INPUT_AUXKC_DESKTOP_BACKLIGHT = 'build/current-assets/swap-path-20261024/adreno-input.kc'
INPUT_AUXKC_DESKTOP_BACKLIGHT_SHA = '9e916a4185bc609178fe80a3110e020d9f3167625da7a76a9bc9ff22412743d5'
# --edp-backlight with --gpu-coherent or --present-early-done: the --edp-backlight collection rebuilt from
# SnapAdreno's desktop-early-done profile, i.e. desktop-backlight (desktop's IO-coherent maintenance skipping plus
# AdrenoBacklight) plus -DADRENO_PRESENT_EARLY_DONE=1 in AdrenoDisplay.o (src/AdrenoDisplayEarlyDone.h: a layer-0 swap
# completed right after its scanout copy, boot-arg adreno_present_early_done 1 always / 2 while WindowServer is behind,
# 0 measures only; the perf done line), and SnapI2CHID desktop-fnkeys. Link it with
# KEXT_PROFILE=desktop-early-done I2C_PROFILE=desktop-fnkeys tools/build-auxkc-desktop.sh NAME (sudo for kmutil's staging)
# and put the two lines it prints here. Every addition is inert without its switch (the monitor's coherent ON with
# adreno_coherent=1; adreno_backlight=1; adreno_present_early_done), so the collection also serves --edp-backlight runs
# without them. None until linked and pinned: those flag sets are refused rather than run on the nocoherent kext.
INPUT_AUXKC_DESKTOP_EARLY_DONE = None
INPUT_AUXKC_DESKTOP_EARLY_DONE_SHA = None
# --guest-net: INPUT_AUXKC's objects with SnapintoshVirtualEthernet's datapath (docs/GUEST-NETWORK-20261019.md).
# None until that collection is built and pinned; --guest-net refuses meanwhile.
INPUT_AUXKC_NET = None
@@ -787,6 +798,9 @@ def route_selected(args):
edp_backlight = getattr(args, 'edp_backlight', False)
need(not edp_backlight or latch,
'--edp-backlight needs --present-latch and its chain (its AdrenoX2 is that collection\'s with ADRENO_BACKLIGHT)')
early = getattr(args, 'present_early_done', None)
need(early is None or edp_backlight,
'--present-early-done needs --edp-backlight and its chain (its AdrenoX2 is the early-done collection)')
need(not (vsync60 and (damage or cached or gpumap)),
'--present-vsync60 already includes --present-gpumap\'s (and so --present-cached-read\'s) kext: give one of them')
need(not (damage and cached), '--present-cached-read already includes --present-damage\'s present: give one of them')
@@ -812,6 +826,14 @@ def route_selected(args):
if vsync60:
return ((INPUT_TOUCHPAD_PROFILE if touchpad else INPUT_PROFILE), INPUT_AUXKC_VSYNC60,
INPUT_AUXKC_VSYNC60_SHA, INPUT_BUNDLES)
if dartpool and ptp and resident and beam and va_reclaim and va_wide and latch and edp_backlight and (
getattr(args, 'gpu_coherent', False) or early is not None):
need(INPUT_AUXKC_DESKTOP_EARLY_DONE is not None and INPUT_AUXKC_DESKTOP_EARLY_DONE_SHA is not None,
'--edp-backlight with --gpu-coherent or --present-early-done: its AuxKC is not pinned yet '
'(INPUT_AUXKC_DESKTOP_EARLY_DONE_SHA; KEXT_PROFILE=desktop-early-done I2C_PROFILE=desktop-fnkeys '
'tools/build-auxkc-desktop.sh NAME links it)')
return (INPUT_TOUCHPAD_PROFILE, INPUT_AUXKC_DESKTOP_EARLY_DONE, INPUT_AUXKC_DESKTOP_EARLY_DONE_SHA,
INPUT_BUNDLES)
if dartpool and ptp and resident and beam and va_reclaim and va_wide and latch and edp_backlight:
need(INPUT_AUXKC_DESKTOP_BACKLIGHT is not None and INPUT_AUXKC_DESKTOP_BACKLIGHT_SHA is not None,
'--edp-backlight: its AuxKC is not pinned yet (INPUT_AUXKC_DESKTOP_BACKLIGHT_SHA; '
@@ -1244,6 +1266,20 @@ def boot_addition(args):
if active_us is not None:
need(active_us == 0 or 2000 <= active_us <= 100000, '--input-active-us must be 0 or 2000..100000')
extra.append(f'snap_i2c_active_us={active_us}')
# --present-early-done: AdrenoDisplay's early completion (src/AdrenoDisplayEarlyDone.h), read only by the
# desktop-early-done AdrenoX2 that --edp-backlight then selects; the kext turns it off with the release hold.
early, early_hold = getattr(args, 'present_early_done', None), getattr(args, 'present_early_hold_ms', None)
need(early is None or getattr(args, 'edp_backlight', False),
'--present-early-done needs --edp-backlight (the early-done collection is that chain\'s)')
need(early is None or early in (0, 1, 2), '--present-early-done is 0 (measure only), 1 (always) or 2 (while behind)')
need(early_hold is None or early == 2, '--present-early-hold-ms needs --present-early-done 2')
need(not early or hold != 'on', '--present-early-done 1/2 and --present-hold on exclude each other (the hold '
'keeps the swap current until its release)')
if early is not None:
extra.append(f'adreno_present_early_done={early}')
if early_hold is not None:
need(100 <= early_hold <= 10000, '--present-early-hold-ms must be 100..10000')
extra.append(f'adreno_present_early_hold_ms={early_hold}')
# --input-fnkeys: the keyboard's Fn-key layer (drivers/snap-i2c-hid/src/hid-keyboard.h HID_FNKEYS_*), read by a
# SnapI2CHID built with SNAP_I2C_HID_FNKEYS; the same sets the kext refuses are refused here.
fnkeys = getattr(args, 'input_fnkeys', None)
+102
View File
@@ -0,0 +1,102 @@
/* SPDX-License-Identifier: MIT */
/* Early completion (gpu/adreno-kext/kext/src/AdrenoDisplayEarlyDone.h): included at the end of
* tools/replay-adreno-pacing.c under -DADRENO_PRESENT_EARLY_DONE=1, after relatch_scenarios. Without the define this
* file adds nothing; with it every other output is byte for byte the define-off build's (the knobs below are 0 there).
*
* The world is the present-pacing one (the A16 beam and latch, the sync gate, the relatch, the pipelined submit lock,
* app IBs) with the deployed kext's switches: relatch and urgent polling on, early release EP1/EP2 on, EP3's assist off
* (the sparse kext compiles it out). AdrenoDisplay's early completion is mirrored step for step in
* replay-adreno-latency.c (l_present, l_swap_begin's arrival, l_tick); mode 1 completes every layer-0 swap at its copy,
* mode 2 only while a late frame engaged it (hold 2 s).
*
* Calibration (the 2026-10-24 desktop runs):
* - busy: installed-fresh-desktop-20261024-org2's 117 busy windows (gfx-probe load): 49.9 copies/s median, 18% of the
* ticks without a new swap, q 6.2 ms, one 1.35 ms IB per frame, TO_DEVICE + FROM_DEVICE 3.4 ms per IB (98 MiB). Only
* input-latency's SLOT WindowServer (two swaps outstanding, the next frame when one completes) reproduces that rate,
* with a render time of 5-9 ms and 20% of the frames 14-30 ms (FREE gives 57-59 copies/s for the same frames).
* - busy_coherent: the same frames with b12v23's maintenance (--gpu-coherent: 1.05 ms and 40 MiB per IB);
* - quiet: the cursor moving over an idle desktop, a layer-0 swap every refresh and no IB (b12v20/b12v21: 59.8/s);
* - sporadic: next2c's idle windows (57.9/s): the quiet frames plus 5% of frames with an IB whose kext time is 13 ms
* (TO_DEVICE 7 ms over 208 MiB and 6 ms of preparation) and 3.4 ms of GPU;
* - sporadic_coherent: the same with the coherent maintenance (the preparation stays: 6.5-8.5 ms in the kext).
* The model has no guest-wide stalls, so its intervals over 34 ms stay under the hardware's (org2: 5.4%). */
static void early_done_scenarios(const char *dir)
{
unsigned c0 = checks, f0 = failures, cc0 = cost_checks, cf0 = cost_failures;
char path[4096]; snprintf(path, sizeof path, "%s/early-done.json", dir);
RJ = fopen(path, "w"); assert(RJ); rfirst = 0;
fprintf(RJ, "{\"scenarios\":{");
const uint64_t M = MS;
struct ecase { struct rcase c; unsigned spike_pct; uint64_t spike_min, spike_max; };
const struct ecase cases[] = {
{{"busy", WS_SLOT, 5 * M, 9 * M, 100, {900 * US, 1400 * US, 3 * M}, {1400 * US, 3 * M, 8 * M}, {75, 23, 2},
2200 * US, 3400 * US, 500 * US, 1 * M, 1200 * M, {1 * M, 3 * M, 10 * M}, {3 * M, 10 * M, 30 * M}, {80, 15, 5},
1 * M, 300 * US, 2, 1 * M, 6 * M, 3}, 20, 14 * M, 30 * M},
{{"busy_coherent", WS_SLOT, 5 * M, 9 * M, 100, {900 * US, 1400 * US, 3 * M}, {1400 * US, 3 * M, 8 * M}, {75, 23, 2},
400 * US, 900 * US, 300 * US, 700 * US, 1200 * M, {1 * M, 3 * M, 10 * M}, {3 * M, 10 * M, 30 * M}, {80, 15, 5},
1 * M, 300 * US, 2, 1 * M, 6 * M, 3}, 20, 14 * M, 30 * M},
{{"busy", WS_FREE, 5 * M, 9 * M, 100, {900 * US, 1400 * US, 3 * M}, {1400 * US, 3 * M, 8 * M}, {75, 23, 2},
2200 * US, 3400 * US, 500 * US, 1 * M, 1200 * M, {1 * M, 3 * M, 10 * M}, {3 * M, 10 * M, 30 * M}, {80, 15, 5},
1 * M, 300 * US, 2, 1 * M, 6 * M, 3}, 20, 14 * M, 30 * M},
{{"quiet", WS_SLOT, 2 * M, 3600 * US, 0, {0, 0, 0}, {0, 0, 0}, {100, 0, 0},
0, 0, 0, 0, 0, {0, 0, 0}, {0, 0, 0}, {100, 0, 0}, 0, 0, 0, 0, 0, 0}, 0, 0, 0},
{{"sporadic", WS_SLOT, 2 * M, 3600 * US, 5, {3 * M, 3 * M, 3 * M}, {4 * M, 4 * M, 4 * M}, {100, 0, 0},
12 * M, 14 * M, 1 * M, 2 * M, 0, {0, 0, 0}, {0, 0, 0}, {100, 0, 0}, 0, 0, 0, 0, 0, 0}, 0, 0, 0},
{{"sporadic_coherent", WS_SLOT, 2 * M, 3600 * US, 5, {3 * M, 3 * M, 3 * M}, {4 * M, 4 * M, 4 * M}, {100, 0, 0},
6500 * US, 8500 * US, 300 * US, 700 * US, 0, {0, 0, 0}, {0, 0, 0}, {100, 0, 0}, 0, 0, 0, 0, 0, 0}, 0, 0, 0},
};
const unsigned ncases = sizeof cases / sizeof cases[0];
for (unsigned i = 0; i < ncases; i++) {
const struct ecase *e = &cases[i];
struct rres x[3];
for (int mode = 0; mode < 3; mode++) {
char tag[32]; snprintf(tag, sizeof tag, "_done%d", mode);
rcase_noassist = 1; rcase_copydone = mode; rcase_ed_hold = 2000 * M;
rcase_spike_pct = e->spike_pct; rcase_spike_min = e->spike_min; rcase_spike_max = e->spike_max;
x[mode] = r_case(&e->c, RP_ALL, 1500, 1000, 0, tag);
}
rcase_noassist = 0; rcase_copydone = 0; rcase_ed_hold = 0; rcase_spike_pct = 0; rcase_spike_min = rcase_spike_max = 0;
const struct rres *t = &x[0], *a = &x[1], *d = &x[2];
/* mode 0 measures only: nothing completed at a copy; the others never complete without a copy */
CHECK(t->ed.early == 0 && t->ed.engages == 0);
CHECK(a->ed.early <= a->copies && d->ed.early <= d->copies);
CHECK(a->ed.engages == 0); /* mode 1 has nothing to engage */
/* never fewer copies than today (1% noise) */
COST(a->cps_x10 * 100 >= t->cps_x10 * 99 && d->cps_x10 * 100 >= t->cps_x10 * 99);
if (e->c.ws == WS_SLOT && t->ed.late * 50 > t->copies) {
/* WindowServer late on more than 2% of the frames: both modes recover most of the missed refreshes */
COST(a->cps_x10 >= t->cps_x10 + 30 && d->cps_x10 >= t->cps_x10 + 25);
COST(d->ed.engages > 0 && d->ed.early > 0);
}
if (!t->ed.late) {
/* nothing late: the adaptive mode never engages and the run is today's frame for frame */
COST(d->ed.engages == 0 && d->ed.early == 0 && d->cps_x10 == t->cps_x10 && d->r.in_mean_us == t->r.in_mean_us);
}
char line[512];
int n = adreno_perf_line_done(line, sizeof line, i + 1, &d->ed, 0);
CHECK(n > 0 && n <= ADRENO_PERF_LINE_MAX);
printf("early-done %s_%s: copies/s today %llu.%llu, always %llu.%llu, adaptive %llu.%llu; >34 ms %llu.%llu%% / "
"%llu.%llu%% / %llu.%llu%%; late frames today %llu of %llu copies; input mean %llu / %llu / %llu us, "
"p95 %llu / "
"%llu / %llu us; q %llu / %llu / %llu us; adaptive engaged %llu times, %llu of %llu copies completed early\n",
e->c.name, ws_name[e->c.ws], (unsigned long long)t->cps_x10 / 10, (unsigned long long)t->cps_x10 % 10,
(unsigned long long)a->cps_x10 / 10, (unsigned long long)a->cps_x10 % 10,
(unsigned long long)d->cps_x10 / 10, (unsigned long long)d->cps_x10 % 10,
(unsigned long long)t->over34_pm / 10, (unsigned long long)t->over34_pm % 10,
(unsigned long long)a->over34_pm / 10, (unsigned long long)a->over34_pm % 10,
(unsigned long long)d->over34_pm / 10, (unsigned long long)d->over34_pm % 10,
(unsigned long long)t->ed.late, (unsigned long long)t->copies,
(unsigned long long)t->r.in_mean_us, (unsigned long long)a->r.in_mean_us, (unsigned long long)d->r.in_mean_us,
(unsigned long long)t->r.in_p95_us, (unsigned long long)a->r.in_p95_us, (unsigned long long)d->r.in_p95_us,
(unsigned long long)t->r.q_us, (unsigned long long)a->r.q_us, (unsigned long long)d->r.q_us,
(unsigned long long)d->ed.engages, (unsigned long long)d->ed.early, (unsigned long long)d->copies);
n = adreno_perf_line_done(line, sizeof line, i + 1, &t->ed, 0);
printf("early-done %s_%s today's arrivals: %s\n", e->c.name, ws_name[e->c.ws], line);
}
fprintf(RJ, "},\"checks\":%u,\"failures\":%u,\"cost_checks\":%u,\"cost_failures\":%u}\n", checks - c0, failures - f0,
cost_checks - cc0, cost_failures - cf0);
fclose(RJ);
printf("{\"early_done_checks\":%u,\"early_done_failures\":%u,\"early_done_cost_checks\":%u,"
"\"early_done_cost_failures\":%u}\n",
checks - c0, failures - f0, cost_checks - cc0, cost_failures - cf0);
}
+36
View File
@@ -57,6 +57,16 @@
/* agents/present-pacing FAILURE-MODES.md: the b12v17 pipeline with its submit lock, and the relatch (relatch_scenarios) */
#include "../gpu/adreno-kext/kext/src/AdrenoDisplayRelatch.h"
#endif
#ifndef ADRENO_PRESENT_EARLY_DONE
#define ADRENO_PRESENT_EARLY_DONE 0
#endif
#if ADRENO_PRESENT_EARLY_DONE
#if !ADRENO_PRESENT_RELATCH
#error "the early-completion scenarios run in the present-pacing world (-DADRENO_PRESENT_RELATCH=1)"
#endif
/* AdrenoDisplayEarlyDone.h: a layer-0 swap completed right after its copy, always or while WindowServer is behind (early_done_scenarios) */
#include "../gpu/adreno-kext/kext/src/AdrenoDisplayEarlyDone.h"
#endif
enum { WS_SLOT = 0, WS_EVENT = 1, WS_TIMER = 2, WS_FREE = 3 };
enum { LP_BEAM = 0, LP_LATCH = 1, LP_HOLD = 2, LP_HOLD_NOGATE = 3 };
@@ -144,6 +154,10 @@ struct lworld {
uint64_t c_ovl, c_tmr, c_pre, c_ast, c_app, c_apps, c_polls, c_late_unmark;
uint64_t ret_at[LMAX]; /* the submission's return: G1 busy until then, S returns then */
#endif
#if ADRENO_PRESENT_EARLY_DONE
/* AdrenoDisplay's fEarly: mode = complete_at_copy (1 always, as the pipeline what-if; 2 adaptive), its hold */
struct adreno_early_done ed; uint64_t ed_hold;
#endif
};
/* ---- WindowServer ---- */
@@ -334,13 +348,25 @@ static void l_present(struct lworld *l, uint32_t id, uint8_t layers) /* presen
l->presented = 1;
if (layers & 1) l_copy(l, id);
#if ADRENO_PIPELINE
#if ADRENO_PRESENT_EARLY_DONE
/* earlyDoneNowGated by mode (1 is also gpu-pipeline's what-if, whose hold rows keep it: the kext refuses the hold
* with early completion, the early-completion scenarios never run the hold) */
if ((layers & 1) && l->complete_at_copy && adreno_early_active(&l->ed, l->w.now)) {
l->early_done = id; adreno_early_completed(&l->ed); ws_completed(l, id);
}
#else
if ((layers & 1) && l->complete_at_copy) { l->early_done = id; ws_completed(l, id); } /* what-if */
#endif
#endif
}
static void l_pop(struct lworld *l) { memmove(l->q, l->q + 1, (l->qn - 1) * sizeof l->q[0]); l->qn--; }
static void l_swap_begin(struct lworld *l, int in_tick) /* swap_begin_gated */
{
struct bworld *w = &l->w;
#if ADRENO_PRESENT_EARLY_DONE
if (!in_tick && l->qn && !l->pending_since && (l->q[0].layers & 1))
adreno_early_arrival(&l->ed, w->now, l->qn == 1); /* swap_begin_gated, F1's read */
#endif
if (!in_tick && l->qn && !l->pending_since) l->pending_since = w->now; /* F1 */
#if ADRENO_LATCH
if (!in_tick && l->pending_since && l->h.released_at) adreno_hold_arrival(&l->h, tb(w, l->pending_since));
@@ -462,12 +488,19 @@ static void l_tick(struct lworld *l) /* vsyncGated */
w->in_tick = 0;
if (!held) {
if (l->rel_armed && l->cur && l->presented) l->rel_armed = 0;
#endif
#if ADRENO_PRESENT_EARLY_DONE
const int ed_had = l->cur && l->presented && (l->cur_layers & 1); /* completed here, or held since its copy */
const uint64_t ed_copies = l->copies;
#endif
if (l->cur && l->presented) l_complete(l);
w->in_tick = 1;
if (l_gate(l)) { l->gated++; l_arm(l); } else l_swap_begin(l, 1);
#if ADRENO_PRESENT_RELATCH
if (l->p2) p2_relatch_arm(l); /* RL4: the GO's frame unused and its head deferred */
#endif
#if ADRENO_PRESENT_EARLY_DONE
adreno_early_tick(&l->ed, w->now, ed_had, l->copies != ed_copies || (l->qn && (l->q[0].layers & 1)));
#endif
w->b.go_row0 = 0; w->in_tick = 0;
#if ADRENO_LATCH
@@ -545,6 +578,9 @@ static void l_start(struct lworld *l)
adreno_beam_latch(&w->b, l->policy == LP_BEAM ? 0 : 3 * ms);
adreno_hold_init(&l->h, l->policy == LP_HOLD || l->policy == LP_HOLD_NOGATE, ms * 3 / 2, ms * ADRENO_HOLD_FLOOR_US / 1000);
l->gate_ns = l->policy == LP_LATCH || l->policy == LP_HOLD ? 4 * MS : 0;
#endif
#if ADRENO_PRESENT_EARLY_DONE
adreno_early_init(&l->ed, (uint32_t)l->complete_at_copy, 16666667, l->ed_hold ? l->ed_hold : 2000 * MS, MS);
#endif
l->started_at = w->now;
if (l->ws_kind == WS_EVENT) l->events = 1;
+22
View File
@@ -277,8 +277,19 @@ struct rres {
uint64_t copies, iv[5], over34_pm, cps_x10, defer, swaps, max_wait_us, gpu_frames, marked, apps;
uint64_t ovl, tmr, pre, ast, app_fin, polls, rl_armed, rl_go, rl_late, rl_gone, rl_gated, rl_ovt, rl_polls, rl_lead_min_us;
uint64_t hist[ADRENO_RELATCH_BINS];
#if ADRENO_PRESENT_EARLY_DONE
struct adreno_early_done ed; /* the run's early completions and arrivals (not windowed) */
#endif
};
static FILE *RJ; static int rfirst;
#if ADRENO_PRESENT_EARLY_DONE
/* replay-adreno-early-done.c's knobs, all 0 for every row above (their outputs stay byte for byte as without the define):
* the swap completed right after its copy (lworld complete_at_copy, AdrenoDisplay's adreno_present_early_done=1),
* WindowServer's render spikes, and EP3's assist off (the deployed sparse kext compiles it out). */
static int rcase_copydone, rcase_noassist;
static unsigned rcase_spike_pct;
static uint64_t rcase_spike_min, rcase_spike_max, rcase_ed_hold;
#endif
static struct rres r_case(const struct rcase *c, int rp, unsigned swaps, uint64_t guard_us, unsigned seed, const char *tag)
{
static struct lworld l;
@@ -297,6 +308,11 @@ static struct rres r_case(const struct rcase *c, int rp, unsigned swaps, uint64_
l.early = rp == RP_EARLY || rp == RP_EARLY_NOASSIST || rp == RP_ALL || rp == RP_ALL_PIPE2;
l.assist = l.early && rp != RP_EARLY_NOASSIST ? 250 * US : 0;
l.tmr_period = 500 * US; l.urgent_period = relatch && rp != RP_RELATCH_NOURGENT ? 100 * US : 0;
#if ADRENO_PRESENT_EARLY_DONE
l.complete_at_copy = rcase_copydone; l.ed_hold = rcase_ed_hold;
l.spike_pct = rcase_spike_pct; l.spike_min = rcase_spike_min; l.spike_max = rcase_spike_max;
if (rcase_noassist) l.assist = 0;
#endif
l.multi_pct = c->multi_pct;
l.w.stall_pct = c->stall_pct; l.w.stall_min = c->stall_min; l.w.stall_max = c->stall_max;
l.tmr_at = l.enter_at = l.rl_at = LINF;
@@ -331,6 +347,9 @@ static struct rres r_case(const struct rcase *c, int rp, unsigned swaps, uint64_
x.rl_armed = l.rl.armed; x.rl_go = l.rl.go; x.rl_late = l.rl.late; x.rl_gone = l.rl.gone; x.rl_gated = l.rl.gated;
x.rl_ovt = l.rl.ovt; x.rl_polls = l.rl.polls; x.rl_lead_min_us = tb_ns(&l.w, l.w.tb_off + l.rl.lead_min) / 1000;
memcpy(x.hist, l.rl.hist, sizeof x.hist);
#if ADRENO_PRESENT_EARLY_DONE
x.ed = l.ed;
#endif
char name[128]; snprintf(name, sizeof name, "%s_%s_%s%s", c->name, ws_name[c->ws], rp_name[rp], tag ? tag : "");
const struct lres *r = &x.r;
fprintf(RJ, "%s\"%s\":{\"frames\":%llu,\"torn\":%llu,\"ovt\":%llu,\"beam_forced\":%llu,\"fps_x10\":%llu,\"copies_x10\":%llu,"
@@ -493,4 +512,7 @@ static void relatch_scenarios(const char *dir)
printf("{\"relatch_checks\":%u,\"relatch_failures\":%u,\"relatch_cost_checks\":%u,\"relatch_cost_failures\":%u}\n",
checks - c0, failures - f0, cost_checks - cc0, cost_failures - cf0);
}
#if ADRENO_PRESENT_EARLY_DONE
#include "replay-adreno-early-done.c"
#endif
#endif
+3
View File
@@ -1153,6 +1153,9 @@ int main(int argc,char **argv) {
#endif
#if ADRENO_PRESENT_RELATCH
relatch_scenarios(argv[1]);
#if ADRENO_PRESENT_EARLY_DONE
early_done_scenarios(argv[1]);
#endif
#endif
#endif
#endif
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Summarize a run's AdrenoDisplay perf windows: frame pacing and where a busy window's time went. Host only.
python3 tools/summarize-present-pacing.py build/PREFIX/console.txt [--report build/PREFIX/report.bin] [--json]
A perf window is the set of "AdrenoDisplay: perf <kind> s=<n>" lines one perfLineGated call prints (600 layer-0
copies, AdrenoDisplay/stats.cpp). Windows are split into busy (at least --busy-ibs completed IBs, default 200: about
20 IBs a second) and quiet ones, and each group is reduced to:
- copies per second (median, min) and the copy intervals by the pace line's buckets (<= 17, <= 20, <= 34, <= 50,
> 50 ms; <= 20 ms is on time, the tick firing late; 20-34 ms is one refresh shown twice; more is worse);
- the ticks: ready (a layer-0 copy started in the tick), missed (a frame was on screen, nothing new copied), idle;
missed less the relatch's copies (the head the tick deferred, copied into the same frame) is the share of refreshes
WindowServer did not deliver in time ("true misses");
- per IB: GPU time (the monitor's poll ticks at 19.2 MHz), the kext's submission time with the lock held, its cache
maintenance time and bytes, its resource preparations; the copy time, the tick's lateness and q (swap to copy);
- with the early-completion kext (the perf done line): completions at the copy, late frames, engagements, and the
arrival and lateness histograms summed over the group.
With --report, the GPU's time per clock from the DVFSREP1 record (tools/dvfs_report.py) is added."""
import argparse
import collections
import json
import re
import statistics
import sys
from pathlib import Path
HOST_HZ = 19_200_000 # the monitor's IB tick counter (gtk)
NEED = ('why', 'pace', 'map', 'gpu', 'pipe', 'sync', 'vsync')
def windows(text):
w = collections.defaultdict(dict)
for line in text.split('\n'):
m = re.search(r'AdrenoDisplay: perf why=(\w+) s=(\d+) (.*)', line)
if m:
w[int(m.group(2))]['why'] = dict(re.findall(r'(\w+)=(\S+)', m.group(3)), kind=m.group(1))
continue
m = re.search(r'AdrenoDisplay: perf (\w+) s=(\d+) (.*)', line)
if m:
w[int(m.group(2))][m.group(1)] = dict(re.findall(r'(\w+)=(\S+)', m.group(3)))
return w
def ints(text):
return [int(x) for x in text.split('/')]
def window_row(s, r):
"""One periodic window's numbers, or None (an incomplete or non-periodic window)."""
if not all(k in r for k in NEED) or r['why'].get('kind') != 'periodic':
return None
p, mp, g, pi, sy, v = (r[k] for k in ('pace', 'map', 'gpu', 'pipe', 'sync', 'vsync'))
pp, tk = ints(p['pp']), ints(p['tk'])
span = sum(pp) * int(p['mean']) / 1e6
ib = int(mp['ib'])
gtk, sub, cache, prep = ints(g['gtk']), ints(g['sub_us']), ints(g['cache']), ints(g['prep'])
relatch = r.get('relatch', {})
row = dict(s=s, pps=float(p['pps']), span=span, ib=ib, buckets=pp, ticks=tk[0], ready=tk[1], missed=tk[2], idle=tk[3],
relatched=int(relatch.get('go', 0)), defer=int(sy['defer']),
gpu_ms_ib=gtk[0] / HOST_HZ * 1e3 / ib if ib else 0.0,
gpu_busy=gtk[0] / HOST_HZ / span if span else 0.0,
sub_ms_ib=sub[0] / 1e3 / ib if ib else 0.0, cache_ms_ib=cache[2] / 1e3 / ib if ib else 0.0,
cache_mib_ib=cache[1] / ib if ib else 0.0, prep_ms_ib=prep[1] / 1e3 / ib if ib else 0.0,
copy_us=int(r['why']['mean_us']), late_us=ints(v['grid'])[0], q_us=int(p['q']))
if 'done' in r:
d = r['done']
row['done'] = dict(mode=int(d['mode']), on=int(d['on']), early=int(d['done']), late=int(d['late']),
engages=int(d['eng']), ahead=int(d['ahd']), arr=ints(d['arr']), lat=ints(d['lat']))
return row
def group(rows):
if not rows:
return None
ticks = sum(r['ticks'] for r in rows) or 1
buckets = [sum(r['buckets'][i] for r in rows) for i in range(5)]
intervals = sum(buckets) or 1
med = lambda k: round(statistics.median(r[k] for r in rows), 3)
out = dict(windows=len(rows), pps_median=med('pps'), pps_min=min(r['pps'] for r in rows),
ticks_per_s=round(ticks / sum(r['span'] for r in rows), 2),
ready_pct=round(100 * sum(r['ready'] for r in rows) / ticks, 1),
missed_pct=round(100 * sum(r['missed'] for r in rows) / ticks, 1),
true_miss_pct=round(100 * sum(r['missed'] - r['relatched'] for r in rows) / ticks, 1),
deferred_pct=round(100 * sum(r['defer'] for r in rows) / ticks, 1),
intervals_pct=[round(100 * b / intervals, 1) for b in buckets],
over34_pct=round(100 * (buckets[3] + buckets[4]) / intervals, 1),
ibs_per_s=round(sum(r['ib'] for r in rows) / sum(r['span'] for r in rows), 1))
for k in ('gpu_ms_ib', 'gpu_busy', 'sub_ms_ib', 'cache_ms_ib', 'cache_mib_ib', 'prep_ms_ib', 'copy_us', 'late_us',
'q_us'):
out[k + '_median'] = med(k)
done = [r['done'] for r in rows if 'done' in r]
if done:
out['done'] = dict(modes=sorted({d['mode'] for d in done}), engaged_windows=sum(1 for d in done if d['on']),
early=sum(d['early'] for d in done), late=sum(d['late'] for d in done),
engages=sum(d['engages'] for d in done), ahead=sum(d['ahead'] for d in done),
arrival_2ms_bins=[sum(d['arr'][i] for d in done) for i in range(len(done[0]['arr']))],
late_bins_2_4_8_more=[sum(d['lat'][i] for d in done) for i in range(len(done[0]['lat']))])
return out
def summarize(text, busy_ibs=200):
rows = [x for x in (window_row(s, r) for s, r in sorted(windows(text).items())) if x]
return dict(busy=group([r for r in rows if r['ib'] >= busy_ibs]),
quiet=group([r for r in rows if r['ib'] < busy_ibs]), busy_ibs=busy_ibs)
def gpu_clocks(report):
sys.path.insert(0, str(Path(__file__).resolve().parent))
import dvfs_report
raw = Path(report).read_bytes()
decoded = dvfs_report.decode_block(raw[dvfs_report.OFFSET:dvfs_report.OFFSET + 8 * 190]) # as its main reads it
gpu = decoded.get('gpu', {}) if isinstance(decoded, dict) else {}
keep = ('status', 'ceiling_mhz', 'seconds_at_mhz', 'mean_mhz', 'errors', 'thermal')
return {k: gpu[k] for k in keep if k in gpu}
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('console', type=Path)
ap.add_argument('--report', type=Path)
ap.add_argument('--busy-ibs', type=int, default=200)
ap.add_argument('--json', action='store_true')
a = ap.parse_args()
out = summarize(a.console.read_text(errors='replace'), a.busy_ibs)
if a.report:
out['gpu_clock'] = gpu_clocks(a.report)
if a.json:
print(json.dumps(out, indent=1))
return
for name in ('busy', 'quiet'):
g = out[name]
if not g:
print(f'{name}: no windows')
continue
print(f"{name}: {g['windows']} windows, copies/s median {g['pps_median']} (min {g['pps_min']}), ticks/s "
f"{g['ticks_per_s']}, IBs/s {g['ibs_per_s']}; ticks ready {g['ready_pct']}% missed {g['missed_pct']}% "
f"(true misses {g['true_miss_pct']}%), deferred {g['deferred_pct']}%; intervals <=17/20/34/50/>50 ms "
f"{'/'.join(str(x) for x in g['intervals_pct'])}%, over 34 ms {g['over34_pct']}%")
print(f" per IB: GPU {g['gpu_ms_ib_median']} ms (GPU busy {g['gpu_busy_median']}), submit {g['sub_ms_ib_median']} ms, "
f"cache {g['cache_ms_ib_median']} ms / {g['cache_mib_ib_median']} MiB, preparation {g['prep_ms_ib_median']} ms; "
f"copy {g['copy_us_median']} us, tick late {g['late_us_median']} us, q {g['q_us_median']} us")
if 'done' in g:
d = g['done']
print(f" early completion: modes {d['modes']}, engaged at {d['engaged_windows']} window ends, {d['early']} "
f"completions at the copy, {d['late']} late frames ({d['engages']} engagements), {d['ahead']} arrivals "
f"behind another; arrivals by 2 ms {d['arrival_2ms_bins']}, lateness <2/<4/<8/more "
f"{d['late_bins_2_4_8_more']}")
if 'gpu_clock' in out:
print(f"gpu clock: {json.dumps(out['gpu_clock'])}")
if __name__ == '__main__':
main()
+157
View File
@@ -0,0 +1,157 @@
/* SPDX-License-Identifier: MIT */
/* Host test for gpu/adreno-kext/kext/src/AdrenoDisplayEarlyDone.h (AdrenoDisplay's early completion and arrival
* measurements). No kext, no A16. Times are plain units with one millisecond = 1000.
*
* cc -std=c11 -Wall -Wextra -Werror tools/test-adreno-early-done.c -o test && ./test
* (-DEARLY_DONE_HEADER='"<path>"' tests another copy of the header: tools/test-present-early-done.py runs mutants)
*
* Exit 0 when every check passes; each failure is printed. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef EARLY_DONE_HEADER
#define EARLY_DONE_HEADER "../gpu/adreno-kext/kext/src/AdrenoDisplayEarlyDone.h"
#endif
#include EARLY_DONE_HEADER
static unsigned checks, failures;
#define CHECK(c) \
do { checks++; if (!(c)) { failures++; fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #c); } } while (0)
enum { MSU = 1000, PERIOD = 16667, HOLD = 2000 * MSU };
static struct adreno_early_done fresh(uint32_t mode)
{
struct adreno_early_done e;
adreno_early_init(&e, mode, PERIOD, HOLD, MSU);
return e;
}
/* Modes: OFF never completes early, ALWAYS always, ADAPTIVE only once engaged; an unknown mode is OFF. */
static void modes(void)
{
struct adreno_early_done off = fresh(ADRENO_EARLY_OFF), always = fresh(ADRENO_EARLY_ALWAYS);
struct adreno_early_done adaptive = fresh(ADRENO_EARLY_ADAPTIVE), bad = fresh(7);
CHECK(!adreno_early_active(&off, 1) && !adreno_early_active(&off, 1u << 30));
CHECK(adreno_early_active(&always, 0) && adreno_early_active(&always, 123456789));
CHECK(!adreno_early_active(&adaptive, 5 * MSU));
CHECK(bad.mode == ADRENO_EARLY_OFF && !adreno_early_active(&bad, 5));
struct adreno_early_done z = fresh(ADRENO_EARLY_ADAPTIVE);
adreno_early_init(&z, ADRENO_EARLY_ADAPTIVE, PERIOD, HOLD, 0);
CHECK(z.ms == 1); /* never a zero millisecond (the bins divide by it) */
}
/* Arrivals: the offset after the last proceeding tick in 2 ms bins, the last bin open; none before the first tick. */
static void arrivals(void)
{
struct adreno_early_done e = fresh(ADRENO_EARLY_OFF);
CHECK(!adreno_early_arrival(&e, 5 * MSU, 1) && e.arrivals == 0); /* no tick yet: nothing to measure from */
adreno_early_tick(&e, 100 * MSU, 1, 1);
const uint64_t offsets[] = {0, 1999, 2000, 7 * MSU, 15999, 16 * MSU, 40 * MSU};
const unsigned bins[] = {0, 0, 1, 3, 7, 8, 8};
for (unsigned i = 0; i < sizeof offsets / sizeof offsets[0]; i++) {
struct adreno_early_done x = e;
adreno_early_arrival(&x, 100 * MSU + offsets[i], 1);
CHECK(x.arrivals == 1 && x.arr[bins[i]] == 1);
}
struct adreno_early_done x = e;
CHECK(!adreno_early_arrival(&x, 99 * MSU, 1) && x.arrivals == 0); /* before the tick (a clock race): ignored */
CHECK(!adreno_early_arrival(&x, 105 * MSU, 0) && x.ahead == 1 && x.arrivals == 0); /* behind another: ahead */
}
/* Late frames: a tick that had a frame and neither copied nor kept a layer-0 head, then an arrival within one refresh.
* Not late: a served tick, an idle tick (no frame on screen), an arrival a refresh or more later, a second arrival. */
static void late_frames(void)
{
struct adreno_early_done e = fresh(ADRENO_EARLY_OFF);
adreno_early_tick(&e, 1000 * MSU, 1, 0); /* missed */
CHECK(adreno_early_arrival(&e, 1000 * MSU + 3 * MSU, 1) == 1);
CHECK(e.late == 1 && e.lat[1] == 1 && e.engages == 0 && !adreno_early_active(&e, 1004 * MSU));
CHECK(adreno_early_arrival(&e, 1000 * MSU + 5 * MSU, 1) == 0 && e.late == 1); /* once per missed tick */
const uint64_t lateness[] = {500, 2500, 6000, 9000, PERIOD - 1};
const unsigned lbins[] = {0, 1, 2, 3, 3};
for (unsigned i = 0; i < sizeof lateness / sizeof lateness[0]; i++) {
struct adreno_early_done x = fresh(ADRENO_EARLY_OFF);
adreno_early_tick(&x, 50 * MSU, 1, 0);
CHECK(adreno_early_arrival(&x, 50 * MSU + lateness[i], 1) == 1 && x.lat[lbins[i]] == 1);
}
struct adreno_early_done x = fresh(ADRENO_EARLY_OFF);
adreno_early_tick(&x, 50 * MSU, 1, 0);
CHECK(adreno_early_arrival(&x, 50 * MSU + PERIOD, 1) == 0 && x.late == 0); /* a refresh later: not this tick's */
adreno_early_tick(&x, 80 * MSU, 1, 1); /* served: a copy, or a deferred head kept */
CHECK(adreno_early_arrival(&x, 81 * MSU, 1) == 0 && x.late == 0);
adreno_early_tick(&x, 90 * MSU, 0, 0); /* idle: nothing on screen for that refresh */
CHECK(adreno_early_arrival(&x, 91 * MSU, 1) == 0 && x.late == 0);
adreno_early_tick(&x, 100 * MSU, 1, 0);
CHECK(adreno_early_arrival(&x, 101 * MSU, 0) == 0 && x.late == 0 && x.ahead == 1); /* behind another: no late */
}
/* Adaptive: a late frame engages it for `hold`, every later late frame renews it, it ends by itself; ALWAYS and OFF
* never count engagements. */
static void adaptive(void)
{
struct adreno_early_done e = fresh(ADRENO_EARLY_ADAPTIVE);
adreno_early_tick(&e, 10 * MSU, 1, 0);
adreno_early_arrival(&e, 12 * MSU, 1);
CHECK(e.engages == 1 && adreno_early_active(&e, 12 * MSU) && adreno_early_active(&e, 12 * MSU + HOLD - 1));
CHECK(!adreno_early_active(&e, 12 * MSU + HOLD));
adreno_early_tick(&e, 1000 * MSU, 1, 0); /* renewed while engaged: not a new engagement */
adreno_early_arrival(&e, 1001 * MSU, 1);
CHECK(e.engages == 1 && adreno_early_active(&e, 1001 * MSU + HOLD - 1) && !adreno_early_active(&e, 1001 * MSU + HOLD));
adreno_early_tick(&e, 5000 * MSU, 1, 0); /* after it ended: a new engagement */
adreno_early_arrival(&e, 5001 * MSU, 1);
CHECK(e.engages == 2 && adreno_early_active(&e, 5002 * MSU));
adreno_early_reset(&e); /* stop / mode set: nothing engaged, no tick */
CHECK(!adreno_early_active(&e, 5002 * MSU) && e.last_tick == 0 && !e.tick_missed && e.mode == ADRENO_EARLY_ADAPTIVE);
struct adreno_early_done a = fresh(ADRENO_EARLY_ALWAYS);
adreno_early_tick(&a, 10 * MSU, 1, 0);
adreno_early_arrival(&a, 11 * MSU, 1);
CHECK(a.late == 1 && a.engages == 0);
}
/* Windows: counters start over, the tick state and an engagement carry across. */
static void windows(void)
{
struct adreno_early_done e = fresh(ADRENO_EARLY_ADAPTIVE);
adreno_early_tick(&e, 10 * MSU, 1, 0);
adreno_early_arrival(&e, 11 * MSU, 1);
adreno_early_completed(&e);
adreno_early_tick(&e, 27 * MSU, 1, 0);
adreno_early_next_window(&e);
CHECK(!e.early && !e.late && !e.engages && !e.arrivals && !e.ahead && !e.arr[0] && !e.lat[0]);
CHECK(e.last_tick == 27 * MSU && e.tick_missed && adreno_early_active(&e, 30 * MSU));
}
/* The perf done line: at most ADRENO_PERF_LINE_MAX bytes at every cap, and the exact text of a known window. */
static void perf_line(void)
{
char line[512];
struct adreno_early_done big;
memset(&big, 0xff, sizeof big);
int n = adreno_perf_line_done(line, sizeof line, ~0ull, &big, 1);
CHECK(n > 0 && n <= ADRENO_PERF_LINE_MAX);
printf("perf done line at caps: %d bytes\n", n);
struct adreno_early_done e = fresh(ADRENO_EARLY_ADAPTIVE);
adreno_early_tick(&e, 10 * MSU, 1, 0);
adreno_early_arrival(&e, 13 * MSU, 1);
adreno_early_completed(&e);
adreno_early_tick(&e, 27 * MSU, 1, 1);
adreno_early_arrival(&e, 36 * MSU, 1);
adreno_early_arrival(&e, 37 * MSU, 0);
n = adreno_perf_line_done(line, sizeof line, 42, &e, adreno_early_active(&e, 40 * MSU));
CHECK(!strcmp(line, "AdrenoDisplay: perf done s=42 mode=2 on=1 done=1 late=1 eng=1 ahd=1 "
"arr=0/1/0/0/1/0/0/0/0 lat=0/1/0/0"));
printf("%s\n", line);
}
int main(void)
{
modes();
arrivals();
late_frames();
adaptive();
windows();
perf_line();
printf("{\"checks\":%u,\"failures\":%u}\n", checks, failures);
return failures ? 1 : 0;
}
+240
View File
@@ -0,0 +1,240 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Host test for --present-early-done: AdrenoDisplay completes a layer-0 swap right after its scanout copy
(gpu/adreno-kext/kext/src/AdrenoDisplayEarlyDone.h, -DADRENO_PRESENT_EARLY_DONE=1, profile desktop-early-done). No A16.
- the header's unit test (tools/test-adreno-early-done.c, ASan/UBSan) passes, and fails on each deliberate mutant;
- the kext uses the feature only inside ADRENO_PRESENT_EARLY_DONE blocks, and the desktop-early-done profile differs from
desktop-backlight only in AdrenoDisplay.o (built when the private *26A428.h headers are present, else SKIP);
- cycle.py: without the new flags the boot arguments and the AuxKC choice are what they were; with them, the boot-args,
the refusals and the early-done collection (refused until pinned);
- the presentation replay: without the define every output is the define-off build's; with it, the earlier outputs are
unchanged and early-done.json has no failure and no cost failure (--no-replay skips this part, about 40 s).
"""
import argparse
import contextlib
import importlib.util
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import types
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
KEXT = ROOT / 'gpu/adreno-kext/kext'
HEADER = KEXT / 'src/AdrenoDisplayEarlyDone.h'
sys.path.insert(0, str(ROOT / 'tools'))
from cycle_lib import installed_fresh as fresh # noqa: E402
from cycle_lib.common import Refused # noqa: E402
spec = importlib.util.spec_from_file_location('cycle_cli', ROOT / 'tools/cycle.py')
cli = importlib.util.module_from_spec(spec)
spec.loader.exec_module(cli)
CC = ['cc', '-std=c11', '-Wall', '-Wextra', '-Werror', '-fsanitize=address,undefined']
# (name, anchor in the header, replacement): each must make the unit test fail.
MUTANTS = [
('late counted twice', ' e->tick_missed = 0; /* one late frame per missed tick */\n', ''),
('late window too wide', 'const int late = e->tick_missed && d < e->period;',
'const int late = e->tick_missed && d < 2 * e->period;'),
('idle ticks missed', 'e->tick_missed = had_frame && !served;', 'e->tick_missed = !served;'),
('no renewal', ' e->until = now + e->hold;', ' if (!e->until) e->until = now + e->hold;'),
('hold one tick long', 'e->until && now < e->until;', 'e->until && now <= e->until;'),
('ahead not counted', 'if (!alone) { e->ahead++; return 0; }', 'if (!alone) return 0;'),
('always is adaptive', 'if (e->mode == ADRENO_EARLY_ALWAYS) return 1;', ''),
('arrival bins of 1 ms', 'two = 2 * e->ms;', 'two = e->ms;'),
]
REPLAY_DEFINES = ['-DADRENO_BEAM=1', '-DADRENO_PRESENT_CURSOR_STAGE=1', '-DADRENO_LATCH=1', '-DADRENO_PRESENT_SYNC=1',
'-DADRENO_PIPELINE=1', '-DADRENO_PRESENT_RELATCH=1']
RUN_REPLAY = True
def run_unit(header=None, out=None):
with tempfile.TemporaryDirectory() as d:
binary = Path(d) / 'early-done'
extra = [f'-DEARLY_DONE_HEADER="{header}"'] if header else []
build = subprocess.run(CC + extra + [str(ROOT / 'tools/test-adreno-early-done.c'), '-o', str(binary)],
capture_output=True, text=True)
if build.returncode:
return 'build', build.stderr
r = subprocess.run([str(binary)], capture_output=True, text=True, cwd=ROOT / 'tools')
return r.returncode, r.stdout + r.stderr
def scope_violations(path, pattern, define):
"""Lines matching pattern outside every '#if...define' block (or in its #else)."""
stack, bad = [], []
for n, line in enumerate(path.read_text().split('\n'), 1):
s = line.strip()
if re.match(r'#\s*if', s):
stack.append([define in s and not s.startswith('#ifndef'), s])
continue
if re.match(r'#\s*(else|elif)', s) and stack:
stack[-1][0] = False
continue
if re.match(r'#\s*endif', s) and stack:
stack.pop()
continue
if s.startswith('*') or s.startswith('/*') or s.startswith('//'):
continue
code = re.sub(r'//.*|/\*.*?(\*/|$)', '', s)
if re.search(pattern, code) and not any(on for on, _ in stack) and not re.match(r'#\s*define', s):
bad.append(f'{path.relative_to(ROOT)}:{n}: {s}')
return bad
class Header(unittest.TestCase):
def test_unit_passes(self):
code, out = run_unit()
self.assertEqual(code, 0, out)
self.assertIn('"failures":0', out)
def test_mutants_fail(self):
text = HEADER.read_text()
for name, old, new in MUTANTS:
with self.subTest(name), tempfile.TemporaryDirectory() as d:
self.assertEqual(text.count(old), 1, f'{name}: anchor not found once')
mutant = Path(d) / 'AdrenoDisplayEarlyDone.h'
mutant.write_text(text.replace(old, new))
code, out = run_unit(mutant)
self.assertNotEqual(code, 0, f'{name}: the unit test passed a mutant\n{out}')
class Kext(unittest.TestCase):
def test_scope(self):
bad = []
for path in sorted((KEXT / 'src').rglob('*')):
if path.suffix in ('.cpp', '.h') and path.name != 'AdrenoDisplayEarlyDone.h':
bad += scope_violations(path, r'\bfEarly\b|\bfScreenHeld\b|adreno_early_|earlyDoneNowGated|'
r'AdrenoDisplayEarlyDone\.h', 'ADRENO_PRESENT_EARLY_DONE')
self.assertEqual(bad, [])
def test_profile(self):
text = (KEXT / 'profiles/desktop-early-done.sh').read_text()
self.assertIn('. "$D/profiles/desktop-backlight.sh"', text)
self.assertEqual(re.findall(r'^U_\w+=.*$', text, re.M),
['U_AdrenoDisplay="$U_AdrenoDisplay -DADRENO_PRESENT_EARLY_DONE=1"'])
def test_profile_build(self):
if not list((KEXT / 'include').glob('*26A428.h')) or not shutil.which('xcrun'):
self.skipTest('SKIP: the private *26A428.h headers or Xcode are not present')
with tempfile.TemporaryDirectory() as d:
outs = {}
for profile in ('desktop-backlight', 'desktop-early-done'):
out = Path(d) / profile
r = subprocess.run(['sh', str(KEXT / 'build.sh')], capture_output=True, text=True,
env=dict(os.environ, OUT=str(out), PROFILE=profile, ZERO_AR_DATE='1'))
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
outs[profile] = out
a, b = outs['desktop-backlight'] / 'obj', outs['desktop-early-done'] / 'obj'
differ = sorted(o.name for o in a.iterdir() if o.read_bytes() != (b / o.name).read_bytes())
self.assertEqual(differ, ['AdrenoDisplay.o'])
da = (outs['desktop-backlight'] / 'DEFINES.txt').read_text().split('\n')
db = (outs['desktop-early-done'] / 'DEFINES.txt').read_text().split('\n')
changed = [(x, y) for x, y in zip(da, db) if x != y]
self.assertEqual(len(changed), 2) # the profile name and AdrenoDisplay's line
self.assertTrue(changed[1][1].startswith('AdrenoDisplay: ') and
changed[1][1] == changed[1][0] + ' -DADRENO_PRESENT_EARLY_DONE=1')
def chain(**kw):
base = dict(variant='fast', input=True, input_touchpad=True, input_ptp=True, present_dartpool=True,
present_resident=True, present_beam=True, present_va_reclaim=True, gpu_va_wide=True,
present_latch=True, edp_backlight=True)
base.update(kw)
return types.SimpleNamespace(**base)
class Cycle(unittest.TestCase):
def test_default_unchanged(self):
args = chain()
self.assertEqual(fresh.selected(args)[1], fresh.INPUT_AUXKC_DESKTOP_BACKLIGHT)
self.assertNotIn('adreno_present_early', fresh.boot_addition(args))
next3 = json.loads((ROOT.parent.parent / 'run/desktop-run-args-next3.json').read_text()) \
if (ROOT.parent.parent / 'run/desktop-run-args-next3.json').is_file() else None
if next3:
a = cli.parse(next3 + ['--check'])
self.assertNotIn('adreno_present_early', fresh.boot_addition(a))
self.assertEqual(fresh.selected(a)[1], fresh.INPUT_AUXKC_DESKTOP_BACKLIGHT)
def test_boot_args(self):
for mode in (0, 1, 2):
self.assertTrue(fresh.boot_addition(chain(present_early_done=mode)).endswith(
f' adreno_present_early_done={mode}'))
self.assertTrue(fresh.boot_addition(chain(present_early_done=2, present_early_hold_ms=5000)).endswith(
' adreno_present_early_done=2 adreno_present_early_hold_ms=5000'))
self.assertIn('adreno_present_early_done=0', fresh.boot_addition(chain(present_early_done=0,
present_hold='on')))
def test_refusals(self):
for bad in (dict(present_early_done=3), dict(present_early_done=1, present_hold='on'),
dict(present_early_done=2, present_hold='on'), dict(present_early_done=1, present_early_hold_ms=500),
dict(present_early_hold_ms=500), dict(present_early_done=2, present_early_hold_ms=99),
dict(present_early_done=2, present_early_hold_ms=10001), dict(present_early_done=1, edp_backlight=False)):
with self.subTest(bad), self.assertRaises(Refused):
fresh.boot_addition(chain(**bad))
with self.assertRaises(Refused): # without the --edp-backlight chain: no collection has it
fresh.selected(types.SimpleNamespace(variant='fast', input=True, present_early_done=1))
def test_collection(self):
for kw in (dict(present_early_done=0), dict(present_early_done=2), dict(gpu_coherent=True)):
with self.subTest(kw):
if fresh.INPUT_AUXKC_DESKTOP_EARLY_DONE is None:
with self.assertRaises(Refused):
fresh.selected(chain(**kw))
else:
self.assertEqual(fresh.selected(chain(**kw))[1:3], (fresh.INPUT_AUXKC_DESKTOP_EARLY_DONE,
fresh.INPUT_AUXKC_DESKTOP_EARLY_DONE_SHA))
# --gpu-coherent without --edp-backlight keeps its collection (the latch chain's)
self.assertEqual(fresh.selected(chain(edp_backlight=False, gpu_coherent=True))[1],
fresh.INPUT_AUXKC_PTP_DARTPOOL_RESIDENT_BEAM_VA_WIDE_LATCH)
def test_cli(self):
base = ['installed-fresh', '--prefix', 'x', '--variant', 'fast', '--check', '--edp-backlight']
a = cli.parse(base + ['--present-early-done', '2', '--present-early-hold-ms', '3000'])
self.assertEqual((a.present_early_done, a.present_early_hold_ms), (2, 3000))
self.assertIsNone(cli.parse(base).present_early_done)
with self.assertRaises(SystemExit), open(os.devnull, 'w') as quiet, contextlib.redirect_stderr(quiet):
cli.parse(base + ['--present-early-done', '5'])
class Replay(unittest.TestCase):
def test_replay(self):
if not RUN_REPLAY:
self.skipTest('SKIP: --no-replay')
with tempfile.TemporaryDirectory() as d:
outs = {}
for name, extra in (('off', []), ('on', ['-DADRENO_PRESENT_EARLY_DONE=1'])):
binary = Path(d) / f'replay-{name}'
build = subprocess.run(['xcrun', 'clang', '-std=c11', '-O2', '-Wall', '-Wextra', '-Werror'] +
REPLAY_DEFINES + extra + ['tools/replay-adreno-presentation.c', '-o', str(binary)],
cwd=ROOT, capture_output=True, text=True)
self.assertEqual(build.returncode, 0, build.stderr)
out = Path(d) / name
out.mkdir()
r = subprocess.run([str(binary), str(out)], capture_output=True, text=True)
self.assertEqual(r.returncode, 0, r.stdout[-2000:] + r.stderr[-2000:])
outs[name] = (out, r.stdout)
off, on = outs['off'][0], outs['on'][0]
for j in sorted(off.glob('*.json')):
self.assertEqual(j.read_bytes(), (on / j.name).read_bytes(), f'{j.name} changed with the define')
self.assertFalse((off / 'early-done.json').exists())
result = json.loads((on / 'early-done.json').read_text())
self.assertEqual((result['failures'], result['cost_failures']), (0, 0))
self.assertGreater(result['checks'], 0)
for line in outs['on'][1].split('\n'):
if line.startswith('early-done ') and 'arrivals' not in line:
print(line)
if __name__ == '__main__':
ap = argparse.ArgumentParser(add_help=False)
ap.add_argument('--no-replay', action='store_true')
known, rest = ap.parse_known_args()
RUN_REPLAY = not known.no_replay
unittest.main(argv=[sys.argv[0]] + rest)
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Host test for tools/summarize-present-pacing.py on a synthetic console: two complete periodic windows (one busy, one
quiet, the quiet one with the early-completion perf done line), one incomplete window and one reject window, which
must both be left out."""
import importlib.util
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location('summarize', ROOT / 'tools/summarize-present-pacing.py')
tool = importlib.util.module_from_spec(spec)
spec.loader.exec_module(tool)
def window(s, ib, pp, mean, tk, gtk, sub, cache, prep, go, defer, done=None, why='periodic'):
lines = [
f'AdrenoDisplay: perf why={why} s={s} presents=600 copies=600 win=600 mean_us=2000 max_us=3000 all_mean_us=2000 '
'all_max_us=9000',
f'AdrenoDisplay: perf pace s={s} pps=50.0 pp={pp} mean={mean} p95=34 tk={tk} late=700 off=0 gpu=1/2000 q=6000',
f'AdrenoDisplay: perf vsync s={s} period_ns=16666666 grid=900/3000 early=1/100 skip=0 rs=0/0 lead=900 arm=1/0 '
'unarmed=0 ticks=1',
f'AdrenoDisplay: perf sync s={s} defer={defer} swaps={defer} forced=0 max_us=2000 marks=1 busy=1 ovf=0 stale=0 unm=0',
f'AdrenoDisplay: perf pipe s={s} on=3 fl={ib} ovl=0 imm=0 sync=0 fold=0 drain=0 tmr=0 polls=0 spin_us=0 '
'gap_us=0 max_ms=1 fail=0',
f'AdrenoDisplay: perf relatch s={s} on=7 arm={go} go={go} late=0 gone=0 gate=0 polls=0 ovt=0 lead=0/0 wait=0 '
'rel=0/0/0/0/0/0/0/0 ef=0/0/0 unm=0',
f'AdrenoDisplay: perf map s={s} ins=0 pg=0/0/0 clr=0 sfree=0 pool=0/0/0/0 va=0/0/0/0 ib={ib}',
f'AdrenoDisplay: perf gpu s={s} gtk={gtk}/0 sub_us={sub}/0 prep={prep} unw=0/0 cache={cache} res=0/0/0/0 '
'mib=0/0',
]
if done:
lines.append(f'AdrenoDisplay: perf done s={s} {done}')
return lines
class Summary(unittest.TestCase):
def test_groups(self):
text = '\n'.join(
window(1, 600, '300/150/100/40/10', 20000, '600/400/180/20', 19200000, 1200000, '60000/60000/1800000',
'120000/600000', 60, 70) +
window(2, 30, '500/90/10/0/0', 16900, '600/590/8/2', 192000, 30000, '3000/1000/30000', '3000/60000', 0, 1,
done='mode=2 on=1 done=5 late=2 eng=1 ahd=3 arr=1/2/3/4/5/6/7/8/9 lat=1/0/1/0') +
window(3, 600, '1/0/0/0/0', 1, '1/1/0/0', 1, 1, '1/1/1', '1/1', 0, 0)[:3] + # incomplete: left out
window(4, 600, '1/0/0/0/0', 1, '1/1/0/0', 1, 1, '1/1/1', '1/1', 0, 0, why='reject'))
out = tool.summarize(text, 200)
busy, quiet = out['busy'], out['quiet']
self.assertEqual((busy['windows'], quiet['windows']), (1, 1))
self.assertEqual(busy['intervals_pct'], [50.0, 25.0, 16.7, 6.7, 1.7])
self.assertEqual(busy['over34_pct'], 8.3)
self.assertEqual((busy['ready_pct'], busy['missed_pct'], busy['true_miss_pct']), (66.7, 30.0, 20.0))
self.assertAlmostEqual(busy['gpu_ms_ib_median'], 1.667, places=3) # 1 s of host ticks over 600 IBs
self.assertAlmostEqual(busy['sub_ms_ib_median'], 2.0, places=3)
self.assertAlmostEqual(busy['cache_ms_ib_median'], 3.0, places=3)
self.assertAlmostEqual(busy['cache_mib_ib_median'], 100.0, places=3)
self.assertAlmostEqual(busy['prep_ms_ib_median'], 1.0, places=3)
self.assertNotIn('done', busy)
self.assertEqual(quiet['done'], dict(modes=[2], engaged_windows=1, early=5, late=2, engages=1, ahead=3,
arrival_2ms_bins=[1, 2, 3, 4, 5, 6, 7, 8, 9], late_bins_2_4_8_more=[1, 0, 1, 0]))
def test_empty(self):
self.assertEqual(tool.summarize('nothing here'), dict(busy=None, quiet=None, busy_ibs=200))
if __name__ == '__main__':
unittest.main()