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]