Skip to content

fix: stop decoding saved JSON on every one-second tick - #1103

Merged
1 commit merged into
mainfrom
work/1098-main-thread-decode
Nov 9, 2026
Merged

1 commit merged into
mainfrom
work/1098-main-thread-decode

Conversation

@shiny-code-app

@shiny-code-app shiny-code-app Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Why

Clicks to change pages or expand sections took about a second on 1.0.80. The account pages tick every second, and every tick read the shared-account cache from disk and decoded it on the main thread. Each saved date in it also built new ISO 8601 formatters.

Hot path

Symbolicated with atos against the shipped 1.0.80 binary from Ship run 42538090475. Its local symbol table is intact, so offsets from the user's sample resolve directly:

  • TimelineView(.periodic(by: 1)) content closure
  • closure #1 in AccountDashboard.body.getter (0x608e8)
  • ContextPanelAppModel.accountPresentation(at:) (0x61288)
  • SnapshotRefreshService.sharedAccountPresentation(...) (0x41a8a8; presentationDocument inlined)
  • MacSharedAccountCache.load(now:) (0x315440)
  • MacSharedAccountCache.Payload.init(from:), then CompanionSyncDocument.init(from:), then UseNextEvidence.init(from:) decoding the launches map
  • JSONDecoder.contextPanelISO8601 date closure, then ContextPanelDateFormatting.date(from:), which allocated up to three ISO8601DateFormatters per date

The user's cache file is 47 KB with 564 dates. 26 KB of it is launch evidence.

Regression

The per-access rebuild has existed since 1.0.76. 1.0.80 made it expensive: a749ad9 added launchEvidence to CompanionSyncDocument, so the cache read on every tick now carries hundreds of launch dates.

What changed

  • The app model keeps the shared presentation document. It rebuilds it when any published model state changes, or after 60 seconds. Each tick only runs the in-memory projection with the current time.
  • SnapshotRefreshService.sharedAccountPresentation becomes sharedPresentationDocument. It returns the document so the caller can reuse it.
  • ContextPanelDateFormatting reuses three formatters instead of building new ones per call.

Measurements

Same Mac, same data.

Before After
Running 1.0.80, top 1 s samples 14 to 30% CPU on an account page needs the next build
Main-thread samples under JSONDecoder.decode 46.2% (1513 of 3276, 5 s sample) and 41.2% (713 of 1729, earlier 3 s sample) none per tick by construction
One cache load and decode, release build, copied cache file 193 ms 53 ms
How often that load runs on the main thread every tick of every 1 s view, plus every currentSnapshot read about twice per 30 s (each model reload)
Per-tick projection from the kept document not separable 3.4 ms

The after CPU figure for the running app needs this build installed. The benchmark ran against a private copy of the cache file, outside the app's data folders.

Verification

  • swift build
  • swift test --filter ContextPanelCoreTests: 1363 tests pass.
  • New test accountPresentationReusesSavedFilesUntilTheModelChangesOrAMinutePasses. It fails if a tick rereads saved files, and fails if a model reload does not pick up new files. Checked by forcing a rebuild on every access: the test fails.
  • JetBrains inspection: UNKNOWN. The configured PyCharm lane has no Swift coverage.

Closes #1098

Removed: an ISO8601DateFormatter allocation per decoded date; disk reads and JSON decoding on each 1 s tick and each currentSnapshot read.
Net lines: +47 (+10 source, +37 test).

The account views tick every second and each tick reloaded and decoded the
shared-account cache, building a new ISO 8601 formatter for every saved date.
Build the shared presentation document once per model change or minute, and
reuse three formatters.

Client-impact: visible
@shiny-code-app

shiny-code-app Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

Ready for the merge train

Head: 733a14c

@shiny-code-app

shiny-code-app Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Launchplane completed the merge-train step for this pull request.

Launchplane landed this pull request through protected batch PR #1104.

Launchplane manages this comment and will update it as the train moves.

shiny-code-app Bot added a commit that referenced this pull request Nov 9, 2026
@shiny-code-app shiny-code-app Bot closed this pull request by merging all changes into main in 92bc717 Nov 9, 2026
@shiny-code-app
shiny-code-app Bot deleted the work/1098-main-thread-decode branch November 9, 2026 02:31
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.

Clicks take a second: the app decodes saved JSON on the main thread every tick

0 participants