I do a lot of debugging in Visual Studio, and while having symbols for all modules loaded in my process (whether from public or internal symbol servers) is a huge boon, most of the time I’m trying to isolate issues in my own code and after every system update (or for some modules for which no symbols are available) I am faced with this screen multiple times, interrupting my state of flow:
There’s a lot that’s wrong with this process. It runs serially for each module, for each symbol server, until a match is found or all symbol servers are exhausted. There is a local symbol cache, and if symbols are found there this process is skipped, but if not then you’re interrupted by a series of network calls made serially meaning in the best case scenario of an infinitely fast connection you’re still bound by the round trip latencies adding up - and sometimes this could happen once you step into a certain section in code when a line loads another module either explicitly or implicitly and suddenly you have to wait for seconds to minutes before you can keep stepping through. YMMV, but this totally kills my state of flow.
I decided to do something about it, and my first thought leaned towards trying to use the Visual Studio extension mechanism to try and parallelize / accelerate this, but the plugin system (from my limited search, which could be incomplete and incorrect) proved to be too complex and not deeply ingrained enough to be able to change such a behavior.
I was going to have to get creative.
I cannot control Visual Studio (or WinDBG, or whatever debugger is trying to lookup symbols) - that part will be serial and O(NumberOfModules * NumberOfServers). There was my speed-of-light. For my solution, I decided I will expose one and exactly one symbol server to Visual Studio: a proxy that immediately returns “symbol not found”, causing that step in Visual Studio to finish as fast the the network calls can be made to localhost. After that, the proxy would launch child processes (in parallel!) to look up the PDBs and actually download them from the symbol server to the symbol cache.
With this workflow, when I start debugging I get the above window flashing many times, then I’m right in my program / stopped at a breakpoint. Whenever I’m at a spot where I want to see stacks / source for a different module, I go to the modules window and invoke module loading again - by then the module symbols, if found, sould be in the symbol cache having been downloaded in the background by the proxy-initiated process.
This was simple, composable, and it worked flawlessly! I wrote the Symbol Proxy in Rust as a learning project, and had it launch a Symbol Downloader written in C++. Just launch the proxy passing it as CLI args your local symbol cache and the remote symbol server(s), and make sure the downloader is somewhere it can find in $PATH. In Visual Studio or your favorite debugger, remove all other symbol servers except for localhost:8000
and you’re good to go.
I’m not 100% satisfied with the proxy / downloader design - a single process with async downloading via threads / async IO would be more performant, but at the time I was looking for an excuse to learn Rust and profile URLDownloadToFileA()
’s performance relative to libcurl (libcurl wins, which is what I’d use in a rewrite). The console window hanging around for the proxy is also less than ideal, and this tool would be better off running with no UI to speak of other than a Notification Area icon. I think it’s worth revisiting, but so far it’s, for better and for worse, Good Enough.