Skip to content

Commit 91c1d93

Browse files
authored
Merge pull request #521 from quotient-im/kitsune-unread-statistics
2 parents a2cc707 + c57d6de commit 91c1d93

File tree

8 files changed

+746
-247
lines changed

8 files changed

+746
-247
lines changed

.github/workflows/ci.yml

+2
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ jobs:
160160
env:
161161
TEST_USER: ${{ secrets.TEST_USER }}
162162
TEST_PWD: ${{ secrets.TEST_PWD }}
163+
QT_LOGGING_RULES: 'quotient.main.debug=true;quotient.jobs.debug=true'
164+
QT_MESSAGE_PATTERN: '%{time h:mm:ss.zzz}|%{category}|%{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}|%{message}'
163165
run: |
164166
[[ -z "$TEST_USER" ]] || $VALGRIND build/quotest/quotest "$TEST_USER" "$TEST_PWD" quotest-gha '#quotest:matrix.org' "$QUOTEST_ORIGIN"
165167
timeout-minutes: 4 # quotest is supposed to finish within 3 minutes, actually

CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ list(APPEND lib_SRCS
135135
lib/avatar.cpp
136136
lib/uri.cpp
137137
lib/uriresolver.cpp
138+
lib/eventstats.cpp
138139
lib/syncdata.cpp
139140
lib/settings.cpp
140141
lib/networksettings.cpp

lib/eventstats.cpp

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// SPDX-FileCopyrightText: 2021 Quotient contributors
2+
// SPDX-License-Identifier: LGPL-2.1-or-later
3+
4+
#include "eventstats.h"
5+
6+
using namespace Quotient;
7+
8+
EventStats EventStats::fromRange(const Room* room, const Room::rev_iter_t& from,
9+
const Room::rev_iter_t& to,
10+
const EventStats& init)
11+
{
12+
Q_ASSERT(to <= room->historyEdge());
13+
Q_ASSERT(from >= Room::rev_iter_t(room->syncEdge()));
14+
Q_ASSERT(from <= to);
15+
QElapsedTimer et;
16+
et.start();
17+
const auto result =
18+
accumulate(from, to, init,
19+
[room](EventStats acc, const TimelineItem& ti) {
20+
acc.notableCount += room->isEventNotable(ti);
21+
acc.highlightCount += room->notificationFor(ti).type
22+
== Notification::Highlight;
23+
return acc;
24+
});
25+
if (et.nsecsElapsed() > profilerMinNsecs() / 10)
26+
qCDebug(PROFILER).nospace()
27+
<< "Event statistics collection over index range [" << from->index()
28+
<< "," << (to - 1)->index() << "] took " << et;
29+
return result;
30+
}
31+
32+
EventStats EventStats::fromMarker(const Room* room,
33+
const EventStats::marker_t& marker)
34+
{
35+
const auto s = fromRange(room, marker_t(room->syncEdge()), marker,
36+
{ 0, 0, marker == room->historyEdge() });
37+
Q_ASSERT(s.isValidFor(room, marker));
38+
return s;
39+
}
40+
41+
EventStats EventStats::fromCachedCounters(Omittable<int> notableCount,
42+
Omittable<int> highlightCount)
43+
{
44+
const auto hCount = std::max(0, highlightCount.value_or(0));
45+
if (!notableCount.has_value())
46+
return { 0, hCount, true };
47+
auto nCount = notableCount.value_or(0);
48+
return { std::max(0, nCount), hCount, nCount != -1 };
49+
}
50+
51+
bool EventStats::updateOnMarkerMove(const Room* room, const marker_t& oldMarker,
52+
const marker_t& newMarker)
53+
{
54+
if (newMarker == oldMarker)
55+
return false;
56+
57+
// Double-check consistency between the old marker and the old stats
58+
Q_ASSERT(isValidFor(room, oldMarker));
59+
Q_ASSERT(oldMarker > newMarker);
60+
61+
// A bit of optimisation: only calculate the difference if the marker moved
62+
// less than half the remaining timeline ahead; otherwise, recalculation
63+
// over the remaining timeline will very likely be faster.
64+
if (oldMarker != room->historyEdge()
65+
&& oldMarker - newMarker < newMarker - marker_t(room->syncEdge())) {
66+
const auto removedStats = fromRange(room, newMarker, oldMarker);
67+
Q_ASSERT(notableCount >= removedStats.notableCount
68+
&& highlightCount >= removedStats.highlightCount);
69+
notableCount -= removedStats.notableCount;
70+
highlightCount -= removedStats.highlightCount;
71+
return removedStats.notableCount > 0 || removedStats.highlightCount > 0;
72+
}
73+
74+
const auto newStats = EventStats::fromMarker(room, newMarker);
75+
if (!isEstimate && newStats == *this)
76+
return false;
77+
*this = newStats;
78+
return true;
79+
}
80+
81+
bool EventStats::isValidFor(const Room* room, const marker_t& marker) const
82+
{
83+
const auto markerAtHistoryEdge = marker == room->historyEdge();
84+
// Either markerAtHistoryEdge and isEstimate are in the same state, or it's
85+
// a special case of no notable events and the marker at history edge
86+
// (then isEstimate can assume any value).
87+
return markerAtHistoryEdge == isEstimate
88+
|| (markerAtHistoryEdge && notableCount == 0);
89+
}
90+
91+
QDebug Quotient::operator<<(QDebug dbg, const EventStats& es)
92+
{
93+
QDebugStateSaver _(dbg);
94+
dbg.nospace() << es.notableCount << '/' << es.highlightCount;
95+
if (es.isEstimate)
96+
dbg << " (estimated)";
97+
return dbg;
98+
}

lib/eventstats.h

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// SPDX-FileCopyrightText: 2021 Quotient contributors
2+
// SPDX-License-Identifier: LGPL-2.1-or-later
3+
4+
#pragma once
5+
6+
#include "room.h"
7+
8+
namespace Quotient {
9+
10+
//! \brief Counters of unread events and highlights with a precision flag
11+
//!
12+
//! This structure contains a static snapshot with values of unread counters
13+
//! returned by Room::partiallyReadStats and Room::unreadStats (properties
14+
//! or methods).
15+
//!
16+
//! \note It's just a simple grouping of counters and is not automatically
17+
//! updated from the room as subsequent syncs arrive.
18+
//! \sa Room::unreadStats, Room::partiallyReadStats, Room::isEventNotable
19+
struct EventStats {
20+
Q_GADGET
21+
Q_PROPERTY(qsizetype notableCount MEMBER notableCount CONSTANT)
22+
Q_PROPERTY(qsizetype highlightCount MEMBER highlightCount CONSTANT)
23+
Q_PROPERTY(bool isEstimate MEMBER isEstimate CONSTANT)
24+
public:
25+
//! The number of "notable" events in an events range
26+
//! \sa Room::isEventNotable
27+
qsizetype notableCount = 0;
28+
qsizetype highlightCount = 0;
29+
//! \brief Whether the counter values above are exact
30+
//!
31+
//! This is false when the end marker (m.read receipt or m.fully_read) used
32+
//! to collect the stats points to an event loaded locally and the counters
33+
//! can therefore be calculated exactly using the locally available segment
34+
//! of the timeline; true when the marker points to an event outside of
35+
//! the local timeline (in which case the estimation is made basing on
36+
//! the data supplied by the homeserver as well as counters saved from
37+
//! the previous run of the client).
38+
bool isEstimate = true;
39+
40+
// TODO: replace with = default once C++20 becomes a requirement on clients
41+
bool operator==(const EventStats& rhs) const
42+
{
43+
return notableCount == rhs.notableCount
44+
&& highlightCount == rhs.highlightCount
45+
&& isEstimate == rhs.isEstimate;
46+
}
47+
bool operator!=(const EventStats& rhs) const { return !operator==(rhs); }
48+
49+
//! \brief Check whether the event statistics are empty
50+
//!
51+
//! Empty statistics have notable and highlight counters of zero and
52+
//! isEstimate set to false.
53+
Q_INVOKABLE bool empty() const
54+
{
55+
return notableCount == 0 && !isEstimate && highlightCount == 0;
56+
}
57+
58+
using marker_t = Room::rev_iter_t;
59+
60+
//! \brief Build event statistics on a range of events
61+
//!
62+
//! This is a factory that returns an EventStats instance with counts of
63+
//! notable and highlighted events between \p from and \p to reverse
64+
//! timeline iterators; the \p init parameter allows to override
65+
//! the initial statistics object and start from other values.
66+
static EventStats fromRange(const Room* room, const marker_t& from,
67+
const marker_t& to,
68+
const EventStats& init = { 0, 0, false });
69+
70+
//! \brief Build event statistics on a range from sync edge to marker
71+
//!
72+
//! This is mainly a shortcut for \code
73+
//! <tt>fromRange(room, marker_t(room->syncEdge()), marker)</tt>
74+
//! \endcode except that it also sets isEstimate to true if (and only if)
75+
//! <tt>to == room->historyEdge()</tt>.
76+
static EventStats fromMarker(const Room* room, const marker_t& marker);
77+
78+
//! \brief Loads a statistics object from the cached counters
79+
//!
80+
//! Sets isEstimate to `true` unless both notableCount and highlightCount
81+
//! are equal to -1.
82+
static EventStats fromCachedCounters(Omittable<int> notableCount,
83+
Omittable<int> highlightCount = none);
84+
85+
//! \brief Update statistics when a read marker moves down the timeline
86+
//!
87+
//! Removes events between oldMarker and newMarker from statistics
88+
//! calculation if \p oldMarker points to an existing event in the timeline,
89+
//! or recalculates the statistics entirely if \p oldMarker points
90+
//! to <tt>room->historyEdge()</tt>. Always results in exact statistics
91+
//! (<tt>isEstimate == false</tt>.
92+
//! \param oldMarker Must point correspond to the _current_ statistics
93+
//! isEstimate state, i.e. it should point to
94+
//! <tt>room->historyEdge()</tt> if <tt>isEstimate == true</tt>, or
95+
//! to a valid position within the timeline otherwise
96+
//! \param newMarker Must point to a valid position in the timeline (not to
97+
//! <tt>room->historyEdge()</tt> that is equal to or closer to
98+
//! the sync edge than \p oldMarker
99+
//! \return true if either notableCount or highlightCount changed, or if
100+
//! the statistics was completely recalculated; false otherwise
101+
bool updateOnMarkerMove(const Room* room, const marker_t& oldMarker,
102+
const marker_t& newMarker);
103+
104+
//! \brief Validate the statistics object against the given marker
105+
//!
106+
//! Checks whether the statistics object data are valid for a given marker.
107+
//! No stats recalculation takes place, only isEstimate and zero-ness
108+
//! of notableCount are checked.
109+
bool isValidFor(const Room* room, const marker_t& marker) const;
110+
};
111+
112+
QDebug operator<<(QDebug dbg, const EventStats& es);
113+
114+
}

0 commit comments

Comments
 (0)