Add a simple speed estimation for mirror list sites.

Use it when no previous (from settings) or explicit (via --site options)
site selection is made, to give a speed-sorted site selection dialog.

Additionally, in unattended mode, automatically select the 'fastest'
mirror site.

Future work: This isn't great. We spend a maximum of 5 seconds reading
from a mirror, but if there's a dead mirror in the mirror list, we spend
however long trying to connect. Maybe NetIO::open() should have the
option to specify a shorter timeout?

But that shouldn't happen too often, since the dead mirror will get
removed from the mirror list in 24 hours, or so.

This also extends the meaning of the NetIO::open() cachable flag, so
that request is neither entered into, nor retrieved from the local cache.

(Otherwise, a cached setup.ini file from a previous run might be used,
which won't give an accurate measurement of the origin server's speed).
---
 Makefile.am           |   2 +
 SiteSetting.cc        |   2 +
 SiteSetting.h         |   4 ++
 SiteSpeedEstimator.cc | 158 ++++++++++++++++++++++++++++++++++++++++++
 SiteSpeedEstimator.h  |  44 ++++++++++++
 gui/SitePage.cc       |  36 +++++++++-
 nio-ie5.cc            |   2 +-
 7 files changed, 244 insertions(+), 4 deletions(-)
 create mode 100644 SiteSpeedEstimator.cc
 create mode 100644 SiteSpeedEstimator.h

diff --git a/Makefile.am b/Makefile.am
index 4da206f..73276b0 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -280,6 +280,8 @@ endif
        gui/SitePage.h \
        SiteSetting.cc \
        SiteSetting.h \
+       SiteSpeedEstimator.cc \
+       SiteSpeedEstimator.h \
        source.cc \
        source.h \
        SourceSetting.cc \
diff --git a/SiteSetting.cc b/SiteSetting.cc
index 8ec42af..594a7a8 100644
--- a/SiteSetting.cc
+++ b/SiteSetting.cc
@@ -157,6 +157,8 @@ site_list_type::site_list_type (const std::string &_url,
       idx = 0;
   } while (idx > 0);
   key += url;
+
+  speed = 0;
 }
 
 bool
diff --git a/SiteSetting.h b/SiteSetting.h
index 2fbef89..81176c8 100644
--- a/SiteSetting.h
+++ b/SiteSetting.h
@@ -17,6 +17,7 @@
 #define SETUP_SITESETTING_H
 
 #include <vector>
+#include <string>
 
 class SiteSetting
 {
@@ -55,6 +56,9 @@ public:
   std::string displayed_url;
   // tld sort key
   std::string key;
+  // speed measurement (optional)
+  double speed;
+
   bool operator == (const site_list_type &) const;
 };
 
diff --git a/SiteSpeedEstimator.cc b/SiteSpeedEstimator.cc
new file mode 100644
index 0000000..ba14fe9
--- /dev/null
+++ b/SiteSpeedEstimator.cc
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2026, Cygwin Contributors.
+ *
+ *     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.
+ *
+ *     A copy of the GNU General Public License can be found at
+ *     http://www.gnu.org/
+ *
+ * Written by Jon Turney <[email protected]>
+ *
+ */
+
+#include "SiteSpeedEstimator.h"
+#include "netio.h"
+#include "ini.h"
+#include "LogSingleton.h"
+
+#include <vector>
+#include <thread>
+#include <chrono>
+#include <iomanip>
+
+SiteSpeedEstimator::SiteSpeedEstimator(SiteList &site_list)
+  : current_site_list_(&site_list)
+{
+}
+
+SiteSpeedEstimator::~SiteSpeedEstimator()
+{
+}
+
+void
+SiteSpeedEstimator::annotate_sitelist()
+{
+  std::vector<std::thread> worker_threads;
+
+  int max_threads = std::thread::hardware_concurrency() * 2;
+
+  /* Populate the work queue with indices of all sites not marked 'noshow' */
+  for (size_t i = 0; i < current_site_list_->size(); ++i)
+    if (!(*current_site_list_)[i].noshow)
+      work_queue_.push(i);
+
+  auto start_time = std::chrono::high_resolution_clock::now();
+
+  Log (LOG_PLAIN) << "Starting speed measurement for "
+                  << work_queue_.size() << " site(s) with "
+                  << max_threads << " concurrent thread(s)" << endLog;
+
+  /* Create worker threads */
+  for (int i = 0; i < max_threads; ++i)
+    worker_threads.emplace_back([this]() { worker_thread(); });
+
+  /* Wait for all worker threads to complete */
+  for (auto &thread : worker_threads)
+    {
+      if (thread.joinable())
+        thread.join();
+    }
+
+  /* Calculate elapsed time */
+  auto end_time = std::chrono::high_resolution_clock::now();
+  double elapsed = (std::chrono::duration<double>(end_time - 
start_time)).count();
+
+  Log (LOG_PLAIN) << "Speed measurement complete in " << elapsed << " seconds" 
<< endLog;
+}
+
+void
+SiteSpeedEstimator::worker_thread()
+{
+  while (true)
+    {
+      size_t site_index;
+
+      /* Get next work item from queue (in a threadsafe manner) */
+      {
+        std::unique_lock<std::mutex> lock(queue_mutex_);
+
+        if (work_queue_.empty())
+          break;
+
+        site_index = work_queue_.front();
+        work_queue_.pop();
+      }
+
+      /* Perform the measurement */
+      double speed = estimate_speed((*current_site_list_)[site_index]);
+      (*current_site_list_)[site_index].speed = speed;
+    }
+}
+
+/* Maximum time allowed for a single speed test in seconds */
+#define TIMEOUT 5
+
+/* Maximum amount of test file to download (100 KB) */
+#define TEST_FILE_SIZE 102400
+
+double
+SiteSpeedEstimator::estimate_speed(const site_list_type &site)
+{
+  double speed = 0;
+
+  std::string test_url = site.url;
+  if (test_url.empty())
+    return speed;
+
+  test_url += SetupArch();
+  test_url += '/';
+  test_url += SetupBaseName();
+  test_url += ".ini";
+
+  /* Attempt to open connection to URL */
+  NetIO *connection = NetIO::open(test_url.c_str(), false);
+  if (!connection || !connection->ok())
+    return speed;
+
+  auto start_time = std::chrono::high_resolution_clock::now();
+  auto timeout_time = start_time + std::chrono::seconds(TIMEOUT);
+
+  char buffer[4096];
+  size_t total_bytes = 0;
+
+  /* Download data until timeout or test_file_size reached */
+  while (true)
+    {
+      /* Check if we've exceeded the timeout */
+      auto current_time = std::chrono::high_resolution_clock::now();
+      if (current_time >= timeout_time)
+        break;
+
+      int bytes_read = connection->read(buffer, sizeof(buffer));
+      if (bytes_read <= 0)
+        break;
+
+      total_bytes += bytes_read;
+
+      /* Stop if we've downloaded enough data */
+      if (total_bytes >= TEST_FILE_SIZE)
+        break;
+    }
+
+  delete connection;
+
+  /* Calculate elapsed time */
+  auto end_time = std::chrono::high_resolution_clock::now();
+  double elapsed = (std::chrono::duration<double>(end_time - 
start_time)).count();
+
+  if (elapsed > 0 && total_bytes > 0)
+    {
+      /* Calculate speed, converting bytes -> bits */
+      speed = (total_bytes * 8.0) / elapsed;
+    }
+
+  return speed;
+}
diff --git a/SiteSpeedEstimator.h b/SiteSpeedEstimator.h
new file mode 100644
index 0000000..f6c2bf4
--- /dev/null
+++ b/SiteSpeedEstimator.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2026, Cygwin Contributors.
+ *
+ *     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.
+ *
+ *     A copy of the GNU General Public License can be found at
+ *     http://www.gnu.org/
+ *
+ * Written by Jon Turney <[email protected]>
+ *
+ */
+
+#ifndef SETUP_SITE_SPEED_ESTIMATOR_H
+#define SETUP_SITE_SPEED_ESTIMATOR_H
+
+#include "SiteSetting.h"
+#include <mutex>
+#include <queue>
+
+class SiteSpeedEstimator
+{
+public:
+  SiteSpeedEstimator(SiteList &site_list);
+  ~SiteSpeedEstimator();
+
+  /* Measure speed for each site in the list using a thread pool. */
+  void annotate_sitelist();
+
+private:
+  /* Measure speed for a single site */
+  double estimate_speed(const site_list_type &site);
+
+  /* Worker thread function for the thread pool */
+  void worker_thread();
+
+  std::mutex queue_mutex_;
+  std::queue<size_t> work_queue_;
+  SiteList *current_site_list_;
+};
+
+#endif /* SETUP_SITE_SPEED_ESTIMATOR_H */
diff --git a/gui/SitePage.cc b/gui/SitePage.cc
index e8ebc8c..5458307 100644
--- a/gui/SitePage.cc
+++ b/gui/SitePage.cc
@@ -40,6 +40,7 @@
 #include "Exception.h"
 #include "String++.h"
 #include "gui/GuiFeedback.h"
+#include "SiteSpeedEstimator.h"
 
 #define MIRROR_LIST_URL "https://cygwin.com/mirrors.lst";
 
@@ -281,12 +282,41 @@ get_site_list (Feedback &feedback)
   delete[] theMirrorString;
   delete[] theCachedString;
 
-  // sort all_site_list by 'tld key'
-  std::sort(all_site_list.begin(), all_site_list.end(),
-            [] (site_list_type const &a, site_list_type const &b) { return 
a.key < b.key; });
+  // if we don't have a selected site (and do have a mirrors list), do a speed 
test
+  if (selected_site_list.empty() && !all_site_list.empty())
+    {
+      // TBD: show "Selecting mirror" or similar via feedback
+
+      SiteSpeedEstimator estimator(all_site_list);
+      estimator.annotate_sitelist();
+
+      // sort all_site_list by descending speed
+      std::sort(all_site_list.begin(), all_site_list.end(),
+                [] (site_list_type const &a, site_list_type const &b) { return 
a.speed > b.speed; });
+
+     for (size_t i = 0; i < all_site_list.size(); ++i)
+       if (!all_site_list[i].noshow)
+         Log (LOG_BABBLE) << all_site_list[i].url << " " << std::fixed << 
all_site_list[i].speed << " bps" << endLog;
+
+     // also, in unattended mode, autoselect the first (fastest) mirror
+     if (unattended_mode)
+       {
+         Log (LOG_PLAIN) << "Defaulted to " << all_site_list[0].url << " (" << 
std::fixed << all_site_list[0].speed << " bps)" << endLog;
+         selected_site_list.push_back (all_site_list[0]);
+       }
+    }
+  else
+    {
+      // sort all_site_list by 'tld key'
+      std::sort(all_site_list.begin(), all_site_list.end(),
+                [] (site_list_type const &a, site_list_type const &b) { return 
a.key < b.key; });
+    }
 
   migrate_selected_site_list();
 
+  // XXX: this never returns true (indicating an error), which is probably good
+  // as the logic below would put us into a loop in unattended mode in that
+  // case.
   return 0;
 }
 
diff --git a/nio-ie5.cc b/nio-ie5.cc
index f76434b..c2ee167 100644
--- a/nio-ie5.cc
+++ b/nio-ie5.cc
@@ -179,7 +179,7 @@ NetIO_IE5::NetIO_IE5 (char const *url, bool cachable)
     INTERNET_FLAG_EXISTING_CONNECT | INTERNET_FLAG_PASSIVE;
 
   if (!cachable) {
-    flags |= INTERNET_FLAG_NO_CACHE_WRITE;
+    flags |= INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_RELOAD;
   } else {
     flags |= INTERNET_FLAG_RESYNCHRONIZE;
   }
-- 
2.51.0

Reply via email to