Skip to content

Fix temperature threshold units and export the full JSON snapshot - #557

Merged
Syllo merged 3 commits into
masterfrom
json-snapshot-fields
Nov 15, 2026
Merged

Syllo merged 3 commits into
masterfrom
json-snapshot-fields

Conversation

@Syllo

@Syllo Syllo commented Nov 15, 2026

Copy link
Copy Markdown
Owner

nvtop -s printed only a subset of what nvtop already collects, so its JSON could not drive a dashboard without falling back to the interactive UI or to vendor tools. This series closes that gap, and fixes the bugs that surfaced once those fields were inspected.

It supersedes #445, which asked for the same thing but had grown into a branch that also carried unrelated amdgpu fan/PCIe work and no longer applies.

Commits

amdgpu, metax: store the temperature thresholds in degrees Celsius

gpuinfo_static_info never documented the unit of temperature_slowdown_threshold / temperature_shutdown_threshold, and the backends disagreed: NVIDIA, Iluvatar, Enflame, Ascend, Tenstorrent and PPU already store degrees Celsius, AMDGPU stored the millidegrees from temp1_crit / temp1_emergency verbatim, and MetaX stored mxSmlGetTemperatureInfo()'s hundredths of a degree as is (its hardcoded 9500 actually meant 95 °C).

The only consumer, draw_temp_color(), compares the slowdown threshold with gpu_temp, which is in Celsius. On AMDGPU that comparison could never be true, so a card at its critical temperature was still drawn green. Document the unit and convert at the two backends that got it wrong. Tenstorrent and the legacy radeon backend already divided by 1000.

interface: export the remaining per-device fields in snapshot mode

Adds pci, integrated_graphics, encode_decode_shared, max_pcie_gen, max_pcie_link_width, n_shared_cores, l2cache_size, n_exec_engines, gpu_clock_max, mem_clock_max, temp_slowdown_threshold, temp_shutdown_threshold, fan_rpm, power_draw_max, pcie_link_gen, pcie_link_width, pcie_rx, pcie_tx, effective_load_rate, ecc_corrected and ecc_uncorrected.

Existing keys keep their name, type, position and raw-byte memory representation, and every new field follows the established convention of printing null when the backend did not report it. pci is null for devices that are not on a PCI bus. The Celsius to Fahrenheit conversion is now one integer-only helper (F = C * 9 / 5 + 32, rounded by adding 2 before the division) shared with the temperature meter, instead of the floating point formula being duplicated.

interface: emit valid JSON for arbitrary process strings

The writer escaped only \n \b \f \r \t \" \\. Any other control character in a process command line — ESC is easy enough to find, and argv can hold anything — was emitted raw, which strict parsers reject (jq accepts it, CPython/Go/JavaScript do not). Invalid UTF-8 broke conforming readers the same way.

print_json_string() now escapes every control character as \u00XX, copies well formed UTF-8 sequences untouched and replaces stray bytes with U+FFFD, and is used for cmdline, user, device_name, device_architecture and memory_type.

Testing

  • Build clean, ctest 28/28 on this machine.
  • Strict validation (python3 json.loads, encoding="utf-8") of -s, -s -P, -s -f, -s -P -f and -d 1 -s.
  • Snapshot validated with processes holding the DRM device whose argv contained \x01, ESC, a Latin-1 \xe9, an overlong \xc0\xaf sequence and an emoji. Before the escaping commit json.loads failed with Invalid control character at: line 119 column 23; after it, all of them round-trip.
  • On AMDGPU (RX 6800 XT, sysfs temp1_crit=100000, temp1_emergency=105000): "temp_slowdown_threshold": "100C", "temp_shutdown_threshold": "105C", and 212F / 221F with -f. Previously those values were stored as 100000, so alerts never triggered on AMD.
  • Integer conversion compared with the previous 32 + nearbyint(C * 1.8) for every value from 0 to 100 000 °C: 0 differences.
  • utf8_sequence_length() compared against CPython's strict decoder on 369 572 sequences (exhaustive 1- and 2-byte, a 3/4-byte boundary grid and 300 000 random ones): 0 mismatches. Against glibc mbrtowc() over 16 911 360 sequences the only 22 differences are the encodings above U+10FFFF that glibc tolerates and RFC 3629 forbids.

Nothing was tested on MetaX hardware; the MetaX change follows the mxSmlGetTemperatureInfo() contract ("degrees Celsius enlarged by 100 times"), which is also why that backend already divides the hotspot reading by 100.

gpuinfo_static_info did not document the unit of its temperature
thresholds, and the backends disagreed: NVIDIA, Iluvatar, Enflame, Ascend
and Tenstorrent report degrees Celsius, AMDGPU stored the millidegrees
read from temp1_crit/temp1_emergency verbatim, and MetaX stored the
values from mxSmlGetTemperatureInfo as is. MXSML documents them as
degrees Celsius enlarged by 100 times, which is why the backend already
divides the hotspot reading by 100; the thresholds were not converted,
so the hardcoded slowdown value of 9500 actually meant 95 degrees.

The only consumer, draw_temp_color(), compares the slowdown threshold
with gpu_temp, which is in degrees Celsius. On AMDGPU that comparison
could never be true, so a GPU close to its critical temperature was
still drawn green instead of yellow or red.

Document the unit in the struct and convert the AMDGPU and MetaX values
when they are stored. Tenstorrent and the legacy radeon backend already
divided by 1000, and the remaining backends already report degrees.
`nvtop -s` printed only a subset of what nvtop already collects, so the JSON
output could not be used to build a dashboard without falling back to the
interactive UI or to vendor tools.

Print the fields that were missing: the PCI address, the static topology
(integrated_graphics, encode_decode_shared, max_pcie_gen,
max_pcie_link_width, n_shared_cores, l2cache_size, n_exec_engines), the
maximum clocks, the slowdown and shutdown temperature thresholds, fan RPM,
maximum power draw, the PCIe link generation, width and throughput, the
effective load rate and the ECC counters.

The output stays a superset of what it was: existing keys keep their name,
type and position relative to each other, and every new field follows the
existing convention of printing null when the backend did not report it.
`pci` is null for devices that are not on a PCI bus.

The temperatures are converted to Fahrenheit on request, like the temperature
field already was. Since the new threshold fields need the same conversion as
the meter, it is factored into one integer-only helper, celsius_to_fahrenheit(),
instead of duplicating the floating point formula.
The snapshot writer escaped only newline, backspace, form feed, carriage
return, tab, backslash and double quote. Any other control character in a
process command line -- ESC is common enough, and argv can hold anything --
was written raw, which is not valid JSON: jq happens to accept it, while
python, go and JavaScript reject the document. Bytes that are not valid
UTF-8 break the encoding requirement in the same way.

Add print_json_string(), which escapes every control character as \u00XX,
copies well formed UTF-8 sequences untouched and replaces stray bytes with
U+FFFD, and use it for the command line, the user name and the device name,
architecture and memory type, so no string field can break the document.
@Syllo
Syllo merged commit 196e48f into master Nov 15, 2026
8 checks passed
@Syllo
Syllo deleted the json-snapshot-fields branch November 15, 2026 01:21
@Syllo Syllo mentioned this pull request Nov 15, 2026
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.

1 participant