Skip to content

Studio: Apply site events to the site list instead of refetching it - #5506

Merged
chubes4 merged 2 commits into
trunkfrom
app/apply-site-events
Oct 28, 2026
Merged

chubes4 merged 2 commits into
trunkfrom
app/apply-site-events

Conversation

@chubes4

@chubes4 chubes4 commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Related issues

How AI was used

Claude (via the Pi coding agent in a Roadie session) traced the start path, wrote the change and its tests, checked the tests by reverting each piece, and counted the site list calls in packaged builds. I reviewed the diff and the evidence.

Proposed Changes

Every site event the renderer received made it invalidate the site list. Each refetch spawns a CLI site list, which takes about 300 ms. A start or stop sends several events and also awaited one more refetch of its own. Yet each site-updated and site-operations-changed event already carries the site's full record.

  • Renderer (use-sites.ts): writes the event's site into the TanStack Query cache with setQueryData, as useUpdateSitesSortOrder already does. It keeps the two fields only Studio stores (sortOrder, autoStart). satisfies ties those to SiteMetadata, so a new metadata field won't compile until it's carried over. A site-operations-changed event keeps the cached running, because it doesn't know whether the site runs.
    • It still refetches for creates, deletes, sites the list doesn't have yet, and events with no site.
    • It also still refetches when it joins an in-flight fetch, as before.
    • Start and stop refetch only if the list doesn't already show the result.
  • Main process: a site serving the PHP error page is shown at a URL only getSites knows. Its events now go out without the CLI's details, so the renderer refetches for them as before.
  • CLI: site list and the events now build the site from one getSiteDetails. Until now the events left out emailPath, so an applied event would have dropped it.

Applying events directly, rather than refetching on each one, exposed two gaps that the refetches had been covering up:

  • Auto-start waited for isFetchedAfterMount. That flag also turns true when an event is applied on top of the list persisted from the last launch, which predates the autoStart flags the quit set. Auto-start now reads the list itself with fetchQuery, which joins the launch fetch already in flight.
  • _events read every site's status before subscribing to the process manager. A site that finished starting in between never got an event. It now subscribes first, so the last event for a site reflects its current state.

No new cache layer: TanStack Query stays the only one.

In packaged builds, I counted site list calls through a probe in the CLI entry. A run of the startup metric (one creation plus five starts and stops) made 33 calls on trunk and 10–16 with this change, across three runs each. Calls only remain where a fallback refetch is needed, for example when an operation's release lands after the start has resolved. Local timings were unusable because the machine's load average was 20–30. The Performance Metrics job is the timing check.

Testing Instructions

  • npx vitest run apps/ui/src/data/queries/use-sites.test.tsx apps/studio/src/modules/cli/lib/tests/cli-events-subscriber.test.ts apps/cli/commands/site/tests/list.test.ts
    • The new start/stop test fails on trunk.
    • The auto-start test fails with the old isFetchedAfterMount gate.
    • It also fails if any of these is reverted: the cached running for operation events, the kept metadata, or the skipped refetches.
  • Start and stop a site in the app. Its status, URL and settings update as before.
  • Break a site's wp-config.php and start it. It shows the PHP error page at its URL, as before.

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

@wpmobilebot

wpmobilebot commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

📊 Performance Test Results

Comparing bac99cf vs trunk

app-size

Metric trunk bac99cf Diff Change
App Size (Mac) 954.29 MB 954.29 MB +0.00 MB ⚪ 0.0%

site-editor

Metric trunk bac99cf Diff Change
load 1208 ms 1174 ms 34 ms ⚪ 0.0%

site-startup

Metric trunk bac99cf Diff Change
siteCreation 4630 ms 4604 ms 26 ms ⚪ 0.0%
siteStartup 1904 ms 1554 ms 350 ms 🟢 -18.4%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

@chubes4
chubes4 merged commit e6796f5 into trunk Oct 28, 2026
13 checks passed
@chubes4
chubes4 deleted the app/apply-site-events branch October 28, 2026 02:53
@youknowriad

Copy link
Copy Markdown
Contributor

Thanks for reworking the approach here.

@chubes4

chubes4 commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Massive performance gains over the past couple of weeks!

chubes4 added a commit that referenced this pull request Oct 28, 2026
#5512)

## Related issues

- Related to #5495 and #5506 (site start and creation timing).

## How AI was used in this PR

Claude (via the Pi coding agent in a Roadie session) traced each step of
`site create`, wrote the change and the test, and ran the A/B timings
below. I reviewed the diff and the measurements.

## Proposed Changes

When a site is created, the first start installs WordPress with `wp core
install`, which creates the admin user with the configured username,
password and email. The same start then sends those credentials again to
the site's admin API (`set_admin_password`). That request is a full
WordPress load plus a `wp_check_password` against the bcrypt hash the
installer just wrote. On my machine it took 250–290 ms, and it changes
nothing.

- `installWordPress` now returns whether it ran the installer.
- `startServer` skips the credentials request when the installer ran on
this start and no Blueprint ran after it. A Blueprint can change users,
so Blueprint creates still apply the credentials afterwards.
- Every other start is unchanged. Restarts still apply credentials, so a
password changed while a site was stopped still takes effect on its next
start.

Measured with the built CLI on macOS, alternating trunk and this branch,
from `Checking PHP binary…` to `WordPress server started` during `site
create`:

| | runs | median |
|---|---|---|
| trunk | 8 | 2,127 ms |
| this branch | 8 | 1,808 ms |

This branch was faster in all 8 pairs (−319 ms median). Local timings on
a busy machine, so CI's `siteCreation` metric is the number to trust.

## Testing Instructions

- `npm run cli:build`
- `npm test -- --tagsFilter='e2e'
apps/cli/commands/site/tests/create.e2e.test.ts`
- The create e2e test now creates a site with `--admin-username
navigator --admin-password e2e-created-password` and signs in to
`wp-login.php` with them. It fails if the installer doesn't set the
credentials the start no longer re-applies (checked by making the
installer ignore `adminUsername`).
- `site-management.e2e.test.ts` still covers changing the password on an
existing site and signing in with it.
- Manually: `node apps/cli/dist/cli/main.mjs site create --path
~/Studio/creds-check --admin-password secret123 --skip-browser`, then
sign in at `/wp-admin` as `admin` / `secret123`.

## Pre-merge Checklist

- [x] Have you checked for TypeScript, React or other console errors?
chubes4 added a commit that referenced this pull request Oct 28, 2026
…metry lets the process exit (#5522)

## Related issues

- Related to #5512 and #5506 (site creation timing).

## How AI was used in this PR

Claude (via the Pi coding agent in a Roadie session) traced where the
app waits on the CLI, wrote the change and the tests, and measured the
timings below. I reviewed the diff and the measurements.

## Proposed Changes

The app treats a CLI command as finished when its process exits. After
`site create` finishes, it sends a Tracks event, and Node keeps the
process alive until that request completes. So the create flow waits on
a telemetry request after the site is ready.

- The CLI sends a `done` message once a command has finished
successfully, from one place after the command handler returns
(`apps/cli/index.ts`).
- `runCliCommand` resolves on that message, and still on exit for
anything that doesn't send it. Failures still wait for the exit and
reject with the command's error.
- Site start, stop and delete keep finishing on exit for now. Their tail
is smaller and they need a separate look.
- The process keeps running in the background until the request
completes, so no events or stats are lost.
- `site create` reports its result before an import finishes, so
`runCliCommand` keeps resolving with that result but only once `done`
arrives.
- The preview manager ignores the new message instead of logging it as
an invalid event.

Measured with a production CLI build forked over IPC like the app does,
with telemetry on, time from `done` to the process exiting:

| command | runs | time after `done` |
|---|---|---|
| `site create` | 5 | 338–421 ms |
| `site start` / `stop`, telemetry opted out | 3 each | 7–9 ms |

That is the time the app no longer waits. It depends on the user's
latency to WordPress.com; these numbers are from a fast connection. CI's
performance metrics won't show it, because Tracks requests are skipped
in E2E runs.

## Testing Instructions

- `npm test -- packages/common/lib/tests/cli-process.test.ts`
- The new test resolves a create from a command that reports `done` and
never exits. It times out without this change.
- Manually, in a packaged build with analytics on: create a site, and
the app shows it as created as soon as the CLI reports it.

## Pre-merge Checklist

- [x] Have you checked for TypeScript, React or other console errors?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants