Skip to content

Commit dc45aff

Browse files
authored
add explain plan to benchmarks (#9167)
1 parent 3029fcb commit dc45aff

File tree

6 files changed

+113
-59
lines changed

6 files changed

+113
-59
lines changed

ydb/public/lib/ydb_cli/commands/benchmark_utils.cpp

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "benchmark_utils.h"
22

33
#include <util/string/split.h>
4+
#include <util/string/builder.h>
45
#include <util/stream/file.h>
56
#include <util/folder/pathsplit.h>
67
#include <util/folder/path.h>
@@ -291,9 +292,10 @@ class TCSVResultScanner: public IQueryResultScanner, public TQueryResultInfo {
291292
}
292293
};
293294

294-
TQueryBenchmarkResult Execute(const TString& query, NTable::TTableClient& client) {
295+
TQueryBenchmarkResult ExecuteImpl(const TString& query, NTable::TTableClient& client, bool explainOnly) {
295296
TStreamExecScanQuerySettings settings;
296297
settings.CollectQueryStats(ECollectQueryStatsMode::Full);
298+
settings.Explain(explainOnly);
297299
auto it = client.StreamExecuteScanQuery(query, settings).GetValueSync();
298300
ThrowOnError(it);
299301

@@ -316,9 +318,18 @@ TQueryBenchmarkResult Execute(const TString& query, NTable::TTableClient& client
316318
}
317319
}
318320

319-
TQueryBenchmarkResult Execute(const TString& query, NQuery::TQueryClient& client) {
321+
TQueryBenchmarkResult Execute(const TString& query, NTable::TTableClient& client) {
322+
return ExecuteImpl(query, client, false);
323+
}
324+
325+
TQueryBenchmarkResult Explain(const TString& query, NTable::TTableClient& client) {
326+
return ExecuteImpl(query, client, true);
327+
}
328+
329+
TQueryBenchmarkResult ExecuteImpl(const TString& query, NQuery::TQueryClient& client, bool explainOnly) {
320330
NQuery::TExecuteQuerySettings settings;
321331
settings.StatsMode(NQuery::EStatsMode::Full);
332+
settings.ExecMode(explainOnly ? NQuery::EExecMode::Explain : NQuery::EExecMode::Execute);
322333
auto it = client.StreamExecuteQuery(
323334
query,
324335
NYdb::NQuery::TTxControl::BeginTx().CommitTx(),
@@ -344,6 +355,14 @@ TQueryBenchmarkResult Execute(const TString& query, NQuery::TQueryClient& client
344355
}
345356
}
346357

358+
TQueryBenchmarkResult Execute(const TString& query, NQuery::TQueryClient& client) {
359+
return ExecuteImpl(query, client, false);
360+
}
361+
362+
TQueryBenchmarkResult Explain(const TString& query, NQuery::TQueryClient& client) {
363+
return ExecuteImpl(query, client, true);
364+
}
365+
347366
NJson::TJsonValue GetQueryLabels(ui32 queryId) {
348367
NJson::TJsonValue labels(NJson::JSON_MAP);
349368
labels.InsertValue("query", Sprintf("Query%02u", queryId));

ydb/public/lib/ydb_cli/commands/benchmark_utils.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ void ThrowOnError(const TStatus& status);
9191
bool HasCharsInString(const TString& str);
9292
TQueryBenchmarkResult Execute(const TString & query, NTable::TTableClient & client);
9393
TQueryBenchmarkResult Execute(const TString & query, NQuery::TQueryClient & client);
94+
TQueryBenchmarkResult Explain(const TString & query, NTable::TTableClient & client);
95+
TQueryBenchmarkResult Explain(const TString & query, NQuery::TQueryClient & client);
9496
NJson::TJsonValue GetQueryLabels(ui32 queryId);
9597
NJson::TJsonValue GetSensorValue(TStringBuf sensor, TDuration& value, ui32 queryId);
9698
NJson::TJsonValue GetSensorValue(TStringBuf sensor, double value, ui32 queryId);

ydb/public/lib/ydb_cli/commands/ydb_benchmark.cpp

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,16 @@ bool TWorkloadCommandBenchmark::RunBench(TClient& client, NYdbWorkload::IWorkloa
331331
ui32 failsCount = 0;
332332
ui32 diffsCount = 0;
333333
std::optional<TString> prevResult;
334+
if (PlanFileName) {
335+
TQueryBenchmarkResult res = TQueryBenchmarkResult::Error("undefined", "undefined", "undefined");
336+
try {
337+
res = Explain(query, client);
338+
} catch (...) {
339+
res = TQueryBenchmarkResult::Error(CurrentExceptionMessage(), "", "");
340+
}
341+
SavePlans(res, queryN, "explain");
342+
}
343+
334344
for (ui32 i = 0; i < IterationsCount; ++i) {
335345
auto t1 = TInstant::Now();
336346
TQueryBenchmarkResult res = TQueryBenchmarkResult::Error("undefined", "undefined", "undefined");
@@ -371,32 +381,7 @@ bool TWorkloadCommandBenchmark::RunBench(TClient& client, NYdbWorkload::IWorkloa
371381
Cerr << query << Endl << Endl;
372382
Sleep(TDuration::Seconds(1));
373383
}
374-
if (PlanFileName) {
375-
TFsPath(PlanFileName).Parent().MkDirs();
376-
const TString planFName = TStringBuilder() << PlanFileName << "." << i << ".";
377-
if (res.GetQueryPlan()) {
378-
{
379-
TFileOutput out(planFName + "table");
380-
TQueryPlanPrinter queryPlanPrinter(EDataFormat::PrettyTable, true, out, 120);
381-
queryPlanPrinter.Print(res.GetQueryPlan());
382-
}
383-
{
384-
TFileOutput out(planFName + "json");
385-
TQueryPlanPrinter queryPlanPrinter(EDataFormat::JsonBase64, true, out, 120);
386-
queryPlanPrinter.Print(res.GetQueryPlan());
387-
}
388-
{
389-
TPlanVisualizer pv;
390-
pv.LoadPlans(res.GetQueryPlan());
391-
TFileOutput out(planFName + "svg");
392-
out << pv.PrintSvgSafe();
393-
}
394-
}
395-
if (res.GetPlanAst()) {
396-
TFileOutput out(planFName + "ast");
397-
out << res.GetPlanAst();
398-
}
399-
}
384+
SavePlans(res, queryN, ToString(i));
400385
}
401386

402387
auto [inserted, success] = queryRuns.emplace(queryN, TTestInfo(std::move(clientTimings), std::move(serverTimings)));
@@ -463,6 +448,36 @@ bool TWorkloadCommandBenchmark::RunBench(TClient& client, NYdbWorkload::IWorkloa
463448
return !someFailQueries;
464449
}
465450

451+
void TWorkloadCommandBenchmark::SavePlans(const BenchmarkUtils::TQueryBenchmarkResult& res, ui32 queryNum, const TStringBuf name) const {
452+
if (!PlanFileName) {
453+
return;
454+
}
455+
TFsPath(PlanFileName).Parent().MkDirs();
456+
const TString planFName = TStringBuilder() << PlanFileName << "." << queryNum << "." << name << ".";
457+
if (res.GetQueryPlan()) {
458+
{
459+
TFileOutput out(planFName + "table");
460+
TQueryPlanPrinter queryPlanPrinter(EDataFormat::PrettyTable, true, out, 120);
461+
queryPlanPrinter.Print(res.GetQueryPlan());
462+
}
463+
{
464+
TFileOutput out(planFName + "json");
465+
TQueryPlanPrinter queryPlanPrinter(EDataFormat::JsonBase64, true, out, 120);
466+
queryPlanPrinter.Print(res.GetQueryPlan());
467+
}
468+
{
469+
TPlanVisualizer pv;
470+
pv.LoadPlans(res.GetQueryPlan());
471+
TFileOutput out(planFName + "svg");
472+
out << pv.PrintSvgSafe();
473+
}
474+
}
475+
if (res.GetPlanAst()) {
476+
TFileOutput out(planFName + "ast");
477+
out << res.GetPlanAst();
478+
}
479+
}
480+
466481
int TWorkloadCommandBenchmark::DoRun(NYdbWorkload::IWorkloadQueryGenerator& workloadGen, TConfig& /*config*/) {
467482
if (QueryExecuterType == "scan") {
468483
return !RunBench(*TableClient, workloadGen);

ydb/public/lib/ydb_cli/commands/ydb_benchmark.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
namespace NYdb::NConsoleClient {
44

5+
namespace BenchmarkUtils {
6+
class TQueryBenchmarkResult;
7+
}
8+
59
class TWorkloadCommandBenchmark final: public TWorkloadCommandBase {
610
public:
711
TWorkloadCommandBenchmark(NYdbWorkload::TWorkloadParams& params, const NYdbWorkload::IWorkloadQueryGenerator::TWorkloadType& workload);
@@ -16,6 +20,7 @@ class TWorkloadCommandBenchmark final: public TWorkloadCommandBase {
1620

1721
template <typename TClient>
1822
bool RunBench(TClient& client, NYdbWorkload::IWorkloadQueryGenerator& workloadGen);
23+
void SavePlans(const BenchmarkUtils::TQueryBenchmarkResult& res, ui32 queryNum, const TStringBuf name) const;
1924

2025
private:
2126
TString QueryExecuterType;

ydb/tests/olap/lib/ydb_cli.py

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from __future__ import annotations
2+
from typing import Optional
23
import yatest.common
34
import json
45
import os
@@ -25,7 +26,7 @@ def get_cli_command() -> list[str]:
2526
else:
2627
return [cli]
2728

28-
class QueuePlan:
29+
class QueryPlan:
2930
def __init__(self, plan: dict | None = None, table: str | None = None, ast: str | None = None, svg: str | None = None) -> None:
3031
self.plan = plan
3132
self.table = table
@@ -34,9 +35,9 @@ def __init__(self, plan: dict | None = None, table: str | None = None, ast: str
3435

3536
class WorkloadRunResult:
3637
def __init__(
37-
self, stats: dict[str, dict[str, any]] = {}, query_out: str = None, stdout: str = None, stderr: str = None,
38-
error_message: str | None = None, plans: list[YdbCliHelper.QueuePlan] | None = None,
39-
errors_by_iter: dict[int, str] | None = None
38+
self, stats: dict[str, dict[str, any]] = {}, query_out: Optional[str] = None, stdout: Optional[str] = None, stderr: Optional[str] = None,
39+
error_message: Optional[str] = None, plans: Optional[list[YdbCliHelper.QueryPlan]] = None,
40+
errors_by_iter: Optional[dict[int, str]] = None, explain_plan: Optional[YdbCliHelper.QueryPlan] = None
4041
) -> None:
4142
self.stats = stats
4243
self.query_out = query_out if str != '' else None
@@ -45,6 +46,7 @@ def __init__(
4546
self.success = error_message is None
4647
self.error_message = '' if self.success else error_message
4748
self.plans = plans
49+
self.explain_plan = explain_plan
4850
self.errors_by_iter = errors_by_iter
4951

5052
@staticmethod
@@ -76,6 +78,23 @@ def _try_extract_error_message(stderr: str) -> str:
7678
else:
7779
result[iter] = stderr[begin_pos:end_pos].strip()
7880

81+
def _load_plans(plan_path: str, name: str) -> YdbCliHelper.QueryPlan:
82+
result = YdbCliHelper.QueryPlan()
83+
pp = f'{plan_path}.{query_num}.{name}'
84+
if (os.path.exists(f'{pp}.json')):
85+
with open(f'{pp}.json') as f:
86+
result.plan = json.load(f)
87+
if (os.path.exists(f'{pp}.table')):
88+
with open(f'{pp}.table') as f:
89+
result.table = f.read()
90+
if (os.path.exists(f'{pp}.ast')):
91+
with open(f'{pp}.ast') as f:
92+
result.ast = f.read()
93+
if (os.path.exists(f'{pp}.svg')):
94+
with open(f'{pp}.svg') as f:
95+
result.svg = f.read()
96+
return result
97+
7998
errors_by_iter = {}
8099
try:
81100
wait_error = YdbCluster.wait_ydb_alive(300, path)
@@ -129,29 +148,16 @@ def _try_extract_error_message(stderr: str) -> str:
129148
if (os.path.exists(qout_path)):
130149
with open(qout_path, 'r') as r:
131150
qout = r.read()
132-
plans = []
133-
for i in range(iterations):
134-
plans.append(YdbCliHelper.QueuePlan())
135-
pp = f'{plan_path}.{i}'
136-
if (os.path.exists(f'{pp}.json')):
137-
with open(f'{pp}.json') as f:
138-
plans[i].plan = json.load(f)
139-
if (os.path.exists(f'{pp}.table')):
140-
with open(f'{pp}.table') as f:
141-
plans[i].table = f.read()
142-
if (os.path.exists(f'{pp}.ast')):
143-
with open(f'{pp}.ast') as f:
144-
plans[i].ast = f.read()
145-
if (os.path.exists(f'{pp}.svg')):
146-
with open(f'{pp}.svg') as f:
147-
plans[i].svg = f.read()
151+
plans = [_load_plans(plan_path, str(i)) for i in range(iterations)]
152+
explain_plan = _load_plans(plan_path, 'explain')
148153

149154
return YdbCliHelper.WorkloadRunResult(
150155
stats=stats,
151156
query_out=qout,
152157
plans=plans,
153-
stdout=exec.stdout.decode('utf-8'),
154-
stderr=exec.stderr.decode('utf-8'),
158+
explain_plan=explain_plan,
159+
stdout=exec.stdout.decode('utf-8', 'ignore'),
160+
stderr=exec.stderr.decode('utf-8', 'ignore'),
155161
error_message=err,
156162
errors_by_iter=errors_by_iter
157163
)

ydb/tests/olap/load/conftest.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ def _get_duraton(stats, field):
2929
result = stats.get(field)
3030
return float(result) / 1e3 if result is not None else None
3131

32+
def _attach_plans(plan: YdbCliHelper.QueryPlan) -> None:
33+
if plan.plan is not None:
34+
allure.attach(json.dumps(plan.plan), 'Plan json', attachment_type=allure.attachment_type.JSON)
35+
if plan.table is not None:
36+
allure.attach(plan.table, 'Plan table', attachment_type=allure.attachment_type.TEXT)
37+
if plan.ast is not None:
38+
allure.attach(plan.ast, 'Plan ast', attachment_type=allure.attachment_type.TEXT)
39+
if plan.svg is not None:
40+
allure.attach(plan.svg, 'Plan svg', attachment_type=allure.attachment_type.SVG)
41+
3242
test = f'Query{query_num:02d}'
3343
allure_test_description(self.suite, test, refference_set=self.refference)
3444
allure_listener = next(filter(lambda x: isinstance(x, AllureListener), plugin_manager.get_plugin_manager().get_plugins()))
@@ -47,19 +57,16 @@ def _get_duraton(stats, field):
4757
stats = {}
4858
if result.query_out is not None:
4959
allure.attach(result.query_out, 'Query output', attachment_type=allure.attachment_type.TEXT)
60+
61+
if result.explain_plan is not None:
62+
with allure.step('Explain'):
63+
_attach_plans(result.explain_plan)
64+
5065
if result.plans is not None:
5166
for i in range(self.iterations):
5267
try:
5368
with allure.step(f'Iteration {i}'):
54-
plan = result.plans[i]
55-
if plan.plan is not None:
56-
allure.attach(json.dumps(plan.plan), 'Plan json', attachment_type=allure.attachment_type.JSON)
57-
if plan.table is not None:
58-
allure.attach(plan.table, 'Plan table', attachment_type=allure.attachment_type.TEXT)
59-
if plan.ast is not None:
60-
allure.attach(plan.ast, 'Plan ast', attachment_type=allure.attachment_type.TEXT)
61-
if plan.svg is not None:
62-
allure.attach(plan.svg, 'Plan svg', attachment_type=allure.attachment_type.SVG)
69+
_attach_plans(result.plans[i])
6370
if i in result.errors_by_iter:
6471
pytest.fail(result.errors_by_iter[i])
6572
except BaseException:

0 commit comments

Comments
 (0)