optimize session refresh with pubsub delta events
- Parse new session/idx format with all data in one read:
name, id, paused, connected, cwd, backend, model, agents
- Handle delta events from eventwait instead of full refresh:
- session.{sid}.agent.{aid}.state -> update cache, emit signal
- Structural changes (new/kill/pause/resume) -> full refresh
- Remove per-agent statewait streams (eliminated N processes)
- Single eventwait stream handles all events via pubsub
This eliminates O(sessions * agents) synchronous reads per refresh,
replacing them with one read + incremental delta updates.
This commit is contained in:
parent
eedee30332
commit
8e8466d4ef
|
|
@ -84,15 +84,13 @@ Ollie9pClient::Ollie9pClient(QObject *parent)
|
||||||
refreshSessions();
|
refreshSessions();
|
||||||
ensureRootDataLoaded();
|
ensureRootDataLoaded();
|
||||||
startAgentConnections();
|
startAgentConnections();
|
||||||
reconcileAgentStateStreams();
|
|
||||||
});
|
});
|
||||||
connect(m_daemon, &NinePConnection::disconnected, this, [this]() {
|
connect(m_daemon, &NinePConnection::disconnected, this, [this]() {
|
||||||
setDaemonConnected(false);
|
setDaemonConnected(false);
|
||||||
stopAgentConnections();
|
stopAgentConnections();
|
||||||
stopAgentStateStreams();
|
|
||||||
});
|
});
|
||||||
connect(m_daemon, &NinePConnection::readyRead, this, [this](const QByteArray &) {
|
connect(m_daemon, &NinePConnection::readyRead, this, [this](const QByteArray &data) {
|
||||||
refreshSessions();
|
handleEvent(QString::fromUtf8(data).trimmed());
|
||||||
});
|
});
|
||||||
|
|
||||||
refreshSessions();
|
refreshSessions();
|
||||||
|
|
@ -288,63 +286,52 @@ void Ollie9pClient::stopAgentConnections()
|
||||||
m_agentConnections.clear();
|
m_agentConnections.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Ollie9pClient::reconcileAgentStateStreams()
|
// handleEvent processes a single event from eventwait.
|
||||||
|
// Event format: "topic [payload]"
|
||||||
|
// Topics: session.{sid}.{action}, session.{sid}.agent.{aid}.{action}
|
||||||
|
void Ollie9pClient::handleEvent(const QString &eventLine)
|
||||||
{
|
{
|
||||||
if (!m_daemonConnected) return;
|
if (eventLine.isEmpty()) return;
|
||||||
QSet<QString> wanted;
|
|
||||||
for (const QVariant &value : std::as_const(m_sessions)) {
|
// Split into topic and payload
|
||||||
const QVariantMap session = value.toMap();
|
int spaceIdx = eventLine.indexOf(' ');
|
||||||
const QString sid = session.value("id").toString();
|
QString topic = spaceIdx > 0 ? eventLine.left(spaceIdx) : eventLine;
|
||||||
const bool paused = session.value("paused").toBool();
|
QString payload = spaceIdx > 0 ? eventLine.mid(spaceIdx + 1) : QString();
|
||||||
for (const QVariant &agentValue : session.value("agents").toList()) {
|
|
||||||
const QVariantMap agent = agentValue.toMap();
|
QStringList parts = topic.split('.');
|
||||||
const QString aid = agent.value("id").toString();
|
// Expected: session.{sid}.* or session.{sid}.agent.{aid}.*
|
||||||
const QString key = agentKey(sid, aid);
|
|
||||||
wanted.insert(key);
|
if (parts.size() >= 3 && parts[0] == "session") {
|
||||||
if (m_agentStateStreams.contains(key)) continue;
|
QString sessionId = parts[1];
|
||||||
if (paused) continue; // Don't start streams for paused sessions
|
QString action = parts[2];
|
||||||
auto *stream = new StreamFsm(StreamFsm::Looping, this);
|
|
||||||
m_agentStateStreams.insert(key, stream);
|
if (action == "agent" && parts.size() >= 5) {
|
||||||
m_agentStateValues.insert(key, "idle");
|
// Agent event: session.{sid}.agent.{aid}.{action}
|
||||||
connect(stream, &StreamFsm::readyRead, this, [this, key, sid, aid](const QByteArray &data) {
|
QString agentId = parts[3];
|
||||||
QString state = QString::fromUtf8(data).trimmed();
|
QString agentAction = parts[4];
|
||||||
if (!state.isEmpty() && m_agentStateValues.value(key) != state) {
|
QString key = agentKey(sessionId, agentId);
|
||||||
m_agentStateValues[key] = state;
|
|
||||||
emit agentStateChanged(sid, aid, state);
|
if (agentAction == "state") {
|
||||||
|
// Update cached state
|
||||||
|
if (!payload.isEmpty() && m_agentStateValues.value(key) != payload) {
|
||||||
|
m_agentStateValues[key] = payload;
|
||||||
|
emit agentStateChanged(sessionId, agentId, payload);
|
||||||
// Also update activeState if this is the active agent
|
// Also update activeState if this is the active agent
|
||||||
if (sid == m_activeSessionId && aid == m_agentId) {
|
if (sessionId == m_activeSessionId && agentId == m_agentId) {
|
||||||
m_activeState = state;
|
m_activeState = payload;
|
||||||
emit activeStateChanged();
|
emit activeStateChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
} else if (agentAction == "new" || agentAction == "kill") {
|
||||||
stream->start(ollie9pBin(), {"-a", serverAddr(), "read",
|
// Structural change — need full refresh
|
||||||
"session/" + sid + "/agent/" + aid + "/statewait"});
|
refreshSessions();
|
||||||
|
}
|
||||||
|
} else if (action == "pause" || action == "resume" ||
|
||||||
|
action == "new" || action == "kill" || action == "rename") {
|
||||||
|
// Session-level structural change — need full refresh
|
||||||
|
refreshSessions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Remove streams for agents that no longer exist
|
|
||||||
for (auto it = m_agentStateStreams.begin(); it != m_agentStateStreams.end();) {
|
|
||||||
if (wanted.contains(it.key())) {
|
|
||||||
++it;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
it.value()->disconnect(this);
|
|
||||||
it.value()->stop();
|
|
||||||
it.value()->deleteLater();
|
|
||||||
m_agentStateValues.remove(it.key());
|
|
||||||
it = m_agentStateStreams.erase(it);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Ollie9pClient::stopAgentStateStreams()
|
|
||||||
{
|
|
||||||
for (StreamFsm *stream : std::as_const(m_agentStateStreams)) {
|
|
||||||
stream->disconnect(this);
|
|
||||||
stream->stop();
|
|
||||||
}
|
|
||||||
qDeleteAll(m_agentStateStreams);
|
|
||||||
m_agentStateStreams.clear();
|
|
||||||
m_agentStateValues.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Ollie9pClient::agentState(const QString &sessionId, const QString &agentId) const
|
QString Ollie9pClient::agentState(const QString &sessionId, const QString &agentId) const
|
||||||
|
|
@ -356,57 +343,46 @@ void Ollie9pClient::refreshSessions()
|
||||||
{
|
{
|
||||||
if (!m_daemonConnected) return;
|
if (!m_daemonConnected) return;
|
||||||
QByteArray out = run9p({"read", "session/idx"});
|
QByteArray out = run9p({"read", "session/idx"});
|
||||||
// Don't trim — trailing tabs are significant fields
|
|
||||||
QString raw = QString::fromUtf8(out);
|
QString raw = QString::fromUtf8(out);
|
||||||
|
|
||||||
m_sessions.clear();
|
m_sessions.clear();
|
||||||
QSet<QString> seen;
|
|
||||||
if (!raw.isEmpty()) {
|
if (!raw.isEmpty()) {
|
||||||
for (const QString &line : raw.split('\n', Qt::SkipEmptyParts)) {
|
for (const QString &line : raw.split('\n', Qt::SkipEmptyParts)) {
|
||||||
QStringList parts = line.split('\t');
|
QStringList parts = line.split('\t');
|
||||||
// Skip lines where the first field (session name) is empty
|
// New format: name\tid\tpaused\tconnected\tcwd\tbackend\tmodel\tagents
|
||||||
if (parts.isEmpty() || parts[0].isEmpty()) continue;
|
// Where agents is semicolon-separated: name:id:state;name:id:state
|
||||||
QString sid = parts[0];
|
if (parts.size() < 2) continue;
|
||||||
if (seen.contains(sid)) continue; // already added (multi-agent)
|
|
||||||
seen.insert(sid);
|
QVariantMap session;
|
||||||
QVariantMap session;
|
session["name"] = parts.value(0);
|
||||||
// session/idx format: name\tstate\tcwd\tbackend\tmodel\tagentName\tid
|
session["id"] = parts.value(1);
|
||||||
// The immutable ID is in field 6 (0-indexed), avoiding a round-trip.
|
session["paused"] = (parts.value(2) == "true");
|
||||||
QString sessionId = parts.size() > 6 ? parts[6] : "";
|
session["connected"] = (parts.value(3) == "true");
|
||||||
if (sessionId.isEmpty()) sessionId = sid;
|
session["cwd"] = parts.value(4);
|
||||||
session["id"] = sessionId;
|
session["backend"] = parts.value(5);
|
||||||
session["name"] = sid;
|
session["model"] = parts.value(6);
|
||||||
session["state"] = parts.size() > 1 ? parts[1] : "";
|
|
||||||
session["cwd"] = parts.size() > 2 ? parts[2] : "";
|
// Parse agents from semicolon-separated list
|
||||||
session["backend"] = parts.size() > 3 ? parts[3] : "";
|
QVariantList agents;
|
||||||
session["model"] = parts.size() > 4 ? parts[4] : "";
|
QString agentStr = parts.value(7);
|
||||||
// Check if session is paused
|
if (!agentStr.isEmpty()) {
|
||||||
QString pausedStr = QString::fromUtf8(run9p({"read", "session/" + sessionId + "/paused"})).trimmed();
|
for (const QString &agentEntry : agentStr.split(';', Qt::SkipEmptyParts)) {
|
||||||
session["paused"] = (pausedStr == "true");
|
QStringList agentParts = agentEntry.split(':');
|
||||||
// Check RPC connection status
|
if (agentParts.size() >= 3) {
|
||||||
QString connectedStr = QString::fromUtf8(run9p({"read", "session/" + sessionId + "/connected"})).trimmed();
|
QVariantMap av;
|
||||||
session["connected"] = (connectedStr == "true");
|
av["name"] = agentParts.value(0);
|
||||||
QVariantList agents;
|
av["id"] = agentParts.value(1);
|
||||||
for (const QString &name : QString::fromUtf8(run9p({"ls", "session/" + sessionId + "/agent"})).trimmed().split('\n', Qt::SkipEmptyParts)) {
|
av["state"] = agentParts.value(2);
|
||||||
if (name == "new") continue;
|
av["connected"] = session["connected"]; // Agent inherits session connectivity
|
||||||
const QString path = "session/" + sessionId + "/agent/" + name;
|
|
||||||
AgentRecord a;
|
|
||||||
a.id = QString::fromUtf8(run9p({"read", path + "/id"})).trimmed();
|
|
||||||
a.name = name;
|
|
||||||
if (a.id.isEmpty()) a.id = name;
|
|
||||||
QString agentState = QString::fromUtf8(run9p({"read", path + "/state"})).trimmed();
|
|
||||||
if (agentState.isEmpty()) agentState = "idle";
|
|
||||||
QVariantMap av; av["id"] = a.id; av["name"] = a.name;
|
|
||||||
av["state"] = agentState;
|
|
||||||
av["connected"] = m_daemonConnected && m_agentConnected.value(agentKey(sessionId, a.id), false);
|
|
||||||
agents.append(av);
|
agents.append(av);
|
||||||
}
|
}
|
||||||
session["agents"] = agents;
|
}
|
||||||
m_sessions.append(session);
|
}
|
||||||
|
session["agents"] = agents;
|
||||||
|
m_sessions.append(session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reconcileAgentConnections();
|
reconcileAgentConnections();
|
||||||
reconcileAgentStateStreams();
|
|
||||||
emit sessionsChanged();
|
emit sessionsChanged();
|
||||||
|
|
||||||
// Check if the currently active session/agent still exist
|
// Check if the currently active session/agent still exist
|
||||||
|
|
@ -625,13 +601,9 @@ void Ollie9pClient::switchAgent(const QString &sessionId, const QString &agentId
|
||||||
stopStreams();
|
stopStreams();
|
||||||
m_activeSessionId = sessionId;
|
m_activeSessionId = sessionId;
|
||||||
m_agentId = agentId;
|
m_agentId = agentId;
|
||||||
m_activeState = "idle";
|
|
||||||
|
// Use cached state from live tracking if available, otherwise default to idle
|
||||||
// Read initial state immediately
|
m_activeState = m_agentStateValues.value(agentKey(sessionId, agentId), "idle");
|
||||||
QByteArray stateOut = run9p({"read", agentPath() + "/state"});
|
|
||||||
QString stateStr = QString::fromUtf8(stateOut).trimmed();
|
|
||||||
if (!stateStr.isEmpty())
|
|
||||||
m_activeState = stateStr;
|
|
||||||
|
|
||||||
emit activeSessionIdChanged();
|
emit activeSessionIdChanged();
|
||||||
emit activeAgentIdChanged();
|
emit activeAgentIdChanged();
|
||||||
|
|
|
||||||
|
|
@ -120,9 +120,8 @@ private:
|
||||||
void startAgentConnections();
|
void startAgentConnections();
|
||||||
void startActiveAgentStreams();
|
void startActiveAgentStreams();
|
||||||
void reconcileAgentConnections();
|
void reconcileAgentConnections();
|
||||||
void reconcileAgentStateStreams();
|
|
||||||
void stopAgentConnections();
|
void stopAgentConnections();
|
||||||
void stopAgentStateStreams();
|
void handleEvent(const QString &eventLine);
|
||||||
|
|
||||||
QVariantList m_sessions;
|
QVariantList m_sessions;
|
||||||
QString m_activeSessionId;
|
QString m_activeSessionId;
|
||||||
|
|
@ -143,8 +142,7 @@ private:
|
||||||
QSet<QString> m_seenAgentConnections;
|
QSet<QString> m_seenAgentConnections;
|
||||||
NinePConnection *m_daemon = nullptr;
|
NinePConnection *m_daemon = nullptr;
|
||||||
|
|
||||||
// Per-agent state tracking
|
// Agent state cache — updated via delta events
|
||||||
QHash<QString, StreamFsm *> m_agentStateStreams;
|
|
||||||
QHash<QString, QString> m_agentStateValues;
|
QHash<QString, QString> m_agentStateValues;
|
||||||
|
|
||||||
// Streaming FSMs — each wraps a QProcess lifecycle
|
// Streaming FSMs — each wraps a QProcess lifecycle
|
||||||
|
|
|
||||||
Reference in New Issue