This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 165efa32a9d [opt](build) Add build-timing and header-closure sweep 
tooling (#66616)
165efa32a9d is described below

commit 165efa32a9d54ac575cb8403a4dd7d6273c52de2
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Tue Aug 11 13:56:02 2026 +0800

    [opt](build) Add build-timing and header-closure sweep tooling (#66616)
    
    > Split out of **https://github.com/apache/doris/pull/66510**. That PR
    carries the
    > whole BE build-time batch and its end-to-end measurements — **please
    refer to
    > #66510 for the complete benefit numbers**. This PR only lands the
    measurement
    > tooling that batch was built on, so that the later
    header/instantiation PRs
    > have a gate to point at. It has no dependency on the rest and can be
    reviewed
    > and merged on its own.
    
    ### What problem does this PR solve?
    
    Related PR: #66510
    
    Problem Summary:
    
    Three measurement tools under `build-support/compile-bench/`, plus one
    real bug
    the tools found. None of them enters the build graph.
    
    #### 1. `closure-sweep.sh` + `syntax_sweep.py --no-pch` — the safety net
    for header surgery
    
    Any PR that cuts an include edge risks breaking a TU that was only
    compiling
    because some other header dragged the symbol in. The existing
    `syntax_sweep.py`
    could not catch that class of debt, for two reasons:
    
    - **With the PCH active, transitive-dependency debt is invisible.**
    Symbols leak
    out of `cmake_pch.hxx` into every TU, so a TU that forgot an `#include`
    still
      compiles. `--no-pch` strips the PCH preamble and runs each TU
    `-fsyntax-only` against its **natural** include closure. As a bonus it
    keeps
    the sweep usable while a header included by `pch.h` has been edited but
    the
      `.pch` is stale.
    - **109 compilation units were never being swept at all.** The scan
    filtered
    `compile_commands.json` by the `be/src` path prefix, but CMake unity
    batches
    emit their `.cxx` under the *build* directory, so every unity-covered TU
    was
    silently skipped. Including them takes the real scan surface to 191
    standalone
      TUs + 109 unity batches = 300 compilation units.
    
    `closure-sweep.sh` wraps this into a gate: it archives per-tag failure
    lists
    under `be/compile-bench-results/sweeps/`, records a baseline (existing
    debt), and
    in diff mode exits 1 on any *new* failure versus that baseline.
    
    #### 2. What the net caught on its first run
    
    `DeleteBitmap::diffset` in `tablet_meta.cpp` uses `std::ranges::views`
    but never
    includes `<ranges>` — the TU only compiled because the PCH leaked it in
    via
    simdjson. Exactly the class of debt the `--no-pch` mode exists to find,
    so the
    one-line fix ships here as the tool's first result.
    
    #### 3. `rebuild_radius.py` — how many objects does touching this header
    rebuild?
    
    Counts, per header, the object targets that list it in ninja's
    dependency
    database, and flags the ones reachable from `pch.h` — those rebuild
    every
    first-party object no matter how few TUs actually use them, which is
    what makes
    them worth cutting.
    
    The obvious alternative, `touch <header> && ninja -n | wc -l`, **cannot
    work in
    this repo**: `CONFIGURE_DEPENDS` globs stop the dry run at `Re-running
    CMake`
    before it lists a single compile edge. Hence the `ninja -t deps`
    approach.
    
    #### 4. `tu-bench.sh` — single-TU compile timing probe
    
    Companion to `--compile-bench` for before/after comparison of one
    translation
    unit. Looks up the TU's real command in `compile_commands.json`, strips
    the
    binary PCH load (keeping the textual include of `cmake_pch.hxx` so
    forced-header
    semantics stay identical), and reports wall seconds, max RSS,
    weak-definition
    count and total `.text` bytes, appending one TSV line per invocation.
    Runs on
    both Linux and macOS (`/usr/bin/time -v`, `size -A` and the ELF
    weak-definition
    nm letters are all GNU-only; the Darwin paths use the Apple
    equivalents).
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test
        - [x] No need to test or manual test. Explain why:
    - [x] Other reason: everything here lives under `build-support/` and is
    not part of the build graph — no target compiles, links or runs these
              scripts. The single source change, `#include <ranges>` in
    `tablet_meta.cpp`, is a missing include for code already in the tree
              and is covered by the existing build.
    - [x] Manual test — the tools were exercised throughout #66510: the
    300-unit
    closure sweep ran clean (300/300) against the natural-closure baseline
    after each header-surgery wave, and `tu-bench.sh` / `rebuild_radius.py`
          produced the per-TU and rebuild-radius numbers quoted there.
    
    - Behavior changed:
        - [x] No.
    
    - Does this need documentation?
    - [x] No. Each script self-documents via `--help` / a header comment.
    
    ### Proactive disclosure
    
    - All three tools were developed and run on macOS / clang 20. They are
    POSIX-shell and Python 3 with no exotic dependencies, and `tu-bench.sh`
    explicitly branches Linux vs Darwin, but the Linux paths of
    `tu-bench.sh` have
      had far less mileage than the Darwin ones.
    - `rebuild_radius.py` reads ninja's dep database, so it needs a
    *populated*
    build directory to be meaningful; on a fresh configure it reports
    nothing.
    - The sweep gate is opt-in — nothing in CI invokes it. Wiring it into CI
    would be
    a separate discussion (the full 300-unit `-fsyntax-only` pass is not
    free).
    
    ---------
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 be/src/storage/tablet/tablet_meta.cpp         |   1 +
 build-support/compile-bench/closure-sweep.sh  |  93 ++++++++++++++++++
 build-support/compile-bench/rebuild_radius.py | 116 ++++++++++++++++++++++
 build-support/compile-bench/syntax_sweep.py   | 120 +++++++++++++++++------
 build-support/compile-bench/tu-bench.sh       | 132 ++++++++++++++++++++++++++
 5 files changed, 433 insertions(+), 29 deletions(-)

diff --git a/be/src/storage/tablet/tablet_meta.cpp 
b/be/src/storage/tablet/tablet_meta.cpp
index 1f7aac050ed..4fd34f1122e 100644
--- a/be/src/storage/tablet/tablet_meta.cpp
+++ b/be/src/storage/tablet/tablet_meta.cpp
@@ -31,6 +31,7 @@
 #include <cstdint>
 #include <memory>
 #include <random>
+#include <ranges>
 #include <set>
 #include <utility>
 
diff --git a/build-support/compile-bench/closure-sweep.sh 
b/build-support/compile-bench/closure-sweep.sh
new file mode 100755
index 00000000000..c42b0265c7b
--- /dev/null
+++ b/build-support/compile-bench/closure-sweep.sh
@@ -0,0 +1,93 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# Safety net for header-closure surgery (Phase 3 of the BE build-speed work).
+#
+# Runs syntax_sweep.py in natural-closure mode (-fsyntax-only, PCH stripped)
+# over every first-party TU and archives the failure list, so that header
+# edits can be gated on "no NEW failures versus a recorded baseline".
+# Baseline failures are pre-existing "compiles only via PCH symbol leak"
+# debt; they matter the moment pch.h stops including the leaking header.
+#
+# Usage:
+#   closure-sweep.sh baseline <tag> [syntax_sweep.py args...]
+#       Full sweep; archive results under 
be/compile-bench-results/sweeps/<tag>/.
+#       Exit 0 even if TUs fail (failures are the recorded debt).
+#
+#   closure-sweep.sh diff <base-tag> <tag> [syntax_sweep.py args...]
+#       Full sweep archived as <tag>, then compared against <base-tag>:
+#       prints regressions (new failures) and improvements (fixed ones).
+#       Exit 1 iff there is at least one regression.
+set -uo pipefail
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+SWEEPS="$ROOT/be/compile-bench-results/sweeps"
+
+usage() {
+    grep '^#   closure-sweep.sh' "$0" >&2
+    exit 2
+}
+
+[ $# -ge 2 ] || usage
+mode=$1
+case "$mode" in
+baseline) base=""; tag=$2; shift 2 ;;
+diff) [ $# -ge 3 ] || usage; base=$2; tag=$3; shift 3 ;;
+*) usage ;;
+esac
+
+out="$SWEEPS/$tag"
+mkdir -p "$out"
+
+python3 "$ROOT/build-support/compile-bench/syntax_sweep.py" \
+    --no-pch \
+    --fail-list "$out/fails.list" \
+    --fail-log "$out/fail-errors.log" \
+    "$@" 2>&1 | tee "$out/sweep.log"
+sweep_rc=${PIPESTATUS[0]}
+touch "$out/fails.list" # sweep writes it only on failures
+
+echo
+echo "== sweep '$tag': $(wc -l <"$out/fails.list" | tr -d ' ') failing TU(s), 
results in ${out#"$ROOT"/} =="
+
+if [ "$mode" = baseline ]; then
+    exit 0
+fi
+
+basef="$SWEEPS/$base/fails.list"
+if [ ! -f "$basef" ]; then
+    echo "ERROR: baseline '$base' not found at $basef" >&2
+    exit 2
+fi
+# temp files instead of process substitution: keeps the script correct even
+# when invoked as `sh closure-sweep.sh` (POSIX mode has no <(...))
+sort "$basef" >"$out/.base.sorted"
+sort "$out/fails.list" >"$out/.cur.sorted"
+regressions=$(comm -13 "$out/.base.sorted" "$out/.cur.sorted")
+fixed=$(comm -23 "$out/.base.sorted" "$out/.cur.sorted")
+if [ -n "$fixed" ]; then
+    echo "-- fixed vs '$base':"
+    echo "$fixed" | sed 's/^/     /'
+fi
+if [ -n "$regressions" ]; then
+    echo "-- REGRESSIONS vs '$base':"
+    echo "$regressions" | sed 's/^/     /' | tee "$out/regressions.list"
+    exit 1
+fi
+echo "-- no regressions vs '$base'"
+exit 0
diff --git a/build-support/compile-bench/rebuild_radius.py 
b/build-support/compile-bench/rebuild_radius.py
new file mode 100644
index 00000000000..ceaa6a6e5a5
--- /dev/null
+++ b/build-support/compile-bench/rebuild_radius.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Report how many objects a header change actually rebuilds.
+
+Reads ninja's dependency database (`ninja -t deps`) and counts, per header,
+the object targets that list it -- the incremental rebuild radius. Headers
+reachable from pch.h are flagged separately: a PCH dependency invalidates
+every first-party object no matter how few TUs name a symbol from it, so
+those are the ones worth cutting first.
+
+Usage:
+  python3 rebuild_radius.py [--build-dir DIR] [header-suffix ...]
+
+Header arguments are matched as path suffixes ("core/field.h"); without
+any, a default set of hot BE headers is reported.
+
+Note that `touch <header> && ninja -n | wc -l` is NOT a usable substitute
+here: CONFIGURE_DEPENDS globs make the dry run stop at "Re-running CMake"
+before it lists a single compile edge.
+"""
+
+import argparse
+import os
+import subprocess
+import sys
+
+DEFAULT_HEADERS = [
+    "storage/olap_common.h",
+    "storage/utils.h",
+    "util/uid_util.h",
+    "util/json/path_in_data.h",
+    "storage/index/zone_map/zone_map_index.h",
+    "exprs/expr_zonemap_filter.h",
+    "storage/index/inverted/inverted_index_iterator.h",
+    "storage/index/inverted/inverted_index_parser.h",
+    "storage/tablet/tablet_schema.h",
+    "util/pretty_printer.h",
+    "exprs/function/function.h",
+    "core/column/column.h",
+    "core/field.h",
+    "core/types.h",
+    "common/status.h",
+    "runtime/runtime_profile.h",
+]
+
+
+def main():
+    repo = 
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+    ap = argparse.ArgumentParser(description=__doc__,
+                                 
formatter_class=argparse.RawDescriptionHelpFormatter)
+    ap.add_argument("--build-dir",
+                    default=os.path.join(repo, "be", 
"build_Release_compile_bench"))
+    ap.add_argument("headers", nargs="*", help="path suffixes to report on")
+    args = ap.parse_args()
+    headers = args.headers or DEFAULT_HEADERS
+
+    counts = {h: 0 for h in headers}
+    pch_deps = set()
+    objects = 0
+
+    proc = subprocess.Popen(["ninja", "-C", args.build_dir, "-t", "deps"],
+                            stdout=subprocess.PIPE, text=True, 
errors="replace",
+                            bufsize=1 << 20)
+    target, target_is_pch, hits = None, False, set()
+
+    def flush():
+        nonlocal objects
+        if target is None:
+            return
+        if target_is_pch:
+            pch_deps.update(hits)
+        else:
+            for h in hits:
+                counts[h] += 1
+        if target.endswith((".o", ".obj")):
+            objects += 1
+
+    for line in proc.stdout:
+        if line[:1] not in (" ", "\t", "\n"):
+            flush()
+            target = line.split(":")[0].strip()
+            target_is_pch = "cmake_pch" in target or target.endswith(".pch")
+            hits = set()
+        elif line.strip():
+            dep = line.strip()
+            for h in headers:
+                if dep.endswith(h):
+                    hits.add(h)
+    flush()
+    proc.wait()
+
+    print(f"{objects:,} object targets in the deps database\n")
+    print(f"{'header':58s} {'dependents':>10s}   via PCH (= rebuilds 
everything)")
+    for h in headers:
+        flag = "YES" if h in pch_deps else "no"
+        print(f"{h:58s} {counts[h]:>10,}   {flag}")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/build-support/compile-bench/syntax_sweep.py 
b/build-support/compile-bench/syntax_sweep.py
index 0e5e2b7d1c3..e3f77bfa023 100644
--- a/build-support/compile-bench/syntax_sweep.py
+++ b/build-support/compile-bench/syntax_sweep.py
@@ -20,16 +20,25 @@
 Validates include-structure changes (header cuts, forward-declaration swaps)
 against every TU without mutating the ninja build state: each compile command
 is replayed with -o/-c/-MD/-MT/-MF/-ftime-trace* stripped and -fsyntax-only
-appended, so nothing is written to the build directory. Front-end-only checks
-run in roughly half the time of a real compile and catch every missing-include
-or missing-declaration fallout a cut can cause.
+appended, so nothing is written to the build directory.
+
+Sweep scope: all first-party TUs -- standalone be/src entries AND unity
+batches (CMake unity .cxx live under the build dir and would be silently
+skipped by a be/src path filter; each batch textually includes its member
+.cpp files, so checking the batch checks the members). Third-party contrib
+(openblas/faiss/clucene/orc) and generated sources are excluded.
+
+--no-pch strips the PCH preamble (-include-pch / forced cmake_pch.hxx
+include) so every TU is checked against its NATURAL include closure. This is
+the mode that catches "TU compiles only because the PCH leaks symbols it
+never includes" debt -- mandatory before slimming pch.h, and the only mode
+that works while a header included by pch.h has been edited but the .pch has
+not been rebuilt.
 
 Usage:
   python3 syntax_sweep.py [--build-dir DIR] [--jobs N] [--filter SUBSTR]
-                          [--fail-log FILE]
-
-  --filter limits the sweep to TUs whose source path contains SUBSTR
-  (e.g. --filter load/memtable for a quick re-check of one subsystem).
+                          [--no-pch] [--fail-list FILE] [--fail-log FILE]
+                          [--timeout SECS]
 
 Exit code: 0 if every TU passes, 1 otherwise.
 """
@@ -52,23 +61,60 @@ STRIP_WITH_ARG = ("-o", "-MT", "-MF")
 STRIP_FLAGS = ("-c", "-MD", "-MMD")
 
 
-def mangle(cmd):
+def mangle(cmd, no_pch=False):
     args = shlex.split(cmd)
-    out, skip = [], False
-    for a in args:
-        if skip:
-            skip = False
-            continue
+    out = []
+    i = 0
+    while i < len(args):
+        a = args[i]
         if a in STRIP_WITH_ARG:
-            skip = True
+            i += 2
             continue
         if a in STRIP_FLAGS or a.startswith("-ftime-trace"):
+            i += 1
             continue
+        if no_pch:
+            if a == "-Winvalid-pch":
+                i += 1
+                continue
+            # -Xclang -include-pch -Xclang <file.pch>
+            # -Xclang -include     -Xclang <cmake_pch.hxx>
+            if (a == "-Xclang" and i + 3 < len(args)
+                    and args[i + 1] in ("-include-pch", "-include")
+                    and args[i + 2] == "-Xclang"
+                    and ("cmake_pch" in args[i + 3]
+                         or args[i + 3].endswith(".pch"))):
+                i += 4
+                continue
+            # plain -include <cmake_pch.hxx> form
+            if a == "-include" and i + 1 < len(args) and "cmake_pch" in args[i 
+ 1]:
+                i += 2
+                continue
         out.append(a)
+        i += 1
     out.append("-fsyntax-only")
     return out
 
 
+def display_name(path, src_prefix):
+    if path.startswith(src_prefix):
+        return path[len(src_prefix):]
+    if "/Unity/" in path:
+        # .../src/<dir>/CMakeFiles/<tgt>.dir/Unity/unity_N_cxx.cxx
+        m = re.search(r"/src/([^/]+)/CMakeFiles/[^/]+/Unity/(unity_\d+)", path)
+        if m:
+            return f"{m.group(1)}/[{m.group(2)}]"
+    return path
+
+
+def first_party(e, src_prefix):
+    f = e["file"]
+    if f.startswith(src_prefix):
+        return True
+    # CMake unity batches for first-party targets live under the build dir
+    return "/Unity/" in f and "/src/" in f
+
+
 def main():
     ap = argparse.ArgumentParser(description=__doc__,
                                  
formatter_class=argparse.RawDescriptionHelpFormatter)
@@ -77,44 +123,60 @@ def main():
     ap.add_argument("--jobs", type=int, default=max(2, (os.cpu_count() or 8) 
// 2))
     ap.add_argument("--filter", default="",
                     help="only sweep TUs whose path contains this substring")
+    ap.add_argument("--no-pch", action="store_true",
+                    help="strip PCH preamble: check each TU's natural include 
closure")
+    ap.add_argument("--fail-list", default="",
+                    help="write sorted display names of failing TUs to this 
file")
     ap.add_argument("--fail-log", default="",
                     help="write full stderr of every failing TU to this file")
+    ap.add_argument("--timeout", type=int, default=600,
+                    help="per-TU timeout in seconds (counts as failure)")
     args = ap.parse_args()
 
     src_prefix = os.path.join(REPO_ROOT, "be", "src") + os.sep
     with open(os.path.join(args.build_dir, "compile_commands.json")) as f:
         entries = [e for e in json.load(f)
-                   if e["file"].startswith(src_prefix) and args.filter in 
e["file"]]
-    print(f"{len(entries)} TUs to check ({args.jobs} jobs)", flush=True)
+                   if first_party(e, src_prefix) and args.filter in e["file"]]
+    print(f"{len(entries)} TUs to check ({args.jobs} jobs"
+          f"{', natural closure / no PCH' if args.no_pch else ''})", 
flush=True)
     t0 = time.time()
     fails = []
     done = 0
 
     def run(e):
-        p = subprocess.run(mangle(e["command"]), cwd=e["directory"],
-                           stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
-                           text=True)
-        return e["file"], p.returncode, p.stderr
+        cmd = mangle(e["command"], no_pch=args.no_pch)
+        name = display_name(e["file"], src_prefix)
+        try:
+            p = subprocess.run(cmd, cwd=e["directory"],
+                               stdout=subprocess.DEVNULL, 
stderr=subprocess.PIPE,
+                               text=True, timeout=args.timeout)
+            return name, p.returncode, p.stderr
+        except subprocess.TimeoutExpired:
+            return name, 124, f"TIMEOUT after {args.timeout}s"
 
     with concurrent.futures.ThreadPoolExecutor(args.jobs) as ex:
-        for src, rc, err in ex.map(run, entries):
+        for name, rc, err in ex.map(run, entries):
             done += 1
             if rc != 0:
-                fails.append((src, err))
-                print(f"FAIL {src.replace(src_prefix, '')}", flush=True)
-            if done % 100 == 0:
-                print(f"  …{done}/{len(entries)} ({time.time() - t0:.0f}s, "
+                fails.append((name, err))
+                print(f"FAIL {name}", flush=True)
+            if done % 50 == 0:
+                print(f"  ...{done}/{len(entries)} ({time.time() - t0:.0f}s, "
                       f"{len(fails)} failures)", flush=True)
 
     print(f"\n==== {len(fails)} failing TU(s) of {len(entries)} "
           f"in {time.time() - t0:.0f}s ====", flush=True)
+    if args.fail_list:
+        with open(args.fail_list, "w") as f:
+            for name, _ in sorted(fails):
+                f.write(name + "\n")
     if args.fail_log and fails:
         with open(args.fail_log, "w") as f:
-            for src, err in fails:
-                f.write(f"===== {src}\n{ANSI.sub('', err)}\n")
-    for src, err in fails[:15]:
+            for name, err in fails:
+                f.write(f"===== {name}\n{ANSI.sub('', err)}\n")
+    for name, err in fails[:15]:
         first = [l for l in ANSI.sub("", err).splitlines() if " error: " in 
l][:2]
-        print(src.replace(src_prefix, ""))
+        print(name)
         for l in first:
             print("   ", l[:200])
     return 1 if fails else 0
diff --git a/build-support/compile-bench/tu-bench.sh 
b/build-support/compile-bench/tu-bench.sh
new file mode 100755
index 00000000000..03d7ecd0dac
--- /dev/null
+++ b/build-support/compile-bench/tu-bench.sh
@@ -0,0 +1,132 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# Single-TU compile timing probe (no-PCH baseline).
+#
+# Looks up the TU's real compile command in compile_commands.json, strips the
+# binary PCH (keeps the textual -include of cmake_pch.hxx so the source still
+# sees the same forced headers), redirects -o to an output dir, then reports:
+#   wall seconds, max RSS, weak-def count, total .text bytes
+# One line per run is appended to $OUT_DIR/results.tsv; the .o, /usr/bin/time
+# log and (by default) an -ftime-trace JSON stay in $OUT_DIR for analysis.
+#
+# Usage:
+#   build-support/compile-bench/tu-bench.sh <label> <source-path> [build-dir]
+#
+#   <source-path>  absolute, or relative to the repo root. Unity TUs work too:
+#                  pass the generated .cxx path under the build dir.
+#   [build-dir]    defaults to be/build_Release_compile_bench
+#
+# Env:
+#   TU_BENCH_OUT=<dir>    output dir (default: <build-dir>/tu-bench)
+#   TU_BENCH_TRACE=OFF    disable -ftime-trace (default ON, matching the
+#                         RESEARCH.md probe baseline numbers)
+#
+# Timing hygiene: run on an idle machine, one TU at a time. Concurrent builds
+# invalidate the numbers (see plan-doc/compile-opt/HANDOFF.md).
+
+set -euo pipefail
+
+if [[ $# -lt 2 ]]; then
+    grep '^#' "$0" | sed 's/^# \{0,1\}//' | head -25
+    exit 1
+fi
+
+LABEL="$1"
+SRC="$2"
+DORIS_HOME="$(cd "$(dirname "$0")/../.." && pwd)"
+BUILD_DIR="${3:-${DORIS_HOME}/be/build_Release_compile_bench}"
+OUT_DIR="${TU_BENCH_OUT:-${BUILD_DIR}/tu-bench}"
+TRACE="${TU_BENCH_TRACE:-ON}"
+
+[[ "${SRC}" = /* ]] || SRC="${DORIS_HOME}/${SRC}"
+CCJSON="${BUILD_DIR}/compile_commands.json"
+[[ -f "${CCJSON}" ]] || { echo "ERROR: ${CCJSON} not found (configure first)" 
>&2; exit 1; }
+mkdir -p "${OUT_DIR}"
+
+# Extract the compile command, strip binary PCH, redirect -o. Token-level
+# rewrite in python (shell-splitting the escaped command string is too 
fragile).
+OBJ="${OUT_DIR}/${LABEL}.o"
+CMDFILE="${OUT_DIR}/${LABEL}.cmd"
+python3 - "${CCJSON}" "${SRC}" "${OBJ}" > "${CMDFILE}" <<'PYEOF'
+import json, shlex, sys
+ccjson, src, obj = sys.argv[1], sys.argv[2], sys.argv[3]
+entries = [e for e in json.load(open(ccjson)) if e["file"] == src]
+if not entries:
+    sys.exit(f"ERROR: no compile_commands.json entry for {src}")
+e = entries[0]
+toks = shlex.split(e["command"])
+out = []
+i = 0
+while i < len(toks):
+    # binary PCH load: -Xclang -include-pch -Xclang <path>
+    if (toks[i] == "-Xclang" and i + 3 < len(toks)
+            and toks[i + 1] == "-include-pch" and toks[i + 2] == "-Xclang"):
+        i += 4
+        continue
+    if toks[i] == "-o" and i + 1 < len(toks):
+        out += ["-o", obj]
+        i += 2
+        continue
+    out.append(toks[i])
+    i += 1
+print(e["directory"])
+print(" ".join(shlex.quote(t) for t in out))
+PYEOF
+
+WORK_DIR="$(head -1 "${CMDFILE}")"
+CMD="$(tail -1 "${CMDFILE}")"
+
+TRACE_ARGS=""
+if [[ "${TRACE}" == "ON" ]]; then
+    TRACE_ARGS="-ftime-trace=${OUT_DIR}/${LABEL}.json 
-ftime-trace-granularity=100"
+fi
+
+TIME_LOG="${OUT_DIR}/${LABEL}.time"
+if [[ "$(uname)" == "Darwin" ]]; then
+    # BSD time: "X.XX real X.XX user X.XX sys" plus "-l" resource lines (RSS 
in bytes)
+    (cd "${WORK_DIR}" && /usr/bin/time -l bash -c "${CMD} ${TRACE_ARGS}" 2> 
"${TIME_LOG}")
+    WALL_S="$(awk '/ real /{printf "%.2f", $1}' "${TIME_LOG}")"
+    MAX_RSS_KB="$(awk '/maximum resident set size/{printf "%d", $1/1024}' 
"${TIME_LOG}")"
+else
+    (cd "${WORK_DIR}" && /usr/bin/time -v bash -c "${CMD} ${TRACE_ARGS}" 2> 
"${TIME_LOG}")
+    WALL_RAW="$(grep 'Elapsed (wall clock)' "${TIME_LOG}" | awk '{print $NF}')"
+    # h:mm:ss or m:ss.ss -> seconds
+    WALL_S="$(echo "${WALL_RAW}" | awk -F: '{ s=0; for (i=1; i<=NF; i++) s = 
s*60 + $i; printf "%.2f", s }')"
+    MAX_RSS_KB="$(grep 'Maximum resident' "${TIME_LOG}" | awk '{print $NF}')"
+fi
+# Use the toolchain's llvm-nm/llvm-size (sit next to the clang++ in the 
command;
+# Apple's /usr/bin/size lacks -A).
+TOOLDIR="$(dirname "$(echo "${CMD}" | awk '{print $1}')")"
+NM="${TOOLDIR}/llvm-nm"
+[[ -x "${NM}" ]] || NM="nm"
+SIZE_TOOL="${TOOLDIR}/llvm-size"
+[[ -x "${SIZE_TOOL}" ]] || SIZE_TOOL="size"
+if [[ "$(uname)" == "Darwin" ]]; then
+    # Mach-O weak definitions don't get a distinct nm letter; use darwin 
format.
+    WEAK="$(${NM} -m "${OBJ}" 2>/dev/null | grep -c 'weak external' || true)"
+else
+    WEAK="$(${NM} "${OBJ}" | grep -cE ' [VvWw] ' || true)"
+fi
+TEXT="$(${SIZE_TOOL} -A "${OBJ}" | awk '$1 ~ /^__text$|^\.text/{s+=$2} 
END{print s+0}')"
+
+RESULTS="${OUT_DIR}/results.tsv"
+[[ -f "${RESULTS}" ]] || printf 
'timestamp\tlabel\twall_s\tmax_rss_kb\tweak_defs\ttext_bytes\tsrc\n' > 
"${RESULTS}"
+printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
+    "$(date +%Y-%m-%dT%H:%M:%S)" "${LABEL}" "${WALL_S}" "${MAX_RSS_KB}" 
"${WEAK}" "${TEXT}" "${SRC}" \
+    | tee -a "${RESULTS}"


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

Reply via email to