Repository navigation
Reducing impact htop on CPU monitoring and lowering RAM consumption - #2135
GermanAizek wants to merge 4 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ➖ Normal Change: Refactor Merge Risk: 🟠 High · up to
|
There was a problem hiding this comment.
Actionable comments posted: 5
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: ASSERTIVE
- Plan: Advanced
- Run ID:
6ac0de2f-4574-4be7-84a3-d676328946ce
📒 Files selected for processing: 32
BacktraceScreen.cCRT.cCommandLine.cHashtable.cHashtable.hMachine.cMachine.hMeter.hProcess.cProcess.hProcessTable.cRow.hScreenManager.cTable.cTable.hVector.cVector.hXUtils.cXUtils.hconfigure.acdarwin/DarwinProcess.cdragonflybsd/DragonFlyBSDProcessTable.cfreebsd/FreeBSDProcessTable.clinux/LinuxMachine.clinux/LinuxProcess.clinux/LinuxProcess.hlinux/LinuxProcessTable.cnetbsd/NetBSDProcessTable.copenbsd/OpenBSDProcessTable.cpcp/PCPProcessTable.csolaris/SolarisProcessTable.cunsupported/UnsupportedProcessTable.c
Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.
|
|
||
|
|
||
| #define METER_TXTBUFFER_LEN 256 | ||
| #define METER_TXTBUFFER_LEN 127 |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect all txtBuffer references and their surrounding code.
rg -n -C 4 'txtBuffer' --glob '*.[ch]' .Repository: htop-dev/htop
Length of output: 22576
🏁 Script executed:
set -euo pipefail
printf '%s\n' '--- diff for Meter.h ---'
git diff 44a59cf1b407df3a1853e9f4f86c3f366808c9e0 cd27007d45adf30209f0fdcf007238ab2c638b2c -- Meter.h
printf '%s\n' '--- buffer helper definitions ---'
rg -n -C 8 -F -e 'METER_BUFFER_CHECK' -e 'METER_BUFFER_APPEND_CHR' -e 'Meter_humanUnit' --glob '*.[ch]' .
printf '%s\n' '--- network writer and input declarations ---'
sed -n '1,155p' NetworkIOMeter.c
rg -n -C 5 -F -e 'cached_rxb_diff_str' -e 'cached_txb_diff_str' --glob '*.[ch]' .
printf '%s\n' '--- pressure writer and file source ---'
sed -n '1,90p' linux/PressureStallMeter.c
rg -n -C 6 -F -e 'PressureStallMeter_updateValues' -e 'const char* file' --glob '*.[ch]' linux .
printf '%s\n' '--- PCP writer block ---'
sed -n '280,370p' pcp/PCPDynamicMeter.c
printf '%s\n' '--- other variable string writers ---'
sed -n '1,215p' linux/ZswapMeter.c
sed -n '160,205p' linux/OpenRCMeter.c
sed -n '295,322p' linux/SystemdMeter.c
sed -n '1,40p' SysArchMeter.cRepository: htop-dev/htop
Length of output: 42613
🏁 Script executed:
set -euo pipefail
printf '%s\n' '--- complete PCP update function ---'
sed -n '315,430p' pcp/PCPDynamicMeter.c
printf '%s\n' '--- PCP dynamic meter declarations and parser ---'
rg -n -C 8 -F -e 'totalMetrics' -e 'PCPDynamicMetric' -e 'PCPDynamicMeter_scan' -e 'pmsprintf' --glob '*.[ch]' pcp
printf '%s\n' '--- bounded helper definitions ---'
rg -n -C 10 -F -e 'xSnprintf' -e 'String_safeStrncpy' --glob '*.[ch]' XUtils.* String.* pcp | head -260Repository: htop-dev/htop
Length of output: 41596
🌐 Web query:
official Performance Co-Pilot pmsprintf return value truncation contract
💡 Result:
**The documented PCP contract is not the usual `snprintf` truncation signal.** If the buffer is too small, `pmsprintf` leaves a null-terminated truncated string in `str`; on success it returns the number of characters actually written (excluding the null terminator). The return value is always nonnegative, so don’t rely on `return >= size` to detect truncation. ([linuxman7.org](https://linuxman7.org/linux/man-pages/man3/pmsprintf.3.html))
If the underlying `vsnprintf` fails, `str` is set to an empty null-terminated string and the function returns `0`. ([linuxman7.org](https://linuxman7.org/linux/man-pages/man3/pmsprintf.3.html))
The current upstream header independently describes the null-termination guarantee, but doesn’t specify a separate truncation indicator. ([github.com](https://github.com/performancecopilot/pcp/blob/main/src/include/pcp/pmapi.h))
Citations:
- 1: https://linuxman7.org/linux/man-pages/man3/pmsprintf.3.html
- 2: https://linuxman7.org/linux/man-pages/man3/pmsprintf.3.html
- 3: https://github.com/performancecopilot/pcp/blob/main/src/include/pcp/pmapi.h
🏁 Script executed:
set -euo pipefail
sed -n '470,530p' Meter.c
sed -n '285,310p' XUtils.cRepository: htop-dev/htop
Length of output: 2045
🏁 Script executed:
set -euo pipefail
rg -n -C 8 -F -e 'void fail' -e 'fail(void)' -e 'fail();' --glob '*.[ch]' .Repository: htop-dev/htop
Length of output: 5995
Guard Meter_humanUnit against insufficient remaining capacity.
PCPDynamicMeter_updateValues can leave too little space for a later human-readable value. Meter_humanUnit calls xSnprintf, and xSnprintf calls fail() when the formatted value does not fit. A long preceding metric can therefore abort htop instead of producing controlled output.
Check the remaining capacity before formatting each value, then roll back or stop when the value cannot fit.
There was a problem hiding this comment.
This one should be left alone in general, as these buffer sizes are used throughout the code …
There was a problem hiding this comment.
@BenBE Thanks for the guidance. I won’t press this recommendation, given the shared use of these buffer sizes.
✏️ Learnings added
Learnt from: BenBE
Repo: htop-dev/htop PR: 2135
File: Meter.h:22-22
Timestamp: 2026-10-16T18:45:09.042Z
Learning: In htop, METER_TXTBUFFER_LEN in Meter.h controls a buffer used throughout the meter code. The maintainer prefers to leave these shared buffer sizes and their handling unchanged in general; avoid isolated recommendations without considering all users.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
You are interacting with an AI system.
| pmAtomValue value; | ||
| if (Metric_instance(PCP_PROC_TTYNAME, pid, offset, &value, PM_TYPE_STRING)) | ||
| Process_updateTty(process, value.cp); | ||
| else | ||
| Process_updateTty(process, NULL); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
Free value.cp after Process_updateTty and Process_updateCwd.
Metric_instance with PM_TYPE_STRING allocates value.cp. Before this change, setString kept that allocation as the field value. Now Process_updateTty and Process_updateCwd copy the string through RefString_assignNew, and nothing frees value.cp. This leaks one string for each process on every TTY change and on every scan where PROCESS_FLAG_CWD is set.
Proposed fix
- if (Metric_instance(PCP_PROC_TTYNAME, pid, offset, &value, PM_TYPE_STRING))
- Process_updateTty(process, value.cp);
- else
+ if (Metric_instance(PCP_PROC_TTYNAME, pid, offset, &value, PM_TYPE_STRING)) {
+ Process_updateTty(process, value.cp);
+ free(value.cp);
+ } else
Process_updateTty(process, NULL);Apply the same change in PCPProcessTable_readCwd.
Also applies to: 301-305
| } else if (mc->highlightCount > 0 && mc->highlightCount < PROCESS_MAX_HIGHLIGHTS) { | ||
| mc->highlights = xReallocArray(mc->highlights, mc->highlightCount, sizeof(ProcessCmdlineHighlight)); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
The shrunk highlight array causes a heap overflow on the next rebuild.
Line 508 shrinks mc->highlights to highlightCount elements. On the next Process_makeCommandStr call, highlightCount resets to 0 but mc->highlights stays non-NULL. As a result, WRITE_HIGHLIGHT does not reallocate. It checks only highlightCount < PROCESS_MAX_HIGHLIGHTS and can write past the smaller allocation. The trigger is common: a settings change or a cmdline/exe change on a process that now needs more highlights than before.
Choose one fix:
- Keep the full
PROCESS_MAX_HIGHLIGHTScapacity. - Free and set
mc->highlights = NULLat the start of each rebuild (near Line 238), soWRITE_HIGHLIGHTallocates full capacity again.
Proposed fix
/* Reset all locations that need extra handling when actually displaying */
mc->highlightCount = 0;
+ free(mc->highlights);
+ mc->highlights = NULL;| } | ||
| redraw = false; | ||
|
|
||
| xTrimMemory(); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '380,420p' ScreenManager.c
rg -n 'halfdelay|timeout\(|nodelay|CRT_enableDelay|CRT_disableDelay|CRT_delay' CRT.c ScreenManager.c Panel.c
git diff 44a59cf1b407df3a1853e9f4f86c3f366808c9e0 cd27007d45adf30209f0fdcf007238ab2c638b2c -- ScreenManager.c | sed -n '1,200p'Repository: htop-dev/htop
Length of output: 6708
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- input path and delay references ---'
rg -n -F --glob '*.{c,h}' -e 'Panel_getCh' -e 'CRT_delay' -e 'CRT_enableDelay' -e 'CRT_disableDelay' -e 'halfdelay' -e 'timeout(' -e 'settings->delay' -e 'delay =' .
printf '%s\n' '--- CRT delay implementation and initialization ---'
sed -n '1170,1225p' CRT.c
sed -n '1310,1365p' CRT.c
printf '%s\n' '--- input consumer and ScreenManager loop context ---'
rg -n -F --glob '*.{c,h}' -- 'int Panel_getCh' 'Panel_getCh(' .
sed -n '260,430p' ScreenManager.c
printf '%s\n' '--- delay defaults and option parsing ---'
rg -n -F --glob '*.{c,h}' -e 'delay' -e 'Delay' Settings.c Settings.h Main.c CRT.c | sed -n '1,240p'
printf '%s\n' '--- merge-base comparison for the call and relevant delay code ---'
git diff --unified=20 44a59cf1b407df3a1853e9f4f86c3f366808c9e0 cd27007d45adf30209f0fdcf007238ab2c638b2c -- ScreenManager.c CRT.cRepository: htop-dev/htop
Length of output: 25044
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- Panel_getCh implementation ---'
sed -n '540,590p' Panel.c
printf '%s\n' '--- delay constants and initialization ---'
rg -n -F --glob '*.{c,h}' -e 'DEFAULT_DELAY' -e 'settings->delay =' -e 'flags.delay =' -e 'delay = DEFAULT_DELAY' .
printf '%s\n' '--- relevant declarations and option parsing ---'
sed -n '50,90p' Settings.h
sed -n '1100,1140p' Settings.c
sed -n '110,145p' CommandLine.c
sed -n '210,240p' CommandLine.c
sed -n '320,350p' CommandLine.c
sed -n '430,455p' CommandLine.c
printf '%s\n' '--- delay semantics in the loop ---'
sed -n '115,175p' ScreenManager.c
sed -n '300,320p' ScreenManager.cRepository: htop-dev/htop
Length of output: 10536
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- xTrimMemory implementation and declarations ---'
rg -n -F --glob '*.{c,h}' -e 'xTrimMemory' -e 'malloc_trim' .
printf '%s\n' '--- complete trim definitions and nearby callers ---'
sed -n '1,180p' XUtils.c
sed -n '1,120p' XUtils.h
rg -n -F --glob '*.{c,h}' -e 'Table_compact' -e 'KEY_FOCUS_OUT' .Repository: htop-dev/htop
Length of output: 8339
Do not call xTrimMemory() after every idle timeout.
The default delay is 15, so halfdelay() waits 1.5 seconds. The ERR path therefore runs about 0.67 times per second while idle, not “many times per second.” The same rate applies in foreground and background because background mode does not change input polling. However, the new path still calls malloc_trim(0) after every timeout when supported.
Suggested fix
- xTrimMemory();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| xTrimMemory(); |
| void xTrimMemory(void) { | ||
| #if defined(HAVE_MALLOC_TRIM) | ||
| malloc_trim(0); | ||
| #endif | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use the same guard for the malloc_trim call and the <malloc.h> include.
The include at Line 28 requires both HAVE_MALLOC_H and HAVE_MALLOC_TRIM. The call at Line 114 requires only HAVE_MALLOC_TRIM. On a system that has malloc_trim but no malloc.h, the call has no declaration. With -std=c99 -pedantic this is an implicit-declaration error, and under -Werror the build fails.
Proposed fix
--- "a/XUtils.c"
+++ "b/XUtils.c"
@@ -110,11 +110,11 @@
return ret;
}
void xTrimMemory(void) {
-#if defined(HAVE_MALLOC_TRIM)
+#if defined(HAVE_MALLOC_H) && defined(HAVE_MALLOC_TRIM)
malloc_trim(0);
#endif
}
inline bool String_contains_i(const char* s1, const char* s2, bool multi) {
// we have a multi-string search term, handle as special case for performance reasons📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| void xTrimMemory(void) { | |
| #if defined(HAVE_MALLOC_TRIM) | |
| malloc_trim(0); | |
| #endif | |
| } | |
| void xTrimMemory(void) { | |
| #if defined(HAVE_MALLOC_H) && defined(HAVE_MALLOC_TRIM) | |
| malloc_trim(0); | |
| #endif | |
| } |
🧰 Tools
🪛 Cppcheck (2.22.0)
[style] 113-113: The function 'xTrimMemory' is never used.
(unusedFunction)
be603a8 to
d667054
Compare
…shrinking, and compact HashtableItem - Add configure checks for <malloc.h> and malloc_trim, and introduce xTrimMemory() wrapper in XUtils to release unused heap memory back to the OS. - Implement RefString reference-counted strings in XUtils to allow sharing identical immutable strings without duplicating heap payloads. - Add Vector_shrink() and Hashtable_shrink() to release excess capacity in dynamic containers. - Compact HashtableItem from 24 to 16 bytes by changing Robin Hood probe distance to unsigned int, reducing hash table bucket array footprint by 33%.
… commandline highlights - Compact Row struct by reducing tree_depth to uint16_t and reordering fields, shrinking sizeof(Row) from 64 to 56 bytes and dropping into a smaller 64-byte glibc malloc chunk. - Compact Meter struct by packing curItems with txtBuffer[127], eliminating trailing alignment padding and shrinking sizeof(Meter) from 376 to 240 bytes. - In Process, dynamically allocate ProcessMergedCommand highlights on demand rather than embedding an 8-element static array, shrinking Process struct by 184 bytes per task (~330 KB heap savings). - Sizing ProcessCmdlineHighlight offset and length to uint32_t. - Embed char procComm[24] inline into struct Process to eliminate separate heap allocations and glibc chunk headers for process command names. - Sizing basename offsets, priority, and nlwp to 32-bit types, and packing isRunningInContainer, elevated_priv, and state into 1-byte integer types. - Remove cached starttime_show[8] and format STARTTIME on demand in Process_writeField, reducing sizeof(Process) from 504 to 264 bytes (-47.6%). - Manage process cmdline, procExe, procCwd, and tty_name via RefString. - Realloc mergedCommand strings and highlights to exact capacity, and add Process_cleanMergedCommand() to free commandline rendering state. - Update BacktraceScreen and platform process tables for inline procComm, dynamic highlights, and Process_updateTty/Process_updateCwd.
…ilds, cull dead rows and trim idle memory - Enable terminal focus reporting (\033[?1004h) in CRT_init() and disable in CRT_done(). - Track inBackground state on KEY_FOCUS_OUT / KEY_FOCUS_IN in ScreenManager_run and reset on any user input event. - Defer Table_rebuildPanel and Process_makeCommandStr during background scans, restoring and rebuilding immediately when returning to the foreground. - On KEY_FOCUS_OUT, prune and shrink table displayList and panel item vectors, clean process mergedCommand strings/highlights, shrink table->rows and table->table to release dead process capacity, and trim heap memory via xTrimMemory(). - In Table_cleanupRow, immediately cull dead or tombed rows when host->inBackground is active, preventing exited processes from lingering as visual zombies in memory while running in the background. - In Table_compact, invoke xTrimMemory() when row counts decrease, returning reclaimed process memory directly to the OS. - Invoke xTrimMemory() during idle event loops (ch == ERR).
…uppress background scans
- Compact LinuxProcess struct by eliminating unused flags field (-8 bytes)
and sizing ctxt_diff to uint32_t (-8 bytes), reducing sizeof(LinuxProcess)
from 872 to 600 bytes.
- Share thread strings across thread groups via RefString: cmdline, procExe,
tty_name, procCwd, secattr, and cgroup, eliminating redundant kernel lookups,
syscalls, and heap allocations.
- Read /proc/<pid>/comm and /proc/<pid>/cmdline using stack buffers to avoid
heap churn for preexisting commands.
- Suppress repetitive /proc/<pid>/cmdline and comm re-reading for preexisting
processes while in background.
- Suppress non-critical background scans:
- Skip /proc/<pid>/maps parsing in LinuxProcessTable_readMaps and smaps reads.
- Skip background capabilities queries (SYS_capget), CTXT reads, cgroup, IO,
delay accounting, OOM, IO priority, autogroup, scheduling policy, and GPU metrics.
- Guard CPU frequency and temperature scans with !super->inBackground.
- Guard zswap, hugepages, zfs arcstats, and zram scans with !super->inBackground.
d667054 to
27bc5ef
Compare
|
All CI tests success passed |
|
Undisclosed use of AI, violating the project requirements |
BenBE
left a comment
There was a problem hiding this comment.
Mixes several unrelated changes.
| for (size_t i = 0; i < process->mergedCommand.highlightCount; i++) { | ||
| const ProcessCmdlineHighlight* highlight = process->mergedCommand.highlights; | ||
| if (highlight->flags & CMDLINE_HIGHLIGHT_FLAG_BASENAME) { | ||
| highlightLen = highlight->length; | ||
| highlightOffset = highlight->offset; | ||
| break; | ||
| if (process->mergedCommand.highlights) { | ||
| for (size_t i = 0; i < process->mergedCommand.highlightCount; i++) { | ||
| const ProcessCmdlineHighlight* highlight = &process->mergedCommand.highlights[i]; | ||
| if (highlight->flags & CMDLINE_HIGHLIGHT_FLAG_BASENAME) { | ||
| highlightLen = highlight->length; | ||
| highlightOffset = highlight->offset; | ||
| break; | ||
| } |
There was a problem hiding this comment.
This doesn't make much sense, as the loop will be skipped regardless. So you are reading more memory, just to do something that a non-broken compiler has been doing since the 1980s … Zero-exec loops were in the standard from the beginning and for maintainability we'll not be catering to broken compilers. Not to mention, that this optimization will likely cause more trouble than it benefits performance, as the likely case with this loop in particular is at least one execution …
| define_key("\033[I", KEY_FOCUS_IN); | ||
| define_key("\033[O", KEY_FOCUS_OUT); | ||
| full_write_str(STDOUT_FILENO, "\033[?1004h"); |
| typedef struct HashtableItem_ { | ||
| ht_key_t key; | ||
| size_t probe; | ||
| unsigned int probe; |
There was a problem hiding this comment.
Contradicts one intended direction we want to go with the Hashtable code for quite some time. Using size_t is intentional. Problem why this hasn't been done throughout the Hashtable code yet is related to the amount of affected code when this is changed.
| void Hashtable_shrink(Hashtable* this) { | ||
| assert(Hashtable_isConsistent(this)); | ||
|
|
||
| /* shrink on load-factor < 0.4 if size can be reduced */ | ||
| if (this->size > 61 && 10 * this->items < 4 * this->size) { | ||
| size_t targetSize = this->items ? (this->items * 2) : 13; | ||
| size_t newSize = nextPrime(targetSize); | ||
| if (newSize < this->size) { | ||
| Hashtable_setSize(this, newSize); | ||
| } | ||
| } | ||
|
|
||
| assert(Hashtable_isConsistent(this)); | ||
| } | ||
|
|
There was a problem hiding this comment.
There's related PRs to this. Don't mix concerns between PRs.
|
|
||
|
|
||
| #define METER_TXTBUFFER_LEN 256 | ||
| #define METER_TXTBUFFER_LEN 127 |
There was a problem hiding this comment.
This one should be left alone in general, as these buffer sizes are used throughout the code …
| static void Process_formatStarttime(const Process* this, char* buffer, size_t n) { | ||
| struct tm date; | ||
| time_t now = this->super.host->realtime.tv_sec; | ||
| time_t now = this->super.host ? this->super.host->realtime.tv_sec : time(NULL); |
There was a problem hiding this comment.
When this->super.host is NULL, we got bigger problems than the start time of a process not showing properly. This change hides bugs …
| uint64_t settingsStamp = settings->lastUpdate; | ||
| uint32_t settingsStamp = (uint32_t)settings->lastUpdate; |
There was a problem hiding this comment.
Wow, you just made the code harder to read just to safe 4 bytes on the stack …
| mc->highlights[mc->highlightCount].offset = str - strStart + (_offset) - mbMismatch; \ | ||
| mc->highlights[mc->highlightCount].length = _length; \ | ||
| mc->highlights[mc->highlightCount].offset = (uint32_t)(str - strStart + (_offset) - mbMismatch); \ | ||
| mc->highlights[mc->highlightCount].length = (uint32_t)(_length); \ |
| assert(mc->highlightCount < ARRAYSIZE(mc->highlights)); \ | ||
| if (mc->highlightCount >= ARRAYSIZE(mc->highlights)) \ | ||
| assert(mc->highlightCount < PROCESS_MAX_HIGHLIGHTS); \ | ||
| if (mc->highlightCount >= PROCESS_MAX_HIGHLIGHTS) \ |
There was a problem hiding this comment.
static array size check intentional …
| /* Establish some shortcuts to data we need */ | ||
| const char* cmdline = this->cmdline; | ||
| const char* procComm = this->procComm; | ||
| const char* procComm = this->procComm[0] ? this->procComm : NULL; |
|
I regularly run htop on embedded MCU at 500 MHz and ~256 MiB without significant impact on resource usage. Most of the current bottlenecks are commonly kernel issues (e.g. querying the memory map) or issues, where the fix would require tons of non-portable code for each individual platform (malloc_trim). If you want to take a look what implementing each platform's quirks just for completeness' sake leads to, you might want to take a look at lsof … |
By reducing the size of structures, we were able to optimize number of clock cycles required to send data to L3 cache and reduce the memory footprint. Thanks to malloc_trim, we were able to free up reserved memory more quickly. A very noticeable performance gain will be observed on processors prior to Core 2 Duo/Quad era, where there was no L3 cache and size of the structures was very important.
I’d like to raise a pressing issue about optimizing memory using malloc_trim by default (https://man7.org/linux/man-pages/man3/malloc_trim.3.html)
The prices of RAM and flash memory continue to rise and are once again breaking records.
https://nl.pcpartpicker.com/trends/price/memory/#ram.ddr5.6000.2x16384
@BenBE
I think we also need to value every user megabyte, because it’s unclear when prices for components will start to fall, and I hope you agree that HTOP also supports reuse electronic waste for ECO environmental purposes.
My benchmarks on old CPU Haswell 2014 year