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