Details
-
Bug
-
Resolution: Fixed
-
P2: Important
-
6.6.0
-
None
-
Dell Latitude 5521
11th Gen Intel(R) Core(TM) i7-11850H @ 2.50GHz
-
-
731b759c00ce17072e3f93fdd7044490e51171ca
Description
The first time a Qt app issues an HTTPS web request, the Qt network libraries automatically initialize the SSL infrastructure. This happens as a result of a series of function calls: a call to QNetworkAccessManager::get() results in a call to QNetworkRequest::sslConfiguration(), which calls defaultConfiguration(), which calls ensureInitialized(), and then there is a chain of ensureInitialized() calls that initialize the SSL infrastructure.
This initialization can also be triggered simply by calling the static function QSslConfiguration::defaultConfiguration().
The elapsed time to perform this first-time SSL initialization has increased dramatically between Qt 5 and Qt 6. Here are times that I recorded on my Windows laptop:
Qt 5.15.14
- Windows Release build: 56 ms
- Windows Debug build: 62 ms
Qt 6.6.0
- Windows Release build: 750 ms
- Windows Debug build: 5200 ms
So, this is taking about 13 x longer in a Release build, and 83 x longer in a Debug build!
These longer delays are quite problematic. The first QNetworkAccessManager::get() is likely to be issued from within the main Qt event loop (in fact, I think it has to be issued on the main Qt thread). That means the application UI locks up until this initialization completes. So - almost 1 second of UI lockup in a Release build, and over 5 seconds in a Debug build.
I captured the above timings by adding the following test code in my application's main() function, early on in the initialization of the app:
QElapsedTimer timer;
timer.start();
QSslConfiguration::defaultConfiguration();
qint64 elapsed = timer.elapsed();
QString message = "**** QSslConfiguration::defaultConfiguration() elapsed = " + QString::number(elapsed);
qDebug() << message;
(You may find that qDebug() doesn't produce any output in a Release build, in which case the message has to be output another way, or inspected in the debugger.)
I was then able to further analyze where the time was being spent, by single-stepping through code in the Visual Studio debugger, and watching the handy "elapsed time" message that Visual Studio displays on each step. This is not super-accurate, but gives a coarse-grained indication of where time is being spent.
When you debug through the initialization code, you can see that the path taken in both Qt 5 and Qt 6 is similar, until the following functions are reached:
Qt 5.15.14: qsslsocket_openssl.cpp, QSslSocketPrivate::systemCaCertificates()
Qt 6.6.0: qtls_schannel.cpp, QSchannelBackend::systemCaCertificatesImplementation()
The vast majority of the time taken to do SSL initialization is spent in these functions, which read the set of CA Certificates from the certificate store. This means the elapsed times I reported above will vary from one machine to the next, depending on the number of CA Certs. My laptop has 72 certificates, which I don't think is a particularly unusual/excessive number. So my timings suggest it's taking around 75 ms to lead each certificate in a Debug build.
Superficially, the Qt 5 and Qt 6 functions referred to above look similar, in that they both iterate over the certificate store and load the certificates into memory. However, there are some important differences. It appears the Qt 5 code loads just a couple of attributes for each certificate, and calls into openssl functions to do this. The Qt 6 code, on the other hand, parses the entire certificate string, calling X509CertificateGeneric::parse() to do this. Drilling down further into the code, I've found that the majority of the time is being spent in two calls to toDateTime(), decoding 'notValidBefore' and 'notValidAfter'. These dates are not even used, they are just checked for validity - if that's even necessary, then I wonder if there might be a more efficient way to do this by just examining the characters in the strings. If you debug into toDateTime() and QDateTimeParser, you find that parsing a date-time is a surprisingly complex and time-consuming process. It sets up a format string, then parses that string, then parses the date-time itself, which includes tasks such as loading locale information from the registry - etc. Each call to toDateTime() takes around 30-35 ms, so ultimately I believe this accounts for the majority of the overall time spent in initializing SSL.
I hope an effort can be made to revise the Qt 6 code so that the elapsed time for SSL initialization comes down to something close to the Qt 5 performance.
FWIW, there is a workaround of sorts. You can take the performance hit early in the lifetime of the app, by defining a thread class:
class InitializeSslThread : public QThread { public: explicit InitializeSslThread() { setObjectName("InitializeSslThread"); } protected: void run() override { // Lower the priority to ensure this doesn't cause any slowdown in the UI // or other app startup processing. setPriority(QThread::LowPriority); // Access the default SSL configuration, which will trigger the automatic // one-time initialization of the SSL infrastructure. QSslConfiguration::defaultConfiguration(); } };
and then running this thread early on in the main() function:
InitializeSslThread initSslThread; initSslThread.start();
This prevents the UI locking up on the first HTTPS web request - but only if that call occurs after SSL initialization has finished. Otherwise it ends up waiting on a mutex, and the UI is still blocked in much the same way.