pg-perf-bench 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pg_perf_bench/__init__.py +3 -0
- pg_perf_bench/__main__.py +4 -0
- pg_perf_bench/benchmark.py +646 -0
- pg_perf_bench/cli.py +820 -0
- pg_perf_bench/client_tools.py +135 -0
- pg_perf_bench/collect_info.py +180 -0
- pg_perf_bench/commands/bash_commands/cpu_info.sh +2 -0
- pg_perf_bench/commands/bash_commands/df_h.sh +3 -0
- pg_perf_bench/commands/bash_commands/etc_fstab.sh +3 -0
- pg_perf_bench/commands/bash_commands/etc_os_release.sh +3 -0
- pg_perf_bench/commands/bash_commands/ip_br_addr.sh +4 -0
- pg_perf_bench/commands/bash_commands/lshw_bridge.sh +1 -0
- pg_perf_bench/commands/bash_commands/lshw_bus.sh +1 -0
- pg_perf_bench/commands/bash_commands/lshw_communication.sh +1 -0
- pg_perf_bench/commands/bash_commands/lshw_disk.sh +1 -0
- pg_perf_bench/commands/bash_commands/lshw_display.sh +1 -0
- pg_perf_bench/commands/bash_commands/lshw_generic.sh +1 -0
- pg_perf_bench/commands/bash_commands/lshw_input.sh +1 -0
- pg_perf_bench/commands/bash_commands/lshw_memory.sh +1 -0
- pg_perf_bench/commands/bash_commands/lshw_multimedia.sh +1 -0
- pg_perf_bench/commands/bash_commands/lshw_network.sh +1 -0
- pg_perf_bench/commands/bash_commands/lshw_power.sh +1 -0
- pg_perf_bench/commands/bash_commands/lshw_processor.sh +1 -0
- pg_perf_bench/commands/bash_commands/lshw_storage.sh +1 -0
- pg_perf_bench/commands/bash_commands/lshw_system.sh +1 -0
- pg_perf_bench/commands/bash_commands/lshw_volume.sh +1 -0
- pg_perf_bench/commands/bash_commands/mount.sh +3 -0
- pg_perf_bench/commands/bash_commands/pg_config.sh +2 -0
- pg_perf_bench/commands/bash_commands/sys_memory_total.sh +6 -0
- pg_perf_bench/commands/bash_commands/sysctl_net_ipv4_tcp.sh +1 -0
- pg_perf_bench/commands/bash_commands/sysctl_net_ipv4_udp.sh +1 -0
- pg_perf_bench/commands/bash_commands/sysctl_vm.sh +2 -0
- pg_perf_bench/commands/bash_commands/total_ram.sh +2 -0
- pg_perf_bench/commands/bash_commands/uname_a.sh +3 -0
- pg_perf_bench/commands/sql_commands/available_server_extensions.sql +1 -0
- pg_perf_bench/commands/sql_commands/full_version.sql +1 -0
- pg_perf_bench/commands/sql_commands/pg_settings.sql +1 -0
- pg_perf_bench/commands/sql_commands/server_version.sql +1 -0
- pg_perf_bench/commands/sql_commands/server_version_major.sql +1 -0
- pg_perf_bench/config.py +418 -0
- pg_perf_bench/connections/__init__.py +11 -0
- pg_perf_bench/connections/common.py +13 -0
- pg_perf_bench/connections/docker.py +271 -0
- pg_perf_bench/connections/local.py +115 -0
- pg_perf_bench/connections/ssh.py +179 -0
- pg_perf_bench/const.py +102 -0
- pg_perf_bench/context/__init__.py +5 -0
- pg_perf_bench/context/base_context.py +79 -0
- pg_perf_bench/context/benchmark.py +157 -0
- pg_perf_bench/context/collect_info.py +125 -0
- pg_perf_bench/context/join.py +14 -0
- pg_perf_bench/contracts.py +121 -0
- pg_perf_bench/db_operations/__init__.py +81 -0
- pg_perf_bench/db_operations/conn_tasks/__init__.py +12 -0
- pg_perf_bench/db_operations/conn_tasks/common.py +48 -0
- pg_perf_bench/db_operations/conn_tasks/docker.py +29 -0
- pg_perf_bench/db_operations/conn_tasks/local.py +35 -0
- pg_perf_bench/db_operations/conn_tasks/ssh.py +32 -0
- pg_perf_bench/db_operations/db.py +124 -0
- pg_perf_bench/errors.py +61 -0
- pg_perf_bench/executors/__init__.py +5 -0
- pg_perf_bench/executors/process.py +156 -0
- pg_perf_bench/join.py +581 -0
- pg_perf_bench/join_catalog.py +134 -0
- pg_perf_bench/join_tasks/README.md +29 -0
- pg_perf_bench/join_tasks/compare-postgresql-major/README.md +19 -0
- pg_perf_bench/join_tasks/compare-postgresql-major/task.json +18 -0
- pg_perf_bench/join_tasks/compare-storage/README.md +19 -0
- pg_perf_bench/join_tasks/compare-storage/task.json +20 -0
- pg_perf_bench/join_tasks/optimize-db-config/README.md +22 -0
- pg_perf_bench/join_tasks/optimize-db-config/task.json +14 -0
- pg_perf_bench/join_tasks/repeatability/README.md +17 -0
- pg_perf_bench/join_tasks/repeatability/task.json +15 -0
- pg_perf_bench/join_tasks/scale-cpu/README.md +19 -0
- pg_perf_bench/join_tasks/scale-cpu/task.json +20 -0
- pg_perf_bench/join_tasks/scale-memory/README.md +18 -0
- pg_perf_bench/join_tasks/scale-memory/task.json +20 -0
- pg_perf_bench/join_tasks/tune-os-kernel/README.md +17 -0
- pg_perf_bench/join_tasks/tune-os-kernel/task.json +20 -0
- pg_perf_bench/log.py +74 -0
- pg_perf_bench/orchestration.py +135 -0
- pg_perf_bench/report/__init__.py +11 -0
- pg_perf_bench/report/commands.py +546 -0
- pg_perf_bench/report/html.py +112 -0
- pg_perf_bench/report/processing.py +146 -0
- pg_perf_bench/run.py +36 -0
- pg_perf_bench/system_metrics.py +218 -0
- pg_perf_bench/templates/all_info_report_struct.json +274 -0
- pg_perf_bench/templates/benchmark_report_struct.json +377 -0
- pg_perf_bench/templates/db_info_report_struct.json +61 -0
- pg_perf_bench/templates/report.html +806 -0
- pg_perf_bench/templates/sys_info_report_struct.json +220 -0
- pg_perf_bench/templates/vendor/THIRD_PARTY_LICENSES.txt +15 -0
- pg_perf_bench/templates/vendor/echarts-6.1.0.LICENSE-d3.txt +27 -0
- pg_perf_bench/templates/vendor/echarts-6.1.0.LICENSE.txt +222 -0
- pg_perf_bench/templates/vendor/echarts-6.1.0.NOTICE.txt +5 -0
- pg_perf_bench/templates/vendor/echarts-6.1.0.min.js +45 -0
- pg_perf_bench/templates/vendor/highlight-11.11.1.LICENSE.txt +29 -0
- pg_perf_bench/templates/vendor/highlight-11.11.1.min.js +1244 -0
- pg_perf_bench/templates/vendor/highlight-github-dark-11.11.1.min.css +10 -0
- pg_perf_bench/validator.py +55 -0
- pg_perf_bench/workload_profiles/README.md +16 -0
- pg_perf_bench/workload_profiles/imdb/README.md +18 -0
- pg_perf_bench/workload_profiles/imdb/generator.py +127 -0
- pg_perf_bench/workload_profiles/imdb/profile.json +25 -0
- pg_perf_bench/workload_profiles/imdb/sql/01_company_catalog.sql +16 -0
- pg_perf_bench/workload_profiles/imdb/sql/02_people_by_keyword.sql +18 -0
- pg_perf_bench/workload_profiles/imdb/sql/03_keyword_trends.sql +14 -0
- pg_perf_bench/workload_profiles/imdb/sql/04_genre_cast.sql +14 -0
- pg_perf_bench/workload_profiles/imdb/sql/05_join_stress.sql +22 -0
- pg_perf_bench/workload_profiles/imdb/sql/indexes.sql +20 -0
- pg_perf_bench/workload_profiles/imdb/sql/schema.sql +69 -0
- pg_perf_bench/workload_profiles/pagila/README.md +17 -0
- pg_perf_bench/workload_profiles/pagila/generator.py +203 -0
- pg_perf_bench/workload_profiles/pagila/profile.json +23 -0
- pg_perf_bench/workload_profiles/pagila/sql/01_select.sql +264 -0
- pg_perf_bench/workload_profiles/pagila/sql/02_insert.sql +273 -0
- pg_perf_bench/workload_profiles/pagila/sql/03_update.sql +149 -0
- pg_perf_bench/workload_profiles/pagila/sql/04_delete.sql +27 -0
- pg_perf_bench/workload_profiles/pagila/sql/pagila-schema.sql +1688 -0
- pg_perf_bench/workloads.py +297 -0
- pg_perf_bench-0.2.0.dist-info/METADATA +530 -0
- pg_perf_bench-0.2.0.dist-info/RECORD +133 -0
- pg_perf_bench-0.2.0.dist-info/WHEEL +5 -0
- pg_perf_bench-0.2.0.dist-info/entry_points.txt +2 -0
- pg_perf_bench-0.2.0.dist-info/licenses/LICENSE +21 -0
- pg_perf_bench-0.2.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +37 -0
- pg_perf_bench-0.2.0.dist-info/licenses/src/pg_perf_bench/templates/vendor/THIRD_PARTY_LICENSES.txt +15 -0
- pg_perf_bench-0.2.0.dist-info/licenses/src/pg_perf_bench/templates/vendor/echarts-6.1.0.LICENSE-d3.txt +27 -0
- pg_perf_bench-0.2.0.dist-info/licenses/src/pg_perf_bench/templates/vendor/echarts-6.1.0.LICENSE.txt +222 -0
- pg_perf_bench-0.2.0.dist-info/licenses/src/pg_perf_bench/templates/vendor/echarts-6.1.0.NOTICE.txt +5 -0
- pg_perf_bench-0.2.0.dist-info/licenses/src/pg_perf_bench/templates/vendor/highlight-11.11.1.LICENSE.txt +29 -0
- pg_perf_bench-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import asyncpg
|
|
11
|
+
|
|
12
|
+
from pg_perf_bench import __version__
|
|
13
|
+
from pg_perf_bench.client_tools import (
|
|
14
|
+
SUPPORTED_SERVER_MAJORS,
|
|
15
|
+
select_local_clients,
|
|
16
|
+
server_major_from_version_num,
|
|
17
|
+
)
|
|
18
|
+
from pg_perf_bench.connections import get_connection
|
|
19
|
+
from pg_perf_bench.const import (
|
|
20
|
+
BENCHMARK_TEMPLATE_JSON_PATH,
|
|
21
|
+
WorkMode,
|
|
22
|
+
get_datetime_report,
|
|
23
|
+
get_default_report_name,
|
|
24
|
+
)
|
|
25
|
+
from pg_perf_bench.contracts import ARTIFACT_SCHEMA_VERSION, canonical_hash, file_hash
|
|
26
|
+
from pg_perf_bench.db_operations import (
|
|
27
|
+
DBTasks,
|
|
28
|
+
collect_db_logs,
|
|
29
|
+
get_conn_type_tasks,
|
|
30
|
+
run_command_result,
|
|
31
|
+
)
|
|
32
|
+
from pg_perf_bench.errors import CollectionError
|
|
33
|
+
from pg_perf_bench.log import display_user_configuration
|
|
34
|
+
from pg_perf_bench.report.commands import fill_info_report
|
|
35
|
+
from pg_perf_bench.report.processing import get_report_structure
|
|
36
|
+
from pg_perf_bench.system_metrics import (
|
|
37
|
+
build_system_metrics_section,
|
|
38
|
+
collect_system_metrics,
|
|
39
|
+
infer_pgbench_duration,
|
|
40
|
+
)
|
|
41
|
+
from pg_perf_bench.workloads import build_workload_evidence
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BenchmarkRunner:
|
|
45
|
+
"""
|
|
46
|
+
A stateless utility class that encapsulates all steps for running
|
|
47
|
+
PostgreSQL performance benchmarks and collecting metrics.
|
|
48
|
+
All methods are static since they share no internal state.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def maximum_tps(benchmark_runs: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
53
|
+
"""Return the complete winning point from a client/load sweep."""
|
|
54
|
+
candidates = [
|
|
55
|
+
run
|
|
56
|
+
for run in benchmark_runs
|
|
57
|
+
if isinstance(run.get('metrics'), dict)
|
|
58
|
+
and isinstance(run['metrics'].get('tps'), (int, float))
|
|
59
|
+
and not isinstance(run['metrics'].get('tps'), bool)
|
|
60
|
+
]
|
|
61
|
+
if not candidates:
|
|
62
|
+
return None
|
|
63
|
+
best = max(candidates, key=lambda run: float(run['metrics']['tps']))
|
|
64
|
+
return {
|
|
65
|
+
'tps': best['metrics']['tps'],
|
|
66
|
+
'iteration': deepcopy(best.get('iteration')),
|
|
67
|
+
'metrics': deepcopy(best['metrics']),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def environment_evidence(report: dict[str, Any]) -> dict[str, Any]:
|
|
72
|
+
"""Build stable dimension hashes without volatile usage counters."""
|
|
73
|
+
system_reports = report['sections']['system']['reports']
|
|
74
|
+
|
|
75
|
+
def stable_value(item_name: str) -> Any:
|
|
76
|
+
value = deepcopy(system_reports[item_name].get('data'))
|
|
77
|
+
if item_name == 'ip_br_addr' and isinstance(value, str):
|
|
78
|
+
return re.sub(r'@if\d+', '@if*', value)
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
dimension_items = {
|
|
82
|
+
'kernel_os': (
|
|
83
|
+
'uname_a',
|
|
84
|
+
'etc_os_release',
|
|
85
|
+
'sysctl_vm',
|
|
86
|
+
'sysctl_net_ipv4_tcp',
|
|
87
|
+
'sysctl_net_ipv4_udp',
|
|
88
|
+
),
|
|
89
|
+
# ``lshw_processor`` includes the instantaneous CPU clock. It is
|
|
90
|
+
# useful raw evidence, but it is not hardware identity: frequency
|
|
91
|
+
# changes naturally with load and power management between runs.
|
|
92
|
+
'cpu': ('cpu_info',),
|
|
93
|
+
'memory_capacity': ('total_ram', 'lshw_memory'),
|
|
94
|
+
'storage_hardware': ('lshw_storage', 'lshw_disk', 'lshw_volume'),
|
|
95
|
+
# Interface addresses and Docker bridge/veth names are runtime
|
|
96
|
+
# topology, not hardware identity. The raw ``ip -br addr`` output
|
|
97
|
+
# remains in the report for inspection.
|
|
98
|
+
'network_hardware': ('lshw_network',),
|
|
99
|
+
}
|
|
100
|
+
dimensions = {
|
|
101
|
+
name: {
|
|
102
|
+
'hash': canonical_hash(
|
|
103
|
+
{item_name: stable_value(item_name) for item_name in item_names}
|
|
104
|
+
),
|
|
105
|
+
'items': list(item_names),
|
|
106
|
+
}
|
|
107
|
+
for name, item_names in dimension_items.items()
|
|
108
|
+
}
|
|
109
|
+
compatibility = report.get('postgresql_compatibility') or {}
|
|
110
|
+
load_generator = compatibility.get('load_generator') or {}
|
|
111
|
+
first_run = next(iter(report.get('benchmark_runs') or []), {})
|
|
112
|
+
collection_scope = (first_run.get('system_metrics') or {}).get('collection_scope')
|
|
113
|
+
identity = {
|
|
114
|
+
'dimensions': {name: item['hash'] for name, item in dimensions.items()},
|
|
115
|
+
'load_generator': load_generator,
|
|
116
|
+
'system_metrics_collection_scope': collection_scope,
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
'schema_version': 'pg_perf_bench/environment-evidence-v1',
|
|
120
|
+
'identity_hash': canonical_hash(identity),
|
|
121
|
+
'dimensions': dimensions,
|
|
122
|
+
'load_generator_hash': canonical_hash(load_generator),
|
|
123
|
+
'system_metrics_collection_scope': collection_scope,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def get_pgbench_results(pgbench_output: str) -> list[int | float]:
|
|
128
|
+
"""
|
|
129
|
+
Extracts key performance metrics from the pgbench output string.
|
|
130
|
+
Returns a list of metrics in the following order:
|
|
131
|
+
[clients, duration, transactions, latency_avg, init_conn_time, tps].
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def get_val(iter_matches, val_type: str) -> int | float | None:
|
|
135
|
+
for match_obj in iter_matches:
|
|
136
|
+
sub_str = pgbench_output[match_obj.span()[0] : match_obj.span()[1]]
|
|
137
|
+
val_iter = re.finditer(r'\d+([.,]\d+)?', sub_str)
|
|
138
|
+
for vv in val_iter:
|
|
139
|
+
numeric_str = sub_str[vv.span()[0] : vv.span()[1]]
|
|
140
|
+
numeric_str = numeric_str.replace(',', '.')
|
|
141
|
+
if val_type == 'float':
|
|
142
|
+
return float(numeric_str)
|
|
143
|
+
elif val_type == 'int':
|
|
144
|
+
return int(numeric_str)
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
clients = get_val(re.finditer(r'number\sof\sclients\:\s(\d+)', pgbench_output), 'int')
|
|
148
|
+
duration = get_val(re.finditer(r'duration\:\s(\d+)', pgbench_output), 'int')
|
|
149
|
+
transactions = get_val(
|
|
150
|
+
re.finditer(
|
|
151
|
+
r'number\sof\stransactions\sactually\sprocessed\:\s((\d+)/\d+|\d+)',
|
|
152
|
+
pgbench_output,
|
|
153
|
+
),
|
|
154
|
+
'int',
|
|
155
|
+
)
|
|
156
|
+
latency_avg = get_val(
|
|
157
|
+
re.finditer(r'latency\saverage\s=\s\d+(?:[.,]\d+)?\sms', pgbench_output),
|
|
158
|
+
'float',
|
|
159
|
+
)
|
|
160
|
+
init_conn_time = get_val(
|
|
161
|
+
re.finditer(
|
|
162
|
+
r'initial\sconnection\stime\s=\s\d+(?:[.,]\d+)?\sms',
|
|
163
|
+
pgbench_output,
|
|
164
|
+
),
|
|
165
|
+
'float',
|
|
166
|
+
)
|
|
167
|
+
tps = get_val(re.finditer(r'tps\s=\s\d+(?:[.,]\d+)?', pgbench_output), 'float')
|
|
168
|
+
|
|
169
|
+
return [
|
|
170
|
+
clients,
|
|
171
|
+
duration,
|
|
172
|
+
transactions,
|
|
173
|
+
latency_avg,
|
|
174
|
+
init_conn_time,
|
|
175
|
+
tps,
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def get_filled_load_commands(
|
|
180
|
+
db_conf: dict,
|
|
181
|
+
workload_conf: dict,
|
|
182
|
+
pgbench_param: str,
|
|
183
|
+
iter_amount: Any,
|
|
184
|
+
) -> list[str]:
|
|
185
|
+
"""
|
|
186
|
+
Replaces placeholders (ARG_*) in the init_command and workload_command
|
|
187
|
+
with actual config values and iteration-specific parameter.
|
|
188
|
+
"""
|
|
189
|
+
arg_values = {**db_conf, **workload_conf, pgbench_param: iter_amount}
|
|
190
|
+
init_command = workload_conf['init_command']
|
|
191
|
+
workload_command = workload_conf['workload_command']
|
|
192
|
+
|
|
193
|
+
for key, value in arg_values.items():
|
|
194
|
+
if isinstance(key, str):
|
|
195
|
+
placeholder = f'ARG_{key.upper()}'
|
|
196
|
+
init_command = init_command.replace(placeholder, str(value))
|
|
197
|
+
workload_command = workload_command.replace(placeholder, str(value))
|
|
198
|
+
|
|
199
|
+
unresolved = sorted(
|
|
200
|
+
set(re.findall(r'ARG_[A-Z][A-Z0-9_]*', init_command + '\n' + workload_command))
|
|
201
|
+
)
|
|
202
|
+
if unresolved:
|
|
203
|
+
raise ValueError('Unresolved workload placeholders: ' + ', '.join(unresolved))
|
|
204
|
+
|
|
205
|
+
return [init_command, workload_command]
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def load_iterations_config(db_conf: dict, workload_conf: dict) -> list[list[str]]:
|
|
209
|
+
"""
|
|
210
|
+
Builds a list of [init_command, workload_command] pairs for each iteration.
|
|
211
|
+
"""
|
|
212
|
+
db_conf_pg = {f'pg_{k}': v for k, v in db_conf.items()}
|
|
213
|
+
|
|
214
|
+
pgbench_param_name = workload_conf.get('pgbench_iter_name')
|
|
215
|
+
iter_list = workload_conf.get('pgbench_iter_list')
|
|
216
|
+
if (
|
|
217
|
+
not workload_conf
|
|
218
|
+
or not isinstance(workload_conf, dict)
|
|
219
|
+
or not pgbench_param_name
|
|
220
|
+
or not iter_list
|
|
221
|
+
or not isinstance(iter_list, list)
|
|
222
|
+
or 'init_command' not in workload_conf
|
|
223
|
+
or 'workload_command' not in workload_conf
|
|
224
|
+
):
|
|
225
|
+
return []
|
|
226
|
+
|
|
227
|
+
return [
|
|
228
|
+
BenchmarkRunner.get_filled_load_commands(
|
|
229
|
+
db_conf_pg, workload_conf, pgbench_param_name, iteration
|
|
230
|
+
)
|
|
231
|
+
for iteration in iter_list
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
async def reset_db_environment(
|
|
236
|
+
logger, conn_type: str, conn, db_conf: dict, workload_conf: dict
|
|
237
|
+
) -> None:
|
|
238
|
+
"""
|
|
239
|
+
Fully resets the database environment before a test iteration.
|
|
240
|
+
"""
|
|
241
|
+
if not workload_conf.get('allow_database_reset'):
|
|
242
|
+
raise CollectionError(
|
|
243
|
+
'Database reset was not explicitly confirmed for this benchmark run'
|
|
244
|
+
)
|
|
245
|
+
try:
|
|
246
|
+
db_tasks = DBTasks(db_conf, logger)
|
|
247
|
+
conn_tasks = get_conn_type_tasks(conn_type)(
|
|
248
|
+
db_conf=workload_conf, conn=conn, logger=logger
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
await conn_tasks.start_db()
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.warning(str(e))
|
|
255
|
+
|
|
256
|
+
await db_tasks.check_db_access()
|
|
257
|
+
await db_tasks.drop_db()
|
|
258
|
+
await conn_tasks.stop_db()
|
|
259
|
+
await conn_tasks.sync()
|
|
260
|
+
if workload_conf.get('drop_os_caches'):
|
|
261
|
+
await conn_tasks.drop_caches()
|
|
262
|
+
await conn_tasks.start_db()
|
|
263
|
+
await db_tasks.check_db_access()
|
|
264
|
+
await db_tasks.init_db()
|
|
265
|
+
await db_tasks.check_user_db_access()
|
|
266
|
+
|
|
267
|
+
except Exception as e:
|
|
268
|
+
raise RuntimeError(f'Failed to reset DB environment:\n{str(e)}') from e
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
async def run_benchmark(logger, load_iteration: list[str]) -> list[Any]:
|
|
272
|
+
"""
|
|
273
|
+
Runs a benchmark iteration and returns the parsed pgbench metrics.
|
|
274
|
+
"""
|
|
275
|
+
init_cmd, workload_cmd = load_iteration
|
|
276
|
+
|
|
277
|
+
logger.info(f'Executing init_command:\n {init_cmd}')
|
|
278
|
+
await run_command_result(logger, init_cmd, check=True)
|
|
279
|
+
|
|
280
|
+
logger.info(f'Executing workload_command:\n {workload_cmd}')
|
|
281
|
+
workload_result = await run_command_result(
|
|
282
|
+
logger,
|
|
283
|
+
workload_cmd,
|
|
284
|
+
check=True,
|
|
285
|
+
)
|
|
286
|
+
perf_result = workload_result.stdout
|
|
287
|
+
|
|
288
|
+
if not perf_result.strip():
|
|
289
|
+
logger.warning('Workload command returned an empty or whitespace-only result.')
|
|
290
|
+
else:
|
|
291
|
+
logger.debug(f'Result of pgbench iteration:\n{perf_result}')
|
|
292
|
+
metrics = BenchmarkRunner.get_pgbench_results(perf_result)
|
|
293
|
+
if metrics[5] is None:
|
|
294
|
+
raise CollectionError(
|
|
295
|
+
'pgbench completed but TPS could not be parsed; raw output is preserved'
|
|
296
|
+
)
|
|
297
|
+
return metrics
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
async def run_benchmark_with_evidence(
|
|
301
|
+
logger,
|
|
302
|
+
load_iteration: list[str],
|
|
303
|
+
*,
|
|
304
|
+
db_conf: dict[str, Any],
|
|
305
|
+
command_timeout: float,
|
|
306
|
+
connection_type: str = 'local',
|
|
307
|
+
connection: Any = None,
|
|
308
|
+
system_metrics_interval: float = 1.0,
|
|
309
|
+
system_metrics_duration: float | None = None,
|
|
310
|
+
) -> dict[str, Any]:
|
|
311
|
+
init_cmd, workload_cmd = load_iteration
|
|
312
|
+
environment = os.environ.copy()
|
|
313
|
+
environment.update(
|
|
314
|
+
{
|
|
315
|
+
'PGHOST': str(db_conf.get('host', '')),
|
|
316
|
+
'PGPORT': str(db_conf.get('port', '')),
|
|
317
|
+
'PGUSER': str(db_conf.get('user', '')),
|
|
318
|
+
'PGDATABASE': str(db_conf.get('database', '')),
|
|
319
|
+
}
|
|
320
|
+
)
|
|
321
|
+
password = db_conf.get('password')
|
|
322
|
+
if password:
|
|
323
|
+
environment['PGPASSWORD'] = str(password)
|
|
324
|
+
secrets = (str(password) if password else None,)
|
|
325
|
+
logger.info('Executing benchmark initialization command.')
|
|
326
|
+
init_result = await run_command_result(
|
|
327
|
+
logger,
|
|
328
|
+
init_cmd,
|
|
329
|
+
check=True,
|
|
330
|
+
timeout=command_timeout,
|
|
331
|
+
env=environment,
|
|
332
|
+
secrets=secrets,
|
|
333
|
+
)
|
|
334
|
+
logger.info('Executing pgbench workload command.')
|
|
335
|
+
sampler_task = None
|
|
336
|
+
if connection is not None:
|
|
337
|
+
sampling_duration = infer_pgbench_duration(
|
|
338
|
+
workload_cmd,
|
|
339
|
+
system_metrics_duration,
|
|
340
|
+
)
|
|
341
|
+
sampler_task = asyncio.create_task(
|
|
342
|
+
collect_system_metrics(
|
|
343
|
+
connection_type=connection_type,
|
|
344
|
+
connection=connection,
|
|
345
|
+
duration_seconds=sampling_duration,
|
|
346
|
+
interval_seconds=system_metrics_interval,
|
|
347
|
+
),
|
|
348
|
+
name='pg-perf-bench:system-metrics',
|
|
349
|
+
)
|
|
350
|
+
try:
|
|
351
|
+
workload_result = await run_command_result(
|
|
352
|
+
logger,
|
|
353
|
+
workload_cmd,
|
|
354
|
+
check=True,
|
|
355
|
+
timeout=command_timeout,
|
|
356
|
+
env=environment,
|
|
357
|
+
secrets=secrets,
|
|
358
|
+
)
|
|
359
|
+
system_metrics = await sampler_task if sampler_task is not None else None
|
|
360
|
+
except BaseException:
|
|
361
|
+
if sampler_task is not None and not sampler_task.done():
|
|
362
|
+
sampler_task.cancel()
|
|
363
|
+
if sampler_task is not None:
|
|
364
|
+
await asyncio.gather(sampler_task, return_exceptions=True)
|
|
365
|
+
raise
|
|
366
|
+
metrics = BenchmarkRunner.get_pgbench_results(workload_result.stdout)
|
|
367
|
+
if metrics[5] is None:
|
|
368
|
+
raise CollectionError(
|
|
369
|
+
'pgbench completed but TPS could not be parsed; raw output is preserved'
|
|
370
|
+
)
|
|
371
|
+
result = {
|
|
372
|
+
'init': init_result.as_dict(secrets=secrets),
|
|
373
|
+
'workload': workload_result.as_dict(secrets=secrets),
|
|
374
|
+
'metrics': {
|
|
375
|
+
'clients': metrics[0],
|
|
376
|
+
'duration_seconds': metrics[1],
|
|
377
|
+
'transactions': metrics[2],
|
|
378
|
+
'latency_average_ms': metrics[3],
|
|
379
|
+
'initial_connection_time_ms': metrics[4],
|
|
380
|
+
'tps': metrics[5],
|
|
381
|
+
},
|
|
382
|
+
'legacy_metrics': metrics,
|
|
383
|
+
}
|
|
384
|
+
if system_metrics is not None:
|
|
385
|
+
result['system_metrics'] = system_metrics
|
|
386
|
+
return result
|
|
387
|
+
|
|
388
|
+
@staticmethod
|
|
389
|
+
async def collect_compatibility_evidence(
|
|
390
|
+
db_conf: dict[str, Any], workload_conf: dict[str, Any]
|
|
391
|
+
) -> dict[str, Any]:
|
|
392
|
+
"""Validate server 10-18 and prove that newest clients run locally."""
|
|
393
|
+
pgbench, psql = select_local_clients(
|
|
394
|
+
str(workload_conf.get('pgbench_path') or '') or None,
|
|
395
|
+
str(workload_conf.get('psql_path') or '') or None,
|
|
396
|
+
)
|
|
397
|
+
connection_kwargs = {
|
|
398
|
+
key: value
|
|
399
|
+
for key, value in db_conf.items()
|
|
400
|
+
if key not in {'database', 'connect_timeout'}
|
|
401
|
+
}
|
|
402
|
+
connection_kwargs['database'] = 'postgres'
|
|
403
|
+
connection_kwargs['timeout'] = float(db_conf.get('connect_timeout', 5.0))
|
|
404
|
+
connection = await asyncpg.connect(**connection_kwargs)
|
|
405
|
+
try:
|
|
406
|
+
version_num = int(await connection.fetchval('SHOW server_version_num'))
|
|
407
|
+
version_text = str(await connection.fetchval('SHOW server_version'))
|
|
408
|
+
finally:
|
|
409
|
+
await connection.close()
|
|
410
|
+
server_major = server_major_from_version_num(version_num)
|
|
411
|
+
return {
|
|
412
|
+
'schema_version': 'pg_perf_bench/postgresql-compatibility-v1',
|
|
413
|
+
'supported_server_majors': list(SUPPORTED_SERVER_MAJORS),
|
|
414
|
+
'server': {
|
|
415
|
+
'major': server_major,
|
|
416
|
+
'version_num': version_num,
|
|
417
|
+
'version': version_text,
|
|
418
|
+
},
|
|
419
|
+
'load_generator': {
|
|
420
|
+
'execution_host': 'pg_perf_bench_local_host',
|
|
421
|
+
'pgbench': pgbench.as_dict(),
|
|
422
|
+
'psql': psql.as_dict(),
|
|
423
|
+
'newest_installed_client_required': True,
|
|
424
|
+
},
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
@staticmethod
|
|
428
|
+
def setup_report_structure(report_conf: dict, logger) -> dict:
|
|
429
|
+
"""
|
|
430
|
+
Prepares and returns the base report structure.
|
|
431
|
+
"""
|
|
432
|
+
report = get_report_structure(BENCHMARK_TEMPLATE_JSON_PATH)
|
|
433
|
+
report['artifact_schema_version'] = ARTIFACT_SCHEMA_VERSION
|
|
434
|
+
report['generator'] = {'name': 'pg_perf_bench', 'version': __version__}
|
|
435
|
+
report['runtime'] = {
|
|
436
|
+
'python': sys.version.split()[0],
|
|
437
|
+
'platform': platform.platform(),
|
|
438
|
+
}
|
|
439
|
+
report['description'] = get_datetime_report('%d/%m/%Y %H:%M:%S')
|
|
440
|
+
if report_conf.get('report_name') is None:
|
|
441
|
+
report['report_name'] = f'{WorkMode.BENCHMARK}-{get_default_report_name()}'
|
|
442
|
+
report_conf['report_name'] = report['report_name']
|
|
443
|
+
else:
|
|
444
|
+
report['report_name'] = report_conf.get('report_name')
|
|
445
|
+
|
|
446
|
+
logger.info('Report structure initialized.')
|
|
447
|
+
return report
|
|
448
|
+
|
|
449
|
+
@staticmethod
|
|
450
|
+
def setup_connection(conn_type: str, conn_conf: dict, logger):
|
|
451
|
+
"""
|
|
452
|
+
Initializes a database connection object based on the selected type.
|
|
453
|
+
"""
|
|
454
|
+
connection_class = get_connection(conn_type)
|
|
455
|
+
if not connection_class:
|
|
456
|
+
logger.error(f'No valid connection factory for type: {conn_type}')
|
|
457
|
+
return None
|
|
458
|
+
logger.info(f'Connection type selected: {conn_type}')
|
|
459
|
+
|
|
460
|
+
connection = connection_class(**conn_conf)
|
|
461
|
+
connection.logger = logger
|
|
462
|
+
return connection
|
|
463
|
+
|
|
464
|
+
@staticmethod
|
|
465
|
+
async def run_benchmark_iterations(
|
|
466
|
+
logger,
|
|
467
|
+
load_iterations: list[list[str]],
|
|
468
|
+
conn_type: str,
|
|
469
|
+
client,
|
|
470
|
+
db_conf: dict,
|
|
471
|
+
workload_conf: dict,
|
|
472
|
+
) -> list[dict[str, Any]]:
|
|
473
|
+
"""
|
|
474
|
+
Executes all load test iterations sequentially and gathers results.
|
|
475
|
+
"""
|
|
476
|
+
perf_results = []
|
|
477
|
+
logger.info('Starting load iterations...')
|
|
478
|
+
for idx, load_iteration in enumerate(load_iterations, start=1):
|
|
479
|
+
logger.info(f'Preparing for iteration {idx}...')
|
|
480
|
+
await BenchmarkRunner.reset_db_environment(
|
|
481
|
+
logger, conn_type, client, db_conf, workload_conf
|
|
482
|
+
)
|
|
483
|
+
result = deepcopy(
|
|
484
|
+
await BenchmarkRunner.run_benchmark_with_evidence(
|
|
485
|
+
logger,
|
|
486
|
+
load_iteration,
|
|
487
|
+
db_conf=db_conf,
|
|
488
|
+
command_timeout=float(workload_conf.get('command_timeout', 300.0)),
|
|
489
|
+
connection_type=conn_type,
|
|
490
|
+
connection=client,
|
|
491
|
+
system_metrics_interval=float(
|
|
492
|
+
workload_conf.get('system_metrics_interval', 1.0)
|
|
493
|
+
),
|
|
494
|
+
system_metrics_duration=workload_conf.get('system_metrics_duration'),
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
iteration_values = workload_conf.get('pgbench_iter_list', [])
|
|
498
|
+
result['iteration'] = {
|
|
499
|
+
'index': idx,
|
|
500
|
+
'parameter': workload_conf.get('pgbench_iter_name'),
|
|
501
|
+
'value': (iteration_values[idx - 1] if idx - 1 < len(iteration_values) else None),
|
|
502
|
+
}
|
|
503
|
+
perf_results.append(result)
|
|
504
|
+
logger.info(f'Iteration {idx} completed.')
|
|
505
|
+
return perf_results
|
|
506
|
+
|
|
507
|
+
@staticmethod
|
|
508
|
+
async def collect_monitoring_metrics(
|
|
509
|
+
logger,
|
|
510
|
+
db_conf: dict,
|
|
511
|
+
report_data: dict,
|
|
512
|
+
report: dict,
|
|
513
|
+
log_conf: dict,
|
|
514
|
+
client,
|
|
515
|
+
) -> None:
|
|
516
|
+
"""
|
|
517
|
+
Collects system and database metrics after the benchmark is complete.
|
|
518
|
+
"""
|
|
519
|
+
logger.info('Connecting to DB for monitoring metrics...')
|
|
520
|
+
connection_kwargs = {
|
|
521
|
+
key: value for key, value in db_conf.items() if key != 'connect_timeout'
|
|
522
|
+
}
|
|
523
|
+
connection_kwargs['timeout'] = float(db_conf.get('connect_timeout', 5.0))
|
|
524
|
+
connection_kwargs['server_settings'] = {
|
|
525
|
+
'default_transaction_read_only': 'on',
|
|
526
|
+
'statement_timeout': '10000',
|
|
527
|
+
}
|
|
528
|
+
db_conn = await asyncpg.connect(**connection_kwargs)
|
|
529
|
+
try:
|
|
530
|
+
await fill_info_report(logger, client, db_conn, report_data, report)
|
|
531
|
+
logger.info('Monitoring data collected.')
|
|
532
|
+
if log_conf.get('collect_pg_logs'):
|
|
533
|
+
await collect_db_logs(
|
|
534
|
+
logger,
|
|
535
|
+
client,
|
|
536
|
+
db_conn,
|
|
537
|
+
report,
|
|
538
|
+
log_conf.get('db_logs_dir'),
|
|
539
|
+
)
|
|
540
|
+
finally:
|
|
541
|
+
await db_conn.close()
|
|
542
|
+
logger.info('Monitoring DB connection closed.')
|
|
543
|
+
|
|
544
|
+
@staticmethod
|
|
545
|
+
async def run_benchmark_and_collect_metrics(
|
|
546
|
+
args: dict,
|
|
547
|
+
conn_type: str,
|
|
548
|
+
conn_conf: dict,
|
|
549
|
+
db_conf: dict,
|
|
550
|
+
workload_conf: dict,
|
|
551
|
+
report_conf: dict,
|
|
552
|
+
log_conf: dict,
|
|
553
|
+
logger,
|
|
554
|
+
) -> dict[str, Any] | None:
|
|
555
|
+
"""
|
|
556
|
+
Main entry point to execute the full benchmarking workflow.
|
|
557
|
+
"""
|
|
558
|
+
display_user_configuration(args, logger)
|
|
559
|
+
|
|
560
|
+
try:
|
|
561
|
+
report = BenchmarkRunner.setup_report_structure(report_conf, logger)
|
|
562
|
+
load_iterations = BenchmarkRunner.load_iterations_config(db_conf, workload_conf)
|
|
563
|
+
if not load_iterations:
|
|
564
|
+
logger.error('No valid load iterations configured.')
|
|
565
|
+
return None
|
|
566
|
+
|
|
567
|
+
connection = BenchmarkRunner.setup_connection(conn_type, conn_conf, logger)
|
|
568
|
+
if not connection:
|
|
569
|
+
return None
|
|
570
|
+
|
|
571
|
+
report_data = {
|
|
572
|
+
'args': args,
|
|
573
|
+
'workload_conf': workload_conf,
|
|
574
|
+
'report_conf': report_conf,
|
|
575
|
+
}
|
|
576
|
+
workload_evidence = build_workload_evidence(workload_conf, load_iterations)
|
|
577
|
+
report_data['workload_evidence'] = workload_evidence
|
|
578
|
+
report['workload_evidence'] = workload_evidence
|
|
579
|
+
report['benchmark_methodology'] = {
|
|
580
|
+
'database_recreated_before_each_iteration': True,
|
|
581
|
+
'os_caches_dropped_before_each_iteration': bool(
|
|
582
|
+
workload_conf.get('drop_os_caches')
|
|
583
|
+
),
|
|
584
|
+
'workload_definition_hash': workload_evidence['definition_hash'],
|
|
585
|
+
'workload_execution_hash': workload_evidence['execution_hash'],
|
|
586
|
+
'system_metrics_engine': 'pg_diag',
|
|
587
|
+
'system_metrics_collected_during_workload': True,
|
|
588
|
+
'system_metrics_interval_seconds': float(
|
|
589
|
+
workload_conf.get('system_metrics_interval', 1.0)
|
|
590
|
+
),
|
|
591
|
+
'system_metrics_duration_override': workload_conf.get('system_metrics_duration'),
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async with connection as client:
|
|
595
|
+
compatibility = await BenchmarkRunner.collect_compatibility_evidence(
|
|
596
|
+
db_conf,
|
|
597
|
+
workload_conf,
|
|
598
|
+
)
|
|
599
|
+
report['postgresql_compatibility'] = compatibility
|
|
600
|
+
if workload_conf.get('pg_custom_config'):
|
|
601
|
+
custom_path = workload_conf['pg_custom_config']
|
|
602
|
+
db_path = workload_conf.get('pg_data_path', '')
|
|
603
|
+
logger.info(f'Sending custom PostgreSQL config: {custom_path}')
|
|
604
|
+
remote_config = await client.send_pg_config_file(custom_path, db_path)
|
|
605
|
+
logger.info(f'Config applied: {custom_path} -> {remote_config}')
|
|
606
|
+
|
|
607
|
+
benchmark_runs = await BenchmarkRunner.run_benchmark_iterations(
|
|
608
|
+
logger,
|
|
609
|
+
load_iterations,
|
|
610
|
+
conn_type,
|
|
611
|
+
client,
|
|
612
|
+
db_conf,
|
|
613
|
+
workload_conf,
|
|
614
|
+
)
|
|
615
|
+
report_data['benchmark_runs'] = benchmark_runs
|
|
616
|
+
report_data['pgbench_outputs'] = [run['legacy_metrics'] for run in benchmark_runs]
|
|
617
|
+
report['benchmark_runs'] = benchmark_runs
|
|
618
|
+
report['maximum_tps'] = BenchmarkRunner.maximum_tps(benchmark_runs)
|
|
619
|
+
report['sections']['os_metrics'] = build_system_metrics_section(benchmark_runs)
|
|
620
|
+
|
|
621
|
+
await BenchmarkRunner.collect_monitoring_metrics(
|
|
622
|
+
logger, db_conf, report_data, report, log_conf, client
|
|
623
|
+
)
|
|
624
|
+
report['environment_evidence'] = BenchmarkRunner.environment_evidence(report)
|
|
625
|
+
effective_settings = report['sections']['db']['reports']['pg_settings'].get('data')
|
|
626
|
+
database_evidence = {
|
|
627
|
+
'schema_version': 'pg_perf_bench/database-configuration-evidence-v1',
|
|
628
|
+
'effective_settings_hash': canonical_hash(effective_settings),
|
|
629
|
+
}
|
|
630
|
+
custom_config = workload_conf.get('pg_custom_config')
|
|
631
|
+
if custom_config:
|
|
632
|
+
custom_path = Path(str(custom_config)).expanduser()
|
|
633
|
+
database_evidence['supplied_config'] = {
|
|
634
|
+
'name': custom_path.name,
|
|
635
|
+
'hash': file_hash(custom_path),
|
|
636
|
+
}
|
|
637
|
+
report['database_configuration_evidence'] = database_evidence
|
|
638
|
+
|
|
639
|
+
logger.info('Benchmarking process completed successfully.')
|
|
640
|
+
return report
|
|
641
|
+
|
|
642
|
+
except Exception as e:
|
|
643
|
+
logger.error(f'Benchmark failed: {e}')
|
|
644
|
+
if isinstance(e, CollectionError):
|
|
645
|
+
raise
|
|
646
|
+
raise CollectionError(f'Benchmark failed: {e}') from e
|