Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

343.sync time estimate #586

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions newsfragments/343.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Estimate remaining sync time
5 changes: 4 additions & 1 deletion src/magic_folder/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,10 @@ def message(payload, is_binary=False):
print(" {}".format(
", ".join(d["relpath"] for d in folder["downloads"])
))
print(" uploads: {}".format(len(folder["uploads"])))
remaining = ""
if folder["remaining-upload-time"] is not None:
remaining = " ({:0.1f}s remaining)".format(folder["remaining-upload-time"])
print(" uploads: {}{}".format(len(folder["uploads"]), remaining))
for u in folder["uploads"]:
queue = humanize.naturaldelta(now - u["queued-at"])
start = " (started {} ago)".format(humanize.naturaldelta(now - u["started-at"])) if "started-at" in u else ""
Expand Down
33 changes: 33 additions & 0 deletions src/magic_folder/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
from .util.file import (
PathState,
ns_to_seconds,
ns_to_seconds_float,
seconds_to_ns,
)

Expand Down Expand Up @@ -1294,6 +1295,38 @@ def get_tahoe_object_sizes(self, cursor):
])
return sizes

@with_cursor
def get_recent_upload_speed(self, cursor):
"""
Average some of our recently uploaded files and return the upload
speed (in bytes per second). If we've never uploaded a file
this is None.

:returns int: bytes per second (or None if we've never uploaded)
"""
cursor.execute(
"""
SELECT
last_updated_ns, size, upload_duration_ns
FROM
[current_snapshots]
ORDER BY
last_updated_ns DESC
LIMIT
10
"""
)
total_size = 0
total_duration = 0
for _, size, duration_ns in cursor.fetchall():
if duration_ns is None:
continue
total_size += size
total_duration += ns_to_seconds_float(duration_ns)
if total_duration == 0 or total_size == 0:
return None
return float(total_size) / total_duration

@with_cursor
def get_recent_remotesnapshot_paths(self, cursor, n):
"""
Expand Down
17 changes: 17 additions & 0 deletions src/magic_folder/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,22 @@ def client_disconnected(self, protocol):
"""
self._clients.remove(protocol)

def upload_seconds_remaining(self, folder_name):
"""
Estimate the number of seconds remaining for any pending uploads.
"""
config = self._config.get_magic_folder(folder_name)
folder = self._folders[folder_name]

remaining_size = 0
for relpath, ps, _, _ in config.get_all_current_snapshot_pathstates():
if relpath in folder["uploads"]:
remaining_size += ps.size
recent_speed = config.get_recent_upload_speed()
if recent_speed is None:
return None
return remaining_size / recent_speed

def _marshal_state(self):
"""
Internal helper. Turn our current notion of the state into a
Expand Down Expand Up @@ -386,6 +402,7 @@ def uploads_and_downloads():
for err in self._folders.get(name, {}).get("errors", [])
],
"recent": most_recent,
"remaining-upload-time": self.upload_seconds_remaining(name),
"tahoe": {
"happy": self._tahoe.is_happy,
"connected": self._tahoe.connected,
Expand Down
40 changes: 40 additions & 0 deletions src/magic_folder/test/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,46 @@ def test_limit(self):
)
)

def test_estimate_remaining_time_empty(self):
"""
If there are no recent snapshots there's no time estimate
"""
self.assertThat(
self.db.get_recent_upload_speed(),
Equals(None),
)

def test_estimate_remaining_time(self):
"""
Add several RemoteSnapshots and ensure times are estimated
"""
relpath = "time_estimate"
# a consistent current-time so we can compute exact duration
self.patch(self.db, '_get_current_timestamp', lambda: 42)
# 3000 bytes in each file, 30 seconds per upload
sizes = (3000, 3000, 3000)
for size in sizes:
remote = RemoteSnapshot(
relpath,
self.author,
{"relpath": relpath, "modification_time": 0},
"URI:DIR2-CHK:",
[],
"URI:CHK:",
"URI:CHK:",
)
self.db.store_currentsnapshot_state(
relpath,
PathState(size, seconds_to_ns(0), seconds_to_ns(0)),
)
# "upload_started_at=12" means each snapshot took (42 - 12) seconds to upload
self.db.store_uploaded_snapshot(relpath, remote, 12)

self.assertThat(
int(self.db.get_recent_upload_speed()),
Equals(sum(sizes) / (30 * 3)),
)


class ConflictTests(SyncTestCase):
"""
Expand Down