Repository navigation
fix: speed up pubky profile loading - #854
Conversation
Profile, avatar and follows lookups are unauthenticated public reads, so they now run through a bounded, cancellable limiter (six at a time) instead of the Paykit SDK operation lock, taking the lock only when no SDK exists yet. Sign-in, sign-up and session import start the identity republish in the background rather than holding the lock while it waits on the DHT.
The choice screen now keeps found Ring profiles for the session and remembers misses until the app returns to the foreground, dedupes lookups in flight, and runs one cancellable load that stops when the screen goes away or an adopt starts. Adopting reuses the row's profile for that key instead of fetching it again, shows a spinner on the tapped row and keeps the other rows disabled. A profile load that started before a save, adopt or sign-out no longer overwrites the newer profile.
A missing profile is never retried, and bulk contact loads and follow discovery no longer retry a failed lookup, so each contact without a published profile costs one lookup instead of two. Adding a contact and importing contacts still retry a failed lookup once, and a lookup cancelled during that wait no longer starts the retry.
Adopting a Pubky Ring identity now stops only the other rows' lookups, so the tapped row's result can still land in time to be reused, and a failed adopt reloads the rows instead of leaving bare keys. Adoption looks the row profile up by the normalized pubky, and when it reuses one it refreshes the profile in the background without holding up navigation or deciding profile setup. The ring lookup tests now fail at a deadline instead of hanging when a request is never answered.
|
Resolve conflicts in ContactsManager and PubkyProfileManager, and drop the duplicated sdkFactory declaration that master carries.
…ght row lookup Adopting a Pubky Ring key writes its reference before sign-in finishes, so a row result landing mid-sign-in swapped the profile destination to an empty profile. The destination now keeps the choice screen while adoption is in progress, and returns to it when adoption fails. Adoption awaits the tapped row's lookup that is still in flight instead of fetching the same key again, and automatic session recovery while signed out no longer discards the choice rows and their lookups.
…tact lookups on tap A disk hit promoted to the memory cache skipped the clear generation check, so a lookup that raced a sign-out could put the previous identity's avatar back in memory. The generation is now read once before the lookup and required for every memory store. The contact detail Retry button retries a transient failure once, and activation is covered returning while the identity republish is still running.
…r row profiles exactly The refresh after adoption reuses a Ring row profile now treats a definitive not-found for the adopted key as a removed profile: it clears the profile and its cached metadata and marks profile setup pending, unless the identity changed or a newer profile write landed. Transport failures keep the reused profile. The choice screen's row profiles now equal the manager's cache outside an adoption and only merge while one runs.
A Ring row now shows a spinner in its avatar slot until its profile lookup finishes, whatever the outcome, so a row still looking up no longer looks like one whose identity has no profile. The adopting row keeps only its key-icon spinner, and stopped lookups drop their spinner straight away.
…ng and share ring row lookups between choice screens
…bky-profile-load-lag # Conflicts: # Bitkit/Views/Profile/CreateProfileView.swift # BitkitTests/PaykitSdkClientConfigTests.swift
Public reads now have a bulk lane: contact list and import profile lookups take one of four bulk slots before one of the six read slots, so they can never hold more than four. Avatars, the own profile, Ring rows and single-contact lookups take only a read slot, so at least two stay free for them while a large contact list loads.
Receiver path and marker reads are public reads, so contact receiver discovery, private receiver path selection and payment request receiver checks now run on the public read lane instead of holding the global SDK lock for every network round trip. Private sync, import and payment request checks use the bulk lane; adding or opening a single contact stays interactive. Saved records are still read and written under the lock, and selection still protects every saved path when no SDK can be built.
Import no longer looks every follow's profile up again, with a retry, before saving it. It saves the profiles the import screens already show. A follow whose lookup failed is still imported as a placeholder, on the wallet receiver path and without receiver discovery, since private sync discovers and merges a saved contact's receivers before it uses them. Only a failed save skips a contact, and an import stops saving once the Pubky session is reset by a sign-out or identity change.
Contacts used to show a spinner over an empty list until every contact's profile lookup had finished, which takes seconds per contact with no published profile. A load now publishes the saved records at once, using a contact's profile override, its stored Paykit profile, a profile resolved earlier this session for the same identity, or its saved label, then looks the rest up in the background on the bulk read lane and updates rows as they resolve. A failed lookup leaves its row as it is. Profiles resolved while preparing an import, importing or adding a contact seed that session cache, which a reset or another identity's load clears. Profile refreshes do not count as a change to the saved contacts, so private Paykit sync does not rerun for every row that fills in.
The import already runs on its own task, so leaving the import screens does not stop it. Select stayed enabled while Import All ran, though, and each screen only knew about its own import, so a second import of the same follows could start alongside the first. The contacts manager now reports whether an import is running, and both import screens disable Select, Import All and Continue until it finishes.
Adds journeys for importing a Ring identity's follows and for Contacts showing saved contacts while their profiles load, documents them in the pubky-profile suite README, and extends the changelog fragment to cover the faster import and contacts list.
Save on a contact's edit screen first waits for the lookup of a profile the row is still waiting for, and then may upload a new avatar. It only bound itself to the Contacts reset generation once those waits were over, and never to the Pubky session. If the user left, signed out and adopted another identity that had saved the same contact before the lookup finished, the old edit filled its form from the new identity's row, saved its label through the new session, wrote back the local override sign-out had cleared, and showed a toast and navigated. ContactsManager.saveContactEdit now binds the edit to the Pubky session and the reset generation when Save is tapped, using the same check as queued tag changes. A stale edit stops after the lookup, after building the edit (including the avatar upload) and around the save, without saving, writing an override, changing a row or reporting an error, and the screen shows no toast and does not navigate.
Adopting a remembered Ring row reuses its cached profile and refreshes it in the background. A profile save only bumped the profile write generation once its publication returned, so a refresh that found the profile missing while the save was still publishing was accepted: it cleared the profile and persisted profile setup as pending. The save then committed the edited profile but left setup pending, so MainNav could prompt Create Profile after a successful edit, also after a relaunch. A profile save now drops every profile read that started before it as soon as its write starts, and a published profile clears pending setup. A save that a sign-out or another identity overtakes no longer commits its profile or touches setup state, so it cannot clear the next identity's pending setup or show the earlier identity's profile.
ovitrif
left a comment
There was a problem hiding this comment.
Suggestion: 👍 Approve
Reaudit: diff 5 files.
No new findings; the rest is in the review.
Pair PR synonymdev/bitkit-android#1399: equivalent. This change binds an iOS contact save to its session and drops a missing-profile refresh that a profile save supersedes.
QA:
Tests wait for CI.
Reviewed by grok-4.7-xhigh via gh-pr-review-loop skill
A contact's tag change or edit checks its session right before it saves, but the save then waits for the Paykit operation lock. A sign-out and another identity's sign-in could take the lock first, so the save wrote the old identity's label to the next identity's contact with the same key. The local override and row were dropped, but the SDK write landed. Edit Contact's avatar upload had the same window. The contact save now carries the identity that was signed in when the change was queued or Save was tapped, and PaykitSdkService checks it against the SDK's signed-in identity in the same locked operation as the write. A mismatch writes nothing and throws identityChanged, which ContactsManager drops quietly, with no error and no toast. Edit Contact's avatar upload passes the same identity to the upload's existing check under the lock.
Edit Profile uploaded a new avatar and then saved the profile without tying either to the session Save was tapped in. A save that a sign-out and another identity's sign-in overtook changed nothing locally, but the screen still showed the saved toast and navigated back, the upload could land for the next identity, and the publication could overwrite the next identity's profile while it waited for the Paykit lock. PubkyProfileManager.saveProfile now takes the avatar image, binds the edit to the current session before uploading, and returns whether it saved. The upload and the profile publication both carry the session's identity, which PaykitSdkService checks under the operation lock, so neither writes once another identity is signed in. A stale or refused save returns false without an error, leaves the profile and setup state alone, and Edit Profile then shows no toast and stays put.
Binding profile and contact avatar uploads to an identity routed them through the subscription icon upload's check, which throws the payment request error requestUnavailable both for another identity and for no live session. Edit Profile then showed "The payment request is no longer available." when an upload failed for the signed-in identity. Profile and contact avatars now use their own upload, which checks only the identity under the Paykit lock: another identity throws identityChanged, which Edit Profile and Edit Contact drop quietly, and any other failure, such as no live session, is the SDK's own error, as before. The payment request upload is unchanged.
ovitrif
left a comment
There was a problem hiding this comment.
Suggestion: 👍 Approve
Reaudit: diff 9 files.
No new findings; the rest is in the review.
Pair PR synonymdev/bitkit-android#1399: equivalent. An iOS contact or profile write is refused once another identity is signed in under the Paykit lock. A profile or contact avatar upload that fails while that identity is still signed in reports the SDK's own error.
QA:
Tests wait for CI.
Reviewed by grok-4.7-xhigh via gh-pr-review-loop skill
|
@ovi-reviewer test |
1 similar comment
|
@ovi-reviewer test |
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Full PR diff and affected callers/tests reviewed at 6d29518 against merge base f751f50, with targeted source comparison of corresponding Android flows at b46f007.
1 actionable finding — resolve or provide an evidence-backed rebuttal.
Validation: current-head unit tests and integration tests succeeded in CI. The public-read SDK contract was checked at Paykit 24162eb. Local host Swift checks passed: 13 extracted lock/limiter tests, two head/base ordering tests and contact-form checks, using dependency stubs. The ordering tests confirmed the reported toggle race; full app suites were not run locally.
iOS import preparation still has no per-follow deadline. Remaining large-contact private-link delays are tracked separately in #861 and #856.
Findings
- [MEDIUM] Invalidate an older enable when a newer toggle turns sharing off — inline at
Bitkit/Views/Settings/GeneralSettingsView.swift:155.
General Settings' first automatic enable runs in its own task, so a disable from a General Settings opened again could start while that enable still prepared endpoints, and the enable then published the marker and endpoints after the disable had removed them. Each change now takes the next change number before it suspends and stops quietly once a newer one starts, as it already did for a session change. Its endpoint publications and removals check this under their locks, and a public removal now also removes the marker under the endpoint lock, so an older change never writes or removes endpoints after a newer one.
Hehe it's offline for maintenance ^^ |
ovitrif
left a comment
There was a problem hiding this comment.
utAck
Went over both fix PRs also in dedicated sessions with AI, to look into the other perf issues and what still needs fixes over the Paykit lib update PRs.
Recommended approach was to merge this and its Android sibling, then have Ben's PRs fix the merge conflicts.
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Full PR review at 3ced8b1, covering the complete diff from merge base f751f50, affected callers and tests. Shared-flow comparison used Android b46f007; that comparison covered the changed contracts only.
2 actionable findings — resolve or provide an evidence-backed rebuttal.
Posting these in case you find this useful and want to handle here, otherwise let's merge as per @ovitrif's recommendation
Findings
- [MEDIUM] Invalidate stale profile reads before uploading an edited avatar — inline at
Bitkit/Managers/PubkyProfileManager.swift:814. - [MEDIUM] Discard interactive profile results after deleting a contact — inline at
Bitkit/Managers/ContactsManager.swift:493.
|
@Jasonvdb build error |
|
Could you share the build log or the error? CI builds 3ced8b1 without errors:
master hasn't moved since f751f50, so a build merged with master is the same code. My guess is the build environment after maintenance, but I can't tell without the error. |
Twin: synonymdev/bitkit-android#1399
Fixes the lag when loading pubky profiles and setting up a profile with contacts, which got much worse with the shared Pubky Ring keychain from #774.
Where the lag came from. All Paykit calls go through one lock, and each call kept it for its whole network round trip. That included simple public reads, like fetching a profile name or avatar, or checking how a contact can be paid. So lookups queued behind each other, behind sign-in, and behind background Paykit work. A pubky that was never published takes 2–7 s to fail, and everything else waited for it. On top of that:
How this fixes it. Public reads skip the lock and run side by side, up to six at once. Background reads (the contact list, private payment setup, payment checks) may use at most four of those six, so what you are looking at (your avatar, your profile, the Ring rows, the import screen) never waits behind them. Anything that touches your session, keys or saved Paykit data still goes through the lock. The choice screen shows its rows straight away with a spinner on each, and keeps found profiles for the session. Adopting reuses the profile its row already found, and sign-in no longer waits for the identity refresh. Contacts shows your saved contacts at once and fills in names and avatars as they load. Saving the imported contacts without new lookups came from #851, which this branch now includes from
master.With QA's 61-follow Pubky Ring profile (simulator, staging network; 54 follows have profiles, 7 were never published). The before column is this PR before the Contacts and Delete fixes:
Description
masterOut of Scope
Bitkit/Services/PubkyService.swift: app launch still restores the Paykit session twice; removing that needs Paykit inputBitkit/MainNavView.swift: opening Profile before initialization finishes still shows the full-screen loading viewBitkit/AppScene.swift: the private link burst still holds the Paykit lock for each link attempt; changing its timing needs Paykit inputBitkit/Views/Profile/CreateProfileView.swift: the form still waits for the existing-profile lookupBitkit/Views/Contacts/EditContactView.swift: editing a contact whose profile lookup failed still saves an empty bio, avatar and links over the published ones, as onmaster; saving only the changed fields needs a new override formatHow to review
Production code is about a quarter of this PR; most of the rest is tests (about 2,470 lines, with near-duplicate cases merged into case tables). Suggested order, one area at a time:
Bitkit/Services/PubkyService.swift, withwithPublicRead,PaykitSdkReadLimiterand the background identity refresh. Tests:PaykitSdkReadLimiterTests,PaykitPublicReadLaneTests,PaykitSdkOperationLockTests,PaykitSdkClientConfigTests,PubkyIdentityRepublishTestsPubkyProfileManager.swift,PubkyChoiceView.swift,PubkyChoiceRow.swift,ProfileView.swift,PubkyImage.swift. Tests:PubkyProfileManagerTests,PubkyChoiceViewTests,PubkyImageCacheTestsContactsManager.swift(the list shown at once, the per-identity session profile cache with its freshness window, the batched background refresh, the saved-contacts announcement, queued tag changes bound to their session, and a screen's lookup taking over a contact's background lookup), then the views that call it:ContactDetailView.swift,EditContactView.swift, the import views andGeneralSettingsView.swift. Tests:ContactsManagerTests,ContactImportUITestsPaykitPaymentRequestService.swiftandPrivatePaykitService+Contacts.swift, which only choose a read lanejourneys/pubky-profile/What to look for: reads no longer queue behind the Paykit lock, so a result can now arrive late, after a sign-out, an identity switch, a newer save or a wallet wipe. Most of the new code drops those late results (owner checks, generation counters, the wipe check). Each of those guards has a test that holds a fake Paykit call halfway, runs the competing action, then releases the call.
Design
N/A — no design available. The spinners reuse existing components.
Preview
Choosing a Pubky Ring identity
ios-before-after.mp4
Left:
master(ab88d1c, plus a two-line compile fix it needed then). Right: this PR (ad6b5d5). Same simulator, same taps, fresh app data, staging network, one take each. Onmasterthe rows show bare keys for about 10 s, and after the tap the screen does not change for about 9 s. With this PR each row shows a spinner until its profile loads, and the tapped row spins while it is adopted.Setting up a profile and importing contacts
ios-import-before-after.mp4
Left:
master(e33dc08). Right: this PR (54e13d0). Same simulator, same taps, staging network, one take each, aligned on the Ring row tap. Each take chooses the Pubky Ring identity, taps Import All, then opens Contacts and Profile. The test identity has no real pubky.app follows, so both builds use the same debug-only hook that returns 8 follows: 2 published and 6 never published, all created for this test. Onmasterthe screen does not change for about 60 s after the tap. With this PR the tapped row spins and the import screen opens in about 9 s. Import All, Contacts and Profile are quick on both (thanks to #851), butmastershows a "Failed to load contacts" error after you leave Contacts.QA Notes
Journeys
cached-profile-header.xml— Profile opened while loading shows the cached name and avatar without edit actions, then the full profilering-choice-rows.xml— Pubky Ring rows show at once with a spinner each until their profile loads; adopting spins only the tapped row and disables the restcontact-import-after-leaving.xml— leaving the import screen right after Import All stays on Home, never opens Pay Contacts, and Contacts later lists every followcontacts-list-loading.xml— saved contacts show at once with their stored names, profiles fill in, and the full-screen spinner shows only until the first loadManual Tests
Automated Checks
PaykitSdkReadLimiterTests.swift— the read limiter keeps its cap and FIFO order, keeps two slots for what you are looking at, gives a freed slot to an interactive read before queued bulk reads, and releases cancelled waitersPaykitPublicReadLaneTests.swift— background lookups leave room for interactive reads, receiver and payment-route reads don't hold the lock, and public reads are refused during a wallet wipe and dropped when a wipe overtakes themPaykitSdkOperationLockTests.swift— unlocked work still goes through wipe admissionPaykitSdkClientConfigTests.swift— sign-in returns while the identity refresh is still runningPubkyIdentityRepublishTests.swift— approvals wait for a refresh sign-in started, up to the capPubkyProfileManagerTests.swift— Ring row cache, adopt reuse, stale profile loads after a write, cached header owner check, row lookup spinners, and lookups shared by two choice screensPubkyChoiceViewTests.swift— when a row shows its lookup spinnerContactsManagerTests.swift— tag changes queued during a contact's lookup keep the profile it finds and each other, a contacts load cancelled while its record read waits for the lock stays quiet, a screen's lookup taking over the contact's background lookup, retry policy per caller, import screen lookups on the interactive lane, the Contacts list publishing before lookups finish and reusing profiles found this session, an import that a sign-out overtakes, opening Pay Contacts only while the import is still pending, skipping lookups for profiles found in the last 10 minutes, batched refresh publishes that skip unchanged profiles, announcing saved-contact changes only when the keys change, stopping lookups for removed contacts, and dropping queued tag changes after a sign-out or identity switchContactImportUITests.swift— an import finishing after you leave the overview or the selection screen does not open Pay ContactsContactPaymentsServiceTests.swift:PaykitPaymentRequestServiceTests.swift— a contact's payment check uses the interactive lanePubkyImageCacheTests.swift— avatar stores that started before a clear are dropped, and failed fetches are remembered until a clear (missing or too large) or for 60 s (other errors)