github-actions[bot] commented on code in PR #67067:
URL: https://github.com/apache/doris/pull/67067#discussion_r4037407345


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownAggThroughJoinOnPkFk.java:
##########
@@ -138,17 +140,29 @@ public List<Rule> buildRules() {
     // select primary_table_pk, primary_table_other from primary_table join 
foreign_table on pk = fk
     // group by pk, primary_table_other_cols;
     private LogicalAggregate<?> eliminatePrimaryOutput(LogicalAggregate<?> 
agg, Plan child,
-            Plan primary, Plan foreign) {
+            PrimaryForeignInfo primaryForeignInfo) {
+        Set<Slot> groupBySlots = agg.getGroupByExpressions().stream()
+                .map(Slot.class::cast)
+                .collect(ImmutableSet.toImmutableSet());
+        DataTrait dataTrait = child.getLogicalProperties().getTrait();
+        if (!groupByDeterminesForeignKey(groupBySlots, 
primaryForeignInfo.foreignKeys, dataTrait)) {

Review Comment:
   [P1] Fence non-movable predicates in the PK/FK cluster rebuild
   
   This indirect-FD admission can now rebuild `(P JOIN F) JOIN C` as `(F JOIN 
C) JOIN P`, but `InnerJoinCluster` does not reject hash conjuncts containing 
`NoneMovableFunction`. For a lower conjunct such as `assert_true(F.row_id > 0, 
'bad') = (P.id > 0)`, an F row with no C match throws in the original lower 
join but is discarded before the rebuilt top join evaluates the assertion. The 
generic join reorder already fences this expression class; apply the same fence 
here (or preserve the original boundary) and add a three-table indirect-FD 
regression.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/eageraggregation/ReorderJoinBeforeEagerAgg.java:
##########
@@ -0,0 +1,60 @@
+// 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.
+
+package org.apache.doris.nereids.rules.rewrite.eageraggregation;
+
+import org.apache.doris.nereids.jobs.JobContext;
+import org.apache.doris.nereids.rules.rewrite.ColumnPruning;
+import org.apache.doris.nereids.rules.rewrite.joinorder.JoinReorderRule;
+import org.apache.doris.nereids.stats.StatsCalculator;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.algebra.CatalogRelation;
+import org.apache.doris.nereids.trees.plans.visitor.CustomRewriter;
+import org.apache.doris.qe.ConnectContext;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/** Reorder joins before eager aggregation. */
+public class ReorderJoinBeforeEagerAgg implements CustomRewriter {
+    private static final Logger LOG = 
LoggerFactory.getLogger(ReorderJoinBeforeEagerAgg.class);
+
+    @Override
+    public Plan rewriteRoot(Plan plan, JobContext jobContext) {
+        List<CatalogRelation> scans = 
plan.collectToList(CatalogRelation.class::isInstance);
+        StatsCalculator.disableJoinReorderIfStatsInvalid(scans, 
jobContext.getCascadesContext());
+        ConnectContext connectContext = 
jobContext.getCascadesContext().getConnectContext();
+        if (connectContext.getSessionVariable().isDisableJoinReorder()

Review Comment:
   [P1] Preserve the existing initial-reorder control
   
   This replacement no longer consults `enable_init_join_order`, and removing 
the only `InitJoinOrder` registration also makes `INIT_JOIN_ORDER` rule 
disabling ineffective. Existing sessions that explicitly disable initial 
reordering will now receive this default-on greedy rewrite for any join plan, 
even without an aggregate. Gate the replacement through the legacy control (or 
provide an explicit compatibility/deprecation migration) and cover both old 
disable paths with a plan test.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/joinorder/JoinReorderRule.java:
##########
@@ -0,0 +1,178 @@
+// 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.
+
+package org.apache.doris.nereids.rules.rewrite.joinorder;
+
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.functions.ExpressionTrait;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.trees.plans.visitor.DefaultPlanRewriter;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+/**JoinReorderRule*/
+public class JoinReorderRule extends DefaultPlanRewriter<Void> {
+    public static final JoinReorderRule INSTANCE = new JoinReorderRule();
+    public static final int MAX_ATOM_NUM_FOR_GREEDY = 16;
+
+    public Plan rewrite(Plan plan, Void context) {
+        return plan.accept(this, context);
+    }
+
+    @Override
+    public Plan visitLogicalJoin(
+            LogicalJoin<? extends Plan, ? extends Plan> join,
+            Void context) {
+        if (!isReorderable(join)) {
+            // The current join is a boundary, but its children may contain 
independent join clusters.
+            return DefaultPlanRewriter.visitChildren(this, join, context);
+        }
+
+        // The current join is the root of a cluster. Reorder the current 
cluster and recursively
+        // process independent clusters below its boundaries.
+        return reorderCluster(join, context);
+    }
+
+    private Plan reorderCluster(
+            LogicalJoin<? extends Plan, ? extends Plan> root,
+            Void context) {
+        JoinCluster cluster = new JoinCluster(root.getOutput());
+        Plan fallback = rewriteAndCollectCluster(root, cluster, context);
+
+        // Use the fallback when the best candidate increases the number of 
cross joins.
+        Plan reordered = reorder(cluster);
+        return reordered == null ? fallback : reordered;
+    }
+
+    private int countCrossJoinsInCluster(Plan plan) {
+        if (plan instanceof LogicalJoin
+                && isReorderable((LogicalJoin<?, ?>) plan)) {
+            LogicalJoin<?, ?> join = (LogicalJoin<?, ?>) plan;
+            int currentCrossJoinCount = join.getJoinType().isCrossJoin() ? 1 : 
0;
+            return currentCrossJoinCount
+                    + countCrossJoinsInCluster(join.left())
+                    + countCrossJoinsInCluster(join.right());
+        }
+        if (plan instanceof LogicalProject
+                && isTransparentProject((LogicalProject<?>) plan)) {
+            return countCrossJoinsInCluster(plan.child(0));
+        }
+        return 0;
+    }
+
+    /*
+     * Traverses once to collect the current reorderable join cluster and 
rewrite independent
+     * clusters below its boundaries.
+     * Collects inputs, predicates, and the cross-join count into the cluster 
parameter, and
+     * returns the fallback plan.
+     */
+    private Plan rewriteAndCollectCluster(Plan plan, JoinCluster cluster, Void 
context) {
+        if (plan instanceof LogicalJoin
+                && isReorderable((LogicalJoin<?, ?>) plan)) {
+            LogicalJoin<?, ?> join = (LogicalJoin<?, ?>) plan;
+            cluster.addPredicates(join.getHashJoinConjuncts());
+            cluster.addPredicates(join.getOtherJoinConjuncts());
+            if (join.getJoinType().isCrossJoin()) {
+                cluster.crossJoinCount++;
+            }
+            Plan left = rewriteAndCollectCluster(join.left(), cluster, 
context);
+            Plan right = rewriteAndCollectCluster(join.right(), cluster, 
context);
+            return left == join.left() && right == join.right()
+                    ? join
+                    : join.withChildren(left, right);
+        }
+        if (plan instanceof LogicalProject
+                && isTransparentProject((LogicalProject<?>) plan)) {
+            LogicalProject<?> project = (LogicalProject<?>) plan;
+
+            /*
+             * The project contains only existing slots and does not replace 
any ExprId, so predicates
+             * from upper joins do not need to be rewritten and flattening can 
continue through it.
+             * The project at the cluster root restores column pruning and the 
original output order.
+             */
+            Plan child = rewriteAndCollectCluster(project.child(), cluster, 
context);
+            return child == project.child() ? project : 
project.withChildren(child);
+        }
+
+        // The plan is a boundary of the current cluster and may contain 
independent clusters.
+        Plan rewrittenInput = plan.accept(this, context);
+        cluster.addInput(rewrittenInput);
+        return rewrittenInput;
+    }
+
+    private boolean isTransparentProject(LogicalProject<?> project) {
+        return !project.isDistinct() && project.isAllSlots();
+    }
+
+    private Plan reorder(JoinCluster joinCluster) {
+        if (joinCluster.inputs.size() > MAX_ATOM_NUM_FOR_GREEDY) {

Review Comment:
   [P1] Preserve pre-eager ordering when greedy reordering declines
   
   For a 17-atom cluster this returns the untouched tree and, because the old 
bottom-up `InitJoinOrder` stage was removed, even the bottom joins keep their 
original orientation. In the reduced plan `Aggregate[g, SUM(a.v*b.v)] -> ... -> 
Join(A_small, B_huge)`, the old stage produced `Join(B_huge, A_small)`, letting 
the small-right broadcast path place an aggregate above that bottom join; this 
fallback leaves the huge relation on the right, so the two-sided aggregate is 
rejected. Later `JoinCommute` runs after eager aggregation and cannot recreate 
the skipped aggregate. Retain the old orientation fallback for declined 
clusters/boundaries or make small-broadcast eager placement 
orientation-independent, with forced-eager tests for the cap and an eligible 
outer boundary.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownAggThroughJoinOnPkFk.java:
##########
@@ -178,19 +192,22 @@ private LogicalAggregate<?> 
eliminatePrimaryOutput(LogicalAggregate<?> agg, Plan
         // Thirdly, construct new Agg below join.
         // For the pk-fk join, the foreign table side will not expand rows.
         // As a result, executing agg(group by fk) before join is same with 
executing agg(group by fk) after join.
-        Set<Expression> newGroupBySlots = 
constructNewGroupBy(minGroupBySlotList, primaryOutputSet,
-                primaryToForeignDeps);
+        List<Expression> newGroupBySlots = 
constructNewGroupBy(minGroupBySlotList, primaryOutputSet,

Review Comment:
   [P1] Preserve the relation targeted by join hints
   
   Once this injected FK makes the indirect-FD rewrite succeed, 
`constructJoinWithPrimary` can turn `p JOIN [broadcast] f` into `Aggregate(f) 
JOIN p`. `LogicalJoin.withChildren` keeps the original `BROADCAST_RIGHT` hint, 
so planning now broadcasts the primary table `p` instead of the foreign table 
the user explicitly hinted; a large parent can make that plan infeasible. Treat 
hinted/leading joins as rewrite boundaries, or preserve child orientation and 
hint ownership, and add an indirect-FD broadcast-hint plan test.



##########
plans/test-in-blackhouse/run_remote_tpch_perf_compare.sh:
##########
@@ -0,0 +1,483 @@
+#!/usr/bin/env bash

Review Comment:
   [P1] Add the required ASF license header
   
   The exact-head License Check rejects this new file as its sole invalid path, 
so this PR cannot pass the required gate. Keep the shebang first and add the 
repository-standard ASF shell header immediately after it.



##########
plans/test-in-blackhouse/run_remote_tpch_perf_compare.sh:
##########
@@ -0,0 +1,483 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+LOCAL_REPORT_ROOT="${SCRIPT_DIR}/tpch-perf-reports"
+
+CLUSTER="${CLUSTER:-cluster1}"
+REMOTE_USER="${REMOTE_USER:-root}"
+REMOTE_HOST="${REMOTE_HOST:-blackhouse}"
+REMOTE_PORT="${REMOTE_PORT:-22}"
+REMOTE_TPCH_TOOLS_DIR="${REMOTE_TPCH_TOOLS_DIR:-/root/mal/tpch/tpch-tools}"
+REMOTE_FE_DIR="${REMOTE_FE_DIR:-}"
+REMOTE_LIB_ARCHIVE_DIR="${REMOTE_LIB_ARCHIVE_DIR:-}"
+BASELINE_ARCHIVE="${BASELINE_ARCHIVE:-without-opt.tar}"
+OPT_ARCHIVE="${OPT_ARCHIVE:-with-opt.tar}"
+REMOTE_CLUSTER_START_CMD="${REMOTE_CLUSTER_START_CMD:-}"
+REMOTE_CLUSTER_STOP_CMD="${REMOTE_CLUSTER_STOP_CMD:-}"
+REMOTE_MYSQL_HOST="${REMOTE_MYSQL_HOST:-127.0.0.1}"
+REMOTE_MYSQL_PORT="${REMOTE_MYSQL_PORT:-}"
+REMOTE_MYSQL_USER="${REMOTE_MYSQL_USER:-root}"
+REMOTE_HTTP_PORT="${REMOTE_HTTP_PORT:-}"
+REMOTE_MYSQL_DB="${REMOTE_MYSQL_DB:-tpch_sf1000}"
+REMOTE_JAVA_HOME="${REMOTE_JAVA_HOME:-/usr/lib/jvm/java-17-openjdk-amd64}"
+WAIT_TIMEOUT_SECONDS="${WAIT_TIMEOUT_SECONDS:-300}"
+QUERY_LIST="${QUERY_LIST:-}"
+PROFILE_OUTPUT_DIR="${PROFILE_OUTPUT_DIR:-}"
+DRY_RUN=0
+
+apply_cluster_defaults() {
+    case "${CLUSTER}" in
+    cluster1)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=9030}"
+        : "${REMOTE_HTTP_PORT:=8030}"
+        ;;
+    cluster2)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=19030}"
+        : "${REMOTE_HTTP_PORT:=18030}"
+        ;;
+    *)
+        echo "Unsupported cluster: ${CLUSTER}" >&2
+        exit 1
+        ;;
+    esac
+
+    : "${REMOTE_CLUSTER_START_CMD:=${REMOTE_FE_DIR}/bin/start_fe.sh --daemon}"
+    : "${REMOTE_CLUSTER_STOP_CMD:=${REMOTE_FE_DIR}/bin/stop_fe.sh}"
+}
+
+usage() {
+    cat <<EOF
+Usage: $0 [--cluster cluster1|cluster2] [--db tpch_sf1000] [--queries '1,3,7'] 
[--profile DIR] [--dry-run]
+
+This script compares TPCH query execution time only.
+It switches baseline/opt FE lib archives, runs selected TPCH queries with
+1 cold run and 2 hot runs, then writes a summary table.
+
+When --profile DIR is set, the final hot-run query profile for each query is 
saved under
+DIR/queryN/without-opt.profile and DIR/queryN/with-opt.profile.
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+    --cluster)
+        CLUSTER="$2"
+        shift 2
+        ;;
+    --db)
+        REMOTE_MYSQL_DB="$2"
+        shift 2
+        ;;
+    --queries)
+        QUERY_LIST="$2"
+        shift 2
+        ;;
+    --profile)
+        PROFILE_OUTPUT_DIR="$2"
+        shift 2
+        ;;
+    --dry-run)
+        DRY_RUN=1
+        shift
+        ;;
+    -h|--help)
+        usage
+        exit 0
+        ;;
+    *)
+        echo "Unknown argument: $1" >&2
+        usage >&2
+        exit 1
+        ;;
+    esac
+done
+
+apply_cluster_defaults
+
+mkdir -p "${LOCAL_REPORT_ROOT}"
+RUN_ID=$(date +%Y%m%d_%H%M%S)
+LOCAL_RUN_DIR="${LOCAL_REPORT_ROOT}/${RUN_ID}"
+mkdir -p "${LOCAL_RUN_DIR}"
+
+if [[ ${DRY_RUN} -eq 1 ]]; then
+    cat <<EOF
+Dry run only. Nothing will be executed.
+cluster: ${CLUSTER}
+db: ${REMOTE_MYSQL_DB}
+queries: ${QUERY_LIST:-all tpch queries}
+tpch-tools dir: ${REMOTE_TPCH_TOOLS_DIR}
+fe dir: ${REMOTE_FE_DIR}
+lib dir: ${REMOTE_LIB_ARCHIVE_DIR}
+mysql port: ${REMOTE_MYSQL_PORT}
+http port: ${REMOTE_HTTP_PORT}
+profile dir: ${PROFILE_OUTPUT_DIR:-disabled}
+EOF
+    exit 0
+fi
+
+SSH_BASE=(ssh -p "${REMOTE_PORT}" -o StrictHostKeyChecking=accept-new 
"${REMOTE_USER}@${REMOTE_HOST}")
+SCP_BASE=(scp -P "${REMOTE_PORT}")
+
+run_remote() {
+    local script_content=$1
+    "${SSH_BASE[@]}" 'bash -s' -- <<EOF
+${script_content}
+EOF
+}
+
+fetch_remote_file() {
+    local remote_path=$1
+    local local_path=$2
+    "${SCP_BASE[@]}" "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}" 
"${local_path}"
+}
+
+fetch_remote_dir() {
+    local remote_path=$1
+    local local_path=$2
+    mkdir -p "${local_path}"
+    "${SCP_BASE[@]}" -r "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}/." 
"${local_path}"
+}
+
+read -r -d '' REMOTE_SCRIPT <<'EOF' || true
+set -euo pipefail
+
+REMOTE_TPCH_TOOLS_DIR="__REMOTE_TPCH_TOOLS_DIR__"
+REMOTE_FE_DIR="__REMOTE_FE_DIR__"
+REMOTE_LIB_ARCHIVE_DIR="__REMOTE_LIB_ARCHIVE_DIR__"
+BASELINE_ARCHIVE="__BASELINE_ARCHIVE__"
+OPT_ARCHIVE="__OPT_ARCHIVE__"
+REMOTE_CLUSTER_START_CMD="__REMOTE_CLUSTER_START_CMD__"
+REMOTE_CLUSTER_STOP_CMD="__REMOTE_CLUSTER_STOP_CMD__"
+REMOTE_MYSQL_HOST="__REMOTE_MYSQL_HOST__"
+REMOTE_MYSQL_PORT="__REMOTE_MYSQL_PORT__"
+REMOTE_MYSQL_USER="__REMOTE_MYSQL_USER__"
+REMOTE_MYSQL_DB="__REMOTE_MYSQL_DB__"
+REMOTE_JAVA_HOME="__REMOTE_JAVA_HOME__"
+WAIT_TIMEOUT_SECONDS="__WAIT_TIMEOUT_SECONDS__"
+QUERY_LIST="__QUERY_LIST__"
+REMOTE_HTTP_PORT="__REMOTE_HTTP_PORT__"
+PROFILE_OUTPUT_DIR="__PROFILE_OUTPUT_DIR__"
+RUN_ID="__RUN_ID__"
+
+REPORT_ROOT="${REMOTE_TPCH_TOOLS_DIR}/perf-reports/${RUN_ID}"
+RESULT_CSV_DIR="${REPORT_ROOT}/csv"
+QUERIES_DIR="${REMOTE_TPCH_TOOLS_DIR}/queries"
+mkdir -p "${RESULT_CSV_DIR}"
+PROFILE_STAGING_DIR="${REPORT_ROOT}/profiles"
+if [[ -n "${PROFILE_OUTPUT_DIR}" ]]; then
+    mkdir -p "${PROFILE_STAGING_DIR}"
+fi
+
+run_shell_command() {
+    local cmd=$1
+    bash -lc "export JAVA_HOME='${REMOTE_JAVA_HOME}'; export 
PATH='${REMOTE_JAVA_HOME}/bin':\"\$PATH\"; ${cmd}"
+}
+
+wait_fe() {
+    local deadline=$(( $(date +%s) + WAIT_TIMEOUT_SECONDS ))
+    while true; do
+        if mysql -h"${REMOTE_MYSQL_HOST}" -P"${REMOTE_MYSQL_PORT}" 
-u"${REMOTE_MYSQL_USER}" -e 'select 1' >/dev/null 2>&1; then
+            return 0
+        fi
+        if [[ $(date +%s) -ge ${deadline} ]]; then
+            exit 1
+        fi
+        sleep 5
+    done
+}
+
+selected_queries() {
+    if [[ -n "${QUERY_LIST}" ]]; then
+        printf '%s\n' "${QUERY_LIST}" | tr ', ' '\n\n' | sed '/^$/d'
+    else
+        seq 1 22
+    fi
+}
+
+switch_lib() {

Review Comment:
   [P1] Serialize runs that target the same performance cluster
   
   Two invocations can stop/delete/extract the same FE concurrently or switch 
one another's library while queries are running. Runs started in the same 
second also share the same local and remote report directories. Acquire an 
atomic remote lock keyed by the cluster/FE directory for the full 
switch-and-query lifecycle, and use a collision-resistant run ID.



##########
plans/test-in-blackhouse/run_remote_tpch_perf_compare.sh:
##########
@@ -0,0 +1,483 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+LOCAL_REPORT_ROOT="${SCRIPT_DIR}/tpch-perf-reports"
+
+CLUSTER="${CLUSTER:-cluster1}"
+REMOTE_USER="${REMOTE_USER:-root}"
+REMOTE_HOST="${REMOTE_HOST:-blackhouse}"
+REMOTE_PORT="${REMOTE_PORT:-22}"
+REMOTE_TPCH_TOOLS_DIR="${REMOTE_TPCH_TOOLS_DIR:-/root/mal/tpch/tpch-tools}"
+REMOTE_FE_DIR="${REMOTE_FE_DIR:-}"
+REMOTE_LIB_ARCHIVE_DIR="${REMOTE_LIB_ARCHIVE_DIR:-}"
+BASELINE_ARCHIVE="${BASELINE_ARCHIVE:-without-opt.tar}"
+OPT_ARCHIVE="${OPT_ARCHIVE:-with-opt.tar}"
+REMOTE_CLUSTER_START_CMD="${REMOTE_CLUSTER_START_CMD:-}"
+REMOTE_CLUSTER_STOP_CMD="${REMOTE_CLUSTER_STOP_CMD:-}"
+REMOTE_MYSQL_HOST="${REMOTE_MYSQL_HOST:-127.0.0.1}"
+REMOTE_MYSQL_PORT="${REMOTE_MYSQL_PORT:-}"
+REMOTE_MYSQL_USER="${REMOTE_MYSQL_USER:-root}"
+REMOTE_HTTP_PORT="${REMOTE_HTTP_PORT:-}"
+REMOTE_MYSQL_DB="${REMOTE_MYSQL_DB:-tpch_sf1000}"
+REMOTE_JAVA_HOME="${REMOTE_JAVA_HOME:-/usr/lib/jvm/java-17-openjdk-amd64}"
+WAIT_TIMEOUT_SECONDS="${WAIT_TIMEOUT_SECONDS:-300}"
+QUERY_LIST="${QUERY_LIST:-}"
+PROFILE_OUTPUT_DIR="${PROFILE_OUTPUT_DIR:-}"
+DRY_RUN=0
+
+apply_cluster_defaults() {
+    case "${CLUSTER}" in
+    cluster1)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=9030}"
+        : "${REMOTE_HTTP_PORT:=8030}"
+        ;;
+    cluster2)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=19030}"
+        : "${REMOTE_HTTP_PORT:=18030}"
+        ;;
+    *)
+        echo "Unsupported cluster: ${CLUSTER}" >&2
+        exit 1
+        ;;
+    esac
+
+    : "${REMOTE_CLUSTER_START_CMD:=${REMOTE_FE_DIR}/bin/start_fe.sh --daemon}"
+    : "${REMOTE_CLUSTER_STOP_CMD:=${REMOTE_FE_DIR}/bin/stop_fe.sh}"
+}
+
+usage() {
+    cat <<EOF
+Usage: $0 [--cluster cluster1|cluster2] [--db tpch_sf1000] [--queries '1,3,7'] 
[--profile DIR] [--dry-run]
+
+This script compares TPCH query execution time only.
+It switches baseline/opt FE lib archives, runs selected TPCH queries with
+1 cold run and 2 hot runs, then writes a summary table.
+
+When --profile DIR is set, the final hot-run query profile for each query is 
saved under
+DIR/queryN/without-opt.profile and DIR/queryN/with-opt.profile.
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+    --cluster)
+        CLUSTER="$2"
+        shift 2
+        ;;
+    --db)
+        REMOTE_MYSQL_DB="$2"
+        shift 2
+        ;;
+    --queries)
+        QUERY_LIST="$2"
+        shift 2
+        ;;
+    --profile)
+        PROFILE_OUTPUT_DIR="$2"
+        shift 2
+        ;;
+    --dry-run)
+        DRY_RUN=1
+        shift
+        ;;
+    -h|--help)
+        usage
+        exit 0
+        ;;
+    *)
+        echo "Unknown argument: $1" >&2
+        usage >&2
+        exit 1
+        ;;
+    esac
+done
+
+apply_cluster_defaults
+
+mkdir -p "${LOCAL_REPORT_ROOT}"
+RUN_ID=$(date +%Y%m%d_%H%M%S)
+LOCAL_RUN_DIR="${LOCAL_REPORT_ROOT}/${RUN_ID}"
+mkdir -p "${LOCAL_RUN_DIR}"
+
+if [[ ${DRY_RUN} -eq 1 ]]; then
+    cat <<EOF
+Dry run only. Nothing will be executed.
+cluster: ${CLUSTER}
+db: ${REMOTE_MYSQL_DB}
+queries: ${QUERY_LIST:-all tpch queries}
+tpch-tools dir: ${REMOTE_TPCH_TOOLS_DIR}
+fe dir: ${REMOTE_FE_DIR}
+lib dir: ${REMOTE_LIB_ARCHIVE_DIR}
+mysql port: ${REMOTE_MYSQL_PORT}
+http port: ${REMOTE_HTTP_PORT}
+profile dir: ${PROFILE_OUTPUT_DIR:-disabled}
+EOF
+    exit 0
+fi
+
+SSH_BASE=(ssh -p "${REMOTE_PORT}" -o StrictHostKeyChecking=accept-new 
"${REMOTE_USER}@${REMOTE_HOST}")
+SCP_BASE=(scp -P "${REMOTE_PORT}")
+
+run_remote() {
+    local script_content=$1
+    "${SSH_BASE[@]}" 'bash -s' -- <<EOF
+${script_content}
+EOF
+}
+
+fetch_remote_file() {
+    local remote_path=$1
+    local local_path=$2
+    "${SCP_BASE[@]}" "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}" 
"${local_path}"
+}
+
+fetch_remote_dir() {
+    local remote_path=$1
+    local local_path=$2
+    mkdir -p "${local_path}"
+    "${SCP_BASE[@]}" -r "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}/." 
"${local_path}"
+}
+
+read -r -d '' REMOTE_SCRIPT <<'EOF' || true
+set -euo pipefail
+
+REMOTE_TPCH_TOOLS_DIR="__REMOTE_TPCH_TOOLS_DIR__"
+REMOTE_FE_DIR="__REMOTE_FE_DIR__"
+REMOTE_LIB_ARCHIVE_DIR="__REMOTE_LIB_ARCHIVE_DIR__"
+BASELINE_ARCHIVE="__BASELINE_ARCHIVE__"
+OPT_ARCHIVE="__OPT_ARCHIVE__"
+REMOTE_CLUSTER_START_CMD="__REMOTE_CLUSTER_START_CMD__"
+REMOTE_CLUSTER_STOP_CMD="__REMOTE_CLUSTER_STOP_CMD__"
+REMOTE_MYSQL_HOST="__REMOTE_MYSQL_HOST__"
+REMOTE_MYSQL_PORT="__REMOTE_MYSQL_PORT__"
+REMOTE_MYSQL_USER="__REMOTE_MYSQL_USER__"
+REMOTE_MYSQL_DB="__REMOTE_MYSQL_DB__"
+REMOTE_JAVA_HOME="__REMOTE_JAVA_HOME__"
+WAIT_TIMEOUT_SECONDS="__WAIT_TIMEOUT_SECONDS__"
+QUERY_LIST="__QUERY_LIST__"
+REMOTE_HTTP_PORT="__REMOTE_HTTP_PORT__"
+PROFILE_OUTPUT_DIR="__PROFILE_OUTPUT_DIR__"
+RUN_ID="__RUN_ID__"
+
+REPORT_ROOT="${REMOTE_TPCH_TOOLS_DIR}/perf-reports/${RUN_ID}"
+RESULT_CSV_DIR="${REPORT_ROOT}/csv"
+QUERIES_DIR="${REMOTE_TPCH_TOOLS_DIR}/queries"
+mkdir -p "${RESULT_CSV_DIR}"
+PROFILE_STAGING_DIR="${REPORT_ROOT}/profiles"
+if [[ -n "${PROFILE_OUTPUT_DIR}" ]]; then
+    mkdir -p "${PROFILE_STAGING_DIR}"
+fi
+
+run_shell_command() {
+    local cmd=$1
+    bash -lc "export JAVA_HOME='${REMOTE_JAVA_HOME}'; export 
PATH='${REMOTE_JAVA_HOME}/bin':\"\$PATH\"; ${cmd}"
+}
+
+wait_fe() {
+    local deadline=$(( $(date +%s) + WAIT_TIMEOUT_SECONDS ))
+    while true; do
+        if mysql -h"${REMOTE_MYSQL_HOST}" -P"${REMOTE_MYSQL_PORT}" 
-u"${REMOTE_MYSQL_USER}" -e 'select 1' >/dev/null 2>&1; then
+            return 0
+        fi
+        if [[ $(date +%s) -ge ${deadline} ]]; then
+            exit 1
+        fi
+        sleep 5
+    done
+}
+
+selected_queries() {
+    if [[ -n "${QUERY_LIST}" ]]; then
+        printf '%s\n' "${QUERY_LIST}" | tr ', ' '\n\n' | sed '/^$/d'
+    else
+        seq 1 22
+    fi
+}
+
+switch_lib() {
+    local archive_path="${REMOTE_LIB_ARCHIVE_DIR}/$1"
+    run_shell_command "${REMOTE_CLUSTER_STOP_CMD}" || true

Review Comment:
   [P1] Do not delete the live FE library before the replacement is ready
   
   If the stop command fails or either archive is missing/corrupt, this flow 
still removes `${REMOTE_FE_DIR}/lib`; `set -e` then exits on `tar` before FE is 
restarted, leaving the shared performance cluster broken. Validate both 
archives and require a successful stop first, extract to a staging directory, 
and provide rollback/restart cleanup before replacing the live library.



##########
plans/test-in-blackhouse/run_remote_tpch_perf_compare.sh:
##########
@@ -0,0 +1,483 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+LOCAL_REPORT_ROOT="${SCRIPT_DIR}/tpch-perf-reports"
+
+CLUSTER="${CLUSTER:-cluster1}"
+REMOTE_USER="${REMOTE_USER:-root}"
+REMOTE_HOST="${REMOTE_HOST:-blackhouse}"
+REMOTE_PORT="${REMOTE_PORT:-22}"
+REMOTE_TPCH_TOOLS_DIR="${REMOTE_TPCH_TOOLS_DIR:-/root/mal/tpch/tpch-tools}"
+REMOTE_FE_DIR="${REMOTE_FE_DIR:-}"
+REMOTE_LIB_ARCHIVE_DIR="${REMOTE_LIB_ARCHIVE_DIR:-}"
+BASELINE_ARCHIVE="${BASELINE_ARCHIVE:-without-opt.tar}"
+OPT_ARCHIVE="${OPT_ARCHIVE:-with-opt.tar}"
+REMOTE_CLUSTER_START_CMD="${REMOTE_CLUSTER_START_CMD:-}"
+REMOTE_CLUSTER_STOP_CMD="${REMOTE_CLUSTER_STOP_CMD:-}"
+REMOTE_MYSQL_HOST="${REMOTE_MYSQL_HOST:-127.0.0.1}"
+REMOTE_MYSQL_PORT="${REMOTE_MYSQL_PORT:-}"
+REMOTE_MYSQL_USER="${REMOTE_MYSQL_USER:-root}"
+REMOTE_HTTP_PORT="${REMOTE_HTTP_PORT:-}"
+REMOTE_MYSQL_DB="${REMOTE_MYSQL_DB:-tpch_sf1000}"
+REMOTE_JAVA_HOME="${REMOTE_JAVA_HOME:-/usr/lib/jvm/java-17-openjdk-amd64}"
+WAIT_TIMEOUT_SECONDS="${WAIT_TIMEOUT_SECONDS:-300}"
+QUERY_LIST="${QUERY_LIST:-}"
+PROFILE_OUTPUT_DIR="${PROFILE_OUTPUT_DIR:-}"
+DRY_RUN=0
+
+apply_cluster_defaults() {
+    case "${CLUSTER}" in
+    cluster1)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=9030}"
+        : "${REMOTE_HTTP_PORT:=8030}"
+        ;;
+    cluster2)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=19030}"
+        : "${REMOTE_HTTP_PORT:=18030}"
+        ;;
+    *)
+        echo "Unsupported cluster: ${CLUSTER}" >&2
+        exit 1
+        ;;
+    esac
+
+    : "${REMOTE_CLUSTER_START_CMD:=${REMOTE_FE_DIR}/bin/start_fe.sh --daemon}"
+    : "${REMOTE_CLUSTER_STOP_CMD:=${REMOTE_FE_DIR}/bin/stop_fe.sh}"
+}
+
+usage() {
+    cat <<EOF
+Usage: $0 [--cluster cluster1|cluster2] [--db tpch_sf1000] [--queries '1,3,7'] 
[--profile DIR] [--dry-run]
+
+This script compares TPCH query execution time only.
+It switches baseline/opt FE lib archives, runs selected TPCH queries with
+1 cold run and 2 hot runs, then writes a summary table.
+
+When --profile DIR is set, the final hot-run query profile for each query is 
saved under
+DIR/queryN/without-opt.profile and DIR/queryN/with-opt.profile.
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+    --cluster)
+        CLUSTER="$2"
+        shift 2
+        ;;
+    --db)
+        REMOTE_MYSQL_DB="$2"
+        shift 2
+        ;;
+    --queries)
+        QUERY_LIST="$2"
+        shift 2
+        ;;
+    --profile)
+        PROFILE_OUTPUT_DIR="$2"
+        shift 2
+        ;;
+    --dry-run)
+        DRY_RUN=1
+        shift
+        ;;
+    -h|--help)
+        usage
+        exit 0
+        ;;
+    *)
+        echo "Unknown argument: $1" >&2
+        usage >&2
+        exit 1
+        ;;
+    esac
+done
+
+apply_cluster_defaults
+
+mkdir -p "${LOCAL_REPORT_ROOT}"
+RUN_ID=$(date +%Y%m%d_%H%M%S)
+LOCAL_RUN_DIR="${LOCAL_REPORT_ROOT}/${RUN_ID}"
+mkdir -p "${LOCAL_RUN_DIR}"
+
+if [[ ${DRY_RUN} -eq 1 ]]; then
+    cat <<EOF
+Dry run only. Nothing will be executed.
+cluster: ${CLUSTER}
+db: ${REMOTE_MYSQL_DB}
+queries: ${QUERY_LIST:-all tpch queries}
+tpch-tools dir: ${REMOTE_TPCH_TOOLS_DIR}
+fe dir: ${REMOTE_FE_DIR}
+lib dir: ${REMOTE_LIB_ARCHIVE_DIR}
+mysql port: ${REMOTE_MYSQL_PORT}
+http port: ${REMOTE_HTTP_PORT}
+profile dir: ${PROFILE_OUTPUT_DIR:-disabled}
+EOF
+    exit 0
+fi
+
+SSH_BASE=(ssh -p "${REMOTE_PORT}" -o StrictHostKeyChecking=accept-new 
"${REMOTE_USER}@${REMOTE_HOST}")
+SCP_BASE=(scp -P "${REMOTE_PORT}")
+
+run_remote() {
+    local script_content=$1
+    "${SSH_BASE[@]}" 'bash -s' -- <<EOF
+${script_content}
+EOF
+}
+
+fetch_remote_file() {
+    local remote_path=$1
+    local local_path=$2
+    "${SCP_BASE[@]}" "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}" 
"${local_path}"
+}
+
+fetch_remote_dir() {
+    local remote_path=$1
+    local local_path=$2
+    mkdir -p "${local_path}"
+    "${SCP_BASE[@]}" -r "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}/." 
"${local_path}"
+}
+
+read -r -d '' REMOTE_SCRIPT <<'EOF' || true
+set -euo pipefail
+
+REMOTE_TPCH_TOOLS_DIR="__REMOTE_TPCH_TOOLS_DIR__"
+REMOTE_FE_DIR="__REMOTE_FE_DIR__"
+REMOTE_LIB_ARCHIVE_DIR="__REMOTE_LIB_ARCHIVE_DIR__"
+BASELINE_ARCHIVE="__BASELINE_ARCHIVE__"
+OPT_ARCHIVE="__OPT_ARCHIVE__"
+REMOTE_CLUSTER_START_CMD="__REMOTE_CLUSTER_START_CMD__"
+REMOTE_CLUSTER_STOP_CMD="__REMOTE_CLUSTER_STOP_CMD__"
+REMOTE_MYSQL_HOST="__REMOTE_MYSQL_HOST__"
+REMOTE_MYSQL_PORT="__REMOTE_MYSQL_PORT__"
+REMOTE_MYSQL_USER="__REMOTE_MYSQL_USER__"
+REMOTE_MYSQL_DB="__REMOTE_MYSQL_DB__"
+REMOTE_JAVA_HOME="__REMOTE_JAVA_HOME__"
+WAIT_TIMEOUT_SECONDS="__WAIT_TIMEOUT_SECONDS__"
+QUERY_LIST="__QUERY_LIST__"
+REMOTE_HTTP_PORT="__REMOTE_HTTP_PORT__"
+PROFILE_OUTPUT_DIR="__PROFILE_OUTPUT_DIR__"
+RUN_ID="__RUN_ID__"
+
+REPORT_ROOT="${REMOTE_TPCH_TOOLS_DIR}/perf-reports/${RUN_ID}"
+RESULT_CSV_DIR="${REPORT_ROOT}/csv"
+QUERIES_DIR="${REMOTE_TPCH_TOOLS_DIR}/queries"
+mkdir -p "${RESULT_CSV_DIR}"
+PROFILE_STAGING_DIR="${REPORT_ROOT}/profiles"
+if [[ -n "${PROFILE_OUTPUT_DIR}" ]]; then
+    mkdir -p "${PROFILE_STAGING_DIR}"
+fi
+
+run_shell_command() {
+    local cmd=$1
+    bash -lc "export JAVA_HOME='${REMOTE_JAVA_HOME}'; export 
PATH='${REMOTE_JAVA_HOME}/bin':\"\$PATH\"; ${cmd}"
+}
+
+wait_fe() {
+    local deadline=$(( $(date +%s) + WAIT_TIMEOUT_SECONDS ))
+    while true; do
+        if mysql -h"${REMOTE_MYSQL_HOST}" -P"${REMOTE_MYSQL_PORT}" 
-u"${REMOTE_MYSQL_USER}" -e 'select 1' >/dev/null 2>&1; then
+            return 0
+        fi
+        if [[ $(date +%s) -ge ${deadline} ]]; then
+            exit 1
+        fi
+        sleep 5
+    done
+}
+
+selected_queries() {
+    if [[ -n "${QUERY_LIST}" ]]; then
+        printf '%s\n' "${QUERY_LIST}" | tr ', ' '\n\n' | sed '/^$/d'
+    else
+        seq 1 22
+    fi
+}
+
+switch_lib() {
+    local archive_path="${REMOTE_LIB_ARCHIVE_DIR}/$1"
+    run_shell_command "${REMOTE_CLUSTER_STOP_CMD}" || true
+    rm -rf "${REMOTE_FE_DIR}/lib"
+    tar --warning=no-unknown-keyword -xf "${archive_path}" -C 
"${REMOTE_FE_DIR}"
+    run_shell_command "${REMOTE_CLUSTER_START_CMD}"
+    wait_fe
+}
+
+profile_file_name() {
+    local label=$1
+    case "${label}" in
+    baseline) printf '%s\n' 'without-opt.profile' ;;
+    with_opt) printf '%s\n' 'with-opt.profile' ;;
+    *) printf '%s.profile\n' "${label}" ;;
+    esac
+}
+
+fetch_query_profile() {
+    local label=$1
+    local query=$2
+    local tag=$3
+    [[ -n "${PROFILE_OUTPUT_DIR}" ]] || return 0
+
+    local query_profile_dir="${PROFILE_STAGING_DIR}/query${query}"
+    mkdir -p "${query_profile_dir}"
+    local profile_path="${query_profile_dir}/$(profile_file_name "${label}")"
+    local profile_list_path="${query_profile_dir}/$(profile_file_name 
"${label}").list.json"
+    local profile_resp_path="${query_profile_dir}/$(profile_file_name 
"${label}").response.json"
+
+    local profile_id=""
+    local profile_list=""
+    local profile_deadline=$(( $(date +%s) + 30 ))

Review Comment:
   [P1] Make the profile deadline bound the HTTP requests
   
   This deadline is checked only after the profile-list `curl` returns, while 
both profile requests have unlimited transfer time. A stalled connection or FE 
HTTP handler can therefore hang the entire comparison before the loop ever 
evaluates the 30-second cutoff (and the later profile-text request is outside 
it). Add connect and total timeouts to both calls, capping each polling request 
by the remaining deadline.



##########
plans/test-in-blackhouse/run_remote_tpch_perf_compare.sh:
##########
@@ -0,0 +1,483 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+LOCAL_REPORT_ROOT="${SCRIPT_DIR}/tpch-perf-reports"
+
+CLUSTER="${CLUSTER:-cluster1}"
+REMOTE_USER="${REMOTE_USER:-root}"
+REMOTE_HOST="${REMOTE_HOST:-blackhouse}"
+REMOTE_PORT="${REMOTE_PORT:-22}"
+REMOTE_TPCH_TOOLS_DIR="${REMOTE_TPCH_TOOLS_DIR:-/root/mal/tpch/tpch-tools}"
+REMOTE_FE_DIR="${REMOTE_FE_DIR:-}"
+REMOTE_LIB_ARCHIVE_DIR="${REMOTE_LIB_ARCHIVE_DIR:-}"
+BASELINE_ARCHIVE="${BASELINE_ARCHIVE:-without-opt.tar}"
+OPT_ARCHIVE="${OPT_ARCHIVE:-with-opt.tar}"
+REMOTE_CLUSTER_START_CMD="${REMOTE_CLUSTER_START_CMD:-}"
+REMOTE_CLUSTER_STOP_CMD="${REMOTE_CLUSTER_STOP_CMD:-}"
+REMOTE_MYSQL_HOST="${REMOTE_MYSQL_HOST:-127.0.0.1}"
+REMOTE_MYSQL_PORT="${REMOTE_MYSQL_PORT:-}"
+REMOTE_MYSQL_USER="${REMOTE_MYSQL_USER:-root}"
+REMOTE_HTTP_PORT="${REMOTE_HTTP_PORT:-}"
+REMOTE_MYSQL_DB="${REMOTE_MYSQL_DB:-tpch_sf1000}"
+REMOTE_JAVA_HOME="${REMOTE_JAVA_HOME:-/usr/lib/jvm/java-17-openjdk-amd64}"
+WAIT_TIMEOUT_SECONDS="${WAIT_TIMEOUT_SECONDS:-300}"
+QUERY_LIST="${QUERY_LIST:-}"
+PROFILE_OUTPUT_DIR="${PROFILE_OUTPUT_DIR:-}"
+DRY_RUN=0
+
+apply_cluster_defaults() {
+    case "${CLUSTER}" in
+    cluster1)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=9030}"
+        : "${REMOTE_HTTP_PORT:=8030}"
+        ;;
+    cluster2)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=19030}"
+        : "${REMOTE_HTTP_PORT:=18030}"
+        ;;
+    *)
+        echo "Unsupported cluster: ${CLUSTER}" >&2
+        exit 1
+        ;;
+    esac
+
+    : "${REMOTE_CLUSTER_START_CMD:=${REMOTE_FE_DIR}/bin/start_fe.sh --daemon}"
+    : "${REMOTE_CLUSTER_STOP_CMD:=${REMOTE_FE_DIR}/bin/stop_fe.sh}"
+}
+
+usage() {
+    cat <<EOF
+Usage: $0 [--cluster cluster1|cluster2] [--db tpch_sf1000] [--queries '1,3,7'] 
[--profile DIR] [--dry-run]
+
+This script compares TPCH query execution time only.
+It switches baseline/opt FE lib archives, runs selected TPCH queries with
+1 cold run and 2 hot runs, then writes a summary table.
+
+When --profile DIR is set, the final hot-run query profile for each query is 
saved under
+DIR/queryN/without-opt.profile and DIR/queryN/with-opt.profile.
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+    --cluster)
+        CLUSTER="$2"
+        shift 2
+        ;;
+    --db)
+        REMOTE_MYSQL_DB="$2"
+        shift 2
+        ;;
+    --queries)
+        QUERY_LIST="$2"
+        shift 2
+        ;;
+    --profile)
+        PROFILE_OUTPUT_DIR="$2"
+        shift 2
+        ;;
+    --dry-run)
+        DRY_RUN=1
+        shift
+        ;;
+    -h|--help)
+        usage
+        exit 0
+        ;;
+    *)
+        echo "Unknown argument: $1" >&2
+        usage >&2
+        exit 1
+        ;;
+    esac
+done
+
+apply_cluster_defaults
+
+mkdir -p "${LOCAL_REPORT_ROOT}"
+RUN_ID=$(date +%Y%m%d_%H%M%S)
+LOCAL_RUN_DIR="${LOCAL_REPORT_ROOT}/${RUN_ID}"
+mkdir -p "${LOCAL_RUN_DIR}"
+
+if [[ ${DRY_RUN} -eq 1 ]]; then
+    cat <<EOF
+Dry run only. Nothing will be executed.
+cluster: ${CLUSTER}
+db: ${REMOTE_MYSQL_DB}
+queries: ${QUERY_LIST:-all tpch queries}
+tpch-tools dir: ${REMOTE_TPCH_TOOLS_DIR}
+fe dir: ${REMOTE_FE_DIR}
+lib dir: ${REMOTE_LIB_ARCHIVE_DIR}
+mysql port: ${REMOTE_MYSQL_PORT}
+http port: ${REMOTE_HTTP_PORT}
+profile dir: ${PROFILE_OUTPUT_DIR:-disabled}
+EOF
+    exit 0
+fi
+
+SSH_BASE=(ssh -p "${REMOTE_PORT}" -o StrictHostKeyChecking=accept-new 
"${REMOTE_USER}@${REMOTE_HOST}")
+SCP_BASE=(scp -P "${REMOTE_PORT}")
+
+run_remote() {
+    local script_content=$1
+    "${SSH_BASE[@]}" 'bash -s' -- <<EOF
+${script_content}
+EOF
+}
+
+fetch_remote_file() {
+    local remote_path=$1
+    local local_path=$2
+    "${SCP_BASE[@]}" "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}" 
"${local_path}"
+}
+
+fetch_remote_dir() {
+    local remote_path=$1
+    local local_path=$2
+    mkdir -p "${local_path}"
+    "${SCP_BASE[@]}" -r "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}/." 
"${local_path}"
+}
+
+read -r -d '' REMOTE_SCRIPT <<'EOF' || true
+set -euo pipefail
+
+REMOTE_TPCH_TOOLS_DIR="__REMOTE_TPCH_TOOLS_DIR__"
+REMOTE_FE_DIR="__REMOTE_FE_DIR__"
+REMOTE_LIB_ARCHIVE_DIR="__REMOTE_LIB_ARCHIVE_DIR__"
+BASELINE_ARCHIVE="__BASELINE_ARCHIVE__"
+OPT_ARCHIVE="__OPT_ARCHIVE__"
+REMOTE_CLUSTER_START_CMD="__REMOTE_CLUSTER_START_CMD__"
+REMOTE_CLUSTER_STOP_CMD="__REMOTE_CLUSTER_STOP_CMD__"
+REMOTE_MYSQL_HOST="__REMOTE_MYSQL_HOST__"
+REMOTE_MYSQL_PORT="__REMOTE_MYSQL_PORT__"
+REMOTE_MYSQL_USER="__REMOTE_MYSQL_USER__"
+REMOTE_MYSQL_DB="__REMOTE_MYSQL_DB__"
+REMOTE_JAVA_HOME="__REMOTE_JAVA_HOME__"
+WAIT_TIMEOUT_SECONDS="__WAIT_TIMEOUT_SECONDS__"
+QUERY_LIST="__QUERY_LIST__"
+REMOTE_HTTP_PORT="__REMOTE_HTTP_PORT__"
+PROFILE_OUTPUT_DIR="__PROFILE_OUTPUT_DIR__"
+RUN_ID="__RUN_ID__"
+
+REPORT_ROOT="${REMOTE_TPCH_TOOLS_DIR}/perf-reports/${RUN_ID}"
+RESULT_CSV_DIR="${REPORT_ROOT}/csv"
+QUERIES_DIR="${REMOTE_TPCH_TOOLS_DIR}/queries"
+mkdir -p "${RESULT_CSV_DIR}"
+PROFILE_STAGING_DIR="${REPORT_ROOT}/profiles"
+if [[ -n "${PROFILE_OUTPUT_DIR}" ]]; then
+    mkdir -p "${PROFILE_STAGING_DIR}"
+fi
+
+run_shell_command() {
+    local cmd=$1
+    bash -lc "export JAVA_HOME='${REMOTE_JAVA_HOME}'; export 
PATH='${REMOTE_JAVA_HOME}/bin':\"\$PATH\"; ${cmd}"
+}
+
+wait_fe() {
+    local deadline=$(( $(date +%s) + WAIT_TIMEOUT_SECONDS ))
+    while true; do
+        if mysql -h"${REMOTE_MYSQL_HOST}" -P"${REMOTE_MYSQL_PORT}" 
-u"${REMOTE_MYSQL_USER}" -e 'select 1' >/dev/null 2>&1; then
+            return 0
+        fi
+        if [[ $(date +%s) -ge ${deadline} ]]; then
+            exit 1
+        fi
+        sleep 5
+    done
+}
+
+selected_queries() {
+    if [[ -n "${QUERY_LIST}" ]]; then
+        printf '%s\n' "${QUERY_LIST}" | tr ', ' '\n\n' | sed '/^$/d'
+    else
+        seq 1 22
+    fi
+}
+
+switch_lib() {
+    local archive_path="${REMOTE_LIB_ARCHIVE_DIR}/$1"
+    run_shell_command "${REMOTE_CLUSTER_STOP_CMD}" || true
+    rm -rf "${REMOTE_FE_DIR}/lib"
+    tar --warning=no-unknown-keyword -xf "${archive_path}" -C 
"${REMOTE_FE_DIR}"
+    run_shell_command "${REMOTE_CLUSTER_START_CMD}"
+    wait_fe
+}
+
+profile_file_name() {
+    local label=$1
+    case "${label}" in
+    baseline) printf '%s\n' 'without-opt.profile' ;;
+    with_opt) printf '%s\n' 'with-opt.profile' ;;
+    *) printf '%s.profile\n' "${label}" ;;
+    esac
+}
+
+fetch_query_profile() {
+    local label=$1
+    local query=$2
+    local tag=$3
+    [[ -n "${PROFILE_OUTPUT_DIR}" ]] || return 0
+
+    local query_profile_dir="${PROFILE_STAGING_DIR}/query${query}"
+    mkdir -p "${query_profile_dir}"
+    local profile_path="${query_profile_dir}/$(profile_file_name "${label}")"
+    local profile_list_path="${query_profile_dir}/$(profile_file_name 
"${label}").list.json"
+    local profile_resp_path="${query_profile_dir}/$(profile_file_name 
"${label}").response.json"
+
+    local profile_id=""
+    local profile_list=""
+    local profile_deadline=$(( $(date +%s) + 30 ))
+    while true; do
+        if ! profile_list=$(curl --fail --silent --show-error \
+                -u "${REMOTE_MYSQL_USER}:${MYSQL_PWD:-}" \
+                
"http://${REMOTE_MYSQL_HOST}:${REMOTE_HTTP_PORT}/rest/v1/query_profile"; 2>&1); 
then
+            printf 'Failed to get query profile list for query%s (%s):\n%s\n' 
"${query}" "${label}" "${profile_list}" \
+                | tee -a "${REPORT_ROOT}/${label}.log" >&2
+            return 1
+        fi
+        printf '%s\n' "${profile_list}" >"${profile_list_path}"
+
+        if ! profile_id=$(python3 - "${tag}" "${profile_list_path}" <<'PY'
+import json
+import sys
+
+tag = sys.argv[1]
+path = sys.argv[2]
+with open(path, encoding="utf-8") as f:
+    payload = json.load(f)
+rows = payload.get("data", {}).get("rows", [])
+for row in rows:
+    if tag in str(row.get("Sql Statement", "")):
+        state = str(row.get("Profile Completion State", ""))
+        if state == "COMPLETE":
+            print(row.get("Profile ID", ""))
+            break
+PY
+        ); then
+            printf 'Failed to parse query profile list for query%s (%s)\n' 
"${query}" "${label}" \
+                | tee -a "${REPORT_ROOT}/${label}.log" >&2
+            return 1
+        fi
+        if [[ -n "${profile_id}" ]] || [[ $(date +%s) -ge ${profile_deadline} 
]]; then
+            break
+        fi
+        sleep 1
+    done
+
+    if [[ -z "${profile_id}" ]]; then
+        printf 'Missing COMPLETE query profile for query%s (%s), tag: %s\n' 
"${query}" "${label}" "${tag}" \
+            | tee -a "${REPORT_ROOT}/${label}.log" >&2
+        return 1
+    fi
+
+    local profile_resp
+    if ! profile_resp=$(curl --fail --silent --show-error \
+            -u "${REMOTE_MYSQL_USER}:${MYSQL_PWD:-}" \
+            
"http://${REMOTE_MYSQL_HOST}:${REMOTE_HTTP_PORT}/rest/v1/query_profile/text/${profile_id}";
 2>&1); then
+        printf 'Failed to get query profile %s for query%s (%s):\n%s\n' 
"${profile_id}" "${query}" "${label}" "${profile_resp}" \
+            | tee -a "${REPORT_ROOT}/${label}.log" >&2
+        return 1
+    fi
+    printf '%s\n' "${profile_resp}" >"${profile_resp_path}"
+
+    if ! python3 - "${profile_path}" "${profile_resp_path}" <<'PY'
+import json
+import sys
+
+target = sys.argv[1]
+path = sys.argv[2]
+with open(path, encoding="utf-8") as f:
+    payload = json.load(f)
+data = payload.get("data", "")
+with open(target, "w", encoding="utf-8") as f:
+    f.write(data)
+    if data and not data.endswith("\n"):
+        f.write("\n")
+PY
+    then
+        printf 'Failed to write query profile %s for query%s (%s)\n' 
"${profile_id}" "${query}" "${label}" \
+            | tee -a "${REPORT_ROOT}/${label}.log" >&2
+        return 1
+    fi
+    rm -f "${profile_list_path}" "${profile_resp_path}"
+}
+
+run_perf() {
+    local label=$1
+    local result_csv="${RESULT_CSV_DIR}/${label}.csv"
+    local log_file="${REPORT_ROOT}/${label}.log"
+    : >"${result_csv}"
+    : >"${log_file}"
+    while IFS= read -r i; do
+        [[ -n "${i}" ]] || continue
+        sql_file="${QUERIES_DIR}/q${i}.sql"
+        if [[ ! -f "${sql_file}" ]]; then

Review Comment:
   [P1] Fail the comparison when a requested query is incomplete
   
   Every missing SQL file or failed cold/hot run reaches `continue`, and the 
summary loader later discards the short row. If a query fails in both variants 
it vanishes entirely, yet the script publishes the partial report and exits 
zero, which can make a broken optimization run look valid. Record failures, 
require the complete identical requested query set in both CSVs, and exit 
nonzero after preserving the logs.



##########
plans/test-in-blackhouse/run_remote_tpch_perf_compare.sh:
##########
@@ -0,0 +1,483 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+LOCAL_REPORT_ROOT="${SCRIPT_DIR}/tpch-perf-reports"
+
+CLUSTER="${CLUSTER:-cluster1}"
+REMOTE_USER="${REMOTE_USER:-root}"
+REMOTE_HOST="${REMOTE_HOST:-blackhouse}"
+REMOTE_PORT="${REMOTE_PORT:-22}"
+REMOTE_TPCH_TOOLS_DIR="${REMOTE_TPCH_TOOLS_DIR:-/root/mal/tpch/tpch-tools}"
+REMOTE_FE_DIR="${REMOTE_FE_DIR:-}"
+REMOTE_LIB_ARCHIVE_DIR="${REMOTE_LIB_ARCHIVE_DIR:-}"
+BASELINE_ARCHIVE="${BASELINE_ARCHIVE:-without-opt.tar}"
+OPT_ARCHIVE="${OPT_ARCHIVE:-with-opt.tar}"
+REMOTE_CLUSTER_START_CMD="${REMOTE_CLUSTER_START_CMD:-}"
+REMOTE_CLUSTER_STOP_CMD="${REMOTE_CLUSTER_STOP_CMD:-}"
+REMOTE_MYSQL_HOST="${REMOTE_MYSQL_HOST:-127.0.0.1}"
+REMOTE_MYSQL_PORT="${REMOTE_MYSQL_PORT:-}"
+REMOTE_MYSQL_USER="${REMOTE_MYSQL_USER:-root}"
+REMOTE_HTTP_PORT="${REMOTE_HTTP_PORT:-}"
+REMOTE_MYSQL_DB="${REMOTE_MYSQL_DB:-tpch_sf1000}"
+REMOTE_JAVA_HOME="${REMOTE_JAVA_HOME:-/usr/lib/jvm/java-17-openjdk-amd64}"
+WAIT_TIMEOUT_SECONDS="${WAIT_TIMEOUT_SECONDS:-300}"
+QUERY_LIST="${QUERY_LIST:-}"
+PROFILE_OUTPUT_DIR="${PROFILE_OUTPUT_DIR:-}"
+DRY_RUN=0
+
+apply_cluster_defaults() {
+    case "${CLUSTER}" in
+    cluster1)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=9030}"
+        : "${REMOTE_HTTP_PORT:=8030}"
+        ;;
+    cluster2)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=19030}"
+        : "${REMOTE_HTTP_PORT:=18030}"
+        ;;
+    *)
+        echo "Unsupported cluster: ${CLUSTER}" >&2
+        exit 1
+        ;;
+    esac
+
+    : "${REMOTE_CLUSTER_START_CMD:=${REMOTE_FE_DIR}/bin/start_fe.sh --daemon}"
+    : "${REMOTE_CLUSTER_STOP_CMD:=${REMOTE_FE_DIR}/bin/stop_fe.sh}"
+}
+
+usage() {
+    cat <<EOF
+Usage: $0 [--cluster cluster1|cluster2] [--db tpch_sf1000] [--queries '1,3,7'] 
[--profile DIR] [--dry-run]
+
+This script compares TPCH query execution time only.
+It switches baseline/opt FE lib archives, runs selected TPCH queries with
+1 cold run and 2 hot runs, then writes a summary table.
+
+When --profile DIR is set, the final hot-run query profile for each query is 
saved under
+DIR/queryN/without-opt.profile and DIR/queryN/with-opt.profile.
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+    --cluster)
+        CLUSTER="$2"
+        shift 2
+        ;;
+    --db)
+        REMOTE_MYSQL_DB="$2"
+        shift 2
+        ;;
+    --queries)
+        QUERY_LIST="$2"
+        shift 2
+        ;;
+    --profile)
+        PROFILE_OUTPUT_DIR="$2"
+        shift 2
+        ;;
+    --dry-run)
+        DRY_RUN=1
+        shift
+        ;;
+    -h|--help)
+        usage
+        exit 0
+        ;;
+    *)
+        echo "Unknown argument: $1" >&2
+        usage >&2
+        exit 1
+        ;;
+    esac
+done
+
+apply_cluster_defaults
+
+mkdir -p "${LOCAL_REPORT_ROOT}"
+RUN_ID=$(date +%Y%m%d_%H%M%S)
+LOCAL_RUN_DIR="${LOCAL_REPORT_ROOT}/${RUN_ID}"
+mkdir -p "${LOCAL_RUN_DIR}"
+
+if [[ ${DRY_RUN} -eq 1 ]]; then
+    cat <<EOF
+Dry run only. Nothing will be executed.
+cluster: ${CLUSTER}
+db: ${REMOTE_MYSQL_DB}
+queries: ${QUERY_LIST:-all tpch queries}
+tpch-tools dir: ${REMOTE_TPCH_TOOLS_DIR}
+fe dir: ${REMOTE_FE_DIR}
+lib dir: ${REMOTE_LIB_ARCHIVE_DIR}
+mysql port: ${REMOTE_MYSQL_PORT}
+http port: ${REMOTE_HTTP_PORT}
+profile dir: ${PROFILE_OUTPUT_DIR:-disabled}
+EOF
+    exit 0
+fi
+
+SSH_BASE=(ssh -p "${REMOTE_PORT}" -o StrictHostKeyChecking=accept-new 
"${REMOTE_USER}@${REMOTE_HOST}")
+SCP_BASE=(scp -P "${REMOTE_PORT}")
+
+run_remote() {
+    local script_content=$1
+    "${SSH_BASE[@]}" 'bash -s' -- <<EOF
+${script_content}
+EOF
+}
+
+fetch_remote_file() {
+    local remote_path=$1
+    local local_path=$2
+    "${SCP_BASE[@]}" "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}" 
"${local_path}"
+}
+
+fetch_remote_dir() {
+    local remote_path=$1
+    local local_path=$2
+    mkdir -p "${local_path}"
+    "${SCP_BASE[@]}" -r "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}/." 
"${local_path}"
+}
+
+read -r -d '' REMOTE_SCRIPT <<'EOF' || true
+set -euo pipefail
+
+REMOTE_TPCH_TOOLS_DIR="__REMOTE_TPCH_TOOLS_DIR__"
+REMOTE_FE_DIR="__REMOTE_FE_DIR__"
+REMOTE_LIB_ARCHIVE_DIR="__REMOTE_LIB_ARCHIVE_DIR__"
+BASELINE_ARCHIVE="__BASELINE_ARCHIVE__"
+OPT_ARCHIVE="__OPT_ARCHIVE__"
+REMOTE_CLUSTER_START_CMD="__REMOTE_CLUSTER_START_CMD__"
+REMOTE_CLUSTER_STOP_CMD="__REMOTE_CLUSTER_STOP_CMD__"
+REMOTE_MYSQL_HOST="__REMOTE_MYSQL_HOST__"
+REMOTE_MYSQL_PORT="__REMOTE_MYSQL_PORT__"
+REMOTE_MYSQL_USER="__REMOTE_MYSQL_USER__"
+REMOTE_MYSQL_DB="__REMOTE_MYSQL_DB__"
+REMOTE_JAVA_HOME="__REMOTE_JAVA_HOME__"
+WAIT_TIMEOUT_SECONDS="__WAIT_TIMEOUT_SECONDS__"
+QUERY_LIST="__QUERY_LIST__"
+REMOTE_HTTP_PORT="__REMOTE_HTTP_PORT__"
+PROFILE_OUTPUT_DIR="__PROFILE_OUTPUT_DIR__"
+RUN_ID="__RUN_ID__"
+
+REPORT_ROOT="${REMOTE_TPCH_TOOLS_DIR}/perf-reports/${RUN_ID}"
+RESULT_CSV_DIR="${REPORT_ROOT}/csv"
+QUERIES_DIR="${REMOTE_TPCH_TOOLS_DIR}/queries"
+mkdir -p "${RESULT_CSV_DIR}"
+PROFILE_STAGING_DIR="${REPORT_ROOT}/profiles"
+if [[ -n "${PROFILE_OUTPUT_DIR}" ]]; then
+    mkdir -p "${PROFILE_STAGING_DIR}"
+fi
+
+run_shell_command() {
+    local cmd=$1
+    bash -lc "export JAVA_HOME='${REMOTE_JAVA_HOME}'; export 
PATH='${REMOTE_JAVA_HOME}/bin':\"\$PATH\"; ${cmd}"
+}
+
+wait_fe() {
+    local deadline=$(( $(date +%s) + WAIT_TIMEOUT_SECONDS ))
+    while true; do
+        if mysql -h"${REMOTE_MYSQL_HOST}" -P"${REMOTE_MYSQL_PORT}" 
-u"${REMOTE_MYSQL_USER}" -e 'select 1' >/dev/null 2>&1; then
+            return 0
+        fi
+        if [[ $(date +%s) -ge ${deadline} ]]; then
+            exit 1
+        fi
+        sleep 5
+    done
+}
+
+selected_queries() {
+    if [[ -n "${QUERY_LIST}" ]]; then
+        printf '%s\n' "${QUERY_LIST}" | tr ', ' '\n\n' | sed '/^$/d'
+    else
+        seq 1 22
+    fi
+}
+
+switch_lib() {
+    local archive_path="${REMOTE_LIB_ARCHIVE_DIR}/$1"
+    run_shell_command "${REMOTE_CLUSTER_STOP_CMD}" || true
+    rm -rf "${REMOTE_FE_DIR}/lib"
+    tar --warning=no-unknown-keyword -xf "${archive_path}" -C 
"${REMOTE_FE_DIR}"
+    run_shell_command "${REMOTE_CLUSTER_START_CMD}"
+    wait_fe
+}
+
+profile_file_name() {
+    local label=$1
+    case "${label}" in
+    baseline) printf '%s\n' 'without-opt.profile' ;;
+    with_opt) printf '%s\n' 'with-opt.profile' ;;
+    *) printf '%s.profile\n' "${label}" ;;
+    esac
+}
+
+fetch_query_profile() {
+    local label=$1
+    local query=$2
+    local tag=$3
+    [[ -n "${PROFILE_OUTPUT_DIR}" ]] || return 0
+
+    local query_profile_dir="${PROFILE_STAGING_DIR}/query${query}"
+    mkdir -p "${query_profile_dir}"
+    local profile_path="${query_profile_dir}/$(profile_file_name "${label}")"
+    local profile_list_path="${query_profile_dir}/$(profile_file_name 
"${label}").list.json"
+    local profile_resp_path="${query_profile_dir}/$(profile_file_name 
"${label}").response.json"
+
+    local profile_id=""
+    local profile_list=""
+    local profile_deadline=$(( $(date +%s) + 30 ))
+    while true; do
+        if ! profile_list=$(curl --fail --silent --show-error \
+                -u "${REMOTE_MYSQL_USER}:${MYSQL_PWD:-}" \
+                
"http://${REMOTE_MYSQL_HOST}:${REMOTE_HTTP_PORT}/rest/v1/query_profile"; 2>&1); 
then
+            printf 'Failed to get query profile list for query%s (%s):\n%s\n' 
"${query}" "${label}" "${profile_list}" \
+                | tee -a "${REPORT_ROOT}/${label}.log" >&2
+            return 1
+        fi
+        printf '%s\n' "${profile_list}" >"${profile_list_path}"
+
+        if ! profile_id=$(python3 - "${tag}" "${profile_list_path}" <<'PY'
+import json
+import sys
+
+tag = sys.argv[1]
+path = sys.argv[2]
+with open(path, encoding="utf-8") as f:
+    payload = json.load(f)
+rows = payload.get("data", {}).get("rows", [])
+for row in rows:
+    if tag in str(row.get("Sql Statement", "")):
+        state = str(row.get("Profile Completion State", ""))
+        if state == "COMPLETE":
+            print(row.get("Profile ID", ""))
+            break
+PY
+        ); then
+            printf 'Failed to parse query profile list for query%s (%s)\n' 
"${query}" "${label}" \
+                | tee -a "${REPORT_ROOT}/${label}.log" >&2
+            return 1
+        fi
+        if [[ -n "${profile_id}" ]] || [[ $(date +%s) -ge ${profile_deadline} 
]]; then
+            break
+        fi
+        sleep 1
+    done
+
+    if [[ -z "${profile_id}" ]]; then
+        printf 'Missing COMPLETE query profile for query%s (%s), tag: %s\n' 
"${query}" "${label}" "${tag}" \
+            | tee -a "${REPORT_ROOT}/${label}.log" >&2
+        return 1
+    fi
+
+    local profile_resp
+    if ! profile_resp=$(curl --fail --silent --show-error \
+            -u "${REMOTE_MYSQL_USER}:${MYSQL_PWD:-}" \
+            
"http://${REMOTE_MYSQL_HOST}:${REMOTE_HTTP_PORT}/rest/v1/query_profile/text/${profile_id}";
 2>&1); then
+        printf 'Failed to get query profile %s for query%s (%s):\n%s\n' 
"${profile_id}" "${query}" "${label}" "${profile_resp}" \
+            | tee -a "${REPORT_ROOT}/${label}.log" >&2
+        return 1
+    fi
+    printf '%s\n' "${profile_resp}" >"${profile_resp_path}"
+
+    if ! python3 - "${profile_path}" "${profile_resp_path}" <<'PY'
+import json
+import sys
+
+target = sys.argv[1]
+path = sys.argv[2]
+with open(path, encoding="utf-8") as f:
+    payload = json.load(f)
+data = payload.get("data", "")
+with open(target, "w", encoding="utf-8") as f:
+    f.write(data)
+    if data and not data.endswith("\n"):
+        f.write("\n")
+PY
+    then
+        printf 'Failed to write query profile %s for query%s (%s)\n' 
"${profile_id}" "${query}" "${label}" \
+            | tee -a "${REPORT_ROOT}/${label}.log" >&2
+        return 1
+    fi
+    rm -f "${profile_list_path}" "${profile_resp_path}"
+}
+
+run_perf() {
+    local label=$1
+    local result_csv="${RESULT_CSV_DIR}/${label}.csv"
+    local log_file="${REPORT_ROOT}/${label}.log"
+    : >"${result_csv}"
+    : >"${log_file}"
+    while IFS= read -r i; do
+        [[ -n "${i}" ]] || continue
+        sql_file="${QUERIES_DIR}/q${i}.sql"
+        if [[ ! -f "${sql_file}" ]]; then
+            printf 'Missing file: %s\n' "${sql_file}" | tee -a "${log_file}"
+            continue
+        fi
+
+        printf 'Running q%s\n' "${i}" >>"${log_file}"
+        printf 'q%s\t' "${i}" | tee -a "${result_csv}"
+
+        start=$(date +%s%3N)
+        if ! output=$(mysql -h"${REMOTE_MYSQL_HOST}" -u"${REMOTE_MYSQL_USER}" 
-P"${REMOTE_MYSQL_PORT}" -D"${REMOTE_MYSQL_DB}" --comments <"${sql_file}" 
2>&1); then
+            printf 'Error: Failed to execute q%s (cold). Output:\n%s\n' "${i}" 
"${output}" | tee -a "${log_file}" >&2
+            printf '\n' | tee -a "${result_csv}"
+            continue
+        fi
+        end=$(date +%s%3N)
+        cold=$((end - start))
+        printf '%s\t' "${cold}" | tee -a "${result_csv}"
+
+        start=$(date +%s%3N)
+        if ! output=$(mysql -h"${REMOTE_MYSQL_HOST}" -u"${REMOTE_MYSQL_USER}" 
-P"${REMOTE_MYSQL_PORT}" -D"${REMOTE_MYSQL_DB}" --comments <"${sql_file}" 
2>&1); then
+            printf 'Error: Failed to execute q%s (hot1). Output:\n%s\n' "${i}" 
"${output}" | tee -a "${log_file}" >&2
+            printf '\n' | tee -a "${result_csv}"
+            continue
+        fi
+        end=$(date +%s%3N)
+        hot1=$((end - start))
+        printf '%s\t' "${hot1}" | tee -a "${result_csv}"
+
+        profile_tag="tpch_perf_compare:${RUN_ID}:${label}:query${i}:hot2"
+        start=$(date +%s%3N)
+        if ! output=$(
+            {
+                if [[ -n "${PROFILE_OUTPUT_DIR}" ]]; then
+                    printf 'SET enable_profile = true;\n'
+                    printf '/* %s */\n' "${profile_tag}"
+                fi
+                cat "${sql_file}"
+            } | mysql -h"${REMOTE_MYSQL_HOST}" -u"${REMOTE_MYSQL_USER}" 
-P"${REMOTE_MYSQL_PORT}" -D"${REMOTE_MYSQL_DB}" --comments 2>&1
+        ); then
+            printf 'Error: Failed to execute q%s (hot2). Output:\n%s\n' "${i}" 
"${output}" | tee -a "${log_file}" >&2
+            printf '\n' | tee -a "${result_csv}"
+            continue
+        fi
+        end=$(date +%s%3N)
+        hot2=$((end - start))
+        best_hot=${hot1}
+        if [[ ${hot2} -lt ${best_hot} ]]; then
+            best_hot=${hot2}
+        fi
+        printf '%s\t%s\n' "${hot2}" "${best_hot}" | tee -a "${result_csv}"
+        fetch_query_profile "${label}" "${i}" "${profile_tag}"
+    done < <(selected_queries)
+}
+
+switch_lib "${BASELINE_ARCHIVE}"

Review Comment:
   [P1] Compare both variants under equivalent cache state
   
   This always runs the full baseline suite first, then restarts only FE before 
the optimized suite. BE page/file caches warmed by every baseline query 
therefore remain available to each optimized `cold` run, so identical binaries 
can appear faster in the optimized column for reasons unrelated to the 
optimizer change. Reset the relevant BE caches before comparable cold samples, 
or interleave/counterbalance the variants with identical warm-up counts and 
report only measurements taken from equivalent states.



##########
plans/test-in-blackhouse/run_remote_tpch_perf_compare.sh:
##########
@@ -0,0 +1,483 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+LOCAL_REPORT_ROOT="${SCRIPT_DIR}/tpch-perf-reports"
+
+CLUSTER="${CLUSTER:-cluster1}"
+REMOTE_USER="${REMOTE_USER:-root}"
+REMOTE_HOST="${REMOTE_HOST:-blackhouse}"
+REMOTE_PORT="${REMOTE_PORT:-22}"
+REMOTE_TPCH_TOOLS_DIR="${REMOTE_TPCH_TOOLS_DIR:-/root/mal/tpch/tpch-tools}"
+REMOTE_FE_DIR="${REMOTE_FE_DIR:-}"
+REMOTE_LIB_ARCHIVE_DIR="${REMOTE_LIB_ARCHIVE_DIR:-}"
+BASELINE_ARCHIVE="${BASELINE_ARCHIVE:-without-opt.tar}"
+OPT_ARCHIVE="${OPT_ARCHIVE:-with-opt.tar}"
+REMOTE_CLUSTER_START_CMD="${REMOTE_CLUSTER_START_CMD:-}"
+REMOTE_CLUSTER_STOP_CMD="${REMOTE_CLUSTER_STOP_CMD:-}"
+REMOTE_MYSQL_HOST="${REMOTE_MYSQL_HOST:-127.0.0.1}"
+REMOTE_MYSQL_PORT="${REMOTE_MYSQL_PORT:-}"
+REMOTE_MYSQL_USER="${REMOTE_MYSQL_USER:-root}"
+REMOTE_HTTP_PORT="${REMOTE_HTTP_PORT:-}"
+REMOTE_MYSQL_DB="${REMOTE_MYSQL_DB:-tpch_sf1000}"
+REMOTE_JAVA_HOME="${REMOTE_JAVA_HOME:-/usr/lib/jvm/java-17-openjdk-amd64}"
+WAIT_TIMEOUT_SECONDS="${WAIT_TIMEOUT_SECONDS:-300}"
+QUERY_LIST="${QUERY_LIST:-}"
+PROFILE_OUTPUT_DIR="${PROFILE_OUTPUT_DIR:-}"
+DRY_RUN=0
+
+apply_cluster_defaults() {
+    case "${CLUSTER}" in
+    cluster1)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=9030}"
+        : "${REMOTE_HTTP_PORT:=8030}"
+        ;;
+    cluster2)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=19030}"
+        : "${REMOTE_HTTP_PORT:=18030}"
+        ;;
+    *)
+        echo "Unsupported cluster: ${CLUSTER}" >&2
+        exit 1
+        ;;
+    esac
+
+    : "${REMOTE_CLUSTER_START_CMD:=${REMOTE_FE_DIR}/bin/start_fe.sh --daemon}"
+    : "${REMOTE_CLUSTER_STOP_CMD:=${REMOTE_FE_DIR}/bin/stop_fe.sh}"
+}
+
+usage() {
+    cat <<EOF
+Usage: $0 [--cluster cluster1|cluster2] [--db tpch_sf1000] [--queries '1,3,7'] 
[--profile DIR] [--dry-run]
+
+This script compares TPCH query execution time only.
+It switches baseline/opt FE lib archives, runs selected TPCH queries with
+1 cold run and 2 hot runs, then writes a summary table.
+
+When --profile DIR is set, the final hot-run query profile for each query is 
saved under
+DIR/queryN/without-opt.profile and DIR/queryN/with-opt.profile.
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+    --cluster)
+        CLUSTER="$2"
+        shift 2
+        ;;
+    --db)
+        REMOTE_MYSQL_DB="$2"
+        shift 2
+        ;;
+    --queries)
+        QUERY_LIST="$2"
+        shift 2
+        ;;
+    --profile)
+        PROFILE_OUTPUT_DIR="$2"
+        shift 2
+        ;;
+    --dry-run)
+        DRY_RUN=1
+        shift
+        ;;
+    -h|--help)
+        usage
+        exit 0
+        ;;
+    *)
+        echo "Unknown argument: $1" >&2
+        usage >&2
+        exit 1
+        ;;
+    esac
+done
+
+apply_cluster_defaults
+
+mkdir -p "${LOCAL_REPORT_ROOT}"
+RUN_ID=$(date +%Y%m%d_%H%M%S)
+LOCAL_RUN_DIR="${LOCAL_REPORT_ROOT}/${RUN_ID}"
+mkdir -p "${LOCAL_RUN_DIR}"
+
+if [[ ${DRY_RUN} -eq 1 ]]; then
+    cat <<EOF
+Dry run only. Nothing will be executed.
+cluster: ${CLUSTER}
+db: ${REMOTE_MYSQL_DB}
+queries: ${QUERY_LIST:-all tpch queries}
+tpch-tools dir: ${REMOTE_TPCH_TOOLS_DIR}
+fe dir: ${REMOTE_FE_DIR}
+lib dir: ${REMOTE_LIB_ARCHIVE_DIR}
+mysql port: ${REMOTE_MYSQL_PORT}
+http port: ${REMOTE_HTTP_PORT}
+profile dir: ${PROFILE_OUTPUT_DIR:-disabled}
+EOF
+    exit 0
+fi
+
+SSH_BASE=(ssh -p "${REMOTE_PORT}" -o StrictHostKeyChecking=accept-new 
"${REMOTE_USER}@${REMOTE_HOST}")
+SCP_BASE=(scp -P "${REMOTE_PORT}")
+
+run_remote() {
+    local script_content=$1
+    "${SSH_BASE[@]}" 'bash -s' -- <<EOF
+${script_content}
+EOF
+}
+
+fetch_remote_file() {
+    local remote_path=$1
+    local local_path=$2
+    "${SCP_BASE[@]}" "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}" 
"${local_path}"
+}
+
+fetch_remote_dir() {
+    local remote_path=$1
+    local local_path=$2
+    mkdir -p "${local_path}"
+    "${SCP_BASE[@]}" -r "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}/." 
"${local_path}"
+}
+
+read -r -d '' REMOTE_SCRIPT <<'EOF' || true
+set -euo pipefail
+
+REMOTE_TPCH_TOOLS_DIR="__REMOTE_TPCH_TOOLS_DIR__"
+REMOTE_FE_DIR="__REMOTE_FE_DIR__"
+REMOTE_LIB_ARCHIVE_DIR="__REMOTE_LIB_ARCHIVE_DIR__"
+BASELINE_ARCHIVE="__BASELINE_ARCHIVE__"
+OPT_ARCHIVE="__OPT_ARCHIVE__"
+REMOTE_CLUSTER_START_CMD="__REMOTE_CLUSTER_START_CMD__"
+REMOTE_CLUSTER_STOP_CMD="__REMOTE_CLUSTER_STOP_CMD__"
+REMOTE_MYSQL_HOST="__REMOTE_MYSQL_HOST__"
+REMOTE_MYSQL_PORT="__REMOTE_MYSQL_PORT__"
+REMOTE_MYSQL_USER="__REMOTE_MYSQL_USER__"
+REMOTE_MYSQL_DB="__REMOTE_MYSQL_DB__"
+REMOTE_JAVA_HOME="__REMOTE_JAVA_HOME__"
+WAIT_TIMEOUT_SECONDS="__WAIT_TIMEOUT_SECONDS__"
+QUERY_LIST="__QUERY_LIST__"
+REMOTE_HTTP_PORT="__REMOTE_HTTP_PORT__"
+PROFILE_OUTPUT_DIR="__PROFILE_OUTPUT_DIR__"
+RUN_ID="__RUN_ID__"
+
+REPORT_ROOT="${REMOTE_TPCH_TOOLS_DIR}/perf-reports/${RUN_ID}"
+RESULT_CSV_DIR="${REPORT_ROOT}/csv"
+QUERIES_DIR="${REMOTE_TPCH_TOOLS_DIR}/queries"
+mkdir -p "${RESULT_CSV_DIR}"
+PROFILE_STAGING_DIR="${REPORT_ROOT}/profiles"
+if [[ -n "${PROFILE_OUTPUT_DIR}" ]]; then
+    mkdir -p "${PROFILE_STAGING_DIR}"
+fi
+
+run_shell_command() {
+    local cmd=$1
+    bash -lc "export JAVA_HOME='${REMOTE_JAVA_HOME}'; export 
PATH='${REMOTE_JAVA_HOME}/bin':\"\$PATH\"; ${cmd}"
+}
+
+wait_fe() {
+    local deadline=$(( $(date +%s) + WAIT_TIMEOUT_SECONDS ))
+    while true; do
+        if mysql -h"${REMOTE_MYSQL_HOST}" -P"${REMOTE_MYSQL_PORT}" 
-u"${REMOTE_MYSQL_USER}" -e 'select 1' >/dev/null 2>&1; then
+            return 0
+        fi
+        if [[ $(date +%s) -ge ${deadline} ]]; then
+            exit 1
+        fi
+        sleep 5
+    done
+}
+
+selected_queries() {
+    if [[ -n "${QUERY_LIST}" ]]; then
+        printf '%s\n' "${QUERY_LIST}" | tr ', ' '\n\n' | sed '/^$/d'
+    else
+        seq 1 22
+    fi
+}
+
+switch_lib() {
+    local archive_path="${REMOTE_LIB_ARCHIVE_DIR}/$1"
+    run_shell_command "${REMOTE_CLUSTER_STOP_CMD}" || true
+    rm -rf "${REMOTE_FE_DIR}/lib"
+    tar --warning=no-unknown-keyword -xf "${archive_path}" -C 
"${REMOTE_FE_DIR}"
+    run_shell_command "${REMOTE_CLUSTER_START_CMD}"
+    wait_fe
+}
+
+profile_file_name() {
+    local label=$1
+    case "${label}" in
+    baseline) printf '%s\n' 'without-opt.profile' ;;
+    with_opt) printf '%s\n' 'with-opt.profile' ;;
+    *) printf '%s.profile\n' "${label}" ;;
+    esac
+}
+
+fetch_query_profile() {
+    local label=$1
+    local query=$2
+    local tag=$3
+    [[ -n "${PROFILE_OUTPUT_DIR}" ]] || return 0
+
+    local query_profile_dir="${PROFILE_STAGING_DIR}/query${query}"
+    mkdir -p "${query_profile_dir}"
+    local profile_path="${query_profile_dir}/$(profile_file_name "${label}")"
+    local profile_list_path="${query_profile_dir}/$(profile_file_name 
"${label}").list.json"
+    local profile_resp_path="${query_profile_dir}/$(profile_file_name 
"${label}").response.json"
+
+    local profile_id=""
+    local profile_list=""
+    local profile_deadline=$(( $(date +%s) + 30 ))
+    while true; do
+        if ! profile_list=$(curl --fail --silent --show-error \
+                -u "${REMOTE_MYSQL_USER}:${MYSQL_PWD:-}" \
+                
"http://${REMOTE_MYSQL_HOST}:${REMOTE_HTTP_PORT}/rest/v1/query_profile"; 2>&1); 
then
+            printf 'Failed to get query profile list for query%s (%s):\n%s\n' 
"${query}" "${label}" "${profile_list}" \
+                | tee -a "${REPORT_ROOT}/${label}.log" >&2
+            return 1
+        fi
+        printf '%s\n' "${profile_list}" >"${profile_list_path}"
+
+        if ! profile_id=$(python3 - "${tag}" "${profile_list_path}" <<'PY'
+import json
+import sys
+
+tag = sys.argv[1]
+path = sys.argv[2]
+with open(path, encoding="utf-8") as f:
+    payload = json.load(f)
+rows = payload.get("data", {}).get("rows", [])
+for row in rows:
+    if tag in str(row.get("Sql Statement", "")):
+        state = str(row.get("Profile Completion State", ""))
+        if state == "COMPLETE":
+            print(row.get("Profile ID", ""))
+            break
+PY
+        ); then
+            printf 'Failed to parse query profile list for query%s (%s)\n' 
"${query}" "${label}" \
+                | tee -a "${REPORT_ROOT}/${label}.log" >&2
+            return 1
+        fi
+        if [[ -n "${profile_id}" ]] || [[ $(date +%s) -ge ${profile_deadline} 
]]; then
+            break
+        fi
+        sleep 1
+    done
+
+    if [[ -z "${profile_id}" ]]; then
+        printf 'Missing COMPLETE query profile for query%s (%s), tag: %s\n' 
"${query}" "${label}" "${tag}" \
+            | tee -a "${REPORT_ROOT}/${label}.log" >&2
+        return 1
+    fi
+
+    local profile_resp
+    if ! profile_resp=$(curl --fail --silent --show-error \
+            -u "${REMOTE_MYSQL_USER}:${MYSQL_PWD:-}" \
+            
"http://${REMOTE_MYSQL_HOST}:${REMOTE_HTTP_PORT}/rest/v1/query_profile/text/${profile_id}";
 2>&1); then
+        printf 'Failed to get query profile %s for query%s (%s):\n%s\n' 
"${profile_id}" "${query}" "${label}" "${profile_resp}" \
+            | tee -a "${REPORT_ROOT}/${label}.log" >&2
+        return 1
+    fi
+    printf '%s\n' "${profile_resp}" >"${profile_resp_path}"
+
+    if ! python3 - "${profile_path}" "${profile_resp_path}" <<'PY'
+import json
+import sys
+
+target = sys.argv[1]
+path = sys.argv[2]
+with open(path, encoding="utf-8") as f:
+    payload = json.load(f)
+data = payload.get("data", "")
+with open(target, "w", encoding="utf-8") as f:
+    f.write(data)
+    if data and not data.endswith("\n"):
+        f.write("\n")
+PY
+    then
+        printf 'Failed to write query profile %s for query%s (%s)\n' 
"${profile_id}" "${query}" "${label}" \
+            | tee -a "${REPORT_ROOT}/${label}.log" >&2
+        return 1
+    fi
+    rm -f "${profile_list_path}" "${profile_resp_path}"
+}
+
+run_perf() {
+    local label=$1
+    local result_csv="${RESULT_CSV_DIR}/${label}.csv"
+    local log_file="${REPORT_ROOT}/${label}.log"
+    : >"${result_csv}"
+    : >"${log_file}"
+    while IFS= read -r i; do
+        [[ -n "${i}" ]] || continue
+        sql_file="${QUERIES_DIR}/q${i}.sql"
+        if [[ ! -f "${sql_file}" ]]; then
+            printf 'Missing file: %s\n' "${sql_file}" | tee -a "${log_file}"
+            continue
+        fi
+
+        printf 'Running q%s\n' "${i}" >>"${log_file}"
+        printf 'q%s\t' "${i}" | tee -a "${result_csv}"
+
+        start=$(date +%s%3N)
+        if ! output=$(mysql -h"${REMOTE_MYSQL_HOST}" -u"${REMOTE_MYSQL_USER}" 
-P"${REMOTE_MYSQL_PORT}" -D"${REMOTE_MYSQL_DB}" --comments <"${sql_file}" 
2>&1); then
+            printf 'Error: Failed to execute q%s (cold). Output:\n%s\n' "${i}" 
"${output}" | tee -a "${log_file}" >&2
+            printf '\n' | tee -a "${result_csv}"
+            continue
+        fi
+        end=$(date +%s%3N)
+        cold=$((end - start))
+        printf '%s\t' "${cold}" | tee -a "${result_csv}"
+
+        start=$(date +%s%3N)
+        if ! output=$(mysql -h"${REMOTE_MYSQL_HOST}" -u"${REMOTE_MYSQL_USER}" 
-P"${REMOTE_MYSQL_PORT}" -D"${REMOTE_MYSQL_DB}" --comments <"${sql_file}" 
2>&1); then
+            printf 'Error: Failed to execute q%s (hot1). Output:\n%s\n' "${i}" 
"${output}" | tee -a "${log_file}" >&2
+            printf '\n' | tee -a "${result_csv}"
+            continue
+        fi
+        end=$(date +%s%3N)
+        hot1=$((end - start))
+        printf '%s\t' "${hot1}" | tee -a "${result_csv}"
+
+        profile_tag="tpch_perf_compare:${RUN_ID}:${label}:query${i}:hot2"
+        start=$(date +%s%3N)
+        if ! output=$(
+            {
+                if [[ -n "${PROFILE_OUTPUT_DIR}" ]]; then

Review Comment:
   [P1] Keep profile collection out of the timed samples
   
   When `--profile` is set, this timer includes `SET enable_profile = true` and 
runs `hot2` with profiling/report-success instrumentation that `hot1` does not 
have. The script then publishes that duration and can select it as `best_hot`, 
so requesting artifacts changes the benchmark being compared and may affect the 
two plan variants differently. Keep both reported hot samples unprofiled, then 
run a separate untimed tagged execution solely for profile collection.



##########
plans/test-in-blackhouse/run_remote_tpch_perf_compare.sh:
##########
@@ -0,0 +1,483 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+LOCAL_REPORT_ROOT="${SCRIPT_DIR}/tpch-perf-reports"
+
+CLUSTER="${CLUSTER:-cluster1}"
+REMOTE_USER="${REMOTE_USER:-root}"
+REMOTE_HOST="${REMOTE_HOST:-blackhouse}"
+REMOTE_PORT="${REMOTE_PORT:-22}"
+REMOTE_TPCH_TOOLS_DIR="${REMOTE_TPCH_TOOLS_DIR:-/root/mal/tpch/tpch-tools}"
+REMOTE_FE_DIR="${REMOTE_FE_DIR:-}"
+REMOTE_LIB_ARCHIVE_DIR="${REMOTE_LIB_ARCHIVE_DIR:-}"
+BASELINE_ARCHIVE="${BASELINE_ARCHIVE:-without-opt.tar}"
+OPT_ARCHIVE="${OPT_ARCHIVE:-with-opt.tar}"
+REMOTE_CLUSTER_START_CMD="${REMOTE_CLUSTER_START_CMD:-}"
+REMOTE_CLUSTER_STOP_CMD="${REMOTE_CLUSTER_STOP_CMD:-}"
+REMOTE_MYSQL_HOST="${REMOTE_MYSQL_HOST:-127.0.0.1}"
+REMOTE_MYSQL_PORT="${REMOTE_MYSQL_PORT:-}"
+REMOTE_MYSQL_USER="${REMOTE_MYSQL_USER:-root}"
+REMOTE_HTTP_PORT="${REMOTE_HTTP_PORT:-}"
+REMOTE_MYSQL_DB="${REMOTE_MYSQL_DB:-tpch_sf1000}"
+REMOTE_JAVA_HOME="${REMOTE_JAVA_HOME:-/usr/lib/jvm/java-17-openjdk-amd64}"
+WAIT_TIMEOUT_SECONDS="${WAIT_TIMEOUT_SECONDS:-300}"
+QUERY_LIST="${QUERY_LIST:-}"
+PROFILE_OUTPUT_DIR="${PROFILE_OUTPUT_DIR:-}"
+DRY_RUN=0
+
+apply_cluster_defaults() {
+    case "${CLUSTER}" in
+    cluster1)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=9030}"
+        : "${REMOTE_HTTP_PORT:=8030}"
+        ;;
+    cluster2)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=19030}"
+        : "${REMOTE_HTTP_PORT:=18030}"
+        ;;
+    *)
+        echo "Unsupported cluster: ${CLUSTER}" >&2
+        exit 1
+        ;;
+    esac
+
+    : "${REMOTE_CLUSTER_START_CMD:=${REMOTE_FE_DIR}/bin/start_fe.sh --daemon}"
+    : "${REMOTE_CLUSTER_STOP_CMD:=${REMOTE_FE_DIR}/bin/stop_fe.sh}"
+}
+
+usage() {
+    cat <<EOF
+Usage: $0 [--cluster cluster1|cluster2] [--db tpch_sf1000] [--queries '1,3,7'] 
[--profile DIR] [--dry-run]
+
+This script compares TPCH query execution time only.
+It switches baseline/opt FE lib archives, runs selected TPCH queries with
+1 cold run and 2 hot runs, then writes a summary table.
+
+When --profile DIR is set, the final hot-run query profile for each query is 
saved under
+DIR/queryN/without-opt.profile and DIR/queryN/with-opt.profile.
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+    --cluster)
+        CLUSTER="$2"
+        shift 2
+        ;;
+    --db)
+        REMOTE_MYSQL_DB="$2"
+        shift 2
+        ;;
+    --queries)
+        QUERY_LIST="$2"
+        shift 2
+        ;;
+    --profile)
+        PROFILE_OUTPUT_DIR="$2"
+        shift 2
+        ;;
+    --dry-run)
+        DRY_RUN=1
+        shift
+        ;;
+    -h|--help)
+        usage
+        exit 0
+        ;;
+    *)
+        echo "Unknown argument: $1" >&2
+        usage >&2
+        exit 1
+        ;;
+    esac
+done
+
+apply_cluster_defaults
+
+mkdir -p "${LOCAL_REPORT_ROOT}"
+RUN_ID=$(date +%Y%m%d_%H%M%S)
+LOCAL_RUN_DIR="${LOCAL_REPORT_ROOT}/${RUN_ID}"
+mkdir -p "${LOCAL_RUN_DIR}"
+
+if [[ ${DRY_RUN} -eq 1 ]]; then
+    cat <<EOF
+Dry run only. Nothing will be executed.
+cluster: ${CLUSTER}
+db: ${REMOTE_MYSQL_DB}
+queries: ${QUERY_LIST:-all tpch queries}
+tpch-tools dir: ${REMOTE_TPCH_TOOLS_DIR}
+fe dir: ${REMOTE_FE_DIR}
+lib dir: ${REMOTE_LIB_ARCHIVE_DIR}
+mysql port: ${REMOTE_MYSQL_PORT}
+http port: ${REMOTE_HTTP_PORT}
+profile dir: ${PROFILE_OUTPUT_DIR:-disabled}
+EOF
+    exit 0
+fi
+
+SSH_BASE=(ssh -p "${REMOTE_PORT}" -o StrictHostKeyChecking=accept-new 
"${REMOTE_USER}@${REMOTE_HOST}")
+SCP_BASE=(scp -P "${REMOTE_PORT}")
+
+run_remote() {
+    local script_content=$1
+    "${SSH_BASE[@]}" 'bash -s' -- <<EOF
+${script_content}
+EOF
+}
+
+fetch_remote_file() {
+    local remote_path=$1
+    local local_path=$2
+    "${SCP_BASE[@]}" "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}" 
"${local_path}"
+}
+
+fetch_remote_dir() {
+    local remote_path=$1
+    local local_path=$2
+    mkdir -p "${local_path}"
+    "${SCP_BASE[@]}" -r "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}/." 
"${local_path}"
+}
+
+read -r -d '' REMOTE_SCRIPT <<'EOF' || true
+set -euo pipefail
+
+REMOTE_TPCH_TOOLS_DIR="__REMOTE_TPCH_TOOLS_DIR__"
+REMOTE_FE_DIR="__REMOTE_FE_DIR__"
+REMOTE_LIB_ARCHIVE_DIR="__REMOTE_LIB_ARCHIVE_DIR__"
+BASELINE_ARCHIVE="__BASELINE_ARCHIVE__"
+OPT_ARCHIVE="__OPT_ARCHIVE__"
+REMOTE_CLUSTER_START_CMD="__REMOTE_CLUSTER_START_CMD__"
+REMOTE_CLUSTER_STOP_CMD="__REMOTE_CLUSTER_STOP_CMD__"
+REMOTE_MYSQL_HOST="__REMOTE_MYSQL_HOST__"
+REMOTE_MYSQL_PORT="__REMOTE_MYSQL_PORT__"
+REMOTE_MYSQL_USER="__REMOTE_MYSQL_USER__"
+REMOTE_MYSQL_DB="__REMOTE_MYSQL_DB__"
+REMOTE_JAVA_HOME="__REMOTE_JAVA_HOME__"
+WAIT_TIMEOUT_SECONDS="__WAIT_TIMEOUT_SECONDS__"
+QUERY_LIST="__QUERY_LIST__"
+REMOTE_HTTP_PORT="__REMOTE_HTTP_PORT__"
+PROFILE_OUTPUT_DIR="__PROFILE_OUTPUT_DIR__"
+RUN_ID="__RUN_ID__"
+
+REPORT_ROOT="${REMOTE_TPCH_TOOLS_DIR}/perf-reports/${RUN_ID}"
+RESULT_CSV_DIR="${REPORT_ROOT}/csv"
+QUERIES_DIR="${REMOTE_TPCH_TOOLS_DIR}/queries"
+mkdir -p "${RESULT_CSV_DIR}"
+PROFILE_STAGING_DIR="${REPORT_ROOT}/profiles"
+if [[ -n "${PROFILE_OUTPUT_DIR}" ]]; then
+    mkdir -p "${PROFILE_STAGING_DIR}"
+fi
+
+run_shell_command() {
+    local cmd=$1
+    bash -lc "export JAVA_HOME='${REMOTE_JAVA_HOME}'; export 
PATH='${REMOTE_JAVA_HOME}/bin':\"\$PATH\"; ${cmd}"
+}
+
+wait_fe() {
+    local deadline=$(( $(date +%s) + WAIT_TIMEOUT_SECONDS ))
+    while true; do
+        if mysql -h"${REMOTE_MYSQL_HOST}" -P"${REMOTE_MYSQL_PORT}" 
-u"${REMOTE_MYSQL_USER}" -e 'select 1' >/dev/null 2>&1; then
+            return 0
+        fi
+        if [[ $(date +%s) -ge ${deadline} ]]; then
+            exit 1
+        fi
+        sleep 5
+    done
+}
+
+selected_queries() {
+    if [[ -n "${QUERY_LIST}" ]]; then
+        printf '%s\n' "${QUERY_LIST}" | tr ', ' '\n\n' | sed '/^$/d'
+    else
+        seq 1 22
+    fi
+}
+
+switch_lib() {
+    local archive_path="${REMOTE_LIB_ARCHIVE_DIR}/$1"
+    run_shell_command "${REMOTE_CLUSTER_STOP_CMD}" || true
+    rm -rf "${REMOTE_FE_DIR}/lib"
+    tar --warning=no-unknown-keyword -xf "${archive_path}" -C 
"${REMOTE_FE_DIR}"
+    run_shell_command "${REMOTE_CLUSTER_START_CMD}"
+    wait_fe
+}
+
+profile_file_name() {
+    local label=$1
+    case "${label}" in
+    baseline) printf '%s\n' 'without-opt.profile' ;;
+    with_opt) printf '%s\n' 'with-opt.profile' ;;
+    *) printf '%s.profile\n' "${label}" ;;
+    esac
+}
+
+fetch_query_profile() {
+    local label=$1
+    local query=$2
+    local tag=$3
+    [[ -n "${PROFILE_OUTPUT_DIR}" ]] || return 0
+
+    local query_profile_dir="${PROFILE_STAGING_DIR}/query${query}"
+    mkdir -p "${query_profile_dir}"
+    local profile_path="${query_profile_dir}/$(profile_file_name "${label}")"
+    local profile_list_path="${query_profile_dir}/$(profile_file_name 
"${label}").list.json"
+    local profile_resp_path="${query_profile_dir}/$(profile_file_name 
"${label}").response.json"
+
+    local profile_id=""
+    local profile_list=""
+    local profile_deadline=$(( $(date +%s) + 30 ))
+    while true; do
+        if ! profile_list=$(curl --fail --silent --show-error \
+                -u "${REMOTE_MYSQL_USER}:${MYSQL_PWD:-}" \
+                
"http://${REMOTE_MYSQL_HOST}:${REMOTE_HTTP_PORT}/rest/v1/query_profile"; 2>&1); 
then
+            printf 'Failed to get query profile list for query%s (%s):\n%s\n' 
"${query}" "${label}" "${profile_list}" \
+                | tee -a "${REPORT_ROOT}/${label}.log" >&2
+            return 1
+        fi
+        printf '%s\n' "${profile_list}" >"${profile_list_path}"
+
+        if ! profile_id=$(python3 - "${tag}" "${profile_list_path}" <<'PY'
+import json
+import sys
+
+tag = sys.argv[1]
+path = sys.argv[2]
+with open(path, encoding="utf-8") as f:
+    payload = json.load(f)
+rows = payload.get("data", {}).get("rows", [])
+for row in rows:
+    if tag in str(row.get("Sql Statement", "")):
+        state = str(row.get("Profile Completion State", ""))
+        if state == "COMPLETE":
+            print(row.get("Profile ID", ""))
+            break
+PY
+        ); then
+            printf 'Failed to parse query profile list for query%s (%s)\n' 
"${query}" "${label}" \
+                | tee -a "${REPORT_ROOT}/${label}.log" >&2
+            return 1
+        fi
+        if [[ -n "${profile_id}" ]] || [[ $(date +%s) -ge ${profile_deadline} 
]]; then
+            break
+        fi
+        sleep 1
+    done
+
+    if [[ -z "${profile_id}" ]]; then
+        printf 'Missing COMPLETE query profile for query%s (%s), tag: %s\n' 
"${query}" "${label}" "${tag}" \
+            | tee -a "${REPORT_ROOT}/${label}.log" >&2
+        return 1
+    fi
+
+    local profile_resp
+    if ! profile_resp=$(curl --fail --silent --show-error \
+            -u "${REMOTE_MYSQL_USER}:${MYSQL_PWD:-}" \
+            
"http://${REMOTE_MYSQL_HOST}:${REMOTE_HTTP_PORT}/rest/v1/query_profile/text/${profile_id}";
 2>&1); then
+        printf 'Failed to get query profile %s for query%s (%s):\n%s\n' 
"${profile_id}" "${query}" "${label}" "${profile_resp}" \
+            | tee -a "${REPORT_ROOT}/${label}.log" >&2
+        return 1
+    fi
+    printf '%s\n' "${profile_resp}" >"${profile_resp_path}"
+
+    if ! python3 - "${profile_path}" "${profile_resp_path}" <<'PY'
+import json
+import sys
+
+target = sys.argv[1]
+path = sys.argv[2]
+with open(path, encoding="utf-8") as f:
+    payload = json.load(f)
+data = payload.get("data", "")
+with open(target, "w", encoding="utf-8") as f:
+    f.write(data)
+    if data and not data.endswith("\n"):
+        f.write("\n")
+PY
+    then
+        printf 'Failed to write query profile %s for query%s (%s)\n' 
"${profile_id}" "${query}" "${label}" \
+            | tee -a "${REPORT_ROOT}/${label}.log" >&2
+        return 1
+    fi
+    rm -f "${profile_list_path}" "${profile_resp_path}"
+}
+
+run_perf() {
+    local label=$1
+    local result_csv="${RESULT_CSV_DIR}/${label}.csv"
+    local log_file="${REPORT_ROOT}/${label}.log"
+    : >"${result_csv}"
+    : >"${log_file}"
+    while IFS= read -r i; do
+        [[ -n "${i}" ]] || continue
+        sql_file="${QUERIES_DIR}/q${i}.sql"
+        if [[ ! -f "${sql_file}" ]]; then
+            printf 'Missing file: %s\n' "${sql_file}" | tee -a "${log_file}"
+            continue
+        fi
+
+        printf 'Running q%s\n' "${i}" >>"${log_file}"
+        printf 'q%s\t' "${i}" | tee -a "${result_csv}"
+
+        start=$(date +%s%3N)
+        if ! output=$(mysql -h"${REMOTE_MYSQL_HOST}" -u"${REMOTE_MYSQL_USER}" 
-P"${REMOTE_MYSQL_PORT}" -D"${REMOTE_MYSQL_DB}" --comments <"${sql_file}" 
2>&1); then
+            printf 'Error: Failed to execute q%s (cold). Output:\n%s\n' "${i}" 
"${output}" | tee -a "${log_file}" >&2
+            printf '\n' | tee -a "${result_csv}"
+            continue
+        fi
+        end=$(date +%s%3N)
+        cold=$((end - start))
+        printf '%s\t' "${cold}" | tee -a "${result_csv}"
+
+        start=$(date +%s%3N)
+        if ! output=$(mysql -h"${REMOTE_MYSQL_HOST}" -u"${REMOTE_MYSQL_USER}" 
-P"${REMOTE_MYSQL_PORT}" -D"${REMOTE_MYSQL_DB}" --comments <"${sql_file}" 
2>&1); then
+            printf 'Error: Failed to execute q%s (hot1). Output:\n%s\n' "${i}" 
"${output}" | tee -a "${log_file}" >&2
+            printf '\n' | tee -a "${result_csv}"
+            continue
+        fi
+        end=$(date +%s%3N)
+        hot1=$((end - start))
+        printf '%s\t' "${hot1}" | tee -a "${result_csv}"
+
+        profile_tag="tpch_perf_compare:${RUN_ID}:${label}:query${i}:hot2"
+        start=$(date +%s%3N)
+        if ! output=$(
+            {
+                if [[ -n "${PROFILE_OUTPUT_DIR}" ]]; then
+                    printf 'SET enable_profile = true;\n'
+                    printf '/* %s */\n' "${profile_tag}"
+                fi
+                cat "${sql_file}"
+            } | mysql -h"${REMOTE_MYSQL_HOST}" -u"${REMOTE_MYSQL_USER}" 
-P"${REMOTE_MYSQL_PORT}" -D"${REMOTE_MYSQL_DB}" --comments 2>&1
+        ); then
+            printf 'Error: Failed to execute q%s (hot2). Output:\n%s\n' "${i}" 
"${output}" | tee -a "${log_file}" >&2
+            printf '\n' | tee -a "${result_csv}"
+            continue
+        fi
+        end=$(date +%s%3N)
+        hot2=$((end - start))
+        best_hot=${hot1}
+        if [[ ${hot2} -lt ${best_hot} ]]; then
+            best_hot=${hot2}
+        fi
+        printf '%s\t%s\n' "${hot2}" "${best_hot}" | tee -a "${result_csv}"
+        fetch_query_profile "${label}" "${i}" "${profile_tag}"
+    done < <(selected_queries)
+}
+
+switch_lib "${BASELINE_ARCHIVE}"
+run_perf baseline
+
+switch_lib "${OPT_ARCHIVE}"
+run_perf with_opt
+
+python3 - <<'PY' "${RESULT_CSV_DIR}/baseline.csv" 
"${RESULT_CSV_DIR}/with_opt.csv" "${REPORT_ROOT}/summary.md"
+import sys
+from pathlib import Path
+
+baseline_path = Path(sys.argv[1])
+opt_path = Path(sys.argv[2])
+summary_path = Path(sys.argv[3])
+
+def load(path):
+    data = {}
+    if not path.exists():
+        return data
+    for raw in path.read_text().splitlines():
+        if not raw.strip():
+            continue
+        parts = raw.split('\t')
+        if len(parts) < 5:
+            continue
+        data[parts[0]] = {
+            'cold': parts[1],
+            'hot1': parts[2],
+            'hot2': parts[3],
+            'best_hot': parts[4],
+        }
+    return data
+
+base = load(baseline_path)
+opt = load(opt_path)
+queries = sorted(set(base) | set(opt), key=lambda x: int(x.replace('q', '')))
+
+lines = []
+lines.append('# TPCH Perf Compare Report\n\n')
+lines.append('## Selected Queries\n\n')
+for q in queries:
+    lines.append(f'- `{q}`\n')
+lines.append('\n')
+lines.append('## Summary Table\n\n')
+lines.append('| Query | Baseline Cold(ms) | Baseline Hot1(ms) | Baseline 
Hot2(ms) | Baseline Best Hot(ms) | Opt Cold(ms) | Opt Hot1(ms) | Opt Hot2(ms) | 
Opt Best Hot(ms) |\n')
+lines.append('| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: 
|\n')
+for q in queries:
+    b = base.get(q, {})
+    o = opt.get(q, {})
+    lines.append(
+        f"| {q} | {b.get('cold', '')} | {b.get('hot1', '')} | {b.get('hot2', 
'')} | {b.get('best_hot', '')} | {o.get('cold', '')} | {o.get('hot1', '')} | 
{o.get('hot2', '')} | {o.get('best_hot', '')} |\n"
+    )
+summary_path.write_text(''.join(lines))
+PY
+
+printf '%s\n' "${REPORT_ROOT}"
+EOF
+
+escape_for_remote() {
+    printf '%s' "$1" | python3 -c 'import sys; 
print(sys.stdin.read().replace("\\", "\\\\").replace("\"", "\\\""))'

Review Comment:
   [P1] Pass remote configuration as data instead of reparsed Bash source
   
   This helper only escapes backslashes and double quotes, but every 
replacement is inserted into a double-quoted assignment that the remote `bash 
-s` parses again. For example, `--db 'db$stage'` expands `$stage` remotely (and 
can abort under `set -u`), while `$()` or backticks execute during assignment 
parsing. Use a static remote script with arguments/environment data, or encode 
assignments with `printf %q`, so data values are preserved literally; command 
strings can then be executed only at the explicit `run_shell_command` boundary.



##########
plans/test-in-blackhouse/run_remote_tpch_perf_compare.sh:
##########
@@ -0,0 +1,483 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+LOCAL_REPORT_ROOT="${SCRIPT_DIR}/tpch-perf-reports"
+
+CLUSTER="${CLUSTER:-cluster1}"
+REMOTE_USER="${REMOTE_USER:-root}"
+REMOTE_HOST="${REMOTE_HOST:-blackhouse}"
+REMOTE_PORT="${REMOTE_PORT:-22}"
+REMOTE_TPCH_TOOLS_DIR="${REMOTE_TPCH_TOOLS_DIR:-/root/mal/tpch/tpch-tools}"
+REMOTE_FE_DIR="${REMOTE_FE_DIR:-}"
+REMOTE_LIB_ARCHIVE_DIR="${REMOTE_LIB_ARCHIVE_DIR:-}"
+BASELINE_ARCHIVE="${BASELINE_ARCHIVE:-without-opt.tar}"
+OPT_ARCHIVE="${OPT_ARCHIVE:-with-opt.tar}"
+REMOTE_CLUSTER_START_CMD="${REMOTE_CLUSTER_START_CMD:-}"
+REMOTE_CLUSTER_STOP_CMD="${REMOTE_CLUSTER_STOP_CMD:-}"
+REMOTE_MYSQL_HOST="${REMOTE_MYSQL_HOST:-127.0.0.1}"
+REMOTE_MYSQL_PORT="${REMOTE_MYSQL_PORT:-}"
+REMOTE_MYSQL_USER="${REMOTE_MYSQL_USER:-root}"
+REMOTE_HTTP_PORT="${REMOTE_HTTP_PORT:-}"
+REMOTE_MYSQL_DB="${REMOTE_MYSQL_DB:-tpch_sf1000}"
+REMOTE_JAVA_HOME="${REMOTE_JAVA_HOME:-/usr/lib/jvm/java-17-openjdk-amd64}"
+WAIT_TIMEOUT_SECONDS="${WAIT_TIMEOUT_SECONDS:-300}"
+QUERY_LIST="${QUERY_LIST:-}"
+PROFILE_OUTPUT_DIR="${PROFILE_OUTPUT_DIR:-}"
+DRY_RUN=0
+
+apply_cluster_defaults() {
+    case "${CLUSTER}" in
+    cluster1)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=9030}"
+        : "${REMOTE_HTTP_PORT:=8030}"
+        ;;
+    cluster2)
+        : "${REMOTE_FE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe}"
+        : "${REMOTE_LIB_ARCHIVE_DIR:=/mnt/hdd01/6PERFORMANCE_ENV/fe/mal}"
+        : "${REMOTE_MYSQL_PORT:=19030}"
+        : "${REMOTE_HTTP_PORT:=18030}"
+        ;;
+    *)
+        echo "Unsupported cluster: ${CLUSTER}" >&2
+        exit 1
+        ;;
+    esac
+
+    : "${REMOTE_CLUSTER_START_CMD:=${REMOTE_FE_DIR}/bin/start_fe.sh --daemon}"
+    : "${REMOTE_CLUSTER_STOP_CMD:=${REMOTE_FE_DIR}/bin/stop_fe.sh}"
+}
+
+usage() {
+    cat <<EOF
+Usage: $0 [--cluster cluster1|cluster2] [--db tpch_sf1000] [--queries '1,3,7'] 
[--profile DIR] [--dry-run]
+
+This script compares TPCH query execution time only.
+It switches baseline/opt FE lib archives, runs selected TPCH queries with
+1 cold run and 2 hot runs, then writes a summary table.
+
+When --profile DIR is set, the final hot-run query profile for each query is 
saved under
+DIR/queryN/without-opt.profile and DIR/queryN/with-opt.profile.
+EOF
+}
+
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+    --cluster)
+        CLUSTER="$2"
+        shift 2
+        ;;
+    --db)
+        REMOTE_MYSQL_DB="$2"
+        shift 2
+        ;;
+    --queries)
+        QUERY_LIST="$2"
+        shift 2
+        ;;
+    --profile)
+        PROFILE_OUTPUT_DIR="$2"
+        shift 2
+        ;;
+    --dry-run)
+        DRY_RUN=1
+        shift
+        ;;
+    -h|--help)
+        usage
+        exit 0
+        ;;
+    *)
+        echo "Unknown argument: $1" >&2
+        usage >&2
+        exit 1
+        ;;
+    esac
+done
+
+apply_cluster_defaults
+
+mkdir -p "${LOCAL_REPORT_ROOT}"
+RUN_ID=$(date +%Y%m%d_%H%M%S)
+LOCAL_RUN_DIR="${LOCAL_REPORT_ROOT}/${RUN_ID}"
+mkdir -p "${LOCAL_RUN_DIR}"
+
+if [[ ${DRY_RUN} -eq 1 ]]; then
+    cat <<EOF
+Dry run only. Nothing will be executed.
+cluster: ${CLUSTER}
+db: ${REMOTE_MYSQL_DB}
+queries: ${QUERY_LIST:-all tpch queries}
+tpch-tools dir: ${REMOTE_TPCH_TOOLS_DIR}
+fe dir: ${REMOTE_FE_DIR}
+lib dir: ${REMOTE_LIB_ARCHIVE_DIR}
+mysql port: ${REMOTE_MYSQL_PORT}
+http port: ${REMOTE_HTTP_PORT}
+profile dir: ${PROFILE_OUTPUT_DIR:-disabled}
+EOF
+    exit 0
+fi
+
+SSH_BASE=(ssh -p "${REMOTE_PORT}" -o StrictHostKeyChecking=accept-new 
"${REMOTE_USER}@${REMOTE_HOST}")
+SCP_BASE=(scp -P "${REMOTE_PORT}")
+
+run_remote() {
+    local script_content=$1
+    "${SSH_BASE[@]}" 'bash -s' -- <<EOF
+${script_content}
+EOF
+}
+
+fetch_remote_file() {
+    local remote_path=$1
+    local local_path=$2
+    "${SCP_BASE[@]}" "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}" 
"${local_path}"
+}
+
+fetch_remote_dir() {
+    local remote_path=$1
+    local local_path=$2
+    mkdir -p "${local_path}"
+    "${SCP_BASE[@]}" -r "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}/." 
"${local_path}"
+}
+
+read -r -d '' REMOTE_SCRIPT <<'EOF' || true
+set -euo pipefail
+
+REMOTE_TPCH_TOOLS_DIR="__REMOTE_TPCH_TOOLS_DIR__"
+REMOTE_FE_DIR="__REMOTE_FE_DIR__"
+REMOTE_LIB_ARCHIVE_DIR="__REMOTE_LIB_ARCHIVE_DIR__"
+BASELINE_ARCHIVE="__BASELINE_ARCHIVE__"
+OPT_ARCHIVE="__OPT_ARCHIVE__"
+REMOTE_CLUSTER_START_CMD="__REMOTE_CLUSTER_START_CMD__"
+REMOTE_CLUSTER_STOP_CMD="__REMOTE_CLUSTER_STOP_CMD__"
+REMOTE_MYSQL_HOST="__REMOTE_MYSQL_HOST__"
+REMOTE_MYSQL_PORT="__REMOTE_MYSQL_PORT__"
+REMOTE_MYSQL_USER="__REMOTE_MYSQL_USER__"
+REMOTE_MYSQL_DB="__REMOTE_MYSQL_DB__"
+REMOTE_JAVA_HOME="__REMOTE_JAVA_HOME__"
+WAIT_TIMEOUT_SECONDS="__WAIT_TIMEOUT_SECONDS__"
+QUERY_LIST="__QUERY_LIST__"
+REMOTE_HTTP_PORT="__REMOTE_HTTP_PORT__"
+PROFILE_OUTPUT_DIR="__PROFILE_OUTPUT_DIR__"
+RUN_ID="__RUN_ID__"
+
+REPORT_ROOT="${REMOTE_TPCH_TOOLS_DIR}/perf-reports/${RUN_ID}"
+RESULT_CSV_DIR="${REPORT_ROOT}/csv"
+QUERIES_DIR="${REMOTE_TPCH_TOOLS_DIR}/queries"
+mkdir -p "${RESULT_CSV_DIR}"
+PROFILE_STAGING_DIR="${REPORT_ROOT}/profiles"
+if [[ -n "${PROFILE_OUTPUT_DIR}" ]]; then
+    mkdir -p "${PROFILE_STAGING_DIR}"
+fi
+
+run_shell_command() {
+    local cmd=$1
+    bash -lc "export JAVA_HOME='${REMOTE_JAVA_HOME}'; export 
PATH='${REMOTE_JAVA_HOME}/bin':\"\$PATH\"; ${cmd}"
+}
+
+wait_fe() {
+    local deadline=$(( $(date +%s) + WAIT_TIMEOUT_SECONDS ))
+    while true; do
+        if mysql -h"${REMOTE_MYSQL_HOST}" -P"${REMOTE_MYSQL_PORT}" 
-u"${REMOTE_MYSQL_USER}" -e 'select 1' >/dev/null 2>&1; then
+            return 0
+        fi
+        if [[ $(date +%s) -ge ${deadline} ]]; then
+            exit 1
+        fi
+        sleep 5
+    done
+}
+
+selected_queries() {
+    if [[ -n "${QUERY_LIST}" ]]; then
+        printf '%s\n' "${QUERY_LIST}" | tr ', ' '\n\n' | sed '/^$/d'
+    else
+        seq 1 22
+    fi
+}
+
+switch_lib() {
+    local archive_path="${REMOTE_LIB_ARCHIVE_DIR}/$1"
+    run_shell_command "${REMOTE_CLUSTER_STOP_CMD}" || true
+    rm -rf "${REMOTE_FE_DIR}/lib"
+    tar --warning=no-unknown-keyword -xf "${archive_path}" -C 
"${REMOTE_FE_DIR}"
+    run_shell_command "${REMOTE_CLUSTER_START_CMD}"
+    wait_fe
+}
+
+profile_file_name() {
+    local label=$1
+    case "${label}" in
+    baseline) printf '%s\n' 'without-opt.profile' ;;
+    with_opt) printf '%s\n' 'with-opt.profile' ;;
+    *) printf '%s.profile\n' "${label}" ;;
+    esac
+}
+
+fetch_query_profile() {
+    local label=$1
+    local query=$2
+    local tag=$3
+    [[ -n "${PROFILE_OUTPUT_DIR}" ]] || return 0
+
+    local query_profile_dir="${PROFILE_STAGING_DIR}/query${query}"
+    mkdir -p "${query_profile_dir}"
+    local profile_path="${query_profile_dir}/$(profile_file_name "${label}")"
+    local profile_list_path="${query_profile_dir}/$(profile_file_name 
"${label}").list.json"
+    local profile_resp_path="${query_profile_dir}/$(profile_file_name 
"${label}").response.json"
+
+    local profile_id=""
+    local profile_list=""
+    local profile_deadline=$(( $(date +%s) + 30 ))
+    while true; do
+        if ! profile_list=$(curl --fail --silent --show-error \

Review Comment:
   [P1] Use a scanner-approved credential mechanism for both profile requests
   
   The exact-head Gitleaks job rejects this `curl -u` form at this line and at 
the profile-text request below, so the PR remains blocked even though the 
password comes from an environment variable. Please use the repository-approved 
curl config/netrc pattern (or otherwise satisfy the rule) at both sites and 
rerun the check.



-- 
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