Dandandan opened a new pull request, #24325:
URL: https://github.com/apache/datafusion/pull/24325

   ## Which issue does this PR close?
   
   None yet — happy to open one if useful.
   
   ## Rationale for this change
   
   `datafusion-catalog` is only 4.9k lines of source, but it takes **42s** of a
   cold `cargo build -p datafusion` (measured with `cargo build --timings`), 
and it
   sits *alone* on the critical path between `datafusion-session` and
   `datafusion-catalog-listing`. That is ~11ms per line, against 
0.09–0.55ms/line
   for crates like `datafusion-datasource` or `datafusion-physical-plan`.
   
   `-Zself-profile` says ~90% of the crate's compile time is
   `evaluate_obligation`, and ~99% of that is proving `Send`/`Sync`:
   
   | trait | time | goals |
   |---|---|---|
   | `Send` | 4.04s | 8245 |
   | `Sync` | 3.97s | 8195 |
   | everything else | 0.03s | 7355 |
   
   The mechanism: `#[async_trait]` rewrites `async fn m(&self, ..)` into a 
method
   carrying `where 'life0: 'async_trait, .., Self: 'async_trait`. Those bounds 
make
   the method's `ParamEnv` non-empty, and rustc only serves auto-trait 
obligations
   from its **global** evaluation cache when the `ParamEnv` is empty. So the
   `Send`/`Sync` proof for every type the returned future captures is redone 
from
   scratch for each `async fn` in each impl. For a `TableProvider` the captured 
set
   includes `&[Expr]`, which pulls in the whole `Expr`/`LogicalPlan` type graph
   (~500 types) through `Expr::{Exists, InSubquery, ScalarSubquery}`.
   
   Two measurements that pin this down:
   
   - 16,440 `Send`/`Sync` goals over only **1,250 distinct** `(trait, type)` 
pairs
     — a mean of 13× redundant re-proving.
   - Goals carrying a non-empty `ParamEnv` cost **~1.6ms** each; the same goals
     with an empty `ParamEnv` cost **~6µs** — 270× cheaper.
   
   Seven `#[async_trait]` impls accounted for 51.7s of the crate's 51.8s of 
trait
   solving:
   
   | impl | trait solving |
   |---|---|
   | `MemTable` | 13.7s |
   | `StreamTable` | 9.0s |
   | `CteWorkTable` | 8.9s |
   | `StreamWrite` | 6.9s |
   | `StreamTableFactory` | 4.5s |
   | `ViewTable` | 4.4s |
   | `StreamingTable` | 4.4s |
   
   ## What changes are included in this PR?
   
   For those seven impls, the future is now constructed in a small shim function
   that has **no** where-clauses, so its auto-trait obligations are proved in an
   empty `ParamEnv` and get cached globally. The trait method is left as a
   hand-written desugaring of what `#[async_trait]` would have generated, and 
only
   forwards — it never creates a coroutine of its own, so it does no auto-trait
   work.
   
   Method bodies are moved verbatim into inherent `async fn`s. Nothing else
   changes: the returned type is still `BoxFuture` (`+ Send`), and futures are
   still lazy — `Box::pin(self.m_inner(..))` does not poll anything.
   
   Isolated probe confirming the shape matters (5 trivial impls of a local
   `#[async_trait]` trait taking `&[Expr]`, added to this crate):
   
   | variant | crate build | cost of the 5 impls |
   |---|---|---|
   | no impls (baseline) | 8.31s | — |
   | `#[async_trait]` + `async fn` | 11.64s | +3.33s |
   | `async fn` delegating body to a boxed helper | 14.28s | +5.97s |
   | desugared signature + boxed shim | 8.01s | ~0 |
   
   Note the middle row: moving only the *body* out makes things worse. The
   `async fn` itself has to go, because its arguments are what the future 
captures.
   
   ## Are these changes tested?
   
   Existing tests: `cargo test -p datafusion-catalog` (9 passed) and
   `cargo test -p datafusion --lib` (442 passed). The change is mechanical and 
the
   compiler checks each rewritten signature against the trait declaration.
   
   Interleaved A/B of `cargo rustc -p datafusion-catalog --lib`, alternating 3
   times so machine drift cancels out:
   
   ```
   before: 20.5s  19.9s  18.3s
   after:   4.8s   4.5s   3.9s
   ```
   
   `evaluate_obligation` drops from 8.05s to 3.39s, and its goal count from 
25,811
   to 16,647. In a full `cargo build -p datafusion` the unit goes from 42.3s to
   ~8s.
   
   ## Are there any user-facing changes?
   
   No. No public signature changes — after macro expansion the trait methods 
have
   the same signatures as before.
   
   ### Follow-ups
   
   The same pattern applies to the rest of the serial tail of the build, which 
is
   where the remaining time is:
   
   | crate | frontend | source lines | ms/line |
   |---|---|---|---|
   | `datafusion` (core) | 74.5s | 33,144 | 2.25 |
   | `datafusion-catalog-listing` | 20.5s | 3,013 | 6.81 |
   | `datafusion-session` | 18.8s | 1,619 | 11.59 |
   
   A more general fix would be to drop `#[async_trait]` from these traits in 
favour
   of an explicit `BoxFuture` return type with a single lifetime and no
   where-clauses. That would make *every* impl cheap, including in downstream
   crates, but it is a breaking change to a public trait, so I have left it out
   here.
   
   Also worth noting separately: this workspace's `dev` profile has incremental
   compilation on, and for this trait-solving-heavy workload it costs 1.8× more 
CPU
   on a cold build (452.6s → 249.9s of CPU across the DataFusion crates with
   `CARGO_INCREMENTAL=0`). The `ci` profile already sets `incremental = false`.
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to