Warning, /pim/trojita/docs/masters/architecture.tex is written in an unsupported language. File is not indexed.
0001 % vim: spelllang=en spell textwidth=120 0002 \documentclass[trojita]{subfiles} 0003 0004 \begin{document} 0005 0006 \chapter{Trojitá's Architecture} 0007 0008 This chapter provides a brief introduction to the architecture of Trojitá from a programmer's point of view. Additional 0009 information is provided in the documentation shipped as a part of the source tree. 0010 0011 \section{Overview of Components} 0012 0013 Trojitá makes heavy use of certain idioms common in Qt programming and in object-oriented software development in 0014 general. 0015 0016 At the highest layer lies the GUI, graphical code managing the actual user interaction. This code contains no knowledge 0017 of IMAP or any other e-mail protocol; it is simply a graphical presentation layer specific to the desktop version of 0018 Trojitá. In the releases intended for mobile platforms, the traditional {\tt QWidget}-based GUI is replaced by a 0019 variant built on top of Qt's QML \cite{qml}, a framework especially suited for touch-centric devices. 0020 0021 Any interaction with IMAP is initiated through the model-view framework \cite{qt-mvc} and related utilities. A core 0022 class encapsulating a representation of a single IMAP server, the {\tt Model} class, is accompanied by various proxy 0023 models and other helpers to segregate and transform the data into a better shape suitable for the upper layers. 0024 0025 Any action which shall take effect is, however, not performed by any of the model-related classes. Trojitá utilizes the 0026 concept of {\em tasks}, a set of single-purpose classes each serving a distinct role. Examples of such tasks are 0027 ``obtain a connection'', ``synchronize a mailbox'', ``download a message'' or ``update message flags''. 0028 0029 One layer below the Tasks, the Parser is located. This class along with its support tools converts data between a byte 0030 stream arriving from or destined to the network and higher-level commands and responses which are utilized by the upper 0031 layers. Actual network I/O operations are handled through a thin wrapper around Qt's own {\tt QIODevice} called {\tt 0032 Streams}.~\footnote{The {\tt QIODevice} is wrapped to allow for transparent compression using the deflate algorithm. 0033 Due to the historic reasons, the {\tt Stream} subclasses use the {\em has-a} instead of the {\em is-a} approach; this 0034 was required back when Trojitá shipped I/O implementations not based on the {\tt QIODevice} class for evaluation 0035 purposes. It also helps to reduce the amount of functions each I/O implementation has to implement.} 0036 0037 \subsection{Handling Input/Output} 0038 0039 The raw byte streams provided by the network abstraction classes are intercepted by the {\tt Parser} class. It takes 0040 any incoming octet sequence and with the help of a {\tt LowLevelParser} instantiates the concrete subclasses of the {\tt 0041 AbstractResponse} class. Actual parsing of the responses is typically deferred to the corresponding {\tt Response} 0042 constructors which will tokenize the incoming data through {\tt LowLevelParser}'s methods and act on their contents 0043 according to the corresponding grammar rules. 0044 0045 The response parsing had to be substantially relaxed from its original state due to observed interoperability issues. 0046 Even the most popular IMAP implementations struggled with following the ABNF-defined \cite{rfc5234} syntax to the 0047 letter; the most iconic example is Google's service which has prevented the IMAP clients talking to it from accessing 0048 forwarded messages for many years~\cite{gmail-bodystructure-sucks}.~\footnote{As of mid-2012, this issue remains 0049 unfixed despite my bug reports raised through both official and unofficial channels.} 0050 0051 For some time, Trojitá's {\tt Parser} was implemented in a separate thread in an attempt to improve performance. 0052 However, profiling showed that the amount of time spent in parsing was negligible compared to the rest of the 0053 application with the only exception being the {\tt THREAD} response which essentially requires a complex transformation 0054 of nested lists. As such, the whole of Trojitá is now a single threaded application.~\footnote{The WebKit engine used 0055 in HTML rendering creates threads for its individual purposes; the similar behavior is observed in the QML engine used 0056 in the mobile version. These threads are not counted here, for they are considered to come from the ``system 0057 libraries''.} 0058 0059 \subsection{The Concept of Tasks} 0060 0061 The Tasks are designed to collaborate with each other, and there is a network of dependencies on how they can be used 0062 so that each task is responsible for only a subset of the overall work required to achieve a particular goal. For 0063 example, when a message shall be marked as read, an {\tt UpdateFlagTask} is created. Because ``marking message as 0064 read'' is implemented by manipulating IMAP message flags, an action which can only happen while a mailbox is selected, 0065 the {\tt UpdateFlagsTask} asks the {\tt Model} to return an instance of a {\tt KeepMailboxOpenTask}, a ``manager'' 0066 object which is responsible for keeping the mailbox state synchronized between the server and the client.~\footnote{The 0067 {\tt KeepMailboxOpenTask} also serves as a dispatcher ensuring that the selected mailbox is switched only when any tasks 0068 which needed a particular mailbox open will have finished.} If the mailbox is already opened, an existing instance of 0069 the {\tt KeepMailboxOpenTask} is returned; if that is not the case, a new instance is returned. The {\tt 0070 UpdateFlagsTask}'s execution is blocked and will commence only when (and if) the acquisition of a synchronized status 0071 succeeds. Similarly, this ``maintaining'' task (the {\tt KeepMailboxOpenTask} itself deals just with incremental {\em 0072 updates} to the mailbox state and is activated only when the mailbox is opened. The actual mailbox synchronization is 0073 delegated to the {\tt ObtainSynchronizedMailboxTask}. This synchronizer obtains the underlying connection handle 0074 through consultation with the {\tt Model} to decide whether a new connection shall be established or an existing one 0075 re-purposed. This policy decision is completely contained in the {\tt Model} and is not visible to other layers (or the 0076 tasks) at all. 0077 0078 Similar divisions of responsibility exist during other scenarios; the supporting infrastructure makes sure that no 0079 actions are started unless their prerequisites have succeeded. 0080 0081 The whole hierarchy is presented to the user in various shapes; the most basic and rudimentary one is a ``busy 0082 indicator'' showing whether any activity is taking place. A more detailed view showing an overview of all active tasks 0083 in a tree-like manner illustrating their dependencies is available (see the {\tt TaskPresentationModel} class in 0084 Trojitá's sources). 0085 0086 The introduction of Tasks to Trojitá in 2010 proved to be an excellent decision allowing for much increased development 0087 pace and contributed to the much improved robustness and code clarity. Thanks to a proper use of git's branches, the 0088 transition was undertaken over a longer time pried without disturbing the ongoing application development. 0089 0090 The Tasks are instantiated by an auxiliary factory class in order to better facilitate automated unit testing with 0091 mock objects. 0092 0093 \subsection{Routing Responses} 0094 0095 The Tasks introduced in the previous section are also used for proper response processing. At all times, the {\tt 0096 Model} can access a list of ``active tasks'' which are assigned to a particular IMAP connection. When a response 0097 arrives, it is dispatched to these tasks in a well-defined order until either of the tasks declares the response as 0098 ``handled''. If no task claims responsibility for a particular response, the {\tt Model} itself takes action. This 0099 action might be either regular processing, or, in case the {\tt Model} cannot safely take care of said response, an 0100 error being raised and the connection sewered to prevent possible data loss due to suspected bug in the IMAP 0101 implementation. 0102 0103 The responses themselves are communicated to the {\tt Model} (and, through it, to the responsible Tasks) through a queue 0104 of responses using Qt's own signal-and-slot mechanism. The same way of passing data is used for additional 0105 ``meta information'' like informing about connection errors, parser exceptions or the low-level SSL/TLS certificate 0106 properties. Having a unified queue of the incoming data made error handling much more elegant.~\footnote{Previously, 0107 matters were complicated by the way how QWidget-based UIs tend to deal with errors through dialog boxes which trigger 0108 nested event loops.} Care has been taken to optimize this parsed data passing to minimize the amount of copying and --- 0109 in general --- Qt's {\tt QMetaObject} invocations while balancing the GUI responsiveness. The IMAP handlers now process 0110 the incoming responses in batches, pausing every so often to allow the GUI to keep up with other incoming events to 0111 prevent an appearance of the whole application ``freezing'' temporarily. The selected approach works well even on cell 0112 phones with limited resources. 0113 0114 Any responses passing through the queue are also logged into an in-memory ring buffer. Should an error occur, this 0115 buffer is used as a ``black box'' device which is shown to the user to give her a better context of what went wrong. 0116 This debug logging proved to be extremely valuable in debugging interoperability issues with non-compliant servers as 0117 well as when fixing bugs in Trojitá. 0118 0119 \subsection{Models and Proxies} 0120 0121 Historically, much of the IMAP logic in Trojitá has been concentrated in the {\tt Model} class. Over the time, I've 0122 refactored that code to separate modules; however, even today, the {\tt Model} handles both data publication through the 0123 Qt's model-view framework as well as IMAP response dispatch to the relevant Tasks. With little less than two thousands 0124 of physical lines of code, the {\tt Model} remains the second-largest source file in the code base (the biggest file is 0125 a unit tests related to various modes of mailbox synchronization). 0126 0127 The {\tt Model} itself exports a view containing anything available from a particular IMAP server account through the 0128 Qt's model-view API. The actual data is stored in a tree formed by various {\tt TreeItem} subclasses. 0129 0130 As showing a single, rich-structured tree containing {\em everything} from mailboxes to individual message parts would 0131 not be particularly user-friendly, a number of so called proxy models were created. These models usually operate on 0132 Qt's {\tt QModelIndex} level, but where profiling showed that a compelling speed increase would result from bypassing 0133 the {\tt QVariant} abstraction, direct access through the underlying (and Trojitá-specific) {\tt TreeItem} classes was 0134 used instead. This has notably affected the design of the {\tt ThreadingMsgListModel} which is actually the biggest 0135 consumer of the CPU time when Trojitá opens a huge mailbox and activates message threading for the first time. 0136 0137 Proxies exist for performing various transformations and filtering of the available data; some of them (like the {\tt 0138 MailboxModel} and {\tt MsgListModel}) are generic enough and used in all of the desktop client, the mobile application 0139 and the single-purpose batch synchronizer (see Appendix \ref{sec:xtuple}, p.~\pageref{sec:xtuple}) while others (like 0140 the {\tt PrettyMsgListModel}) are exclusive to the traditional desktop GUI. 0141 0142 \subsection{Lazy Loading and Cache} 0143 0144 The model-view separation strictly followed throughout Trojitá proved to be very useful when leveraging the full 0145 potential of many advanced IMAP features. Because the individual message parts are accessible separately, Trojitá's 0146 native mode of operation supported the often-mentioned use case of ``only download a tiny text accompanying the big 0147 photo attachment and then ask user whether to fetch the photo'' without any effort. At the same time, this separation 0148 made certain tasks which would typically be considered trivial a bit more demanding --- e.g. when forwarding a message, 0149 Trojitá has to explicitly retrieve two independent body parts, one for the headers and one for the message payload, and 0150 explicitly join them together. On the other hand, in most of the situations this separation brought benefits visible to 0151 the end user which trumped the minor, uncommon complications. 0152 0153 Any data once downloaded are kept in a persistent cache. This feature allows Trojitá to work extremely well under 0154 unfavorable network conditions. It also allows its users to access any data which were already known previously in an 0155 offline mode. 0156 0157 Several caching backends are shipped in Trojitá; some of them store data exclusively in memory and therefore are not 0158 {\em persistent} per se, others use SQLite \cite{sqlite} for actual data storage. Another mode which offloads ``big 0159 data'' storage to additional on-disk files is used by default.~\footnote{Work on the structured cache backends was 0160 sponsored by the KWest GbmH. (Appendix \ref{sec:kwest}, p.~\pageref{sec:kwest}).} 0161 0162 \section{The Mobile Version} 0163 0164 Trojitá also ships with a special version targeted for use on cell phones with touch screen such as Nokia's N9. The 0165 release was tested on the Nokia N950, a developer device which I obtained through Nokia's Community Device Programme 0166 (Appendix \ref{sec:nokia-cdp}, p.~\pageref{sec:nokia-cdp}). 0167 0168 The touch-optimized version of Trojitá shares most of the code with the desktop version; in particular, none of the 0169 underlying IMAP code had to be modified. The changes were concentrated solely in the GUI layer which was completely 0170 rewritten using QML \cite{qml}, Qt's declarative language for fluid user interface creation. A certain amount of C++ 0171 code was also required, mainly for seamless integration of the underlying data providers with the JavaScript-based QML 0172 data presentation. 0173 0174 I also had to extend some of the existing Qt classes with proper functions required for Trojitá. One of them was 0175 replacing QML's native {\tt QDeclarativeWebView} component, an QML item encapsulating WebKit, the HTML renderer. This 0176 replacement was required because the stock version from upstream Qt did not support specifying a {\tt 0177 QNetworkAccessManager} instance to use on a per-item basis~\cite{jkt-qdeclarativewebview}. In QML, the whole QML scene 0178 shares a single instance of the {\tt QNetworkAccessManager} which is used for all sorts of data fetching. In Trojitá, 0179 such a behavior is unacceptable for security reasons because the actual message data are transferred through said 0180 manager, and the manager therefore has to implement a proper security policy denying any requests for external 0181 elements.~\footnote{If the e-mail rendered was able to access external data, e.g. fetch images from the Internet at 0182 will, the resulting HTTP transfers would compromise the user's privacy and reveal confidential information like presence 0183 and schedule of a real person accessing a particular e-mail account. Such tracing can be trivially implemented by 0184 including a unique identifier in the image URL, which is the reason why any decent MUA rejects such network requests by 0185 default.} Another reason for this separation is to make sure that each {\tt QNetworkAccessManager} can only access data 0186 originating from a single e-mail message, an enforcement crucial to prevent phishing attempts and maintain the 0187 confidentiality of the user's messages. There were also additional technical reasons for this extension, some of which 0188 were related to a documented requirement of QML's global instance adding provisions for future multithreaded access, a 0189 requirement which Trojitá's implementation explicitly does not guarantee for performance reasons. 0190 0191 Apart from the mentioned issues, the porting proved to be very easy, to the extent when the most limiting factor were 0192 the author's lack of familiarity with QML and this technology's relative immaturity and lack of existing components when 0193 compared to the ``regular'' Qt applications.~\footnote{An example is the lack of a {\tt QSettings} equivalent in QML; 0194 users are suggested to use raw SQL access through {\tt openDatabaseSync} method which is indeed much more flexible, but 0195 also located several layers below the {\tt QSettings} on an index of the out-of-box usability --- clearly, writing 0196 custom SQL queries is more demanding than issuing a call to {\tt int i = QSettings().value("i").toInt()}.} No design 0197 decisions of the application were hit as obstacles during the porting process. 0198 0199 Trojitá's asynchronous mode of operation where any data transfers are performed in the background, without blocking the 0200 user interface, was paramount in achieving a decent performance and smooth UI updates. The batched data processing 0201 mentioned in previous sections of this chapter is an example of a performance optimization which contributed to the 0202 excellent responsiveness of the cell phone version. 0203 0204 The mobile version is still not as mature as the regular desktop release; most importantly, it does not support sending 0205 e-mails for now. Proper platform integration should also be done in future. For these reasons, the application was 0206 submitted to the Ovi store, Nokia's ``app store'' product, as a {\em technical preview}. Even at its present state, 0207 however, the application is very useful for accessing user's e-mail on the go and for quick checking of whether 0208 something important has happened. Trojitá's support for many IMAP extensions which reduce the overall bandwidth 0209 consumption was also tremendously useful --- many of them, like the {\tt CONDSTORE} and {\tt QRESYNC}, were designed 0210 with exactly this use case in mind and are also reasonably wide deployed among the publicly available IMAP server 0211 implementations. 0212 0213 \section{Regression Testing} 0214 0215 The IMAP protocol implementation developed as a part of Trojitá is covered by an extensive test suite verifying its 0216 behavior under varying conditions. All extensions supported by Trojitá which relate to e-mail retrieval are covered by 0217 the test suite. 0218 0219 Most of the testing works by replacing the connection to the IMAP server by a mock object capable of verifying that the 0220 transmitted data matches the developer's expectations and returning the sample responses back. This approach is 0221 self-contained, does not require any other software to test and therefore increases the chances of regressions getting 0222 caught before they made their way to a production release. 0223 0224 The following is an example on how an actual test might look like: 0225 0226 \begin{minted}{c++} 0227 /** @short Test QRESYNC reporting changed flags */ 0228 void ImapModelObtainSynchronizedMailboxTest::testQresyncChangedFlags() 0229 { 0230 // Trick the IMAP code into believing that the server supports QRESYNC 0231 // The test environment can do that without the burden of explicitly 0232 // faking the CAPABILITY response. 0233 FakeCapabilitiesInjector injector(model); 0234 injector.injectCapability("QRESYNC"); 0235 0236 // Simulate the previous state of the mailbox 0237 Imap::Mailbox::SyncState sync; 0238 sync.setExists(3); 0239 sync.setUidValidity(666); 0240 sync.setUidNext(15); 0241 sync.setHighestModSeq(33); 0242 QList<uint> uidMap; 0243 uidMap << 6 << 9 << 10; 0244 0245 // Store the simulated state in the persistent cache 0246 model->cache()->setMailboxSyncState("a", sync); 0247 model->cache()->setUidMapping("a", uidMap); 0248 model->cache()->setMsgFlags("a", 6, QStringList() << "x"); 0249 model->cache()->setMsgFlags("a", 9, QStringList() << "y"); 0250 model->cache()->setMsgFlags("a", 10, QStringList() << "z"); 0251 0252 // At this point, we can proceed with actual testing 0253 0254 // Initiate the activity 0255 model->resyncMailbox(idxA); 0256 // Check the I/O communication 0257 cClient(t.mk("SELECT a (QRESYNC (666 33 (2 9)))\r\n")); 0258 cServer("* 3 EXISTS\r\n" 0259 "* OK [UIDVALIDITY 666] .\r\n" 0260 "* OK [UIDNEXT 15] .\r\n" 0261 "* OK [HIGHESTMODSEQ 36] .\r\n" 0262 "* 2 FETCH (UID 9 FLAGS (x2 \\Seen))\r\n" 0263 ); 0264 cServer(t.last("OK selected\r\n")); 0265 // Make sure that nothing else was transferred "over the network" 0266 cEmpty(); 0267 0268 // Verify that the persistent cache had been updated properly 0269 sync.setHighestModSeq(36); 0270 QCOMPARE(model->cache()->mailboxSyncState("a"), sync); 0271 QCOMPARE(static_cast<int>(model->cache()->mailboxSyncState("a").exists()), 0272 uidMap.size()); 0273 QCOMPARE(model->cache()->uidMapping("a"), uidMap); 0274 QCOMPARE(model->cache()->msgFlags("a", 6), 0275 QStringList() << "x"); 0276 QCOMPARE(model->cache()->msgFlags("a", 9), 0277 QStringList() << "\\Seen" << "x2"); 0278 QCOMPARE(model->cache()->msgFlags("a", 10), 0279 QStringList() << "z"); 0280 0281 // Make sure that we've picked up the changed flag 0282 QCOMPARE(idxA.data(Imap::Mailbox::RoleUnreadMessageCount).toInt(), 2); 0283 0284 // Nothing else should be running 0285 justKeepTask(); 0286 } 0287 \end{minted} 0288 0289 Trojitá includes many of these individual test cases covering both regular operation as well as several pathological 0290 scenarios which would be tricky to hit in the real world. In addition to that, the automated tests also verify speed of 0291 mailbox synchronization with folders containing hundreds of thousands of messages, a scenario which might be rare in 0292 practice~\footnote{The {\em Seznam.cz}, the most popular Czech free e-mail provider, imposes a limit of just 30,000 0293 messages per account as of early 2012.} but extremely useful for catching programmer mistakes like proposing an $O(n^2)$ 0294 algorithm~\footnote{It was necessary to locate a message in the mailbox by its UID. A conventional binary search 0295 algorithm was not applicable because the search could encounter ``message placeholders'' whose UID was not know, and 0296 therefore could not be compared to the reference value. In the end, measurements have shown that starting with binary 0297 search and falling back on a linear scan later in the process upon encountering the first undefined value works fast 0298 enough in all tested circumstances.} over an $O(n \log n)$ one --- in spite of the fact that the overhead of the testing 0299 framework, especially when working with huge data transfers, as is indeed the case when synchronizing such a mailbox, is 0300 comparable to the actual code being profiled. 0301 0302 \subsection{Scalability} 0303 0304 I'm regularly testing Trojitá on mailboxes with hundreds of thousands of messages. Trojitá's unique design which 0305 guarantees that only the {\em required} data is transferred is paramount in achieving (and retaining) reasonable 0306 performance under varying workloads. 0307 0308 Trojitá respects all relevant recommendations about incremental retrieval of data. Unless in the so called ``expensive 0309 mode'' where Trojitá tries to save bandwidth at the cost of increased waiting time, an intelligent preload is 0310 implemented which will seamlessly ask for data which is likely to be required in future in a background preload 0311 operation. 0312 0313 \subsection{Command Pipelining} 0314 0315 Whenever possible, Trojitá issues the IMAP commands in parallel.~\footnote{The parallelization imposes a configurable 0316 limit on the number of concurrently active tasks so as not to overload the server.} Unfortunately, many expensive 0317 operations invoked on the server side often requires mailbox-wide locks, at least in the current server's 0318 implementations. An example of such behavior are Dovecot's adaptive indexes \cite{dovecot-adaptive-indexes} which are 0319 created on-demand, in response to actual client's requests. When Trojitá asks for threading on a big mailbox for the 0320 first time, Dovecot will block handling the threading request and building the required index, not responding to a query 0321 for metadata of messages, even though Trojitá has already issued a command requesting their delivery. 0322 0323 Under normal circumstances, though, pipelining is crucial for decent performance in presence of excess network round 0324 trip times, and this parallel delivery of commands works extremely well even in case where the servers are unable to 0325 fulfill the requests in parallel. 0326 0327 \subsection{Low-level optimization} 0328 0329 Performance measurements were undertaken using Valgrind's \cite{valgrind} {\tt callgrind} \cite{valgrind-callgrind} tool 0330 while memory usage was tracked using Valgrind's {\tt massif} \cite{valgrind-massif}. It is hard to compare existing 0331 clients based on the ``consumed memory'' alone, especially when considering that certain desktop clients like KDE's 0332 Akonadi-based \cite{akonadi} products are split into several processes, many of which are handling both IMAP-related and 0333 unrelated operations. The memory profiling was therefore concentrated on making Trojitá's memory usage ``reasonable'' 0334 and for checking of obvious regressions. 0335 0336 Some of the results I have observed when profiling the code were initially rather surprising --- for example, replacing 0337 a {\tt QStringList} implementation by a {\tt QSet<QString>} one actually {\em increased} the memory usage from 296~MB in 0338 a synthetic benchmark~\footnote{The test used was {\tt ImapModelObtainSynchronizedMailboxTest::testFlagReSyncBenchmark} 0339 as of commit {\tt cf92f5f9f8e929e1427877c0db470d8c59651d3b} (March 2012) with the number of messages in mailbox being 0340 increased to half a million. Measurements were performed on an {\tt x86\_64} Gentoo Linux machine running Qt~4.7.1.} to 0341 385~MB, i.e. by roughly one third --- a result caused by the {\tt QSet}'s hash table memory overhead. 0342 0343 In the end, the total use in the mentioned benchmark was reduced to just 186~MB by explicit use of {\tt QString}'s 0344 implicit sharing where the actual text data are saved in memory only once, using {\tt QString}'s native reference 0345 counting. The results could be likely improved by moving the data sharing one level up to the level of the whole {\tt 0346 QStringList} instead of the underlying elements. 0347 0348 More drastic reductions could be obtained by actively removing unused data from memory and reducing the amount of empty 0349 placeholders being initialized to ``null'' values in the {\tt TreeItemMessage} instances, or, potentially, deferring 0350 their instantiations indefinitely till the corresponding messages come to the widget's viewport. However, no compelling 0351 reason to do so on the target platforms was observed so far. 0352 0353 \end{document}