nextcloud-desktop/src/libsync/progressdispatcher.cpp

404 lines
12 KiB
C++
Raw Normal View History

/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "progressdispatcher.h"
#include <QObject>
#include <QMetaType>
#include <QCoreApplication>
2014-11-10 00:34:07 +03:00
namespace OCC {
ProgressDispatcher *ProgressDispatcher::_instance = nullptr;
2017-05-17 11:55:42 +03:00
QString Progress::asResultString(const SyncFileItem &item)
{
2017-05-17 11:55:42 +03:00
switch (item._instruction) {
case CSYNC_INSTRUCTION_SYNC:
case CSYNC_INSTRUCTION_NEW:
case CSYNC_INSTRUCTION_TYPE_CHANGE:
if (item._direction != SyncFileItem::Up) {
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "Downloaded");
} else {
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "Uploaded");
}
case CSYNC_INSTRUCTION_CONFLICT:
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "Server version downloaded, copied changed local file into conflict file");
case CSYNC_INSTRUCTION_REMOVE:
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "Deleted");
case CSYNC_INSTRUCTION_EVAL_RENAME:
case CSYNC_INSTRUCTION_RENAME:
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "Moved to %1").arg(item._renameTarget);
case CSYNC_INSTRUCTION_IGNORE:
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "Ignored");
case CSYNC_INSTRUCTION_STAT_ERROR:
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "Filesystem access error");
case CSYNC_INSTRUCTION_ERROR:
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "Error");
csync: Use an explicit instruction for should_update_metadata The current way of tracking the need to update the metadata without propagation using a separate flag makes it difficult to track priorities between the local and remote tree. The logic is also difficult to logically cover since the possibilities matrix isn't 100% covered, leaving the flag only used in a few situations (mostly involving folders, but not only). The reason we need to change this is to be able to track the sync state of files for overlay icons. The instruction alone can't be used since CSYNC_INSTRUCTION_SYNC is used for folders even though they won't be propagated. Removing this logic is however not possible without using something else than CSYNC_INSTRUCTION_NONE since too many codepath interpret (rightfully) this as meaning "nothing to do". This patch adds a new CSYNC_INSTRUCTION_UPDATE_METADATA instruction to let the update and reconcile steps tell the SyncEngine to update the metadata of a file without any propagation. Other flags are left to be interpretted by the implementation as implicitly needing metadata update or not, as this was already the case for most file propagation jobs. For example, CSYNC_INSTRUCTION_NEW for directories now also implicitly update the metadata. Since it's not impossible for folders to emit CSYNC_INSTRUCTION_SYNC or CSYNC_INSTRUCTION_CONFLICT, the corresponding code paths in the sync engine have been removed. Since the reconcile step can now know if the local tree needs metadata update while the remote side might want propagation, the localMetadataUpdate logic in SyncEngine::treewalkFile now simply use a CSYNC_INSTRUCTION_UPDATE_METADATA for the local side, which is now implemented as a different database query.
2016-08-15 15:17:51 +03:00
case CSYNC_INSTRUCTION_UPDATE_METADATA:
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "Updated local metadata");
case CSYNC_INSTRUCTION_NONE:
case CSYNC_INSTRUCTION_EVAL:
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "Unknown");
}
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "Unknown");
}
2017-05-17 11:55:42 +03:00
QString Progress::asActionString(const SyncFileItem &item)
{
2017-05-17 11:55:42 +03:00
switch (item._instruction) {
case CSYNC_INSTRUCTION_CONFLICT:
case CSYNC_INSTRUCTION_SYNC:
case CSYNC_INSTRUCTION_NEW:
case CSYNC_INSTRUCTION_TYPE_CHANGE:
if (item._direction != SyncFileItem::Up)
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "downloading");
else
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "uploading");
case CSYNC_INSTRUCTION_REMOVE:
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "deleting");
case CSYNC_INSTRUCTION_EVAL_RENAME:
case CSYNC_INSTRUCTION_RENAME:
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "moving");
case CSYNC_INSTRUCTION_IGNORE:
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "ignoring");
case CSYNC_INSTRUCTION_STAT_ERROR:
case CSYNC_INSTRUCTION_ERROR:
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "error");
csync: Use an explicit instruction for should_update_metadata The current way of tracking the need to update the metadata without propagation using a separate flag makes it difficult to track priorities between the local and remote tree. The logic is also difficult to logically cover since the possibilities matrix isn't 100% covered, leaving the flag only used in a few situations (mostly involving folders, but not only). The reason we need to change this is to be able to track the sync state of files for overlay icons. The instruction alone can't be used since CSYNC_INSTRUCTION_SYNC is used for folders even though they won't be propagated. Removing this logic is however not possible without using something else than CSYNC_INSTRUCTION_NONE since too many codepath interpret (rightfully) this as meaning "nothing to do". This patch adds a new CSYNC_INSTRUCTION_UPDATE_METADATA instruction to let the update and reconcile steps tell the SyncEngine to update the metadata of a file without any propagation. Other flags are left to be interpretted by the implementation as implicitly needing metadata update or not, as this was already the case for most file propagation jobs. For example, CSYNC_INSTRUCTION_NEW for directories now also implicitly update the metadata. Since it's not impossible for folders to emit CSYNC_INSTRUCTION_SYNC or CSYNC_INSTRUCTION_CONFLICT, the corresponding code paths in the sync engine have been removed. Since the reconcile step can now know if the local tree needs metadata update while the remote side might want propagation, the localMetadataUpdate logic in SyncEngine::treewalkFile now simply use a CSYNC_INSTRUCTION_UPDATE_METADATA for the local side, which is now implemented as a different database query.
2016-08-15 15:17:51 +03:00
case CSYNC_INSTRUCTION_UPDATE_METADATA:
2017-05-17 11:55:42 +03:00
return QCoreApplication::translate("progress", "updating local metadata");
case CSYNC_INSTRUCTION_NONE:
case CSYNC_INSTRUCTION_EVAL:
break;
}
return QString();
}
2017-05-17 11:55:42 +03:00
bool Progress::isWarningKind(SyncFileItem::Status kind)
{
2017-05-17 11:55:42 +03:00
return kind == SyncFileItem::SoftError || kind == SyncFileItem::NormalError
|| kind == SyncFileItem::FatalError || kind == SyncFileItem::FileIgnored
|| kind == SyncFileItem::Conflict || kind == SyncFileItem::Restoration
|| kind == SyncFileItem::DetailError || kind == SyncFileItem::BlacklistedError
|| kind == SyncFileItem::FileLocked;
}
2017-05-17 11:55:42 +03:00
bool Progress::isIgnoredKind(SyncFileItem::Status kind)
{
2017-05-17 11:55:42 +03:00
return kind == SyncFileItem::FileIgnored;
}
2017-05-17 11:55:42 +03:00
ProgressDispatcher *ProgressDispatcher::instance()
{
if (!_instance) {
_instance = new ProgressDispatcher();
}
return _instance;
}
2017-05-17 11:55:42 +03:00
ProgressDispatcher::ProgressDispatcher(QObject *parent)
: QObject(parent)
{
}
ProgressDispatcher::~ProgressDispatcher() = default;
2017-05-17 11:55:42 +03:00
void ProgressDispatcher::setProgressInfo(const QString &folder, const ProgressInfo &progress)
{
2017-05-17 11:55:42 +03:00
if (folder.isEmpty())
// The update phase now also has progress
// (progress._currentItems.size() == 0
// && progress._totalFileCount == 0) )
{
return;
}
2017-05-17 11:55:42 +03:00
emit progressInfo(folder, progress);
}
ProgressInfo::ProgressInfo()
{
connect(&_updateEstimatesTimer, &QTimer::timeout, this, &ProgressInfo::updateEstimates);
reset();
}
void ProgressInfo::reset()
{
_status = Starting;
_currentItems.clear();
_currentDiscoveredRemoteFolder.clear();
_currentDiscoveredLocalFolder.clear();
_sizeProgress = Progress();
_fileProgress = Progress();
_totalSizeOfCompletedJobs = 0;
// Historically, these starting estimates were way lower, but that lead
// to gross overestimation of ETA when a good estimate wasn't available.
_maxBytesPerSecond = 2000000.0; // 2 MB/s
_maxFilesPerSecond = 10.0;
_updateEstimatesTimer.stop();
_lastCompletedItem = SyncFileItem();
}
ProgressInfo::Status ProgressInfo::status() const
{
return _status;
}
void ProgressInfo::startEstimateUpdates()
{
_updateEstimatesTimer.start(1000);
}
bool ProgressInfo::isUpdatingEstimates() const
{
return _updateEstimatesTimer.isActive();
}
static bool shouldCountProgress(const SyncFileItem &item)
{
const auto instruction = item._instruction;
csync: Use an explicit instruction for should_update_metadata The current way of tracking the need to update the metadata without propagation using a separate flag makes it difficult to track priorities between the local and remote tree. The logic is also difficult to logically cover since the possibilities matrix isn't 100% covered, leaving the flag only used in a few situations (mostly involving folders, but not only). The reason we need to change this is to be able to track the sync state of files for overlay icons. The instruction alone can't be used since CSYNC_INSTRUCTION_SYNC is used for folders even though they won't be propagated. Removing this logic is however not possible without using something else than CSYNC_INSTRUCTION_NONE since too many codepath interpret (rightfully) this as meaning "nothing to do". This patch adds a new CSYNC_INSTRUCTION_UPDATE_METADATA instruction to let the update and reconcile steps tell the SyncEngine to update the metadata of a file without any propagation. Other flags are left to be interpretted by the implementation as implicitly needing metadata update or not, as this was already the case for most file propagation jobs. For example, CSYNC_INSTRUCTION_NEW for directories now also implicitly update the metadata. Since it's not impossible for folders to emit CSYNC_INSTRUCTION_SYNC or CSYNC_INSTRUCTION_CONFLICT, the corresponding code paths in the sync engine have been removed. Since the reconcile step can now know if the local tree needs metadata update while the remote side might want propagation, the localMetadataUpdate logic in SyncEngine::treewalkFile now simply use a CSYNC_INSTRUCTION_UPDATE_METADATA for the local side, which is now implemented as a different database query.
2016-08-15 15:17:51 +03:00
// Skip any ignored, error or non-propagated files and directories.
if (instruction == CSYNC_INSTRUCTION_NONE
2017-05-17 11:55:42 +03:00
|| instruction == CSYNC_INSTRUCTION_UPDATE_METADATA
|| instruction == CSYNC_INSTRUCTION_IGNORE
|| instruction == CSYNC_INSTRUCTION_ERROR) {
return false;
}
return true;
}
void ProgressInfo::adjustTotalsForFile(const SyncFileItem &item)
{
if (!shouldCountProgress(item)) {
return;
}
_fileProgress._total += item._affectedItems;
if (isSizeDependent(item)) {
_sizeProgress._total += item._size;
}
}
quint64 ProgressInfo::totalFiles() const
{
return _fileProgress._total;
}
quint64 ProgressInfo::completedFiles() const
{
return _fileProgress._completed;
}
quint64 ProgressInfo::currentFile() const
{
return completedFiles() + _currentItems.size();
}
quint64 ProgressInfo::totalSize() const
{
return _sizeProgress._total;
}
quint64 ProgressInfo::completedSize() const
{
return _sizeProgress._completed;
}
void ProgressInfo::setProgressComplete(const SyncFileItem &item)
{
if (!shouldCountProgress(item)) {
return;
}
_currentItems.remove(item._file);
_fileProgress.setCompleted(_fileProgress._completed + item._affectedItems);
if (ProgressInfo::isSizeDependent(item)) {
_totalSizeOfCompletedJobs += item._size;
}
recomputeCompletedSize();
_lastCompletedItem = item;
}
void ProgressInfo::setProgressItem(const SyncFileItem &item, quint64 completed)
{
if (!shouldCountProgress(item)) {
return;
}
_currentItems[item._file]._item = item;
_currentItems[item._file]._progress._total = item._size;
_currentItems[item._file]._progress.setCompleted(completed);
recomputeCompletedSize();
// This seems dubious!
_lastCompletedItem = SyncFileItem();
}
ProgressInfo::Estimates ProgressInfo::totalProgress() const
{
Estimates file = _fileProgress.estimates();
if (_sizeProgress._total == 0) {
return file;
}
Estimates size = _sizeProgress.estimates();
// Ideally the remaining time would be modeled as:
// remaning_file_sizes / transfer_speed
// + remaining_file_count * per_file_overhead
// + remaining_chunked_file_sizes / chunked_reassembly_speed
// with us estimating the three parameters in conjunction.
//
// But we currently only model the bandwidth and the files per
// second independently, which leads to incorrect values. To slightly
// mitigate this problem, we combine the two models depending on
// which factor dominates (essentially big-file-upload vs.
// many-small-files)
//
// If we have size information, we prefer an estimate based
// on the upload speed. That's particularly relevant for large file
// up/downloads, where files per second will be close to 0.
//
// However, when many *small* files are transfered, the estimate
// can become very pessimistic as the transfered amount per second
// drops significantly.
//
// So, if we detect a high rate of files per second or a very low
// transfer rate (often drops hugely during a sequence of deletes,
// for instance), we gradually prefer an optimistic estimate and
// assume the remaining transfer will be done with the highest speed
// we've seen.
// Compute a value that is 0 when fps is <=L*max and 1 when fps is >=U*max
double fps = _fileProgress._progressPerSec;
double fpsL = 0.5;
double fpsU = 0.8;
double nearMaxFps =
2017-05-17 11:55:42 +03:00
qBound(0.0,
(fps - fpsL * _maxFilesPerSecond) / ((fpsU - fpsL) * _maxFilesPerSecond),
1.0);
// Compute a value that is 0 when transfer is >= U*max and
// 1 when transfer is <= L*max
double trans = _sizeProgress._progressPerSec;
double transU = 0.1;
double transL = 0.01;
2017-05-17 11:55:42 +03:00
double slowTransfer = 1.0 - qBound(0.0,
(trans - transL * _maxBytesPerSecond) / ((transU - transL) * _maxBytesPerSecond),
1.0);
double beOptimistic = nearMaxFps * slowTransfer;
size.estimatedEta = (1.0 - beOptimistic) * size.estimatedEta
2017-05-17 11:55:42 +03:00
+ beOptimistic * optimisticEta();
return size;
}
quint64 ProgressInfo::optimisticEta() const
{
// This assumes files and transfers finish as quickly as possible
// *but* note that maxPerSecond could be serious underestimate
// (if we never got to fully excercise transfer or files/second)
return _fileProgress.remaining() / _maxFilesPerSecond * 1000
2017-05-17 11:55:42 +03:00
+ _sizeProgress.remaining() / _maxBytesPerSecond * 1000;
}
bool ProgressInfo::trustEta() const
{
return totalProgress().estimatedEta < 100 * optimisticEta();
}
ProgressInfo::Estimates ProgressInfo::fileProgress(const SyncFileItem &item) const
{
return _currentItems[item._file]._progress.estimates();
}
void ProgressInfo::updateEstimates()
{
_sizeProgress.update();
_fileProgress.update();
// Update progress of all running items.
QMutableHashIterator<QString, ProgressItem> it(_currentItems);
while (it.hasNext()) {
it.next();
it.value()._progress.update();
}
_maxFilesPerSecond = qMax(_fileProgress._progressPerSec,
2017-05-17 11:55:42 +03:00
_maxFilesPerSecond);
_maxBytesPerSecond = qMax(_sizeProgress._progressPerSec,
2017-05-17 11:55:42 +03:00
_maxBytesPerSecond);
}
void ProgressInfo::recomputeCompletedSize()
{
quint64 r = _totalSizeOfCompletedJobs;
2017-05-17 11:55:42 +03:00
foreach (const ProgressItem &i, _currentItems) {
if (isSizeDependent(i._item))
r += i._progress._completed;
}
_sizeProgress.setCompleted(r);
}
ProgressInfo::Estimates ProgressInfo::Progress::estimates() const
{
Estimates est;
est.estimatedBandwidth = _progressPerSec;
if (_progressPerSec != 0) {
est.estimatedEta = (_total - _completed) / _progressPerSec * 1000.0;
} else {
est.estimatedEta = 0; // looks better than quint64 max
}
return est;
}
quint64 ProgressInfo::Progress::completed() const
{
return _completed;
}
quint64 ProgressInfo::Progress::remaining() const
{
return _total - _completed;
}
void ProgressInfo::Progress::update()
{
// A good way to think about the smoothing factor:
// If we make progress P per sec and then stop making progress at all,
// after N calls to this function (and thus seconds) the _progressPerSec
// will have reduced to P*smoothing^N.
// With a value of 0.9, only 4% of the original value is left after 30s
//
// In the first few updates we want to go to the correct value quickly.
// Therefore, smoothing starts at 0 and ramps up to its final value over time.
const double smoothing = 0.9 * (1.0 - _initialSmoothing);
_initialSmoothing *= 0.7; // goes from 1 to 0.03 in 10s
_progressPerSec = smoothing * _progressPerSec + (1.0 - smoothing) * (_completed - _prevCompleted);
_prevCompleted = _completed;
}
void ProgressInfo::Progress::setCompleted(quint64 completed)
{
_completed = qMin(completed, _total);
_prevCompleted = qMin(_prevCompleted, _completed);
}
}