Parse a cold statement once instead of three times - #674
Conversation
A text the plan cache has not seen was parsed once to say it was a query rather than one of the four things that is not, once again to read the `USE` in front of it, and a third time to compile it. It is the same text and the same tree every time. #657 took the first of those off the warm path; this carries the tree through the miss path so there is one parse on it. `query::categorise` is `not_a_query` handing back what it parsed, and `Session::categorised` answers a three-way `Kind`: the text has a plan already, or it is a query and here is the tree, or it is not a query and here is what it is. `plan_from` and `compiled_from` take the tree when the caller has one and parse when it does not, so nothing that arrives with only the text changed. `run_with` in query.rs, which has no cache to read and so paid both parses on every call, categorises and hands the tree to `prepare_from`. The read-only refusal in db.rs sits in front of the session, and deciding whether a statement writes is a parse of its own. A text the session already holds a plan for is one that refusal let through before, so it is answered from the cache now. A cold statement on a read-only connection still pays it, about 1.7 us, because threading the tree past it would mean a parsed variant of run, run_streaming, prepare and profile. benches/compile.rs is new and measures the cold send the way the session bench measures the warm one. Two binaries, interleaved, four rounds, on this laptop: cold compile through a session 7.8 to 8.3 us -> 5.8 to 6.2 us cold send on a read-only conn 15.8 to 16.6 us -> 11.8 to 12.5 us About a quarter off either way. A warm plan cache hit is 0.04 us and is unchanged, which is the point: this is the path a shell session, a conformance corpus and the first minute of a workload are made of. While here, plan_for's doc comment had been left stranded on top of `categorised` when #655 landed, so it goes back where it belongs. Closes #658
|
I was wrong earlier in this thread when I said there was no mechanism by which this could move the write gate. There is a signal, it is consistent across three runs a side, and here is what it looks like. The absolute numbers overlap completely, which is why it read as flake at first:
Main's own values run 49.6 to 75.5, so 674's 67 to 75 is inside main's range and no single run tells you anything. What separates them is the ratio of the value to the ceiling, and that is the right thing to look at because both sides are scaled by the same host calibration, so the box speed cancels:
Three and three with no overlap. That is not flake. Now what it means, which is the part I cannot settle from CI alone. The ceiling is Take a fixed amount P off a read costing R and off a write costing W, with W larger than R. The ceiling moves by (R-P)/R and the value moves by (W-P)/W, and the second is closer to one than the first, so the ratio of value to ceiling goes up. Solving that against the observed 21 percent shift puts P at about 9 us a statement on a hosted runner, which is the right order for two parses of a short statement on a box running two to three times slower than the reference. So the same data supports two readings:
The comment on CI cannot separate the two, because the hosted runners never give the same host twice. What separates them is one A to B on one quiet box: |
Closes #658.
A text the plan cache has not seen was parsed three times: once to
decide it was a query rather than one of the four things that is not,
once to read the
USEin front of it, and once to compile it. Sametext, same tree, three times. #657 took the first of those off the warm
path. This carries the tree through the miss path so there is one parse
on it.
What changed:
query::categoriseisnot_a_queryhanding back what it parsedrather than a verdict, and
not_a_queryis a thin wrapper over it forthe callers that only want the verdict.
Session::categorisedanswers a three-wayKind: the text has a planalready, or it is a query and here is the tree, or it is not a query
and here is what it is.
run_withinandstream_inboth match on it.plan_fromandcompiled_fromtake the tree when the caller has oneand parse when it does not, so nothing that arrives with only the text
changed.
query::run_with, the one-shot path with no cache to read, categorisesand hands the tree to a new
prepare_from.db.rssits in front of the session anddeciding whether a statement writes is a parse of its own. A text the
session already holds a plan for is one that refusal let through
before, so it is answered from the cache now.
What did not change: the warm plan cache hit, at 0.04 us either side.
This is the other path, the one a shell session, a conformance corpus
and the first minute of any workload are made of.
benches/compile.rsis new and measures the cold send the waybenches/session.rsmeasures the warm one. Two binaries built frombefore and after, interleaved, four rounds, on an M-series laptop:
About a quarter off either way. Round one of the old binary read 11.29
on the session line, which is the first-run outlier every A/B on this
machine has, and it is left in the log rather than dropped from it.
One thing is deliberately left: a cold statement on a read-only
connection still pays the refusal parse, about 1.7 us of the 11.8, and
threading the tree past it would mean a parsed variant of
run,run_streaming,prepareandprofile. The repeated statement, whichis what a read-only connection actually serves, is answered from the
cache.
cargo test -p zuis green, 328 lib tests and every integration binary,and clippy and fmt are clean.