mirror of
https://github.com/nextcloud/desktop.git
synced 2024-12-20 12:52:06 +03:00
030e3a5d4a
To 2 MB/s and 10 files/s.
406 lines
12 KiB
C++
406 lines
12 KiB
C++
/*
|
|
* 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 <QDebug>
|
|
#include <QCoreApplication>
|
|
|
|
namespace OCC {
|
|
|
|
ProgressDispatcher* ProgressDispatcher::_instance = 0;
|
|
|
|
QString Progress::asResultString( const SyncFileItem& item)
|
|
{
|
|
switch(item._instruction) {
|
|
case CSYNC_INSTRUCTION_SYNC:
|
|
case CSYNC_INSTRUCTION_NEW:
|
|
case CSYNC_INSTRUCTION_TYPE_CHANGE:
|
|
if (item._direction != SyncFileItem::Up) {
|
|
return QCoreApplication::translate( "progress", "Downloaded");
|
|
} else {
|
|
return QCoreApplication::translate( "progress", "Uploaded");
|
|
}
|
|
case CSYNC_INSTRUCTION_CONFLICT:
|
|
return QCoreApplication::translate( "progress", "Server version downloaded, copied changed local file into conflict file");
|
|
case CSYNC_INSTRUCTION_REMOVE:
|
|
return QCoreApplication::translate( "progress", "Deleted");
|
|
case CSYNC_INSTRUCTION_EVAL_RENAME:
|
|
case CSYNC_INSTRUCTION_RENAME:
|
|
return QCoreApplication::translate( "progress", "Moved to %1").arg(item._renameTarget);
|
|
case CSYNC_INSTRUCTION_IGNORE:
|
|
return QCoreApplication::translate( "progress", "Ignored");
|
|
case CSYNC_INSTRUCTION_STAT_ERROR:
|
|
return QCoreApplication::translate( "progress", "Filesystem access error");
|
|
case CSYNC_INSTRUCTION_ERROR:
|
|
return QCoreApplication::translate( "progress", "Error");
|
|
case CSYNC_INSTRUCTION_UPDATE_METADATA:
|
|
return QCoreApplication::translate( "progress", "Updated local metadata");
|
|
case CSYNC_INSTRUCTION_NONE:
|
|
case CSYNC_INSTRUCTION_EVAL:
|
|
return QCoreApplication::translate( "progress", "Unknown");
|
|
|
|
}
|
|
return QCoreApplication::translate( "progress", "Unknown");
|
|
}
|
|
|
|
QString Progress::asActionString( const SyncFileItem &item )
|
|
{
|
|
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)
|
|
return QCoreApplication::translate( "progress", "downloading");
|
|
else
|
|
return QCoreApplication::translate( "progress", "uploading");
|
|
case CSYNC_INSTRUCTION_REMOVE:
|
|
return QCoreApplication::translate( "progress", "deleting");
|
|
case CSYNC_INSTRUCTION_EVAL_RENAME:
|
|
case CSYNC_INSTRUCTION_RENAME:
|
|
return QCoreApplication::translate( "progress", "moving");
|
|
case CSYNC_INSTRUCTION_IGNORE:
|
|
return QCoreApplication::translate( "progress", "ignoring");
|
|
case CSYNC_INSTRUCTION_STAT_ERROR:
|
|
return QCoreApplication::translate( "progress", "error");
|
|
case CSYNC_INSTRUCTION_ERROR:
|
|
return QCoreApplication::translate( "progress", "error");
|
|
case CSYNC_INSTRUCTION_UPDATE_METADATA:
|
|
return QCoreApplication::translate( "progress", "updating local metadata");
|
|
case CSYNC_INSTRUCTION_NONE:
|
|
case CSYNC_INSTRUCTION_EVAL:
|
|
break;
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
bool Progress::isWarningKind( SyncFileItem::Status kind)
|
|
{
|
|
return kind == SyncFileItem::SoftError || kind == SyncFileItem::NormalError
|
|
|| kind == SyncFileItem::FatalError || kind == SyncFileItem::FileIgnored
|
|
|| kind == SyncFileItem::Conflict || kind == SyncFileItem::Restoration;
|
|
|
|
}
|
|
|
|
bool Progress::isIgnoredKind( SyncFileItem::Status kind)
|
|
{
|
|
return kind == SyncFileItem::FileIgnored;
|
|
|
|
}
|
|
|
|
ProgressDispatcher* ProgressDispatcher::instance() {
|
|
if (!_instance) {
|
|
_instance = new ProgressDispatcher();
|
|
}
|
|
return _instance;
|
|
}
|
|
|
|
ProgressDispatcher::ProgressDispatcher(QObject *parent) :
|
|
QObject(parent)
|
|
{
|
|
|
|
}
|
|
|
|
ProgressDispatcher::~ProgressDispatcher()
|
|
{
|
|
|
|
}
|
|
|
|
void ProgressDispatcher::setProgressInfo(const QString& folder, const ProgressInfo& progress)
|
|
{
|
|
if( folder.isEmpty())
|
|
// The update phase now also has progress
|
|
// (progress._currentItems.size() == 0
|
|
// && progress._totalFileCount == 0) )
|
|
{
|
|
return;
|
|
}
|
|
emit progressInfo( folder, progress );
|
|
}
|
|
|
|
ProgressInfo::ProgressInfo()
|
|
{
|
|
connect(&_updateEstimatesTimer, SIGNAL(timeout()), SLOT(updateEstimates()));
|
|
reset();
|
|
}
|
|
|
|
void ProgressInfo::reset()
|
|
{
|
|
_currentItems.clear();
|
|
_currentDiscoveredFolder.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();
|
|
}
|
|
|
|
void ProgressInfo::startEstimateUpdates()
|
|
{
|
|
_updateEstimatesTimer.start(1000);
|
|
}
|
|
|
|
bool ProgressInfo::isUpdatingEstimates() const
|
|
{
|
|
return _updateEstimatesTimer.isActive();
|
|
}
|
|
|
|
static bool shouldCountProgress(const SyncFileItem &item)
|
|
{
|
|
const auto instruction = item._instruction;
|
|
|
|
// Skip any ignored, error or non-propagated files and directories.
|
|
if (instruction == CSYNC_INSTRUCTION_NONE
|
|
|| 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 =
|
|
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;
|
|
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
|
|
+ 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
|
|
+ _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,
|
|
_maxFilesPerSecond);
|
|
_maxBytesPerSecond = qMax(_sizeProgress._progressPerSec,
|
|
_maxBytesPerSecond);
|
|
}
|
|
|
|
void ProgressInfo::recomputeCompletedSize()
|
|
{
|
|
quint64 r = _totalSizeOfCompletedJobs;
|
|
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);
|
|
}
|
|
|
|
|
|
}
|