Merge branch 'feature/add_throttle_ui'

# Conflicts:
#	Duplicati/Server/webroot/ngax/styles/style.css
This commit is contained in:
Kenneth Skovhede
2017-06-20 14:53:13 +02:00
13 changed files with 318 additions and 58 deletions
+26 -7
View File
@@ -699,14 +699,33 @@ namespace Duplicati.Library.Main
}
}
}
private void HandleProgress(long pg)
private string m_lastThrottleUploadValue = null;
private string m_lastThrottleDownloadValue = null;
private void HandleProgress(ThrottledStream ts, long pg)
{
// TODO: Should we pause here as well?
// It might give annoying timeouts for transfers
if (m_taskControl != null)
m_taskControl.TaskControlRendevouz();
// Update the throttle speeds if they have changed
string tmp;
m_options.RawOptions.TryGetValue("throttle-upload", out tmp);
if (tmp != m_lastThrottleUploadValue)
{
ts.WriteSpeed = m_options.MaxUploadPrSecond;
m_lastThrottleUploadValue = tmp;
}
m_options.RawOptions.TryGetValue("throttle-download", out tmp);
if (tmp != m_lastThrottleDownloadValue)
{
ts.ReadSpeed = m_options.MaxDownloadPrSecond;
m_lastThrottleDownloadValue = tmp;
}
m_statwriter.BackendProgressUpdater.UpdateProgress(pg);
}
@@ -734,8 +753,8 @@ namespace Duplicati.Library.Main
if (m_backend is Library.Interface.IStreamingBackend && !m_options.DisableStreamingTransfers)
{
using (var fs = System.IO.File.OpenRead(item.LocalFilename))
using (var ts = new ThrottledStream(fs, m_options.MaxUploadPrSecond, m_options.MaxDownloadPrSecond))
using (var pgs = new Library.Utility.ProgressReportingStream(ts, item.Size, HandleProgress))
using (var ts = new ThrottledStream(fs, m_options.MaxUploadPrSecond, m_options.MaxDownloadPrSecond))
using (var pgs = new Library.Utility.ProgressReportingStream(ts, item.Size, pg => HandleProgress(ts, pg)))
((Library.Interface.IStreamingBackend)m_backend).Put(item.RemoteFilename, pgs);
}
else
@@ -829,7 +848,7 @@ namespace Duplicati.Library.Main
using (var ss = new ShaderStream(nextTierWriter, false))
{
using (var ts = new ThrottledStream(ss, m_options.MaxUploadPrSecond, m_options.MaxDownloadPrSecond))
using (var pgs = new Library.Utility.ProgressReportingStream(ts, item.Size, HandleProgress))
using (var pgs = new Library.Utility.ProgressReportingStream(ts, item.Size, pg => HandleProgress(ts, pg)))
{
taskHasher.Start(); // We do not start tasks earlier to be sure the input always gets closed.
if (taskDecrypter != null) taskDecrypter.Start();
@@ -913,8 +932,8 @@ namespace Duplicati.Library.Main
using (var hs = GetFileHasherStream(fs, System.Security.Cryptography.CryptoStreamMode.Write, out getFileHash))
using (var ss = new ShaderStream(hs, true))
{
using (var ts = new ThrottledStream(ss, m_options.MaxUploadPrSecond, m_options.MaxDownloadPrSecond))
using (var pgs = new Library.Utility.ProgressReportingStream(ts, item.Size, HandleProgress))
using (var ts = new ThrottledStream(ss, m_options.MaxUploadPrSecond, m_options.MaxDownloadPrSecond))
using (var pgs = new Library.Utility.ProgressReportingStream(ts, item.Size, pg => HandleProgress(ts, pg)))
{ ((Library.Interface.IStreamingBackend)m_backend).Get(item.RemoteFilename, pgs); }
ss.Flush();
retDownloadSize = ss.TotalBytesWritten;
+15 -3
View File
@@ -1017,9 +1017,21 @@ namespace Duplicati.Library.Main
t.Abort();
}
#region IDisposable Members
public void Dispose()
public long MaxUploadSpeed
{
get { return m_options.MaxUploadPrSecond; }
set { m_options.MaxUploadPrSecond = value; }
}
public long MaxDownloadSpeed
{
get { return m_options.MaxDownloadPrSecond; }
set { m_options.MaxDownloadPrSecond = value; }
}
#region IDisposable Members
public void Dispose()
{
}
+55 -11
View File
@@ -66,12 +66,40 @@ namespace Duplicati.Library.Utility
/// <summary>
/// The current measured read speed
/// </summary>
private double m_current_write_speed;
private double m_current_write_speed;
/// <summary>
/// The number of ticks to have passed before a sample is taken
/// </summary>
private const long SAMPLE_PERIOD = TimeSpan.TicksPerSecond / 4;
/// The time the last read was sampled
/// </summary>
private DateTime m_last_limit_update;
/// <summary>
/// The number of ticks to have passed before a sample is taken
/// </summary>
private const long SAMPLE_PERIOD = TimeSpan.TicksPerSecond / 4;
/// <summary>
/// The number of ticks to have passed before the limits updater is called
/// </summary>
private const long UPDATE_PERIOD = TimeSpan.TicksPerSecond;
/// <summary>
/// Callback method used to update limits
/// </summary>
private readonly Action<ThrottledStream> m_updateLimits;
/// <summary>
/// Creates a throttle around a stream.
/// </summary>
/// <param name="basestream">The stream to throttle</param>
/// <param name="readspeed">The maximum number of bytes pr. second to read. Specify a number less than 1 to allow unlimited speed.</param>
/// <param name="writespeed">The maximum number of bytes pr. second to write. Specify a number less than 1 to allow unlimited speed.</param>
public ThrottledStream(Stream basestream, Action<ThrottledStream> updateLimits)
: base(basestream)
{
m_last_read_sample = m_last_write_sample = m_last_limit_update = new DateTime(0);
m_updateLimits = updateLimits;
}
/// <summary>
/// Creates a throttle around a stream.
@@ -85,7 +113,8 @@ namespace Duplicati.Library.Utility
m_readspeed = readspeed;
m_writespeed = writespeed;
m_last_read_sample = m_last_write_sample = new DateTime(0);
m_last_read_sample = m_last_write_sample = m_last_limit_update = new DateTime(0);
m_updateLimits = null;
}
/// <summary>
@@ -99,9 +128,10 @@ namespace Duplicati.Library.Utility
var remaining = count;
while (remaining > 0)
{
// To avoid excessive waiting, the delay will wait at most 2 seconds,
// so we split the blocks to limit the number of seconds we can wait
{
// To avoid excessive waiting, the delay will wait at most 2 seconds,
// so we split the blocks to limit the number of seconds we can wait
UpdateLimits();
var chunksize = (int)Math.Min(remaining, m_readspeed <= 0 ? remaining : m_readspeed * 2);
DelayIfRequired(ref m_readspeed, chunksize, ref m_last_read_sample, ref m_current_read_counter, ref m_current_read_speed);
@@ -128,8 +158,9 @@ namespace Duplicati.Library.Utility
{
while (count > 0)
{
// To avoid excessive waiting, the delay will wait at most 2 seconds,
// so we split the blocks to limit the number of seconds we can wait
// To avoid excessive waiting, the delay will wait at most 2 seconds,
// so we split the blocks to limit the number of seconds we can wait
UpdateLimits();
var chunksize = (int)Math.Min(count, m_writespeed <= 0 ? count : m_writespeed * 2);
DelayIfRequired(ref m_writespeed, chunksize, ref m_last_write_sample, ref m_current_write_counter, ref m_current_write_speed);
m_basestream.Write(buffer, offset, chunksize);
@@ -170,6 +201,19 @@ namespace Duplicati.Library.Utility
/// </summary>
public double MeasuredWriteSpeed { get { return m_current_write_speed; } }
/// <summary>
/// Helper method called to update the limits
/// </summary>
private void UpdateLimits()
{
var now = DateTime.Now;
if (m_updateLimits != null && now.Ticks - m_last_limit_update.Ticks > UPDATE_PERIOD)
{
m_updateLimits(this);
m_last_limit_update = now;
}
}
/// <summary>
/// Calculates the speed, and inserts appropriate delays
/// </summary>
+12 -10
View File
@@ -90,12 +90,9 @@ namespace Duplicati.Server.Database
m_values.Remove(k.Key);
else
m_values[k.Key] = newsettings[k.Key];
SaveSettings();
}
System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID);
Program.StatusEventNotifyer.SignalNewEvent();
}
SaveSettings();
}
private void SaveSettings()
@@ -106,10 +103,15 @@ namespace Duplicati.Server.Database
Filter = "",
Name = n.Key,
Value = n.Value
}, Database.Connection.SERVER_SETTINGS_ID);
// In case the usage reporter is enabled or disabled, refresh now
Program.StartOrStopUsageReporter();
}, Database.Connection.SERVER_SETTINGS_ID);
System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID);
Program.StatusEventNotifyer.SignalNewEvent();
// In case the usage reporter is enabled or disabled, refresh now
Program.StartOrStopUsageReporter();
// If throttle options were changed, update now
Program.UpdateThrottleSpeeds();
}
public string StartupDelayDuration
+10
View File
@@ -563,6 +563,16 @@ namespace Duplicati.Server
Library.UsageReporter.Reporter.SetReportLevel(reportLevel, disableUsageReporter);
}
public static void UpdateThrottleSpeeds()
{
if (Program.WorkThread == null)
return;
var cur = Program.WorkThread.CurrentTask;
if (cur != null)
cur.UpdateThrottleSpeed();
}
private static void SignalNewEvent(object sender, EventArgs e)
{
StatusEventNotifyer.SignalNewEvent();
+64 -4
View File
@@ -33,6 +33,7 @@ namespace Duplicati.Server
void Abort();
void Pause();
void Resume();
void UpdateThrottleSpeed();
void SetController(Duplicati.Library.Main.Controller controller);
}
@@ -82,7 +83,49 @@ namespace Duplicati.Server
if (c != null)
c.Resume();
}
public long OriginalUploadSpeed { get; set; }
public long OriginalDownloadSpeed { get; set; }
public void UpdateThrottleSpeed()
{
var controller = this.Controller;
if (controller == null)
return;
var job_upload_throttle = this.OriginalUploadSpeed <= 0 ? long.MaxValue : this.OriginalUploadSpeed;
var job_download_throttle = this.OriginalDownloadSpeed <= 0 ? long.MaxValue : this.OriginalDownloadSpeed;
var server_upload_throttle = long.MaxValue;
var server_download_throttle = long.MaxValue;
try
{
if (!string.IsNullOrWhiteSpace(Program.DataConnection.ApplicationSettings.UploadSpeedLimit))
server_upload_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(Program.DataConnection.ApplicationSettings.UploadSpeedLimit, "kb");
}
catch { }
try
{
if (!string.IsNullOrWhiteSpace(Program.DataConnection.ApplicationSettings.DownloadSpeedLimit))
server_download_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(Program.DataConnection.ApplicationSettings.DownloadSpeedLimit, "kb");
}
catch { }
var upload_throttle = Math.Min(job_upload_throttle, server_upload_throttle);
var download_throttle = Math.Min(job_download_throttle, server_download_throttle);
if (upload_throttle <= 0 || upload_throttle == long.MaxValue)
upload_throttle = 0;
if (download_throttle <= 0 || download_throttle == long.MaxValue)
download_throttle = 0;
controller.MaxUploadSpeed = upload_throttle;
controller.MaxDownloadSpeed = download_throttle;
}
private readonly long m_taskID;
public RunnerData()
@@ -488,9 +531,24 @@ namespace Duplicati.Server
using(tempfolder)
using(var controller = new Duplicati.Library.Main.Controller(backup.TargetURL, options, sink))
{
((RunnerData)data).Controller = controller;
switch (data.Operation)
try
{
if (options.ContainsKey("throttle-upload"))
((RunnerData)data).OriginalUploadSpeed = Duplicati.Library.Utility.Sizeparser.ParseSize(options["throttle-upload"], "kb");
}
catch { }
try
{
if (options.ContainsKey("throttle-download"))
((RunnerData)data).OriginalDownloadSpeed = Duplicati.Library.Utility.Sizeparser.ParseSize(options["throttle-download"], "kb");
}
catch { }
((RunnerData)data).Controller = controller;
data.UpdateThrottleSpeed();
switch (data.Operation)
{
case DuplicatiOperation.Backup:
{
@@ -843,6 +901,8 @@ namespace Duplicati.Server
select (Duplicati.Library.Utility.IFilter)(new Duplicati.Library.Utility.FilterExpression(exp, n.Include)))
.Aggregate((a, b) => Duplicati.Library.Utility.FilterExpression.Combine(a, b));
}
}
}
@@ -77,23 +77,27 @@ namespace Duplicati.Server.WebServer.RESTMethods
serversettings.Remove("server-ssl-certificate");
serversettings.Remove("ServerSSLCertificate");
Program.DataConnection.ApplicationSettings.UpdateSettings(serversettings, false);
// Update based on inputs
var existing = Program.DataConnection.Settings.ToDictionary(x => x.Name, x => x);
foreach (var g in globalsettings)
if (g.Value == null)
existing.Remove(g.Key);
else
{
if (existing.ContainsKey(g.Key))
existing[g.Key].Value = g.Value;
if (serversettings.Any())
Program.DataConnection.ApplicationSettings.UpdateSettings(serversettings, false);
if (globalsettings.Any())
{
// Update based on inputs
var existing = Program.DataConnection.Settings.ToDictionary(x => x.Name, x => x);
foreach (var g in globalsettings)
if (g.Value == null)
existing.Remove(g.Key);
else
existing[g.Key] = new Setting() { Name = g.Key, Value = g.Value };
}
{
if (existing.ContainsKey(g.Key))
existing[g.Key].Value = g.Value;
else
existing[g.Key] = new Setting() { Name = g.Key, Value = g.Value };
}
Program.DataConnection.Settings = existing.Select(x => x.Value).ToArray();
Program.DataConnection.Settings = existing.Select(x => x.Value).ToArray();
}
info.OutputOK();
}
+3 -2
View File
@@ -94,6 +94,7 @@
<script type="text/javascript" src="scripts/controllers/AddWizardController.js"></script>
<script type="text/javascript" src="scripts/controllers/PauseController.js"></script>
<script type="text/javascript" src="scripts/controllers/CommandlineController.js"></script>
<script type="text/javascript" src="scripts/controllers/ThrottleController.js"></script>
<script type="text/javascript" src="scripts/filters/timeremaining.js"></script>
<script type="text/javascript" src="scripts/filters/highlight.js"></script>
@@ -152,7 +153,7 @@
<div class="action-icons-small">
<div class="pause" ng-class="{'active': state.programState != 'Running'}" ng-click="pauseOptions()" title="{{state.programState == 'Running' ? 'Click to see the pause options' : 'Click to resume'}}"></div>
<!-- <div class="throttle" ng-click="throttleOptions()"></div> -->
<div class="throttle {{throttle_active ? 'active' : 'inactive'}}" ng-click="throttleOptions()" title="{{'Click to set throttle options' | translate}}" alt="{{'Click to set throttle options' | translate}}"></div>
</div>
<div class="statepadding">
@@ -195,7 +196,7 @@
<div class="action-icons">
<div class="pause" ng-class="{'active': state.programState != 'Running'}" ng-click="pauseOptions()" title="{{state.programState == 'Running' ? 'Click to see the pause options' : 'Click to resume'}}"></div>
<!--<div class="throttle" ng-click="throttleOptions()"></div> -->
<div class="throttle {{throttle_active ? 'active' : 'inactive'}}" ng-click="throttleOptions()" title="{{'Click to set throttle options' | translate}}" alt="{{'Click to set throttle options' | translate}}"></div>
</div>
</div>
</div>
+40 -4
View File
@@ -585,11 +585,11 @@ body {
height: 26px;
display: inline-block;
cursor: pointer;
background: url('../img/throttle-inactive.png');
background: url('../img/throttle.png');
}
> pause.active {
background: url('../img/throttle.png');
> .throttle.inactive {
opacity: 0.5;
}
}
@@ -2083,6 +2083,40 @@ div.modal-dialog {
}
}
.throttlesettings
{
div.multiple {
select {
width: auto;
margin-right: 5px;
}
input {
width: 100px;
}
input.checkbox {
width: auto;
}
label {
line-height: 35px;
padding: 0 15px;
width: auto;
min-width: 150px;
}
}
.disabled {
color: lighten(@tColor, 25%);
input, select {
color: lighten(@tColor, 25%);
}
}
}
// Smaller screen sizes
@media (max-width: 1200px) {
body {
@@ -2177,7 +2211,7 @@ div.modal-dialog {
.content {
float: none;
padding: 20px 20px;
margin: 0 auto;
margin: 0px auto 30px auto;
.state {
width: auto;
@@ -2331,6 +2365,8 @@ div.modal-dialog {
padding-bottom: 10px;
.content {
margin: 0px auto;
div.add,
div.restore {
form {
@@ -6,6 +6,7 @@ backupApp.controller('AppController', function($scope, $cookies, $location, AppS
$scope.localized = {};
$scope.location = $location;
$scope.saved_theme = $scope.active_theme = $cookies.get('current-theme') || 'default';
$scope.throttle_active = false;
// If we want the theme settings
// to be persisted on the server,
@@ -52,7 +53,23 @@ backupApp.controller('AppController', function($scope, $cookies, $location, AppS
};
$scope.throttleOptions = function() {
alert('Throttle options are not implemented yet');
DialogService.htmlDialog(
gettextCatalog.getString('Throttle settings'),
'templates/throttle.html',
[gettextCatalog.getString('OK'), gettextCatalog.getString('Cancel')],
function(index, text, cur) {
if (index == 0 && cur != null && cur.uploadspeed != null && cur.downloadspeed != null) {
var patchdata = {
'max-download-speed': cur.downloadthrottleenabled ? cur.downloadspeed : '',
'max-upload-speed': cur.uploadthrottleenabled ? cur.uploadspeed : '',
};
AppService.patch('/serversettings', patchdata, {headers: {'Content-Type': 'application/json; charset=utf-8'}}).then(function(data) {
$scope.throttle_active = cur.downloadthrottleenabled || cur.uploadthrottleenabled;
}, AppUtils.connectionError);
}
}
);
};
function updateCurrentPage() {
@@ -139,6 +156,12 @@ backupApp.controller('AppController', function($scope, $cookies, $location, AppS
$scope.active_theme = $scope.saved_theme;
else
$scope.active_theme = args.theme || '';
});
AppService.get('/serversettings').then(function(data) {
var ut = data.data['max-upload-speed'];
var dt = data.data['max-download-speed'];
$scope.throttle_active = (ut != null && ut.trim().length != 0) || (dt != null && dt.trim().length != 0);
}, AppUtils.connectionError);
});
@@ -0,0 +1,22 @@
backupApp.controller('ThrottleController', function($scope, AppService, ServerStatus, SystemInfo, AppUtils) {
$scope.speedMultipliers = AppUtils.speedMultipliers;
$scope.selection = $scope.$parent.state.CurrentItem;
AppService.get('/serversettings').then(function(data) {
$scope.selection.uploadspeed = data.data['max-upload-speed'];
$scope.selection.downloadspeed = data.data['max-download-speed'];
$scope.selection.uploadthrottleenabled = ($scope.selection.uploadspeed != '');
$scope.selection.downloadthrottleenabled = ($scope.selection.downloadspeed != '');
// Nicer looking UI
if (!$scope.selection.uploadthrottleenabled)
$scope.selection.uploadspeed = "10MB";
if (!$scope.selection.downloadthrottleenabled)
$scope.selection.downloadspeed = "10MB";
}, AppUtils.connectionError);
});
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
<div ng-controller="ThrottleController" class="throttlesettings">
<form class="styled">
<div class="input mixed multiple" ng-class="selection.uploadthrottleenabled ? '' : 'disabled'">
<input type="checkbox" class="checkbox" name="uploadthrottleenabled" ng-model="selection.uploadthrottleenabled">
<label ng-click="selection.uploadthrottleenabled = !selection.uploadthrottleenabled" for="uploadspeed" translate>Max upload speed</label>
<input ng-disabled="!selection.uploadthrottleenabled" type="number" id="uploadspeed" name="uploadspeed" parse-size-number="uppercase" ng-model="selection.uploadspeed" />
<select ng-disabled="!selection.uploadthrottleenabled" parse-size-multiplier="uppercase" ng-model="selection.uploadspeed" ng-options="item.value as item.name for item in speedMultipliers">
</select>
</div>
<div class="input mixed multiple" ng-class="selection.downloadthrottleenabled ? '' : 'disabled'">
<input type="checkbox" class="checkbox" name="downloadthrottleenabled" ng-model="selection.downloadthrottleenabled">
<label ng-click="selection.downloadthrottleenabled = !selection.downloadthrottleenabled" for="downloadspeed" translate>Max download speed</label>
<input ng-disabled="!selection.downloadthrottleenabled" type="number" id="downloadspeed" name="downloadspeed" parse-size-number="uppercase" ng-model="selection.downloadspeed" />
<select ng-disabled="!selection.downloadthrottleenabled" parse-size-multiplier="uppercase" ng-model="selection.downloadspeed" ng-options="item.value as item.name for item in speedMultipliers">
</select>
</div>
<div>
Note that speeds are entered in bytes, and line speeds are typically reported in bits. Use a factor of 8 to convert, such that an 8 mbit/s line is equivalent to 1 MByte/s.
<div>
</form>
</div>