Commit 33d96154 authored by Omid Tourzan's avatar Omid Tourzan Committed by Commit Bot

[files-progress] Background implementation of transfer speed details.

It adds a Speedometer to Task and get updated when new progress update
comes to the task.

The average window is set to 20, based on some experiment update rate
is not consistent but it showed about per sec, so the average is for
about 20 seconds. It would be updated through more experiments.

It only works with CopyTask as the task types like Zip or Move don't
report processed bytes.

The following new fields dispatched to FE as part of task status:
 - currentSpeed
 - averageSpeed
 - remainingTime

All the times unit to pass to FE is in second and speed is in B/s.
FE supposed to polish it how to arrange magnitute.

Bug: 953308
Change-Id: Idccb0385dc10ff5a6cee83c55f49b1cc0b39da5d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2217921
Commit-Queue: Omid Tourzan <oto@chromium.org>
Reviewed-by: default avatarAlex Danilo <adanilo@chromium.org>
Cr-Commit-Position: refs/heads/master@{#776863}
parent 74a68a32
...@@ -70,6 +70,9 @@ class FileOperationHandler { ...@@ -70,6 +70,9 @@ class FileOperationHandler {
item.progressValue = event.status.processedBytes; item.progressValue = event.status.processedBytes;
item.cancelCallback = this.fileOperationManager_.requestTaskCancel.bind( item.cancelCallback = this.fileOperationManager_.requestTaskCancel.bind(
this.fileOperationManager_, event.taskId); this.fileOperationManager_, event.taskId);
item.currentSpeed = event.status.currentSpeed;
item.averageSpeed = event.status.averageSpeed;
item.remainingTime = event.status.remainingTime;
progressCenter.updateItem(item); progressCenter.updateItem(item);
break; break;
...@@ -82,6 +85,9 @@ class FileOperationHandler { ...@@ -82,6 +85,9 @@ class FileOperationHandler {
item.message = FileOperationHandler.getMessage_(event); item.message = FileOperationHandler.getMessage_(event);
item.progressMax = event.status.totalBytes; item.progressMax = event.status.totalBytes;
item.progressValue = event.status.processedBytes; item.progressValue = event.status.processedBytes;
item.currentSpeed = event.status.currentSpeed;
item.averageSpeed = event.status.averageSpeed;
item.remainingTime = event.status.remainingTime;
progressCenter.updateItem(item); progressCenter.updateItem(item);
break; break;
...@@ -100,6 +106,9 @@ class FileOperationHandler { ...@@ -100,6 +106,9 @@ class FileOperationHandler {
item.message = ''; item.message = '';
item.state = ProgressItemState.COMPLETED; item.state = ProgressItemState.COMPLETED;
item.progressValue = item.progressMax; item.progressValue = item.progressMax;
item.currentSpeed = event.status.currentSpeed;
item.averageSpeed = event.status.averageSpeed;
item.remainingTime = event.status.remainingTime;
} else if (event.reason === EventType.CANCELED) { } else if (event.reason === EventType.CANCELED) {
item.message = ''; item.message = '';
item.state = ProgressItemState.CANCELED; item.state = ProgressItemState.CANCELED;
......
...@@ -614,6 +614,20 @@ fileOperationUtil.Task = class { ...@@ -614,6 +614,20 @@ fileOperationUtil.Task = class {
// For example, if 'dir' was copied as 'dir (1)', then 'dir/file.txt' should // For example, if 'dir' was copied as 'dir (1)', then 'dir/file.txt' should
// become 'dir (1)/file.txt'. // become 'dir (1)/file.txt'.
this.renamedDirectories_ = []; this.renamedDirectories_ = [];
/**
* Number of progress item sequence used in calculating moving average
* speed of task.
* @private {number}
*/
this.SPEED_BUFFER_WINDOW_ = 20;
/**
* Speedometer object used to calculate and track speed and remaining time.
* @protected {fileOperationUtil.Speedometer}
*/
this.speedometer_ =
new fileOperationUtil.Speedometer(this.SPEED_BUFFER_WINDOW_);
} }
...@@ -661,7 +675,10 @@ fileOperationUtil.Task = class { ...@@ -661,7 +675,10 @@ fileOperationUtil.Task = class {
totalBytes: this.totalBytes, totalBytes: this.totalBytes,
processedBytes: this.processedBytes, processedBytes: this.processedBytes,
processingEntryName: processingEntry ? processingEntry.name : '', processingEntryName: processingEntry ? processingEntry.name : '',
targetDirEntryName: this.targetDirEntry.name targetDirEntryName: this.targetDirEntry.name,
currentSpeed: this.speedometer_.getCurrentSpeed(),
averageSpeed: this.speedometer_.getAverageSpeed(),
remainingTime: this.speedometer_.getRemainingTime()
}; };
} }
...@@ -785,6 +802,7 @@ fileOperationUtil.CopyTask = class extends fileOperationUtil.Task { ...@@ -785,6 +802,7 @@ fileOperationUtil.CopyTask = class extends fileOperationUtil.Task {
this.totalBytes += this.processingEntries[i][entryURL].size; this.totalBytes += this.processingEntries[i][entryURL].size;
} }
} }
this.speedometer_.setTotalBytes(this.totalBytes);
callback(); callback();
}); });
...@@ -861,6 +879,7 @@ fileOperationUtil.CopyTask = class extends fileOperationUtil.Task { ...@@ -861,6 +879,7 @@ fileOperationUtil.CopyTask = class extends fileOperationUtil.Task {
const size = opt_size !== undefined ? opt_size : processedEntry.size; const size = opt_size !== undefined ? opt_size : processedEntry.size;
this.processedBytes += size - processedEntry.processedBytes; this.processedBytes += size - processedEntry.processedBytes;
processedEntry.processedBytes = size; processedEntry.processedBytes = size;
this.speedometer_.update(this.processedBytes);
// updateProgress can be called multiple times for a single file copy, and // updateProgress can be called multiple times for a single file copy, and
// it might not be called for a small file. // it might not be called for a small file.
...@@ -920,6 +939,7 @@ fileOperationUtil.CopyTask = class extends fileOperationUtil.Task { ...@@ -920,6 +939,7 @@ fileOperationUtil.CopyTask = class extends fileOperationUtil.Task {
this.processingSourceIndex_ = index + 1; this.processingSourceIndex_ = index + 1;
this.processedBytes = this.calcProcessedBytes_(); this.processedBytes = this.calcProcessedBytes_();
this.numRemainingItems = this.calcNumRemainingItems_(); this.numRemainingItems = this.calcNumRemainingItems_();
this.speedometer_.update(this.processedBytes);
errorCount = 0; errorCount = 0;
callback(); callback();
}, },
...@@ -930,6 +950,7 @@ fileOperationUtil.CopyTask = class extends fileOperationUtil.Task { ...@@ -930,6 +950,7 @@ fileOperationUtil.CopyTask = class extends fileOperationUtil.Task {
this.processingSourceIndex_ = index + 1; this.processingSourceIndex_ = index + 1;
this.processedBytes = this.calcProcessedBytes_(); this.processedBytes = this.calcProcessedBytes_();
this.numRemainingItems = this.calcNumRemainingItems_(); this.numRemainingItems = this.calcNumRemainingItems_();
this.speedometer_.update(this.processedBytes);
errorCount++; errorCount++;
lastError = error; lastError = error;
if (errorCount < if (errorCount <
...@@ -1373,3 +1394,157 @@ fileOperationUtil.EventRouter.EventType = { ...@@ -1373,3 +1394,157 @@ fileOperationUtil.EventRouter.EventType = {
PROGRESS: 'PROGRESS', PROGRESS: 'PROGRESS',
SUCCESS: 'SUCCESS' SUCCESS: 'SUCCESS'
}; };
/**
* Class to calculate transfer speed and remaining time. Each update from
* transfer task stores the last speed in a ring buffer and recalculates the
* cumulative moving average (CMA).
*
* Current speed (average of window) and remaining time is calculated per calls
* from progress updater in the task.
*
* The length of buffer specifies the moving window length.
*/
fileOperationUtil.Speedometer = class {
/**
* @param {number} bufferLength Max number of recent data used in
* calculation as time window.
*/
constructor(bufferLength) {
/***
* The buffer length controlling average window length.
* @type {number}
* @private
*/
this.length_ = bufferLength;
/**
* @private {Array} internal buffer to track recent data.
*/
this.buffer_ = [];
/**
* @private {number} index of current position in buffer.
*/
this.index_ = 0;
/**
* @private {number} Count of how many updates have been processed.
* It helps to update CMA speed without keeping all data.
*/
this.count_ = 0;
/**
* @private {number} Current cumulative moving average speed in bytes per
* second.
*/
this.cma_ = 0;
/**
* @private {number} Last timestamp the speed calculated in millisecond.
*/
this.lastTimestamp_ = 0;
/**
* @private {number} Last reported processed bytes.
*/
this.lastProcessedBytes_ = 0;
/**
*
* @private {number} Total bytes to be processed by the task.
*/
this.totalBytes_ = 0;
}
/**
* Pushes the new speed into the internal queue and call to update CMA.
* @param {number} speed The last calculated speed to track.
*/
push_(speed) {
this.buffer_[this.index_] = speed;
this.index_ = (this.index_ + 1) % this.length_;
}
/**
* Updates cumulative average speed and count.
*
* It updates cma progressively using this formula:
* CMAnew = (CMAprev * count_ + SPEEDcurr) / (count_ + 1)
*
* @param {number} speed The last speed added to the ring.
*/
updateCMA_(speed) {
this.cma_ =
Math.floor((this.cma_ * this.count_ + speed) / (this.count_ + 1));
this.count_++;
}
/**
* Calculates and returns the current speed in bytes per second.
*/
getCurrentSpeed() {
if (this.buffer_.length == 0) {
return 0;
}
const sum =
this.buffer_.reduce((accumulated, current) => accumulated + current, 0);
return Math.floor(sum / this.buffer_.length);
}
/**
* @returns {number} Returns calculated cumulative average speed in bytes per
* second.
*/
getAverageSpeed() {
return this.cma_;
}
/**
* Calculates the remaining time of the task based on remaining bytes and
* current speed.
*
* @returns {number} The remaining time in seconds.
*/
getRemainingTime() {
// Return zero if no data added yet or the last calculated speed was zero.
// It is mapped in UI to show unknown remaining time.
const currentSpeed = this.getCurrentSpeed();
if (currentSpeed == 0) {
return 0;
}
return Math.ceil(
(this.totalBytes_ - this.lastProcessedBytes_) / currentSpeed);
}
/**
*
* @param {number} totalBytes Number of total bytes task handles.
*/
setTotalBytes(totalBytes) {
this.totalBytes_ = totalBytes;
}
/**
* Update speedometer with the latest status.
*
* It calculates speed using the processed bytes reported in the status and
* previous status.
* @param {number} processedBytes Number of processed bytes calculated by the
* task.
* processed bytes.
*/
update(processedBytes) {
const currentTime = Date.now();
const currSpeed = Math.floor(
(processedBytes - this.lastProcessedBytes_) /
((currentTime - this.lastTimestamp_) / 1000));
this.push_(currSpeed);
this.updateCMA_(currSpeed);
this.lastProcessedBytes_ = processedBytes;
this.lastTimestamp_ = currentTime;
}
};
...@@ -131,6 +131,26 @@ class ProgressCenterItem { ...@@ -131,6 +131,26 @@ class ProgressCenterItem {
* @type {?function()} * @type {?function()}
*/ */
this.cancelCallback = null; this.cancelCallback = null;
/**
* The current speed of the progress item in bytes per second.
* It's calculated using moving average formula.
* @type {number}
*/
this.currentSpeed;
/**
* The average speed of the progress item in bytes per second.
* It is calculated using cumulative moving average.
* @type {number}
*/
this.averageSpeed;
/**
* The predicted remaining time to complete the progress item in seconds.
* @type {number}
*/
this.remainingTime;
} }
/** /**
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment