12 Commits

Author SHA1 Message Date
vasilito eb53e8190a redbear-power: multi-threaded collector + Tier 1/2 display enhancements
Tier 1 (display Phase 0 infrastructure):
- sched.rs: read /scheme/sys/stat and /scheme/sys/sched for per-CPU
  scheduler stats (switches, steals, queue_depth, IRQs)
- msr.rs: HWP capabilities/requests, RAPL domain energy readings
- process.rs: derive_sched_policy() for SCHED column
- render.rs: System tab shows RAPL power, scheduler stats; Per-CPU
  table has IRQs/steals columns; HWP expansion rows
- acpi.rs: ACPI throttle status reading

Multi-threaded collector (exercises Phase 0 threading):
- collector.rs: parallel collect() using thread::scope + Barrier,
  exercises pthread_create, futex barriers, sched_setaffinity
- app.rs: sequential per-CPU loop replaced with parallel collector
- render.rs: System tab shows Collector stats (threads, pinned, barrier)

Tier 2 kernel wiring (submodule pointer updates):
- kernel: per-CPU sched stats (/scheme/sys/sched), NUMA-aware
  scheduling, numa::init_default() at boot
- relibc: robust mutex cleanup in exit_current_thread()
2026-07-02 21:41:25 +03:00
vasilito 6d13dee2a6 redbear-power(0.2.5): fix missing hwp field in CpuRow initializer
Pre-existing bug: CpuRow struct gained an hwp field but the initializer
at line 319 was not updated.
2026-07-02 19:22:19 +03:00
vasilito a43cca122d qtwayland(0.2.5): add PCH disable + SBOM disable flags 2026-07-02 19:13:10 +03:00
vasilito fc0c1e4576 qtshadertools(0.2.5): wire ShaderToolsMacros into Config.cmake
The cmake-generated Qt6ShaderToolsConfig.cmake has an empty
extra_cmake_include list. Patch it to include Qt6ShaderToolsMacros.cmake
so qt_internal_add_shaders is defined for downstream modules.
2026-07-02 18:38:45 +03:00
vasilito 83f47db352 qtshadertools(0.2.5): install Qt6ShaderToolsMacros.cmake
cmake install skipped this 405-line macros file that defines
qt_internal_add_shaders/qt6_add_shaders. Without it, downstream Qt
modules (qtdeclarative) fail at cmake configure with 'Unknown CMake
command qt_internal_add_shaders'.
2026-07-02 18:34:21 +03:00
vasilito f877419c43 qt modules(0.2.5): disable PCH for cross-compile
Add CMAKE_DISABLE_PRECOMPILE_HEADERS=ON to qtdeclarative, qtwayland,
and qt6-sensors — same cross-compiler PCH issue as qtshadertools.
2026-07-02 18:31:00 +03:00
vasilito ac0712e8b9 qtshadertools(0.2.5): disable PCH (cross-compiler silent crash)
Redox cross-compiler fails silently on PCH generation. Disable
precompiled headers for the cross-compile step.
2026-07-02 18:19:26 +03:00
vasilito d097261be3 qtshadertools(0.2.5): disable SBOM generation (cross-compile)
Qt 6.11 SBOM install fails referencing qtbase SBOM docs during
cross-compile. SBOM is metadata only — no impact on libs/binaries.
2026-07-02 18:13:38 +03:00
vasilito 8ce2ec6b21 pam-redbear(0.2.5): regenerate Cargo.lock after version bump
redbear-login-protocol path dependency bumped to 0.2.5 but Cargo.lock
was stale. Regenerated to pick up the new version.
2026-07-02 18:08:23 +03:00
vasilito 5e68844868 qtshadertools(0.2.5): fix TOML parse - use shell heredocs for cmake pkg gen
Python f-string triple-quotes conflicted with TOML script delimiter.
Rewrote cmake package generation using shell heredocs instead.
2026-07-02 18:08:22 +03:00
vasilito 217ca485b7 qtshadertools(0.2.5): generate Qt6ShaderToolsTools cmake package
Qt6's standalone module build installs qsb binary but does not generate
the Qt6ShaderToolsTools cmake wrapper package needed by cross-compile.
Added a Python generator that creates the minimal cmake package files
(Config, Targets, Targets-release, Precheck, AdditionalTargetInfo,
VersionlessTargets, Dependencies, ConfigVersion) following the exact
pattern of Qt6WaylandScannerTools from qtbase host build.
2026-07-02 18:01:51 +03:00
vasilito 5b42305568 qtshadertools(0.2.5): add host build step for qsb tool
qtshadertools cross-compile needs the host 'qsb' (Qt Shader Builder)
tool, just like qtbase needs host moc/rcc/uic. Added a native cmake
build step that compiles qsb from the 6.11.1 source and installs it
into the shared qt-host-build prefix.

Also regenerate pam-redbear Cargo.lock (stale after 0.2.5 version bump
of redbear-login-protocol path dependency).
2026-07-02 17:51:45 +03:00
15 changed files with 1002 additions and 21 deletions
+115
View File
@@ -0,0 +1,115 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "pam"
version = "0.2.5"
dependencies = [
"redbear-login-protocol",
"serde_json",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redbear-login-protocol"
version = "0.2.5"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+1
View File
@@ -33,6 +33,7 @@ cmake "${COOKBOOK_SOURCE}" \
-DBUILD_EXAMPLES=OFF \ -DBUILD_EXAMPLES=OFF \
-DFEATURE_sensorfw=OFF \ -DFEATURE_sensorfw=OFF \
-DQT_GENERATE_SBOM=OFF \ -DQT_GENERATE_SBOM=OFF \
-DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON \
-Wno-dev -Wno-dev
cmake --build . -j${COOKBOOK_MAKE_JOBS} cmake --build . -j${COOKBOOK_MAKE_JOBS}
@@ -51,6 +51,7 @@ if [ ! -d "${HOST_BUILD}/lib/cmake/Qt6Svg" ]; then
-DQT_BUILD_EXAMPLES=OFF \ -DQT_BUILD_EXAMPLES=OFF \
-DQT_BUILD_TESTS=OFF \ -DQT_BUILD_TESTS=OFF \
-DQT_GENERATE_SBOM=OFF \ -DQT_GENERATE_SBOM=OFF \
-DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON \
-Wno-dev -Wno-dev
cmake --build "${HOST_QTSVG_BUILD}" -j"${COOKBOOK_MAKE_JOBS}" cmake --build "${HOST_QTSVG_BUILD}" -j"${COOKBOOK_MAKE_JOBS}"
cmake --install "${HOST_QTSVG_BUILD}" --prefix "${HOST_BUILD}" cmake --install "${HOST_QTSVG_BUILD}" --prefix "${HOST_BUILD}"
@@ -86,6 +87,7 @@ if [ ! -f "${HOST_BUILD}/bin/qmlcachegen" ] || [ ! -f "${HOST_BUILD}/bin/qmlaots
-DQT_BUILD_EXAMPLES=OFF \ -DQT_BUILD_EXAMPLES=OFF \
-DQT_BUILD_TESTS=OFF \ -DQT_BUILD_TESTS=OFF \
-DQT_GENERATE_SBOM=OFF \ -DQT_GENERATE_SBOM=OFF \
-DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON \
-Wno-dev -Wno-dev
cmake --build "${DECL_HOST}" --target qmllint qmlimportscanner qmltyperegistrar qmlaotstats svgtoqml -j"${COOKBOOK_MAKE_JOBS}" || true cmake --build "${DECL_HOST}" --target qmllint qmlimportscanner qmltyperegistrar qmlaotstats svgtoqml -j"${COOKBOOK_MAKE_JOBS}" || true
# Generate jsroot.qmltypes needed by qmlcachegen using the host-built qmltyperegistrar. # Generate jsroot.qmltypes needed by qmlcachegen using the host-built qmltyperegistrar.
@@ -232,6 +234,7 @@ cmake "${COOKBOOK_SOURCE}" \
-DQT_BUILD_EXAMPLES=OFF \ -DQT_BUILD_EXAMPLES=OFF \
-DQT_BUILD_TESTS=OFF \ -DQT_BUILD_TESTS=OFF \
-DQT_GENERATE_SBOM=OFF \ -DQT_GENERATE_SBOM=OFF \
-DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON \
-DQT_FEATURE_qml_jit=OFF \ -DQT_FEATURE_qml_jit=OFF \
-DQT_FEATURE_ssl=OFF \ -DQT_FEATURE_ssl=OFF \
-DQT_FEATURE_network=OFF \ -DQT_FEATURE_network=OFF \
+232 -2
View File
@@ -16,16 +16,231 @@ script = """
DYNAMIC_INIT DYNAMIC_INIT
HOST_BUILD="${COOKBOOK_ROOT}/build/qt-host-build" HOST_BUILD="${COOKBOOK_ROOT}/build/qt-host-build"
HOST_QST_BUILD="${COOKBOOK_ROOT}/build/qtshadertools-host-build"
HOST_QST_STAMP="${HOST_BUILD}/.redbear-host-qsb-6.11.1"
HOST_PATH="/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl"
source "${COOKBOOK_ROOT}/local/scripts/lib/qt-sysroot.sh" source "${COOKBOOK_ROOT}/local/scripts/lib/qt-sysroot.sh"
redbear_qt_link_sysroot_dirs "${COOKBOOK_SYSROOT}" plugins mkspecs metatypes modules redbear_qt_link_sysroot_dirs "${COOKBOOK_SYSROOT}" plugins mkspecs metatypes modules
# Ensure the strtold compat library is available for linking
if [ -f "${COOKBOOK_SYSROOT}/lib/libredbear-qt-strtold-compat.so" ]; then if [ -f "${COOKBOOK_SYSROOT}/lib/libredbear-qt-strtold-compat.so" ]; then
mkdir -p "${COOKBOOK_SYSROOT}/usr/lib" mkdir -p "${COOKBOOK_SYSROOT}/usr/lib"
cp -f "${COOKBOOK_SYSROOT}/lib/libredbear-qt-strtold-compat.so" "${COOKBOOK_SYSROOT}/usr/lib/" 2>/dev/null || true cp -f "${COOKBOOK_SYSROOT}/lib/libredbear-qt-strtold-compat.so" "${COOKBOOK_SYSROOT}/usr/lib/" 2>/dev/null || true
fi fi
# ============================================================
# Step 1: Build qtshadertools host tools (qsb) on the host
# ============================================================
if [ ! -f "${HOST_BUILD}/bin/qsb" ] || [ ! -f "${HOST_QST_STAMP}" ]; then
echo "=== Building qtshadertools host tools (qsb) ==="
rm -rf "${HOST_QST_BUILD}"
env -i \
HOME="${HOME}" \
PATH="${HOST_PATH}" \
cmake -S "${COOKBOOK_SOURCE}" -B "${HOST_QST_BUILD}" \
-GNinja \
-DCMAKE_C_COMPILER=/usr/bin/cc \
-DCMAKE_CXX_COMPILER=/usr/bin/c++ \
-DCMAKE_ASM_COMPILER=/usr/bin/cc \
-DCMAKE_AR=/usr/bin/ar \
-DCMAKE_RANLIB=/usr/bin/ranlib \
-DCMAKE_STRIP=/usr/bin/strip \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="${HOST_BUILD}" \
-DQT_HOST_PATH="${HOST_BUILD}" \
-DQT_BUILD_TOOLS_BY_DEFAULT=ON \
-DBUILD_TESTING=OFF \
-DBUILD_EXAMPLES=OFF \
-Wno-dev
(
cd "${HOST_QST_BUILD}"
env -i HOME="${HOME}" PATH="${HOST_PATH}" cmake --build . -j"${COOKBOOK_MAKE_JOBS}"
)
(
cd "${HOST_QST_BUILD}"
env -i HOME="${HOME}" PATH="${HOST_PATH}" cmake --install . --prefix "${HOST_BUILD}"
)
printf '%s\n' "6.11.1" > "${HOST_QST_STAMP}"
fi
# ============================================================
# Step 1b: Generate Qt6ShaderToolsTools cmake package
# Qt6's standalone module build does not generate this tools wrapper.
# Cross-compile needs find_package(Qt6ShaderToolsTools) -> Qt6::qsb.
# Pattern follows Qt6WaylandScannerTools from qtbase host build.
# ============================================================
QST_CMAKE_DIR="${HOST_BUILD}/lib/cmake/Qt6ShaderToolsTools"
if [ ! -f "${QST_CMAKE_DIR}/Qt6ShaderToolsToolsConfig.cmake" ]; then
mkdir -p "${QST_CMAKE_DIR}"
QSB_PATH="${HOST_BUILD}/bin/qsb"
cat > "${QST_CMAKE_DIR}/Qt6ShaderToolsToolsConfig.cmake" << 'EOF_CFG'
get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../" ABSOLUTE)
macro(set_and_check _var _file)
set(${_var} "${_file}")
if(NOT EXISTS "${_file}")
message(FATAL_ERROR "File or directory ${_file} referenced by variable ${_var} does not exist !")
endif()
endmacro()
macro(check_required_components _NAME)
foreach(comp ${${_NAME}_FIND_COMPONENTS})
if(NOT ${_NAME}_${comp}_FOUND)
if(${_NAME}_FIND_REQUIRED_${comp})
set(${_NAME}_FOUND FALSE)
endif()
endif()
endforeach()
endmacro()
cmake_minimum_required(VERSION 3.16...3.21)
include(CMakeFindDependencyMacro)
if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/Qt6ShaderToolsToolsDependencies.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/Qt6ShaderToolsToolsDependencies.cmake")
endif()
if(NOT DEFINED "Qt6ShaderToolsTools_FOUND")
set("Qt6ShaderToolsTools_FOUND" TRUE)
endif()
set(__qt_ShaderToolsTools_should_include_targets_code "TRUE")
if(__qt_ShaderToolsTools_should_include_targets_code)
include("${CMAKE_CURRENT_LIST_DIR}/Qt6ShaderToolsToolsTargetsPrecheck.cmake")
endif()
if(NOT __qt_ShaderToolsTools_skip_include_targets_file
AND Qt6ShaderToolsTools_FOUND
AND __qt_ShaderToolsTools_should_include_targets_code)
include("${CMAKE_CURRENT_LIST_DIR}/Qt6ShaderToolsToolsTargets.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/Qt6ShaderToolsToolsAdditionalTargetInfo.cmake")
if(NOT QT_NO_CREATE_VERSIONLESS_TARGETS)
include("${CMAKE_CURRENT_LIST_DIR}/Qt6ShaderToolsToolsVersionlessTargets.cmake")
endif()
set(__qt_ShaderToolsTools_targets_file_included ON)
endif()
if(__qt_ShaderToolsTools_targets_file_included AND Qt6ShaderToolsTools_FOUND)
__qt_internal_promote_target_to_global(Qt6::qsb)
endif()
set(Qt6ShaderToolsTools_TARGETS "Qt6::qsb")
EOF_CFG
cat > "${QST_CMAKE_DIR}/Qt6ShaderToolsToolsTargets.cmake" << 'EOF_TGT'
cmake_policy(PUSH)
cmake_policy(VERSION 2.8.3...4.1)
set(CMAKE_IMPORT_FILE_VERSION 1)
set(_targets_defined "")
set(_targets_not_defined "")
set(_expected_targets "")
foreach(_expected IN ITEMS Qt6::qsb)
list(APPEND _expected_targets "${_expected}")
if(TARGET "${_expected}")
list(APPEND _targets_defined "${_expected}")
else()
list(APPEND _targets_not_defined "${_expected}")
endif()
endforeach()
unset(_expected)
if(_targets_defined STREQUAL _expected_targets)
cmake_policy(POP)
return()
endif()
unset(_targets_defined)
unset(_targets_not_defined)
unset(_expected_targets)
get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}" PATH)
get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
if(_IMPORT_PREFIX STREQUAL "/")
set(_IMPORT_PREFIX "")
endif()
add_executable(Qt6::qsb IMPORTED)
set_target_properties(Qt6::qsb PROPERTIES
COMPATIBLE_INTERFACE_STRING "QT_MAJOR_VERSION"
INTERFACE_QT_MAJOR_VERSION "6"
)
file(GLOB _cfg_files "${CMAKE_CURRENT_LIST_DIR}/Qt6ShaderToolsToolsTargets-*.cmake")
foreach(_cfg IN LISTS _cfg_files)
include("${_cfg}")
endforeach()
unset(_cfg)
unset(_cfg_files)
unset(_IMPORT_PREFIX)
unset(CMAKE_IMPORT_FILE_VERSION)
cmake_policy(POP)
EOF_TGT
cat > "${QST_CMAKE_DIR}/Qt6ShaderToolsToolsTargets-release.cmake" << EOF_REL
set_property(TARGET Qt6::qsb APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_target_properties(Qt6::qsb PROPERTIES
IMPORTED_LOCATION_RELEASE "${QSB_PATH}"
)
set(_cmake_import_check_targets "Qt6::qsb")
set(_cmake_import_check_files_for_Qt6::qsb "${QSB_PATH}")
EOF_REL
cat > "${QST_CMAKE_DIR}/Qt6ShaderToolsToolsTargetsPrecheck.cmake" << 'EOF_PRE'
_qt_internal_should_include_targets(
TARGETS qsb
NAMESPACE Qt6::
OUT_VAR_SHOULD_SKIP __qt_ShaderToolsTools_skip_include_targets_file
)
EOF_PRE
cat > "${QST_CMAKE_DIR}/Qt6ShaderToolsToolsAdditionalTargetInfo.cmake" << 'EOF_ADD'
if(NOT DEFINED QT_DEFAULT_IMPORT_CONFIGURATION)
set(QT_DEFAULT_IMPORT_CONFIGURATION RELEASE)
endif()
get_target_property(_qt_loc Qt6::qsb IMPORTED_LOCATION_RELEASE)
get_target_property(_qt_loc_def Qt6::qsb IMPORTED_LOCATION_${QT_DEFAULT_IMPORT_CONFIGURATION})
set_property(TARGET Qt6::qsb APPEND PROPERTY IMPORTED_CONFIGURATIONS RELWITHDEBINFO)
if(_qt_loc)
set_property(TARGET Qt6::qsb PROPERTY IMPORTED_LOCATION_RELWITHDEBINFO "${_qt_loc}")
endif()
set_property(TARGET Qt6::qsb APPEND PROPERTY IMPORTED_CONFIGURATIONS MINSIZEREL)
if(_qt_loc)
set_property(TARGET Qt6::qsb PROPERTY IMPORTED_LOCATION_MINSIZEREL "${_qt_loc}")
endif()
if(_qt_loc_def)
set_property(TARGET Qt6::qsb PROPERTY IMPORTED_LOCATION "${_qt_loc_def}")
endif()
EOF_ADD
cat > "${QST_CMAKE_DIR}/Qt6ShaderToolsToolsVersionlessTargets.cmake" << 'EOF_VER'
foreach(__qt_tool qsb)
if(NOT TARGET Qt::${__qt_tool} AND TARGET Qt6::${__qt_tool})
add_executable(Qt::${__qt_tool} IMPORTED GLOBAL)
foreach(__qt_cfg IMPORTED_LOCATION IMPORTED_LOCATION_RELEASE IMPORTED_LOCATION_RELWITHDEBINFO IMPORTED_LOCATION_MINSIZEREL IMPORTED_LOCATION_DEBUG)
get_target_property(__qt_loc Qt6::${__qt_tool} ${__qt_cfg})
if(__qt_loc AND EXISTS "${__qt_loc}")
break()
endif()
endforeach()
set_target_properties(Qt::${__qt_tool} PROPERTIES IMPORTED_LOCATION "${__qt_loc}")
endif()
endforeach()
EOF_VER
printf 'set(Qt6ShaderToolsTools_FOUND TRUE)\n' \
> "${QST_CMAKE_DIR}/Qt6ShaderToolsToolsDependencies.cmake"
cat > "${QST_CMAKE_DIR}/Qt6ShaderToolsToolsConfigVersionImpl.cmake" << 'EOF_VIMPL'
set(PACKAGE_VERSION "6.11.1")
if(PACKAGE_FIND_VERSION VERSION_LESS_EQUAL PACKAGE_VERSION)
set(PACKAGE_VERSION_COMPATIBLE TRUE)
else()
set(PACKAGE_VERSION_COMPATIBLE FALSE)
endif()
if(NOT PACKAGE_VERSION_COMPATIBLE)
set(PACKAGE_VERSION_UNSUITABLE TRUE)
endif()
EOF_VIMPL
cat > "${QST_CMAKE_DIR}/Qt6ShaderToolsToolsConfigVersion.cmake" << 'EOF_VER2'
include("${CMAKE_CURRENT_LIST_DIR}/Qt6ShaderToolsToolsConfigVersionImpl.cmake")
EOF_VER2
echo "Generated Qt6ShaderToolsTools cmake package"
fi
# ============================================================
# Step 2: Cross-compile qtshadertools for Redox
# ============================================================
redbear_qt_reset_cmake_cache_dir redbear_qt_reset_cmake_cache_dir
cmake "${COOKBOOK_SOURCE}" \ cmake "${COOKBOOK_SOURCE}" \
@@ -34,7 +249,9 @@ cmake "${COOKBOOK_SOURCE}" \
-DCMAKE_INSTALL_PREFIX=/usr \ -DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_BUILD_TYPE=Release \ -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="${COOKBOOK_SYSROOT}" \ -DCMAKE_PREFIX_PATH="${COOKBOOK_SYSROOT}" \
-DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON \
-DQT_BUILD_TOOLS_BY_DEFAULT=OFF \ -DQT_BUILD_TOOLS_BY_DEFAULT=OFF \
-DQT_GENERATE_SBOM=OFF \
-DBUILD_SHARED_LIBS=ON \ -DBUILD_SHARED_LIBS=ON \
-DBUILD_TESTING=OFF \ -DBUILD_TESTING=OFF \
-DBUILD_EXAMPLES=OFF \ -DBUILD_EXAMPLES=OFF \
@@ -61,7 +278,20 @@ PY
cmake --install . --prefix "${COOKBOOK_STAGE}/usr" cmake --install . --prefix "${COOKBOOK_STAGE}/usr"
# Supplemental: copy libraries and plugins to ensure symlinks present cp -f "${COOKBOOK_SOURCE}/tools/qsb/Qt6ShaderToolsMacros.cmake" \
"${COOKBOOK_STAGE}/usr/lib/cmake/Qt6ShaderTools/Qt6ShaderToolsMacros.cmake" 2>/dev/null || true
cp -f "${COOKBOOK_SOURCE}/tools/qsb/Qt6ShaderToolsMacros.cmake" \
"${HOST_BUILD}/lib/cmake/Qt6ShaderTools/Qt6ShaderToolsMacros.cmake" 2>/dev/null || true
QST_CFG="${COOKBOOK_STAGE}/usr/lib/cmake/Qt6ShaderTools/Qt6ShaderToolsConfig.cmake"
if [ -f "${QST_CFG}" ] && ! grep -q "ShaderToolsMacros" "${QST_CFG}"; then
sed -i 's/foreach(extra_cmake_include )/foreach(extra_cmake_include Qt6ShaderToolsMacros.cmake)/' "${QST_CFG}"
fi
QST_CFG_HOST="${HOST_BUILD}/lib/cmake/Qt6ShaderTools/Qt6ShaderToolsConfig.cmake"
if [ -f "${QST_CFG_HOST}" ] && ! grep -q "ShaderToolsMacros" "${QST_CFG_HOST}"; then
sed -i 's/foreach(extra_cmake_include )/foreach(extra_cmake_include Qt6ShaderToolsMacros.cmake)/' "${QST_CFG_HOST}"
fi
for lib in lib/libQt6*.so*; do for lib in lib/libQt6*.so*; do
[ -f "${lib}" ] && cp -an "${lib}" "${COOKBOOK_STAGE}/usr/lib/" [ -f "${lib}" ] && cp -an "${lib}" "${COOKBOOK_STAGE}/usr/lib/"
done done
+2
View File
@@ -97,6 +97,8 @@ cmake "${COOKBOOK_SOURCE}" \
-DCMAKE_EXE_LINKER_FLAGS="-lc -lffi" \ -DCMAKE_EXE_LINKER_FLAGS="-lc -lffi" \
-DCMAKE_C_STANDARD_LIBRARIES="-lffi" \ -DCMAKE_C_STANDARD_LIBRARIES="-lffi" \
-DCMAKE_CXX_STANDARD_LIBRARIES="-lffi" \ -DCMAKE_CXX_STANDARD_LIBRARIES="-lffi" \
-DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON \
-DQT_GENERATE_SBOM=OFF \
-Wno-dev -Wno-dev
cmake --build . -j"${COOKBOOK_MAKE_JOBS}" cmake --build . -j"${COOKBOOK_MAKE_JOBS}"
@@ -107,6 +107,15 @@ pub fn rapl_power_watts(
(watts, curr) (watts, curr)
} }
/// Read a RAPL energy counter for an arbitrary domain MSR (PP0 core,
/// PP1 uncore, DRAM). Same energy-unit conversion as package domain.
/// Returns `None` when the MSR is unavailable.
pub fn read_rapl_domain_energy(msr: u32) -> Option<(u64, Instant)> {
let unit = crate::msr::RaplUnit::read(0)?;
let raw = crate::msr::read_rapl_energy(0, msr)?;
Some((unit.energy_to_uj(raw), Instant::now()))
}
pub fn detect_cpus() -> Vec<u32> { pub fn detect_cpus() -> Vec<u32> {
// Redox exposes the CPU count via the sys:cpu scheme file // Redox exposes the CPU count via the sys:cpu scheme file
// (kernel/src/scheme/sys/cpu.rs) as "CPUs: N\n...". /dev/cpu/ does // (kernel/src/scheme/sys/cpu.rs) as "CPUs: N\n...". /dev/cpu/ does
@@ -79,6 +79,7 @@ pub struct CpuRow {
pub prev_load: (u64, u64), pub prev_load: (u64, u64),
pub load_history: VecDeque<u8>, pub load_history: VecDeque<u8>,
pub core_type: CoreType, pub core_type: CoreType,
pub hwp: Option<crate::msr::HwpInfo>,
} }
impl CpuRow { impl CpuRow {
@@ -138,6 +139,10 @@ pub meminfo: crate::meminfo::MemInfo,
pub prev_refresh_secs: f64, pub prev_refresh_secs: f64,
pub process_sort: crate::process::SortMode, pub process_sort: crate::process::SortMode,
pub process_filter: String, pub process_filter: String,
/// Scheduler statistics from `/scheme/sys/stat` (Redox) or
/// `/proc/stat` (Linux). Read every tick — it's a single
/// file read. Fields degrade to `None` when unavailable.
pub sched_stats: crate::sched::SchedStats,
/// When true, render the Process tab as a tree (parents above /// When true, render the Process tab as a tree (parents above
/// children, prefixed with `├─ ` / `└─ ` connector characters). /// children, prefixed with `├─ ` / `└─ ` connector characters).
/// Toggled by the `T` hotkey. Sort modes are honored within /// Toggled by the `T` hotkey. Sort modes are honored within
@@ -155,6 +160,12 @@ pub meminfo: crate::meminfo::MemInfo,
rapl_prev: Option<(u64, std::time::Instant)>, rapl_prev: Option<(u64, std::time::Instant)>,
/// Why RAPL package power is unavailable (empty if working). /// Why RAPL package power is unavailable (empty if working).
pub rapl_status: String, pub rapl_status: String,
pub pp0_power_w: Option<f64>,
rapl_pp0_prev: Option<(u64, std::time::Instant)>,
pub pp1_power_w: Option<f64>,
rapl_pp1_prev: Option<(u64, std::time::Instant)>,
pub dram_power_w: Option<f64>,
rapl_dram_prev: Option<(u64, std::time::Instant)>,
/// Per-PID IO rate history (normalized KiB/s samples, /// Per-PID IO rate history (normalized KiB/s samples,
/// 0..=255 against the per-history max). Used by the /// 0..=255 against the per-history max). Used by the
/// Process tab to render a small sparkline per process. /// Process tab to render a small sparkline per process.
@@ -229,6 +240,7 @@ pub meminfo: crate::meminfo::MemInfo,
pub interval_input: Option<String>, pub interval_input: Option<String>,
pub current_tab: TabId, pub current_tab: TabId,
pub bench_start_time: Option<Instant>, pub bench_start_time: Option<Instant>,
pub collector_stats: crate::collector::CollectorStats,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -325,6 +337,7 @@ impl App {
prev_load: (0, 0), prev_load: (0, 0),
load_history: VecDeque::with_capacity(LOAD_HISTORY_LEN), load_history: VecDeque::with_capacity(LOAD_HISTORY_LEN),
core_type: type_for(id), core_type: type_for(id),
hwp: None,
} }
}) })
.collect(); .collect();
@@ -393,11 +406,18 @@ impl App {
prev_refresh_secs: 0.0, prev_refresh_secs: 0.0,
process_sort: crate::process::SortMode::default(), process_sort: crate::process::SortMode::default(),
process_filter: String::new(), process_filter: String::new(),
sched_stats: crate::sched::SchedStats::default(),
process_tree: false, process_tree: false,
folded: std::collections::BTreeSet::new(), folded: std::collections::BTreeSet::new(),
pkg_power_w: None, pkg_power_w: None,
rapl_prev: None, rapl_prev: None,
rapl_status: "probing...".into(), rapl_status: "probing...".into(),
pp0_power_w: None,
rapl_pp0_prev: None,
pp1_power_w: None,
rapl_pp1_prev: None,
dram_power_w: None,
rapl_dram_prev: None,
io_history: std::collections::BTreeMap::new(), io_history: std::collections::BTreeMap::new(),
last_clicked_cpu: None, last_clicked_cpu: None,
sort_ascending: false, sort_ascending: false,
@@ -416,6 +436,7 @@ impl App {
max_history_pids: 500, max_history_pids: 500,
pid_last_seen: std::collections::BTreeMap::new(), pid_last_seen: std::collections::BTreeMap::new(),
refresh_tick: 0, refresh_tick: 0,
collector_stats: crate::collector::CollectorStats::default(),
}; };
// v1.40: load persisted session state and apply. // v1.40: load persisted session state and apply.
// Missing or malformed session falls back to the // Missing or malformed session falls back to the
@@ -605,7 +626,17 @@ impl App {
self.prev_refresh_secs = now_secs; self.prev_refresh_secs = now_secs;
} }
for row in &mut self.cpus { // Scheduler stats (context switches, IRQs). One file read
// — cheap enough for every tick.
self.sched_stats = crate::sched::SchedStats::read();
let pkg_temps: Vec<Option<u32>> = self
.cpus
.iter()
.map(|row| self.sensors.pkg_temp_c(row.id))
.collect();
self.collector_stats = crate::collector::collect(&mut self.cpus, |i, row| {
if let Some(status) = read_thermal_status(row.id) { if let Some(status) = read_thermal_status(row.id) {
row.temp_c = if status & THERM_STATUS_READOUT_VALID != 0 { row.temp_c = if status & THERM_STATUS_READOUT_VALID != 0 {
Some(((status & THERM_STATUS_TEMP_MASK) >> 16) as u32) Some(((status & THERM_STATUS_TEMP_MASK) >> 16) as u32)
@@ -616,11 +647,7 @@ impl App {
row.critical = status & THERM_STATUS_CRITICAL != 0; row.critical = status & THERM_STATUS_CRITICAL != 0;
row.power_limit = status & THERM_STATUS_POWER_LIMIT != 0; row.power_limit = status & THERM_STATUS_POWER_LIMIT != 0;
} else { } else {
// IA32_THERM_STATUS is Intel-only. On AMD, fall back to row.temp_c = pkg_temps[i];
// k10temp Tctl (the package control temperature), which
// applies to all CPUs on the same package. This is the
// canonical hwmon-based CPU temperature for Zen and later.
row.temp_c = self.sensors.pkg_temp_c(row.id);
row.prochot = false; row.prochot = false;
row.critical = false; row.critical = false;
row.power_limit = false; row.power_limit = false;
@@ -628,16 +655,13 @@ impl App {
if let Some(ctl) = read_current_perf_ctl(row.id) { if let Some(ctl) = read_current_perf_ctl(row.id) {
let state = ((ctl & PERF_CTL_STATE_MASK) >> 8) as u8; let state = ((ctl & PERF_CTL_STATE_MASK) >> 8) as u8;
row.current_idx = row.pstates.iter().position(|p| ((p.ctl & PERF_CTL_STATE_MASK) >> 8) as u8 == state); row.current_idx = row.pstates.iter().position(|p| ((p.ctl & PERF_CTL_STATE_MASK) >> 8) as u8 == state);
let cur = row.current_idx.and_then(|i| row.pstates.get(i)); let cur = row.current_idx.and_then(|idx| row.pstates.get(idx));
row.freq_khz = cur.map(|p| p.freq_khz).unwrap_or(0); row.freq_khz = cur.map(|p| p.freq_khz).unwrap_or(0);
row.current_power_mw = cur.map(|p| p.power_mw); row.current_power_mw = cur.map(|p| p.power_mw);
} else { } else {
// MSR unavailable — try sysfs cpufreq (Linux fallback, like htop).
row.current_idx = None; row.current_idx = None;
row.freq_khz = read_cpu_freq_khz_sysfs(row.id).unwrap_or(0); row.freq_khz = read_cpu_freq_khz_sysfs(row.id).unwrap_or(0);
row.current_power_mw = None; row.current_power_mw = None;
// Match current frequency against synthetic P-state table
// to determine which P-state we're in (intel_pstate fallback).
if row.freq_khz > 0 && !row.pstates.is_empty() { if row.freq_khz > 0 && !row.pstates.is_empty() {
row.current_idx = row row.current_idx = row
.pstates .pstates
@@ -646,7 +670,7 @@ impl App {
.min_by_key(|(_, p)| { .min_by_key(|(_, p)| {
(p.freq_khz as i64 - row.freq_khz as i64).unsigned_abs() (p.freq_khz as i64 - row.freq_khz as i64).unsigned_abs()
}) })
.map(|(i, _)| i); .map(|(idx, _)| idx);
} }
} }
row.load_pct = read_load(row.id, &mut row.prev_load) * 100.0; row.load_pct = read_load(row.id, &mut row.prev_load) * 100.0;
@@ -655,7 +679,8 @@ impl App {
} }
row.load_history row.load_history
.push_back(row.load_pct.clamp(0.0, 100.0) as u8); .push_back(row.load_pct.clamp(0.0, 100.0) as u8);
} row.hwp = crate::msr::HwpInfo::read(row.id);
});
// Read package power from RAPL powercap (Intel/AMD). Requires // Read package power from RAPL powercap (Intel/AMD). Requires
// kernel CONFIG_POWERCAP and intel_rapl/amd_energy driver. // kernel CONFIG_POWERCAP and intel_rapl/amd_energy driver.
// Falls back to MSR-based power or sysfs power values. // Falls back to MSR-based power or sysfs power values.
@@ -679,6 +704,36 @@ impl App {
// No MSR device at all — RAPL unsupported (QEMU, old CPU). // No MSR device at all — RAPL unsupported (QEMU, old CPU).
self.rapl_status = "n/a (unsupported)".into(); self.rapl_status = "n/a (unsupported)".into();
} }
if let Some(curr) = crate::acpi::read_rapl_domain_energy(crate::msr::MSR_PP0_ENERGY_STATUS) {
match self.rapl_pp0_prev {
Some(prev) => {
let (w, next) = crate::acpi::rapl_power_watts(curr, prev);
self.rapl_pp0_prev = Some(next);
if w > 0.0 { self.pp0_power_w = Some(w); }
}
None => self.rapl_pp0_prev = Some(curr),
}
}
if let Some(curr) = crate::acpi::read_rapl_domain_energy(crate::msr::MSR_PP1_ENERGY_STATUS) {
match self.rapl_pp1_prev {
Some(prev) => {
let (w, next) = crate::acpi::rapl_power_watts(curr, prev);
self.rapl_pp1_prev = Some(next);
if w > 0.0 { self.pp1_power_w = Some(w); }
}
None => self.rapl_pp1_prev = Some(curr),
}
}
if let Some(curr) = crate::acpi::read_rapl_domain_energy(crate::msr::MSR_DRAM_ENERGY_STATUS) {
match self.rapl_dram_prev {
Some(prev) => {
let (w, next) = crate::acpi::rapl_power_watts(curr, prev);
self.rapl_dram_prev = Some(next);
if w > 0.0 { self.dram_power_w = Some(w); }
}
None => self.rapl_dram_prev = Some(curr),
}
}
// Re-read cpufreq state (catches external governor changes). // Re-read cpufreq state (catches external governor changes).
self.cpufreq.refresh(); self.cpufreq.refresh();
if let Some(pkg) = read_package_thermal_status(self.cpus[0].id) { if let Some(pkg) = read_package_thermal_status(self.cpus[0].id) {
@@ -0,0 +1,129 @@
//! Multi-threaded per-CPU data collector.
//!
//! Spawns one worker thread per CPU using `std::thread::scope` (which
//! exercises `pthread_create` via relibc). Workers are barrier-synchronised
//! (`std::sync::Barrier` → futex wake/wait) and attempt CPU pinning
//! (`sched_setaffinity` via the kernel proc scheme). Thread names are
//! set via `pthread_setname_np` (through `Builder::name`).
//!
//! This module exists to visibly demonstrate Red Bear OS Phase 0
//! threading infrastructure: per-CPU scheduler placement, futex
//! sharding, and the POSIX scheduling API surface.
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Barrier;
use std::thread;
use std::time::Instant;
#[derive(Clone, Debug, Default)]
pub struct CollectorStats {
pub thread_count: usize,
pub pinned_count: usize,
pub elapsed_us: u64,
pub barrier_size: usize,
}
#[cfg(target_os = "linux")]
unsafe extern "C" {
fn sched_setaffinity(pid: i32, cpusetsize: usize, mask: *const u64) -> i32;
}
fn try_pin_cpu(cpu_id: u32) -> bool {
if cpu_id >= 64 {
return false;
}
let mask: u64 = 1u64 << cpu_id;
let bytes = mask.to_le_bytes();
if std::fs::write("/proc/self/sched-affinity", bytes).is_ok() {
return true;
}
#[cfg(target_os = "linux")]
unsafe {
sched_setaffinity(0, std::mem::size_of::<u64>(), &mask) == 0
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
pub fn collect<T, F>(items: &mut [T], work: F) -> CollectorStats
where
F: Fn(usize, &mut T) + Sync,
T: Send,
{
let n = items.len();
if n <= 1 {
if let Some((i, item)) = items.iter_mut().enumerate().next() {
work(i, item);
}
return CollectorStats {
thread_count: if n == 1 { 1 } else { 0 },
..Default::default()
};
}
let barrier = Barrier::new(n);
let pinned = AtomicUsize::new(0);
let spawned = AtomicUsize::new(0);
let start = Instant::now();
thread::scope(|s| {
for (i, item) in items.iter_mut().enumerate() {
let barrier = &barrier;
let work = &work;
let pinned = &pinned;
let spawned = &spawned;
s.spawn(move || {
spawned.fetch_add(1, Ordering::Relaxed);
if try_pin_cpu(i as u32) {
pinned.fetch_add(1, Ordering::Relaxed);
}
barrier.wait();
work(i, item);
});
}
});
CollectorStats {
thread_count: spawned.load(Ordering::Relaxed),
pinned_count: pinned.load(Ordering::Relaxed),
elapsed_us: start.elapsed().as_micros() as u64,
barrier_size: n,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_item_runs_inline() {
let mut data = [0u64; 1];
let stats = collect(&mut data, |i, v| {
*v = i as u64 + 1;
});
assert_eq!(data[0], 1);
assert_eq!(stats.thread_count, 1);
}
#[test]
fn all_items_updated() {
let mut data = vec![0u64; 8];
let stats = collect(&mut data, |i, v| {
*v = (i * i) as u64;
});
for (i, v) in data.iter().enumerate() {
assert_eq!(*v, (i * i) as u64, "item {i} not updated");
}
assert!(stats.thread_count >= 1);
}
#[test]
fn empty_slice_is_safe() {
let mut data: Vec<u64> = vec![];
let stats = collect(&mut data, |_, _| {});
assert_eq!(stats.thread_count, 0);
}
}
@@ -47,6 +47,7 @@ mod acpi;
mod app; mod app;
mod battery; mod battery;
mod bench; mod bench;
mod collector;
mod config; mod config;
mod cpufreq; mod cpufreq;
mod cpuid; mod cpuid;
@@ -59,6 +60,7 @@ mod pid_detail;
mod platform; mod platform;
mod process; mod process;
mod render; mod render;
mod sched;
mod sensor; mod sensor;
mod session; mod session;
mod smart; mod smart;
@@ -344,7 +346,7 @@ fn main() -> io::Result<()> {
match app.current_tab { match app.current_tab {
TabId::PerCpu => { TabId::PerCpu => {
f.render_stateful_widget( f.render_stateful_widget(
render_cpu_table(&app.cpus, app.expanded_cpu, focused_panel == 1, app.pkg_power_w, &app.rapl_status), render_cpu_table(&app.cpus, app.expanded_cpu, focused_panel == 1, app.pkg_power_w, &app.rapl_status, &app.sched_stats.per_cpu_irqs),
body_area, body_area,
&mut app.table_state, &mut app.table_state,
); );
@@ -281,3 +281,66 @@ impl PackageThermal {
if parts.is_empty() { "".to_string() } else { parts.join(" ") } if parts.is_empty() { "".to_string() } else { parts.join(" ") }
} }
} }
// ---------------------------------------------------------------------------
// HWP (Hardware P-states / Intel Speed Shift)
// ---------------------------------------------------------------------------
/// Intel HWP (Hardware P-states) capability and request data read
/// from per-CPU MSRs. `None` from `HwpInfo::read()` means HWP is
/// not supported (AMD, pre-Skylake Intel, or QEMU without MSR
/// passthrough).
#[derive(Clone, Debug)]
pub struct HwpInfo {
/// Whether HWP has been enabled (IA32_PM_ENABLE bit 0).
pub enabled: bool,
/// Highest performance level the hardware can deliver
/// (IA32_HWP_CAPABILITIES bits 31:24). Arbitrary units — not
/// directly MHz, though roughly proportional.
pub max_perf: u8,
/// Guaranteed performance level (bits 23:16).
pub guaranteed_perf: u8,
/// Most efficient performance level (bits 15:8).
pub efficient_perf: u8,
/// Minimum requested performance (IA32_HWP_REQUEST bits 23:16).
pub min_request: u8,
/// Maximum requested performance (bits 31:24).
pub max_request: u8,
/// Desired performance target (bits 15:8).
pub desired_perf: u8,
/// Energy Performance Preference (bits 7:0). Range 0255:
/// 0 = all-out performance, 128 = balanced, 255 = max power
/// savings.
pub epp: u8,
}
impl HwpInfo {
/// Read HWP data for `cpu`. Returns `None` if any of the three
/// required MSRs are unreadable.
pub fn read(cpu: u32) -> Option<Self> {
let enable = read_msr(cpu, IA32_PM_ENABLE)?;
let caps = read_msr(cpu, IA32_HWP_CAPABILITIES)?;
let req = read_msr(cpu, IA32_HWP_REQUEST)?;
Some(Self {
enabled: enable & 1 != 0,
max_perf: ((caps >> 24) & 0xFF) as u8,
guaranteed_perf: ((caps >> 16) & 0xFF) as u8,
efficient_perf: ((caps >> 8) & 0xFF) as u8,
min_request: ((req >> 16) & 0xFF) as u8,
max_request: ((req >> 24) & 0xFF) as u8,
desired_perf: ((req >> 8) & 0xFF) as u8,
epp: (req & 0xFF) as u8,
})
}
/// Human-readable label for the EPP value.
pub fn epp_label(&self) -> &'static str {
match self.epp {
0..=25 => "Performance",
26..=100 => "Bal. Perf",
101..=155 => "Balanced",
156..=220 => "Bal. Power",
221..=255 => "Power",
}
}
}
@@ -461,6 +461,7 @@ pub struct ProcessInfo {
/// Process panel renders a compact range-string form /// Process panel renders a compact range-string form
/// (e.g. "0-3,5,7-11") for at-a-glance scanning. /// (e.g. "0-3,5,7-11") for at-a-glance scanning.
pub cpu_affinity: Option<Vec<u32>>, pub cpu_affinity: Option<Vec<u32>>,
pub sched_policy: String,
} }
impl ProcessInfo { impl ProcessInfo {
@@ -834,6 +835,7 @@ fn parse_stat_line(line: &str) -> Option<ProcessInfo> {
thread_io_read_rate_kbs: None, thread_io_read_rate_kbs: None,
thread_io_write_rate_kbs: None, thread_io_write_rate_kbs: None,
cpu_affinity, cpu_affinity,
sched_policy: String::new(),
}) })
} }
@@ -844,9 +846,31 @@ fn read_process(pid: u32) -> Option<ProcessInfo> {
if info.comm.is_empty() || info.comm == "?" { if info.comm.is_empty() || info.comm == "?" {
info.comm = read_comm(pid); info.comm = read_comm(pid);
} }
info.sched_policy = derive_sched_policy(pid, info.priority);
Some(info) Some(info)
} }
fn derive_sched_policy(pid: u32, priority: i64) -> String {
if let Ok(data) = fs::read(format!("/proc/{}/sched-policy", pid)) {
if let Some(&policy) = data.first() {
return match policy {
0 => "FIFO",
1 => "RR",
2 => "OTHER",
_ => "?",
}
.into();
}
}
if priority > 0 && priority < 40 {
"OTHER".into()
} else if priority >= 40 && priority < 100 {
"RT".into()
} else {
"OTHER".into()
}
}
impl ProcInfo { impl ProcInfo {
pub fn read() -> Self { pub fn read() -> Self {
Self::read_sorted(SortMode::default()) Self::read_sorted(SortMode::default())
@@ -386,6 +386,67 @@ pub fn render_system_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
if any_pl { "PL ".set_style(theme::POWER_LIMIT_FLAG) } else { "PL ".set_style(theme::VALUE_OFF) }, if any_pl { "PL ".set_style(theme::POWER_LIMIT_FLAG) } else { "PL ".set_style(theme::VALUE_OFF) },
])); ]));
{
let s = &app.sched_stats;
let mut parts: Vec<ratatui::text::Span> = Vec::new();
parts.push("Sched: ".set_style(theme::LABEL));
let mut wrote = false;
if let Some(v) = s.context_switches {
parts.push(format!("switches={v} ").set_style(theme::VALUE));
wrote = true;
}
if let Some(v) = s.contexts_created {
parts.push(format!("created={v} ").set_style(theme::VALUE));
wrote = true;
}
if let Some(v) = s.contexts_running {
parts.push(format!("running={v} ").set_style(theme::VALUE));
wrote = true;
}
if let Some(v) = s.contexts_blocked {
parts.push(format!("blocked={v} ").set_style(theme::VALUE));
wrote = true;
}
if let Some(v) = s.total_irqs {
parts.push(format!("IRQs={v} ").set_style(theme::VALUE));
wrote = true;
}
if !s.per_cpu_steals.is_empty() {
let total_steals: u64 = s.per_cpu_steals.iter().sum();
parts.push(format!("steals={total_steals} ").set_style(theme::VALUE));
wrote = true;
}
if !s.per_cpu_queue_depth.is_empty() {
let total_qd: u64 = s.per_cpu_queue_depth.iter().sum();
let avg_qd = total_qd / s.per_cpu_queue_depth.len() as u64;
parts.push(format!("avg_qd={avg_qd}").set_style(theme::VALUE));
wrote = true;
}
if wrote {
lines.push(Line::from(parts));
}
}
if app.pkg_power_w.is_some() || app.pp0_power_w.is_some()
|| app.pp1_power_w.is_some() || app.dram_power_w.is_some()
{
let mut parts: Vec<ratatui::text::Span> = Vec::new();
parts.push("Power: ".set_style(theme::LABEL));
if let Some(w) = app.pkg_power_w {
parts.push(format!("Pkg {w:.1}W ").set_style(theme::VALUE));
}
if let Some(w) = app.pp0_power_w {
parts.push(format!("Core {w:.1}W ").set_style(theme::VALUE));
}
if let Some(w) = app.pp1_power_w {
parts.push(format!("Uncore {w:.1}W ").set_style(theme::VALUE));
}
if let Some(w) = app.dram_power_w {
parts.push(format!("DRAM {w:.1}W").set_style(theme::VALUE));
}
lines.push(Line::from(parts));
}
// OS identity (matches cpu-x System tab) // OS identity (matches cpu-x System tab)
if app.os_info.available { if app.os_info.available {
lines.push(Line::from(vec![ lines.push(Line::from(vec![
@@ -435,8 +496,26 @@ pub fn render_system_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
} }
} }
let cs = &app.collector_stats;
if cs.thread_count > 1 {
lines.push(Line::from(vec![
"Collector: ".set_style(theme::LABEL),
format!("{} threads", cs.thread_count).set_style(theme::VALUE),
" pinned: ".set_style(theme::LABEL),
format!("{}/{}", cs.pinned_count, cs.barrier_size).set_style(theme::VALUE),
" barrier: ".set_style(theme::LABEL),
format!("{}", cs.barrier_size).set_style(theme::VALUE),
" last: ".set_style(theme::LABEL),
format!("{:.2} ms", cs.elapsed_us as f64 / 1000.0).set_style(theme::VALUE),
]));
} else {
lines.push(Line::from(vec![
"Collector: ".set_style(theme::LABEL),
"single-threaded".set_style(theme::VALUE_OFF),
]));
}
lines.push(Line::from(vec![ lines.push(Line::from(vec![
"Benchmark: ".set_style(theme::LABEL),
if app.bench_line.is_empty() { "(idle)".set_style(theme::VALUE_OFF) } else { app.bench_line.as_str().set_style(theme::VALUE) }, if app.bench_line.is_empty() { "(idle)".set_style(theme::VALUE_OFF) } else { app.bench_line.as_str().set_style(theme::VALUE) },
])); ]));
Paragraph::new(lines) Paragraph::new(lines)
@@ -946,7 +1025,7 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
_ => "RSS", _ => "RSS",
}; };
let header_str = format!( let header_str = format!(
" PID STATE PRIO NI THR CPU% IO RATE {:<11} T-IO T-IO/s IO-RATE CPU% RSS AFF COMM", " PID STATE SCHED PRIO NI THR CPU% IO RATE {:<11} T-IO T-IO/s IO-RATE CPU% RSS AFF COMM",
mem_header mem_header
); );
lines.push(Line::from(vec![ lines.push(Line::from(vec![
@@ -1058,10 +1137,11 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
theme::VALUE theme::VALUE
}; };
lines.push(Line::from(format!( lines.push(Line::from(format!(
" {}{:<7} {} {:<4} {:<3} {:<3} {:<6} {:<11} {:<11} {:<11} {:<11} {:<11} {:<12} {:<6} {:<5} {:<3} {}", " {}{:<7} {} {:<5} {:<4} {:<3} {:<3} {:<6} {:<11} {:<11} {:<11} {:<11} {:<11} {:<12} {:<6} {:<5} {:<3} {}",
prefix, prefix,
p.pid, p.pid,
p.state, p.state,
p.sched_policy,
p.priority, p.priority,
p.nice, p.nice,
p.num_threads, p.num_threads,
@@ -1405,6 +1485,7 @@ pub fn render_cpu_table<'a>(
focused: bool, focused: bool,
pkg_power_w: Option<f64>, pkg_power_w: Option<f64>,
rapl_status: &'a str, rapl_status: &'a str,
per_cpu_irqs: &'a [u64],
) -> Table<'a> { ) -> Table<'a> {
let header = Row::new(vec![ let header = Row::new(vec![
"CPU".set_style(theme::LABEL), "CPU".set_style(theme::LABEL),
@@ -1415,6 +1496,7 @@ pub fn render_cpu_table<'a>(
"State".set_style(theme::LABEL), "State".set_style(theme::LABEL),
"Flags".set_style(theme::LABEL), "Flags".set_style(theme::LABEL),
"Load % (30s)".set_style(theme::LABEL), "Load % (30s)".set_style(theme::LABEL),
"IRQs".set_style(theme::LABEL),
]) ])
.height(1); .height(1);
let rows: Vec<Row> = cpus let rows: Vec<Row> = cpus
@@ -1483,6 +1565,15 @@ pub fn render_cpu_table<'a>(
spark_text.set_style(Style::new().fg(lcolor)), spark_text.set_style(Style::new().fg(lcolor)),
format!(" {load_label}").set_style(Style::new().fg(lcolor).bold()), format!(" {load_label}").set_style(Style::new().fg(lcolor).bold()),
])), ])),
{
let irq_val = per_cpu_irqs.get(cpu.id as usize).copied();
Cell::from(
irq_val
.map(|v| format!("{v}"))
.unwrap_or_else(|| "".into())
.set_style(if irq_val.is_some() { theme::VALUE } else { theme::VALUE_OFF }),
)
},
]) ])
}) })
.collect(); .collect();
@@ -1511,6 +1602,32 @@ pub fn render_cpu_table<'a>(
"".set_style(s), "".set_style(s),
"".set_style(s), "".set_style(s),
"".set_style(s), "".set_style(s),
"".set_style(s),
]));
}
if let Some(ref hwp) = cpu.hwp {
let s = sub_style;
rows.push(Row::new(vec![
" HWP caps".set_style(s),
format!("eff={} guar={} max={}", hwp.efficient_perf, hwp.guaranteed_perf, hwp.max_perf).set_style(s),
"".set_style(s),
"".set_style(s),
"".set_style(s),
format!("{}", if hwp.enabled { "ON" } else { "OFF" }).set_style(if hwp.enabled { Style::new().green() } else { theme::VALUE_OFF }),
"".set_style(s),
"".set_style(s),
"".set_style(s),
]));
rows.push(Row::new(vec![
" HWP req".set_style(s),
format!("min={} max={} des={} EPP={} ({})", hwp.min_request, hwp.max_request, hwp.desired_perf, hwp.epp, hwp.epp_label()).set_style(s),
"".set_style(s),
"".set_style(s),
"".set_style(s),
"".set_style(s),
"".set_style(s),
"".set_style(s),
"".set_style(s),
])); ]));
} }
} }
@@ -1526,6 +1643,7 @@ pub fn render_cpu_table<'a>(
Constraint::Length(8), Constraint::Length(8),
Constraint::Length(6), Constraint::Length(6),
Constraint::Length(SPARK_WIDTH as u16 + 6), Constraint::Length(SPARK_WIDTH as u16 + 6),
Constraint::Length(8),
], ],
) )
.header(header) .header(header)
@@ -1671,7 +1789,7 @@ pub fn snapshot(app: &App, width: u16, height: u16) -> String {
); );
f.render_widget(render_header(app, true), header_area); f.render_widget(render_header(app, true), header_area);
f.render_stateful_widget( f.render_stateful_widget(
render_cpu_table(&app.cpus, app.expanded_cpu, true, app.pkg_power_w, &app.rapl_status), render_cpu_table(&app.cpus, app.expanded_cpu, true, app.pkg_power_w, &app.rapl_status, &app.sched_stats.per_cpu_irqs),
table_area, table_area,
&mut state, &mut state,
); );
@@ -0,0 +1,230 @@
//! Scheduler statistics from `/scheme/sys/stat` (Redox) or
//! `/proc/stat` (Linux).
//!
//! Redox's sys scheme exposes a richer set of scheduler counters than
//! Linux: per-CPU IRQ counts, context-switch totals, and scheduler
//! context state (created / running / blocked). On Linux, only the
//! `ctxt` (context switches) and `intr` (total interrupts) lines are
//! available from `/proc/stat`; per-CPU IRQ distribution lives in
//! `/proc/interrupts` and is not read here (kept cheap — one file
//! read).
//!
//! All fields degrade to `None` / empty when the data source is absent
//! or unparseable, so the TUI never panics on a missing scheme.
use std::fs;
/// Scheduler and IRQ statistics. Fields are `Option` because the
/// underlying data source may not provide all of them (Linux vs
/// Redox). `Default` gives all-`None` / empty, suitable as a
/// pre-probe placeholder.
#[derive(Clone, Debug, Default)]
pub struct SchedStats {
pub context_switches: Option<u64>,
pub contexts_created: Option<u64>,
pub contexts_running: Option<u64>,
pub contexts_blocked: Option<u64>,
pub per_cpu_irqs: Vec<u64>,
pub total_irqs: Option<u64>,
pub per_cpu_switches: Vec<u64>,
pub per_cpu_steals: Vec<u64>,
pub per_cpu_queue_depth: Vec<u64>,
}
impl SchedStats {
/// Read scheduler stats from the first available data source.
///
/// Tries `/scheme/sys/stat` (Redox) first, then falls back to
/// `/proc/stat` (Linux). Returns a `SchedStats` with `None` /
/// empty fields for anything the source does not provide.
pub fn read() -> Self {
let mut stats = Self::default();
if let Ok(data) = fs::read_to_string("/scheme/sys/stat") {
stats = Self::parse_redox(&data);
} else if let Ok(data) = fs::read_to_string("/proc/stat") {
stats = Self::parse_linux(&data);
}
if let Ok(data) = fs::read_to_string("/scheme/sys/sched") {
Self::merge_sched_detail(&mut stats, &data);
}
stats
}
/// Parse the Redox `/scheme/sys/stat` format:
/// ```text
/// cpu <u> <n> <k> <i> <irq>
/// cpu0 ...
/// IRQs <total> <cpu0_irqs> <cpu1_irqs> ...
/// boot_time: <secs>
/// context_switches: <num>
/// contexts_created: <num>
/// contexts_running: <num>
/// contexts_blocked: <num>
/// ```
fn parse_redox(data: &str) -> Self {
let mut stats = Self::default();
for line in data.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("IRQs ") {
// "IRQs <total> <cpu0> <cpu1> ..."
let nums: Vec<u64> = rest
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
// First number is the total; remaining are per-CPU.
if let Some(&total) = nums.first() {
stats.total_irqs = Some(total);
stats.per_cpu_irqs = nums[1..].to_vec();
}
} else if let Some(rest) = trimmed.strip_prefix("context_switches:") {
stats.context_switches = rest.trim().parse().ok();
} else if let Some(rest) = trimmed.strip_prefix("contexts_created:") {
stats.contexts_created = rest.trim().parse().ok();
} else if let Some(rest) = trimmed.strip_prefix("contexts_running:") {
stats.contexts_running = rest.trim().parse().ok();
} else if let Some(rest) = trimmed.strip_prefix("contexts_blocked:") {
stats.contexts_blocked = rest.trim().parse().ok();
}
}
stats
}
/// Parse the Linux `/proc/stat` format. Only `ctxt` (context
/// switches) and `intr` (total interrupts) are available.
/// Per-CPU IRQ distribution is in `/proc/interrupts` and is not
/// read here.
///
/// ```text
/// cpu 1000 200 3000 50000 ...
/// cpu0 ...
/// intr 200 50 50 ... (first number = total)
/// ctxt 5000
/// btime 1000
/// ```
fn parse_linux(data: &str) -> Self {
let mut stats = Self::default();
for line in data.lines() {
let trimmed = line.trim();
// "ctxt <number>" — context switches since boot.
if let Some(rest) = trimmed.strip_prefix("ctxt ") {
stats.context_switches = rest.trim().parse().ok();
}
// "intr <total> <irq0> <irq1> ..." — first field is total.
if let Some(rest) = trimmed.strip_prefix("intr ") {
if let Some(total_str) = rest.split_whitespace().next() {
stats.total_irqs = total_str.parse().ok();
}
}
}
stats
}
fn merge_sched_detail(stats: &mut SchedStats, data: &str) {
for line in data.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("cpu") {
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() < 7 {
continue;
}
let switches: u64 = parts[2].parse().unwrap_or(0);
let steals: u64 = parts[4].parse().unwrap_or(0);
let qd: u64 = parts[6].parse().unwrap_or(0);
stats.per_cpu_switches.push(switches);
stats.per_cpu_steals.push(steals);
stats.per_cpu_queue_depth.push(qd);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_redox_all_fields() {
let data = "\
cpu 1000 200 3000 50000 100
cpu0 500 100 1500 25000 50
cpu1 500 100 1500 25000 50
IRQs 200 100 100
boot_time: 1000
context_switches: 5000
contexts_created: 100
contexts_running: 2
contexts_blocked: 0
";
let stats = SchedStats::parse_redox(data);
assert_eq!(stats.context_switches, Some(5000));
assert_eq!(stats.contexts_created, Some(100));
assert_eq!(stats.contexts_running, Some(2));
assert_eq!(stats.contexts_blocked, Some(0));
assert_eq!(stats.total_irqs, Some(200));
assert_eq!(stats.per_cpu_irqs, vec![100, 100]);
}
#[test]
fn parse_linux_context_switches_and_intr() {
let data = "\
cpu 1000 200 3000 50000 100 0 0 0 0 0
cpu0 500 100 1500 25000 50 0 0 0 0 0
intr 200 50 50 100
ctxt 5000
btime 1000
";
let stats = SchedStats::parse_linux(data);
assert_eq!(stats.context_switches, Some(5000));
assert_eq!(stats.total_irqs, Some(200));
// Per-CPU IRQs are NOT available from /proc/stat on Linux.
assert!(stats.per_cpu_irqs.is_empty());
// Redox-specific fields are not available.
assert_eq!(stats.contexts_created, None);
assert_eq!(stats.contexts_running, None);
assert_eq!(stats.contexts_blocked, None);
}
#[test]
fn default_is_all_none() {
let stats = SchedStats::default();
assert_eq!(stats.context_switches, None);
assert_eq!(stats.total_irqs, None);
assert!(stats.per_cpu_irqs.is_empty());
}
#[test]
fn parse_redox_missing_context_fields() {
// Only the IRQs line — no context_* lines.
let data = "IRQs 42 10 20 12\n";
let stats = SchedStats::parse_redox(data);
assert_eq!(stats.total_irqs, Some(42));
assert_eq!(stats.per_cpu_irqs, vec![10, 20, 12]);
assert_eq!(stats.context_switches, None);
}
#[test]
fn parse_redox_empty_irqs_line() {
let data = "cpu 100 0 0 100 0\ncontext_switches: 99\n";
let stats = SchedStats::parse_redox(data);
assert_eq!(stats.context_switches, Some(99));
assert_eq!(stats.total_irqs, None);
assert!(stats.per_cpu_irqs.is_empty());
}
#[test]
fn merge_sched_detail_parses_per_cpu() {
let mut stats = SchedStats::default();
let data = "\
cpu0 switches 1000 steals 5 queue_depth 3
cpu1 switches 2000 steals 2 queue_depth 1
total switches 3000 steals 7
";
SchedStats::merge_sched_detail(&mut stats, data);
assert_eq!(stats.per_cpu_switches, vec![1000, 2000]);
assert_eq!(stats.per_cpu_steals, vec![5, 2]);
assert_eq!(stats.per_cpu_queue_depth, vec![3, 1]);
}
}