Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: WebSocket is closed before the connection is established warning #8042

Open
wants to merge 1 commit into
base: next
Choose a base branch
from

Conversation

stipsan
Copy link
Member

@stipsan stipsan commented Dec 13, 2024

Description

Ever since I gave up on #7227 it's been in the back of my mind, and taunting me every time I've been in a studio and seen this warning:

WebSocket connection to 'wss://:projectId.api.sanity.io/v2022-06-30/socket/:dataset?tag=sanity.studio' failed: WebSocket is closed before the connection is established.

That ends today, I've finally found a stable workaround!
At some point we should dive into @sanity/bifur-client and find out exactly why the observable teardown calling ws.close causes the warning, maybe we have to check ws.readyState? Maybe we can't avoid this warning if the observable is unsubscribed to before the connection is established, and while the warning is annoying it remains the right thing to do.
Or maybe we should wait a tick Promise.resolve().then before creating the WebSocket, in case react does a new render as userStore changed so we avoid creating a resource that will be immediately closed anyway (we use this technique in react-rx, as well as in @sanity/client for its client.listen() observable).
I don't know, I'm not an WebSocket expert.

What I do know is that in #7227 the regression that lead to the WebSocket warning showing 3 times instead of just once, was because it shifted useGlobalPresence and useDocumentPresence completely away from useEffect to useObservable from react-rx.
The benefits of react-rx's useObservable are many:

But, since useObservable is designed to allow synchronous emits, it warms up the observable you give it, and subscribes to it during render.
Previously useGlobalPresence and useDocumentPresence subscribed much later, in a useEffect, causing the bifur websocket to be setup after react is done with initial renders.

The reason why the issue is resolved this time around is because the observables aren't subscribed to until after an initial useEffect runs and tracks mounted state. By the time this happens the dependencies for usePresenceStore (bifur, connectionStatusStore, and userStore) have stabilized and we no longer see eager closing of the WebSocket.

What to review

Hopefully this makes sense 🙌

Testing

I've tested manually with pnpm dev, as well as the preview deployment and I've verified that the warning no longer happens.
I've also verified on network devtools that there's only one websocket resource related to bifur.
Here's the before screenshot, that shows the eagerly closed resource with its Finished status, in addition to the live one with status 101, and with Time set to Pending (indicating it's open and active):
image
And here's the after, showing only a single resource:

image

Notes for release

Copy link

vercel bot commented Dec 13, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
page-building-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 13, 2024 4:35pm
performance-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 13, 2024 4:35pm
test-next-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 13, 2024 4:35pm
test-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 13, 2024 4:35pm
1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
studio-workshop ⬜️ Ignored (Inspect) Visit Preview Dec 13, 2024 4:35pm

Copy link
Contributor

No changes to documentation

Copy link
Contributor

github-actions bot commented Dec 13, 2024

Component Testing Report Updated Dec 13, 2024 4:31 PM (UTC)

❌ Failed Tests (6) -- expand for details
File Status Duration Passed Skipped Failed
comments/CommentInput.spec.tsx ✅ Passed (Inspect) 1m 11s 15 0 0
formBuilder/ArrayInput.spec.tsx ✅ Passed (Inspect) 14s 3 0 0
formBuilder/inputs/PortableText/Annotations.spec.tsx ✅ Passed (Inspect) 41s 6 0 0
formBuilder/inputs/PortableText/copyPaste/CopyPaste.spec.tsx ✅ Passed (Inspect) 57s 11 7 0
formBuilder/inputs/PortableText/copyPaste/CopyPasteFields.spec.tsx ✅ Passed (Inspect) 0s 0 12 0
formBuilder/inputs/PortableText/Decorators.spec.tsx ✅ Passed (Inspect) 29s 6 0 0
formBuilder/inputs/PortableText/DisableFocusAndUnset.spec.tsx ✅ Passed (Inspect) 16s 3 0 0
formBuilder/inputs/PortableText/DragAndDrop.spec.tsx ❌ Failed (Inspect) 4m 27s 0 0 6
formBuilder/inputs/PortableText/FocusTracking.spec.tsx ✅ Passed (Inspect) 1m 14s 15 0 0
formBuilder/inputs/PortableText/Input.spec.tsx ✅ Passed (Inspect) 3m 3s 21 0 0
formBuilder/inputs/PortableText/ObjectBlock.spec.tsx ✅ Passed (Inspect) 1m 48s 18 0 0
formBuilder/inputs/PortableText/PresenceCursors.spec.tsx ✅ Passed (Inspect) 14s 3 9 0
formBuilder/inputs/PortableText/RangeDecoration.spec.tsx ✅ Passed (Inspect) 42s 9 0 0
formBuilder/inputs/PortableText/Styles.spec.tsx ✅ Passed (Inspect) 29s 6 0 0
formBuilder/inputs/PortableText/Toolbar.spec.tsx ✅ Passed (Inspect) 1m 53s 21 0 0
formBuilder/tree-editing/TreeEditing.spec.tsx ✅ Passed (Inspect) 0s 0 3 0
formBuilder/tree-editing/TreeEditingNestedObjects.spec.tsx ✅ Passed (Inspect) 0s 0 3 0

Copy link
Contributor

github-actions bot commented Dec 13, 2024

⚡️ Editor Performance Report

Updated Fri, 13 Dec 2024 16:32:20 GMT

Benchmark reference
latency of sanity@latest
experiment
latency of this branch
Δ (%)
latency difference
article (title) 22.2 efps (45ms) 21.7 efps (46ms) +1ms (+2.2%)
article (body) 59.2 efps (17ms) 63.3 efps (16ms) -1ms (-6.5%)
article (string inside object) 23.8 efps (42ms) 22.7 efps (44ms) +2ms (+4.8%)
article (string inside array) 20.4 efps (49ms) 20.8 efps (48ms) -1ms (-2.0%)
recipe (name) 51.3 efps (20ms) 52.6 efps (19ms) -1ms (-2.6%)
recipe (description) 58.8 efps (17ms) 58.8 efps (17ms) +0ms (-/-%)
recipe (instructions) 99.9+ efps (5ms) 99.9+ efps (5ms) +0ms (-/-%)
synthetic (title) 19.2 efps (52ms) 18.9 efps (53ms) +1ms (+1.9%)
synthetic (string inside object) 20.0 efps (50ms) 18.3 efps (55ms) +5ms (+9.0%)

efps — editor "frames per second". The number of updates assumed to be possible within a second.

Derived from input latency. efps = 1000 / input_latency

Detailed information

🏠 Reference result

The performance result of sanity@latest

Benchmark latency p75 p90 p99 blocking time test duration
article (title) 45ms 48ms 51ms 171ms 164ms 10.7s
article (body) 17ms 20ms 25ms 116ms 257ms 5.6s
article (string inside object) 42ms 44ms 48ms 261ms 399ms 7.5s
article (string inside array) 49ms 52ms 55ms 69ms 148ms 7.6s
recipe (name) 20ms 21ms 24ms 47ms 0ms 6.7s
recipe (description) 17ms 19ms 20ms 33ms 0ms 4.5s
recipe (instructions) 5ms 7ms 8ms 9ms 0ms 3.1s
synthetic (title) 52ms 55ms 61ms 84ms 255ms 13.6s
synthetic (string inside object) 50ms 52ms 58ms 331ms 113ms 7.8s

🧪 Experiment result

The performance result of this branch

Benchmark latency p75 p90 p99 blocking time test duration
article (title) 46ms 51ms 58ms 261ms 270ms 11.9s
article (body) 16ms 19ms 32ms 185ms 221ms 5.6s
article (string inside object) 44ms 47ms 54ms 228ms 248ms 7.3s
article (string inside array) 48ms 51ms 58ms 188ms 167ms 7.6s
recipe (name) 19ms 21ms 25ms 51ms 0ms 6.6s
recipe (description) 17ms 18ms 19ms 34ms 0ms 4.4s
recipe (instructions) 5ms 6ms 8ms 13ms 0ms 3.0s
synthetic (title) 53ms 55ms 58ms 111ms 272ms 13.3s
synthetic (string inside object) 55ms 57ms 64ms 401ms 781ms 8.4s

📚 Glossary

column definitions

  • benchmark — the name of the test, e.g. "article", followed by the label of the field being measured, e.g. "(title)".
  • latency — the time between when a key was pressed and when it was rendered. derived from a set of samples. the median (p50) is shown to show the most common latency.
  • p75 — the 75th percentile of the input latency in the test run. 75% of the sampled inputs in this benchmark were processed faster than this value. this provides insight into the upper range of typical performance.
  • p90 — the 90th percentile of the input latency in the test run. 90% of the sampled inputs were faster than this. this metric helps identify slower interactions that occurred less frequently during the benchmark.
  • p99 — the 99th percentile of the input latency in the test run. only 1% of sampled inputs were slower than this. this represents the worst-case scenarios encountered during the benchmark, useful for identifying potential performance outliers.
  • blocking time — the total time during which the main thread was blocked, preventing user input and UI updates. this metric helps identify performance bottlenecks that may cause the interface to feel unresponsive.
  • test duration — how long the test run took to complete.

@@ -16,7 +16,7 @@ export interface ResourceCacheProviderProps {

/** @internal */
export function ResourceCacheProvider({children}: ResourceCacheProviderProps) {
const resourceCache = useMemo((): ResourceCache => {
const [resourceCache] = useState((): ResourceCache => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A great rule of thumb for determining wether a useMemo is safe and side-effects free is wether you can remove it and what happens to your app:

  • a) it still works, but slower
  • b) it breaks, or is so much slower it's unusable

In this case it's b), as the assumption here is that useMemo with zero dependencies means resourceCache will run exactly once, and have a stable instance for hte lifecycle of components calling it. This is false. In fact, it can choose to throw away the memoized value in certain suspense scenarios: https://react.dev/reference/react/useMemo#caveats

A better solution is to use useState, as it does have a guarantee that it's never throwing away the value, and it has a lazy init function so that our function that creates the map is still only run once.

This is also reflected in how much simpler the React Compiler optimization is for useState, compared to the useMemo.

@stipsan stipsan marked this pull request as ready for review December 13, 2024 16:31
@stipsan stipsan requested review from a team as code owners December 13, 2024 16:31
@stipsan stipsan requested review from ricokahler and removed request for a team December 13, 2024 16:31
@stipsan stipsan enabled auto-merge December 13, 2024 16: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.

1 participant