Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 8 additions & 158 deletions .github/workflows/test-runner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,165 +165,15 @@ jobs:
- name: Parse Results and Generate Table
env:
BRANCH_NAME: ${{ github.ref_name }}
COMMIT_SHA: ${{ needs.pre-flight.outputs.checkout-sha }}
run: |
python3 <<'EOF'
import os
import re

branch_name = os.environ.get('BRANCH_NAME', 'Current')

def clean_test_name(name):
# GTest names follow [Instantiation/]TestSuite.TestCase[/Param]
# Extract everything after '.' to generically remove TestSuite/Instantiation prefix
if '.' in name:
name = name.split('.', 1)[1]
# Strip legacy parameter suffix if comparing against older baselines
name = re.sub(r'/(?:With|Without)HTAPI$', '', name)
return name

def parse_gtest_output(filepath):
# The suite is run several times across iterations, so each test
# appears once per repetition. Take the median of its timings.
samples = {}
if not os.path.exists(filepath):
return {}

ok_pattern = re.compile(r'\[\s+OK\s+\]\s+(\S+)\s+\(([^)]+)\)')
metric_pattern = re.compile(r'\[\s+METRIC\s+\]\s+(\S+)\s+\(([^)]+)\):\s*([0-9.]+\s*ms)')
try:
with open(filepath, 'r') as f:
for line in f:
m_match = metric_pattern.search(line)
if m_match:
test_name = m_match.group(1)
metric_name = m_match.group(2)
entry_name = f'{test_name} - {metric_name}'
ms = parse_time_to_ms(m_match.group(3))
if ms is not None:
samples.setdefault(entry_name, []).append(ms)
continue

ok_match = ok_pattern.search(line)
if ok_match:
raw_name = ok_match.group(1)
test_name = clean_test_name(raw_name)
if 'DataFetchPerformanceParamTest' in raw_name or test_name.startswith('Benchmark/'):
base_name = test_name.split('Benchmark/', 1)[-1]
if not re.search(r'_(?:10k|100k|1M)$', base_name):
base_name = f'{base_name}_1M'
entry_name = f'{base_name} - Total execution time'
else:
entry_name = test_name
ms = parse_time_to_ms(ok_match.group(2))
if ms is not None:
samples.setdefault(entry_name, []).append(ms)
except Exception as e:
print(f'Error reading {filepath}: {e}')

results = {}
for entry_name, values in samples.items():
values.sort()
n = len(values)
median = (values[n // 2] if n % 2 == 1
else (values[n // 2 - 1] + values[n // 2]) / 2.0)
results[entry_name] = f'{median}ms'
if n > 1:
print(f'{entry_name}: median={median:.0f}ms of {n} runs '
f'(min={values[0]:.0f}ms max={values[-1]:.0f}ms)')
return results

def parse_time_to_ms(time_str):
if not time_str or time_str == 'N/A':
return None
time_str = time_str.strip()
match = re.match(r'^([\d.]+)\s*(\w+)$', time_str)
if not match:
return None
val = float(match.group(1))
unit = match.group(2).lower()
if unit == 'ms':
return val
elif unit == 's':
return val * 1000
elif unit == 'us':
return val / 1000
elif unit == 'ns':
return val / 1000000
return val

def get_percentage_str(val_ms, ref_ms):
if val_ms is None or ref_ms is None or ref_ms == 0:
return ' (N/A)'
pct = round(((val_ms - ref_ms) / ref_ms) * 100)
if pct > 0:
return f' (+{pct}%)'
elif pct < 0:
return f' ({pct}%)'
else:
return ' (0%)'

def sort_key(test_name):
match = re.match(r'^(.+?)_(10k|100k|1M) - (.+)$', test_name)
if match:
table_name, limit_label, metric_name = match.groups()
limit_order = {'10k': 1, '100k': 2, '1M': 3}.get(limit_label, 4)
metric_order = {
'Time to first byte': 1,
'Iteration time': 2,
'Total execution time': 3,
}.get(metric_name, 4)
return (0, table_name, limit_order, metric_order)
return (1, test_name, 0, 0)

existing_data = parse_gtest_output('./benchmark_results/current_core.txt')
current_bq_data = parse_gtest_output('./benchmark_results/current_bq.txt')
main_bq_data = parse_gtest_output('./benchmark_results/main_bq.txt')

all_tests = set(existing_data.keys()).union(set(current_bq_data.keys())).union(set(main_bq_data.keys()))
sorted_tests = sorted(list(all_tests), key=sort_key)

rows = []
for test in sorted_tests:
existing_raw = existing_data.get(test, 'N/A')
cur_bq_raw = current_bq_data.get(test, 'N/A')
main_bq_raw = main_bq_data.get(test, 'N/A')

existing_ms = parse_time_to_ms(existing_raw)
cur_bq_ms = parse_time_to_ms(cur_bq_raw)
main_bq_ms = parse_time_to_ms(main_bq_raw)

cur_bq_pct = get_percentage_str(cur_bq_ms, existing_ms) if cur_bq_raw != 'N/A' else ''
main_bq_pct = get_percentage_str(main_bq_ms, cur_bq_ms) if (main_bq_raw != 'N/A' and cur_bq_raw != 'N/A') else ' (N/A)' if main_bq_raw == 'N/A' else ''

cur_bq_val = f'{cur_bq_raw}{cur_bq_pct}'
main_bq_val = f'{main_bq_raw}{main_bq_pct}'

rows.append((test, existing_raw, cur_bq_val, main_bq_val))

# Define headers
h1 = 'Test Case'
h2 = 'Existing Driver (Current)'
h3 = f'Google Driver ({branch_name})'
h4 = 'Google Driver (Main)'

# Calculate dynamic widths for formatting
w1 = max([len(h1)] + [len(r[0]) for r in rows]) if rows else len(h1)
w2 = max([len(h2)] + [len(r[1]) for r in rows]) if rows else len(h2)
w3 = max([len(h3)] + [len(r[2]) for r in rows]) if rows else len(h3)
w4 = max([len(h4)] + [len(r[3]) for r in rows]) if rows else len(h4)

# Construct the Markdown table
table = f"*Percentages in **{h3}** show change relative to **{h2}**. Percentages in **{h4}** show change relative to **{h3}**. Negative values indicate improvement (faster test execution), positive values indicate degradation (slower).*\n\n"
table += f'| {h1.ljust(w1)} | {h2.ljust(w2)} | {h3.ljust(w3)} | {h4.ljust(w4)} |\n'
table += '|-' + ('-' * w1) + '-|-' + ('-' * w2) + '-|-' + ('-' * w3) + '-|-' + ('-' * w4) + '-|\n'

for r in rows:
table += f'| {r[0].ljust(w1)} | {r[1].ljust(w2)} | {r[2].ljust(w3)} | {r[3].ljust(w4)} |\n'

# Write table to file
with open('benchmark_summary_table.txt', 'w') as f:
f.write(table)
EOF
python3 ci/cloudbuild/builds/lib/benchmark_results.py \
--existing ./benchmark_results/current_core.txt \
--current ./benchmark_results/current_bq.txt \
--main ./benchmark_results/main_bq.txt \
--branch-name "${BRANCH_NAME}" \
--commit-sha "${COMMIT_SHA}" \
--output benchmark_summary_table.txt

- name: Upload Table to GCS
env:
Expand Down
44 changes: 44 additions & 0 deletions ci/cloudbuild/builds/integration-production-bq-driver-dm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ io::run cmake -B "$BUILD_DIR" \
"${cmake_args[@]}" \
-DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" \
-DCMAKE_CXX_STANDARD=17 \
-DCMAKE_BUILD_TYPE=Release \
-DODBC_INTEGRATION_TESTING=ON \
-DBQ_DRIVER_INTEGRATION_TESTS=ON \
-DODBC_DEMO_TESTING=ON \
Expand All @@ -81,6 +82,49 @@ io::run cmake -B "$BUILD_DIR" \
-DCLIENT_LIBRARY_INTEGRATION_TESTING=OFF
io::run cmake --build cmake-out

# ---------------------------------------------------------------------------
# Publish ODBC driver .so for performance benchmarks
# ---------------------------------------------------------------------------

if [[ "${UNIXODBC_INSTALLED}" == "false" ]]; then
DRIVER_SO="cmake-out/google/cloud/odbc/libgoogle_cloud_odbc_bq_driver.so"

if [[ ! -f "$DRIVER_SO" ]]; then
echo "ERROR: ODBC driver .so was not found:"
echo " $DRIVER_SO"
exit 1
fi

echo "ODBC driver .so found:"
ls -lh "$DRIVER_SO"

# Sanitize branch name for use in GCS path.
SANITIZED_BRANCH=$(
echo "${BRANCH_NAME}" |
sed -E 's/[^a-zA-Z0-9-]/_/g'
)

INTEGRATION_BUILDS_BUCKET="gs://odbc-integration-builds"

echo "Uploading driver artifact..."
echo "Branch: ${SANITIZED_BRANCH}"

gcloud storage cp \
"$DRIVER_SO" \
"${INTEGRATION_BUILDS_BUCKET}/${SANITIZED_BRANCH}/libgoogle_cloud_odbc_bq_driver.so"

echo "Driver benchmark artifact uploaded:"
echo "${INTEGRATION_BUILDS_BUCKET}/${SANITIZED_BRANCH}/libgoogle_cloud_odbc_bq_driver.so"

if [[ "${BRANCH_NAME}" == "main" ]]; then
echo "Triggering Linux benchmark pipeline for main..."
gcloud beta builds triggers run linux-bq-driver-benchmark-ci \
--branch=main \
--project="${PROJECT_ID:-bigquery-devtools-drivers}" \
--region=us-east1
fi
fi

# Copy the roots.pem file to the .so directory to run test cases.
cp /opt/odbc-driver/roots.pem "cmake-out/google/cloud/odbc/roots.pem"
mapfile -t ctest_args < <(ctest::common_args)
Expand Down
Loading
Loading