Something went wrong. Try again.
The agentic engineering control plane for the posthuman future
Something went wrong. Try again.
7.1 kB · 257 lines
C++
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258#include "FrecencyStore.h"
#include <QDateTime>#include <QDir>#include <QFile>#include <QFileInfo>#include <QJsonDocument>#include <QJsonObject>#include <QJsonValue>#include <QMutexLocker>#include <QSaveFile>
#include <algorithm>#include <limits>#include <utility>#include <vector>
namespace {
constexpr qint64 kDayMs = 24LL * 60LL * 60LL * 1000LL;constexpr int kMaxScore = std::numeric_limits<int>::max();
} // namespace
FrecencyStore::FrecencyStore(QString filePath) : m_path(std::move(filePath)) {}
FrecencyStore::~FrecencyStore(){ // An open recorded inside the last save interval still lands, so a quit // never costs the most recent work. const QMutexLocker locker(&m_mutex); if (m_dirty) { writeLocked(); }}
FrecencyStore& FrecencyStore::instance(){ static FrecencyStore store(defaultPath()); return store;}
QString FrecencyStore::defaultPath(){ QString fromEnv = qEnvironmentVariable("XENO_FRECENCY_FILE"); if (!fromEnv.isEmpty()) { return fromEnv; } // XDG Base Directory spec: configuration lives under $XDG_CONFIG_HOME, // defaulting to ~/.config. QStandardPaths resolves a different directory on // macOS, so the path is built here to keep one config directory everywhere. QString configDir = qEnvironmentVariable("XDG_CONFIG_HOME"); if (configDir.isEmpty()) { configDir = QDir::home().filePath(QStringLiteral(".config")); } return QDir(configDir).filePath(QStringLiteral("xenomorphic/frecency.json"));}
int FrecencyStore::weightForAge(qint64 ageMs){ if (ageMs < 4 * kDayMs) { return 100; } if (ageMs < 14 * kDayMs) { return 70; } if (ageMs < 31 * kDayMs) { return 50; } if (ageMs < 90 * kDayMs) { return 30; } return 20;}
void FrecencyStore::record(const QString& path){ if (path.isEmpty()) { return; } const qint64 now = QDateTime::currentMSecsSinceEpoch(); const QMutexLocker locker(&m_mutex); Entry& entry = m_entries[path]; entry.count += 1; entry.lastUsedMs = now; m_dirty = true; if (now - m_lastSaveMs >= kFrecencySaveIntervalMs) { pruneLocked(); writeLocked(); m_lastSaveMs = now; m_dirty = false; }}
int FrecencyStore::score(const QString& path) const{ const QMutexLocker locker(&m_mutex); const auto it = m_entries.constFind(path); if (it == m_entries.constEnd()) { return 0; } const qint64 age = QDateTime::currentMSecsSinceEpoch() - it->lastUsedMs; if (age > kFrecencyMaxAgeMs) { return 0; } const qint64 product = qint64(it->count) * weightForAge(age); return product > kMaxScore ? kMaxScore : int(product);}
int FrecencyStore::count(const QString& path) const{ const QMutexLocker locker(&m_mutex); return m_entries.value(path).count;}
qint64 FrecencyStore::lastUsedMs(const QString& path) const{ const QMutexLocker locker(&m_mutex); return m_entries.value(path).lastUsedMs;}
QHash<QString, int> FrecencyStore::snapshot() const{ const qint64 now = QDateTime::currentMSecsSinceEpoch(); const QMutexLocker locker(&m_mutex); QHash<QString, int> scores; scores.reserve(m_entries.size()); for (auto it = m_entries.constBegin(); it != m_entries.constEnd(); ++it) { const qint64 age = now - it->lastUsedMs; if (age > kFrecencyMaxAgeMs) { continue; } const qint64 product = qint64(it->count) * weightForAge(age); scores.insert(it.key(), product > kMaxScore ? kMaxScore : int(product)); } return scores;}
void FrecencyStore::load(){ QHash<QString, Entry> loaded; QFile file(m_path); if (file.open(QIODevice::ReadOnly)) { const QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); if (doc.isObject()) { const QJsonObject entries = doc.object().value("entries").toObject(); for (auto it = entries.constBegin(); it != entries.constEnd(); ++it) { if (!it.value().isObject()) { continue; } const QJsonObject raw = it.value().toObject(); Entry entry; entry.count = raw.value("count").toInt(); entry.lastUsedMs = qint64(raw.value("lastUsedMs").toDouble()); if (entry.count > 0 && entry.lastUsedMs > 0) { loaded.insert(it.key(), entry); } } } } const QMutexLocker locker(&m_mutex); m_entries = std::move(loaded); m_dirty = false;}
void FrecencyStore::save(){ const QMutexLocker locker(&m_mutex); pruneLocked(); writeLocked(); m_lastSaveMs = QDateTime::currentMSecsSinceEpoch(); m_dirty = false;}
void FrecencyStore::prune(){ const QMutexLocker locker(&m_mutex); pruneLocked(); m_dirty = true;}
int FrecencyStore::size() const{ const QMutexLocker locker(&m_mutex); return int(m_entries.size());}
void FrecencyStore::writeLocked() const{ QJsonObject entries; for (auto it = m_entries.constBegin(); it != m_entries.constEnd(); ++it) { QJsonObject raw; raw.insert(QStringLiteral("count"), it->count); raw.insert(QStringLiteral("lastUsedMs"), double(it->lastUsedMs)); entries.insert(it.key(), raw); } QJsonObject root; root.insert(QStringLiteral("version"), 1); root.insert(QStringLiteral("entries"), entries);
// QSaveFile writes a temporary file and renames it over the target, so a // reader never meets a half-written table. QDir().mkpath(QFileInfo(m_path).absolutePath()); QSaveFile file(m_path); if (file.open(QIODevice::WriteOnly)) { file.write(QJsonDocument(root).toJson(QJsonDocument::Compact)); file.commit(); }}
void FrecencyStore::pruneLocked(){ const qint64 now = QDateTime::currentMSecsSinceEpoch(); // The keys are collected first, because removing from the table while a // range-for reads it would invalidate the iteration. std::vector<QString> stale; for (auto it = m_entries.constBegin(); it != m_entries.constEnd(); ++it) { if (now - it->lastUsedMs > kFrecencyMaxAgeMs) { stale.push_back(it.key()); } } for (const QString& key : stale) { m_entries.remove(key); } if (m_entries.size() <= kFrecencyMaxEntries) { return; }
// Beyond the cap the highest scores stay, and the path breaks every tie so // two runs of the same table keep the same entries. std::vector<QString> keys; keys.reserve(std::size_t(m_entries.size())); for (auto it = m_entries.constBegin(); it != m_entries.constEnd(); ++it) { keys.push_back(it.key()); } const auto scoreOf = [this, now](const QString& key) { const Entry entry = m_entries.value(key); const qint64 age = now - entry.lastUsedMs; return qint64(entry.count) * (age > 0 ? weightForAge(age) : 100); }; std::ranges::sort(keys, [&scoreOf](const QString& left, const QString& right) { const qint64 leftScore = scoreOf(left); const qint64 rightScore = scoreOf(right); if (leftScore != rightScore) { return leftScore > rightScore; } return left < right; }); keys.resize(std::size_t(kFrecencyMaxEntries));
QHash<QString, Entry> kept; kept.reserve(qsizetype(keys.size())); for (const QString& key : keys) { kept.insert(key, m_entries.value(key)); } m_entries = std::move(kept);}