Skip to content
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
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Changelog

## v0.3.0.0 (2026-07-02) — Startup optimization release

Fixes the multi-second delay on the **first** click of a pinned taskbar group after a reboot.
See `OPTIMIZATION_PLAN.md` (final plan) and `PLAN_REVIEW.md` (independent verification) for the full analysis.

### Background client (`Taskbar Groups Background.exe` — the hot path)
- **Fixed broken Multicore JIT**: `ProfileOptimization.SetProfileRoot` was never called in the
background client, silently disabling the `StartProfile` call that already existed. The JIT
profile now records/replays across launches (`%LocalAppData%\...\JITComp\`).
- **Lazy group loading**: startup now only indexes group folders (cheap directory scan).
A group's XML + icons are deserialized/decoded on demand at click time, and all groups are
pre-warmed on a background thread after the tray host starts — nothing heavy runs on the
startup or first-paint path anymore.
- **Deferred jump-list construction**: the taskbar jump-list (WindowsAPICodePack + shell COM)
is now built after the popup is shown instead of before, keeping first paint fast.
The `setGroupContextMenu` registration path is unchanged.

### Editor (`Taskbar Groups.exe`)
- **New: "Start at login (instant first click)" toggle** (default **on**, next to the
portability toggle). Registers the background host in `HKCU\...\CurrentVersion\Run` so it is
already tray-resident before your first click — this is what makes the first click after a
reboot instant. Disabled automatically in portable mode. The host also starts immediately
when the setting is applied, so no reboot is needed for it to take effect.
- Fixed: disabling portable mode now writes `portableMode=false` immediately instead of
relying on next-launch self-correction.

### Reliability
- **Fixed editor crash when upgrading while the background host is running** (latent upstream
bug, surfaced during this release's rollout): `closeBackgroundApp` now waits for the exit
messenger process and every remaining instance to terminate, and the background-exe update
retries briefly on `IOException` instead of crashing the editor from a static initializer.
With launch-at-login, a resident host during upgrades is the normal case, so this mattered.

### Compatibility
- Existing groups, pinned taskbar shortcuts, `Settings.xml`, AppUserModelIDs, and the
command-line contract are all unchanged — upgrading is running the new editor once
(it replaces the installed background exe automatically via the existing MD5 check).
105 changes: 105 additions & 0 deletions FINDINGS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Findings — First-Click Startup Delay Investigation & Fix (v0.3.0.0)

> Companion docs: `OPTIMIZATION_PLAN.md` (the implementation plan, final),
> `PLAN_REVIEW.md` (independent second-model verification of the diagnosis),
> `CHANGELOG.md` (release notes). This file is the condensed record of *what
> was found and proven*, for future contributors.

## The symptom

On Windows 11, the **first** click on a pinned taskbar group after a reboot
took ~5 seconds to open the popup. Every subsequent click on any group was
instant until the next reboot.

## Architecture (why the symptom has that shape)

Two executables:

- **`Taskbar Groups.exe`** (project `main/`) — the editor. Embeds the
background exe as a resource and installs/updates it at
`%LocalAppData%\Jack Schierbeck\taskbar-groups\` via an MD5 comparison on
every editor launch.
- **`Taskbar Groups Background.exe`** (project `backgroundClient/`) — a
**tray-resident, single-instance** process. Pinned taskbar `.lnk` files
target this exe with the **bare group name as the only argument**
(note: `ShellLink.InstallShortcut`'s `"<path> shortcut"` string is the
shortcut *description*, not its arguments — an earlier draft of the plan
got this wrong; see `PLAN_REVIEW.md` §2.1).

First click after boot = full cold start of the background process, which
then stays resident; later clicks only deliver an argument to the warm
instance over the single-instance channel. That asymmetry **is** the bug
surface.

## Root causes found (all verified against source, then fixed)

| # | Finding | Location | Fix |
|---|---------|----------|-----|
| 1 | **Multicore JIT was silently disabled** on the hot path: `ProfileOptimization.StartProfile` was called in `frmMain`, but `SetProfileRoot` only ever ran in the editor — without a root, `StartProfile` is a documented no-op. Corroborated on a real install: `JITComp\` contained only editor profiles. | `backgroundClient/Program.cs`, `frmMain.cs:85` | `SetProfileRoot` + a startup profile added to `Program.Main`. Proven: `bkgStartup.Profile` and `frmMain.Profile` now appear in `JITComp\`. |
| 2 | **Every group was fully loaded at startup** — XML deserialization + decoding of every shortcut icon PNG + every `GroupIcon.ico`, before the first popup could show, even though a click needs one group. | `bkgProcess.cs` ctor | Startup now only indexes group folders (cheap scan). `Category` loads on demand at click; all groups pre-warm on a background thread after tray init. |
| 3 | **Jump-list built on the popup's critical path** (WindowsAPICodePack + shell COM, and the project references WPF assemblies). | `frmMain.cs` ctor | Deferred to the form's `Shown` event; the `setGroupContextMenu` registration path still builds it before closing. |
| 4 | **Nothing kept the process warm across reboots** — the cold start was simply paid on the first click. | — | New **"Start at login (instant first click)"** toggle (default on) in the editor: HKCU `Run` key pointing at the background exe. Launching it with **no arguments** already starts it tray-resident with no popup, so no new hot-path code was needed. Disabled in portable mode. |

### Cost-attribution correction (matters for future work)

An earlier hypothesis blamed ".NET Framework JIT of everything." Wrong
emphasis: Framework's own assemblies are NGen'd and its CLR is typically
warm; the JIT tax is limited to the app's ~1.5 MB plus the byte-array-loaded
WindowsAPICodePack DLLs (`Assembly.Load(byte[])` can never use native
images). The dominant cold cost is **disk I/O after boot** plus that
app-code JIT. This is why the login prewarm (#4) is the decisive fix — it
hides the cold start regardless of its exact composition — and why a .NET 8
self-contained single-file migration (~100 MB to page in cold) is **not**
automatically a startup win. See `OPTIMIZATION_PLAN.md` Phase 2 (deferred).

## Bug found *during* rollout (latent upstream bug, now fixed)

Upgrading while the background host was resident **crashed the editor**
(unhandled `IOException` from the `Paths` static initializer; confirmed via
Windows Event Log). Cause: `closeBackgroundApp()` spawns a *messenger copy*
of the background exe to deliver the exit signal but only waited on the
resident process — the messenger itself holds a lock on the same exe file,
and `Process.Kill()` is asynchronous — so the immediate `File.WriteAllBytes`
raced the lock. This was rare before (helper seldom resident during an
upgrade) but becomes the **normal case** with launch-at-login.

Fix: wait on the messenger and on every remaining instance (including after
`Kill()`), and retry the exe write briefly on `IOException`; on persistent
failure keep the previous helper and inform the user instead of crashing.

Also fixed while in the area: the portable-mode toggle wrote
`portableMode=true` even when *disabling* portable mode (previously
self-corrected only on the next launch).

## Verification performed

- Both projects build clean (only pre-existing CS0168 warnings); `build.ps1`
reproduces the Release build end-to-end, including offline NuGet layout
restore and COM interop generation.
- New background exe: no-arg launch goes tray-resident with no popup;
group-name launch hands off to the resident instance and shows the popup;
`exitApplicationModeReserved` cleanly terminates it; JIT profiles are
written.
- Live upgrade on a real install (3 groups: ADOBE/TOOLS/TOPAZ): helper
replaced in place, Run key registered, helper resident immediately, popup
opens through the same code path a taskbar click uses. Existing pins and
groups untouched (AppUserModelIDs, exe path/name, config layout, and the
`.lnk` argument contract are all preserved — see `OPTIMIZATION_PLAN.md`
§4.2 for the invariants).
- Full data backup taken before any change (`Backup_TaskbarGroups_*`,
gitignored).

## Compatibility invariants for future changes

Preserve these or existing pinned groups break (details in
`OPTIMIZATION_PLAN.md` §4.2):

1. AppData layout `Jack Schierbeck\taskbar-groups\{config,Shortcuts,Settings.xml}`.
2. Background exe name and `%LocalAppData%` path.
3. AppUserModelID scheme `tjackenpacken.taskbarGroup.menu.<GroupName>`.
4. `.lnk` argument contract (bare group name; plus `editingGroupMode`,
`tskBaropen_allGroup`, `setGroupContextMenu`, `exitApplicationModeReserved`).
5. `Category` XML schema backward-compatibility (new fields optional with
defaults — `launchAtStartup` in `Settings.xml` follows this rule).
6. Kill the resident background process before swapping its exe (now
handled robustly by `closeBackgroundApp`).
Loading