1 /*
2 * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 * for more details.
13 */
14
15 #include "progressdispatcher.h"
16
17 #include <QObject>
18 #include <QMetaType>
19 #include <QCoreApplication>
20
21 namespace OCC {
22
23 ProgressDispatcher *ProgressDispatcher::_instance = nullptr;
24
asResultString(const SyncFileItem & item)25 QString Progress::asResultString(const SyncFileItem &item)
26 {
27 switch (item._instruction) {
28 case CSYNC_INSTRUCTION_SYNC:
29 case CSYNC_INSTRUCTION_NEW:
30 case CSYNC_INSTRUCTION_TYPE_CHANGE:
31 if (item._direction != SyncFileItem::Up) {
32 if (item._type == ItemTypeVirtualFile) {
33 return QCoreApplication::translate("progress", "Virtual file created");
34 } else if (item._type == ItemTypeVirtualFileDehydration) {
35 return QCoreApplication::translate("progress", "Replaced by virtual file");
36 } else {
37 return QCoreApplication::translate("progress", "Downloaded");
38 }
39 } else {
40 return QCoreApplication::translate("progress", "Uploaded");
41 }
42 case CSYNC_INSTRUCTION_CONFLICT:
43 return QCoreApplication::translate("progress", "Server version downloaded, copied changed local file into conflict file");
44 case CSYNC_INSTRUCTION_REMOVE:
45 return QCoreApplication::translate("progress", "Deleted");
46 case CSYNC_INSTRUCTION_EVAL_RENAME:
47 case CSYNC_INSTRUCTION_RENAME:
48 return QCoreApplication::translate("progress", "Moved to %1").arg(item._renameTarget);
49 case CSYNC_INSTRUCTION_IGNORE:
50 return QCoreApplication::translate("progress", "Ignored");
51 case CSYNC_INSTRUCTION_STAT_ERROR:
52 return QCoreApplication::translate("progress", "Filesystem access error");
53 case CSYNC_INSTRUCTION_ERROR:
54 return QCoreApplication::translate("progress", "Error");
55 case CSYNC_INSTRUCTION_UPDATE_METADATA:
56 return QCoreApplication::translate("progress", "Updated local metadata");
57 case CSYNC_INSTRUCTION_NONE:
58 case CSYNC_INSTRUCTION_EVAL:
59 return QCoreApplication::translate("progress", "Unknown");
60 }
61 return QCoreApplication::translate("progress", "Unknown");
62 }
63
asActionString(const SyncFileItem & item)64 QString Progress::asActionString(const SyncFileItem &item)
65 {
66 switch (item._instruction) {
67 case CSYNC_INSTRUCTION_CONFLICT:
68 case CSYNC_INSTRUCTION_SYNC:
69 case CSYNC_INSTRUCTION_NEW:
70 case CSYNC_INSTRUCTION_TYPE_CHANGE:
71 if (item._direction != SyncFileItem::Up)
72 return QCoreApplication::translate("progress", "downloading");
73 else
74 return QCoreApplication::translate("progress", "uploading");
75 case CSYNC_INSTRUCTION_REMOVE:
76 return QCoreApplication::translate("progress", "deleting");
77 case CSYNC_INSTRUCTION_EVAL_RENAME:
78 case CSYNC_INSTRUCTION_RENAME:
79 return QCoreApplication::translate("progress", "moving");
80 case CSYNC_INSTRUCTION_IGNORE:
81 return QCoreApplication::translate("progress", "ignoring");
82 case CSYNC_INSTRUCTION_STAT_ERROR:
83 return QCoreApplication::translate("progress", "error");
84 case CSYNC_INSTRUCTION_ERROR:
85 return QCoreApplication::translate("progress", "error");
86 case CSYNC_INSTRUCTION_UPDATE_METADATA:
87 return QCoreApplication::translate("progress", "updating local metadata");
88 case CSYNC_INSTRUCTION_NONE:
89 case CSYNC_INSTRUCTION_EVAL:
90 break;
91 }
92 return QString();
93 }
94
isWarningKind(SyncFileItem::Status kind)95 bool Progress::isWarningKind(SyncFileItem::Status kind)
96 {
97 return kind == SyncFileItem::SoftError || kind == SyncFileItem::NormalError
98 || kind == SyncFileItem::FatalError || kind == SyncFileItem::FileIgnored
99 || kind == SyncFileItem::Conflict || kind == SyncFileItem::Restoration
100 || kind == SyncFileItem::DetailError || kind == SyncFileItem::BlacklistedError;
101 }
102
isIgnoredKind(SyncFileItem::Status kind)103 bool Progress::isIgnoredKind(SyncFileItem::Status kind)
104 {
105 return kind == SyncFileItem::FileIgnored;
106 }
107
instance()108 ProgressDispatcher *ProgressDispatcher::instance()
109 {
110 if (!_instance) {
111 _instance = new ProgressDispatcher();
112 }
113 return _instance;
114 }
115
ProgressDispatcher(QObject * parent)116 ProgressDispatcher::ProgressDispatcher(QObject *parent)
117 : QObject(parent)
118 {
119 }
120
~ProgressDispatcher()121 ProgressDispatcher::~ProgressDispatcher()
122 {
123 }
124
ProgressInfo()125 ProgressInfo::ProgressInfo()
126 {
127 connect(&_updateEstimatesTimer, &QTimer::timeout, this, &ProgressInfo::updateEstimates);
128 reset();
129 }
130
reset()131 void ProgressInfo::reset()
132 {
133 _status = Starting;
134
135 _currentItems.clear();
136 _currentDiscoveredRemoteFolder.clear();
137 _currentDiscoveredLocalFolder.clear();
138 _sizeProgress = Progress();
139 _fileProgress = Progress();
140 _totalSizeOfCompletedJobs = 0;
141
142 // Historically, these starting estimates were way lower, but that lead
143 // to gross overestimation of ETA when a good estimate wasn't available.
144 _maxBytesPerSecond = 2000000.0; // 2 MB/s
145 _maxFilesPerSecond = 10.0;
146
147 _updateEstimatesTimer.stop();
148 _lastCompletedItem = SyncFileItem();
149 }
150
status() const151 ProgressInfo::Status ProgressInfo::status() const
152 {
153 return _status;
154 }
155
startEstimateUpdates()156 void ProgressInfo::startEstimateUpdates()
157 {
158 _updateEstimatesTimer.start(1000);
159 }
160
isUpdatingEstimates() const161 bool ProgressInfo::isUpdatingEstimates() const
162 {
163 return _updateEstimatesTimer.isActive();
164 }
165
shouldCountProgress(const SyncFileItem & item)166 static bool shouldCountProgress(const SyncFileItem &item)
167 {
168 const auto instruction = item._instruction;
169
170 // Skip any ignored, error or non-propagated files and directories.
171 if (instruction == CSYNC_INSTRUCTION_NONE
172 || instruction == CSYNC_INSTRUCTION_UPDATE_METADATA
173 || instruction == CSYNC_INSTRUCTION_IGNORE
174 || instruction == CSYNC_INSTRUCTION_ERROR) {
175 return false;
176 }
177
178 return true;
179 }
180
adjustTotalsForFile(const SyncFileItem & item)181 void ProgressInfo::adjustTotalsForFile(const SyncFileItem &item)
182 {
183 if (!shouldCountProgress(item)) {
184 return;
185 }
186
187 _fileProgress._total += item._affectedItems;
188 if (isSizeDependent(item)) {
189 _sizeProgress._total += item._size;
190 }
191 }
192
updateTotalsForFile(const SyncFileItem & item,qint64 newSize)193 void ProgressInfo::updateTotalsForFile(const SyncFileItem &item, qint64 newSize)
194 {
195 if (!shouldCountProgress(item)) {
196 return;
197 }
198
199 if (!_currentItems.contains(item._file)) {
200 _sizeProgress._total += newSize - item._size;
201 } else {
202 _sizeProgress._total += newSize - _currentItems[item._file]._progress._total;
203 }
204
205 setProgressItem(item, 0);
206 _currentItems[item._file]._progress._total = newSize;
207 }
208
totalFiles() const209 qint64 ProgressInfo::totalFiles() const
210 {
211 return _fileProgress._total;
212 }
213
completedFiles() const214 qint64 ProgressInfo::completedFiles() const
215 {
216 return _fileProgress._completed;
217 }
218
currentFile() const219 qint64 ProgressInfo::currentFile() const
220 {
221 return completedFiles() + _currentItems.size();
222 }
223
totalSize() const224 qint64 ProgressInfo::totalSize() const
225 {
226 return _sizeProgress._total;
227 }
228
completedSize() const229 qint64 ProgressInfo::completedSize() const
230 {
231 return _sizeProgress._completed;
232 }
233
setProgressComplete(const SyncFileItem & item)234 void ProgressInfo::setProgressComplete(const SyncFileItem &item)
235 {
236 if (!shouldCountProgress(item)) {
237 return;
238 }
239
240 _fileProgress.setCompleted(_fileProgress._completed + item._affectedItems);
241 if (ProgressInfo::isSizeDependent(item)) {
242 _totalSizeOfCompletedJobs += _currentItems[item._file]._progress._total;
243 }
244 _currentItems.remove(item._file);
245 recomputeCompletedSize();
246 _lastCompletedItem = item;
247 }
248
setProgressItem(const SyncFileItem & item,qint64 completed)249 void ProgressInfo::setProgressItem(const SyncFileItem &item, qint64 completed)
250 {
251 if (!shouldCountProgress(item)) {
252 return;
253 }
254
255 if (!_currentItems.contains(item._file)) {
256 _currentItems[item._file]._item = item;
257 _currentItems[item._file]._progress._total = item._size;
258 }
259 _currentItems[item._file]._progress.setCompleted(completed);
260 recomputeCompletedSize();
261
262 // This seems dubious!
263 _lastCompletedItem = SyncFileItem();
264 }
265
totalProgress() const266 ProgressInfo::Estimates ProgressInfo::totalProgress() const
267 {
268 Estimates file = _fileProgress.estimates();
269 if (_sizeProgress._total == 0) {
270 return file;
271 }
272
273 Estimates size = _sizeProgress.estimates();
274
275 // Ideally the remaining time would be modeled as:
276 // remaning_file_sizes / transfer_speed
277 // + remaining_file_count * per_file_overhead
278 // + remaining_chunked_file_sizes / chunked_reassembly_speed
279 // with us estimating the three parameters in conjunction.
280 //
281 // But we currently only model the bandwidth and the files per
282 // second independently, which leads to incorrect values. To slightly
283 // mitigate this problem, we combine the two models depending on
284 // which factor dominates (essentially big-file-upload vs.
285 // many-small-files)
286 //
287 // If we have size information, we prefer an estimate based
288 // on the upload speed. That's particularly relevant for large file
289 // up/downloads, where files per second will be close to 0.
290 //
291 // However, when many *small* files are transfered, the estimate
292 // can become very pessimistic as the transfered amount per second
293 // drops significantly.
294 //
295 // So, if we detect a high rate of files per second or a very low
296 // transfer rate (often drops hugely during a sequence of deletes,
297 // for instance), we gradually prefer an optimistic estimate and
298 // assume the remaining transfer will be done with the highest speed
299 // we've seen.
300
301 // Compute a value that is 0 when fps is <=L*max and 1 when fps is >=U*max
302 double fps = _fileProgress._progressPerSec;
303 double fpsL = 0.5;
304 double fpsU = 0.8;
305 double nearMaxFps =
306 qBound(0.0,
307 (fps - fpsL * _maxFilesPerSecond) / ((fpsU - fpsL) * _maxFilesPerSecond),
308 1.0);
309
310 // Compute a value that is 0 when transfer is >= U*max and
311 // 1 when transfer is <= L*max
312 double trans = _sizeProgress._progressPerSec;
313 double transU = 0.1;
314 double transL = 0.01;
315 double slowTransfer = 1.0 - qBound(0.0,
316 (trans - transL * _maxBytesPerSecond) / ((transU - transL) * _maxBytesPerSecond),
317 1.0);
318
319 double beOptimistic = nearMaxFps * slowTransfer;
320 size.estimatedEta = quint64((1.0 - beOptimistic) * size.estimatedEta
321 + beOptimistic * optimisticEta());
322
323 return size;
324 }
325
optimisticEta() const326 quint64 ProgressInfo::optimisticEta() const
327 {
328 // This assumes files and transfers finish as quickly as possible
329 // *but* note that maxPerSecond could be serious underestimate
330 // (if we never got to fully excercise transfer or files/second)
331
332 return _fileProgress.remaining() / _maxFilesPerSecond * 1000
333 + _sizeProgress.remaining() / _maxBytesPerSecond * 1000;
334 }
335
trustEta() const336 bool ProgressInfo::trustEta() const
337 {
338 return totalProgress().estimatedEta < 100 * optimisticEta();
339 }
340
fileProgress(const SyncFileItem & item) const341 ProgressInfo::Estimates ProgressInfo::fileProgress(const SyncFileItem &item) const
342 {
343 return _currentItems[item._file]._progress.estimates();
344 }
345
updateEstimates()346 void ProgressInfo::updateEstimates()
347 {
348 _sizeProgress.update();
349 _fileProgress.update();
350
351 // Update progress of all running items.
352 QMutableHashIterator<QString, ProgressItem> it(_currentItems);
353 while (it.hasNext()) {
354 it.next();
355 it.value()._progress.update();
356 }
357
358 _maxFilesPerSecond = qMax(_fileProgress._progressPerSec,
359 _maxFilesPerSecond);
360 _maxBytesPerSecond = qMax(_sizeProgress._progressPerSec,
361 _maxBytesPerSecond);
362 }
363
recomputeCompletedSize()364 void ProgressInfo::recomputeCompletedSize()
365 {
366 qint64 r = _totalSizeOfCompletedJobs;
367 foreach (const ProgressItem &i, _currentItems) {
368 if (isSizeDependent(i._item))
369 r += i._progress._completed;
370 }
371 _sizeProgress.setCompleted(r);
372 }
373
estimates() const374 ProgressInfo::Estimates ProgressInfo::Progress::estimates() const
375 {
376 Estimates est;
377 est.estimatedBandwidth = qint64(_progressPerSec);
378 if (_progressPerSec != 0.0) {
379 est.estimatedEta = quint64((_total - _completed) / _progressPerSec * 1000.0);
380 } else {
381 est.estimatedEta = 0; // looks better than qint64 max
382 }
383 return est;
384 }
385
completed() const386 qint64 ProgressInfo::Progress::completed() const
387 {
388 return _completed;
389 }
390
remaining() const391 qint64 ProgressInfo::Progress::remaining() const
392 {
393 return _total - _completed;
394 }
395
update()396 void ProgressInfo::Progress::update()
397 {
398 // A good way to think about the smoothing factor:
399 // If we make progress P per sec and then stop making progress at all,
400 // after N calls to this function (and thus seconds) the _progressPerSec
401 // will have reduced to P*smoothing^N.
402 // With a value of 0.9, only 4% of the original value is left after 30s
403 //
404 // In the first few updates we want to go to the correct value quickly.
405 // Therefore, smoothing starts at 0 and ramps up to its final value over time.
406 const double smoothing = 0.9 * (1.0 - _initialSmoothing);
407 _initialSmoothing *= 0.7; // goes from 1 to 0.03 in 10s
408 _progressPerSec = smoothing * _progressPerSec + (1.0 - smoothing) * (_completed - _prevCompleted);
409 _prevCompleted = _completed;
410 }
411
setCompleted(qint64 completed)412 void ProgressInfo::Progress::setCompleted(qint64 completed)
413 {
414 _completed = qMin(completed, _total);
415 _prevCompleted = qMin(_prevCompleted, _completed);
416 }
417 }
418