Skip to contentSkip to conversation

alpha/perf: Enable duration-based sampling throttling

PATCH Superseded Magnus Lindholm 2 participants 4 messages Nov 7 – Nov 10

Subsystems (2)

v1 of 2 Superseded Latest is v2 →

lore
Share & tools
lore

Magnus Lindholm

author maintainer
#0 lore raw

cover alpha/perf: Enable duration-based sampling throttling

Hi,

While testing Matt's v7 ProfileMe series [1] on my EV67 AlphaServer ES40, I found that short-period sampling could make the machine unresponsive. Once serial logging was enabled, the console showed RCU grace-period kthread starvation reports.

Count-based throttling was working: at HZ=1024 and the default limit of 100000 samples/s, the observed cadence was approximately 98 interrupts per throttle cycle. However, Alpha never called perf_sample_event_took(), so perf could not reduce that limit based on interrupt handling cost. Setting perf_cpu_time_max_percent to 25 did not provide that feedback.

Keeping period 17 but lowering the limit to 1024 or 4096 samples/s made the tests pass while still exercising repeated stop/park/restart cycles. Adding duration feedback also made the original high-rate test pass, without changing those counter-state operations. The minimal fix measured average handler costs of 24-26 us against a 2.5 us budget and reduced the limit to 7168-8192 samples/s.

Reproducer
==========

The reproducer requires Matt's ProfileMe kernel series and matching perf tools. On the ES40 I used the following one-second workload, built natively as "busy":

  #include <stdint.h>
  #include <time.h>

  static double now(void)
  {
          struct timespec t;
          clock_gettime(CLOCK_MONOTONIC, &t);
          return t.tv_sec + t.tv_nsec * 1e-9;

}

  int main(void)
  {
          uint64_t x = 1;
          double end = now() + 1;
          do {
                  for (int i = 0; i < 1000; i++)
                          asm volatile("mulq %0,3,%0" : "+r" (x));
          } while (now() < end);
          return 0;

}

cc -O2 -o busy busy.c

As root, with serial capture running:

dmesg -n 7
sysctl -w kernel.perf_event_max_sample_rate=100000 sysctl -w kernel.perf_cpu_time_max_percent=25

The period-10007 control completed successfully:

  taskset -c 0 perf record -B -R \
      -e cpu/instructions,profileme,randomize/ -c 10007 \
      -o control.data -- ./busy

Changing only the period reproduced the failure:

  taskset -c 0 perf record -B -R \
      -e cpu/instructions,profileme,randomize/ -c 17 \
      -o period17.data -- ./busy

The unpatched runs required a manual reset; a userspace timeout did not reliably recover the system. The commands above are the essential recording commands from the controlled tests, which also used a timeout supervisor and saved logs on another machine.

Validation and submission scope
===============================

The minimal fix, without diagnostic instrumentation, passed three repeats of the high-rate reproducer and the two low-rate controls. Broader testing covered 21 cases on all three CPUs: randomized ProfileMe instruction sampling at periods 16, 17 and 31, ProfileMe and Aggregate cycle sampling, and concurrent ProfileMe and mixed-mode workloads. All completed, with 1352488 samples, no recorded losses and no new RCU stalls or kernel faults. This was bounded stress testing, not a long soak.

The missing feedback predates ProfileMe. This patch is therefore based on mainline and can be reviewed and applied independently of Matt's series. I cross-built vmlinux, the Alpha boot image and modules on mainline d48723711bae with the ES40 configuration and GCC 15.0.1. The only build warning was the existing saved_config COMMON-symbol warning.

The hardware results above used Matt's v7 series with this fix applied; the mainline-only build has not been boot-tested. RPCC supplies a short, same-CPU interval because Alpha's tick-based sched_clock() is too coarse for this measurement. Counter programming and event handling are unchanged.

[1] https://lore.kernel.org/linux-alpha/[email protected]/

Thanks,
Magnus

Magnus Lindholm (1):
alpha/perf: Account for time spent handling PMU interrupts

 arch/alpha/kernel/perf_event.c | 26 ++++++++++++++++++++++++--
 1 file changed, 24 insertions(+), 2 deletions(-)

base-commit: d48723711bae9b8cdc56719c448ba42fef28cbee

author maintainer
#1 lore raw

PATCH 1/1 alpha/perf: Account for time spent handling PMU interrupts

Alpha does not report PMU interrupt duration to perf, preventing its adaptive throttling from reducing an excessively costly sampling rate.

Measure handler time with RPCC and report it to perf_sample_event_took(). Convert cycles using the calibrated frequency selected by time_init(); Alpha's tick-based sched_clock() is too coarse for this measurement. Wrap the existing handler so all return paths are accounted for, without changing counter programming or event handling.

---
 arch/alpha/kernel/perf_event.c | 26 ++++++++++++++++++++++++--
 1 file changed, 24 insertions(+), 2 deletions(-)
arch/alpha/kernel/perf_event.c +24 -2
index a3eaab094ece..dd3bc70b71d7 100644
@@ -16,8 +16,10 @@
 #include <linux/kdebug.h>
 #include <linux/mutex.h>
 #include <linux/init.h>
+#include <linux/math64.h>
 
 #include <asm/hwrpb.h>
+#include <asm/timex.h>
 #include <linux/atomic.h>
 #include <asm/irq.h>
 #include <asm/irq_regs.h>
@@ -26,6 +28,9 @@
 #include <asm/hw_irq.h>
 
 
+extern unsigned long est_cycle_freq;
+static unsigned long pmu_cycle_freq __read_mostly;
+
 /* The maximum number of PMCs on any Alpha CPU whatsoever. */
 #define MAX_HWEVENTS 3
 #define PMC_NO_INDEX -1
@@ -797,8 +802,8 @@ void perf_event_print_debug(void)
  * Performance Monitoring Interrupt Service Routine called when a PMC
  * overflows.  The PMC that overflowed is passed in la_ptr.
  */
-static void alpha_perf_event_irq_handler(unsigned long la_ptr,
-					struct pt_regs *regs)
+static void alpha_perf_event_irq_inner(unsigned long la_ptr,
+				       struct pt_regs *regs)
 {
 	struct cpu_hw_events *cpuc;
 	struct perf_sample_data data;
@@ -862,6 +867,21 @@ static void alpha_perf_event_irq_handler(unsigned long la_ptr,
 
 
 
+/* sched_clock() is tick-based on Alpha; measure a short same-CPU interval. */
+static void alpha_perf_event_irq_handler(unsigned long la_ptr,
+					 struct pt_regs *regs)
+{
+	u32 start = get_cycles();
+	u32 cycles;
+
+	alpha_perf_event_irq_inner(la_ptr, regs);
+	/* Unsigned subtraction handles a wrap of the low 32-bit counter. */
+	cycles = (u32)get_cycles() - start;
+	if (likely(pmu_cycle_freq))
+		perf_sample_event_took(div64_u64((u64)cycles * NSEC_PER_SEC,
+						 pmu_cycle_freq));
+}
+
 /*
  * Init call to initialise performance events at kernel startup.
  */
@@ -878,6 +898,8 @@ static int __init init_hw_perf_events(void)
 
 	/* Override performance counter IRQ vector */
 
+	/* Match the calibrated frequency selected by time_init(). */
+	pmu_cycle_freq = est_cycle_freq ? est_cycle_freq : hwrpb->cycle_freq;
 	perf_irq = alpha_perf_event_irq_handler;
 
 	/* And set up PMU specification */
maintainer
#2 lore raw

Re: [PATCH 1/1] alpha/perf: Account for time spent handling PMU interrupts

Quoted 2 lines · Magnus Lindholm

Alpha does not report PMU interrupt duration to perf, preventing its adaptive throttling from reducing an excessively costly sampling rate.

Nice find, and thanks for the thorough testing write-up. The logic looks right to me: time_init() runs before early initcalls, and since it zeroes est_cycle_freq when the HWRPB value is within tolerance, the "est_cycle_freq ? : hwrpb->cycle_freq" idiom picks the same frequency it did. A few small things below.

It would be worth adding a sentence on the symptom from the cover letter (short sampling periods leave the machine unresponsive with RCU grace-period kthread starvation reports) so the commit stands on its own.

Quoted 1 line

+#include <linux/math64.h>

On 64-bit, div64_u64() is just a plain division, so this include only serves that one call. See below for an alternative that drops it.

Quoted 1 line

+extern unsigned long est_cycle_freq;

This is already declared in arch/alpha/kernel/proto.h. Please #include "proto.h" instead, like the rest of arch/alpha/kernel does.

Quoted 1 line

+static unsigned long pmu_cycle_freq __read_mostly;

Since this is only written from an early_initcall, __ro_after_init seems a better fit.

arch/alpha/kernel/perf_event.c static void alpha_perf_event_irq_handler(unsigned long la_ptr, · line 879

Quoted 6 lines
arch/alpha/kernel/perf_event.c +6 -0
+     u32 start = get_cycles();
+     u32 cycles;
+
+     alpha_perf_event_irq_inner(la_ptr, regs);
+     /* Unsigned subtraction handles a wrap of the low 32-bit counter. */
+     cycles = (u32)get_cycles() - start;

cycles_t is already a 32-bit unsigned int on Alpha, so the cast isn't needed. Declaring these as cycles_t would read a bit more naturally.

arch/alpha/kernel/perf_event.c static void alpha_perf_event_irq_handler(unsigned long la_ptr, · line 882

Quoted 3 lines
arch/alpha/kernel/perf_event.c +3 -0
+     if (likely(pmu_cycle_freq))
+             perf_sample_event_took(div64_u64((u64)cycles * NSEC_PER_SEC,
+                                              pmu_cycle_freq));

Alpha has no integer divide instruction, so this compiles to a __divqu millicode call on every PMU interrupt. It's small next to the handler cost you measured, but it's easy to avoid: compute a mult/shift pair once at init with clocks_calc_mult_shift() and convert with mul_u64_u32_shr(cycles, mult, shift) here.

arch/alpha/kernel/perf_event.c static int __init init_hw_perf_events(void) · line 902

Quoted 2 lines
arch/alpha/kernel/perf_event.c +2 -0
+     /* Match the calibrated frequency selected by time_init(). */
+     pmu_cycle_freq = est_cycle_freq ? est_cycle_freq : hwrpb->cycle_freq;

Not for this patch, but this is now the second copy of that selection (show_cpuinfo() has the other). Having time_init() store the frequency it chose in a global would let both just read it. Fine as a follow-up.

With the proto.h and cast nits fixed (the rest at your discretion),

Reviewed-by <[email protected]>

Thanks,
Matt

author maintainer
#3 lore raw

Re: [PATCH 1/1] alpha/perf: Account for time spent handling PMU interrupts

Hi Matt,

Quoted 11 lines · Matt Turner
Quoted 2 lines · Magnus Lindholm

Alpha does not report PMU interrupt duration to perf, preventing its adaptive throttling from reducing an excessively costly sampling rate.

Nice find, and thanks for the thorough testing write-up. The logic looks right to me: time_init() runs before early initcalls, and since it zeroes est_cycle_freq when the HWRPB value is within tolerance, the "est_cycle_freq ? : hwrpb->cycle_freq" idiom picks the same frequency it did. A few small things below.

Quoted 4 lines

It would be worth adding a sentence on the symptom from the cover letter (short sampling periods leave the machine unresponsive with RCU grace-period kthread starvation reports) so the commit stands on its own.

Agree, I'll update the commit message in next version.

Quoted 10 lines
Quoted 1 line

+#include <linux/math64.h>

On 64-bit, div64_u64() is just a plain division, so this include only serves that one call. See below for an alternative that drops it.

Quoted 1 line

+extern unsigned long est_cycle_freq;

This is already declared in arch/alpha/kernel/proto.h. Please #include "proto.h" instead, like the rest of arch/alpha/kernel does.

yes, will do.

Quoted 12 lines
Quoted 1 line

+static unsigned long pmu_cycle_freq __read_mostly;

Since this is only written from an early_initcall, __ro_after_init seems a better fit.

arch/alpha/kernel/perf_event.c static void alpha_perf_event_irq_handler(unsigned long la_ptr, · line 879

Quoted 6 lines
arch/alpha/kernel/perf_event.c +6 -0
+     u32 start = get_cycles();
+     u32 cycles;
+
+     alpha_perf_event_irq_inner(la_ptr, regs);
+     /* Unsigned subtraction handles a wrap of the low 32-bit counter. */
+     cycles = (u32)get_cycles() - start;
Quoted 3 lines

cycles_t is already a 32-bit unsigned int on Alpha, so the cast isn't needed. Declaring these as cycles_t would read a bit more naturally.

you' re right.

Quoted 10 lines

arch/alpha/kernel/perf_event.c static void alpha_perf_event_irq_handler(unsigned long la_ptr, · line 882

Quoted 3 lines
arch/alpha/kernel/perf_event.c +3 -0
+     if (likely(pmu_cycle_freq))
+             perf_sample_event_took(div64_u64((u64)cycles * NSEC_PER_SEC,
+                                              pmu_cycle_freq));

Alpha has no integer divide instruction, so this compiles to a __divqu millicode call on every PMU interrupt. It's small next to the handler cost you measured, but it's easy to avoid: compute a mult/shift pair once at init with clocks_calc_mult_shift() and convert with mul_u64_u32_shr(cycles, mult, shift) here.

True, that makes sense. I'll fix this in next version.

Quoted 7 lines

arch/alpha/kernel/perf_event.c static int __init init_hw_perf_events(void) · line 902

Quoted 2 lines
arch/alpha/kernel/perf_event.c +2 -0
+     /* Match the calibrated frequency selected by time_init(). */
+     pmu_cycle_freq = est_cycle_freq ? est_cycle_freq : hwrpb->cycle_freq;

Not for this patch, but this is now the second copy of that selection (show_cpuinfo() has the other). Having time_init() store the frequency it chose in a global would let both just read it. Fine as a follow-up.

Quoted 6 lines

With the proto.h and cast nits fixed (the rest at your discretion),

Reviewed-by <[email protected]>

Thanks,
Matt

Thanks for taking the time to look at this, I'll post a v2

Regards

Magnus