1 // Copyright (C) 2014-2018 Manuel Schneider
2 
3 #include <QApplication>
4 #include <QDebug>
5 #include <QSettings>
6 #include <QSqlDatabase>
7 #include <QSqlDriver>
8 #include <QSqlError>
9 #include <QSqlQuery>
10 #include <QSqlRecord>
11 #include <QStandardPaths>
12 #include <chrono>
13 #include <vector>
14 #include "albert/extension.h"
15 #include "albert/fallbackprovider.h"
16 #include "albert/item.h"
17 #include "albert/queryhandler.h"
18 #include "extensionmanager.h"
19 #include "matchcompare.h"
20 #include "queryexecution.h"
21 #include "querymanager.h"
22 using namespace Core;
23 using namespace std;
24 using namespace std::chrono;
25 
26 namespace {
27 const char* CFG_INCREMENTAL_SORT = "incrementalSort";
28 const bool  DEF_INCREMENTAL_SORT = false;
29 }
30 
31 /** ***************************************************************************/
QueryManager(ExtensionManager * em,QObject * parent)32 Core::QueryManager::QueryManager(ExtensionManager* em, QObject *parent)
33     : QObject(parent),
34       extensionManager_(em) {
35 
36     QSqlQuery q;
37 
38     // Get last query id
39     lastQueryId_ = 0;
40     q.prepare("SELECT MAX(id) FROM query;");
41     if (!q.exec())
42         qFatal("SQL ERROR: %s %s", qPrintable(q.executedQuery()), qPrintable(q.lastError().text()));
43     if (q.next())
44         lastQueryId_ = q.value(0).toULongLong();
45 
46     // Get the handlers Ids
47     q.exec("SELECT string_id, id FROM query_handler;");
48     if (!q.exec())
49         qFatal("SQL ERROR: %s %s", qPrintable(q.executedQuery()), qPrintable(q.lastError().text()));
50     while(q.next())
51         handlerIds_.emplace(q.value(0).toString(), q.value(1).toULongLong());
52 
53     // Initialize the order
54     updateScores();
55 
56     QSettings s(qApp->applicationName());
57     incrementalSort_ = s.value(CFG_INCREMENTAL_SORT, DEF_INCREMENTAL_SORT).toBool();
58 }
59 
60 
61 /** ***************************************************************************/
~QueryManager()62 QueryManager::~QueryManager() {
63 
64 }
65 
66 
67 /** ***************************************************************************/
setupSession()68 void Core::QueryManager::setupSession() {
69 
70     qDebug() << "========== SESSION SETUP STARTED ==========";
71 
72     system_clock::time_point start = system_clock::now();
73 
74     // Call all setup routines
75     for (Core::QueryHandler *handler : extensionManager_->queryHandlers()) {
76         system_clock::time_point start = system_clock::now();
77         handler->setupSession();
78         long duration = duration_cast<microseconds>(system_clock::now()-start).count();
79         qDebug() << qPrintable(QString("TIME: %1 µs SESSION SETUP [%2]").arg(duration, 6).arg(handler->id));
80     }
81 
82     long duration = duration_cast<microseconds>(system_clock::now()-start).count();
83     qDebug() << qPrintable(QString("TIME: %1 µs SESSION SETUP OVERALL").arg(duration, 6));
84 }
85 
86 
87 /** ***************************************************************************/
teardownSession()88 void Core::QueryManager::teardownSession() {
89 
90     qDebug() << "========== SESSION TEARDOWN STARTED ==========";
91 
92     system_clock::time_point start = system_clock::now();
93 
94     // Call all teardown routines
95     for (Core::QueryHandler *handler : extensionManager_->queryHandlers()) {
96         system_clock::time_point start = system_clock::now();
97         handler->teardownSession();
98         long duration = duration_cast<microseconds>(system_clock::now()-start).count();
99         qDebug() << qPrintable(QString("TIME: %1 µs SESSION TEARDOWN [%2]").arg(duration, 6).arg(handler->id));
100     }
101 
102     // Clear views
103     emit resultsReady(nullptr);
104 
105     // Store statistics
106     QSqlDatabase db = QSqlDatabase::database();
107     db.transaction();
108     QSqlQuery query(db);
109     for ( QueryExecution *queryExecution : pastQueries_ ){
110 
111         ++lastQueryId_;
112         const QueryStatistics &stats = queryExecution->stats;
113 
114         // Create a query record
115         query.prepare("INSERT INTO query (id, input, cancelled, runtime, timestamp) "
116                       "VALUES (:id, :input, :cancelled, :runtime, :timestamp);");
117         query.bindValue(":id", lastQueryId_);
118         query.bindValue(":input", stats.input);
119         query.bindValue(":cancelled", stats.cancelled);
120         query.bindValue(":runtime", static_cast<qulonglong>(duration_cast<microseconds>(stats.end-stats.start).count()));
121         query.bindValue(":timestamp", static_cast<qulonglong>(duration_cast<seconds>(stats.start.time_since_epoch()).count()));
122         if (!query.exec())
123             qFatal("SQL ERROR: %s", qPrintable(query.lastError().text()));
124 
125         // Make sure all handlers exits in database
126         query.prepare("INSERT INTO query_handler (string_id) VALUES (:id);");
127         for ( auto & runtime : stats.runtimes ) {
128             auto it = handlerIds_.find(runtime.first);
129             if ( it == handlerIds_.end()){
130                 query.bindValue(":id", runtime.first);
131                 if (!query.exec())
132                     qFatal("SQL ERROR: %s %s", qPrintable(query.executedQuery()), qPrintable(query.lastError().text()));
133                 handlerIds_.emplace(runtime.first, query.lastInsertId().toULongLong());
134             }
135         }
136 
137         // Create execution records
138         query.prepare("INSERT INTO execution (query_id, handler_id, runtime) "
139                       "VALUES (:query_id, :handler_id, :runtime);");
140         for ( auto & runtime : stats.runtimes ) {
141             query.bindValue(":query_id", lastQueryId_);
142             query.bindValue(":handler_id", handlerIds_[runtime.first]);
143             query.bindValue(":runtime", runtime.second);
144             if (!query.exec())
145                 qFatal("SQL ERROR: %s %s", qPrintable(query.executedQuery()), qPrintable(query.lastError().text()));
146         }
147 
148         // Create activation record
149         if (!stats.activatedItem.isNull()) {
150             query.prepare("INSERT INTO activation (query_id, item_id) VALUES (:query_id, :item_id);");
151             query.bindValue(":query_id", lastQueryId_);
152             query.bindValue(":item_id", stats.activatedItem);
153             if (!query.exec())
154                 qFatal("SQL ERROR: %s %s", qPrintable(query.executedQuery()), qPrintable(query.lastError().text()));
155         }
156     }
157     db.commit();
158 
159     // Delete queries
160     for ( QueryExecution *query : pastQueries_ )
161         if ( query->state() == QueryExecution::State::Running )
162             connect(query, &QueryExecution::stateChanged,
163                     query, [query](){ query->deleteLater(); });
164         else
165             delete query;
166     pastQueries_.clear();
167 
168     // Compute new match rankings
169     updateScores();
170 
171     long duration = duration_cast<microseconds>(system_clock::now()-start).count();
172     qDebug() << qPrintable(QString("TIME: %1 µs SESSION TEARDOWN OVERALL").arg(duration, 6));
173 }
174 
175 
176 /** ***************************************************************************/
startQuery(const QString & searchTerm)177 void Core::QueryManager::startQuery(const QString &searchTerm) {
178 
179     qDebug() << "========== QUERY:" << searchTerm << " ==========";
180 
181     if ( pastQueries_.size() ) {
182         // Stop last query
183         QueryExecution *last = pastQueries_.back();
184         disconnect(last, &QueryExecution::resultsReady, this, &QueryManager::resultsReady);
185         if (last->state() != QueryExecution::State::Finished)
186             last->cancel();
187     }
188 
189     system_clock::time_point start = system_clock::now();
190 
191     // Start query
192     QueryExecution *currentQuery = new QueryExecution(extensionManager_->queryHandlers(),
193                                                       extensionManager_->fallbackProviders(),
194                                                       searchTerm,
195                                                       scores_,
196                                                       incrementalSort_);
197     connect(currentQuery, &QueryExecution::resultsReady, this, &QueryManager::resultsReady);
198     currentQuery->run();
199 
200     connect(currentQuery, &QueryExecution::stateChanged, [start](QueryExecution::State state){
201         if ( state == QueryExecution::State::Finished ) {
202             long duration = duration_cast<microseconds>(system_clock::now()-start).count();
203             qDebug() << qPrintable(QString("TIME: %1 µs QUERY OVERALL").arg(duration, 6));
204         }
205     });
206 
207     pastQueries_.emplace_back(currentQuery);
208 
209     long duration = duration_cast<microseconds>(system_clock::now()-start).count();
210     qDebug() << qPrintable(QString("TIME: %1 µs SESSION TEARDOWN OVERALL").arg(duration, 6));
211 }
212 
213 
214 /** ***************************************************************************/
incrementalSort()215 bool QueryManager::incrementalSort(){
216     return incrementalSort_;
217 }
218 
219 
220 /** ***************************************************************************/
setIncrementalSort(bool value)221 void QueryManager::setIncrementalSort(bool value){
222     QSettings(qApp->applicationName()).setValue(CFG_INCREMENTAL_SORT, value);
223     incrementalSort_ = value;
224 }
225 
226 
227 /** ***************************************************************************
228  * @brief Core::MatchCompare::update
229  * Update the usage score:
230  * Score of a single usage is 1/(<age_in_days>+1).
231  * Accumulate all scores groupes by itemId.
232  * Normalize the scores to the range of UINT_MAX.
233  */
updateScores()234 void QueryManager::updateScores()
235 {
236     scores_.clear();
237     QSqlQuery query("SELECT a.item_id AS id, SUM(1/(julianday('now')-julianday(timestamp, 'unixepoch')+1)) AS score "
238                     "FROM activation a JOIN  query q ON a.query_id = q.id "
239                     "WHERE a.item_id<>'' "
240                     "GROUP BY a.item_id "
241                     "ORDER BY score DESC");
242     if ( query.next() ){
243         double max = query.value(1).toDouble();
244         do {
245             scores_.emplace(query.value(0).toString(), static_cast<uint>(query.value(1).toDouble()*UINT_MAX/max));
246         } while (query.next());
247     }
248 }
249