Skip to content

[opt](build) Add ENABLE_UNITY_BUILD and pilot unity builds on three glue targets - #66712

Merged
morningman merged 9 commits into
apache:masterfrom
morningman:be-build-opt-2-unity-glue
Aug 14, 2026
Merged

[opt](build) Add ENABLE_UNITY_BUILD and pilot unity builds on three glue targets#66712
morningman merged 9 commits into
apache:masterfrom
morningman:be-build-opt-2-unity-glue

Conversation

@morningman

@morningman morningman commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Part of the BE build-time optimization series tracked in #66715.

Split out of #66510, which carries the whole
BE build-time batch. After the header-closure surgery in #66400 and #66672,
this PR opens the second line of that batch: CMake unity builds. It adds the
infrastructure switch and converts the three lowest-risk glue segments as a pilot;
the heavier targets (Exec, Exprs, and the rest) follow in separate PRs once this
one has proven the mechanism cross-platform.

What problem does this PR solve?

Related PR: #66510, #66672

Problem Summary:

Most of the BE's cold-build time is not spent compiling our code — it is spent
re-parsing the same shared header closure once per small .cpp. For the glue
directories this ratio is extreme: the 51 InformationSchema scanners cost ~124 CPU s
(with PCH) of which almost everything is the closure re-parse; the ~58 http handlers
sum to ~5.6 min of slot time for a few thousand lines of handler logic.

CMake's built-in UNITY_BUILD (CMake ≥ 3.16; we require 3.19.2) concatenates groups
of .cpp files into jumbo TUs, so a closure is parsed once per batch instead of
once per file. This PR:

  1. Adds the switchoption(ENABLE_UNITY_BUILD ON) in be/CMakeLists.txt,
    plumbed through build.sh and run-be-ut.sh exactly like ENABLE_PCH,
    overridable from the environment / custom_env.sh. Every per-target
    UNITY_BUILD property is set unconditionally from it -- including an explicit
    OFF, so the switch also wins over CMake's own CMAKE_UNITY_BUILD in a reused
    cache. Turn it OFF for per-file diagnostics, per-file tooling (clang-tidy,
    coverage), or the finest-grained incremental rebuilds.

  2. Pilots unity on three glue segments (and only there — everything else is
    explicitly opted out per file, not by omission):

    • InformationSchema: all 51 scanner TUs → 1 unity TU (UNITY_BUILD_BATCH_SIZE 0).
    • Service, scoped to http/: ~58 handler TUs → 1 unity TU. The non-http
      service sources (service entry points, arrow_flight) are heterogeneous heavy TUs
      that gain nothing from merging and stay individual.
    • Storage, scoped to index/: ~111 TUs sharing one CLucene-heavy closure →
      4 unity TUs of ≤ 32 sources (batch size bounds jumbo-TU size and memory).
  3. Makes the merged TUs legal C++ with two hygiene commits that stand on their
    own even without unity:

    • 27 http action files each carried a private copy of the same file-scope
      constants (HEADER_JSON ×16, TABLET_ID ×7, SCHEMA_HASH ×4, …). They move to
      one shared header as C++17 inline variables, values unchanged.
    • prefix_query declared get_prefix_terms(IndexReader*) relying on a
      header-level CL_NS_USE(index) using-directive. Inside doris::segment_v2 that
      unqualified name silently flips to doris::segment_v2::IndexReader as soon as
      any sibling source brings that type into scope — a landmine with or without
      unity. The declaration now spells out lucene::index::IndexReader.
  4. Fixes a latent bug unity exposed: schema_scanner_helper.h opened with
    #ifndef _SCHEMA_SCANNER_HELPER_H_ but never defined the macro (and one include
    sat outside the guard), so the guard never worked. Harmless while every TU
    included it exactly once; breaks immediately under unity. Now #pragma once.

  5. Keeps the compile-bench tooling honest under unity — with the default ON,
    build.sh --compile-bench produces a build directory whose sources are jumbo TUs,
    which two of the already-merged tools silently mis-read: cut_impact.py dropped
    every unity dependency block (and with it all 211 pilot sources) from its TU set,
    and report.py filed each batch's time under the build directory instead of the
    module it came from. cut_impact.py now refuses a unity build directory with the
    rebuild instruction — its analysis needs per-source closures, and splitting a
    batch's union back over its members would over-estimate instead of under-report —
    report.py attributes a unity TU to its target's directory, and
    rebuild_radius.py says when its per-header counts are batch-granular.
    build-support/tests/test-compile-bench-unity.sh pins all three behaviours.

Files whose file-scope macros must not leak into unity siblings stay individual via
SKIP_UNITY_BUILD_INCLUSION: http_parser.cpp (CR/LF), be_thread_stack_action.cpp
(UNW_LOCAL_ONLY), and four index files (CL_MAX_PATH and friends,
IS_CHINESE_CHAR, APPLY_FOR_PRIMITITYPE). A future file whose file-scope symbols
clash inside a unity TU can opt out the same way.

Measured results

All numbers from the development branch this series is split from (which also
carried the #66672 include cuts), macOS arm64 + clang 20, -j14,
ENABLE_PCH=ON, cold builds, back-to-back A/B:

metric before after
build phase wall 11m38s 10m16s (-82.4 s / -11.8%)
Σ per-TU CPU 147.9 min 134.8 min (-8.9%)
TU count 8384 8180
failures 0 0

Per-segment slot time (sum of compile time the segment occupies across slots):

segment before after ratio
information_schema 221.7 s 17.0 s 13×
service/http 273.6 s 26.3 s 10.4×
storage/index 264.2 s 120.1 s 2.2× (remainder includes the four macro SKIPs)

Why unity rather than more PCH: a serial cold micro-benchmark of the
InformationSchema target alone measured 9.9× with PCH (124 s → 12.5 s) and
14.6× without PCH (206 s → 14.1 s) — and unity without PCH still beats
individual TUs with PCH by 8.8×. Unity removes the repeated parse instead of
amortizing it, and the two compose.

Side effects on artifacts: libInformationSchema.a 334 MB → 29 MB, libStorage.a
1492 MB → 1318 MB (linkonce_odr instantiations dedup inside each unity TU). The
largest unity TU peaks at 2.1 GB RSS — below the largest existing individual TU in
the tree (3.9 GB), so -jN memory envelopes are unchanged.

Risk and verification

  • Unity changes TU grouping only; no code changes ride along beyond the two
    hygiene commits described above (constant dedup with identical values, one
    qualified name, one include guard).
  • Archive symbol parity was checked per target on the development branch:
    InformationSchema keeps all 2303 external defined symbols (plus 5 weak
    unique_ptr<SchemaXxxScanner> instantiations that dedup), Service keeps all 4054,
    Storage keeps all external symbols with 8 weak linkonce_odr template
    instantiations deduping away — which is the point of unity, not a loss.
  • This exact branch, rebased onto current master, full BE build from scratch
    (macOS arm64, clang 20, ENABLE_PCH=ON, ENABLE_UNITY_BUILD=ON):
    8349/8349 ninja edges, zero failures, doris_be links. The six expected unity
    TUs (InformationSchema ×1, Service http ×1, storage/index ×4) all compile.
    This includes schema_tso_status_scanner.cpp, added upstream after the pilot was
    measured — it lands inside the InformationSchema unity TU via the existing
    GLOB_RECURSE with zero CMakeLists edits, which is the intended maintenance story.
  • The OFF path is verified on the same tree: reconfiguring with
    ENABLE_UNITY_BUILD=OFF removes every unity_*.cxx entry from
    compile_commands.json and flips exactly 219 ninja edges — the three targets'
    per-file objects plus their archives and the final link, nothing else. All of them
    compile per-file with zero failures and doris_be links again. The blast radius
    of the switch is precisely the three pilot targets.
  • OFF also wins over a native-unity cache: configuring with
    -DENABLE_UNITY_BUILD=OFF -DCMAKE_UNITY_BUILD=ON produced 28 unity TUs across the
    three pilot targets before the gate fix and 0 after, while the ordinary
    -DENABLE_UNITY_BUILD=ON configure still produces exactly the 6 advertised
    batches (InformationSchema ×1, Service ×1, Storage ×4).
  • These three segments have been building as unity TUs on the development branch
    since 2026-08-08
    , through repeated full-tree builds and the BE UT builds that
    verified [opt](build) Cut three more waves of hot include edges in the BE header graph #66672 (the UT binaries link against these same target libraries).

Proactive disclosure

  • Cross-platform is the blind spot, and the default is deliberately ON so this
    PR's own CI closes it.
    Every local build and measurement above is macOS arm64 +
    clang 20. Nothing here is platform-specific by construction, but with the default
    ON, the Linux compile lanes and every regression pipeline in this PR's CI run
    against unity builds — that is the validation. Please give the Linux gcc lane in
    particular a look. If some environment trips over unity after merge, the
    escape hatches are, in order: per-user ENABLE_UNITY_BUILD=OFF (env or
    custom_env.sh), per-file SKIP_UNITY_BUILD_INCLUSION, or a one-line default
    flip — the infrastructure stays either way.
  • The incremental-rebuild trade-off is real: touching one .cpp inside a unity
    batch recompiles the whole batch. For the glue chosen here a batch compiles in
    ~16–30 s, comparable to single mid-weight TUs elsewhere in the tree; and
    ENABLE_UNITY_BUILD=OFF restores per-file granularity for workflows that need it.
    This is also why the pilot targets are glue directories and not the hot-edit paths.
  • contrib (openblas/clucene) was evaluated and deliberately left alone: the
    f2c-generated LAPACK sources and the snowball stemmers define clashing file-scope
    statics (static c__1 and friends) — structurally un-unifiable without rewriting
    generated code — and contrib compiles once and rarely changes.
  • The rest of Storage and Service is opted out per file, on purpose. Merging
    heavy heterogeneous TUs earns nothing (the closure parse is not the dominant cost
    there) and risks monster TUs. Follow-up PRs extend unity to Exec, Exprs and the
    remaining targets with the same SKIP discipline; on the development branch the
    full rollout takes the same tree from 10m16s to 6m18s.

Release note

None

Check List (For Author)

  • Test

    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change. (full BE build + BE UT builds on the development branch; symbol-parity checks per target)
  • Behavior changed:

    • No.
  • Does this need documentation?

    • No.

morningman and others added 5 commits August 13, 2026 12:02
Unity builds merge groups of .cpp files into jumbo translation units for
much faster full builds, but three workflows need them off: per-file
diagnostics while iterating on a single source, per-file tooling
(clang-tidy, coverage), and A/B isolation when debugging a unity-only
failure. Introduce the knob ahead of the first unity targets (following
commits) so every per-target UNITY_BUILD property is gated on it from
day one:

- be/CMakeLists.txt: option(ENABLE_UNITY_BUILD ON), reported via message()
- build.sh: pass -DENABLE_UNITY_BUILD through (default ON, overridable
  from the environment / custom_env.sh, same pattern as ENABLE_PCH)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gdfkk7RqgD5e3Uv7bTM3NV
The header opened with `#ifndef _SCHEMA_SCANNER_HELPER_H_` but never
defined the macro (and one include sat outside the guard), so the guard
never took effect. Harmless while every TU included it exactly once; it
breaks immediately under a unity build. Use #pragma once like the other
50 headers in this directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 51 schema-scanner TUs are homogeneous glue code whose compile cost
is dominated by re-parsing the same header closure once per file.
Merging them into a single unity TU (UNITY_BUILD_BATCH_SIZE 0) measured
on clang 20 arm64, serial, cold file cache:

- sum CPU with PCH: 124s -> 12.5s (9.9x); without PCH: 206s -> 14.1s
  (14.6x). Unity without PCH still beats individual TUs with PCH (8.8x).
- archive symbol parity: all 2303 external defined symbols preserved
  (plus 5 harmless weak unique_ptr<SchemaXxxScanner> instantiations)
- __text shrinks 1.07MB -> 0.50MB (linkonce_odr dedup) and
  libInformationSchema.a shrinks 334MB -> 29MB
- exposed one latent bug: the broken include guard fixed in the
  previous commit

The UNITY_BUILD property is gated on ENABLE_UNITY_BUILD (previous
commit). A future file whose file-scope symbols clash inside the unity
TU can opt out via SKIP_UNITY_BUILD_INCLUSION.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Reader

Preparation for merging these sources into unity TUs:

- 27 http action files each carried a private copy of the same file-scope
  constants (HEADER_JSON x16, TABLET_ID x7, SCHEMA_HASH x4, OP/PATH/
  TOKEN_PARAMETER/MEBIBYTE x2). Move them to one shared header,
  service/http/action/action_constants.h, as C++17 inline variables.
  Values are unchanged; two now-empty anonymous namespaces are removed.

- prefix_query declared get_prefix_terms(IndexReader*) relying on a
  header-level CL_NS_USE(index) using-directive. Inside doris::segment_v2
  that unqualified name flips to doris::segment_v2::IndexReader as soon
  as any sibling source brings that type into scope. Spell out
  lucene::index::IndexReader in the declaration and definition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second and third targets of the unity rollout (after InformationSchema):

- Service: merge the ~58 http handler TUs into one unity TU. Non-http
  sources (service entry points, arrow_flight) and two macro-defining
  files (http_parser.cpp, be_thread_stack_action.cpp) stay individual.
  Unity TU compiles in 16s where the members summed to ~5.6min of
  slot time in the cold bench; archive keeps all 4054 external symbols.

- Storage: merge the storage/index/ subtree (~111 TUs, CLucene-heavy
  closure) into four unity TUs of <=32 sources; the rest of Storage is
  explicitly opted out per file. Four macro-defining index files stay
  individual. Archive keeps all external symbols (8 weak linkonce_odr
  template instantiations dedup away) and shrinks 1492MB -> 1318MB.

Both UNITY_BUILD properties are gated on ENABLE_UNITY_BUILD; the SKIP
lists stay unconditional (inert without unity).

contrib (openblas/clucene) was evaluated and intentionally left alone:
the f2c-generated LAPACK sources and snowball stemmers define clashing
file-scope statics, and contrib compiles once and rarely changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100% (0/0) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 76.09% (32566/42802)
Line Coverage 61.03% (365946/599628)
Region Coverage 57.74% (308105/533630)
Branch Coverage 59.06% (139744/236610)

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review status: capped/incomplete after the third and final review round. The final round surfaced two additional accepted tooling defects, so this should not be read as convergence.

I found four actionable build/configuration issues, attached inline: the OFF switch is dropped by the BE UT/UT-coverage configure path; native CMake unity can override the advertised custom OFF state; the default unity dependency shape is silently discarded by cut_impact.py; and compile-benchmark directory attribution is redirected into the build-tree bucket.

Checkpoint conclusions:

  • Goal and proof: The default-ON pilot does form the intended current batches (52 InformationSchema, 58 service/http, and 101 storage/index sources), and completed Linux/macOS compile, BE UT, coverage, formatting, and regression checks are green. Those results prove the ordinary ON path, not the four deterministic negative/tooling paths reported here.
  • Smallness and clarity: The source-level qualification, guard repair, and constant consolidation are focused. The configuration/tool integration is not complete enough for a default-on switch.
  • Concurrency and lifecycle: No runtime thread, lock, shared mutable ownership, memory-accounting, or cleanup path changes. Exact unity batch order, macro/lookup leakage, archive grouping, and static initialization/destruction were reviewed; no additional defect was substantiated.
  • Configuration and parallel paths: Normal build, clean/reused cache, BE UT, UT coverage, compile-bench, clang-tidy, native/custom unity controls, PCH, optional TLS/Variant, ANN, and platform paths were traced. The four comments are the unresolved implementation gaps; clang-tidy command inference was separately reproduced and the earlier silent-skip concern was dismissed.
  • Compatibility and data correctness: No FE/BE protocol, callable ABI, storage format, persisted metadata, transaction, visibility, delete-bitmap, nullable-column, or rolling-upgrade behavior changes. The explicit CLucene type names the same type and preserves ownership/cleanup.
  • Conditions, errors, and observability: Runtime status/exception behavior is unchanged. The build-tool failures are silent or misleading specifically where called out inline; runtime logs and metrics do not need changes.
  • Tests and results: No runtime golden result is required for the source-hygiene changes, but negative fixtures are needed for clean UT OFF propagation, native-ON/custom-OFF precedence, unity-shaped Ninja dependencies, and unity-path report grouping.
  • Performance: The reported clean-build speed/RSS evidence and bounded Storage batches are credible; no runtime regression was found. The benchmark attribution issue prevents its directory report from accurately explaining the optimized segments.
  • Other/user focus: All 36 changed paths and every current unity sibling were reviewed. No additional user focus was supplied; the full PR was reviewed without further substantiated issues.

Because new valuable findings appeared in Round 3, this review is submitted as capped/incomplete under the review contract even though every current candidate has been accepted, deduplicated, or dismissed with evidence.

Comment thread build.sh
-DSTRIP_DEBUG_INFO="${STRIP_DEBUG_INFO}" \
-DDISPLAY_BUILD_TIME="${DISPLAY_BUILD_TIME}" \
-DENABLE_PCH="${ENABLE_PCH}" \
-DENABLE_UNITY_BUILD="${ENABLE_UNITY_BUILD:-ON}" \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new switch is only forwarded by build.sh. run-be-ut.sh has its own clean CMake configure (run-be-ut.sh:321-341) and does not pass this value, so ENABLE_UNITY_BUILD=OFF ./run-be-ut.sh --clean ... still recreates the cache with this option's default ON. That means the advertised per-file escape hatch cannot be used for the standard BE UT/UT-coverage workflow when diagnosing a unity-only failure. Please mirror this plumbing and effective-value logging in run-be-ut.sh.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0dcc67crun-be-ut.sh now forwards -DENABLE_UNITY_BUILD and reports the effective value next to ENABLE_PCH.

One correction to the report: the gap was wider than --clean. Since the script never passed the variable at all, the very first configure cached the option default ON and no later run could change it, so ENABLE_UNITY_BUILD=OFF ./run-be-ut.sh was inert with or without --clean.

# Batch size 0 merges all sources into a single unity TU (~10x faster than
# compiling them individually). A new file whose file-scope symbols clash
# inside the unity TU can opt out via SKIP_UNITY_BUILD_INCLUSION.
if (ENABLE_UNITY_BUILD)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This false branch is not authoritative if the cache already has CMake's standard CMAKE_UNITY_BUILD=ON. That variable initializes each target's UNITY_BUILD property when add_library runs; with ENABLE_UNITY_BUILD=OFF, this block simply leaves the inherited ON value untouched. I reproduced ENABLE_UNITY_BUILD=OFF, CMAKE_UNITY_BUILD=ON, and UNITY_BUILD=ON, with a database containing only a generated unity source. The same positive-only gate appears in Service and Storage, so the advertised OFF escape hatch can fail on a reused native-unity cache. Please explicitly set the pilot targets' property OFF when this option is OFF.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e1299ba — agreed, and thanks for reproducing it. The option is now normalized to a strict ON/OFF once in be/CMakeLists.txt, and all three pilot targets set UNITY_BUILD ${DORIS_UNITY_BUILD} unconditionally, so OFF is an explicit OFF rather than an empty branch.

Verified on this tree with your exact repro, -DENABLE_UNITY_BUILD=OFF -DCMAKE_UNITY_BUILD=ON: 28 unity TUs across InformationSchema/Service/Storage before the change, 0 after. The ordinary -DENABLE_UNITY_BUILD=ON configure still produces exactly the 6 advertised batches (1 + 1 + 4).

Normalizing has a second benefit: an empty -DENABLE_UNITY_BUILD= forwarded by a script would otherwise reach set_target_properties as a missing value and fail the configure with an argument-count error.

Comment thread be/CMakeLists.txt
# Merge groups of .cpp files into jumbo translation units for much faster full
# builds. Turn OFF for precise per-file diagnostics, per-file tooling
# (clang-tidy/coverage), or the finest-grained incremental rebuilds.
option(ENABLE_UNITY_BUILD "Enable CMake unity builds for BE targets" ON)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaulting this option ON makes the standard compile-benchmark dependency model silently incomplete. cut_impact.py::load_tus() accepts a Ninja dependency block only when its first dependency is an original source under be/src or gensrc/build; a unity block starts with a generated .../Unity/unity_N_cxx.cxx, so it discards the whole block, including all later original-source/header dependencies. The real loader retained the standalone fixture but returned no TU for the equivalent unity shape. Consequently edge/audit/why omit all six pilot batches (211 sources). Please expand unity blocks or fail explicitly with a non-unity rebuild instruction.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 90d33da, taking the second of your two options: load_tus() now counts unity blocks and exits with the rebuild instruction (ENABLE_UNITY_BUILD=OFF ./build.sh --compile-bench) instead of returning a truncated TU set.

Expanding the block is not a safe alternative. A unity block carries the union of its members' closures, so attributing it back to each member would make every source look like it reaches every header any sibling pulls in — inflating both the affected-TU counts and the seeding advice that edge/audit emit. Silent under-coverage would be traded for silent over-estimation, and this tool exists to be precise about exactly that.

Fixture added: build-support/tests/test-compile-bench-unity.sh feeds load_tus() a canned ninja -t deps database in both shapes and asserts the standalone block is still kept and the unity block is refused with the instruction.

Comment thread be/CMakeLists.txt
# Merge groups of .cpp files into jumbo translation units for much faster full
# builds. Turn OFF for precise per-file diagnostics, per-file tooling
# (clang-tidy/coverage), or the finest-grained incremental rebuilds.
option(ENABLE_UNITY_BUILD "Enable CMake unity builds for BE targets" ON)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This default also breaks the compile benchmark's advertised per-directory attribution for every pilot batch. The timing wrapper records the generated source path (be/build_<Type>_compile_bench/src/<dir>/.../Unity/unity_N_cxx.cxx), but report.py::group_keys() recognizes logical modules only when a path starts with be/src. With the real helpers, an InformationSchema source groups under be/src/information_schema, while its unity source groups under be / be/build_Release_compile_bench. The report therefore hides the optimized segments' costs in a build-directory bucket. Please normalize unity paths to logical target/source directories and add a report fixture.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 90d33dareport.py maps a generated unity source back to its target's directory before grouping, so be/build_<Type>_compile_bench/src/storage/CMakeFiles/Storage.dir/Unity/unity_0_cxx.cxx now rolls up under be/src/storage instead of the build-tree bucket. The regex was checked against the paths a real configure generates for the six pilot batches.

One limit worth stating rather than hiding: attribution is only as fine as the target's own directory. The Service batch merges just be/src/service/http sources but lands on be/src/service, and the Storage batches land on be/src/storage rather than be/src/storage/index. A unity TU is one timing for all of its members by construction, so second-level attribution inside a batch is not recoverable from the timing data — only from an ENABLE_UNITY_BUILD=OFF run.

Fixture added in build-support/tests/test-compile-bench-unity.sh (unity path, ordinary source, and a non-unity build-tree path).

morningman and others added 4 commits August 13, 2026 18:20
run-be-ut.sh runs its own cmake configure and did not forward the new switch,
so the option default (ON) always won there: `ENABLE_UNITY_BUILD=OFF
./run-be-ut.sh` was silently ignored, with or without --clean, because the
first configure caches ON and nothing ever overrides it. That removes the
escape hatch exactly where it is needed most -- diagnosing a unity-only
failure in the BE UT / UT-coverage build.

Forward the value and report it, mirroring ENABLE_PCH.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gdfkk7RqgD5e3Uv7bTM3NV
…TY_BUILD

Each pilot target set its UNITY_BUILD property inside `if (ENABLE_UNITY_BUILD)`,
which is a one-way gate: OFF took the empty branch and left the property alone.
CMake's own CMAKE_UNITY_BUILD initializes UNITY_BUILD when add_library runs, so
a cache carrying CMAKE_UNITY_BUILD=ON kept the pilot targets unity-built while
the build advertised the opposite.

Normalize the option to a strict ON/OFF once and set the property
unconditionally from it, so OFF is an explicit OFF. Normalizing also keeps an
empty -DENABLE_UNITY_BUILD= (an unset variable forwarded by a script) from
turning into a malformed set_target_properties call.

Verified on this tree with `-DENABLE_UNITY_BUILD=OFF -DCMAKE_UNITY_BUILD=ON`:
28 unity TUs across the three pilot targets before, 0 after; the ordinary
`-DENABLE_UNITY_BUILD=ON` configure still produces exactly the 6 advertised
batches (InformationSchema 1, Service 1, Storage 4).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gdfkk7RqgD5e3Uv7bTM3NV
…n units

Both compile-bench tools read `ninja -t deps` / the timing records of a build
directory that, with unity builds on by default, no longer has one entry per
source file. Neither noticed:

- cut_impact.py accepted a dependency block only when its first dependency was
  an original source, so a block starting with a generated unity .cxx was
  dropped whole -- along with every original source and header listed after it.
  `edge`/`audit`/`why` then ran on a silently truncated TU set. It cannot be
  repaired by attributing the batch closure to each member: that union would
  make every member look like it reaches every header a sibling pulls in,
  inflating both the affected counts and the seeding advice. So refuse the
  build dir and say how to rebuild it.
- report.py grouped by directory only for paths under be/src, so every unity
  batch landed in a be/build_<Type>_compile_bench bucket instead of the module
  it came from -- hiding the cost of exactly the segments unity optimizes.
  Map a unity source back to its target's directory before grouping.

Both behaviours are invisible on a normal build (one is a refusal, the other a
grouping key), so build-support/tests/test-compile-bench-unity.sh pins them
with a canned unity deps database and a unity path fixture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gdfkk7RqgD5e3Uv7bTM3NV
rebuild_radius.py counts, per header, how many object targets depend on it. A
unity build answers that question at batch granularity: the 51 InformationSchema
scanners become one object, so a header they all include reports 1 dependent
instead of 51. The number is correct for that configuration but not comparable
with a per-file run, which is how the tool is normally used.

Say so in the output when unity objects are present, rather than letting the
count quietly change meaning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gdfkk7RqgD5e3Uv7bTM3NV
@morningman

morningman commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Pushed fixes for all four findings from the review above (each answered inline):

# finding fix
1 run-be-ut.sh drops the switch 0dcc67c — forwards -DENABLE_UNITY_BUILD and logs the effective value, mirroring ENABLE_PCH
2 if (ENABLE_UNITY_BUILD) is a one-way gate e1299ba — option normalized to strict ON/OFF, all three pilot targets set UNITY_BUILD unconditionally from it
3 cut_impact.py silently discards unity blocks 90d33da — refuses a unity build dir with the rebuild instruction
4 report.py files unity time under the build dir 90d33da — unity TUs attributed to their target's directory

Plus 6bb5239, which was not in the review: rebuild_radius.py reads the same ninja -t deps database at per-object granularity, so under unity a header included by all 51 InformationSchema scanners reports 1 dependent instead of 51. Same class of silent meaning-change as #3, so it now says when its counts are batch-granular.

Verification:

  • CMake gate, real configure of this tree: -DENABLE_UNITY_BUILD=OFF -DCMAKE_UNITY_BUILD=ON gave 28 unity TUs across the pilot targets before, 0 after; -DENABLE_UNITY_BUILD=ON still gives exactly the 6 advertised batches (1 + 1 + 4). An empty -DENABLE_UNITY_BUILD= now normalizes to OFF instead of failing the configure with an argument-count error.
  • Tooling, build-support/tests/test-compile-bench-unity.sh (new, 6 checks, passing): a canned ninja -t deps database in both shapes — the standalone block is still loaded, the unity block is refused with the ENABLE_UNITY_BUILD=OFF instruction — plus unity / ordinary / non-unity-build-tree path grouping.

Two notes where the fix deliberately differs from the suggestion:

  • For 3 the review offered "expand unity blocks or fail explicitly". Expanding is not sound: a unity block carries the union of its members' closures, so attributing it back per member would make every source look like it reaches every header a sibling pulls in, inflating the affected counts and the seeding advice. Silent under-coverage would become silent over-estimation, so the tool refuses.
  • For 4, attribution is only as fine as the target's own directory: the Service batch merges just service/http sources but rolls up under be/src/service, and the Storage batches under be/src/storage. A unity TU is one timing for all its members by construction — finer attribution is only recoverable from an ENABLE_UNITY_BUILD=OFF run.

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review status: converged in Round 3. All three final-round reviewers returned NO_NEW_VALUABLE_FINDINGS; every candidate was validated, deduplicated, accepted, or dismissed with concrete evidence.

I found two actionable P2 tooling issues, attached inline: compile-benchmark results do not persist/display the effective unity mode, and the natural-closure sweep can false-clean a source through earlier unity-batch members.

Checkpoint conclusions:

  • Goal and scope: The PR's core goal is faster cold/full BE builds through default-on unity for three pilot targets with an authoritative OFF escape hatch. The normalized target properties, collision repairs, and build/UT propagation implement that core goal, but the two companion-tool paths above remain incomplete. The changes are otherwise focused.
  • Correctness and lifecycle: All admitted InformationSchema, Service HTTP, and Storage/index sources were inventoried for macro/pragma leakage, lookup collisions, anonymous/header-internal state, static initialization/destruction, archive extraction, and configuration-specific admission. The shared HTTP constants preserve their values and immutable use, and the CLucene qualification preserves the intended type. No additional C++ runtime or lifecycle defect was substantiated.
  • Concurrency: No Doris runtime threads, locks, ownership, transactions, persistence, or shared mutable data are changed. The build timing JSONL remains serialized by the existing flock.
  • Configuration and parallel paths: Normal BE, BE UT, inherited native unity, PCH, compile-benchmark, clang-tidy, coverage, TU/syntax/closure probes, and Ninja dependency tools were traced. ON/OFF propagation is correct in normal and UT configure paths. The natural-closure path is not source-isolated, and archived benchmark configuration is not self-describing, as called out inline.
  • Compatibility and data safety: There is no FE/BE protocol, storage format, persisted Doris metadata, transaction, data-write, or rolling-upgrade change. Runtime error/status behavior is unchanged.
  • Conditions and observability: Current HTTP/parser/index skip rules and TLS, Variant, ANN, test, compiler, and macOS branches were checked. Configure output reports the effective option; persistent benchmark observability is the missing boundary in the first comment.
  • Tests and evidence: build-support/tests/test-compile-bench-unity.sh, relevant Bash syntax checks, Python syntax checks, and git diff --check passed. Minimal CMake/Ninja probes reproduced the closure false clean and confirmed that removed unity targets are filtered after an ON-to-OFF reconfigure, which dismissed the stale-dependency candidate. No Doris build was run, as required by the review prompt. The added fixture does not cover either accepted issue.
  • Performance: The intended trade is fewer repeated header parses for coarser incremental/object granularity and larger TUs. Storage is bounded at 32 sources per batch, the two smaller targets intentionally use batch size 0, and OFF remains available. No additional concrete performance defect was found.
  • User focus: No additional user-provided focus was supplied; all 41 changed paths and the full PR were reviewed.

Comment thread build.sh
-DSTRIP_DEBUG_INFO="${STRIP_DEBUG_INFO}" \
-DDISPLAY_BUILD_TIME="${DISPLAY_BUILD_TIME}" \
-DENABLE_PCH="${ENABLE_PCH}" \
-DENABLE_UNITY_BUILD="${ENABLE_UNITY_BUILD:-ON}" \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Record unity mode in compile-benchmark metadata

This forwards a setting that changes the benchmark's TU population and timing identities, but compile_bench_init persists PCH/toolchain/generator/build type without the effective ENABLE_UNITY_BUILD, so summary.json and the human report cannot say whether an archived run used the generated pilot TUs or the original sources. The prior UT thread fixed configure propagation; it does not make benchmark results self-describing. Please persist and display the effective mode, and add an ON/OFF metadata fixture.

Comment thread be/CMakeLists.txt
# Merge groups of .cpp files into jumbo translation units for much faster full
# builds. Turn OFF for precise per-file diagnostics, per-file tooling
# (clang-tidy/coverage), or the finest-grained incremental rebuilds.
option(ENABLE_UNITY_BUILD "Enable CMake unity builds for BE targets" ON)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep the natural-closure sweep source-isolated

The earlier cut_impact.py fix now refuses this unity database, but closure-sweep.sh still runs syntax_sweep.py --no-pch, whose stated guarantee is to check each source's natural include closure. That tool accepts a generated unity file as one TU and compiles its ordered .cpp includes together, so declarations, headers, or macros from an earlier member can make a broken later member pass; baseline failures also collapse to a batch name. Please make this sweep refuse unity with the existing OFF rebuild instruction or replay every member independently, and add a masking-sibling fixture.

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 60.28% (26483/43933)
Line Coverage 44.75% (270334/604155)
Region Coverage 40.49% (215712/532774)
Branch Coverage 41.96% (99344/236744)

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100% (0/0) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 74.76% (32001/42806)
Line Coverage 59.31% (355664/599648)
Region Coverage 55.63% (296879/533636)
Branch Coverage 56.47% (133609/236618)

@hello-stephen

Copy link
Copy Markdown
Contributor

run performance

@github-actions github-actions Bot added the approved Indicates a PR has been approved by one committer. label Aug 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR approved by at least one committer and no changes requested.

@eldenmoon eldenmoon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@airborne12 airborne12 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@morningman
morningman merged commit 7b00434 into apache:master Aug 14, 2026
41 checks passed
CalvinKirs pushed a commit that referenced this pull request Aug 14, 2026
…per-db scanners (#66769)

### What problem does this PR solve?

Related PR: #66732, #66712

Problem Summary:

`schema_per_db_scanner.h` gives its `unique_ptr` member a default member
initializer:

```cpp
std::unique_ptr<Block> _fetched_block = nullptr;
```

That initializer makes gcc instantiate `~unique_ptr<Block>()` in
**every** translation unit that
includes the header, and the instantiation needs `Block` to be a
complete type. None of the seven
`SchemaPerDbScanner` subclasses include `core/block/block.h`, so each of
them fails to compile with
g++ 15:

```
In instantiation of 'void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = doris::Block]':
bits/unique_ptr.h:399:17:   required from 'std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = doris::Block]'
schema_per_db_scanner.h:68:45:   required from here
   68 |     std::unique_ptr<Block> _fetched_block = nullptr;
      |                                             ^~~~~~~
bits/unique_ptr.h:91:23: error: invalid application of 'sizeof' to incomplete type 'doris::Block'
```

Declaring the destructor out of line -- which the header already does --
does not help: the default
member initializer is what marks `~unique_ptr<Block>()` as used. clang
does not instantiate the
destructor there, which is why only the gcc build reports it.
`SchemaScanner::_data_block` has the
same type and the same forward-declared `Block` and is fine, because it
has no initializer.

Dropping the `= nullptr` leaves the member null just the same, and costs
nothing at build time --
the alternative, including `core/block/block.h` from the header, would
pull that closure into seven
more translation units.

Note that the failure is currently masked on master: since #66712 turned
`ENABLE_UNITY_BUILD` on by
default, all of `information_schema` is merged into a single unity TU,
and
`schema_per_db_scanner.cpp` in that TU includes `core/block/block.h`, so
`Block` ends up complete for
its neighbours. Building with `ENABLE_UNITY_BUILD=OFF` still fails, as
does any file that later opts
out via `SKIP_UNITY_BUILD_INCLUSION`.

### Release note

None

### Check List (For Author)

- Test
    - [x] No need to test or manual test. Explain why:
- [x] This is a refactor/code format and no logic has been changed.

Verified as a build fix instead: reproduced the exact diagnostic with
g++ 15.2 on a reduced case
and confirmed it compiles clean after removing the initializer, then
re-checked the real
translation units (`schema_key_column_usage_scanner.cpp`,
`schema_partitions_scanner.cpp`,
`schema_table_options_scanner.cpp`, `schema_per_db_scanner.cpp`) against
the edited header.

- Behavior changed:
    - [x] No.

- Does this need documentation?
    - [x] No.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
morningman added a commit to morningman/doris that referenced this pull request Aug 14, 2026
Fourth target of the unity rollout (after InformationSchema, the http
glue and storage/index in apache#66712). 167 of 174 Exec TUs join unity
batches of <=12 sources (14 unity TUs), gated on the ENABLE_UNITY_BUILD
knob so -DENABLE_UNITY_BUILD=OFF still compiles every TU individually.
Opted out: five files whose file-scope macros must not leak into unity
siblings, plus the two heaviest template-instantiation TUs
(operator.cpp, hashjoin_build_sink.cpp) which would dominate any batch
they join; scan_operator.cpp is both.

Measured on the validation build (-j14 + PCH): the 14 unity TUs take
425s of slot time under full parallel load where their members summed
to ~1418s in the cold bench (3.3x); the whole target drops from 1533s
to 539s. The largest unity TU compiles in 32s / 2.6GB RSS standalone,
below the existing per-TU peaks elsewhere in the build. The archive
keeps all external symbols (three weak linkonce_odr template
instantiations dedup away) and shrinks 2450MB -> 641MB.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ND7L1ZVTJf91TBpLwYSqct
morningman added a commit that referenced this pull request Aug 14, 2026
> Part of the BE build-time optimization series tracked in #66715.
>
> Split out of **#66510, which
carries the whole
> BE build-time batch. #66712 introduced the `ENABLE_UNITY_BUILD` switch
and piloted
> unity builds on three low-risk glue targets. This PR extends unity to
**Exec and
> Exprs** — the two heaviest targets in the BE build and the largest
single source of
> the unity line's win. The remaining targets follow in one more PR.

### What problem does this PR solve?

Related PR: #66510, #66712

Problem Summary:

Same mechanism as #66712: most of the cold-build cost of glue-heavy
targets is
**re-parsing the shared header closure once per small `.cpp`**, and
CMake's
`UNITY_BUILD` makes a batch pay that parse once. What is new here is the
scale —
Exec (174 TUs, `libExec.a` 2450 MB) and Exprs (287 TUs, `libExprs.a`
2721 MB) are
the two heaviest targets in the tree, and their glue shares the heaviest
closures
(`operator.h`/`dependency.h` for Exec; the vexpr/factory closure for
Exprs).

The four commits:

1. **Deduplicate exec file-scope names that clash under unity** (no
behavior
change): `file_scanner.cpp`/`file_scanner_v2.cpp` both defined the
Iceberg
delete content codes and `is_iceberg_position_deletes_sys_table()` in
anonymous
   namespaces — the shared trio moves to `iceberg_scan_semantics.h`
(`file_scanner_v2_test.cpp` carried a third copy, kept file-local by
#66615
because this header move had not landed yet; it now uses the header
too).
`vtablet_writer.cpp`/`vtablet_writer_v2.cpp` both defined a file-scope
   `CLOSE_WAIT_EVENT_FALLBACK_MS` — scoped into `IndexChannel` and
`VTabletWriterV2`; v2's file-scope `on_partitions_created()` trampoline
renamed
`on_partitions_created_v2` (the two functions cast to different writer
types).
   `exchange_sink_operator.cpp`'s namespace-scope `timer_name` renamed
`wait_for_dependency_timer_name` (shadowed unity siblings' locals under
   `-Wshadow -Werror`).
2. **Unity for the whole Exec target**: 167 of 174 TUs join 14 unity
batches of
≤12 sources, gated on `ENABLE_UNITY_BUILD` like the pilot targets. Opted
out:
five files whose file-scope macros must not leak into siblings, plus the
two
heaviest template-instantiation TUs (`operator.cpp`,
`hashjoin_build_sink.cpp`)
which would dominate any batch they join; `scan_operator.cpp` is both.
3. **Three latent defects the Exprs conversion surfaced** (stand on
their own):
`dictionary_factory.h` had **no include guard at all** — any TU reaching
it
through two include paths fails with a class redefinition, and under
unity the
   clang error recovery poisoned unrelated batch members with spurious
   `-Warray-bounds` diagnostics. Now `#pragma once`. And
`function_dict_get_many.cpp` had copy-pasted the `DictGetState` struct
from
`function_dict_get.cpp` at namespace scope — renamed `DictGetManyState`
so the
   two TUs can share a batch. And `function_variant_element_v2.cpp` kept
`OwnedPathSegment` in an anonymous namespace while using it as a field
of the
   externally-visible `ResolvedVariantElementV2Path::Impl` — gcc's
`-Wsubobject-linkage` (`-Werror`) rejects exactly that once the file is
`#include`d into a unity batch instead of being the main file of its TU
   (clang has no such warning); the struct moves to namespace scope.
4. **Unity for the Exprs glue**: 246 of 287 TUs join 31 unity batches of
≤8
sources, same switch. Opted out: the flex/bison/gperf generated tables,
seven
macro-leaking files, the 30 heavy template-instantiation TUs (>15 s wall
or
>2.2 GB RSS in the compile bench: the min_max/collect/topn/percentile
aggregate
family, `in.cpp`, `multiply.cpp`, `function_array_aggregation.cpp`, …)
whose
per-file codegen would only stack into jumbo poles — and three files
that
   tests compile a second time by `#include`ing the `.cpp`
(`function_variant_element.cpp`, `uuid.cpp`,
`function_jsonb_transform.cpp`),
   see the verification section.

### Measured results

All numbers from the development branch this series is split from, macOS
arm64 +
clang 20, `-j14`, `ENABLE_PCH=ON`, cold builds, back-to-back A/B. The
baseline is
the #66712 state of that branch (10m16s), so the two waves compose with
the pilot:

| wave | build phase wall | target slot time | archive size |
|---|---|---|---|
| Exec unity | 10m16s → **8m57s (-79.2 s / -12.9%)** | 1533 s → 579 s
(2.6×) | `libExec.a` 2450 MB → 641 MB |
| Exprs unity | 8m57s → **7m57s (-60.0 s / -11.2%)** | 2147 s → 1333 s |
`libExprs.a` 2721 MB → 1392 MB |

Jumbo-TU envelope: the largest Exec unity TU compiles in 32 s / 2.6 GB
RSS
standalone, the largest Exprs one in 25 s / 1.95 GB — both below the
largest
existing individual TU in the tree (3.9 GB), so `-jN` memory envelopes
are
unchanged.

### Risk and verification

- **Unity changes TU grouping only.** The code commits riding along are
hygiene:
constants deduplicated with identical values, one constant scoped into
its class,
  two renames, one `#pragma once`. No logic change.
- **Archive symbol parity** (checked on the development branch): Exec
keeps all
external defined symbols — three weak linkonce_odr template
instantiations dedup
away, which is the point of unity, not a loss. Exprs likewise (one weak
instantiation dedups; the `DictGetManyState` rename carries its
`shared_ptr`
  machinery under the new name).
- **This exact branch, rebased onto current master, full BE build from
scratch**
  (macOS arm64, clang 20, `ENABLE_PCH=ON`, `ENABLE_UNITY_BUILD=ON`):
**7981/7981 ninja edges, zero failures, `doris_be` links (325 MB).**
Exec
produces exactly 14 unity TUs and Exprs exactly 31, as advertised. This
includes
`pipeline/rec_cte_shared_state.cpp`, added upstream after the waves were
measured — it lands inside an Exec unity batch via the existing
`GLOB_RECURSE`
  with zero CMakeLists edits, which is the intended maintenance story.
- **The first CI round of this PR did its job and caught two issues;
both are
  fixed in the current revision.**
1. *BE UT lane, duplicate symbols at link*: three test files compile a
src
`.cpp` a second time by `#include`ing it
(`function_variant_element_test`,
`function_uuid_test`, `function_json_object_flatten_test`). Pre-unity
this
linked only by archive-member selectivity: the test object defines the
symbols first and the library member is never pulled. A unity batch,
     however, is pulled in for its *siblings* and brings a second strong
     definition. The three `#include`d files
(`function_variant_element.cpp`, `uuid.cpp`,
`function_jsonb_transform.cpp`)
are now `SKIP_UNITY_BUILD_INCLUSION` — individual archive members
restore
     exactly the shadowing semantics master links with today.
  2. *Performance lane (the one gcc lane), `-Werror=subobject-linkage`*:
`function_variant_element_v2.cpp` held `OwnedPathSegment` in an
anonymous
namespace as a field type of the externally-visible `...Path::Impl`. gcc
     only raises `-Wsubobject-linkage` when the definition sits in an
`#include`d file — which is what unity turns a `.cpp` into; clang has no
     such warning, so every local build was green. The struct moves to
namespace scope (name unique to the TU); fixed at the source rather than
     SKIPped.
- **The OFF path, checked on the same tree**: reconfiguring with
  `ENABLE_UNITY_BUILD=OFF` drops **all 51** `unity_*.cxx` entries from
`compile_commands.json` (Exec 14, Exprs 31, the #66712 pilots 6) and the
TU
count goes 8464 → 9042 — the batches return to exactly their 629 member
files.
  Reconfiguring back ON restores exactly the same 51 batches. The switch
semantics themselves (including winning over a stale `CMAKE_UNITY_BUILD`
  cache) were established in #66712.
- **These two targets have been building as unity TUs on the development
branch
since 2026-08-08**, through repeated full-tree builds and the BE UT
builds that
verified #66672 (the UT binaries link against these same target
libraries).
- The `file_scanner_v2_test.cpp` hunk was compile-verified standalone
against this
  branch (`-fsyntax-only` with the test TU's full include closure).

### Proactive disclosure

- **Cross-platform is the blind spot, closed by this PR's own CI** —
every local
build and measurement above is macOS arm64 + clang 20. With
`ENABLE_UNITY_BUILD`
defaulting ON since #66712, the Linux compile lanes and every regression
pipeline
in this PR's CI run against unity Exec/Exprs — that is the validation,
and the
first round proved it works: the BE UT and gcc lanes each caught one
real
unity interaction (detailed above), fixed in this revision. Escape
hatches, in order:
per-user `ENABLE_UNITY_BUILD=OFF`, per-file
`SKIP_UNITY_BUILD_INCLUSION`, or a
  one-line default flip.
- **The incremental-rebuild trade-off is real**: touching one `.cpp`
inside a
batch recompiles the whole batch (≤12 sources for Exec, ≤8 for Exprs; a
batch
  compiles in ~25–32 s). This is why the heaviest, most-edited TUs
(`operator.cpp`, `hashjoin_build_sink.cpp`, the aggregate families,
`in.cpp`,
`multiply.cpp`, …) are deliberately SKIPped and keep per-file
granularity, and
  `ENABLE_UNITY_BUILD=OFF` restores it everywhere.
- **The SKIP lists are coverage policy, not leftovers**: 7 Exec + 38
Exprs files
stay individual on purpose — generated parsers (flex/bison/gperf), files
whose
file-scope macros would leak into siblings, and the heavy codegen TUs
where
merging saves no closure parse worth the jumbo-TU cost. A future file
whose
file-scope symbols clash inside a unity TU opts out the same one-line
way.
- Unity covers the *glue* of these targets, not the codegen-heavy
families — the
30 heavy Exprs SKIPs mean the headline per-target ratios (2.6× Exec slot
time)
are earned on the batched part; the SKIPped monsters keep their cost and
their
  per-file granularity.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by one committer.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants