fix: column index desync after reorder
This commit is contained in:
@@ -19,6 +19,7 @@ qml_module(ZShell-services
|
||||
diskinfo.hpp diskinfo.cpp
|
||||
storage.hpp storage.cpp
|
||||
processes.hpp processes.cpp
|
||||
networkusage.hpp networkusage.cpp
|
||||
lyriccandidate.hpp lyriccandidate.cpp
|
||||
lyrics.hpp lyrics.cpp
|
||||
LIBRARIES
|
||||
@@ -30,6 +31,11 @@ qml_module(ZShell-services
|
||||
Sensors::Sensors
|
||||
ZShell-config
|
||||
ZShell-settings
|
||||
ZShell-internal
|
||||
ZShell
|
||||
zshell-util
|
||||
DEPENDENCIES
|
||||
ZShell
|
||||
ZShell.Config
|
||||
ZShell.Internal
|
||||
)
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#include "networkusage.hpp"
|
||||
|
||||
#include <qbytearrayview.h>
|
||||
#include <qfile.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <system_error>
|
||||
|
||||
#include "config/rootnodes.hpp"
|
||||
#include "config/dashboard.hpp"
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
using Qt::StringLiterals::operator""_s;
|
||||
|
||||
NetworkUsage::NetworkUsage(QObject* parent)
|
||||
: TickingService(parent)
|
||||
, m_downloadBuffer(new internal::CircularBuffer(this))
|
||||
, m_uploadBuffer(new internal::CircularBuffer(this)) {
|
||||
m_downloadBuffer->setSmoothingRadius(2);
|
||||
m_uploadBuffer->setSmoothingRadius(2);
|
||||
|
||||
auto* resources =
|
||||
config::ConfigSingleton::instance()->dashboard()->performance();
|
||||
setHistoryLength(resources->showVram() ? 70 : 30);
|
||||
QObject::connect(
|
||||
resources,
|
||||
&ZShell::config::Performance::showVramChanged,
|
||||
this,
|
||||
[this, resources] {
|
||||
setHistoryLength(resources->showVram() ? 70 : 30);
|
||||
});
|
||||
}
|
||||
|
||||
qreal NetworkUsage::downloadSpeed() const {
|
||||
return m_downloadSpeed;
|
||||
}
|
||||
|
||||
qreal NetworkUsage::uploadSpeed() const {
|
||||
return m_uploadSpeed;
|
||||
}
|
||||
|
||||
qreal NetworkUsage::downloadTotal() const {
|
||||
return m_downloadTotal;
|
||||
}
|
||||
|
||||
qreal NetworkUsage::uploadTotal() const {
|
||||
return m_uploadTotal;
|
||||
}
|
||||
|
||||
int NetworkUsage::historyLength() const {
|
||||
return m_historyLength;
|
||||
}
|
||||
|
||||
void NetworkUsage::setHistoryLength(int length) {
|
||||
if (length <= 0 || length == m_historyLength) {
|
||||
return;
|
||||
}
|
||||
m_historyLength = length;
|
||||
m_downloadBuffer->setCapacity(m_historyLength + 1);
|
||||
m_uploadBuffer->setCapacity(m_historyLength + 1);
|
||||
Q_EMIT historyLengthChanged();
|
||||
}
|
||||
|
||||
internal::CircularBuffer* NetworkUsage::downloadBuffer() const {
|
||||
return m_downloadBuffer;
|
||||
}
|
||||
|
||||
internal::CircularBuffer* NetworkUsage::uploadBuffer() const {
|
||||
return m_uploadBuffer;
|
||||
}
|
||||
|
||||
void NetworkUsage::tick() {
|
||||
QFile f(u"/proc/net/dev"_s);
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return;
|
||||
}
|
||||
// Skip headers
|
||||
f.readLine();
|
||||
f.readLine();
|
||||
|
||||
quint64 totalRx = 0;
|
||||
quint64 totalTx = 0;
|
||||
|
||||
while (!f.atEnd()) {
|
||||
const QByteArray line = f.readLine();
|
||||
const qsizetype splitIdx = line.indexOf(':');
|
||||
if (splitIdx == -1) {
|
||||
continue;
|
||||
}
|
||||
const QByteArray iface = line.left(splitIdx).trimmed();
|
||||
if (iface == QByteArrayView("lo")) {
|
||||
continue; // Skip loopback interface
|
||||
}
|
||||
|
||||
// Parse every counter through tx bytes to validate the row
|
||||
const char* pos = line.constData() + splitIdx + 1;
|
||||
const char* const end = line.constData() + line.size();
|
||||
|
||||
std::array<unsigned long long, 9> fields{};
|
||||
bool valid = true;
|
||||
for (unsigned long long& field : fields) {
|
||||
while (pos < end && (*pos == ' ' || *pos == '\t'))
|
||||
++pos;
|
||||
|
||||
const auto [next, ec] = std::from_chars(pos, end, field);
|
||||
if (ec != std::errc{}) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
pos = next;
|
||||
}
|
||||
|
||||
if (!valid) continue;
|
||||
|
||||
totalRx += static_cast<quint64>(fields[0]);
|
||||
totalTx += static_cast<quint64>(fields[8]);
|
||||
}
|
||||
f.close();
|
||||
|
||||
if (!m_initialized) {
|
||||
m_prevRx = totalRx;
|
||||
m_prevTx = totalTx;
|
||||
m_timer.start();
|
||||
m_initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const qreal elapsed = static_cast<qreal>(m_timer.restart()) / 1000.0;
|
||||
const quint64 rxDelta = totalRx >= m_prevRx ? totalRx - m_prevRx : 0;
|
||||
const quint64 txDelta = totalTx >= m_prevTx ? totalTx - m_prevTx : 0;
|
||||
|
||||
m_downloadTotal += static_cast<qreal>(rxDelta);
|
||||
m_uploadTotal += static_cast<qreal>(txDelta);
|
||||
|
||||
if (elapsed > 0.0) {
|
||||
// Calculate speeds
|
||||
m_downloadSpeed = static_cast<qreal>(rxDelta) / elapsed;
|
||||
m_uploadSpeed = static_cast<qreal>(txDelta) / elapsed;
|
||||
|
||||
m_downloadBuffer->push(m_downloadSpeed);
|
||||
m_uploadBuffer->push(m_uploadSpeed);
|
||||
}
|
||||
|
||||
m_prevRx = totalRx;
|
||||
m_prevTx = totalTx;
|
||||
|
||||
emit changed();
|
||||
}
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <qelapsedtimer.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
#include "internal/circularbuffer.hpp"
|
||||
#include "tickingservice.hpp"
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
class NetworkUsage : public TickingService {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
Q_PROPERTY(qreal downloadSpeed READ downloadSpeed NOTIFY changed)
|
||||
Q_PROPERTY(qreal uploadSpeed READ uploadSpeed NOTIFY changed)
|
||||
Q_PROPERTY(qreal downloadTotal READ downloadTotal NOTIFY changed)
|
||||
Q_PROPERTY(qreal uploadTotal READ uploadTotal NOTIFY changed)
|
||||
Q_PROPERTY(int historyLength READ historyLength NOTIFY historyLengthChanged)
|
||||
|
||||
Q_PROPERTY(
|
||||
ZShell::internal::CircularBuffer* downloadBuffer READ downloadBuffer
|
||||
CONSTANT)
|
||||
Q_PROPERTY(
|
||||
ZShell::internal::CircularBuffer* uploadBuffer READ uploadBuffer
|
||||
CONSTANT)
|
||||
|
||||
public:
|
||||
explicit NetworkUsage(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] qreal downloadSpeed() const;
|
||||
[[nodiscard]] qreal uploadSpeed() const;
|
||||
[[nodiscard]] qreal downloadTotal() const;
|
||||
[[nodiscard]] qreal uploadTotal() const;
|
||||
[[nodiscard]] int historyLength() const;
|
||||
|
||||
[[nodiscard]] internal::CircularBuffer* downloadBuffer() const;
|
||||
[[nodiscard]] internal::CircularBuffer* uploadBuffer() const;
|
||||
|
||||
signals:
|
||||
void changed();
|
||||
void historyLengthChanged();
|
||||
|
||||
protected:
|
||||
void tick() override;
|
||||
|
||||
private:
|
||||
void setHistoryLength(int length);
|
||||
|
||||
qreal m_downloadSpeed = 0.0;
|
||||
qreal m_uploadSpeed = 0.0;
|
||||
qreal m_downloadTotal = 0.0;
|
||||
qreal m_uploadTotal = 0.0;
|
||||
int m_historyLength = 30;
|
||||
|
||||
internal::CircularBuffer* m_downloadBuffer = nullptr;
|
||||
internal::CircularBuffer* m_uploadBuffer = nullptr;
|
||||
|
||||
quint64 m_prevRx = 0;
|
||||
quint64 m_prevTx = 0;
|
||||
bool m_initialized = false;
|
||||
QElapsedTimer m_timer;
|
||||
};
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -2,8 +2,13 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <pwd.h>
|
||||
#include <signal.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <qbytearray.h>
|
||||
@@ -12,6 +17,7 @@
|
||||
#include <qfile.h>
|
||||
#include <qfuturewatcher.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qthreadpool.h>
|
||||
#include <qregularexpression.h>
|
||||
#include <qset.h>
|
||||
#include <QtConcurrent/qtconcurrentrun.h>
|
||||
@@ -53,6 +59,82 @@ template <typename T> int cmpVals(const T& a, const T& b) {
|
||||
return (a < b) - (a > b);
|
||||
}
|
||||
|
||||
// glibc hides getdents64 behind __USE_MISC; declare directly (no _GNU_SOURCE
|
||||
// in this project).
|
||||
extern "C" {
|
||||
struct ProcDirEnt64 {
|
||||
unsigned long d_ino;
|
||||
off_t d_off;
|
||||
unsigned short d_reclen;
|
||||
unsigned char d_type;
|
||||
char d_name[1];
|
||||
};
|
||||
|
||||
ssize_t getdents64(int fd, void* dirp, size_t count);
|
||||
}
|
||||
|
||||
// Raw open/read/close instead of QFile: QFSFileEngine issues an extra
|
||||
// statx per open, and the per-file object churn adds up across ~500
|
||||
// processes per refresh.
|
||||
QByteArray readProcFile(const char* path) {
|
||||
const int fd = ::open(path, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
return {};
|
||||
}
|
||||
QByteArray data;
|
||||
char buf[4096];
|
||||
for (;;) {
|
||||
const ssize_t n = ::read(fd, buf, sizeof(buf));
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (n == 0) {
|
||||
break;
|
||||
}
|
||||
data.append(buf, static_cast<qsizetype>(n));
|
||||
}
|
||||
::close(fd);
|
||||
return data;
|
||||
}
|
||||
|
||||
void procPath(char* out, size_t size, pid_t pid, const char* what) {
|
||||
::snprintf(out, size, "/proc/%d/%s", static_cast<int>(pid), what);
|
||||
}
|
||||
|
||||
QVector<pid_t> listProcPids() {
|
||||
QVector<pid_t> pids;
|
||||
const int fd = ::openat(AT_FDCWD, "/proc", O_RDONLY | O_DIRECTORY);
|
||||
if (fd < 0) {
|
||||
return pids;
|
||||
}
|
||||
|
||||
std::array<char, 256 * 1024> buf;
|
||||
for (;;) {
|
||||
const ssize_t n = ::getdents64(
|
||||
fd, reinterpret_cast<ProcDirEnt64*>(buf.data()), buf.size());
|
||||
if (n <= 0) {
|
||||
break;
|
||||
}
|
||||
for (ssize_t off = 0; off < n;) {
|
||||
const auto* d =
|
||||
reinterpret_cast<const ProcDirEnt64*>(buf.data() + off);
|
||||
if (d->d_type == DT_DIR) {
|
||||
bool ok = false;
|
||||
const long v = QByteArray(d->d_name).toLong(&ok);
|
||||
if (ok && v > 0) {
|
||||
pids.push_back(static_cast<pid_t>(v));
|
||||
}
|
||||
}
|
||||
off += static_cast<ssize_t>(d->d_reclen);
|
||||
}
|
||||
}
|
||||
::close(fd);
|
||||
return pids;
|
||||
}
|
||||
|
||||
|
||||
struct FuzzyText {
|
||||
QString chars;
|
||||
@@ -460,19 +542,24 @@ void ProcessModel::setDesktopEntryIcons(
|
||||
}
|
||||
|
||||
QString ProcessModel::resolveProcessExe(pid_t pid) {
|
||||
const QString path = QStringLiteral("/proc/%1/exe").arg(pid);
|
||||
const QString target = QFile::symLinkTarget(path);
|
||||
|
||||
return target;
|
||||
char path[40];
|
||||
procPath(path, sizeof(path), pid, "exe");
|
||||
char buf[4096];
|
||||
const ssize_t n = ::readlink(path, buf, sizeof(buf) - 1);
|
||||
if (n < 0) {
|
||||
return {};
|
||||
}
|
||||
buf[n] = '\0';
|
||||
return QString::fromLocal8Bit(buf, static_cast<int>(n));
|
||||
}
|
||||
|
||||
std::optional<ProcessModel::RawStat> ProcessModel::readStat(pid_t pid) {
|
||||
QFile f(QStringLiteral("/proc/%1/stat").arg(pid));
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
char path[40];
|
||||
procPath(path, sizeof(path), pid, "stat");
|
||||
const QByteArray data = readProcFile(path);
|
||||
if (data.isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const QByteArray data = f.readAll();
|
||||
f.close();
|
||||
|
||||
const qsizetype openParen = data.indexOf('(');
|
||||
const qsizetype closeParen = data.lastIndexOf(')');
|
||||
@@ -500,12 +587,9 @@ std::optional<ProcessModel::RawStat> ProcessModel::readStat(pid_t pid) {
|
||||
}
|
||||
|
||||
QString ProcessModel::readCommandLine(pid_t pid, const QString& fallbackComm) {
|
||||
QFile f(QStringLiteral("/proc/%1/cmdline").arg(pid));
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
return QStringLiteral("[%1]").arg(fallbackComm);
|
||||
}
|
||||
QByteArray data = f.readAll();
|
||||
f.close();
|
||||
char path[40];
|
||||
procPath(path, sizeof(path), pid, "cmdline");
|
||||
QByteArray data = readProcFile(path);
|
||||
|
||||
if (data.isEmpty()) {
|
||||
return QStringLiteral("[%1]").arg(fallbackComm);
|
||||
@@ -523,12 +607,9 @@ QString ProcessModel::readCommandLine(pid_t pid, const QString& fallbackComm) {
|
||||
}
|
||||
|
||||
ProcessModel::StatusInfo ProcessModel::readStatus(pid_t pid) {
|
||||
QFile f(QStringLiteral("/proc/%1/status").arg(pid));
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
return {};
|
||||
}
|
||||
const QByteArray data = f.readAll();
|
||||
f.close();
|
||||
char path[40];
|
||||
procPath(path, sizeof(path), pid, "status");
|
||||
const QByteArray data = readProcFile(path);
|
||||
|
||||
StatusInfo out;
|
||||
qsizetype pos = 0;
|
||||
@@ -554,12 +635,9 @@ ProcessModel::StatusInfo ProcessModel::readStatus(pid_t pid) {
|
||||
}
|
||||
|
||||
ProcessModel::IoInfo ProcessModel::readIo(pid_t pid) {
|
||||
QFile f(QStringLiteral("/proc/%1/io").arg(pid));
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
return {};
|
||||
}
|
||||
const QByteArray data = f.readAll();
|
||||
f.close();
|
||||
char path[40];
|
||||
procPath(path, sizeof(path), pid, "io");
|
||||
const QByteArray data = readProcFile(path);
|
||||
|
||||
IoInfo out;
|
||||
qsizetype pos = 0;
|
||||
@@ -606,12 +684,10 @@ QString ProcessModel::resolveUsername(uid_t uid, QHash<uid_t, QString>& cache) {
|
||||
}
|
||||
|
||||
std::optional<double> ProcessModel::readUptimeSeconds() {
|
||||
QFile f(QStringLiteral("/proc/uptime"));
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
const QByteArray data = readProcFile("/proc/uptime");
|
||||
if (data.isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const QByteArray data = f.readAll();
|
||||
f.close();
|
||||
|
||||
const auto fields = splitWhitespace(QByteArrayView(data));
|
||||
if (fields.isEmpty()) {
|
||||
@@ -623,35 +699,17 @@ std::optional<double> ProcessModel::readUptimeSeconds() {
|
||||
return ok ? std::make_optional(uptime) : std::nullopt;
|
||||
}
|
||||
|
||||
ProcessModel::ScanResult ProcessModel::scanProcesses(
|
||||
double uptime,
|
||||
double deltaSecs,
|
||||
QHash<pid_t, int> prevPidToAll,
|
||||
QVector<ProcessEntry> prevAll,
|
||||
QHash<uid_t, QString> uidCache,
|
||||
long clockTicksPerSec,
|
||||
QHash<QString, QString> execPathToIcon,
|
||||
QHash<QString, QString> execNameToIcon,
|
||||
QStringList pathDirs,
|
||||
bool desktopEntryIconsChanged,
|
||||
quint64 iconVersion) {
|
||||
Q_UNUSED(
|
||||
pathDirs); // kept for future cgroup fallback; sanitizeExec already ran on GUI thread
|
||||
|
||||
QDir procDir(QStringLiteral("/proc"));
|
||||
const auto entries =
|
||||
procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::NoSort);
|
||||
|
||||
QVector<ProcessEntry> fresh;
|
||||
fresh.reserve(entries.size());
|
||||
|
||||
for (const QString& name : entries) {
|
||||
bool isPid = false;
|
||||
const pid_t pid = static_cast<pid_t>(name.toLong(&isPid));
|
||||
if (!isPid || pid <= 0) {
|
||||
continue;
|
||||
}
|
||||
void ProcessModel::scanChunk(
|
||||
const ScanContext& ctx,
|
||||
const QVector<pid_t>& pids,
|
||||
QVector<ProcessEntry>& out,
|
||||
QHash<uid_t, QString>& uidCache) {
|
||||
const QHash<pid_t, int>& prevPidToAll = *ctx.prevPidToAll;
|
||||
const QVector<ProcessEntry>& prevAll = *ctx.prevAll;
|
||||
const QHash<QString, QString>& execPathToIcon = *ctx.execPathToIcon;
|
||||
const QHash<QString, QString>& execNameToIcon = *ctx.execNameToIcon;
|
||||
|
||||
for (pid_t pid : pids) {
|
||||
const auto statOpt = readStat(pid);
|
||||
if (!statOpt) {
|
||||
continue;
|
||||
@@ -675,7 +733,7 @@ ProcessModel::ScanResult ProcessModel::scanProcesses(
|
||||
entry.commandLine = readCommandLine(pid, stat.comm);
|
||||
}
|
||||
|
||||
if (!samePidInstance || desktopEntryIconsChanged) {
|
||||
if (!samePidInstance || ctx.iconsChanged) {
|
||||
const QString exePath = resolveProcessExe(pid);
|
||||
|
||||
QString iconName;
|
||||
@@ -711,18 +769,23 @@ ProcessModel::ScanResult ProcessModel::scanProcesses(
|
||||
entry.uid = status.uid;
|
||||
entry.username = resolveUsername(status.uid, uidCache);
|
||||
|
||||
const IoInfo io = readIo(pid);
|
||||
// /proc/PID/io is only readable by root or the process owner; skip
|
||||
// the open when it is guaranteed to fail.
|
||||
IoInfo io;
|
||||
if (ctx.selfEuid == 0 || status.uid == ctx.selfEuid) {
|
||||
io = readIo(pid);
|
||||
}
|
||||
entry.ioReadBytes = io.readBytes;
|
||||
entry.ioWriteBytes = io.writeBytes;
|
||||
|
||||
entry.elapsedSecs = std::max(
|
||||
0.0,
|
||||
uptime - (static_cast<double>(stat.starttime) /
|
||||
static_cast<double>(clockTicksPerSec)));
|
||||
ctx.uptime - (static_cast<double>(stat.starttime) /
|
||||
static_cast<double>(ctx.clockTicksPerSec)));
|
||||
entry.lastUtime = stat.utime;
|
||||
entry.lastStime = stat.stime;
|
||||
|
||||
if (samePidInstance && deltaSecs > 0.0) {
|
||||
if (samePidInstance && ctx.deltaSecs > 0.0) {
|
||||
const ProcessEntry& prev = prevAll.at(prevIt.value());
|
||||
const quint64 prevTicks = prev.lastUtime + prev.lastStime;
|
||||
const quint64 nowTicks = stat.utime + stat.stime;
|
||||
@@ -730,13 +793,97 @@ ProcessModel::ScanResult ProcessModel::scanProcesses(
|
||||
nowTicks > prevTicks ? nowTicks - prevTicks : 0;
|
||||
entry.cpuPercent =
|
||||
(static_cast<double>(deltaTicks) /
|
||||
(deltaSecs * static_cast<double>(clockTicksPerSec))) *
|
||||
(ctx.deltaSecs *
|
||||
static_cast<double>(ctx.clockTicksPerSec))) *
|
||||
100.0;
|
||||
} else {
|
||||
entry.cpuPercent = 0.0;
|
||||
}
|
||||
|
||||
fresh.push_back(std::move(entry));
|
||||
out.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
|
||||
ProcessModel::ScanResult ProcessModel::scanProcesses(
|
||||
double uptime,
|
||||
double deltaSecs,
|
||||
QHash<pid_t, int> prevPidToAll,
|
||||
QVector<ProcessEntry> prevAll,
|
||||
QHash<uid_t, QString> uidCache,
|
||||
long clockTicksPerSec,
|
||||
QHash<QString, QString> execPathToIcon,
|
||||
QHash<QString, QString> execNameToIcon,
|
||||
QStringList pathDirs,
|
||||
bool desktopEntryIconsChanged,
|
||||
quint64 iconVersion) {
|
||||
Q_UNUSED(
|
||||
pathDirs); // kept for future cgroup fallback; sanitizeExec already ran on GUI thread
|
||||
|
||||
const QVector<pid_t> pids = listProcPids();
|
||||
|
||||
ScanContext ctx;
|
||||
ctx.uptime = uptime;
|
||||
ctx.deltaSecs = deltaSecs;
|
||||
ctx.prevPidToAll = &prevPidToAll;
|
||||
ctx.prevAll = &prevAll;
|
||||
ctx.clockTicksPerSec = clockTicksPerSec;
|
||||
ctx.execPathToIcon = &execPathToIcon;
|
||||
ctx.execNameToIcon = &execNameToIcon;
|
||||
ctx.iconsChanged = desktopEntryIconsChanged;
|
||||
ctx.selfEuid = ::geteuid();
|
||||
|
||||
QVector<ProcessEntry> fresh;
|
||||
fresh.reserve(pids.size());
|
||||
|
||||
// Split the pid list across worker threads; a single pass over /proc is
|
||||
// syscall-bound (open/read/close per file per process) and serializes
|
||||
// badly on large systems. Keep one pool thread free for this function,
|
||||
// which itself runs inside the global pool.
|
||||
int nThreads = qMin(16, QThreadPool::globalInstance()->maxThreadCount() - 1);
|
||||
if (pids.size() < 128 || nThreads < 2) {
|
||||
nThreads = 1;
|
||||
}
|
||||
|
||||
if (nThreads <= 1) {
|
||||
scanChunk(ctx, pids, fresh, uidCache);
|
||||
} else {
|
||||
const qsizetype per = (pids.size() + nThreads - 1) / nThreads;
|
||||
QVector<QVector<pid_t>> chunks(nThreads);
|
||||
QVector<QVector<ProcessEntry>> outs(nThreads);
|
||||
QVector<QHash<uid_t, QString>> uidParts(nThreads);
|
||||
QVector<QFuture<void>> futures;
|
||||
|
||||
for (int t = 0; t < nThreads; ++t) {
|
||||
const qsizetype a = static_cast<qsizetype>(t) * per;
|
||||
const qsizetype b = qMin<qsizetype>(a + per, pids.size());
|
||||
if (a >= b) {
|
||||
continue;
|
||||
}
|
||||
chunks[t] = pids.mid(a, b - a);
|
||||
futures.append(QtConcurrent::run(
|
||||
[&ctx, &chunks, &outs, &uidParts, t]() {
|
||||
scanChunk(ctx, chunks[t], outs[t], uidParts[t]);
|
||||
}));
|
||||
}
|
||||
for (QFuture<void>& f : futures) {
|
||||
f.waitForFinished();
|
||||
}
|
||||
|
||||
for (qsizetype t = 0; t < nThreads; ++t) {
|
||||
for (ProcessEntry& e : outs[t]) {
|
||||
fresh.push_back(std::move(e));
|
||||
}
|
||||
for (auto it = uidParts[t].constBegin();
|
||||
it != uidParts[t].constEnd();
|
||||
++it) {
|
||||
uidCache.insert(it.key(), it.value());
|
||||
}
|
||||
}
|
||||
std::sort(
|
||||
fresh.begin(), fresh.end(), [](const ProcessEntry& a,
|
||||
const ProcessEntry& b) {
|
||||
return a.pid < b.pid;
|
||||
});
|
||||
}
|
||||
|
||||
ScanResult result;
|
||||
@@ -810,6 +957,13 @@ void ProcessModel::onScanFinished() {
|
||||
for (int i = 0; i < mAll.size(); ++i) {
|
||||
mPidToAll.insert(mAll.at(i).pid, i);
|
||||
}
|
||||
for (auto it = mFuzzyCache.begin(); it != mFuzzyCache.end();) {
|
||||
if (mPidToAll.contains(it.key())) {
|
||||
++it;
|
||||
} else {
|
||||
it = mFuzzyCache.erase(it);
|
||||
}
|
||||
}
|
||||
for (auto it = result.newUidNames.constBegin();
|
||||
it != result.newUidNames.constEnd();
|
||||
++it) {
|
||||
@@ -846,11 +1000,35 @@ void ProcessModel::applyView() {
|
||||
|
||||
ProcessEntry entry = src;
|
||||
if (searching) {
|
||||
int score = fuzzyQueryScore(fuzzyPrepare(src.command), terms);
|
||||
if (!src.commandLine.isEmpty() && src.commandLine != src.command) {
|
||||
score = qMax(
|
||||
score,
|
||||
fuzzyQueryScore(fuzzyPrepare(src.commandLine), terms));
|
||||
// Normalizing (NFD) + lowering per process per tick is the
|
||||
// expensive part of search; commands are stable for the life of
|
||||
// a process instance (pid + starttime), so cache the prepared
|
||||
// text. comm can still change without an exec (prctl), so
|
||||
// re-prepare when the raw command differs.
|
||||
FuzzyCacheEntry& fe = mFuzzyCache[src.pid];
|
||||
if (fe.starttime != src.starttime || fe.command != src.command) {
|
||||
fe.starttime = src.starttime;
|
||||
fe.command = src.command;
|
||||
const FuzzyText prep = fuzzyPrepare(src.command);
|
||||
fe.commandChars = std::move(prep.chars);
|
||||
fe.commandClasses = std::move(prep.classes);
|
||||
fe.hasCommandLine =
|
||||
!src.commandLine.isEmpty() && src.commandLine != src.command;
|
||||
if (fe.hasCommandLine) {
|
||||
const FuzzyText prepCl = fuzzyPrepare(src.commandLine);
|
||||
fe.commandLineChars = std::move(prepCl.chars);
|
||||
fe.commandLineClasses = std::move(prepCl.classes);
|
||||
} else {
|
||||
fe.commandLineChars = QString();
|
||||
fe.commandLineClasses = {};
|
||||
}
|
||||
}
|
||||
const FuzzyText cmdFuzzy{fe.commandChars, fe.commandClasses};
|
||||
int score = fuzzyQueryScore(cmdFuzzy, terms);
|
||||
if (fe.hasCommandLine) {
|
||||
const FuzzyText clFuzzy{
|
||||
fe.commandLineChars, fe.commandLineClasses};
|
||||
score = qMax(score, fuzzyQueryScore(clFuzzy, terms));
|
||||
}
|
||||
if (score < 0) {
|
||||
continue;
|
||||
|
||||
@@ -75,6 +75,16 @@ class ProcessModel : public QAbstractListModel {
|
||||
void sortAscendingChanged();
|
||||
|
||||
private:
|
||||
struct FuzzyCacheEntry {
|
||||
quint64 starttime = 0;
|
||||
QString command;
|
||||
QString commandChars;
|
||||
QVector<quint8> commandClasses;
|
||||
QString commandLineChars;
|
||||
QVector<quint8> commandLineClasses;
|
||||
bool hasCommandLine = false;
|
||||
};
|
||||
|
||||
struct RawStat {
|
||||
pid_t pid = 0;
|
||||
pid_t ppid = 0;
|
||||
@@ -116,6 +126,18 @@ class ProcessModel : public QAbstractListModel {
|
||||
quint64 writeBytes = 0;
|
||||
};
|
||||
|
||||
struct ScanContext {
|
||||
double uptime = 0.0;
|
||||
double deltaSecs = -1.0;
|
||||
const QHash<pid_t, int>* prevPidToAll = nullptr;
|
||||
const QVector<ProcessEntry>* prevAll = nullptr;
|
||||
long clockTicksPerSec = 100;
|
||||
const QHash<QString, QString>* execPathToIcon = nullptr;
|
||||
const QHash<QString, QString>* execNameToIcon = nullptr;
|
||||
bool iconsChanged = false;
|
||||
uid_t selfEuid = 0;
|
||||
};
|
||||
|
||||
struct ScanResult {
|
||||
QVector<ProcessEntry> entries;
|
||||
QHash<uid_t, QString> newUidNames;
|
||||
@@ -130,6 +152,12 @@ class ProcessModel : public QAbstractListModel {
|
||||
static QString resolveUsername(uid_t uid, QHash<uid_t, QString>& cache);
|
||||
static QString resolveProcessExe(pid_t pid);
|
||||
|
||||
static void scanChunk(
|
||||
const ScanContext& ctx,
|
||||
const QVector<pid_t>& pids,
|
||||
QVector<ProcessEntry>& out,
|
||||
QHash<uid_t, QString>& uidCache);
|
||||
|
||||
static ScanResult scanProcesses(
|
||||
double uptime,
|
||||
double deltaSecs,
|
||||
@@ -153,6 +181,7 @@ class ProcessModel : public QAbstractListModel {
|
||||
|
||||
QVector<ProcessEntry> mAll;
|
||||
QHash<pid_t, int> mPidToAll;
|
||||
QHash<pid_t, FuzzyCacheEntry> mFuzzyCache;
|
||||
|
||||
QVector<ProcessEntry> mProcesses;
|
||||
QHash<uid_t, QString> mUidToName;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
#include "tickingservice.hpp"
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QTimer>
|
||||
|
||||
#include "config/dashboard.hpp"
|
||||
#include "config/rootnodes.hpp"
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
@@ -17,53 +13,13 @@ TickingService::TickingService(QObject* parent)
|
||||
updateTriggered();
|
||||
});
|
||||
|
||||
QString configPath =
|
||||
QDir::homePath() + QStringLiteral("/.config/zshell/config.json");
|
||||
|
||||
auto reloadConfig = [this, configPath]() {
|
||||
QFile file(configPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return;
|
||||
}
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
if (doc.isNull()) {
|
||||
return;
|
||||
}
|
||||
QJsonObject dashboard = doc.object().value("dashboard").toObject();
|
||||
if (dashboard.contains("resourceUpdateInterval")) {
|
||||
applyInterval(
|
||||
dashboard.value("resourceUpdateInterval").toInt(1000));
|
||||
}
|
||||
};
|
||||
|
||||
reloadConfig();
|
||||
|
||||
static auto* watcher = new QFileSystemWatcher();
|
||||
if (!watcher->files().contains(configPath)) {
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QFileSystemWatcher::fileChanged,
|
||||
this,
|
||||
[this, configPath]() {
|
||||
QTimer::singleShot(100, this, [this, configPath]() {
|
||||
QFile file(configPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return;
|
||||
}
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
if (doc.isNull()) {
|
||||
return;
|
||||
}
|
||||
QJsonObject dashboard =
|
||||
doc.object().value("dashboard").toObject();
|
||||
if (dashboard.contains("resourceUpdateInterval")) {
|
||||
applyInterval(dashboard.value("resourceUpdateInterval")
|
||||
.toInt(1000));
|
||||
}
|
||||
});
|
||||
});
|
||||
watcher->addPath(configPath);
|
||||
}
|
||||
auto* dash = ZShell::config::ConfigSingleton::instance()->dashboard();
|
||||
applyInterval(dash->resourceUpdateInterval());
|
||||
QObject::connect(
|
||||
dash,
|
||||
&ZShell::config::Dashboard::resourceUpdateIntervalChanged,
|
||||
this,
|
||||
[this, dash] { applyInterval(dash->resourceUpdateInterval()); });
|
||||
}
|
||||
|
||||
int TickingService::updateInterval() const {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "service.hpp"
|
||||
#include <qtimer.h>
|
||||
|
||||
#include "service.hpp"
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
class TickingService : public Service {
|
||||
|
||||
@@ -48,4 +48,20 @@ FormatResult UsageFmt::formatKib(qreal kib, qreal total) const {
|
||||
return {kib, total, "KiB"};
|
||||
}
|
||||
|
||||
FormatResult UsageFmt::formatBytes(qreal bytes) const {
|
||||
static constexpr std::array<const char*, 5> units = {
|
||||
"B", "KB", "MB", "GB", "TB"};
|
||||
|
||||
qreal value = finitePositive(bytes) ? bytes : 0.0;
|
||||
const qreal k = 1024.0;
|
||||
std::size_t i = 0;
|
||||
|
||||
while (value >= k && i < units.size() - 1) {
|
||||
value /= k;
|
||||
i++;
|
||||
}
|
||||
|
||||
return {value, 0.0, units[i]};
|
||||
}
|
||||
|
||||
} // namespace ZShell::services::usagefmt
|
||||
|
||||
@@ -31,6 +31,7 @@ class UsageFmt : public QObject {
|
||||
Q_INVOKABLE [[nodiscard]] FormatResult formatKib(qreal kib) const;
|
||||
Q_INVOKABLE [[nodiscard]] FormatResult formatKib(
|
||||
qreal kib, qreal total) const;
|
||||
Q_INVOKABLE [[nodiscard]] FormatResult formatBytes(qreal bytes) const;
|
||||
};
|
||||
|
||||
} // namespace ZShell::services::usagefmt
|
||||
|
||||
Reference in New Issue
Block a user