Add misc scripts to repoitory
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/python3
|
||||
import argparse
|
||||
import configparser
|
||||
import os
|
||||
import sys
|
||||
import urllib3
|
||||
import redfish
|
||||
|
||||
# Suppress SSL warnings for self-signed iLO certificates
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
parser = argparse.ArgumentParser(description="Power off an HPE server via iLO Redfish")
|
||||
parser.add_argument("hostname", help="iLO hostname or IP address")
|
||||
parser.add_argument("-c", "--credentials", default="/etc/ilo_credentials",
|
||||
help="Path to credentials file (default: /etc/ilo_credentials)")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Read credentials from file (chmod 600 recommended: sudo chmod 600 /etc/ilo_credentials)
|
||||
# File format:
|
||||
# [ilo]
|
||||
# username = root
|
||||
# password = yourpassword
|
||||
cred_path = args.credentials
|
||||
if not os.path.exists(cred_path):
|
||||
print(f"Error: credentials file not found: {cred_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(cred_path)
|
||||
try:
|
||||
LOGIN_ACCOUNT = config.get("ilo", "username", fallback="root")
|
||||
LOGIN_PASSWORD = config.get("ilo", "password")
|
||||
except (configparser.NoSectionError, configparser.NoOptionError) as e:
|
||||
print(f"Error reading credentials file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
BASE_URL = f"https://{args.hostname}"
|
||||
|
||||
REDFISHOBJ = redfish.RedfishClient(
|
||||
base_url=BASE_URL,
|
||||
username=LOGIN_ACCOUNT,
|
||||
password=LOGIN_PASSWORD,
|
||||
)
|
||||
REDFISHOBJ.login(auth="session")
|
||||
|
||||
# Discover the correct reset action URI from the system resource
|
||||
sys_response = REDFISHOBJ.get("/redfish/v1/Systems/1/")
|
||||
reset_uri = sys_response.dict["Actions"]["#ComputerSystem.Reset"]["target"]
|
||||
|
||||
body = {"ResetType": "PushPowerButton"} # iLO 4: PushPowerButton = graceful shutdown; use "ForceOff" for hard power cut
|
||||
response = REDFISHOBJ.post(reset_uri, body=body)
|
||||
|
||||
print(f"Status: {response.status}")
|
||||
if response.status >= 400:
|
||||
print(f"Error: {response.read}")
|
||||
|
||||
REDFISHOBJ.logout()
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/python3
|
||||
import argparse
|
||||
import configparser
|
||||
import os
|
||||
import sys
|
||||
import urllib3
|
||||
import redfish
|
||||
|
||||
# Suppress SSL warnings for self-signed iLO certificates
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
parser = argparse.ArgumentParser(description="Power off an HPE server via iLO Redfish")
|
||||
parser.add_argument("hostname", help="iLO hostname or IP address")
|
||||
parser.add_argument("-c", "--credentials", default="/etc/ilo_credentials",
|
||||
help="Path to credentials file (default: /etc/ilo_credentials)")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Read credentials from file (chmod 600 recommended: sudo chmod 600 /etc/ilo_credentials)
|
||||
# File format:
|
||||
# [ilo]
|
||||
# username = root
|
||||
# password = yourpassword
|
||||
cred_path = args.credentials
|
||||
if not os.path.exists(cred_path):
|
||||
print(f"Error: credentials file not found: {cred_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(cred_path)
|
||||
try:
|
||||
LOGIN_ACCOUNT = config.get("ilo", "username", fallback="root")
|
||||
LOGIN_PASSWORD = config.get("ilo", "password")
|
||||
except (configparser.NoSectionError, configparser.NoOptionError) as e:
|
||||
print(f"Error reading credentials file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
BASE_URL = f"https://{args.hostname}"
|
||||
|
||||
REDFISHOBJ = redfish.RedfishClient(
|
||||
base_url=BASE_URL,
|
||||
username=LOGIN_ACCOUNT,
|
||||
password=LOGIN_PASSWORD,
|
||||
)
|
||||
REDFISHOBJ.login(auth="session")
|
||||
|
||||
# Discover the correct reset action URI from the system resource
|
||||
sys_response = REDFISHOBJ.get("/redfish/v1/Systems/1/")
|
||||
reset_uri = sys_response.dict["Actions"]["#ComputerSystem.Reset"]["target"]
|
||||
|
||||
body = {"ResetType": "PushPowerButton"} # iLO 4: PushPowerButton = graceful shutdown; use "ForceOff" for hard power cut
|
||||
response = REDFISHOBJ.post(reset_uri, body=body)
|
||||
|
||||
print(f"Status: {response.status}")
|
||||
if response.status >= 400:
|
||||
print(f"Error: {response.read}")
|
||||
|
||||
REDFISHOBJ.logout()
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import configparser
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
DEFAULT_EK = "0000000000000000000000000000000000000000"
|
||||
|
||||
|
||||
def load_credentials(path):
|
||||
if not os.path.exists(path):
|
||||
print(f"Error: credentials file not found: {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(path)
|
||||
|
||||
try:
|
||||
username = config.get("credentials", "username", fallback="root")
|
||||
password = config.get("credentials", "password")
|
||||
encryption_key = config.get("credentials", "encryption_key", fallback=DEFAULT_EK)
|
||||
except (configparser.NoSectionError, configparser.NoOptionError) as e:
|
||||
print(f"Error reading credentials file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return username, password, encryption_key
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Send IPMI power off")
|
||||
p.add_argument("-H", "--host", required=True, help="IPMI host address")
|
||||
p.add_argument(
|
||||
"-c",
|
||||
"--credentials",
|
||||
default="credentials",
|
||||
help="Path to credentials file (default: credentials)",
|
||||
)
|
||||
args = p.parse_args()
|
||||
|
||||
ipmi_user, ipmi_pw, ipmi_ek = load_credentials(args.credentials)
|
||||
|
||||
cmd = [
|
||||
"ipmitool",
|
||||
"-I", "lanplus",
|
||||
"-H", args.host,
|
||||
"-U", ipmi_user,
|
||||
"-P", ipmi_pw,
|
||||
"-y", ipmi_ek,
|
||||
"power", "off",
|
||||
]
|
||||
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if proc.returncode != 0:
|
||||
print("ipmitool failed:", proc.stderr.strip(), file=sys.stderr)
|
||||
sys.exit(proc.returncode)
|
||||
|
||||
print(proc.stdout.strip())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import configparser
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
def ts_print(msg):
|
||||
"""ts_print message prefixed with a timestamp (YYYY-MM-DD HH:MM:SS)."""
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"{now} {msg}")
|
||||
|
||||
DEFAULT_EK = "0000000000000000000000000000000000000000"
|
||||
|
||||
|
||||
def load_credentials(path):
|
||||
if not os.path.exists(path):
|
||||
print(f"Error: credentials file not found: {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(path)
|
||||
|
||||
try:
|
||||
username = config.get("ilo", "username", fallback="root")
|
||||
password = config.get("ilo", "password")
|
||||
encryption_key = config.get("ilo", "encryption_key", fallback=DEFAULT_EK)
|
||||
except (configparser.NoSectionError, configparser.NoOptionError) as e:
|
||||
print(f"Error reading credentials file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return username, password, encryption_key
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Send IPMI power on")
|
||||
p.add_argument("-H", "--host", required=True, help="IPMI host address")
|
||||
p.add_argument(
|
||||
"-c",
|
||||
"--credentials",
|
||||
default="credentials",
|
||||
help="Path to credentials file (default: credentials)",
|
||||
)
|
||||
args = p.parse_args()
|
||||
|
||||
ipmi_user, ipmi_pw, ipmi_ek = load_credentials(args.credentials)
|
||||
|
||||
cmd = [
|
||||
"ipmitool",
|
||||
"-I", "lanplus",
|
||||
"-H", args.host,
|
||||
"-U", ipmi_user,
|
||||
"-P", ipmi_pw,
|
||||
"-y", ipmi_ek,
|
||||
"power", "on",
|
||||
]
|
||||
print(f"Sending power on to {args.host}")
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if proc.returncode != 0:
|
||||
print("ipmitool failed:", proc.stderr.strip(), file=sys.stderr)
|
||||
sys.exit(proc.returncode)
|
||||
print(proc.stdout.strip())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# Configuration
|
||||
BACKUP_DIR="/var/lib/postgresql_archive/pg_dumpfiles"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
RETENTION_DAYS=7
|
||||
|
||||
# Ensure directory exists
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Get all non-system databases and back each one up.
|
||||
psql -U postgres -d postgres -Atc "SELECT datname FROM pg_database WHERE datistemplate = false AND datname <> 'postgres' ORDER BY datname" | while IFS= read -r DB_NAME; do
|
||||
pg_dump -U postgres -d "$DB_NAME" | gzip > "$BACKUP_DIR/${DB_NAME}_$TIMESTAMP.sql.gz"
|
||||
echo "Backup completed: ${DB_NAME}_$TIMESTAMP.sql.gz"
|
||||
done
|
||||
|
||||
# Delete backups older than retention period
|
||||
find "$BACKUP_DIR" -type f -mtime +$RETENTION_DAYS -delete
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
BACKUP_DIR="/var/lib/postgresql_archive/pgbackups/$(date +%Y%m%d)"
|
||||
mkdir -p $BACKUP_DIR
|
||||
# Perform base backup with WAL streaming
|
||||
pg_basebackup -h localhost -U wjones -D $BACKUP_DIR -Fp -Xs -P
|
||||
# Optional: Compress
|
||||
tar -czf ${BACKUP_DIR}_$(date +%Y%m%d_%H%M%S).tar.gz $BACKUP_DIR
|
||||
rm -rf $BACKUP_DIR
|
||||
# Retention: Delete backups older than 7 days
|
||||
find /var/lib/postgresql_archive/pgbackups/ -type f -mtime +7 -name "*.tar.gz" -delete
|
||||
@@ -0,0 +1,276 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Airsonic Advanced + PostgreSQL diagnostics
|
||||
# Focus: slow media scans, index efficiency, write pressure, and cache tuning signals.
|
||||
|
||||
PGHOST="${PGHOST:-localhost}"
|
||||
PGPORT="${PGPORT:-5432}"
|
||||
PGUSER="${PGUSER:-postgres}"
|
||||
PGDATABASE="${PGDATABASE:-airsonic}"
|
||||
OUT_FILE="${1:-airsonic_pg_diagnostics_$(date +%Y%m%d_%H%M%S).txt}"
|
||||
|
||||
PSQL=(psql -X -v ON_ERROR_STOP=1 -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE")
|
||||
|
||||
run_section() {
|
||||
local title="$1"
|
||||
local sql="$2"
|
||||
|
||||
{
|
||||
echo
|
||||
echo "================================================================"
|
||||
echo "$title"
|
||||
echo "================================================================"
|
||||
} >> "$OUT_FILE"
|
||||
|
||||
if ! "${PSQL[@]}" -c "$sql" >> "$OUT_FILE" 2>&1; then
|
||||
echo "Query failed for section: $title" >> "$OUT_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
query_single_value() {
|
||||
local sql="$1"
|
||||
"${PSQL[@]}" -Atc "$sql" 2>/dev/null || true
|
||||
}
|
||||
|
||||
bytes_to_human() {
|
||||
local bytes="$1"
|
||||
if [[ -z "$bytes" || ! "$bytes" =~ ^[0-9]+$ ]]; then
|
||||
echo "unknown"
|
||||
return
|
||||
fi
|
||||
|
||||
local kib=$((1024))
|
||||
local mib=$((1024 * 1024))
|
||||
local gib=$((1024 * 1024 * 1024))
|
||||
|
||||
if (( bytes >= gib )); then
|
||||
awk -v b="$bytes" -v g="$gib" 'BEGIN { printf "%.2f GiB", b/g }'
|
||||
elif (( bytes >= mib )); then
|
||||
awk -v b="$bytes" -v m="$mib" 'BEGIN { printf "%.2f MiB", b/m }'
|
||||
elif (( bytes >= kib )); then
|
||||
awk -v b="$bytes" -v k="$kib" 'BEGIN { printf "%.2f KiB", b/k }'
|
||||
else
|
||||
echo "${bytes} B"
|
||||
fi
|
||||
}
|
||||
|
||||
{
|
||||
echo "Airsonic PostgreSQL Diagnostics"
|
||||
echo "Generated at: $(date -Iseconds)"
|
||||
echo "Target: host=$PGHOST port=$PGPORT db=$PGDATABASE user=$PGUSER"
|
||||
} > "$OUT_FILE"
|
||||
|
||||
run_section "1) Server identity and uptime" "
|
||||
SELECT version();
|
||||
SELECT now() AS current_time, pg_postmaster_start_time() AS postmaster_start_time;
|
||||
"
|
||||
|
||||
run_section "2) Key PostgreSQL tuning parameters" "
|
||||
SELECT name, setting, unit, short_desc
|
||||
FROM pg_settings
|
||||
WHERE name IN (
|
||||
'shared_buffers',
|
||||
'work_mem',
|
||||
'maintenance_work_mem',
|
||||
'effective_cache_size',
|
||||
'max_connections',
|
||||
'effective_io_concurrency',
|
||||
'random_page_cost',
|
||||
'seq_page_cost',
|
||||
'checkpoint_timeout',
|
||||
'checkpoint_completion_target',
|
||||
'autovacuum',
|
||||
'autovacuum_max_workers',
|
||||
'autovacuum_naptime'
|
||||
)
|
||||
ORDER BY name;
|
||||
"
|
||||
|
||||
run_section "3) Database cache hit ratio and temp usage" "
|
||||
SELECT
|
||||
datname,
|
||||
blks_read,
|
||||
blks_hit,
|
||||
ROUND(100.0 * blks_hit / NULLIF(blks_hit + blks_read, 0), 2) AS cache_hit_pct,
|
||||
temp_files,
|
||||
pg_size_pretty(temp_bytes) AS temp_bytes,
|
||||
deadlocks,
|
||||
stats_reset
|
||||
FROM pg_stat_database
|
||||
WHERE datname = current_database();
|
||||
"
|
||||
|
||||
run_section "4) Most read tables and their index-vs-seq scan profile" "
|
||||
SELECT
|
||||
relname,
|
||||
seq_scan,
|
||||
idx_scan,
|
||||
n_live_tup,
|
||||
n_dead_tup,
|
||||
ROUND((100.0 * idx_scan / NULLIF(idx_scan + seq_scan, 0))::numeric, 2) AS index_scan_pct,
|
||||
ROUND((100.0 * heap_blks_hit / NULLIF(heap_blks_hit + heap_blks_read, 0))::numeric, 2) AS table_cache_hit_pct
|
||||
FROM pg_stat_user_tables t
|
||||
JOIN pg_statio_user_tables io USING (relid)
|
||||
ORDER BY (seq_scan + idx_scan) DESC NULLS LAST
|
||||
LIMIT 25;
|
||||
"
|
||||
|
||||
run_section "5) Potentially unused indexes (can slow writes during scans/indexing)" "
|
||||
SELECT
|
||||
s.schemaname,
|
||||
s.relname AS table_name,
|
||||
s.indexrelname AS index_name,
|
||||
s.idx_scan,
|
||||
pg_size_pretty(pg_relation_size(s.indexrelid)) AS index_size,
|
||||
t.n_tup_ins + t.n_tup_upd + t.n_tup_del AS table_writes_since_reset
|
||||
FROM pg_stat_user_indexes s
|
||||
JOIN pg_index i ON i.indexrelid = s.indexrelid
|
||||
JOIN pg_stat_user_tables t ON t.relid = s.relid
|
||||
WHERE s.idx_scan = 0
|
||||
AND NOT i.indisunique
|
||||
AND NOT i.indisprimary
|
||||
ORDER BY pg_relation_size(s.indexrelid) DESC
|
||||
LIMIT 50;
|
||||
"
|
||||
|
||||
run_section "6) Write-heavy tables (hot spot during media metadata indexing)" "
|
||||
SELECT
|
||||
relname,
|
||||
n_tup_ins,
|
||||
n_tup_upd,
|
||||
n_tup_del,
|
||||
n_tup_hot_upd,
|
||||
n_dead_tup,
|
||||
vacuum_count,
|
||||
autovacuum_count,
|
||||
analyze_count,
|
||||
autoanalyze_count,
|
||||
COALESCE(last_autovacuum::text, 'never') AS last_autovacuum,
|
||||
COALESCE(last_autoanalyze::text, 'never') AS last_autoanalyze
|
||||
FROM pg_stat_user_tables
|
||||
ORDER BY (n_tup_ins + n_tup_upd + n_tup_del) DESC
|
||||
LIMIT 25;
|
||||
"
|
||||
|
||||
run_section "7) Largest relations (tables and indexes)" "
|
||||
SELECT
|
||||
n.nspname AS schema_name,
|
||||
c.relname,
|
||||
c.relkind,
|
||||
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size
|
||||
FROM pg_class c
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
||||
AND c.relkind IN ('r', 'i')
|
||||
ORDER BY pg_total_relation_size(c.oid) DESC
|
||||
LIMIT 30;
|
||||
"
|
||||
|
||||
if [[ "$(query_single_value "SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements';")" == "1" ]]; then
|
||||
run_section "8) Slowest normalized SQL from pg_stat_statements" "
|
||||
SELECT
|
||||
calls,
|
||||
ROUND(total_exec_time::numeric, 2) AS total_exec_ms,
|
||||
ROUND((total_exec_time / NULLIF(calls, 0))::numeric, 2) AS mean_exec_ms,
|
||||
ROUND(rows::numeric / NULLIF(calls, 0), 2) AS avg_rows,
|
||||
shared_blks_read,
|
||||
shared_blks_hit,
|
||||
temp_blks_read,
|
||||
temp_blks_written,
|
||||
LEFT(query, 300) AS query_sample
|
||||
FROM pg_stat_statements
|
||||
WHERE dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
|
||||
ORDER BY mean_exec_ms DESC
|
||||
LIMIT 20;
|
||||
"
|
||||
else
|
||||
{
|
||||
echo
|
||||
echo "================================================================"
|
||||
echo "8) Slowest normalized SQL from pg_stat_statements"
|
||||
echo "================================================================"
|
||||
echo "Extension pg_stat_statements is not installed."
|
||||
echo "Install with: CREATE EXTENSION pg_stat_statements;"
|
||||
} >> "$OUT_FILE"
|
||||
fi
|
||||
|
||||
# Heuristic recommendation summary
|
||||
CACHE_HIT_PCT="$(query_single_value "
|
||||
SELECT ROUND(100.0 * blks_hit / NULLIF(blks_hit + blks_read, 0), 2)
|
||||
FROM pg_stat_database
|
||||
WHERE datname = current_database();")"
|
||||
|
||||
UNUSED_INDEX_COUNT="$(query_single_value "
|
||||
SELECT COUNT(*)
|
||||
FROM pg_stat_user_indexes s
|
||||
JOIN pg_index i ON i.indexrelid = s.indexrelid
|
||||
WHERE s.idx_scan = 0
|
||||
AND NOT i.indisunique
|
||||
AND NOT i.indisprimary;")"
|
||||
|
||||
SHARED_BUFFERS_BYTES="$(query_single_value "
|
||||
SELECT pg_size_bytes(setting || unit)
|
||||
FROM pg_settings
|
||||
WHERE name = 'shared_buffers';")"
|
||||
|
||||
TOTAL_RAM_KB=""
|
||||
if [[ -r /proc/meminfo ]]; then
|
||||
TOTAL_RAM_KB="$(awk '/MemTotal:/ {print $2}' /proc/meminfo)"
|
||||
fi
|
||||
|
||||
{
|
||||
echo
|
||||
echo "================================================================"
|
||||
echo "9) Recommendations summary"
|
||||
echo "================================================================"
|
||||
|
||||
if [[ -n "$CACHE_HIT_PCT" ]]; then
|
||||
echo "Cache hit ratio: ${CACHE_HIT_PCT}%"
|
||||
awk -v hit="$CACHE_HIT_PCT" 'BEGIN {
|
||||
if (hit < 98.0) {
|
||||
print "- Action: cache hit ratio is low for media workloads. Increase shared_buffers/effective_cache_size and review slow seq scans."
|
||||
} else if (hit < 99.0) {
|
||||
print "- Action: cache hit ratio is acceptable but can likely improve for large libraries."
|
||||
} else {
|
||||
print "- Action: cache hit ratio looks healthy."
|
||||
}
|
||||
}'
|
||||
else
|
||||
echo "Cache hit ratio: unavailable"
|
||||
fi
|
||||
|
||||
if [[ -n "$UNUSED_INDEX_COUNT" ]]; then
|
||||
echo "Potentially unused non-unique indexes: $UNUSED_INDEX_COUNT"
|
||||
if [[ "$UNUSED_INDEX_COUNT" =~ ^[0-9]+$ ]] && (( UNUSED_INDEX_COUNT > 0 )); then
|
||||
echo "- Action: validate and drop truly unused indexes to reduce write overhead during scans/indexing."
|
||||
else
|
||||
echo "- Action: no obvious unused non-unique indexes from current stats."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$SHARED_BUFFERS_BYTES" ]]; then
|
||||
echo "shared_buffers: $(bytes_to_human "$SHARED_BUFFERS_BYTES")"
|
||||
if [[ -n "$TOTAL_RAM_KB" && "$TOTAL_RAM_KB" =~ ^[0-9]+$ ]]; then
|
||||
TOTAL_RAM_BYTES=$((TOTAL_RAM_KB * 1024))
|
||||
SHARED_BUFFER_PCT=$(awk -v s="$SHARED_BUFFERS_BYTES" -v t="$TOTAL_RAM_BYTES" 'BEGIN { printf "%.2f", (s/t)*100 }')
|
||||
echo "Estimated shared_buffers/system RAM: ${SHARED_BUFFER_PCT}%"
|
||||
awk -v p="$SHARED_BUFFER_PCT" 'BEGIN {
|
||||
if (p < 10.0) {
|
||||
print "- Action: shared_buffers appears low; typical starting point is ~15-25% of system RAM."
|
||||
} else if (p > 40.0) {
|
||||
print "- Action: shared_buffers appears high; ensure OS cache remains adequate."
|
||||
} else {
|
||||
print "- Action: shared_buffers proportion looks reasonable."
|
||||
}
|
||||
}'
|
||||
else
|
||||
echo "- Action: compare shared_buffers to system RAM manually (target often 15-25% for dedicated DB hosts)."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "- Action: for media scans, verify high-read tables use selective indexes and that autovacuum keeps dead tuples controlled."
|
||||
echo "- Action: rerun this script before and during a scan to compare write pressure and cache behavior over time."
|
||||
} >> "$OUT_FILE"
|
||||
|
||||
echo "Diagnostics complete. Report written to: $OUT_FILE"
|
||||
@@ -0,0 +1,268 @@
|
||||
param(
|
||||
[string]$PgHost = "localhost",
|
||||
[int]$PgPort = 5432,
|
||||
[string]$PgUser = "postgres",
|
||||
[string]$BenchDb = "pgbench",
|
||||
[int]$Scale = 50,
|
||||
[int]$Duration = 60,
|
||||
[string]$Clients = "1 4 8 16 32",
|
||||
[int]$Jobs = 4,
|
||||
[int]$WarmupSeconds = 15,
|
||||
[bool]$RunReadOnly = $true,
|
||||
[bool]$RunCustom = $false,
|
||||
[string]$CustomSqlFile = "pgbench_custom_workload.sql",
|
||||
[string]$CustomModeName = "custom_sql",
|
||||
[ValidateRange(1, 99)][int]$CustomReadPct = 80,
|
||||
[ValidateRange(1, 100)][int]$CustomHotPct = 90,
|
||||
[ValidateRange(1, 4)][int]$CustomTxnSize = 2,
|
||||
[ValidateRange(100, 100000000)][int]$CustomHotAccounts = 10000,
|
||||
[bool]$Reinit = $false
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Require-Command {
|
||||
param([Parameter(Mandatory = $true)][string]$Name)
|
||||
|
||||
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||
throw "Missing required command: $Name"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-PsqlScalar {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Database,
|
||||
[Parameter(Mandatory = $true)][string]$Sql
|
||||
)
|
||||
|
||||
$output = & psql -X -v ON_ERROR_STOP=1 -h $PgHost -p $PgPort -U $PgUser -d $Database -Atc $Sql
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "psql failed running SQL: $Sql"
|
||||
}
|
||||
|
||||
return ($output | Select-Object -First 1)
|
||||
}
|
||||
|
||||
function Invoke-PsqlNonQuery {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Database,
|
||||
[Parameter(Mandatory = $true)][string]$Sql
|
||||
)
|
||||
|
||||
& psql -X -v ON_ERROR_STOP=1 -h $PgHost -p $PgPort -U $PgUser -d $Database -c $Sql | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "psql failed running SQL: $Sql"
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-BenchmarkDatabase {
|
||||
$exists = Invoke-PsqlScalar -Database "postgres" -Sql "SELECT 1 FROM pg_database WHERE datname = '$BenchDb';"
|
||||
if ($exists -ne "1") {
|
||||
Write-Host "Creating benchmark database: $BenchDb"
|
||||
Invoke-PsqlNonQuery -Database "postgres" -Sql "CREATE DATABASE $BenchDb;"
|
||||
}
|
||||
}
|
||||
|
||||
function Needs-Init {
|
||||
$hasTable = Invoke-PsqlScalar -Database $BenchDb -Sql "SELECT to_regclass('public.pgbench_accounts') IS NOT NULL;"
|
||||
return $hasTable -ne "t"
|
||||
}
|
||||
|
||||
function Init-OrReinitData {
|
||||
if ($Reinit -or (Needs-Init)) {
|
||||
Write-Host "Initializing pgbench schema/data (scale=$Scale)"
|
||||
& pgbench -h $PgHost -p $PgPort -U $PgUser -i -s $Scale $BenchDb | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "pgbench initialization failed"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "Using existing pgbench data in $BenchDb"
|
||||
}
|
||||
}
|
||||
|
||||
function Parse-Tps {
|
||||
param([Parameter(Mandatory = $true)][string]$Text)
|
||||
|
||||
$matches = [regex]::Matches($Text, "(?m)^tps\s*=\s*([0-9]+(?:\.[0-9]+)?)")
|
||||
if ($matches.Count -gt 0) {
|
||||
return $matches[$matches.Count - 1].Groups[1].Value
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
function Parse-AvgLatencyMs {
|
||||
param([Parameter(Mandatory = $true)][string]$Text)
|
||||
|
||||
$matches = [regex]::Matches($Text, "(?m)^latency average\s*=\s*([0-9]+(?:\.[0-9]+)?)\s*ms")
|
||||
if ($matches.Count -gt 0) {
|
||||
return $matches[$matches.Count - 1].Groups[1].Value
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
function Run-Case {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Mode,
|
||||
[Parameter(Mandatory = $true)][int]$ClientCount,
|
||||
[AllowEmptyCollection()][string[]]$ExtraArgs = @(),
|
||||
[Parameter(Mandatory = $true)][string]$ResultDir,
|
||||
[Parameter(Mandatory = $true)][string]$SummaryCsv
|
||||
)
|
||||
|
||||
$logFile = Join-Path $ResultDir ("{0}_c{1}.txt" -f $Mode, $ClientCount)
|
||||
$txLogPrefix = Join-Path $ResultDir ("{0}_c{1}_txlog" -f $Mode, $ClientCount)
|
||||
Write-Host "Running mode=$Mode, clients=$ClientCount, duration=${Duration}s"
|
||||
|
||||
# Warm cache and avoid measuring first-run effects.
|
||||
& pgbench -h $PgHost -p $PgPort -U $PgUser -n -M prepared -j $Jobs -c $ClientCount -T $WarmupSeconds @ExtraArgs $BenchDb | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Warm-up run failed for mode=$Mode clients=$ClientCount"
|
||||
}
|
||||
|
||||
$runOutput = & pgbench -h $PgHost -p $PgPort -U $PgUser -n -M prepared -j $Jobs -c $ClientCount -T $Duration -r -l --log-prefix $txLogPrefix @ExtraArgs $BenchDb 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Measured run failed for mode=$Mode clients=$ClientCount"
|
||||
}
|
||||
|
||||
$runOutput | Set-Content -Path $logFile -Encoding UTF8
|
||||
|
||||
$text = ($runOutput -join [Environment]::NewLine)
|
||||
$tps = Parse-Tps -Text $text
|
||||
$latencyMs = Parse-AvgLatencyMs -Text $text
|
||||
$percentiles = Get-PercentileLatency -LogPrefix $txLogPrefix
|
||||
|
||||
"$Mode,$ClientCount,$Duration,$Jobs,$tps,$latencyMs,$($percentiles.P95Ms),$($percentiles.P99Ms),$logFile" | Add-Content -Path $SummaryCsv
|
||||
}
|
||||
|
||||
function Get-PercentileLatency {
|
||||
param([Parameter(Mandatory = $true)][string]$LogPrefix)
|
||||
|
||||
$logFiles = Get-ChildItem -Path ($LogPrefix + "*") -File -ErrorAction SilentlyContinue
|
||||
if (-not $logFiles -or $logFiles.Count -eq 0) {
|
||||
return @{ P95Ms = ""; P99Ms = "" }
|
||||
}
|
||||
|
||||
# Keep memory bounded even for very large transaction logs.
|
||||
$maxSampleSize = 200000
|
||||
$reservoir = New-Object System.Collections.Generic.List[double]
|
||||
$reservoir.Capacity = $maxSampleSize
|
||||
$random = [System.Random]::new()
|
||||
$seen = 0
|
||||
|
||||
foreach ($file in $logFiles) {
|
||||
$reader = [System.IO.File]::OpenText($file.FullName)
|
||||
try {
|
||||
while (($line = $reader.ReadLine()) -ne $null) {
|
||||
if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith("#")) {
|
||||
continue
|
||||
}
|
||||
|
||||
$tokens = $line -split "[\s,]+" | Where-Object { $_ -ne "" }
|
||||
if ($tokens.Count -ge 3 -and $tokens[2] -match "^[0-9]+(?:\.[0-9]+)?$") {
|
||||
$latUs = [double]$tokens[2]
|
||||
$seen++
|
||||
|
||||
if ($reservoir.Count -lt $maxSampleSize) {
|
||||
$reservoir.Add($latUs)
|
||||
}
|
||||
else {
|
||||
$idx = $random.Next(0, $seen)
|
||||
if ($idx -lt $maxSampleSize) {
|
||||
$reservoir[$idx] = $latUs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$reader.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
if ($reservoir.Count -eq 0) {
|
||||
return @{ P95Ms = ""; P99Ms = "" }
|
||||
}
|
||||
|
||||
$sorted = $reservoir | Sort-Object
|
||||
$p95Us = Select-PercentileValue -SortedValues $sorted -Percentile 95
|
||||
$p99Us = Select-PercentileValue -SortedValues $sorted -Percentile 99
|
||||
|
||||
return @{
|
||||
P95Ms = [math]::Round(($p95Us / 1000.0), 3)
|
||||
P99Ms = [math]::Round(($p99Us / 1000.0), 3)
|
||||
}
|
||||
}
|
||||
|
||||
function Select-PercentileValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][double[]]$SortedValues,
|
||||
[Parameter(Mandatory = $true)][ValidateRange(1, 99)][int]$Percentile
|
||||
)
|
||||
|
||||
$n = $SortedValues.Count
|
||||
$rank = [math]::Ceiling(($Percentile / 100.0) * $n)
|
||||
if ($rank -lt 1) { $rank = 1 }
|
||||
if ($rank -gt $n) { $rank = $n }
|
||||
return $SortedValues[$rank - 1]
|
||||
}
|
||||
|
||||
function Resolve-CustomSqlPath {
|
||||
if ([System.IO.Path]::IsPathRooted($CustomSqlFile)) {
|
||||
return $CustomSqlFile
|
||||
}
|
||||
|
||||
return (Join-Path (Get-Location) $CustomSqlFile)
|
||||
}
|
||||
|
||||
Require-Command -Name "psql"
|
||||
Require-Command -Name "pgbench"
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
$resultDir = Join-Path (Get-Location) ("pgbench_results_{0}" -f $timestamp)
|
||||
$summaryCsv = Join-Path $resultDir "summary.csv"
|
||||
|
||||
New-Item -Path $resultDir -ItemType Directory -Force | Out-Null
|
||||
"mode,clients,duration_seconds,jobs,tps,latency_ms,p95_ms,p99_ms,details_file" | Set-Content -Path $summaryCsv -Encoding UTF8
|
||||
|
||||
Ensure-BenchmarkDatabase
|
||||
Init-OrReinitData
|
||||
|
||||
$customSqlPath = ""
|
||||
if ($RunCustom) {
|
||||
$customSqlPath = Resolve-CustomSqlPath
|
||||
if (-not (Test-Path -Path $customSqlPath -PathType Leaf)) {
|
||||
throw "Custom SQL file not found: $customSqlPath"
|
||||
}
|
||||
}
|
||||
|
||||
$clientValues = $Clients -split "\s+" | Where-Object { $_ -match "^[0-9]+$" }
|
||||
if ($clientValues.Count -eq 0) {
|
||||
throw "No valid client values found. Example: -Clients '1 4 8 16 32'"
|
||||
}
|
||||
|
||||
foreach ($c in $clientValues) {
|
||||
$clientInt = [int]$c
|
||||
Run-Case -Mode "read_write" -ClientCount $clientInt -ExtraArgs @() -ResultDir $resultDir -SummaryCsv $summaryCsv
|
||||
|
||||
if ($RunReadOnly) {
|
||||
Run-Case -Mode "read_only" -ClientCount $clientInt -ExtraArgs @("-S") -ResultDir $resultDir -SummaryCsv $summaryCsv
|
||||
}
|
||||
|
||||
if ($RunCustom) {
|
||||
Run-Case -Mode $CustomModeName -ClientCount $clientInt -ExtraArgs @(
|
||||
"-D", "read_pct=$CustomReadPct",
|
||||
"-D", "hot_pct=$CustomHotPct",
|
||||
"-D", "txn_size=$CustomTxnSize",
|
||||
"-D", "hot_accounts=$CustomHotAccounts",
|
||||
"-f", $customSqlPath
|
||||
) -ResultDir $resultDir -SummaryCsv $summaryCsv
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Benchmark completed."
|
||||
Write-Host "Summary: $summaryCsv"
|
||||
Write-Host "Raw outputs: $resultDir"
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Repeatable pgbench benchmark harness.
|
||||
# Usage example:
|
||||
# PGHOST=localhost PGPORT=5432 PGUSER=postgres PGPASSWORD=secret \
|
||||
# ./pgbench_benchmark.sh
|
||||
#
|
||||
# Optional environment variables:
|
||||
# BENCH_DB=pgbench
|
||||
# SCALE=50
|
||||
# DURATION=60
|
||||
# CLIENTS="1 4 8 16 32"
|
||||
# JOBS=4
|
||||
# WARMUP_SECONDS=15
|
||||
# RUN_READ_ONLY=1
|
||||
# RUN_CUSTOM=0
|
||||
# CUSTOM_SQL_FILE=pgbench_custom_workload.sql
|
||||
# CUSTOM_MODE_NAME=custom_sql
|
||||
# CUSTOM_READ_PCT=80
|
||||
# CUSTOM_HOT_PCT=90
|
||||
# CUSTOM_TXN_SIZE=2
|
||||
# CUSTOM_HOT_ACCOUNTS=10000
|
||||
# REINIT=0
|
||||
|
||||
PGHOST="${PGHOST:-localhost}"
|
||||
PGPORT="${PGPORT:-5432}"
|
||||
PGUSER="${PGUSER:-postgres}"
|
||||
BENCH_DB="${BENCH_DB:-pgbench}"
|
||||
SCALE="${SCALE:-50}"
|
||||
DURATION="${DURATION:-60}"
|
||||
CLIENTS="${CLIENTS:-1 4 8 16 32}"
|
||||
JOBS="${JOBS:-4}"
|
||||
WARMUP_SECONDS="${WARMUP_SECONDS:-15}"
|
||||
RUN_READ_ONLY="${RUN_READ_ONLY:-1}"
|
||||
RUN_CUSTOM="${RUN_CUSTOM:-0}"
|
||||
CUSTOM_SQL_FILE="${CUSTOM_SQL_FILE:-pgbench_custom_workload.sql}"
|
||||
CUSTOM_MODE_NAME="${CUSTOM_MODE_NAME:-custom_sql}"
|
||||
CUSTOM_READ_PCT="${CUSTOM_READ_PCT:-80}"
|
||||
CUSTOM_HOT_PCT="${CUSTOM_HOT_PCT:-90}"
|
||||
CUSTOM_TXN_SIZE="${CUSTOM_TXN_SIZE:-2}"
|
||||
CUSTOM_HOT_ACCOUNTS="${CUSTOM_HOT_ACCOUNTS:-10000}"
|
||||
REINIT="${REINIT:-0}"
|
||||
|
||||
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
RESULT_DIR="pgbench_results_${TIMESTAMP}"
|
||||
SUMMARY_CSV="${RESULT_DIR}/summary.csv"
|
||||
|
||||
PSQL=(psql -X -v ON_ERROR_STOP=1 -h "$PGHOST" -p "$PGPORT" -U "$PGUSER")
|
||||
PGBENCH=(pgbench -h "$PGHOST" -p "$PGPORT" -U "$PGUSER")
|
||||
|
||||
require_command() {
|
||||
local cmd="$1"
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "Missing required command: $cmd" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
create_database_if_missing() {
|
||||
local exists
|
||||
exists="$("${PSQL[@]}" -d postgres -Atc "SELECT 1 FROM pg_database WHERE datname = '${BENCH_DB}';" || true)"
|
||||
if [[ "$exists" != "1" ]]; then
|
||||
echo "Creating benchmark database: ${BENCH_DB}"
|
||||
"${PSQL[@]}" -d postgres -c "CREATE DATABASE ${BENCH_DB};"
|
||||
fi
|
||||
}
|
||||
|
||||
needs_init() {
|
||||
local rel
|
||||
rel="$("${PSQL[@]}" -d "$BENCH_DB" -Atc "SELECT to_regclass('public.pgbench_accounts') IS NOT NULL;")"
|
||||
[[ "$rel" != "t" ]]
|
||||
}
|
||||
|
||||
init_or_reinit_data() {
|
||||
if [[ "$REINIT" == "1" ]] || needs_init; then
|
||||
echo "Initializing pgbench schema/data (scale=${SCALE})"
|
||||
"${PGBENCH[@]}" -i -s "$SCALE" "$BENCH_DB"
|
||||
else
|
||||
echo "Using existing pgbench data in ${BENCH_DB}"
|
||||
fi
|
||||
}
|
||||
|
||||
extract_tps() {
|
||||
local output_file="$1"
|
||||
awk -F'= ' '/tps =/{print $2}' "$output_file" | awk '{print $1}' | tail -n 1
|
||||
}
|
||||
|
||||
extract_avg_latency() {
|
||||
local output_file="$1"
|
||||
awk -F'= ' '/latency average =/{print $2}' "$output_file" | awk '{print $1}' | tail -n 1
|
||||
}
|
||||
|
||||
select_percentile_ms_from_prefix() {
|
||||
local log_prefix="$1"
|
||||
local percentile="$2"
|
||||
local -a log_files
|
||||
local -a values_us
|
||||
local count rank value_us
|
||||
|
||||
shopt -s nullglob
|
||||
log_files=("${log_prefix}"*)
|
||||
shopt -u nullglob
|
||||
|
||||
if (( ${#log_files[@]} == 0 )); then
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
|
||||
mapfile -t values_us < <(awk 'NF >= 3 && $3 ~ /^[0-9]+(\.[0-9]+)?$/ {print $3}' "${log_files[@]}" | sort -n)
|
||||
count="${#values_us[@]}"
|
||||
if (( count == 0 )); then
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
|
||||
rank=$(( (percentile * count + 99) / 100 ))
|
||||
if (( rank < 1 )); then rank=1; fi
|
||||
if (( rank > count )); then rank=count; fi
|
||||
|
||||
value_us="${values_us[$((rank - 1))]}"
|
||||
awk -v us="$value_us" 'BEGIN { printf "%.3f", us/1000.0 }'
|
||||
}
|
||||
|
||||
run_case() {
|
||||
local mode="$1"
|
||||
local clients="$2"
|
||||
shift 2
|
||||
local -a extra_args=("$@")
|
||||
local log_file="${RESULT_DIR}/${mode}_c${clients}.txt"
|
||||
local tx_log_prefix="${RESULT_DIR}/${mode}_c${clients}_txlog"
|
||||
|
||||
echo "Running mode=${mode}, clients=${clients}, duration=${DURATION}s"
|
||||
# Warm cache and avoid measuring first-run effects.
|
||||
"${PGBENCH[@]}" -n -M prepared -j "$JOBS" -c "$clients" -T "$WARMUP_SECONDS" "${extra_args[@]}" "$BENCH_DB" > /dev/null
|
||||
|
||||
"${PGBENCH[@]}" -n -M prepared -j "$JOBS" -c "$clients" -T "$DURATION" -r -l --log-prefix="$tx_log_prefix" "${extra_args[@]}" "$BENCH_DB" > "$log_file"
|
||||
|
||||
local tps latency p95 p99
|
||||
tps="$(extract_tps "$log_file")"
|
||||
latency="$(extract_avg_latency "$log_file")"
|
||||
p95="$(select_percentile_ms_from_prefix "$tx_log_prefix" 95)"
|
||||
p99="$(select_percentile_ms_from_prefix "$tx_log_prefix" 99)"
|
||||
|
||||
echo "${mode},${clients},${DURATION},${JOBS},${tps},${latency},${p95},${p99},${log_file}" >> "$SUMMARY_CSV"
|
||||
}
|
||||
|
||||
main() {
|
||||
require_command psql
|
||||
require_command pgbench
|
||||
|
||||
mkdir -p "$RESULT_DIR"
|
||||
echo "mode,clients,duration_seconds,jobs,tps,latency_ms,p95_ms,p99_ms,details_file" > "$SUMMARY_CSV"
|
||||
|
||||
create_database_if_missing
|
||||
init_or_reinit_data
|
||||
|
||||
if [[ "$RUN_CUSTOM" == "1" ]]; then
|
||||
if [[ ! -f "$CUSTOM_SQL_FILE" ]]; then
|
||||
echo "Custom SQL file not found: $CUSTOM_SQL_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
for c in $CLIENTS; do
|
||||
run_case "read_write" "$c"
|
||||
|
||||
if [[ "$RUN_READ_ONLY" == "1" ]]; then
|
||||
run_case "read_only" "$c" "-S"
|
||||
fi
|
||||
|
||||
if [[ "$RUN_CUSTOM" == "1" ]]; then
|
||||
run_case "$CUSTOM_MODE_NAME" "$c" \
|
||||
"-D" "read_pct=${CUSTOM_READ_PCT}" \
|
||||
"-D" "hot_pct=${CUSTOM_HOT_PCT}" \
|
||||
"-D" "txn_size=${CUSTOM_TXN_SIZE}" \
|
||||
"-D" "hot_accounts=${CUSTOM_HOT_ACCOUNTS}" \
|
||||
"-f" "$CUSTOM_SQL_FILE"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "Benchmark completed."
|
||||
echo "Summary: $SUMMARY_CSV"
|
||||
echo "Raw outputs: $RESULT_DIR"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export pgbench summary.csv into an HTML report with TPS and latency charts."""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Create an HTML chart report from pgbench summary.csv")
|
||||
parser.add_argument("summary_csv", help="Path to summary.csv generated by pgbench benchmark script")
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
default=None,
|
||||
help="Output HTML path (default: <summary_dir>/pgbench_report.html)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_rows(path: Path) -> List[Dict[str, str]]:
|
||||
with path.open("r", encoding="utf-8-sig", newline="") as f:
|
||||
reader = csv.DictReader(f)
|
||||
expected = {"mode", "clients", "tps", "latency_ms"}
|
||||
if not expected.issubset(set(reader.fieldnames or [])):
|
||||
missing = sorted(expected - set(reader.fieldnames or []))
|
||||
raise ValueError(f"summary.csv missing required columns: {', '.join(missing)}")
|
||||
return list(reader)
|
||||
|
||||
|
||||
def parse_optional_float(raw: str) -> Optional[float]:
|
||||
value = (raw or "").strip()
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def build_series(rows: List[Dict[str, str]]) -> Dict[str, List[Dict[str, float]]]:
|
||||
series: Dict[str, List[Dict[str, float]]] = {}
|
||||
for row in rows:
|
||||
mode = row["mode"].strip()
|
||||
try:
|
||||
clients = int(row["clients"])
|
||||
tps = float(row["tps"])
|
||||
latency = float(row["latency_ms"])
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
point: Dict[str, float] = {
|
||||
"clients": clients,
|
||||
"tps": tps,
|
||||
"latency_ms": latency,
|
||||
}
|
||||
|
||||
p95 = parse_optional_float(row.get("p95_ms", ""))
|
||||
p99 = parse_optional_float(row.get("p99_ms", ""))
|
||||
if p95 is not None:
|
||||
point["p95_ms"] = p95
|
||||
if p99 is not None:
|
||||
point["p99_ms"] = p99
|
||||
|
||||
series.setdefault(mode, []).append(point)
|
||||
|
||||
for mode in series:
|
||||
series[mode].sort(key=lambda item: item["clients"])
|
||||
|
||||
return series
|
||||
|
||||
|
||||
def html_report(title: str, data: Dict[str, List[Dict[str, float]]]) -> str:
|
||||
payload = json.dumps(data)
|
||||
return f"""<!doctype html>
|
||||
<html lang=\"en\">
|
||||
<head>
|
||||
<meta charset=\"utf-8\" />
|
||||
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
|
||||
<title>{title}</title>
|
||||
<style>
|
||||
:root {{
|
||||
--bg: #f7f7f2;
|
||||
--fg: #182020;
|
||||
--panel: #ffffff;
|
||||
--muted: #4f5b5b;
|
||||
--grid: #dde4e4;
|
||||
}}
|
||||
* {{ box-sizing: border-box; }}
|
||||
body {{
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: radial-gradient(circle at 8% 15%, #e7f6f2, var(--bg));
|
||||
color: var(--fg);
|
||||
}}
|
||||
.container {{
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}}
|
||||
.card {{
|
||||
background: var(--panel);
|
||||
border: 1px solid #e2ebeb;
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
margin-bottom: 18px;
|
||||
box-shadow: 0 10px 26px rgba(20, 30, 30, 0.06);
|
||||
}}
|
||||
h1 {{ margin: 0 0 8px 0; font-size: 1.6rem; }}
|
||||
p {{ margin: 0; color: var(--muted); }}
|
||||
canvas {{ width: 100%; height: 360px; display: block; }}
|
||||
.legend {{ display: flex; gap: 12px; flex-wrap: wrap; margin-top: 10px; }}
|
||||
.legend-item {{ display: inline-flex; align-items: center; gap: 8px; font-size: 0.9rem; color: var(--muted); }}
|
||||
.swatch {{ width: 14px; height: 14px; border-radius: 3px; display: inline-block; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class=\"container\">
|
||||
<div class=\"card\">
|
||||
<h1>{title}</h1>
|
||||
<p>Generated from summary.csv. X-axis = clients. Lines are grouped by mode.</p>
|
||||
</div>
|
||||
|
||||
<div class=\"card\">
|
||||
<h2>TPS by Clients</h2>
|
||||
<canvas id=\"tpsChart\" width=\"1040\" height=\"360\"></canvas>
|
||||
<div id=\"legend\" class=\"legend\"></div>
|
||||
</div>
|
||||
|
||||
<div class=\"card\">
|
||||
<h2>Latency (ms) by Clients</h2>
|
||||
<canvas id=\"latencyChart\" width=\"1040\" height=\"360\"></canvas>
|
||||
</div>
|
||||
|
||||
<div class=\"card\">
|
||||
<h2>p95 Latency (ms) by Clients</h2>
|
||||
<canvas id=\"p95Chart\" width=\"1040\" height=\"360\"></canvas>
|
||||
</div>
|
||||
|
||||
<div class=\"card\">
|
||||
<h2>p99 Latency (ms) by Clients</h2>
|
||||
<canvas id=\"p99Chart\" width=\"1040\" height=\"360\"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const series = {payload};
|
||||
const palette = ["#0b6e4f", "#f26419", "#33658a", "#2f4858", "#5f0f40", "#9a031e", "#386641", "#6a4c93"];
|
||||
|
||||
function flattenValues(metric) {{
|
||||
const out = [];
|
||||
Object.values(series).forEach(items => items.forEach(i => {{
|
||||
const v = i[metric];
|
||||
if (typeof v === 'number' && Number.isFinite(v)) out.push(v);
|
||||
}}));
|
||||
return out;
|
||||
}}
|
||||
|
||||
function flattenClients() {{
|
||||
const out = [];
|
||||
Object.values(series).forEach(items => items.forEach(i => out.push(i.clients)));
|
||||
return out;
|
||||
}}
|
||||
|
||||
function drawChart(canvasId, metric, yLabel) {{
|
||||
const c = document.getElementById(canvasId);
|
||||
const ctx = c.getContext('2d');
|
||||
const W = c.width;
|
||||
const H = c.height;
|
||||
const m = {{ top: 22, right: 20, bottom: 44, left: 60 }};
|
||||
const x0 = m.left, y0 = H - m.bottom, x1 = W - m.right, y1 = m.top;
|
||||
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
ctx.fillStyle = '#182020';
|
||||
ctx.font = '13px Segoe UI, sans-serif';
|
||||
ctx.fillText(yLabel, 8, 16);
|
||||
|
||||
const clientsAll = flattenClients();
|
||||
const valuesAll = flattenValues(metric);
|
||||
if (!clientsAll.length || !valuesAll.length) {{
|
||||
ctx.fillText('No data in summary.csv for this metric', x0 + 10, y0 - 10);
|
||||
return;
|
||||
}}
|
||||
|
||||
const minX = Math.min(...clientsAll);
|
||||
const maxX = Math.max(...clientsAll);
|
||||
const minY = 0;
|
||||
const maxY = Math.max(...valuesAll) * 1.1;
|
||||
|
||||
const x = v => x0 + ((v - minX) / Math.max(1, maxX - minX)) * (x1 - x0);
|
||||
const y = v => y0 - ((v - minY) / Math.max(1e-9, maxY - minY)) * (y0 - y1);
|
||||
|
||||
ctx.strokeStyle = '#dde4e4';
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i <= 5; i++) {{
|
||||
const yy = y0 - ((y0 - y1) * i / 5);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x0, yy);
|
||||
ctx.lineTo(x1, yy);
|
||||
ctx.stroke();
|
||||
const val = (maxY * i / 5).toFixed(1);
|
||||
ctx.fillStyle = '#4f5b5b';
|
||||
ctx.fillText(val, 16, yy + 4);
|
||||
}}
|
||||
|
||||
const xTicks = [...new Set(clientsAll)].sort((a, b) => a - b);
|
||||
xTicks.forEach(v => {{
|
||||
const xx = x(v);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(xx, y0);
|
||||
ctx.lineTo(xx, y0 + 5);
|
||||
ctx.strokeStyle = '#9da9a9';
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = '#4f5b5b';
|
||||
ctx.fillText(String(v), xx - 6, y0 + 18);
|
||||
}});
|
||||
|
||||
ctx.strokeStyle = '#182020';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x0, y0);
|
||||
ctx.lineTo(x1, y0);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x0, y0);
|
||||
ctx.lineTo(x0, y1);
|
||||
ctx.stroke();
|
||||
|
||||
const legend = document.getElementById('legend');
|
||||
if (metric === 'tps') legend.innerHTML = '';
|
||||
|
||||
Object.entries(series).forEach(([mode, items], idx) => {{
|
||||
const color = palette[idx % palette.length];
|
||||
const sorted = [...items]
|
||||
.filter(p => typeof p[metric] === 'number' && Number.isFinite(p[metric]))
|
||||
.sort((a, b) => a.clients - b.clients);
|
||||
|
||||
if (!sorted.length) return;
|
||||
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
sorted.forEach((p, i) => {{
|
||||
const px = x(p.clients);
|
||||
const py = y(p[metric]);
|
||||
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
|
||||
}});
|
||||
ctx.stroke();
|
||||
|
||||
sorted.forEach(p => {{
|
||||
const px = x(p.clients);
|
||||
const py = y(p[metric]);
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}});
|
||||
|
||||
if (metric === 'tps') {{
|
||||
const item = document.createElement('div');
|
||||
item.className = 'legend-item';
|
||||
item.innerHTML = `<span class=\"swatch\" style=\"background:${{color}}\"></span>${{mode}}`;
|
||||
legend.appendChild(item);
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
|
||||
drawChart('tpsChart', 'tps', 'TPS');
|
||||
drawChart('latencyChart', 'latency_ms', 'Latency (ms)');
|
||||
drawChart('p95Chart', 'p95_ms', 'p95 (ms)');
|
||||
drawChart('p99Chart', 'p99_ms', 'p99 (ms)');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
summary_path = Path(args.summary_csv).resolve()
|
||||
if not summary_path.is_file():
|
||||
raise FileNotFoundError(f"summary.csv not found: {summary_path}")
|
||||
|
||||
rows = load_rows(summary_path)
|
||||
if not rows:
|
||||
raise ValueError("summary.csv has no data rows")
|
||||
|
||||
series = build_series(rows)
|
||||
if not series:
|
||||
raise ValueError("No valid numeric rows found in summary.csv")
|
||||
|
||||
output_path = (
|
||||
Path(args.output).resolve()
|
||||
if args.output
|
||||
else summary_path.parent / "pgbench_report.html"
|
||||
)
|
||||
|
||||
title = f"pgbench report: {summary_path.parent.name}"
|
||||
output_path.write_text(html_report(title=title, data=series), encoding="utf-8")
|
||||
print(f"Report written: {output_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,102 @@
|
||||
# pgbench benchmark quickstart
|
||||
|
||||
This workspace now includes repeatable benchmark harnesses:
|
||||
|
||||
- `pgbench_benchmark.ps1` (Windows PowerShell)
|
||||
- `pgbench_benchmark.sh` (Bash)
|
||||
- `pgbench_custom_workload.sql` (sample custom SQL transaction mix)
|
||||
- `pgbench_export_report.py` (exports TPS/latency chart report from `summary.csv`)
|
||||
|
||||
## 1) Prerequisites
|
||||
|
||||
- PostgreSQL client tools installed (`psql`, `pgbench`)
|
||||
- Network access to your PostgreSQL server
|
||||
- Credentials available via environment variables (`PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`)
|
||||
|
||||
## 2) Run the benchmark (Windows PowerShell)
|
||||
|
||||
From this folder:
|
||||
|
||||
```powershell
|
||||
$env:PGPASSWORD = "your_password"
|
||||
.\pgbench_benchmark.ps1 -PgHost localhost -PgPort 5432 -PgUser postgres
|
||||
```
|
||||
|
||||
You can override defaults:
|
||||
|
||||
```powershell
|
||||
$env:PGPASSWORD = "your_password"
|
||||
.\pgbench_benchmark.ps1 -BenchDb pgbench -Scale 100 -Duration 120 -Clients "1 8 16 32 64" -Jobs 8 -Reinit $true
|
||||
```
|
||||
|
||||
Run with custom SQL workload mode enabled:
|
||||
|
||||
```powershell
|
||||
$env:PGPASSWORD = "your_password"
|
||||
.\pgbench_benchmark.ps1 -RunCustom $true -CustomSqlFile .\pgbench_custom_workload.sql -CustomModeName app_like -CustomReadPct 85 -CustomHotPct 92 -CustomTxnSize 3 -CustomHotAccounts 12000
|
||||
```
|
||||
|
||||
## 3) Optional Bash usage (WSL/Git Bash)
|
||||
|
||||
```bash
|
||||
chmod +x pgbench_benchmark.sh
|
||||
PGHOST=localhost PGPORT=5432 PGUSER=postgres PGPASSWORD=your_password ./pgbench_benchmark.sh
|
||||
```
|
||||
|
||||
Run with custom SQL workload mode enabled:
|
||||
|
||||
```bash
|
||||
PGHOST=localhost PGPORT=5432 PGUSER=postgres PGPASSWORD=your_password RUN_CUSTOM=1 CUSTOM_SQL_FILE=./pgbench_custom_workload.sql CUSTOM_MODE_NAME=app_like CUSTOM_READ_PCT=85 CUSTOM_HOT_PCT=92 CUSTOM_TXN_SIZE=3 CUSTOM_HOT_ACCOUNTS=12000 ./pgbench_benchmark.sh
|
||||
```
|
||||
|
||||
## 4) Optional tuning inputs (Bash script)
|
||||
|
||||
```bash
|
||||
BENCH_DB=pgbench SCALE=100 DURATION=120 CLIENTS="1 8 16 32 64" JOBS=8 REINIT=1 ./pgbench_benchmark.sh
|
||||
```
|
||||
|
||||
Environment variables:
|
||||
|
||||
- `BENCH_DB`: database name for benchmark data (default: `pgbench`)
|
||||
- `SCALE`: data size scale factor (default: `50`)
|
||||
- `DURATION`: measured run time per test case in seconds (default: `60`)
|
||||
- `CLIENTS`: space-separated client counts (default: `"1 4 8 16 32"`)
|
||||
- `JOBS`: worker threads for pgbench (default: `4`)
|
||||
- `WARMUP_SECONDS`: unmeasured warm-up before each test (default: `15`)
|
||||
- `RUN_READ_ONLY`: set to `0` to skip `-S` read-only tests (default: `1`)
|
||||
- `REINIT`: set to `1` to reinitialize data each run (default: `0`)
|
||||
- `RUN_CUSTOM`: set to `1` to run custom SQL workload mode (default: `0`)
|
||||
- `CUSTOM_SQL_FILE`: path to custom SQL file for `pgbench -f` (default: `pgbench_custom_workload.sql`)
|
||||
- `CUSTOM_MODE_NAME`: mode label written to `summary.csv` for custom runs (default: `custom_sql`)
|
||||
- `CUSTOM_READ_PCT` / `-CustomReadPct`: read share percentage for custom mode; writes happen in `100-read_pct` (default: `80`)
|
||||
- `CUSTOM_HOT_PCT` / `-CustomHotPct`: chance to pick from hot account set in custom mode (default: `90`)
|
||||
- `CUSTOM_TXN_SIZE` / `-CustomTxnSize`: custom transaction size tier from `1` to `4` (default: `2`)
|
||||
- `CUSTOM_HOT_ACCOUNTS` / `-CustomHotAccounts`: number of hot accounts near start of keyspace (default: `10000`)
|
||||
|
||||
## 5) Output
|
||||
|
||||
Each run creates:
|
||||
|
||||
- `pgbench_results_YYYYMMDD_HHMMSS/summary.csv`
|
||||
- Per-test raw output files in the same results directory
|
||||
|
||||
Use `summary.csv` to compare TPS and latency across runs.
|
||||
|
||||
`summary.csv` now includes percentile latency columns:
|
||||
|
||||
- `p95_ms`
|
||||
- `p99_ms`
|
||||
|
||||
## 6) Export chart report from summary.csv
|
||||
|
||||
Generate an HTML report with TPS and latency charts:
|
||||
|
||||
```powershell
|
||||
python .\pgbench_export_report.py .\pgbench_results_YYYYMMDD_HHMMSS\summary.csv
|
||||
```
|
||||
|
||||
Optional output path:
|
||||
|
||||
```powershell
|
||||
python .\pgbench_export_report.py .\pgbench_results_YYYYMMDD_HHMMSS\summary.csv -o .\pgbench_results_YYYYMMDD_HHMMSS\my_report.html
|
||||
```
|
||||
@@ -6,7 +6,7 @@ Script to read and parse Powerball numbers from a CSV file.
|
||||
import csv
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Dict
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
from collections import Counter
|
||||
|
||||
|
||||
@@ -85,7 +85,84 @@ def display_powerball_data(data: List[Dict]) -> None:
|
||||
print(f"{entry['date_str']:<12} {numbers_str:<30} {entry['multiplier']:<10}")
|
||||
|
||||
|
||||
def analyze_most_frequent_numbers(data: List[Dict]) -> None:
|
||||
def _get_unweighted_top_numbers(draws: List[Dict]) -> Tuple[List[int], Optional[int]]:
|
||||
"""Return top 5 white balls and top Powerball from a draw list."""
|
||||
if not draws:
|
||||
return [], None
|
||||
|
||||
white_counter = Counter()
|
||||
powerball_counter = Counter()
|
||||
|
||||
for entry in draws:
|
||||
numbers = entry['winning_numbers']
|
||||
for number in numbers[:5]:
|
||||
white_counter[number] += 1
|
||||
|
||||
if len(numbers) >= 6:
|
||||
powerball_counter[numbers[5]] += 1
|
||||
|
||||
top_white = [number for number, _ in white_counter.most_common(5)]
|
||||
top_powerball = powerball_counter.most_common(1)[0][0] if powerball_counter else None
|
||||
|
||||
return top_white, top_powerball
|
||||
|
||||
|
||||
def _get_recency_weighted_top_numbers(draws: List[Dict]) -> Tuple[List[int], Optional[int]]:
|
||||
"""Return top numbers weighted so newer draws contribute more."""
|
||||
if not draws:
|
||||
return [], None
|
||||
|
||||
sorted_draws = sorted(draws, key=lambda x: x['draw_date'])
|
||||
total_draws = len(sorted_draws)
|
||||
|
||||
white_scores = Counter()
|
||||
powerball_scores = Counter()
|
||||
|
||||
for index, entry in enumerate(sorted_draws, start=1):
|
||||
weight = index / total_draws
|
||||
numbers = entry['winning_numbers']
|
||||
|
||||
for number in numbers[:5]:
|
||||
white_scores[number] += weight
|
||||
|
||||
if len(numbers) >= 6:
|
||||
powerball_scores[numbers[5]] += weight
|
||||
|
||||
top_white = [number for number, _ in white_scores.most_common(5)]
|
||||
top_powerball = powerball_scores.most_common(1)[0][0] if powerball_scores else None
|
||||
|
||||
return top_white, top_powerball
|
||||
|
||||
|
||||
def _get_aggressive_recency_weighted_top_numbers(draws: List[Dict]) -> Tuple[List[int], Optional[int]]:
|
||||
"""Return top numbers with an aggressive recency curve (cubic weighting)."""
|
||||
if not draws:
|
||||
return [], None
|
||||
|
||||
sorted_draws = sorted(draws, key=lambda x: x['draw_date'])
|
||||
total_draws = len(sorted_draws)
|
||||
|
||||
white_scores = Counter()
|
||||
powerball_scores = Counter()
|
||||
|
||||
for index, entry in enumerate(sorted_draws, start=1):
|
||||
normalized_position = index / total_draws
|
||||
weight = normalized_position ** 3
|
||||
numbers = entry['winning_numbers']
|
||||
|
||||
for number in numbers[:5]:
|
||||
white_scores[number] += weight
|
||||
|
||||
if len(numbers) >= 6:
|
||||
powerball_scores[numbers[5]] += weight
|
||||
|
||||
top_white = [number for number, _ in white_scores.most_common(5)]
|
||||
top_powerball = powerball_scores.most_common(1)[0][0] if powerball_scores else None
|
||||
|
||||
return top_white, top_powerball
|
||||
|
||||
|
||||
def analyze_most_frequent_numbers(data: List[Dict]) -> Dict:
|
||||
"""
|
||||
Analyze and display the most frequent number in each slot position.
|
||||
|
||||
@@ -131,27 +208,51 @@ def analyze_most_frequent_numbers(data: List[Dict]) -> None:
|
||||
for i in range(min(5, num_positions)):
|
||||
first_five_combined.extend(position_numbers[i])
|
||||
|
||||
top_five_numbers = []
|
||||
top_ten_numbers = []
|
||||
if first_five_combined:
|
||||
counter_first_five = Counter(first_five_combined)
|
||||
top_five_numbers = counter_first_five.most_common(5)
|
||||
top_ten_numbers = counter_first_five.most_common(10)
|
||||
print("Top 5 most drawn numbers in first 5 positions combined:")
|
||||
for rank, (number, frequency) in enumerate(top_five_numbers, start=1):
|
||||
percentage = (frequency / len(first_five_combined)) * 100
|
||||
print(f" {rank}. {number:<3} (appears {frequency} times, {percentage:.1f}%)")
|
||||
|
||||
# Analyze the sixth position (Powerball)
|
||||
powerball_ranked = []
|
||||
most_common_sixth = None
|
||||
if num_positions >= 6:
|
||||
sixth_position_numbers = position_numbers[5]
|
||||
counter_sixth = Counter(sixth_position_numbers)
|
||||
powerball_ranked = counter_sixth.most_common(5)
|
||||
most_common_sixth, freq_sixth = counter_sixth.most_common(1)[0]
|
||||
percentage_sixth = (freq_sixth / len(sixth_position_numbers)) * 100
|
||||
print(f"\nMost drawn number in position 6 (Powerball): {most_common_sixth} (appears {freq_sixth} times, {percentage_sixth:.1f}%)")
|
||||
|
||||
# Third strategy: most frequent numbers from the most recent 104 draws
|
||||
sorted_by_date = sorted(data, key=lambda x: x['draw_date'])
|
||||
recent_104_draws = sorted_by_date[-104:]
|
||||
recent_104_top_five, recent_104_powerball = _get_unweighted_top_numbers(recent_104_draws)
|
||||
|
||||
# Fourth strategy: weighted-by-recency across all draws
|
||||
weighted_top_five, weighted_powerball = _get_recency_weighted_top_numbers(data)
|
||||
|
||||
# Fifth strategy: aggressive weighted-by-recency across all draws
|
||||
aggressive_weighted_top_five, aggressive_weighted_powerball = _get_aggressive_recency_weighted_top_numbers(data)
|
||||
|
||||
# Return statistics for prediction
|
||||
return {
|
||||
'top_five_combined': [num for num, freq in top_five_numbers] if first_five_combined else [],
|
||||
'most_common_powerball': most_common_sixth
|
||||
'top_ten_combined': [num for num, freq in top_ten_numbers],
|
||||
'most_common_powerball': most_common_sixth,
|
||||
'powerball_ranked': [num for num, freq in powerball_ranked],
|
||||
'recent_104_top_five': recent_104_top_five,
|
||||
'recent_104_powerball': recent_104_powerball,
|
||||
'weighted_top_five': weighted_top_five,
|
||||
'weighted_powerball': weighted_powerball,
|
||||
'aggressive_weighted_top_five': aggressive_weighted_top_five,
|
||||
'aggressive_weighted_powerball': aggressive_weighted_powerball
|
||||
}
|
||||
|
||||
|
||||
@@ -171,20 +272,80 @@ def generate_suggested_combination(stats: dict) -> None:
|
||||
print("Each draw is random and independent.\n")
|
||||
|
||||
top_five = stats['top_five_combined']
|
||||
top_ten = stats.get('top_ten_combined', [])
|
||||
powerball = stats['most_common_powerball']
|
||||
ranked_powerballs = stats.get('powerball_ranked', [])
|
||||
recent_104_top_five = stats.get('recent_104_top_five', [])
|
||||
recent_104_powerball = stats.get('recent_104_powerball')
|
||||
weighted_top_five = stats.get('weighted_top_five', [])
|
||||
weighted_powerball = stats.get('weighted_powerball')
|
||||
aggressive_weighted_top_five = stats.get('aggressive_weighted_top_five', [])
|
||||
aggressive_weighted_powerball = stats.get('aggressive_weighted_powerball')
|
||||
|
||||
if len(top_five) >= 5 and powerball:
|
||||
# Use the top 5 most frequent numbers from positions 1-5
|
||||
suggested_numbers = sorted(top_five[:5])
|
||||
primary_numbers = sorted(top_five[:5])
|
||||
primary_powerball = powerball
|
||||
|
||||
# Build a secondary set from the next-most-frequent historical numbers.
|
||||
secondary_pool = top_ten[5:10]
|
||||
if len(secondary_pool) < 5:
|
||||
secondary_pool = top_ten[:5]
|
||||
|
||||
secondary_numbers = sorted(secondary_pool[:5])
|
||||
secondary_powerball = ranked_powerballs[1] if len(ranked_powerballs) > 1 else primary_powerball
|
||||
|
||||
third_numbers = sorted(recent_104_top_five[:5]) if len(recent_104_top_five) >= 5 else []
|
||||
third_powerball = recent_104_powerball
|
||||
|
||||
fourth_numbers = sorted(weighted_top_five[:5]) if len(weighted_top_five) >= 5 else []
|
||||
fourth_powerball = weighted_powerball
|
||||
|
||||
fifth_numbers = sorted(aggressive_weighted_top_five[:5]) if len(aggressive_weighted_top_five) >= 5 else []
|
||||
fifth_powerball = aggressive_weighted_powerball
|
||||
|
||||
print(f"Suggested white ball numbers: {' '.join(map(str, suggested_numbers))}")
|
||||
print(f"Suggested Powerball number: {powerball}")
|
||||
print("Primary set (most frequent):")
|
||||
print(f" White balls: {' '.join(map(str, primary_numbers))}")
|
||||
print(f" Powerball: {primary_powerball}")
|
||||
|
||||
print("\nSecondary set (next-most frequent):")
|
||||
print(f" White balls: {' '.join(map(str, secondary_numbers))}")
|
||||
print(f" Powerball: {secondary_powerball}")
|
||||
|
||||
if third_numbers and third_powerball:
|
||||
print("\nThird set (most frequent in recent 104 draws):")
|
||||
print(f" White balls: {' '.join(map(str, third_numbers))}")
|
||||
print(f" Powerball: {third_powerball}")
|
||||
|
||||
if fourth_numbers and fourth_powerball:
|
||||
print("\nFourth set (weighted by recency):")
|
||||
print(f" White balls: {' '.join(map(str, fourth_numbers))}")
|
||||
print(f" Powerball: {fourth_powerball}")
|
||||
|
||||
if fifth_numbers and fifth_powerball:
|
||||
print("\nFifth set (aggressive recency weighting):")
|
||||
print(f" White balls: {' '.join(map(str, fifth_numbers))}")
|
||||
print(f" Powerball: {fifth_powerball}")
|
||||
print()
|
||||
print(f"Complete combination: {' '.join(map(str, suggested_numbers))} + {powerball}")
|
||||
print(f"Primary combination: {' '.join(map(str, primary_numbers))} + {primary_powerball}")
|
||||
print(f"Secondary combination: {' '.join(map(str, secondary_numbers))} + {secondary_powerball}")
|
||||
|
||||
if third_numbers and third_powerball:
|
||||
print(f"Third combination: {' '.join(map(str, third_numbers))} + {third_powerball}")
|
||||
|
||||
if fourth_numbers and fourth_powerball:
|
||||
print(f"Fourth combination: {' '.join(map(str, fourth_numbers))} + {fourth_powerball}")
|
||||
|
||||
if fifth_numbers and fifth_powerball:
|
||||
print(f"Fifth combination: {' '.join(map(str, fifth_numbers))} + {fifth_powerball}")
|
||||
print()
|
||||
print("This combination is based on:")
|
||||
print("These combinations are based on:")
|
||||
print(" • The 5 most frequently drawn numbers across all white ball positions")
|
||||
print(f" • The most frequently drawn Powerball number ({powerball})")
|
||||
print(f" • The most frequently drawn Powerball number ({primary_powerball})")
|
||||
print(" • A secondary set from the next-most-frequent white ball and Powerball values")
|
||||
print(" • Recent-window frequency (last 104 draws)")
|
||||
print(" • Recency-weighted scoring where newer draws count more")
|
||||
print(" • Aggressive recency weighting using a cubic curve")
|
||||
else:
|
||||
print("Insufficient data to generate a suggestion.")
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import csv
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def collect_first_tokens_from_full_table_files(directory: Path) -> list[str]:
|
||||
first_tokens: list[str] = []
|
||||
|
||||
for file_path in directory.iterdir():
|
||||
# Match file names like "Full_Table_List_..." regardless of case.
|
||||
if file_path.is_file() and file_path.name.lower().startswith("full_table_list"):
|
||||
with file_path.open("r", encoding="utf-8", errors="ignore") as f:
|
||||
for line in f:
|
||||
parts = line.strip().split()
|
||||
if parts:
|
||||
first_tokens.append(parts[0])
|
||||
|
||||
return first_tokens
|
||||
|
||||
|
||||
def main() -> None:
|
||||
current_directory = Path.cwd()
|
||||
tokens = collect_first_tokens_from_full_table_files(current_directory)
|
||||
tokens.sort()
|
||||
|
||||
for token in tokens:
|
||||
print(token)
|
||||
|
||||
output_file = current_directory / "full_table_list_tokens.csv"
|
||||
with output_file.open("w", newline="", encoding="utf-8") as csv_file:
|
||||
writer = csv.writer(csv_file)
|
||||
writer.writerow(["value"])
|
||||
for token in tokens:
|
||||
writer.writerow([token])
|
||||
|
||||
print(f"\nTotal extracted values: {len(tokens)}")
|
||||
print(f"Saved CSV file: {output_file}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user