BookingCart::resolvePointer() reads the private tempstore every time it is called, and it is the way in to both accessors: current() and activeOrder() call it directly, and heldLines() calls current(). A surface that asks the cart more than one question therefore reads the same key/value row once per question.
PlaceSelectionController::act() asks several: current() to find the order, heldLines() to check the visitor's existing holds, current() again inside the branch that holds the place, and heldLines() once more to build the response. One place click on the graphical map reads tempstore.private.yoyaku_booking_cart four times, for a value that cannot change unless this same class writes it.
Measured
A performance test on the place map, following core's Gander pattern, puts a recurring click (one landing in a basket that already exists) at 25 queries. Four of them are that identical tempstore read:
- 4 x SELECT name, value FROM key_value_expire ... collection = tempstore.private.yoyaku_booking_cart
- 2 x SELECT ... FROM yoyaku_booking WHERE transaction = N, with no state filter
- 2 x SELECT ... FROM yoyaku_booking WHERE transaction = N AND state IN (...)
Why core does not already cache this
PrivateTempStore::get() delegates to DatabaseStorageExpirable::get(), which runs a plain SELECT every call with no static or request cache. That is deliberate on core's part, because the tempstore is shared across requests and another request may have changed it. It argues for the memo rather than against it: four unsynchronised reads inside one request can in principle return four different values, and a request that acts on one basket id throughout is more correct than one that re-asks and may get a different answer halfway through.
Proposed
Memoize the resolved pointer id for the request in BookingCart, invalidated by adopt() and forget(). Those are the only writers of the key and both are on this class, so the memo cannot go stale behind its own back. Expected: 25 to 22 queries per click.
Deliberately out of scope
Memoizing the lines. lineBookings() also runs twice per click and the just-saved booking is reloaded afterwards, but a hold is written in the middle of that request and the response payload has to see it, so a lines memo needs write invalidation. That is a separate and riskier change, noted here rather than folded in.
Issue fork yoyaku-3615284
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #4
mably commentedComment #6
mably commentedMerged to 1.x as 7ebea45a.
The basket pointer is now read at most once per request, with adopt() and forget() keeping the held value in step. Taking a place in an already-open basket went from 25 queries to 22.
Two performance test classes came with it, following core's Gander pattern: the per-slot booking page and the place map. They assert query counts exactly, and deliberately assert nothing about the measurements that answer to the environment rather than to this module, which is why there is no cold-cache scenario, no CacheGetCount, and no assertion on the opening click of a visit.
The rest of the remaining 22 is recorded in #3615297: Taking one place still costs twenty-two queries, four of them removable without changing a guarantee, postponed: four reductions that need no change of guarantee, and two that are decisions rather than cleanups.