Repository navigation
Push 2026 10 09 - #1057
Merged
Merged
Push 2026 10 09#1057
Conversation
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
Merge opcm 2026 07 09
* 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
…vailable Co-authored-by: rdementi <25432609+rdementi@users.noreply.github.com>
Change-Id: I2b26cc3a49cc177118a2f5114ebbd3fe0051380e
…overy" This reverts commit fd6599a.
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 merge 2026 10 07
opcm
approved these changes
Oct 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.