Skip to content

Push 2026 10 09 - #1057

Merged
opcm merged 44 commits into
masterfrom
push-2026-10-09
Oct 9, 2026
Merged

opcm merged 44 commits into
masterfrom
push-2026-10-09

Conversation

@rdementi

@rdementi rdementi commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

No description provided.

rdementi and others added 30 commits August 21, 2026 13:35
PCM_USE_TPMI_RAPL=1 makes PCM read the package (socket) energy through
the architectural RAPL TPMI interface instead of MSR_PKG_ENERGY_STATUS,
see RAPL_TPMI_public_disclosure_FINAL-rev3.pdf in
https://github.com/intel/tpmi_power_management

PCM::initPackageEnergyTPMI enumerates the RAPL domains (128 byte register
blocks) of every entry of every TPMI instance, selects the domains with
DOMAIN_HEADER.TYPE = package, maps the TPMI instances to sockets through
their NUMA node and derives the energy/power units and the package power
info from the POWER_UNIT and PL_INFO registers of the domain. It falls
back to the MSR-based counters if the package RAPL domain can not be
found on every socket.

TPMIHandle gets a getEntrySize() method to report the size of a TPMI
entry which is needed to enumerate the RAPL domains of an entry. Two
out-of-range accesses in the driver-based TPMI implementation (a
requested TPMI ID that the instance does not implement and an offset
outside of the entry) now throw instead of relying on an assert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PCM_USE_TPMI_RAPL=1 now additionally replaces MSR_DRAM_ENERGY_STATUS
with the ENERGY_STATUS register of the memory RAPL TPMI domain (domain
type 4). The energy unit of the DRAM domain is taken from the POWER_UNIT
register of that domain, so PCM::getDRAMJoulesPerEnergyUnit() is used
for the DRAM domain now (it returns the package energy unit as before
when no separate DRAM unit is known).

The domain discovery in PCM::initRAPLTPMI (renamed from
initPackageEnergyTPMI) collects the package and the memory domain of
every socket in one pass. The DRAM part falls back to MSRs on its own if
the memory RAPL domain is not present on every socket.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PCM_USE_TPMI_RAPL=1 now additionally replaces MSR_SYS_ENERGY_STATUS with
the ENERGY_STATUS register of the system (platform) RAPL TPMI domain
(domain type 1) of socket 0, mirroring the MSR-based counter which is
also read on socket 0 only. On GNR the register of the system TPMI
domain reports the same raw values as MSR_SYS_ENERGY_STATUS, therefore
the energy unit handling of getSystemConsumedJoules stays unchanged.

The system energy metric falls back to the MSR-based counter on its own
if the system RAPL domain is not present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The package, DRAM and system energy metrics are read through the
architectural RAPL TPMI interface whenever the RAPL TPMI feature is
discovered on a server processor and fall back to the legacy RAPL MSRs
otherwise. PCM_USE_TPMI_RAPL is replaced by PCM_NO_TPMI_RAPL=1 which
disables the RAPL TPMI usage and enforces the legacy MSR interfaces.

Since the fallback is a normal situation now (processors without the RAPL
TPMI feature) it is reported through the debug output instead of error and
warning messages. The RAPL TPMI discovery is skipped on client processors
to avoid the TPMI/PCI enumeration there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
getSystemConsumedJoules used the package energy unit (or the fixed 1 Joule
granularity of SPR/EMR/GNR/SRF) also when the system energy is read
through the RAPL TPMI interface. The energy unit of the system (platform)
domain is read from the POWER_UNIT register of that domain now and
returned by the new PCM::getSystemJoulesPerEnergyUnit(), which also hosts
the model specific granularity that was inline in
getSystemConsumedJoules before.

Note that the fixed 1 Joule granularity keeps precedence on
SPR/EMR/GNR/SRF: the system RAPL TPMI domain of these processors reports
the architectural default energy unit (61 uJ) in its POWER_UNIT register
while its ENERGY_STATUS register is the same counter as
MSR_SYS_ENERGY_STATUS and counts Joules (measured on GNR: 318 counts/s
at idle, 548 counts/s under load).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ent and MSR-free

Co-authored-by: rdementi <25432609+rdementi@users.noreply.github.com>
…inating

Co-authored-by: rdementi <25432609+rdementi@users.noreply.github.com>
…6-07-09

Change-Id: Ie55e4282817521c4295fe89d378ed785097e506f
* Fix sensor-server fuzz shutdown ordering

Co-authored-by: rdementi <25432609+rdementi@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rdementi <25432609+rdementi@users.noreply.github.com>
Change-Id: I8b0402e259c9480bac2a0774dbeb14f7e19196bb
… TPMI)

Change-Id: I686c8db1c3ac090f242c22d718d13677b98124fd
…object (#979)

* Split CPU topology discovery into independent CoreTopology used by PCM and pcm-msr

Co-authored-by: rdementi <25432609+rdementi@users.noreply.github.com>

* set_signal_handlers() not needed since we don't create PCM object

Change-Id: I58b66c8d7161c74b5d77121eb97cc62d6f16a645

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rdementi <25432609+rdementi@users.noreply.github.com>
Co-authored-by: Dementiev, Roman <roman.dementiev@intel.com>
* Make RDT initialization on-demand via public PCM::initRDT() API

Co-authored-by: rdementi <25432609+rdementi@users.noreply.github.com>


---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rdementi <25432609+rdementi@users.noreply.github.com>
Co-authored-by: Roman Dementiev <roman.dementiev@intel.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The telemetry instances of a socket are children of the same PCI device,
so the NUMA node of that device identifies the socket an instance belongs
to. This is needed to attribute per-instance telemetry (e.g. one instance
per compute die) to a socket.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
initRDT() associated the cores with RMIDs counting down from the maximum
supported RMID and did not remember the association, so users of RDT-based
metrics had no way to tell which RMID belongs to a core.

Record the association in PCM (getCoreRMID(), getUsedRMIDs()) and allocate
the RMIDs per socket in increasing order instead, starting at
firstUsableRMID (1). RMID 0 is skipped because it is the default RMID
(resctrl root group) which other software may use at the same time.

Programming more cores than the socket supports RMIDs is now reported as an
error instead of writing a wrapped-around (invalid) RMID.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The AET PMT telemetry aggregator (CWF) reports per RMID a core energy
counter in Joule and an activity (Cdyn) counter in nanofarad, both in the
"U63.45.18" fixed point format with a valid bit, in one telemetry instance
per compute die.

Add helpers to build and program the PMT events of chosen RMIDs
(AETEventEncoding(), addAETEvents(), programAET()), to read them back as raw
counters or as metrics in Joule/nanofarad, either accumulated or for the
sample interval (getAETCounter(), getAETMetric(), getAETCoreEnergyJoule(),
getAETActivityNanoFarad()), and to map the telemetry instances to sockets
(getAETNumInstances(), getAETInstanceSocket()).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With --aet the pcm utility programs the AET counters of the RMIDs that RDT
associated with the cores (direct MSR programming only, in resctrl mode the
RMIDs of the driver are unknown) and shows two new columns per core, die,
socket and for the whole system, in the text and in the CSV output:

  CENRG : core energy consumed in the sample interval (in Joules)
  CDYN  : dynamic capacitance (Cdyn) of the cores in the sample interval (in nanofarad)

Without the option nothing is programmed and the output is unchanged. If the
metrics are requested but unavailable (processor without AET support, no AET
telemetry exposed by the OS, RDT via resctrl) the reason is reported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
… valid

Co-authored-by: rdementi <25432609+rdementi@users.noreply.github.com>
…ently

memory_bw_local/memory_bw_total hold one CounterWidthExtender per online
core and each of them runs a watchdog thread that polls the stop request
in 50 ms sleep slices. Clearing the vectors destroyed the counters one by
one and every destructor set the stop flag and only then joined its
thread, so the wake-up latency was paid serially: on a 384-thread system
(768 counters) cleanupRDT took about 19 seconds.

Add CounterWidthExtender::requestStopUpdateThread() which only requests
the watchdog thread to finish (the destructor still joins it) and ask all
counters to stop before destroying them. cleanupRDT() sends the requests
before the loop resetting the RMID MSRs, so the threads exit while that
loop is running. The initRDT() error path does the same.

Runtime of "pcm 1 -i=1 -nc -ns" on a 2-socket 384-thread Granite Rapids
system drops from 18.8 to 3.7 seconds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: rdementi <25432609+rdementi@users.noreply.github.com>
…tection

checkForIncomingSSLConnection() peeked the first byte of a new connection
into a (signed) char and compared it with EOF. A first byte of 0xff is sign
extended to -1 and was therefore rejected as "Peeking for SSL resulted in
EOF", even though recv() had just returned one byte of data and 0xff is a
valid start of an SSLv2 record header (bit 7 set) that the very next check
is looking for.

Peek into an unsigned char instead, which also makes the bit test below
independent of integer promotion of a signed value, and drop the EOF
comparison: after a successful one byte peek there is no EOF to detect.

The debug output now prints the byte as hex. It used to write the raw byte
into the log, which was invisible for exactly the bytes of interest (0x16
for a TLS ClientHello), making the logs of a failed handshake ambiguous.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
None of the signal handlers the server installs use SA_RESTART, so any
signal delivered to a thread that is blocked in a socket operation makes
that operation fail with EINTR. The server treated such a failure as a
broken connection: the MSG_PEEK that detects whether a client speaks SSL
threw and closed the connection, reads and writes returned eof (turning a
request into a 400 Bad Request), and SSL_accept() gave up on the handshake.
A client doing nothing wrong was disconnected.

This is easy to observe when fuzzing: libFuzzer arms a repeating SIGALRM
timer, and once every few hundred connections that signal lands inside the
TLS handshake. The SSL fuzz target then fails with "Failed to establish SSL
connection" and aborts the whole run.

Retry the operation instead, which is what SA_RESTART would do:
  - recv(MSG_PEEK) in checkForIncomingSSLConnection()
  - read()/recv() in underflow() and send() in writeToSocket()
  - SSL_read()/SSL_write() on SSL_ERROR_SYSCALL with EINTR
  - SSL_accept() on SSL_ERROR_SYSCALL with EINTR (SSL_ERROR_WANT_READ and
    SSL_ERROR_WANT_WRITE, which is how OpenSSL usually reports an
    interrupted handshake on a blocking socket, were already retried)

Retrying a receive must stay bounded. A receive on a socket with
SO_RCVTIMEO set is never restarted after a signal, not even for an
SA_RESTART handler (signal(7)), and each new receive starts the timeout
over. Unbounded retries would therefore let a client that connects and
sends nothing keep a worker thread for as long as signals keep arriving,
defeating both the receive timeout and the request deadline that
readLineBounded() enforces. underflow() now derives a deadline from the
timeout currently set on the socket - read with getsockopt because
ScopedRecvTimeout tightens SO_RCVTIMEO directly, on purpose without
touching timeout_ - and gives up once it passes, exactly as a timeout
would. A timeout of zero keeps the "block indefinitely" semantics.

The error code is snapshotted immediately after each OpenSSL call because
SSL_get_error(), the ERR_* helpers and DBG() may all overwrite it before
the checks run, and it is cleared before the call so that no stale value
from an unrelated operation can be mistaken for EINTR (which would retry a
call that can never succeed). Both the new checks and the pre-existing
EAGAIN ones use that snapshot now. It is read and cleared through helpers
because Windows keeps the code in WSAGetLastError() rather than in errno,
where the previous direct errno comparisons could never match.

writeToSocket() returns early when there is nothing to send. sync() is
called that way on every flush that has nothing left and on close(), and
OpenSSL documents SSL_write() with num == 0 as undefined: it returns 0,
which SSL_get_error() reports as SSL_ERROR_SYSCALL with no error queued
and no error code, i.e. indistinguishable from a real transport failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hake

libFuzzer arms a repeating SIGALRM timer and installs its signal handlers
without SA_RESTART, so a signal can interrupt the client side of the TLS
handshake even though the socket is blocking. OpenSSL reports the
interrupted read as retryable, SSL_connect() returns -1 and the harness
turned that into a fatal "Failed to establish SSL connection", calling
exit(1). libFuzzer reported it as "fuzz target exited" and wrote a crash
artifact for an input that the server had not even received yet, because
the handshake happens before the fuzz data is sent.

Retry SSL_connect() while the error is SSL_ERROR_WANT_READ,
SSL_ERROR_WANT_WRITE or SSL_ERROR_SYSCALL with EINTR. A handshake that
fails for any other reason stays fatal, but now logs the SSL error code,
errno and the OpenSSL error string so the next real failure can be
diagnosed from the CI log alone.

connect() gets the same treatment in both places it is called:
  - make_request() waits for an interrupted connect() to complete with
    poll() and checks SO_ERROR, since calling connect() again on the same
    socket would fail with EALREADY
  - waitForPort() creates a fresh socket for every attempt instead of
    reusing one whose connect() was interrupted, where every later attempt
    would have failed with EALREADY / EISCONN until the 60 second timeout
    expired and init() called exit(1)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
poll() is interrupted by a signal just like connect() was, and its timeout
is relative, so retrying it with the full 60 seconds started a fresh minute
of waiting on every SIGALRM. With libFuzzer's repeating timer the loop could
therefore never reach its timeout and a connection that never completes
would hang the fuzz target instead of failing it.

Compute the deadline once and pass the remaining time to each poll() call.
Reaching the deadline yields a poll_ret of 0, which the existing code
already treats as the timeout it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…988)

* Fix unused variable warnings in PerfmonEventResolver::getEventNames

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rdementi <25432609+rdementi@users.noreply.github.com>
Change-Id: Ibc1d054679398369a11260472095f5a97ea6838c
Copilot AI and others added 14 commits September 25, 2026 12:57
…vailable

Co-authored-by: rdementi <25432609+rdementi@users.noreply.github.com>
Change-Id: I2b26cc3a49cc177118a2f5114ebbd3fe0051380e
IA32_PERF_STATUS (0x198) reports the current operating point of a core: the
RATIO field (bits 15:8) holds the performance state value, i.e. the multiplier
of the bus clock, and the VOLTAGE field (bits 47:32) holds the operating
voltage in 3.13 fixed point format. Neither was available through the counter
states so far, although both are cheap to read alongside the temperature.

Read the MSR in BasicCounterState::readAndAggregate() and expose the two
fields via getPerfStatusRatio() and getPerfStatusVoltage(). Both are
instantaneous values rather than deltas over the measurement interval, so they
are averaged over the logical cores a state aggregates instead of being added
up, which makes the socket, die and system states report the mean operating
point of their cores.

The register is not architectural, therefore the two fields are validated and
counted separately: a platform may leave one of them reserved, where it reads
back as 0. Since neither the ratio nor the voltage of a running core can be 0,
a zero field is treated as not populated and the corresponding getter returns
PCM_INVALID_PERF_STATUS_METRIC instead of a fabricated measurement. The same
applies to cores whose MSR read fails outright.

Reading one more MSR per core per sample is not free on a machine with a few
hundred cores, so the collection is off by default and has to be requested
with enablePerfStatusCollection(), the way initRDT() gates the RDT metrics.
Tools that do not ask for it are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Report the operating point of the cores from IA32_PERF_STATUS in two new
columns: RATIO, the current performance state value, and VOLT, the current
operating voltage in Volt. Both are shown for every kind of row, per core as
well as aggregated per die, per socket and for the whole system, and in the
CSV output as well as in the text output.

The metrics are opt in. They cost one MSR read per core per sample, and adding
two columns unconditionally would change the CSV column layout that existing
scripts parse, so the option enables the collection in PCM and both outputs
keep their current shape without it.

RATIO and VOLT are instantaneous samples, unlike every other metric of the
tool, which is a delta over the sample interval. The values of the cores are
read one after another during a sweep over the cores, so a row aggregating
several cores averages samples taken at slightly different points in time and
the ratio of a core does not have to agree with its CFREQ. Both the legend and
the help text say so, and point at CFREQ for the core frequency over the
interval.

A field that the processor does not populate is printed as "N/A" rather than as
a zero measurement, and a diagnostic names the affected field once at startup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ratio_format() formatted with a field width of 5, which is what the text output
needs, but the CSV output shares the same helper and wrote " 30.0" instead of
"30.0". Format without a field width and leave the padding to the setw() that
both text call sites already apply, so the text output is unchanged.

Also describe the averaging of the two metrics more precisely: a logical core
whose RATIO or VOLTAGE field is not populated is left out of the average, so
the aggregated rows show the mean over the cores that report the metric and not
over every core of the row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The UFS (Uncore Frequency Scaling) TPMI UFS_STATUS register is already read
once per sample for the per die uncore frequency, but only its CURRENT_RATIO
and agent type fields were decoded. Two more fields of the same register are
useful and come for free:

  - CURRENT_VOLTAGE (bits 22:7), the fabric voltage in U3.13 format, exposed
    by getUncoreVoltage() in Volt. Like the frequency it is an instantaneous
    sample and not an average over the measurement interval.
  - THROTTLE_COUNTER (bits 63:32), which the hardware increments at most once
    per 1ms interval in which the fabric frequency violated the bound
    programmed in UFS_CONTROL. getUncoreThrottleCount() returns the difference
    between the two states, i.e. the number of milliseconds of the interval in
    which the uncore frequency was clipped. The counter is 32 bit wide and the
    subtraction is masked accordingly, so a wrap around after ~50 days of
    uninterrupted throttling does not produce a bogus delta.

The field positions get named constants and the existing frequency decoding
uses them as well, which also documents why a bus clock of 100MHz is assumed:
it is the only encoding defined by UFS_HEADER.RATIO_UNIT.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per die uncore columns showed the fabric frequency only. With
--ufs-status every die cell also holds the fabric voltage in Volt and the
number of milliseconds of the sample interval in which the uncore frequency
was clipped below the bound programmed in UFS_CONTROL, in the text as well as
in the CSV output.

The metrics are opt in because the two extra values per die widen a table that
already has one column per uncore die, and because adding them unconditionally
would change the CSV column layout that existing scripts parse. Unlike
--perf-status the option does not enable any additional hardware access:
UFS_STATUS is read in full for the frequency column anyway.

Both new values keep the per die cells of the header and of the data rows the
same width, so every die column stays under its own header. The throttling
field is six characters wide, which covers a fully throttled sample interval of
about 16 minutes.

Frequency and voltage are instantaneous samples while the throttling is a count
over the interval; the legend and the help text say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The register is not architectural and the collection probed it on every
processor, relying on the read failing gracefully where it is not implemented.
That is not failure-safe on Windows: the bundled MSR driver reports the
exception of an unimplemented register as an error and MsrHandle::read asserts
on it, so a debug build could abort before reaching the code that reports the
metrics as unavailable.

Add perfStatusMetricAvailable() and enable the collection only on the server
processors on which the RATIO and VOLTAGE fields have been validated: SKX (and
with it the CLX and CPX steppings of the same model), ICX, ICX_D, SPR, EMR, GNR
and GNR_D. enablePerfStatusCollection() ignores the request everywhere else, so
no tool can trigger the read on a processor that may not implement the register,
and pcm reports the option as unsupported instead of printing N/A columns
without an explanation.

MSR 0x198 is now read 768 times on a 384 thread GNR machine sampled twice with
--perf-status, and not at all without the option or on a processor outside the
list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MsrHandle::read asserted on a failing DeviceIoControl and then returned the
length reported by the driver regardless. The MSR driver catches the exception
of a register the processor does not implement and completes the request with
the exception code, so reading such a register aborted a debug build and looked
like a successful read in a release build, where the output buffer keeps
whatever the caller had put there.

Return 0 for a failed read, which is what the winring0 path in the same
function, the Linux path and the FreeBSD path already do, and which the callers
that test the result against sizeof(uint64) already expect: the probes of
MSR_TSX_FORCE_ABORT, MSR_PERF_CAPABILITIES, MSR_IA32_BIOS_SIGN_ID and
IA32_PERF_STATUS now detect an unimplemented register on Windows as they do
everywhere else.

The failure is no longer silent for developers either: the driver prints the
exception code with DbgPrint and the host side logs the core and the register
number at debug level 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…6-10-07

Change-Id: I8f3ddf2595d6925dc40ed5ec626aafc091c1a986
The hybrid client models were grouped with the Atom parts in the package
C-state table (PC2=0x3F8, PC4=0x3F9, PC6=0x3FA). On these Core client
parts MSR_PKG_C2_RESIDENCY is 0x60D and 0x3F8/0x3F9/0x3FA are PC3/PC6/PC7,
with PC8-PC10 at 0x630-0x632, the same layout as the Skylake-path clients.
As a result PC2 was never read and package C0 was always reported as 100%.

Verified on Panther Lake (Core Ultra X7 358H): idle system now reports
~92-95% PC2 (raw 0x60D/TSC ratio: ~97%) instead of C0=100%; under full
load package C0 is 100%. Other listed models share the same layout but
were not tested on hardware.

Fixes #995

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ating Telegraf configuration'

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@opcm
opcm merged commit 3dda3f1 into master Oct 9, 2026
43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants