Show the tray icon 40ms sooner - #861
Merged
Merged
Conversation
The icon appeared 121ms into a warm start, and most of what came before it was work nothing about the icon needed. Measured from process start, it is now 81ms. Time to fully ready is unchanged at around 210ms, which is the honest summary: this moves the wait rather than removing much of it, and the wait it moves is the one the user is watching. Inner reads the settings file after creating the NotifyIcon rather than before. Nothing between the two needs a setting - AlwaysKill and the hot keys are both used further down - and until that line there is nothing in the notification area to look at. Still after the mutex, so a second instance exits without ever showing an icon. Worth 33ms. Images are decoded on first use. A static constructor decoded all eleven the moment anything touched one, and the first thing to touch one is the tray icon, where only Default is wanted; four more are on the fixed menu items and the last five are only ever shown when something is pending. Lazy rather than field initialisers, which would not have helped, since the runtime initialises a type's static fields together. Thread safe because the icon is set from the scan timer as well as from the UI thread. Worth 5ms, and it stops decoding images for a menu that may never have anything in it. Settings deserialise through a source generated context, and the file is read whole and parsed in one go rather than off an async FileStream. Together 31ms to 21ms - less than it looks, and less than the generator promises: it is worth 4ms of that, and the async machinery 6ms, while the remaining 20ms is System.Text.Json being loaded and jitted for the first time, which no amount of generated code avoids for a file under 200 bytes. Kept anyway, since it is strictly cheaper and keeps the reflection based serialiser off the startup path, which is the part that grows on a cold start. Deliberately not touched: Logging.Init is 23ms and still ahead of the icon. It could move after it, but then "Mutex already exists. Exiting." and anything the Main catch reports would have nowhere to go, and a tray that failed to start with no explanation costs more than 23ms is worth. The 43ms in new ContextMenuStrip, and 15ms building its items, are already after the icon is up. Deferring them to the first right click only moves the cost to somewhere the user is waiting on it. The duplicate EnableVisualStyles and SetCompatibleTextRenderingDefault calls go with the reorder: Main already makes both immediately before calling Inner.
The only await left in SettingsHelper.Read was a 2 byte first run write, and it kept a state machine and a Task on the path that runs at every startup. Read and GetSettings are now sync, and the ReSharper suppression on the ReadAllBytes call goes with them - that inspection only fires inside an async method. Write stays async: Swap retries File.Move ten times with a 20ms delay, and blocking the UI thread for 200ms while a backup holds the file is the failure it exists to avoid.
This was referenced Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The tray icon appeared 121 ms into a warm start, and most of what came before it was work nothing about the icon needed. Measured from process start with probes at each step:
MainTrayViewerDirectory.RegisterLogging.InitNotifyIconTime to fully ready is unchanged at ~210 ms, which is the honest summary: this moves the wait rather than removing much of it, and the wait it moves is the one the user is watching.
The icon before the settings file
Nothing between the two needs a setting —
AlwaysKilland the hot keys are both used further down — and until that line there is nothing in the notification area to look at. Still after the mutex, so a second instance exits without ever showing an icon. Worth 33 ms.Images decoded on first use
A static constructor decoded all eleven the moment anything touched one, and the first thing to touch one is the tray icon, where only
Defaultis wanted. Four more are on the fixed menu items; the last five are only ever shown when something is pending.Lazy<T>rather than field initialisers, which would not have helped — the runtime initialises a type's static fields together, soDefaultwould still have brought the other ten with it. Thread safe because the icon is set from the scan timer as well as from the UI thread. Worth 5 ms, and it stops decoding images for a menu that may never have anything in it.Settings through a source generated context
And read whole and parsed in one go rather than off an async
FileStream. Together 31 ms → 21 ms — less than the generator promises, and worth saying plainly:Kept anyway, since it is strictly cheaper and keeps the reflection based serialiser off the startup path — the part that grows on a cold start.
Deliberately not touched
Logging.Init, 23 ms, still ahead of the icon. It could move after it, but then"Mutex already exists. Exiting."and anything theMaincatch reports would have nowhere to go. A tray that failed to start with no explanation costs more than 23 ms is worth.43 ms in
new ContextMenuStrip, plus 15 ms building its items. Already after the icon is up. Deferring them to the first right click only moves the cost to somewhere the user is waiting on it.The duplicate
EnableVisualStyles/SetCompatibleTextRenderingDefaultcalls go with the reorder:Mainalready makes both immediately before callingInner.Tests
Full solution: 1905 tests, 0 failed, 20 skipped.
ImagesTestalready touches every image, so the lazy fields stay covered, andSettingsHelperTests.ReadWritepins that the written JSON is unchanged.