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