Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F169562442
D57631.id179976.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
41 KB
Referenced Files
None
Subscribers
None
D57631.id179976.diff
View Options
diff --git a/sys/dev/hwpmc/hwpmc_pmu.h b/sys/dev/hwpmc/hwpmc_pmu.h
new file mode 100644
--- /dev/null
+++ b/sys/dev/hwpmc/hwpmc_pmu.h
@@ -0,0 +1,320 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * hwpmc PMU grouping/multiplex layer. This is a lightweight virtual
+ * layer that lets multiple events be programmed atomically (group)
+ * and, when there are not enough hardware rows, time-shared
+ * (multiplex). It does NOT replace the existing pcd_allocate_pmc /
+ * pcd_release_pmc / pcd_start_pmc / pcd_stop_pmc paths; it only defers
+ * those calls until the group is started, and drives them via a
+ * sorted, constraint-aware assigner.
+ */
+
+#ifndef _DEV_HWPMC_PMU_H_
+#define _DEV_HWPMC_PMU_H_
+
+#include <sys/pmc.h>
+#include <sys/queue.h>
+
+#define PMC_ROW_IS_UNASSIGNED(P) (PMC_TO_ROWINDEX(P) == PMC_ROW_UNASSIGNED)
+
+#ifdef _KERNEL
+
+/*
+ * Mode flags shared by the hwpmc process/thread descriptor lookup
+ * helpers. Originally local to hwpmc_mod.c; promoted to this header
+ * so the PMU grouping layer (hwpmc_pmu.c) can use the same names.
+ */
+enum pmc_flags {
+ PMC_FLAG_NONE = 0x00, /* do nothing */
+ PMC_FLAG_REMOVE = 0x01, /* atomically remove entry from hash */
+ PMC_FLAG_ALLOCATE = 0x02, /* add entry to hash if not found */
+ PMC_FLAG_NOWAIT = 0x04, /* do not wait for mallocs */
+};
+
+/*
+ * Per-event lifecycle state. Events transition INACTIVE -> ACTIVE on
+ * a successful pmu_group_on_start, and back to INACTIVE on csw_out
+ * (when multiplexed) or on group release.
+ */
+enum pmu_event_state {
+ PMU_EVENT_STATE_INACTIVE = 0,
+ PMU_EVENT_STATE_ACTIVE,
+ PMU_EVENT_STATE_ERROR,
+};
+
+/*
+ * Scheduling constraint emitted by the per-class backend. pc_allowed_rows
+ * is a bitmask over the backend-defined row index namespace ("row 0..N-1
+ * for this class on this CPU"), not a raw MSR/RDPMC number.
+ */
+#define PMC_SC_F_FIXED 0x0001 /* must use pc_fixed_row */
+#define PMC_SC_F_EXCLUSIVE 0x0002 /* needs the whole PMU exclusively */
+#define PMC_SC_F_SHARED 0x0004 /* tolerates sharing siblings */
+
+struct pmc_sched_constraint {
+ uint32_t pc_allowed_rows; /* bitmask of legal rows */
+ uint8_t pc_weight; /* popcount(pc_allowed_rows) */
+ uint8_t pc_fixed_row; /* used iff PMC_SC_F_FIXED */
+ uint16_t pc_flags; /* PMC_SC_F_* */
+};
+typedef struct pmc_sched_constraint pmc_sched_constraint_t;
+
+/*
+ * pmu_event wraps a single struct pmc with grouping/multiplex state.
+ * Time fields use sbintime_t units and are only updated when the parent
+ * group is multiplexing (pg_mux); for plain grouped allocations they
+ * stay at zero and the read path returns the raw hardware count.
+ */
+struct pmu_event {
+ TAILQ_ENTRY(pmu_event) pe_sibling;
+ pmu_group_t *pe_group;
+ struct pmc *pe_pmc;
+ enum pmu_event_state pe_state;
+ bool pe_is_leader;
+ struct pmc_op_pmcallocate pe_alloc;
+ pmc_sched_constraint_t pe_cons;
+ uint64_t pe_time_enabled;
+ uint64_t pe_time_running;
+ uint64_t pe_count_at_in;
+ int pe_assigned_row;
+};
+
+/*
+ * pmu_group is the unit of all-or-none scheduling. Either every
+ * sibling is bound to a HW row and attached to the target process
+ * (pg_assigned == true == "scheduled in") or none of them is (pg_assigned
+ * == false == "deferred"). When the union of all groups attached to
+ * one pp exceeds the HW counter budget the per-pp rotation kthread
+ * evicts the front-most scheduled group atomically and tries to bring
+ * in any deferred group that now fits. Within a group placement is
+ * never partial -- every sibling is on a HW row whenever pg_assigned.
+ */
+struct pmu_group {
+ LIST_ENTRY(pmu_group) pg_owner_next;
+ LIST_ENTRY(pmu_group) pg_proc_next;
+ uint32_t pg_id;
+ struct pmc_owner *pg_owner;
+ pmu_event_t *pg_leader;
+ TAILQ_HEAD(, pmu_event) pg_events;
+ u_int pg_nevents;
+ bool pg_committed;
+ bool pg_assigned; /* SCHEDULED if true */
+ bool pg_running; /* between start/stop */
+ bool pg_defer_ok; /* PMC_F_GROUP_MUX hint */
+ struct proc *pg_attach_proc;
+ struct pmc_process *pg_pp; /* TARGET pp we hung off
+ * pp_pmu_groups; cleared on
+ * release. */
+ uint32_t pg_used_rows_mask;
+};
+
+/*
+ * Hooks exported by hwpmc_mod.c so that hwpmc_pmu.c and hwpmc_assign.c
+ * stay file-local for the rest of the hwpmc module.
+ */
+struct pmc_mdep *hwpmc_get_mdep(void);
+struct pmc_classdep *hwpmc_ri_to_classdep(int ri, int *adjri);
+bool hwpmc_can_allocate_row(int ri, enum pmc_mode mode);
+bool hwpmc_can_allocate_rowindex(struct proc *p, unsigned int ri, int cpu);
+void hwpmc_mark_row_thread(int ri);
+void hwpmc_mark_row_free(int ri);
+
+/*
+ * The PMU grouping/multiplex scheduler is currently implemented only on
+ * architectures whose backend can describe per-event scheduling
+ * constraints (x86/AMD today; see amd_get_sched_constraint()). There
+ * hwpmc_pmu.c and hwpmc_assign.c are compiled in and export the real
+ * entry points below. Everywhere else the architecture-independent
+ * hwpmc_mod.c is linked against the no-op stubs in the #else branch, so
+ * the module builds and behaves exactly as it did before the grouping
+ * layer was added.
+ */
+#if defined(__amd64__) || defined(__i386__)
+#define HWPMC_PMU_GROUPS 1
+#endif
+
+#ifdef HWPMC_PMU_GROUPS
+
+/*
+ * Per-class constraint provider. Implemented for AMD in hwpmc_amd.c;
+ * Intel/ARM keep returning EOPNOTSUPP until they are wired in.
+ */
+int amd_can_assign_pmc(int ri, struct pmc *pm,
+ const struct pmc_op_pmcallocate *a);
+int amd_get_sched_constraint(struct pmc *pm,
+ const struct pmc_op_pmcallocate *a, pmc_sched_constraint_t *cons);
+
+/*
+ * Assigner (hwpmc_assign.c). Scheduling is strictly all-or-none at
+ * the group level: pmu_assign_group either places every sibling on a
+ * HW row or rolls back; pmu_unassign_group releases every row in one
+ * pass. No partial-placement helpers are exposed -- the inter-group
+ * rotation in hwpmc_pmu.c achieves multiplexing by atomically
+ * scheduling out one group and scheduling in another, never by
+ * splitting a single group across windows.
+ */
+int pmu_assign_group(pmu_group_t *pg, struct proc *p, int cpu);
+void pmu_unassign_group(pmu_group_t *pg, int cpu);
+u_int pmu_count_core_hw_slots(pmu_group_t *pg, struct proc *p);
+u_int pmu_count_class_total(pmu_group_t *pg);
+int pmu_validate_group(pmu_group_t *pg);
+int pmu_event_get_constraint(pmu_event_t *pe,
+ pmc_sched_constraint_t *cons);
+
+/*
+ * Group lifecycle (hwpmc_pmu.c).
+ */
+int pmu_group_create(struct pmc_owner *po, uint32_t *pg_id);
+int pmu_group_add(pmu_group_t *pg, struct pmc *pm, bool leader);
+int pmu_group_commit(pmu_group_t *pg);
+pmu_group_t *pmu_group_lookup(struct pmc_owner *po, uint32_t pg_id);
+void pmu_group_release(pmu_group_t *pg);
+void pmu_event_destroy(pmu_event_t *pe);
+pmu_event_t *pmu_event_from_pmc(struct pmc *pm);
+
+int pmu_group_on_allocate(struct pmc *pm, const struct pmc_op_pmcallocate *pa);
+int pmu_group_on_start(struct pmc *pm);
+void pmu_group_on_stop(struct pmc *pm);
+void pmu_group_on_release(struct pmc *pm);
+void pmu_group_csw_in(struct thread *td, struct pmc_process *pp);
+void pmu_group_csw_out(struct thread *td, int cpu);
+int pmu_group_read_value(struct pmc *pm, pmc_value_t *value);
+
+/*
+ * Cleanup hook for callers that are about to free a pmc_process out from
+ * under the PMU layer (target-process exit, descriptor destroy). Tears
+ * down the per-pp rotation kthread (which holds pp as its arg) and
+ * unhooks every pmu_group still hanging off pp->pp_pmu_groups so the
+ * eventual pmu_group_on_release won't dereference freed memory. Caller
+ * must hold pmc_sx exclusive.
+ */
+void pmu_pp_release_all(struct pmc_process *pp);
+
+/*
+ * Multiplex time-accounting / rotation helpers. Active when pg_mux is
+ * set; otherwise these are no-ops on the hot path.
+ */
+void pmu_event_account_in(pmu_event_t *pe, uint64_t now);
+void pmu_event_account_out(pmu_event_t *pe, uint64_t now);
+void pmu_rotate_groups(int cpu);
+
+#else /* !HWPMC_PMU_GROUPS */
+
+/*
+ * No-op stubs for architectures that do not compile in the PMU grouping
+ * layer. Userland never sets the PMC_F_GROUP_* flags or issues the
+ * group ioctls on such kernels, so allocate/create paths fail cleanly
+ * with EOPNOTSUPP and the context-switch / release hooks are nothing
+ * but cheap no-ops -- behaviour is identical to a pre-grouping hwpmc.
+ */
+static __inline int
+pmu_group_on_allocate(struct pmc *pm __unused,
+ const struct pmc_op_pmcallocate *pa __unused)
+{
+ return (EOPNOTSUPP);
+}
+
+static __inline int
+pmu_group_on_start(struct pmc *pm __unused)
+{
+ return (0);
+}
+
+static __inline void
+pmu_group_on_stop(struct pmc *pm __unused)
+{
+}
+
+static __inline void
+pmu_group_on_release(struct pmc *pm __unused)
+{
+}
+
+static __inline void
+pmu_group_csw_in(struct thread *td __unused, struct pmc_process *pp __unused)
+{
+}
+
+static __inline void
+pmu_group_csw_out(struct thread *td __unused, int cpu __unused)
+{
+}
+
+static __inline int
+pmu_group_read_value(struct pmc *pm __unused, pmc_value_t *value __unused)
+{
+ return (EOPNOTSUPP);
+}
+
+static __inline int
+pmu_group_create(struct pmc_owner *po __unused, uint32_t *pg_id __unused)
+{
+ return (EOPNOTSUPP);
+}
+
+static __inline int
+pmu_group_add(pmu_group_t *pg __unused, struct pmc *pm __unused,
+ bool leader __unused)
+{
+ return (EOPNOTSUPP);
+}
+
+static __inline int
+pmu_group_commit(pmu_group_t *pg __unused)
+{
+ return (EOPNOTSUPP);
+}
+
+static __inline pmu_group_t *
+pmu_group_lookup(struct pmc_owner *po __unused, uint32_t pg_id __unused)
+{
+ return (NULL);
+}
+
+static __inline void
+pmu_pp_release_all(struct pmc_process *pp __unused)
+{
+}
+
+#endif /* HWPMC_PMU_GROUPS */
+
+/*
+ * Hooks exported into hwpmc_mod.c so the PMU layer can find/unlink a
+ * pmc_process descriptor without reaching into mod.c internals.
+ */
+struct pmc_process *pmc_find_process_descriptor_pmu(struct proc *p,
+ uint32_t mode);
+void pmc_unlink_target_process_pmu(struct pmc *pm, struct pmc_process *pp);
+
+void hwpmc_pmu_sx_xlock(void);
+void hwpmc_pmu_sx_xunlock(void);
+int hwpmc_pmu_sx_sleep(void *chan, int timo, const char *wmesg);
+void hwpmc_pmu_sx_assert_xlocked(void);
+pmc_value_t hwpmc_pmc_read_delta(int cpu, int ri, struct pmc *pm);
+void hwpmc_pmu_accumulate_remove(int cpu, int ri, struct pmc *pm,
+ struct pmc_process *pp);
+
+/*
+ * Rotation helpers (hwpmc_mod.c). These manipulate pp_pmcs[ri]
+ * and the pmc_target list directly so the multiplex rotation kthread
+ * can swap which sibling owns each HW row without going through
+ * pmc_attach_one_process / pmc_release_pmc_descriptor (which would
+ * also stop-the-world / signal SIGIO). Cumulative counts are kept
+ * in pm->pm_gv.pm_savedvalue exactly as the framework's csw_out path
+ * already maintains them, so detach/attach do not move counters.
+ * pmc_rotation_drain spins until pm->pm_runcount drops to zero so
+ * the swap is safe.
+ */
+void pmc_rotation_drain(struct pmc *pm);
+void pmc_rotation_detach(struct pmc *pm, struct pmc_process *pp);
+void pmc_rotation_attach(struct pmc *pm, struct pmc_process *pp);
+
+/* Set by hwpmc_mod.c at pmc_initialize() time. */
+extern int (*hwpmc_pmu_attach_p)(struct proc *p, struct pmc *pm);
+
+#endif /* _KERNEL */
+
+#endif /* _DEV_HWPMC_PMU_H_ */
diff --git a/sys/dev/hwpmc/hwpmc_pmu.c b/sys/dev/hwpmc/hwpmc_pmu.c
new file mode 100644
--- /dev/null
+++ b/sys/dev/hwpmc/hwpmc_pmu.c
@@ -0,0 +1,1006 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * hwpmc PMU layer: event groups, all-or-none commit, time accounting and
+ * round-robin multiplex rotation when nevents > available HW slots. The
+ * heavy lifting (per-class hardware row binding, KASSERT-checked AMD
+ * sub-class match, etc.) lives in hwpmc_assign.c and the per-class
+ * back-ends (hwpmc_amd.c, hwpmc_intel.c).
+ */
+
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/kernel.h>
+#include <sys/kthread.h>
+#include <sys/malloc.h>
+#include <sys/pmc.h>
+#include <sys/proc.h>
+#include <sys/sysctl.h>
+#include <sys/sx.h>
+#include <sys/unistd.h>
+
+#include "hwpmc_pmu.h"
+
+static MALLOC_DEFINE(M_PMU, "pmu", "hwpmc PMU grouping");
+
+static uint32_t pmu_next_group_id = 1;
+
+SYSCTL_DECL(_kern_hwpmc);
+
+/*
+ * Multiplex rotation period. Default 50ms balances counter accuracy
+ * (longer windows -> more stable counts per sibling) against fairness
+ * (shorter windows -> every sibling gets time on HW within a few
+ * scheduler quanta). Tunable so tests with very long or very short
+ * runtimes can adjust.
+ */
+static int pmu_mux_period_ms = 50;
+SYSCTL_INT(_kern_hwpmc, OID_AUTO, mux_period_ms, CTLFLAG_RWTUN,
+ &pmu_mux_period_ms, 0,
+ "PMU multiplex rotation period in milliseconds");
+
+static void pmu_pp_rotate_thread(void *arg);
+static int pmu_pp_schedule_in(struct pmc_process *pp, pmu_group_t *pg);
+static void pmu_pp_schedule_out(struct pmc_process *pp, pmu_group_t *pg);
+static void pmu_pp_kick_rotate(struct pmc_process *pp);
+static int pmu_group_attach_siblings(pmu_group_t *pg,
+ struct pmc_process *pp);
+
+pmu_event_t *
+pmu_event_from_pmc(struct pmc *pm)
+{
+ return (pm != NULL ? pm->pm_pmu : NULL);
+}
+
+void
+pmu_event_destroy(pmu_event_t *pe)
+{
+ if (pe == NULL)
+ return;
+ free(pe, M_PMU);
+}
+
+int
+pmu_group_create(struct pmc_owner *po, uint32_t *pg_id)
+{
+ pmu_group_t *pg;
+
+ if (po == NULL || pg_id == NULL)
+ return (EINVAL);
+
+ pg = malloc(sizeof(*pg), M_PMU, M_WAITOK | M_ZERO);
+ pg->pg_id = pmu_next_group_id++;
+ pg->pg_owner = po;
+ TAILQ_INIT(&pg->pg_events);
+ LIST_INSERT_HEAD(&po->po_groups, pg, pg_owner_next);
+ *pg_id = pg->pg_id;
+ return (0);
+}
+
+pmu_group_t *
+pmu_group_lookup(struct pmc_owner *po, uint32_t pg_id)
+{
+ pmu_group_t *pg;
+
+ LIST_FOREACH(pg, &po->po_groups, pg_owner_next) {
+ if (pg->pg_id == pg_id)
+ return (pg);
+ }
+ return (NULL);
+}
+
+int
+pmu_group_add(pmu_group_t *pg, struct pmc *pm, bool leader)
+{
+ pmu_event_t *pe;
+
+ if (pg == NULL || pm == NULL)
+ return (EINVAL);
+ if (pg->pg_committed) {
+ PMCDBG1(PMC, OPS, 1, "group_add: gid=%u already committed",
+ pg->pg_id);
+ return (EBUSY);
+ }
+
+ pe = pmu_event_from_pmc(pm);
+ if (pe == NULL) {
+ PMCDBG2(PMC, OPS, 1,
+ "group_add: pm_pmu==NULL pm=%p flags=0x%x",
+ pm, pm->pm_flags);
+ return (EINVAL);
+ }
+ if (!PMC_ROW_IS_UNASSIGNED(pe->pe_pmc)) {
+ PMCDBG2(PMC, OPS, 1,
+ "group_add: already assigned pm=%p id=0x%jx",
+ pm, (uintmax_t)pm->pm_id);
+ return (EBUSY);
+ }
+ if (pe->pe_group == pg)
+ return (0);
+ if (pe->pe_group != NULL) {
+ PMCDBG3(PMC, OPS, 1,
+ "group_add: pe %p in gid=%u, requested gid=%u",
+ pe, pe->pe_group->pg_id, pg->pg_id);
+ return (EBUSY);
+ }
+
+ pe->pe_group = pg;
+ pe->pe_is_leader = leader;
+ TAILQ_INSERT_TAIL(&pg->pg_events, pe, pe_sibling);
+ pg->pg_nevents++;
+
+ if (leader)
+ pg->pg_leader = pe;
+ else if (pg->pg_leader == NULL)
+ pg->pg_leader = pe;
+
+ return (0);
+}
+
+int
+pmu_group_commit(pmu_group_t *pg)
+{
+ pmu_event_t *pe;
+ struct pmc_owner *po;
+ struct proc *p;
+ u_int hw_slots, class_total;
+ int error;
+
+ if (pg == NULL)
+ return (EINVAL);
+ if (pg->pg_committed)
+ return (0);
+
+ error = pmu_validate_group(pg);
+ if (error != 0) {
+ PMCDBG3(PMC, OPS, 1,
+ "group_commit: validate gid=%u err=%d nevents=%u",
+ pg->pg_id, error, pg->pg_nevents);
+ return (error);
+ }
+
+ /*
+ * Pre-compute each event's constraint up front so the assigner
+ * can sort by weight without recomputing.
+ */
+ TAILQ_FOREACH(pe, &pg->pg_events, pe_sibling) {
+ error = pmu_event_get_constraint(pe, &pe->pe_cons);
+ if (error != 0) {
+ PMCDBG3(PMC, OPS, 1,
+ "group_commit: constraint err=%d class=%d sub=%d",
+ error, (int)pe->pe_alloc.pm_class,
+ (int)pe->pe_alloc.pm_md.pm_amd.pm_amd_sub_class);
+ return (error);
+ }
+ }
+
+ pg->pg_defer_ok = (pg->pg_leader->pe_alloc.pm_flags &
+ PMC_F_GROUP_MUX) != 0;
+
+ po = pg->pg_owner;
+ p = po != NULL ? po->po_owner : NULL;
+ if (p != NULL) {
+ class_total = pmu_count_class_total(pg);
+ if (class_total == 0)
+ return (EOPNOTSUPP);
+ /*
+ * Reject groups that can never fit on the hardware no
+ * matter what. Within-group placement is strictly
+ * all-or-none, so a single group with more events than
+ * the entire class has rows is unschedulable forever.
+ */
+ if (pg->pg_nevents > class_total)
+ return (ENOSPC);
+
+ hw_slots = pmu_count_core_hw_slots(pg, p);
+
+ /*
+ * Two scheduling regimes, both atomic:
+ *
+ * 1. The group fits in the rows currently free for this
+ * target proc -- bind every sibling to a HW row right
+ * now. pmc_start(leader) finds an already-scheduled
+ * group and just flips the per-PMC running flag.
+ *
+ * 2. The group does NOT fit at the moment (other groups
+ * already attached to this proc are holding the rows
+ * we would need). If the caller passed
+ * PMC_F_GROUP_MUX on the leader we accept the commit
+ * in the DEFERRED state; the per-pp rotation kthread
+ * will atomically schedule this group in -- ALL of
+ * its siblings, never a subset -- whenever an evicted
+ * peer frees enough rows for it. Without the flag we
+ * preserve the historical strict semantics and reject
+ * with ENOSPC so the caller can pick a smaller group.
+ */
+ PMCDBG5(PMC, OPS, 1,
+ "group_commit: gid=%u nevents=%u class_total=%u "
+ "hw_slots=%u defer_ok=%d", pg->pg_id, pg->pg_nevents,
+ class_total, hw_slots, (int)pg->pg_defer_ok);
+
+ if (pg->pg_nevents <= hw_slots) {
+ error = pmu_assign_group(pg, p, PMC_CPU_ANY);
+ if (error != 0) {
+ PMCDBG2(PMC, OPS, 1,
+ "group_commit: assign gid=%u err=%d",
+ pg->pg_id, error);
+ return (error);
+ }
+ PMCDBG1(PMC, OPS, 1,
+ "group_commit: gid=%u BOUND", pg->pg_id);
+ } else if (!pg->pg_defer_ok) {
+ PMCDBG3(PMC, OPS, 1,
+ "group_commit: gid=%u ENOSPC %u > %u slots",
+ pg->pg_id, pg->pg_nevents, hw_slots);
+ return (ENOSPC);
+ } else {
+ PMCDBG3(PMC, OPS, 1,
+ "group_commit: gid=%u DEFERRED %u > %u slots",
+ pg->pg_id, pg->pg_nevents, hw_slots);
+ }
+ }
+
+ pg->pg_committed = true;
+ return (0);
+}
+
+void
+pmu_group_release(pmu_group_t *pg)
+{
+ pmu_event_t *pe;
+
+ if (pg == NULL)
+ return;
+
+ /*
+ * The per-pp rotation kthread is torn down by pmu_group_on_release
+ * when the last group leaves a pp, so we have no per-group
+ * kthread to drain here. pmu_group_release is reachable only
+ * after every sibling has been removed from the TAILQ, but be
+ * defensive against direct callers that bypass that path.
+ */
+ while ((pe = TAILQ_FIRST(&pg->pg_events)) != NULL) {
+ TAILQ_REMOVE(&pg->pg_events, pe, pe_sibling);
+ pe->pe_group = NULL;
+ pe->pe_is_leader = false;
+ }
+
+ if (pg->pg_owner != NULL)
+ LIST_REMOVE(pg, pg_owner_next);
+ /*
+ * Membership of pg on a pp's pp_pmu_groups list is tracked by
+ * pg_pp, not pg_attach_proc. pg_attach_proc is set by
+ * pmc_attach_one_process even for groups that are never started
+ * (so never inserted onto pp_pmu_groups), so gating the
+ * LIST_REMOVE on pg_attach_proc would unlink a pg that was
+ * never linked and panic LIST_REMOVE with "prev->next != elm".
+ * Conversely, when pmu_group_on_release already pulled pg off
+ * pp_pmu_groups it cleared pg_pp, so this branch correctly
+ * skips the second LIST_REMOVE.
+ */
+ if (pg->pg_pp != NULL) {
+ if (pg->pg_pp->pp_pmu_rot_cursor == pg)
+ pg->pg_pp->pp_pmu_rot_cursor = LIST_NEXT(pg,
+ pg_proc_next);
+ LIST_REMOVE(pg, pg_proc_next);
+ pg->pg_pp = NULL;
+ }
+ free(pg, M_PMU);
+}
+
+int
+pmu_group_on_allocate(struct pmc *pm, const struct pmc_op_pmcallocate *pa)
+{
+ pmu_event_t *pe;
+
+ pe = malloc(sizeof(*pe), M_PMU, M_WAITOK | M_ZERO);
+ pe->pe_pmc = pm;
+ pe->pe_alloc = *pa;
+ pe->pe_state = PMU_EVENT_STATE_INACTIVE;
+ pe->pe_assigned_row = -1;
+ pm->pm_pmu = pe;
+ return (0);
+}
+
+/*
+ * Resolve the TARGET proc the group is being scheduled against. All
+ * pp_pmu_groups bookkeeping, pp_pmcs[] writes and rotation must hang
+ * off the target's pmc_process -- that is the pp the scheduler walks
+ * at csw_in/out time for the proc whose threads are actually running
+ * the workload being measured.
+ *
+ * Lookup order:
+ * 1. pg->pg_attach_proc, recorded by pmc_attach_one_process for the
+ * first sibling that successfully attaches (committed-bound or
+ * deferred-skip path; see hwpmc_mod.c). This is the canonical
+ * and only correct source.
+ * 2. Fallback: the pmc_target list of any committed sibling. In
+ * practice all siblings of one group target the same proc, so
+ * the first non-empty pm_targets gives us the answer. This is
+ * defensive only; once (1) is set on attach we never get here.
+ *
+ * pg->pg_owner / pg->pg_owner->po_owner is INTENTIONALLY NOT used
+ * here: the owner (e.g. pmcstat parent) is typically NOT the target
+ * (e.g. the child workload), and using owner-pp produced the
+ * "Row index mismatch pmc 255 != ri N" csw_in panic where rotation
+ * unbound rows on owner-pp while target-pp still held the stale
+ * pp_pmcs[ri] -> pm pointer.
+ */
+static struct proc *
+pmu_group_target_proc(pmu_group_t *pg)
+{
+ pmu_event_t *pe;
+ struct pmc_target *ptgt;
+
+ if (pg == NULL)
+ return (NULL);
+ if (pg->pg_attach_proc != NULL)
+ return (pg->pg_attach_proc);
+ TAILQ_FOREACH(pe, &pg->pg_events, pe_sibling) {
+ if (pe->pe_pmc == NULL)
+ continue;
+ ptgt = LIST_FIRST(&pe->pe_pmc->pm_targets);
+ if (ptgt != NULL && ptgt->pt_process != NULL)
+ return (ptgt->pt_process->pp_proc);
+ }
+ return (NULL);
+}
+
+int
+pmu_group_on_start(struct pmc *pm)
+{
+ pmu_event_t *pe;
+ pmu_group_t *pg;
+ struct pmc_process *pp;
+ struct proc *p;
+ int error;
+
+ pe = pmu_event_from_pmc(pm);
+ if (pe == NULL || pe->pe_group == NULL)
+ return (0);
+
+ pg = pe->pe_group;
+ if (!pg->pg_committed)
+ return (EDOOFUS);
+
+ p = pmu_group_target_proc(pg);
+ if (p == NULL) {
+ PMCDBG2(PMC, OPS, 1,
+ "on_start: gid=%u pm=%p no target proc -> EINVAL",
+ pg->pg_id, pm);
+ return (EINVAL);
+ }
+
+ pp = pmc_find_process_descriptor_pmu(p, PMC_FLAG_ALLOCATE);
+ if (pp == NULL)
+ return (ENOMEM);
+
+ if (pg->pg_attach_proc == NULL)
+ pg->pg_attach_proc = p;
+
+ if (pg->pg_pp == NULL) {
+ LIST_INSERT_HEAD(&pp->pp_pmu_groups, pg, pg_proc_next);
+ pg->pg_pp = pp;
+ }
+
+ pg->pg_running = true;
+
+ if (pg->pg_assigned) {
+ PMCDBG3(PMC, OPS, 2,
+ "on_start: gid=%u pm=%p ASSIGNED, attaching siblings "
+ "on pp=%p", pg->pg_id, pm, pp);
+ error = pmu_group_attach_siblings(pg, pp);
+ if (error != 0) {
+ PMCDBG2(PMC, OPS, 1,
+ "on_start: gid=%u attach_siblings err=%d",
+ pg->pg_id, error);
+ return (error);
+ }
+ } else {
+ int sin_err __unused;
+
+ sin_err = pmu_pp_schedule_in(pp, pg);
+ PMCDBG4(PMC, OPS, 2,
+ "on_start: gid=%u pm=%p DEFERRED schedule_in=%d pp=%p",
+ pg->pg_id, pm, sin_err, pp);
+ }
+
+ TAILQ_FOREACH(pe, &pg->pg_events, pe_sibling) {
+ pe->pe_pmc->pm_state = PMC_STATE_RUNNING;
+ PMCDBG3(PMC, OPS, 3,
+ "on_start: gid=%u pm=%p ri=%d -> RUNNING",
+ pg->pg_id, pe->pe_pmc, PMC_TO_ROWINDEX(pe->pe_pmc));
+ }
+
+ pmu_pp_kick_rotate(pp);
+ return (0);
+}
+
+/*
+ * Mark the group stopped. We deliberately do NOT release HW rows
+ * here:
+ *
+ * 1. The user-facing PMC_OP_PMCSTOP path runs with pmc_sx
+ * DOWNGRADED to a shared lock (see PMC_DOWNGRADE_SX in
+ * pmc_syscall_handler). pmc_rotation_detach asserts
+ * sx_assert(&pmc_sx, SX_XLOCKED), so calling schedule-out
+ * from here would panic.
+ *
+ * 2. Stop semantics in the framework only freeze the HW counter,
+ * they do not unbind the row from the target proc -- so
+ * preserving rows across stop matches the existing per-PMC
+ * behaviour and lets a subsequent pmc_start re-run the group
+ * with the same row assignments.
+ *
+ * The rotation kthread skips stopped groups (it gates on
+ * pg_running == true), so a stopped group does not get evicted as
+ * a victim. Final HW row release happens in pmu_group_on_release
+ * which always runs under sx_xlock.
+ */
+void
+pmu_group_on_stop(struct pmc *pm)
+{
+ pmu_event_t *pe;
+ pmu_group_t *pg;
+
+ pe = pmu_event_from_pmc(pm);
+ if (pe == NULL || pe->pe_group == NULL)
+ return;
+ pg = pe->pe_group;
+ if (!pg->pg_running)
+ return;
+
+ pg->pg_running = false;
+
+ TAILQ_FOREACH(pe, &pg->pg_events, pe_sibling)
+ pe->pe_pmc->pm_state = PMC_STATE_STOPPED;
+}
+
+/*
+ * Time accounting and CSW/rotation hooks. When a group is multiplexed the
+ * read path scales the raw HW count by time_running/time_enabled; when it
+ * is not multiplexed these helpers degenerate to no-ops because the active
+ * flag never toggles. pmu_rotate_groups is fired by a per-CPU callout
+ * driven by kern.hwpmc.mux_period_ns and walks pg_proc_next to swap the
+ * active sub-group.
+ */
+void pmu_event_account_in(pmu_event_t *pe __unused,
+ uint64_t now __unused) { }
+void pmu_event_account_out(pmu_event_t *pe __unused,
+ uint64_t now __unused) { }
+void pmu_rotate_groups(int cpu __unused) { }
+
+void
+pmu_group_csw_in(struct thread *td __unused, struct pmc_process *pp __unused)
+{
+}
+
+void
+pmu_group_csw_out(struct thread *td __unused, int cpu __unused)
+{
+}
+
+int
+pmu_group_read_value(struct pmc *pm __unused, pmc_value_t *value __unused)
+{
+ return (0);
+}
+
+void
+pmu_group_on_release(struct pmc *pm)
+{
+ pmu_event_t *pe;
+ pmu_group_t *pg;
+ struct pmc_process *pp;
+ struct proc *p;
+ bool was_first_release;
+
+ pe = pmu_event_from_pmc(pm);
+ if (pe == NULL)
+ return;
+ pg = pe->pe_group;
+ if (pg == NULL)
+ goto destroy;
+
+ /*
+ * The pp we hung off pp_pmu_groups is recorded as pg->pg_pp.
+ * It is the TARGET pp, NOT the owner. Use that directly so we
+ * tear down rotation on the same pp the scheduler walks; falling
+ * back to looking up by attach_proc still works after pg_pp is
+ * cleared by an earlier sibling.
+ */
+ pp = pg->pg_pp;
+ p = pg->pg_attach_proc;
+ if (pp == NULL && p != NULL)
+ pp = pmc_find_process_descriptor_pmu(p, 0);
+
+ /*
+ * Tear down the per-pp rotation kthread BEFORE we mutate any
+ * group state. Once we hit this path we are about to remove pe
+ * from pg->pg_events; allowing the kthread to schedule_in or
+ * schedule_out concurrently could race with that removal. We
+ * only do this on the FIRST release of any sibling (pg_pp set)
+ * and only when this is the last group on the pp -- otherwise
+ * other groups still want rotation.
+ */
+ was_first_release = (pg->pg_pp != NULL);
+ if (was_first_release && pp != NULL &&
+ LIST_FIRST(&pp->pp_pmu_groups) == pg &&
+ LIST_NEXT(pg, pg_proc_next) == NULL &&
+ pp->pp_pmu_rot_running) {
+ pp->pp_pmu_rot_running = false;
+ wakeup(pp);
+ while (pp->pp_pmu_rot_td != NULL)
+ (void)hwpmc_pmu_sx_sleep(&pp->pp_pmu_rot_td, 1,
+ "muxrel");
+ }
+
+ /*
+ * Critical: remove THIS pe from pg_events before any whole-
+ * group operation runs, because the framework's
+ * pmc_release_pmc_descriptor already called pcd_release_pmc /
+ * PMC_UNMARK_ROW_THREAD for this pm. If pmu_pp_schedule_out
+ * (below) walked the still-present pe it would invoke
+ * pcd_release_pmc on this row a second time and corrupt the
+ * per-class accounting.
+ */
+ TAILQ_REMOVE(&pg->pg_events, pe, pe_sibling);
+ pe->pe_group = NULL;
+ if (pg->pg_leader == pe)
+ pg->pg_leader = TAILQ_FIRST(&pg->pg_events);
+ if (pg->pg_nevents > 0)
+ pg->pg_nevents--;
+
+ /*
+ * Schedule the remaining siblings out atomically: stop them,
+ * drain in-flight csw, detach from pp, release their HW rows.
+ * Skipped on subsequent sibling releases (pg_assigned was
+ * cleared by the first one) and on the last release (no more
+ * siblings to schedule out).
+ */
+ if (pg->pg_assigned && pp != NULL && pg->pg_nevents > 0)
+ pmu_pp_schedule_out(pp, pg);
+ else if (pg->pg_assigned)
+ pg->pg_assigned = false;
+
+ /*
+ * Unhook pg from pp->pp_pmu_groups now, while pp is still
+ * alive. Callers (pmc_release_pmc_descriptor) are required
+ * to invoke us BEFORE their pm_targets unlink loop, because
+ * that loop is what eventually drives pp_refcnt to zero and
+ * frees pp; doing this LIST_REMOVE afterwards would touch
+ * freed memory. Subsequent sibling releases see pg_pp ==
+ * NULL and skip this branch (and pmu_group_release below
+ * also short-circuits on pg_pp == NULL).
+ */
+ if (was_first_release) {
+ PMCDBG3(PMC, OPS, 3,
+ "release: unlinking pg=%p from pp=%p (cursor=%p)",
+ pg, pp,
+ pp != NULL ? pp->pp_pmu_rot_cursor : NULL);
+ if (pp != NULL && pp->pp_pmu_rot_cursor == pg)
+ pp->pp_pmu_rot_cursor = LIST_NEXT(pg, pg_proc_next);
+ LIST_REMOVE(pg, pg_proc_next);
+ pg->pg_pp = NULL;
+ pg->pg_attach_proc = NULL;
+ }
+
+ if (pg->pg_nevents == 0) {
+ pg->pg_used_rows_mask = 0;
+ pmu_group_release(pg);
+ }
+
+destroy:
+ pm->pm_pmu = NULL;
+ pmu_event_destroy(pe);
+}
+
+/*
+ * Hook every still-active sibling of pg onto pp via the rotation
+ * attach helper. Called both from pmu_group_on_start (initial
+ * placement when commit already bound the rows) and from
+ * pmu_pp_schedule_in (rotation brings the group in mid-flight).
+ * Non-ACTIVE siblings should never exist when we get here -- group
+ * scheduling is all-or-none, so once pmu_assign_group succeeds every
+ * pe is ACTIVE; we still skip them defensively to keep an inconsistent
+ * state from panicking.
+ */
+static int
+pmu_group_attach_siblings(pmu_group_t *pg, struct pmc_process *pp)
+{
+ pmu_event_t *pe;
+ struct pmc *pm;
+
+ TAILQ_FOREACH(pe, &pg->pg_events, pe_sibling) {
+ if (pe->pe_state != PMU_EVENT_STATE_ACTIVE)
+ continue;
+ pm = pe->pe_pmc;
+ if (pp->pp_pmcs[PMC_TO_ROWINDEX(pm)].pp_pmc == pm)
+ continue; /* idempotent: already attached */
+ pmc_rotation_attach(pm, pp);
+ }
+ return (0);
+}
+
+/*
+ * Atomically schedule pg in: assign HW rows for ALL siblings or do
+ * nothing. Called either from pmu_group_on_start (lazy first
+ * placement) or from the per-pp rotation kthread (when an evicted
+ * peer freed enough rows for pg). Returns 0 if pg is now scheduled,
+ * ENOSPC if it still does not fit (pg stays deferred), other errno
+ * if something hard-failed.
+ */
+static int
+pmu_pp_schedule_in(struct pmc_process *pp, pmu_group_t *pg)
+{
+ pmu_event_t *pe;
+ struct proc *p;
+ u_int hw_slots;
+ int error;
+
+ if (pg == NULL || pg->pg_assigned)
+ return (0);
+ /*
+ * Use the recorded TARGET proc only. Falling back to the
+ * owner here is what created the cross-pp inconsistency that
+ * panicked csw_in with "pmc 255 != ri N": owner-pp is NOT the
+ * pp the scheduler walks, so attach/detach there silently
+ * corrupts state.
+ */
+ p = pmu_group_target_proc(pg);
+ if (p == NULL)
+ return (EINVAL);
+ KASSERT(pp != NULL && pp->pp_proc == p,
+ ("[pmu] schedule_in: pp/p mismatch pp=%p pp->proc=%p p=%p",
+ pp, pp != NULL ? pp->pp_proc : NULL, p));
+
+ hw_slots = pmu_count_core_hw_slots(pg, p);
+ if (pg->pg_nevents > hw_slots)
+ return (ENOSPC);
+
+ error = pmu_assign_group(pg, p, PMC_CPU_ANY);
+ if (error != 0) {
+ PMCDBG2(PMC, OPS, 1,
+ "pp_schedule_in: assign gid=%u err=%d",
+ pg->pg_id, error);
+ return (error);
+ }
+
+ /*
+ * Restore the per-pmc RUNNING state BEFORE we publish the row
+ * via attach_siblings. pmu_pp_schedule_out stamped pm_state =
+ * STOPPED on every sibling so that pmc_process_csw_in (which
+ * checks pm_state == RUNNING before loading the HW counter,
+ * see hwpmc_mod.c csw_in path) would not race the drain phase
+ * of eviction and let new csw_in traffic increment pm_runcount
+ * while we were detaching.
+ *
+ * Doing the restore here -- after assign (rows are valid, pe
+ * is ACTIVE) but before attach (pp_pmcs[ri] is still NULL) --
+ * means by the time pmc_rotation_attach publishes the row via
+ * atomic_store_rel, pm_state is already RUNNING and any csw_in
+ * that observes the new row will load the counter immediately.
+ *
+ * Without this, a rotation-evicted-then-rebound group stayed at
+ * STOPPED forever and csw_in silently skipped the rebinding,
+ * which is the bug behind "Group 0 delta=0 between snapshots
+ * after a single eviction window" in pmc_mux_works_test.
+ */
+ TAILQ_FOREACH(pe, &pg->pg_events, pe_sibling) {
+ if (pe->pe_state == PMU_EVENT_STATE_ACTIVE)
+ pe->pe_pmc->pm_state = PMC_STATE_RUNNING;
+ }
+
+ (void)pmu_group_attach_siblings(pg, pp);
+ return (0);
+}
+
+/*
+ * Atomically schedule pg out: detach every sibling from pp and
+ * release every HW row in one pass. Counts already accumulated in
+ * pm->pm_gv.pm_savedvalue are preserved (we never zero them).
+ * Caller holds pmc_sx exclusive.
+ */
+static void
+pmu_pp_schedule_out(struct pmc_process *pp, pmu_group_t *pg)
+{
+ pmu_event_t *pe;
+
+ if (pg == NULL || !pg->pg_assigned)
+ return;
+
+ /* Phase 1: stop every sibling so csw_in/csw_out skip them. */
+ TAILQ_FOREACH(pe, &pg->pg_events, pe_sibling) {
+ if (pe->pe_state != PMU_EVENT_STATE_ACTIVE)
+ continue;
+ pe->pe_pmc->pm_state = PMC_STATE_STOPPED;
+ }
+
+ /* Phase 2: drain any in-flight csw_out. */
+ TAILQ_FOREACH(pe, &pg->pg_events, pe_sibling) {
+ if (pe->pe_state != PMU_EVENT_STATE_ACTIVE)
+ continue;
+ pmc_rotation_drain(pe->pe_pmc);
+ }
+
+ /* Phase 3: detach every active sibling from the target. */
+ TAILQ_FOREACH(pe, &pg->pg_events, pe_sibling) {
+ struct pmc *pm = pe->pe_pmc;
+
+ if (pe->pe_state != PMU_EVENT_STATE_ACTIVE)
+ continue;
+ if (pp->pp_pmcs[PMC_TO_ROWINDEX(pm)].pp_pmc == pm)
+ pmc_rotation_detach(pm, pp);
+ }
+
+ /* Phase 4: release every HW row in one pass. */
+ pmu_unassign_group(pg, 0);
+}
+
+/*
+ * Purge every pmu_group hanging off pp->pp_pmu_groups and tear down the
+ * per-pp rotation kthread. Called from paths that are about to free pp
+ * out from under the PMU layer (pmc_process_exit when the target proc
+ * goes away, pmc_destroy_process_descriptor as a defensive belt-and-
+ * suspenders), so that the eventual pmu_group_on_release for each
+ * sibling sees pg_pp == NULL and skips its LIST_REMOVE/schedule_out
+ * branch instead of dereferencing freed memory.
+ *
+ * The pmu_group structs themselves are NOT freed here -- they are owned
+ * by the pmc_owner / pmu_event lifetime and will be reclaimed by
+ * pmu_group_on_release / pmu_group_release in the normal way. We only
+ * sever the pp linkage.
+ *
+ * Caller must hold pmc_sx exclusive.
+ */
+void
+pmu_pp_release_all(struct pmc_process *pp)
+{
+ pmu_group_t *pg, *next;
+
+ hwpmc_pmu_sx_assert_xlocked();
+
+ if (pp == NULL)
+ return;
+
+ /*
+ * Tear down the rotation kthread first. It holds pp as its
+ * arg, so once pp is freed the next muxrot tick would
+ * dereference freed memory. The sx_sleep below transiently
+ * drops pmc_sx; the kthread acquires it, observes
+ * pp_pmu_rot_running == false, clears pp_pmu_rot_td, and
+ * exits.
+ */
+ if (pp->pp_pmu_rot_running) {
+ pp->pp_pmu_rot_running = false;
+ wakeup(pp);
+ while (pp->pp_pmu_rot_td != NULL)
+ (void)hwpmc_pmu_sx_sleep(&pp->pp_pmu_rot_td, 1,
+ "muxpurge");
+ }
+
+ /*
+ * Schedule out every group that is currently bound to pp, then
+ * unhook every pg from pp->pp_pmu_groups regardless of state.
+ * pg_pp gets cleared so a later pmu_group_on_release for any
+ * surviving sibling sees was_first_release == false and skips
+ * the now-stale LIST_REMOVE.
+ */
+ pp->pp_pmu_rot_cursor = NULL;
+ LIST_FOREACH_SAFE(pg, &pp->pp_pmu_groups, pg_proc_next, next) {
+ if (pg->pg_assigned)
+ pmu_pp_schedule_out(pp, pg);
+ LIST_REMOVE(pg, pg_proc_next);
+ pg->pg_pp = NULL;
+ pg->pg_attach_proc = NULL;
+ pg->pg_running = false;
+ }
+ LIST_INIT(&pp->pp_pmu_groups);
+}
+
+/*
+ * Spawn the per-pp rotation kthread the first time multiple groups
+ * are attached to one pp, OR if a deferred group is sitting on the
+ * pp waiting to be brought in. Idempotent: returns immediately once
+ * the kthread is up.
+ */
+static void
+pmu_pp_kick_rotate(struct pmc_process *pp)
+{
+ pmu_group_t *pg;
+ int n_running, n_deferred;
+ int error;
+
+ if (pp == NULL || pp->pp_pmu_rot_running)
+ return;
+
+ n_running = n_deferred = 0;
+ LIST_FOREACH(pg, &pp->pp_pmu_groups, pg_proc_next) {
+ if (!pg->pg_running)
+ continue;
+ n_running++;
+ if (!pg->pg_assigned)
+ n_deferred++;
+ }
+ /*
+ * Only worth running rotation if at least one running group
+ * is currently deferred (i.e., something is waiting for HW)
+ * AND there is at least one peer that could be evicted.
+ */
+ if (n_deferred == 0 || n_running < 2)
+ return;
+
+ pp->pp_pmu_rot_running = true;
+ error = kthread_add(pmu_pp_rotate_thread, pp, NULL,
+ &pp->pp_pmu_rot_td, 0, 0, "pmu_rot_pp_%d",
+ pp->pp_proc != NULL ? pp->pp_proc->p_pid : -1);
+ if (error != 0) {
+ pp->pp_pmu_rot_running = false;
+ pp->pp_pmu_rot_td = NULL;
+ PMCDBG1(PMC, OPS, 1,
+ "pp_kick_rotate: kthread_add err=%d", error);
+ }
+}
+
+/*
+ * Run one rotation window for pp.
+ *
+ * Algorithm (simple, fair round-robin -- no FIFO list mutation):
+ *
+ * Phase 1: schedule out EVERY currently scheduled+running group
+ * atomically. After this every running group is deferred
+ * and the HW counter pool is empty.
+ *
+ * Phase 2: walk pp_pmu_groups starting at pp_pmu_rot_cursor (or
+ * head if cursor is stale) and try to schedule each
+ * running group in. As soon as one returns ENOSPC, that
+ * group becomes the cursor for the NEXT tick and we stop
+ * (don't try to fit smaller groups behind it -- that
+ * would re-introduce the starvation we are trying to
+ * avoid: a small high-rate group could slot in over and
+ * over while a larger group at the cursor never gets HW).
+ *
+ * If we wrap around back to the cursor without ever
+ * hitting ENOSPC, every group fits simultaneously
+ * (oversubscription went away mid-test) and we just
+ * advance the cursor by one slot for the next tick so
+ * that future ticks rotate the starting position.
+ *
+ * Example: 3 running groups G1=3, G2=2, G3=3 events on a 6-counter
+ * core pool. Cursor starts at G1.
+ *
+ * tick 0: cursor=G1. Schedule G1 (3/6). Schedule G2 (5/6). Try
+ * G3: ENOSPC. Next cursor=G3. Live: {G1, G2}.
+ * tick 1: cursor=G3. Schedule G3 (3/6). Schedule G1 (6/6). Try
+ * G2: ENOSPC. Next cursor=G2. Live: {G3, G1}.
+ * tick 2: cursor=G2. Schedule G2 (2/6). Schedule G3 (5/6). Try
+ * G1: ENOSPC. Next cursor=G1. Live: {G2, G3}.
+ * tick 3: same as tick 0.
+ *
+ * Each group runs in 2 of every 3 ticks; HW utilisation is 2*total/3.
+ *
+ * Caller holds pmc_sx exclusive.
+ */
+static void
+pmu_pp_rotate_one(struct pmc_process *pp)
+{
+ pmu_group_t *pg, *cursor;
+ bool found_cursor;
+ u_int ngroups, seen;
+ int sin_err;
+
+ if (pp == NULL || LIST_EMPTY(&pp->pp_pmu_groups))
+ return;
+
+ /*
+ * Pre-check: if no running group is currently deferred there
+ * is nothing to rotate. Skip the evict-and-rebind cycle
+ * entirely so that the kthread, once awake, costs us nothing
+ * when oversubscription has subsided (e.g., the user stopped
+ * one of two groups).
+ */
+ {
+ bool need_rotate = false;
+ LIST_FOREACH(pg, &pp->pp_pmu_groups, pg_proc_next) {
+ if (pg->pg_running && !pg->pg_assigned) {
+ need_rotate = true;
+ break;
+ }
+ }
+ if (!need_rotate)
+ return;
+ }
+
+ /* Phase 1: evict every currently scheduled group. */
+ LIST_FOREACH(pg, &pp->pp_pmu_groups, pg_proc_next) {
+ if (pg->pg_assigned) {
+ PMCDBG3(PMC, OPS, 4,
+ "rotate: pp=%p evict gid=%u nevents=%u",
+ pp, pg->pg_id, pg->pg_nevents);
+ pmu_pp_schedule_out(pp, pg);
+ }
+ }
+
+ /*
+ * Phase 2 setup: validate the cursor is still in pp_pmu_groups.
+ * A release path could have removed the cursor group between
+ * ticks; in that case (or first tick), reset to the head.
+ */
+ ngroups = 0;
+ cursor = pp->pp_pmu_rot_cursor;
+ found_cursor = false;
+ LIST_FOREACH(pg, &pp->pp_pmu_groups, pg_proc_next) {
+ if (pg == cursor)
+ found_cursor = true;
+ ngroups++;
+ }
+ if (!found_cursor)
+ cursor = LIST_FIRST(&pp->pp_pmu_groups);
+
+ /*
+ * Phase 2: round-robin schedule_in starting at cursor, wrap
+ * once. First ENOSPC pins the next tick's cursor and stops
+ * the walk.
+ */
+ pg = cursor;
+ seen = 0;
+ while (seen < ngroups) {
+ if (pg == NULL)
+ pg = LIST_FIRST(&pp->pp_pmu_groups);
+
+ if (pg->pg_running && !pg->pg_assigned) {
+ sin_err = pmu_pp_schedule_in(pp, pg);
+ PMCDBG4(PMC, OPS, 4,
+ "rotate: pp=%p schedule_in gid=%u "
+ "nevents=%u -> %d",
+ pp, pg->pg_id, pg->pg_nevents, sin_err);
+ if (sin_err == ENOSPC) {
+ pp->pp_pmu_rot_cursor = pg;
+ return;
+ }
+ /* sin_err == 0 (success) or hard fail; advance. */
+ }
+
+ seen++;
+ pg = LIST_NEXT(pg, pg_proc_next);
+ }
+
+ /*
+ * Everything fit. Advance cursor by one for the next tick so
+ * we rotate the start position even when there is no
+ * oversubscription this window.
+ */
+ pp->pp_pmu_rot_cursor = LIST_NEXT(cursor, pg_proc_next);
+ if (pp->pp_pmu_rot_cursor == NULL)
+ pp->pp_pmu_rot_cursor = LIST_FIRST(&pp->pp_pmu_groups);
+}
+
+/*
+ * Per-pp rotation kthread. Wakes every kern.hwpmc.mux_period_ms,
+ * runs one pmu_pp_rotate_one tick, and goes back to sleep. Exits
+ * when the last group is released from the pp (pmu_group_on_release
+ * clears pp_pmu_rot_running and wakes us).
+ */
+static void
+pmu_pp_rotate_thread(void *arg)
+{
+ struct pmc_process *pp = arg;
+ int period_ticks;
+
+ hwpmc_pmu_sx_xlock();
+ while (pp->pp_pmu_rot_running) {
+ period_ticks = (pmu_mux_period_ms * hz) / 1000;
+ if (period_ticks < 1)
+ period_ticks = 1;
+ (void)hwpmc_pmu_sx_sleep(pp, period_ticks, "muxrot");
+ if (!pp->pp_pmu_rot_running)
+ break;
+ pmu_pp_rotate_one(pp);
+ }
+ pp->pp_pmu_rot_td = NULL;
+ wakeup(&pp->pp_pmu_rot_td);
+ hwpmc_pmu_sx_xunlock();
+ kthread_exit();
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Sep 2, 10:33 PM (4 h, 32 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
34068295
Default Alt Text
D57631.id179976.diff (41 KB)
Attached To
Mode
D57631: hwpmc: add PMU event/group structures and rotation runtime
Attached
Detach File
Event Timeline
Log In to Comment