Page MenuHomePhabricator

Improve the saving of persistent settings
Closed, DuplicatePublic5 Estimated Story Points

Description

The Vector 2022 persistent saving should be reconsidered now that it saves to cookie as well as user preference. It also doesn't quite work as expected. In particular

  1. Should we reconsider the debounce time length? 1s is likely too high
  2. Make sure we are cancelling API calls when we trigger a new one to avoid conflicting saves.
  3. Improve UI rendering to happen outside the timeout

In T300826#8568297 @Od1n wrote:

The code to trigger the window resize event is inside the saveSidebarState() function, which has a 1000 ms debounce when it is called from bindSidebarClickEvent(). Thus, there is an undesirable delay of 1 second before repositioning the elements when the sidebar is toggled. This trigger of a "fake" window resize event should be decoupled from the API save, and executed immediately. This will result in a much snappier interface. And of course, triggering the fake window resize event and saving the state to API are two completely different things, which shouldn't be blended together.

In the current case, there is no need to throttle/debounce the handler, executing such event once (and in response to an user click) is very cheap, the issue might be it gets executed repeatedly, for example in a real window resize.

For the record, I noticed this issue when editing AbuseFilters, as there was a delay before the Ace editor got resized.

Another thing: in collapseSidebar() and expandSidebar() functions, note they call saveSidebarState()() in a way that every time they are called, they create and call a new debounced saveSidebarState function… The debouncing isn't coded correctly (there should be an single instance of the debounced function). As a result, it's basically as if the function wasn't debounced. Thus, the API save happens without debouncing, but fortunately, the event is triggered only when the media query passes through the max-width: 999px threshold.

Also notice the timeout of the debounce is 0 ms. I think it's intended so that the handler gets lower priority than the most important things such as recalculation of display. Still, the debouncing should be fixed as I exposed in the above paragraph. (edit: or maybe it's because the only way to make an API save (and nothing else) is to call saveSidebarState() with timeout=0 and shouldTriggerResize=false, as the code is nested into this function)

QA

Check sidebar

  • As a logged in user, load page with sidebar pinned (visible by default)
  • Hide the sidebar
  • Quickly refresh the page

Expected: sidebar remains hidden

Check toggle

  • As a logged in user, load page with limited width enabled
  • Click the limited width toggle in bottom right corner to disable limited width
  • Quickly refresh the page

Expected: limited width remains disabled.

Check editor

When reviewing this task, please take care to ensure the patches y'all write to fix this issue do not "dislodge" / "undo" the fixes written in T328121:

  • Visit a page in Vector 2022
  • Open the visual editor
  • Toggle limited content width using the tiny button in bottom-right corner
  • Check that the position and width of the visual editor toolbar reflects the new position and width of the page content
  • Also repeat the above: when starting from limited vs unlimited content width when opening the editor; when scrolled down on a long page and opening the editor from the sticky header

Event Timeline

Change 885267 had a related patch set uploaded (by Gerrit Patch Uploader; author: Anne Haunime):

[mediawiki/skins/Vector@master] Separate the fake resize event from the API save

https://gerrit.wikimedia.org/r/885267

Don't miss the comments by Vlakoff on the Gerrit page, some of them contain important information. Also, don't hesitate to compare between patchsets, as it may help understanding the individual changes.

Basically, most of the issues are because this script handles three different tasks and race conditions may happen between them:

  • automatic sidebar collapse on narrow width (+ restoring the state on large width)
  • saving the sidebar state using API (on toggle click, only for large width)
  • the "trigger window resize" fix (on both, toggle click and automatic collapse/restore)

Another issue is that the automatic collapse/expand are done by setting the checkbox.checked property, which doesn't trigger the input change event. See in checkboxHack.js for thorough explanations. So we are missing everything that is added/modified using JS event handlers, such as ARIA, and possibly things bound elsewhere. And because of this, the state of these "JS managed" things may become stale, mismatching the actual collapsed/expanded state.

However, we are relying on the fact that the input change event is not triggered! Because as so, we are not triggering the handler bound in bindSidebarClickEvent(), thus we are not executing the API save (which is the desired behaviour, we want to avoid the API save on automatic collapse/expand).

To recapitulate, there are two problems:

  • The automatic collapse/expand is done in a way it doesn't change the application state entirely. It misses triggering the bound events, so it lacks the JS enhanced stuff, and worse, some things may become stale.
  • We are relying on the fact that setting the checkbox.checked property doesn't trigger the input change event. This isn't very robust at all, nor documented in the code of sidebarPersistence.js.

Therefore, ideally the automatic collapse/expand should trigger a real event as if the user clicked on the toggle, but with a difference, it should avoid the API save.

The best would be to call checkboxHack's setCheckedState() method, and only when it would actually change the sidedar state (e.g. if the sidebar is already collapsed, avoid calling setCheckedState(false)), in order to avoid triggering extraneous "input" events (as it may have unexpected side-effects). And of course, there is still the need to avoid the API save, which may be solved by setting a temporary flag.

Though, this setCheckedState() is not exported by the module currently. Would it be acceptable to export it?

(Reminders to self: Require through the 'mediawiki.page.ready' module, for codebase consistency. Also, CheckboxHack.d.ts would have to be updated accordingly.)

I noticed the (currently abandonned) T257075 that proposed to refactor the checkbox hack, replacing the multiple exported method with a single super object.

That may conflict with the need here to add export of the setCheckedState() method.

ovasileva lowered the priority of this task from High to Medium.Feb 10 2023, 10:17 PM

This impacts editing workflow. Currently if editors (either anon/logged in) expand the sidebar or click the width toggle and click a link shortly after the feature will not save. This is likely giving the impression the feature is not sticky. Happy to talk about this more if the task is not clear but personally I think we're under-prioritizing this work possibly due to a misunderstanding.

That's due to the debounce of the API save. Such a debounce is mandatory, otherwise the user could trigger a lot of API calls. Not to mention that because the HTTP requests don't necessarily finish in the same order they were started, another request than the latest one could end up doing the lastest save.

The debounce could be lowered from 1000 ms to e.g. 500 ms, but it wouldn't cleanly solve the issue actually. And too low a debounce could introduce an HTTP request race condition as I stated in above sentence.

A better solution could be to use localStorage for quick and efficient saving, keeping the debounced API save for persistence across browsers/computers.

Scenario when user clicks on sidebar toggle:

  1. Save state in localStorage, maybe using a small debounce, just for the sake of having some kind of debouncing (though localStorage is extremely fast), I'd say something in the range of 50~250 ms.
  2. At the same time, trigger a debounced API save. When the function is finally debounced, trigger the HTTP request, and an important point: reading the state from localStorage, so we really have the latest state, avoiding the various race conditions.

Note it wouldn't fully solve your use case, because my solution wouldn't trigger the API save either. But it would solve the issue on same browser.

The stale state could only be found back on another browser/computer. Considering the low likeliness of the issue, and its low disturbance in particular on different browser/computer, that's not too bad.

About the HTTP requests race condition, maybe it could even be fixed, by remembering the issued requests, and aborting the pending requests when a new one is issued. Refs mediawiki.api/options.js. But that's getting very complicated!

However, for this localStorage to be effective, its value should be read on startup, when the script is executed on subsequent page views.

But in turn, this introduces a conflict with the value stored using API. Which one is the "freshest" and should be used?

We could then add timestamps to the stored values… but I think these race conditions are just endless. Not to mention the levels of crazyness the code is reaching. Combining two "storages" (localStorage and API) might improve situation on the surface, but underneath there is a growing nest of race conditions, which denotes the approach is wrong.

If it were up to me, I would entirely drop the API save and only use localStorage. Yes, the state would not be stored on the user account, but it would work reliably on a given browser, all race conditions would be solved, and the code would be much simpler. Considering this combination of robustness and simplicity, I think it may be the proper approach. What is your stance on this?

That's due to the debounce of the API save. Such a debounce is mandatory,

The debounce is mandatory, but the length of time is not. Provided we abort any existing API requests we could set it to 0ms.

If it were up to me, I would entirely drop the API save and only use localStorage.

The feature requires modifications to the server side HTML and is meant to be portable (e.g. change across devices). We currently do not have the capability of varying the HTML via the client (E.g. localStorage), however we are making slow progress in that direction (using client side cookies which are more performant). This is out of scope for this ticket but we are thinking about it as part of https://phabricator.wikimedia.org/T329557

For this task: our goal is to simply lower the existing delay.

Jdlrobson lowered the priority of this task from Medium to Low.Mar 15 2023, 4:59 PM

It does look like most of the associated code will be removed as part of T332090. We can circle back to this ticket when that one is resolved to see if any work remains here.

Change 885267 abandoned by Jdlrobson:

[mediawiki/skins/Vector@master] Fixes and improvements to the adaptative collapsing of the sidebar

Reason:

We will be removing this code as part of https://phabricator.wikimedia.org/T332090 next week so I am abandoning this patchset. Let me know if you have any questions or concerns either here or on the ticket, particularly if I'm missing something that is not resolved by the work we'll be doing in https://phabricator.wikimedia.org/T332090.

https://gerrit.wikimedia.org/r/885267