ClickHouse

Форк
0
/
Benchmark.cpp 
688 строк · 23.9 Кб
1
#include <unistd.h>
2
#include <cstdlib>
3
#include <csignal>
4
#include <iostream>
5
#include <iomanip>
6
#include <optional>
7
#include <random>
8
#include <string_view>
9
#include <pcg_random.hpp>
10
#include <Poco/Util/Application.h>
11
#include <Common/Stopwatch.h>
12
#include <Common/ThreadPool.h>
13
#include <AggregateFunctions/ReservoirSampler.h>
14
#include <AggregateFunctions/registerAggregateFunctions.h>
15
#include <boost/program_options.hpp>
16
#include <Common/ConcurrentBoundedQueue.h>
17
#include <Common/Exception.h>
18
#include <Common/randomSeed.h>
19
#include <Common/clearPasswordFromCommandLine.h>
20
#include <IO/ReadBufferFromFileDescriptor.h>
21
#include <IO/WriteBufferFromFile.h>
22
#include <IO/ReadHelpers.h>
23
#include <IO/WriteHelpers.h>
24
#include <IO/Operators.h>
25
#include <IO/ConnectionTimeouts.h>
26
#include <IO/UseSSL.h>
27
#include <QueryPipeline/RemoteQueryExecutor.h>
28
#include <Interpreters/Context.h>
29
#include <Client/Connection.h>
30
#include <Common/InterruptListener.h>
31
#include <Common/Config/ConfigProcessor.h>
32
#include <Common/Config/getClientConfigPath.h>
33
#include <Common/TerminalSize.h>
34
#include <Common/StudentTTest.h>
35
#include <Common/CurrentMetrics.h>
36
#include <Common/ErrorCodes.h>
37
#include <Core/BaseSettingsProgramOptions.h>
38

39

40
/** A tool for evaluating ClickHouse performance.
41
  * The tool emulates a case with fixed amount of simultaneously executing queries.
42
  */
43

44
namespace CurrentMetrics
45
{
46
    extern const Metric LocalThread;
47
    extern const Metric LocalThreadActive;
48
    extern const Metric LocalThreadScheduled;
49
}
50

51
namespace DB
52
{
53

54
using Ports = std::vector<UInt16>;
55
static constexpr std::string_view DEFAULT_CLIENT_NAME = "benchmark";
56

57
namespace ErrorCodes
58
{
59
    extern const int CANNOT_BLOCK_SIGNAL;
60
    extern const int EMPTY_DATA_PASSED;
61
}
62

63
class Benchmark : public Poco::Util::Application
64
{
65
public:
66
    Benchmark(unsigned concurrency_,
67
            double delay_,
68
            Strings && hosts_,
69
            Ports && ports_,
70
            bool round_robin_,
71
            bool cumulative_,
72
            bool secure_,
73
            const String & default_database_,
74
            const String & user_,
75
            const String & password_,
76
            const String & quota_key_,
77
            const String & stage,
78
            bool randomize_,
79
            size_t max_iterations_,
80
            double max_time_,
81
            size_t confidence_,
82
            const String & query_id_,
83
            const String & query_to_execute_,
84
            size_t max_consecutive_errors_,
85
            bool continue_on_errors_,
86
            bool reconnect_,
87
            bool display_client_side_time_,
88
            bool print_stacktrace_,
89
            const Settings & settings_)
90
        :
91
        round_robin(round_robin_),
92
        concurrency(concurrency_),
93
        delay(delay_),
94
        queue(concurrency),
95
        randomize(randomize_),
96
        cumulative(cumulative_),
97
        max_iterations(max_iterations_),
98
        max_time(max_time_),
99
        confidence(confidence_),
100
        query_id(query_id_),
101
        query_to_execute(query_to_execute_),
102
        continue_on_errors(continue_on_errors_),
103
        max_consecutive_errors(max_consecutive_errors_),
104
        reconnect(reconnect_),
105
        display_client_side_time(display_client_side_time_),
106
        print_stacktrace(print_stacktrace_),
107
        settings(settings_),
108
        shared_context(Context::createShared()),
109
        global_context(Context::createGlobal(shared_context.get())),
110
        pool(CurrentMetrics::LocalThread, CurrentMetrics::LocalThreadActive, CurrentMetrics::LocalThreadScheduled, concurrency)
111
    {
112
        const auto secure = secure_ ? Protocol::Secure::Enable : Protocol::Secure::Disable;
113
        size_t connections_cnt = std::max(ports_.size(), hosts_.size());
114

115
        connections.reserve(connections_cnt);
116
        comparison_info_total.reserve(round_robin ? 1 : connections_cnt);
117
        comparison_info_per_interval.reserve(round_robin ? 1 : connections_cnt);
118

119
        for (size_t i = 0; i < connections_cnt; ++i)
120
        {
121
            UInt16 cur_port = i >= ports_.size() ? 9000 : ports_[i];
122
            std::string cur_host = i >= hosts_.size() ? "localhost" : hosts_[i];
123

124
            connections.emplace_back(std::make_unique<ConnectionPool>(
125
                concurrency,
126
                cur_host, cur_port,
127
                default_database_, user_, password_, quota_key_,
128
                /* cluster_= */ "",
129
                /* cluster_secret_= */ "",
130
                /* client_name_= */ std::string(DEFAULT_CLIENT_NAME),
131
                Protocol::Compression::Enable,
132
                secure));
133

134
            if (!round_robin || comparison_info_per_interval.empty())
135
            {
136
                comparison_info_per_interval.emplace_back(std::make_shared<Stats>());
137
                comparison_info_total.emplace_back(std::make_shared<Stats>());
138
            }
139
        }
140

141
        global_context->makeGlobalContext();
142
        global_context->setSettings(settings);
143
        global_context->setClientName(std::string(DEFAULT_CLIENT_NAME));
144
        global_context->setQueryKindInitial();
145

146
        std::cerr << std::fixed << std::setprecision(3);
147

148
        /// This is needed to receive blocks with columns of AggregateFunction data type
149
        /// (example: when using stage = 'with_mergeable_state')
150
        registerAggregateFunctions();
151

152
        query_processing_stage = QueryProcessingStage::fromString(stage);
153
    }
154

155
    void initialize(Poco::Util::Application & self [[maybe_unused]]) override
156
    {
157
        std::string home_path;
158
        const char * home_path_cstr = getenv("HOME"); // NOLINT(concurrency-mt-unsafe)
159
        if (home_path_cstr)
160
            home_path = home_path_cstr;
161

162
        std::optional<std::string> config_path;
163
        if (config().has("config-file"))
164
            config_path.emplace(config().getString("config-file"));
165
        else
166
            config_path = getClientConfigPath(home_path);
167
        if (config_path.has_value())
168
        {
169
            ConfigProcessor config_processor(*config_path);
170
            auto loaded_config = config_processor.loadConfig();
171
            config().add(loaded_config.configuration);
172
        }
173
    }
174

175
    int main(const std::vector<std::string> &) override
176
    {
177
        readQueries();
178
        runBenchmark();
179
        return 0;
180
    }
181

182
private:
183
    using Entry = ConnectionPool::Entry;
184
    using EntryPtr = std::shared_ptr<Entry>;
185
    using EntryPtrs = std::vector<EntryPtr>;
186

187
    bool round_robin;
188
    unsigned concurrency;
189
    double delay;
190

191
    using Query = std::string;
192
    using Queries = std::vector<Query>;
193
    Queries queries;
194

195
    using Queue = ConcurrentBoundedQueue<Query>;
196
    Queue queue;
197

198
    using ConnectionPoolUniq = std::unique_ptr<ConnectionPool>;
199
    using ConnectionPoolUniqs = std::vector<ConnectionPoolUniq>;
200
    ConnectionPoolUniqs connections;
201

202
    bool randomize;
203
    bool cumulative;
204
    size_t max_iterations;
205
    double max_time;
206
    size_t confidence;
207
    String query_id;
208
    String query_to_execute;
209
    bool continue_on_errors;
210
    size_t max_consecutive_errors;
211
    bool reconnect;
212
    bool display_client_side_time;
213
    bool print_stacktrace;
214
    const Settings & settings;
215
    SharedContextHolder shared_context;
216
    ContextMutablePtr global_context;
217
    QueryProcessingStage::Enum query_processing_stage;
218

219
    std::atomic<size_t> consecutive_errors{0};
220

221
    /// Don't execute new queries after timelimit or SIGINT or exception
222
    std::atomic<bool> shutdown{false};
223

224
    std::atomic<size_t> queries_executed{0};
225

226
    struct Stats
227
    {
228
        std::atomic<size_t> queries{0};
229
        size_t errors = 0;
230
        size_t read_rows = 0;
231
        size_t read_bytes = 0;
232
        size_t result_rows = 0;
233
        size_t result_bytes = 0;
234

235
        using Sampler = ReservoirSampler<double>;
236
        Sampler sampler {1 << 16};
237

238
        void add(double duration, size_t read_rows_inc, size_t read_bytes_inc, size_t result_rows_inc, size_t result_bytes_inc)
239
        {
240
            ++queries;
241
            read_rows += read_rows_inc;
242
            read_bytes += read_bytes_inc;
243
            result_rows += result_rows_inc;
244
            result_bytes += result_bytes_inc;
245
            sampler.insert(duration);
246
        }
247

248
        void clear()
249
        {
250
            queries = 0;
251
            read_rows = 0;
252
            read_bytes = 0;
253
            result_rows = 0;
254
            result_bytes = 0;
255
            sampler.clear();
256
        }
257
    };
258

259
    using MultiStats = std::vector<std::shared_ptr<Stats>>;
260
    MultiStats comparison_info_per_interval;
261
    MultiStats comparison_info_total;
262
    StudentTTest t_test;
263

264
    Stopwatch total_watch;
265
    Stopwatch delay_watch;
266

267
    std::mutex mutex;
268

269
    ThreadPool pool;
270

271
    void readQueries()
272
    {
273
        if (query_to_execute.empty())
274
        {
275
            ReadBufferFromFileDescriptor in(STDIN_FILENO);
276

277
            while (!in.eof())
278
            {
279
                String query;
280
                readText(query, in);
281
                assertChar('\n', in);
282

283
                if (!query.empty())
284
                    queries.emplace_back(std::move(query));
285
            }
286

287
            if (queries.empty())
288
                throw Exception(ErrorCodes::EMPTY_DATA_PASSED, "Empty list of queries.");
289
        }
290
        else
291
        {
292
            queries.emplace_back(query_to_execute);
293
        }
294

295

296
        std::cerr << "Loaded " << queries.size() << " queries.\n";
297
    }
298

299

300
    void printNumberOfQueriesExecuted(size_t num)
301
    {
302
        std::cerr << "\nQueries executed: " << num;
303
        if (queries.size() > 1)
304
            std::cerr << " (" << (num * 100.0 / queries.size()) << "%)";
305
        std::cerr << ".\n";
306
    }
307

308
    /// Try push new query and check cancellation conditions
309
    bool tryPushQueryInteractively(const String & query, InterruptListener & interrupt_listener)
310
    {
311
        bool inserted = false;
312

313
        while (!inserted)
314
        {
315
            inserted = queue.tryPush(query, 100);
316

317
            if (shutdown)
318
            {
319
                /// An exception occurred in a worker
320
                return false;
321
            }
322

323
            if (max_time > 0 && total_watch.elapsedSeconds() >= max_time)
324
            {
325
                std::cout << "Stopping launch of queries."
326
                          << " Requested time limit " << max_time << " seconds is exhausted.\n";
327
                return false;
328
            }
329

330
            if (interrupt_listener.check())
331
            {
332
                std::cout << "Stopping launch of queries. SIGINT received." << std::endl;
333
                return false;
334
            }
335

336
            double seconds = delay_watch.elapsedSeconds();
337
            if (delay > 0 && seconds > delay)
338
            {
339
                printNumberOfQueriesExecuted(queries_executed);
340
                cumulative
341
                    ? report(comparison_info_total, total_watch.elapsedSeconds())
342
                    : report(comparison_info_per_interval, seconds);
343
                delay_watch.restart();
344
            }
345
        }
346

347
        return true;
348
    }
349

350
    void runBenchmark()
351
    {
352
        pcg64 generator(randomSeed());
353
        std::uniform_int_distribution<size_t> distribution(0, queries.size() - 1);
354

355
        try
356
        {
357
            for (size_t i = 0; i < concurrency; ++i)
358
                pool.scheduleOrThrowOnError([this]() mutable { thread(); });
359
        }
360
        catch (...)
361
        {
362
            shutdown = true;
363
            pool.wait();
364
            throw;
365
        }
366

367
        InterruptListener interrupt_listener;
368
        delay_watch.restart();
369

370
        /// Push queries into queue
371
        for (size_t i = 0; !max_iterations || i < max_iterations; ++i)
372
        {
373
            size_t query_index = randomize ? distribution(generator) : i % queries.size();
374

375
            if (!tryPushQueryInteractively(queries[query_index], interrupt_listener))
376
            {
377
                shutdown = true;
378
                break;
379
            }
380
        }
381

382
        /// Now we don't block the Ctrl+C signal and second signal will terminate the program without waiting.
383
        interrupt_listener.unblock();
384

385
        pool.wait();
386
        total_watch.stop();
387

388
        printNumberOfQueriesExecuted(queries_executed);
389
        report(comparison_info_total, total_watch.elapsedSeconds());
390
    }
391

392

393
    void thread()
394
    {
395
        Query query;
396

397
        /// Randomly choosing connection index
398
        pcg64 generator(randomSeed());
399
        std::uniform_int_distribution<size_t> distribution(0, connections.size() - 1);
400

401
        /// In these threads we do not accept INT signal.
402
        sigset_t sig_set;
403
        if (sigemptyset(&sig_set)
404
            || sigaddset(&sig_set, SIGINT)
405
            || pthread_sigmask(SIG_BLOCK, &sig_set, nullptr))
406
        {
407
            throw ErrnoException(ErrorCodes::CANNOT_BLOCK_SIGNAL, "Cannot block signal");
408
        }
409

410
        while (true)
411
        {
412
            bool extracted = false;
413

414
            while (!extracted)
415
            {
416
                extracted = queue.tryPop(query, 100);
417

418
                if (shutdown || (max_iterations && queries_executed == max_iterations))
419
                    return;
420
            }
421

422
            const auto connection_index = distribution(generator);
423
            try
424
            {
425
                execute(query, connection_index);
426
                consecutive_errors = 0;
427
            }
428
            catch (...)
429
            {
430
                std::lock_guard lock(mutex);
431
                std::cerr << "An error occurred while processing the query " << "'" << query << "'"
432
                          << ": " << getCurrentExceptionMessage(false) << std::endl;
433
                if (!(continue_on_errors || max_consecutive_errors > ++consecutive_errors))
434
                {
435
                    shutdown = true;
436
                    throw;
437
                }
438
                else
439
                {
440
                    std::cerr << getCurrentExceptionMessage(print_stacktrace,
441
                        true /*check embedded stack trace*/) << std::endl;
442

443
                    size_t info_index = round_robin ? 0 : connection_index;
444
                    ++comparison_info_per_interval[info_index]->errors;
445
                    ++comparison_info_total[info_index]->errors;
446
                }
447
            }
448
            // Count failed queries toward executed, so that we'd reach
449
            // max_iterations even if every run fails.
450
            ++queries_executed;
451
        }
452
    }
453

454
    void execute(Query & query, size_t connection_index)
455
    {
456
        Stopwatch watch;
457

458
        ConnectionPool::Entry entry = connections[connection_index]->get(
459
            ConnectionTimeouts::getTCPTimeoutsWithoutFailover(settings));
460

461
        if (reconnect)
462
            entry->disconnect();
463

464
        RemoteQueryExecutor executor(
465
            *entry, query, {}, global_context, nullptr, Scalars(), Tables(), query_processing_stage);
466
        if (!query_id.empty())
467
            executor.setQueryId(query_id);
468

469
        Progress progress;
470
        executor.setProgressCallback([&progress](const Progress & value) { progress.incrementPiecewiseAtomically(value); });
471

472
        executor.sendQuery(ClientInfo::QueryKind::INITIAL_QUERY);
473

474
        ProfileInfo info;
475
        while (Block block = executor.readBlock())
476
            info.update(block);
477

478
        executor.finish();
479

480
        double duration = (display_client_side_time || progress.elapsed_ns == 0)
481
            ? watch.elapsedSeconds()
482
            : progress.elapsed_ns / 1e9;
483

484
        std::lock_guard lock(mutex);
485

486
        size_t info_index = round_robin ? 0 : connection_index;
487
        comparison_info_per_interval[info_index]->add(duration, progress.read_rows, progress.read_bytes, info.rows, info.bytes);
488
        comparison_info_total[info_index]->add(duration, progress.read_rows, progress.read_bytes, info.rows, info.bytes);
489
        t_test.add(info_index, duration);
490
    }
491

492
    void report(MultiStats & infos, double seconds)
493
    {
494
        std::lock_guard lock(mutex);
495

496
        std::cerr << "\n";
497
        for (size_t i = 0; i < infos.size(); ++i)
498
        {
499
            const auto & info = infos[i];
500

501
            /// Avoid zeros, nans or exceptions
502
            if (0 == info->queries)
503
                return;
504

505
            std::string connection_description = connections[i]->getDescription();
506
            if (round_robin)
507
            {
508
                connection_description.clear();
509
                for (const auto & conn : connections)
510
                {
511
                    if (!connection_description.empty())
512
                        connection_description += ", ";
513
                    connection_description += conn->getDescription();
514
                }
515
            }
516
            std::cerr
517
                    << connection_description << ", "
518
                    << "queries: " << info->queries << ", ";
519
            if (info->errors)
520
            {
521
                std::cerr << "errors: " << info->errors << ", ";
522
            }
523
            std::cerr
524
                    << "QPS: " << (info->queries / seconds) << ", "
525
                    << "RPS: " << (info->read_rows / seconds) << ", "
526
                    << "MiB/s: " << (info->read_bytes / seconds / 1048576) << ", "
527
                    << "result RPS: " << (info->result_rows / seconds) << ", "
528
                    << "result MiB/s: " << (info->result_bytes / seconds / 1048576) << "."
529
                    << "\n";
530
        }
531
        std::cerr << "\n";
532

533
        auto print_percentile = [&](double percent)
534
        {
535
            std::cerr << percent << "%\t\t";
536
            for (const auto & info : infos)
537
            {
538
                std::cerr << info->sampler.quantileNearest(percent / 100.0) << " sec.\t";
539
            }
540
            std::cerr << "\n";
541
        };
542

543
        for (int percent = 0; percent <= 90; percent += 10)
544
            print_percentile(percent);
545

546
        print_percentile(95);
547
        print_percentile(99);
548
        print_percentile(99.9);
549
        print_percentile(99.99);
550

551
        std::cerr << "\n" << t_test.compareAndReport(confidence).second << "\n";
552

553
        if (!cumulative)
554
        {
555
            for (auto & info : infos)
556
                info->clear();
557
        }
558
    }
559

560
public:
561

562
    ~Benchmark() override
563
    {
564
        shutdown = true;
565
    }
566
};
567

568
}
569

570

571
int mainEntryClickHouseBenchmark(int argc, char ** argv)
572
{
573
    using namespace DB;
574
    bool print_stacktrace = true;
575

576
    try
577
    {
578
        using boost::program_options::value;
579

580
        /// Note: according to the standard, subsequent calls to getenv can mangle previous result.
581
        /// So we copy the results to std::string.
582
        std::optional<std::string> env_user_str;
583
        std::optional<std::string> env_password_str;
584
        std::optional<std::string> env_quota_key_str;
585

586
        const char * env_user = getenv("CLICKHOUSE_USER"); // NOLINT(concurrency-mt-unsafe)
587
        if (env_user != nullptr)
588
            env_user_str.emplace(std::string(env_user));
589

590
        const char * env_password = getenv("CLICKHOUSE_PASSWORD"); // NOLINT(concurrency-mt-unsafe)
591
        if (env_password != nullptr)
592
            env_password_str.emplace(std::string(env_password));
593

594
        const char * env_quota_key = getenv("CLICKHOUSE_QUOTA_KEY"); // NOLINT(concurrency-mt-unsafe)
595
        if (env_quota_key != nullptr)
596
            env_quota_key_str.emplace(std::string(env_quota_key));
597

598
        boost::program_options::options_description desc = createOptionsDescription("Allowed options", getTerminalWidth());
599
        desc.add_options()
600
            ("help",                                                            "produce help message")
601
            ("query,q",       value<std::string>()->default_value(""),          "query to execute")
602
            ("concurrency,c", value<unsigned>()->default_value(1),              "number of parallel queries")
603
            ("delay,d",       value<double>()->default_value(1),                "delay between intermediate reports in seconds (set 0 to disable reports)")
604
            ("stage",         value<std::string>()->default_value("complete"),  "request query processing up to specified stage: complete,fetch_columns,with_mergeable_state,with_mergeable_state_after_aggregation,with_mergeable_state_after_aggregation_and_limit")
605
            ("iterations,i",  value<size_t>()->default_value(0),                "amount of queries to be executed")
606
            ("timelimit,t",   value<double>()->default_value(0.),               "stop launch of queries after specified time limit")
607
            ("randomize,r",                                                     "randomize order of execution")
608
            ("host,h",        value<Strings>()->multitoken(),                   "list of hosts")
609
            ("port",          value<Ports>()->multitoken(),                     "list of ports")
610
            ("roundrobin",    "Instead of comparing queries for different --host/--port just pick one random --host/--port for every query and send query to it.")
611
            ("cumulative",    "prints cumulative data instead of data per interval")
612
            ("secure,s",      "Use TLS connection")
613
            ("user,u",        value<std::string>()->default_value(env_user_str.value_or("default")), "")
614
            ("password",      value<std::string>()->default_value(env_password_str.value_or("")), "")
615
            ("quota_key",     value<std::string>()->default_value(env_quota_key_str.value_or("")), "")
616
            ("database", value<std::string>()->default_value("default"), "")
617
            ("stacktrace", "print stack traces of exceptions")
618
            ("confidence", value<size_t>()->default_value(5), "set the level of confidence for T-test [0=80%, 1=90%, 2=95%, 3=98%, 4=99%, 5=99.5%(default)")
619
            ("query_id", value<std::string>()->default_value(""), "")
620
            ("max-consecutive-errors", value<size_t>()->default_value(0), "set number of allowed consecutive errors")
621
            ("ignore-error,continue_on_errors", "continue testing even if a query fails")
622
            ("reconnect", "establish new connection for every query")
623
            ("client-side-time", "display the time including network communication instead of server-side time; note that for server versions before 22.8 we always display client-side time")
624
        ;
625

626
        Settings settings;
627
        addProgramOptions(settings, desc);
628

629
        boost::program_options::variables_map options;
630
        boost::program_options::store(boost::program_options::parse_command_line(argc, argv, desc), options);
631
        boost::program_options::notify(options);
632

633
        clearPasswordFromCommandLine(argc, argv);
634

635
        if (options.count("help"))
636
        {
637
            std::cout << "Usage: " << argv[0] << " [options] < queries.txt\n";
638
            std::cout << desc << "\n";
639
            std::cout << "\nSee also: https://clickhouse.com/docs/en/operations/utilities/clickhouse-benchmark/\n";
640
            return 0;
641
        }
642

643
        print_stacktrace = options.count("stacktrace");
644

645
        /// NOTE Maybe clickhouse-benchmark should also respect .xml configuration of clickhouse-client.
646

647
        UInt16 default_port = options.count("secure") ? DBMS_DEFAULT_SECURE_PORT : DBMS_DEFAULT_PORT;
648

649
        UseSSL use_ssl;
650
        Ports ports = options.count("port")
651
            ? options["port"].as<Ports>()
652
            : Ports({default_port});
653

654
        Strings hosts = options.count("host") ? options["host"].as<Strings>() : Strings({"localhost"});
655

656
        Benchmark benchmark(
657
            options["concurrency"].as<unsigned>(),
658
            options["delay"].as<double>(),
659
            std::move(hosts),
660
            std::move(ports),
661
            options.count("roundrobin"),
662
            options.count("cumulative"),
663
            options.count("secure"),
664
            options["database"].as<std::string>(),
665
            options["user"].as<std::string>(),
666
            options["password"].as<std::string>(),
667
            options["quota_key"].as<std::string>(),
668
            options["stage"].as<std::string>(),
669
            options.count("randomize"),
670
            options["iterations"].as<size_t>(),
671
            options["timelimit"].as<double>(),
672
            options["confidence"].as<size_t>(),
673
            options["query_id"].as<std::string>(),
674
            options["query"].as<std::string>(),
675
            options["max-consecutive-errors"].as<size_t>(),
676
            options.count("ignore-error"),
677
            options.count("reconnect"),
678
            options.count("client-side-time"),
679
            print_stacktrace,
680
            settings);
681
        return benchmark.run();
682
    }
683
    catch (...)
684
    {
685
        std::cerr << getCurrentExceptionMessage(print_stacktrace, true) << std::endl;
686
        return getCurrentExceptionCode();
687
    }
688
}
689

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.