Faster initial card load: two calls for channel labels, and a skeleton while they arrive #331
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "faster-initial-card-load"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Initial load went past ten seconds on a few hundred cards after
83996c0(cards store references only, content comes live from Mattermost). Two commits:
the round trips, then what a card shows while it waits for them.
1. Resolve card-channel labels in two calls instead of one per channel
Not the number of cards - the number of channels.
POST /api/mm/cards/contentbulked posts and authors, then resolved channels one at a time:
awaitin aforis not concurrency, andMattermostClient._client()opens anew
httpx.AsyncClientper call (26 call sites doasync with self._client()),so each of those round trips also paid its own TCP and TLS handshake. At ~60ms
RTT that is roughly 3 RTT per channel.
Verified against the Mattermost server source: there is no cross-team
bulk-by-id route for channels (
POST /teams/{id}/channels/idsis team-scopedand public-only). So the resolver asks the two collection endpoints instead -
GET /users/me/channelsandGET /users/me/teams- and filters in process.Four round trips per linked server whatever the card count, two once the
labels are warm.
Those listings only carry current memberships, so a card from a channel the
user has left is absent there and still needs its own fetch. That fallback is
concurrent, and a channel that stays unresolvable (403/404) is left out of the
result rather than failing the page.
The label caches move to the process, keyed on
(base_url, id)so twolinked servers cannot answer for each other, and holding only fields that are
equal for every member of a channel. They were unreachable before:
build_mm_clienthands every request a fresh client, so the hour-long TTL theendpoint's docstring promised never scored a hit, not even on a reload.
2. Render a loading card as a skeleton, not a half-filled card
Two placeholders lied about what they stood for: the breadcrumb fell back to
source_data.channel_name(Mattermost's URL name, not a name it displays), andthe timestamp fell back to
card.updated_at- the moment Waggle last touchedthe row, in the format a real timestamp uses. A date never looks like a
placeholder, so a loading card read as a loaded one with the wrong date.
Both stay as a label of last resort for a card whose post is gone; what they
stop being is a stand-in for something still on its way. In their place: static
placeholder bars, and the quick actions held back until the content is in. The
actions do not need it - their
show/disabledpredicates read only card statevisibilityreserves their exact boxand drops them from the tab order, so their arrival is not a second reflow.
This follows NLDD's design guidelines, which are specific about loading and
corrected the implementation twice while it was being built:
to the interface being built.
not the placeholder - which is what
nldd-activity-indicator's 1000ms hold isfor.
behind it. That is the component in overlay mode, over the list rather than
per card: the hold, the frosted backdrop,
role="status"announcing"Berichten laden" and
inertcontent are all its own defaults, so this addsno timer of its own.
The guidelines also state the principle both fixes rest on: elements must not
appear in a provisional state and then change or disappear.
Bugs found on the way, each by a test rather than by reading
get_team_namewas the one method in the client that did not translate 401,so an expired token during team resolution surfaced as a 502 "mattermost
error" instead of prompting re-auth.
they shared channel id
c1. That was luck - with different ids the cross-testcontamination would have travelled silently. Hence the reset fixture in
conftest.py.500px wide (
attachments-multi.spec.ts).Measuring
The dev Mattermost mock answered in microseconds, which hid the whole class of
bug: N calls in a row read exactly like one.
WAGGLE_DEV_MM_MOCK_LATENCY_MSnow gives every mocked call a round-trip cost -wrapped on the class rather than per method, because a benchmark that depends on
someone remembering to add a
sleepto the next mock method is a benchmark thatlies.
At 60ms per call, 40 channels over 3 teams:
The regression guard is a request counter, not a timing assertion: an
httpx.MockTransportrecords paths, and the test requires 40 channels to costtwo round trips. A wall-clock test would be flaky and would pin the wrong
property.
Two ways this measurement misleads, both documented in the
run-waggleskillafter walking into them:
fetchfrom the page measures the browser's connection queue, notthe endpoint. With latency on, every request is slow and the
six-connections-per-host limit queues yours behind them: 8.4s in the browser
for a request that took 260ms server-side.
dev_seedputs every card in one channel, so the preview cannot reproducea per-channel N+1 at all, latency or not.
Also measured, on the preview with a delayed response: 21 of 42 cards
aria-busywhile loading, six bars per card, no animation on any of them, theaction bar holding its 102px, and the row settling 4px shorter when a one-line
message lands (the 3em the old text placeholder reserved made that 20px). Two
body bars is a deliberate guess - the inbox body has no clamp, so no fixed count
is right for every message.
Verification
including both 401 re-raises - they have tests now).
vue-tscclean, build clean.uvx pre-commit run --all-filesgreen on the pinned ruff 0.8.6.Not measured: wall-clock against a real Mattermost. The preview runs the MM mock,
so the ten seconds are not reproducible locally. What is proven is the round-trip
reduction; what the clock does depends on the RTT to the server.
Deliberately not in here
Viewport-first batching and a browser-local content cache. The decision was to
land this and measure first: with the label cache warm, what remains per load is
posts + users per server, so localStorage would save roughly 240ms at 60ms RTT
and only on a hard reload - against stale content for its TTL, quota management,
and partly reversing #189 phase 2. That one gets its own WDR if it happens.
Card content resolved channels sequentially - `await client.get_channel()` inside a for loop - and MattermostClient._client() opens a new httpx.AsyncClient per call, so every one of those round trips paid its own TCP and TLS handshake. A few hundred cards over ~40 channels took upwards of ten seconds to paint. Mattermost has no cross-team bulk-by-id route for channels (POST /teams/{id}/channels/ids is team-scoped and public-only, verified against the server source), so ask the collection endpoints instead: /users/me/channels and /users/me/teams, filtered in process. That is four round trips per linked server regardless of card count. Those listings only carry current memberships, so a card from a channel the user has left still falls back to a per-channel fetch - concurrently, and a channel that stays unresolvable is omitted rather than failing the request. The label caches move from the client instance to the process. They were unreachable before: build_mm_client hands every request a fresh client, so the hour-long TTL the endpoint's docstring promised never scored a hit, not even on a reload. Keyed on (base_url, id) so two linked servers cannot answer for each other, and holding only fields that are equal for every member. That shared cache needs the reset fixture in conftest.py: three existing tests already shared channel id "c1" and broke immediately, which was luck - with different ids the cross-test contamination would have travelled silently. Also translate 401 in get_team_name, which was the one method in the client that did not, so an expired token surfaced as a 502 instead of prompting re-auth.b79153c791e92dfcc32bResolve card-channel labels in two calls instead of one per channelto Faster initial card load: two calls for channel labels, and a skeleton while they arrivee92dfcc32b33650d533f33650d533fe3c63ab5c1e3c63ab5c1bde7846665