• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

STEllAR-GROUP / hpx / #853

19 Dec 2022 01:01AM UTC coverage: 86.287% (+0.4%) from 85.912%
#853

push

StellarBot
Merge #6109

6109: Modernize serialization module r=hkaiser a=hkaiser

- flyby separate serialization of Boost types

working towards https://github.com/STEllAR-GROUP/hpx/issues/5497

Co-authored-by: Hartmut Kaiser <hartmut.kaiser@gmail.com>

53 of 53 new or added lines in 6 files covered. (100.0%)

173939 of 201582 relevant lines covered (86.29%)

1931657.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

26.87
/components/parcel_plugins/coalescing/src/performance_counters.cpp
1
//  Copyright (c) 2016-2022 Hartmut Kaiser
2
//
3
//  SPDX-License-Identifier: BSL-1.0
4
//  Distributed under the Boost Software License, Version 1.0. (See accompanying
5
//  file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6

7
#include <hpx/config.hpp>
8

9
#if defined(HPX_HAVE_NETWORKING) && defined(HPX_HAVE_PARCEL_COALESCING)
10
#include <hpx/modules/functional.hpp>
11
#include <hpx/modules/runtime_local.hpp>
12
#include <hpx/modules/string_util.hpp>
13
#include <hpx/util/from_string.hpp>
14

15
#include <hpx/components_base/component_startup_shutdown.hpp>
16
#include <hpx/naming_base/id_type.hpp>
17
#include <hpx/parcel_coalescing/counter_registry.hpp>
18
#include <hpx/performance_counters/counter_creators.hpp>
19
#include <hpx/performance_counters/counters.hpp>
20
#include <hpx/performance_counters/manage_counter_type.hpp>
21

22
#include <cstdint>
23
#include <exception>
24
#include <string>
25
#include <utility>
26
#include <vector>
27

28
namespace hpx::plugins::parcel {
29

30
    ///////////////////////////////////////////////////////////////////////////
31
    // Discoverer for the explicit (hand-rolled performance counter. The
32
    // purpose of this function is to invoke the supplied function f for all
33
    // allowed counter instance names supported by the counter type this
34
    // function has been registered with.
35
    bool counter_discoverer(hpx::performance_counters::counter_info const& info,
9✔
36
        hpx::performance_counters::discover_counter_func const& f,
37
        hpx::performance_counters::discover_counters_mode mode,
38
        hpx::error_code& ec)
39
    {
40
        // compose the counter name templates
41
        performance_counters::counter_path_elements p;
9✔
42
        performance_counters::counter_status status =
9✔
43
            get_counter_path_elements(info.fullname_, p, ec);
9✔
44
        if (!status_is_valid(status))
9✔
45
            return false;
×
46

47
        bool result =
9✔
48
            coalescing_counter_registry::instance().counter_discoverer(
9✔
49
                info, p, f, mode, ec);
9✔
50
        if (!result || ec)
9✔
51
            return false;
×
52

53
        if (&ec != &throws)
9✔
54
            ec = make_success_code();
×
55

56
        return true;
9✔
57
    }
9✔
58

59
    ///////////////////////////////////////////////////////////////////////////
60
    // Creation function for explicit sine performance counter. It's purpose is
61
    // to create and register a new instance of the given name (or reuse an
62
    // existing instance).
63
    struct num_parcels_counter_surrogate
×
64
    {
65
        explicit num_parcels_counter_surrogate(std::string const& parameters)
×
66
          : parameters_(parameters)
×
67
        {
68
        }
×
69

70
        std::int64_t operator()(bool reset)
×
71
        {
72
            if (counter_.empty())
×
73
            {
74
                counter_ =
×
75
                    coalescing_counter_registry::instance().get_parcels_counter(
×
76
                        parameters_);
×
77
                if (counter_.empty())
×
78
                    return 0;    // no counter available yet
×
79
            }
×
80

81
            // dispatch to actual counter
82
            return counter_(reset);
×
83
        }
×
84

85
        hpx::function<std::int64_t(bool)> counter_;
86
        std::string parameters_;
87
    };
88

89
    hpx::naming::gid_type num_parcels_counter_creator(
2✔
90
        hpx::performance_counters::counter_info const& info,
91
        hpx::error_code& ec)
92
    {
93
        switch (info.type_)
2✔
94
        {
95
        // NOLINTNEXTLINE(bugprone-branch-clone)
96
        case performance_counters::counter_type::monotonically_increasing:
97
        {
98
            performance_counters::counter_path_elements paths;
2✔
99
            performance_counters::get_counter_path_elements(
2✔
100
                info.fullname_, paths, ec);
2✔
101
            if (ec)
2✔
102
                return naming::invalid_gid;
×
103

104
            if (paths.parentinstance_is_basename_)
2✔
105
            {
106
                HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
107
                    "num_parcels_counter_creator",
108
                    "invalid counter name for number of parcels (instance "
109
                    "name must not be a valid base counter name)");
110
                return naming::invalid_gid;
×
111
            }
112

113
            if (paths.parameters_.empty())
2✔
114
            {
115
                HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
116
                    "num_parcels_counter_creator",
117
                    "invalid counter parameter for number of parcels: must "
118
                    "specify an action type");
119
                return naming::invalid_gid;
×
120
            }
121

122
            // ask registry
123
            hpx::function<std::int64_t(bool)> f =
124
                coalescing_counter_registry::instance().get_parcels_counter(
2✔
125
                    paths.parameters_);
2✔
126

127
            if (!f.empty())
2✔
128
            {
129
                return performance_counters::detail::create_raw_counter(
2✔
130
                    info, HPX_MOVE(f), ec);
2✔
131
            }
132

133
            // the counter is not available yet, create surrogate function
134
            return performance_counters::detail::create_raw_counter(
×
135
                info, num_parcels_counter_surrogate(paths.parameters_), ec);
×
136
        }
2✔
137
        break;
138

139
        default:
140
            HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
141
                "num_parcels_counter_creator",
142
                "invalid counter type requested");
143
            return naming::invalid_gid;
×
144
        }
145
    }
2✔
146

147
    ///////////////////////////////////////////////////////////////////////////
148
    struct num_messages_counter_surrogate
×
149
    {
150
        explicit num_messages_counter_surrogate(std::string const& parameters)
×
151
          : parameters_(parameters)
×
152
        {
153
        }
×
154

155
        std::int64_t operator()(bool reset)
×
156
        {
157
            if (counter_.empty())
×
158
            {
159
                counter_ = coalescing_counter_registry::instance()
×
160
                               .get_messages_counter(parameters_);
×
161
                if (counter_.empty())
×
162
                    return 0;    // no counter available yet
×
163
            }
×
164

165
            // dispatch to actual counter
166
            return counter_(reset);
×
167
        }
×
168

169
        hpx::function<std::int64_t(bool)> counter_;
170
        std::string parameters_;
171
    };
172

173
    hpx::naming::gid_type num_messages_counter_creator(
2✔
174
        hpx::performance_counters::counter_info const& info,
175
        hpx::error_code& ec)
176
    {
177
        switch (info.type_)
2✔
178
        {
179
        // NOLINTNEXTLINE(bugprone-branch-clone)
180
        case performance_counters::counter_type::monotonically_increasing:
181
        {
182
            performance_counters::counter_path_elements paths;
2✔
183
            performance_counters::get_counter_path_elements(
2✔
184
                info.fullname_, paths, ec);
2✔
185
            if (ec)
2✔
186
                return naming::invalid_gid;
×
187

188
            if (paths.parentinstance_is_basename_)
2✔
189
            {
190
                HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
191
                    "num_messages_counter_creator",
192
                    "invalid counter name for number of parcels (instance "
193
                    "name must not be a valid base counter name)");
194
                return naming::invalid_gid;
×
195
            }
196

197
            if (paths.parameters_.empty())
2✔
198
            {
199
                HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
200
                    "num_messages_counter_creator",
201
                    "invalid counter parameter for number of parcels: must "
202
                    "specify an action type");
203
                return naming::invalid_gid;
×
204
            }
205

206
            // ask registry
207
            hpx::function<std::int64_t(bool)> f =
208
                coalescing_counter_registry::instance().get_messages_counter(
2✔
209
                    paths.parameters_);
2✔
210

211
            if (!f.empty())
2✔
212
            {
213
                return performance_counters::detail::create_raw_counter(
2✔
214
                    info, HPX_MOVE(f), ec);
2✔
215
            }
216

217
            // the counter is not available yet, create surrogate function
218
            return performance_counters::detail::create_raw_counter(
×
219
                info, num_messages_counter_surrogate(paths.parameters_), ec);
×
220
        }
2✔
221
        break;
222

223
        default:
224
            HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
225
                "num_messages_counter_creator",
226
                "invalid counter type requested");
227
            return naming::invalid_gid;
×
228
        }
229
    }
2✔
230

231
    ///////////////////////////////////////////////////////////////////////////
232
    struct num_parcels_per_message_counter_surrogate
×
233
    {
234
        explicit num_parcels_per_message_counter_surrogate(
×
235
            std::string const& parameters)
236
          : parameters_(parameters)
×
237
        {
238
        }
×
239

240
        std::int64_t operator()(bool reset)
×
241
        {
242
            if (counter_.empty())
×
243
            {
244
                counter_ = coalescing_counter_registry::instance()
×
245
                               .get_parcels_per_message_counter(parameters_);
×
246
                if (counter_.empty())
×
247
                    return 0;    // no counter available yet
×
248
            }
×
249

250
            // dispatch to actual counter
251
            return counter_(reset);
×
252
        }
×
253

254
        hpx::function<std::int64_t(bool)> counter_;
255
        std::string parameters_;
256
    };
257

258
    hpx::naming::gid_type num_parcels_per_message_counter_creator(
×
259
        hpx::performance_counters::counter_info const& info,
260
        hpx::error_code& ec)
261
    {
262
        switch (info.type_)
×
263
        {
264
        // NOLINTNEXTLINE(bugprone-branch-clone)
265
        case performance_counters::counter_type::average_count:
266
        {
267
            performance_counters::counter_path_elements paths;
×
268
            performance_counters::get_counter_path_elements(
×
269
                info.fullname_, paths, ec);
×
270
            if (ec)
×
271
                return naming::invalid_gid;
×
272

273
            if (paths.parentinstance_is_basename_)
×
274
            {
275
                HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
276
                    "num_parcels_per_message_counter_creator",
277
                    "invalid counter name for number of parcels (instance "
278
                    "name must not be a valid base counter name)");
279
                return naming::invalid_gid;
×
280
            }
281

282
            if (paths.parameters_.empty())
×
283
            {
284
                HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
285
                    "num_parcels_per_message_counter_creator",
286
                    "invalid counter parameter for number of parcels: must "
287
                    "specify an action type");
288
                return naming::invalid_gid;
×
289
            }
290

291
            // ask registry
292
            hpx::function<std::int64_t(bool)> f =
293
                coalescing_counter_registry::instance()
×
294
                    .get_parcels_per_message_counter(paths.parameters_);
×
295

296
            if (!f.empty())
×
297
            {
298
                return performance_counters::detail::create_raw_counter(
×
299
                    info, HPX_MOVE(f), ec);
×
300
            }
301

302
            // the counter is not available yet, create surrogate function
303
            return performance_counters::detail::create_raw_counter(info,
×
304
                num_parcels_per_message_counter_surrogate(paths.parameters_),
×
305
                ec);
×
306
        }
×
307
        break;
308

309
        default:
310
            HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
311
                "num_parcels_per_message_counter_creator",
312
                "invalid counter type requested");
313
            return naming::invalid_gid;
×
314
        }
315
    }
×
316

317
    ///////////////////////////////////////////////////////////////////////////
318
    struct average_time_between_parcels_counter_surrogate
×
319
    {
320
        explicit average_time_between_parcels_counter_surrogate(
×
321
            std::string const& parameters)
322
          : parameters_(parameters)
×
323
        {
324
        }
×
325

326
        std::int64_t operator()(bool reset)
×
327
        {
328
            if (counter_.empty())
×
329
            {
330
                counter_ =
×
331
                    coalescing_counter_registry::instance()
×
332
                        .get_average_time_between_parcels_counter(parameters_);
×
333
                if (counter_.empty())
×
334
                    return 0;    // no counter available yet
×
335
            }
×
336

337
            // dispatch to actual counter
338
            return counter_(reset);
×
339
        }
×
340

341
        hpx::function<std::int64_t(bool)> counter_;
342
        std::string parameters_;
343
    };
344

345
    hpx::naming::gid_type average_time_between_parcels_counter_creator(
×
346
        hpx::performance_counters::counter_info const& info,
347
        hpx::error_code& ec)
348
    {
349
        switch (info.type_)
×
350
        {
351
        case performance_counters::counter_type::average_timer:
352
        {
353
            performance_counters::counter_path_elements paths;
×
354
            performance_counters::get_counter_path_elements(
×
355
                info.fullname_, paths, ec);
×
356
            if (ec)
×
357
                return naming::invalid_gid;
×
358

359
            if (paths.parentinstance_is_basename_)
×
360
            {
361
                HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
362
                    "average_time_between_parcels_counter_creator",
363
                    "invalid counter name for number of parcels (instance "
364
                    "name must not be a valid base counter name)");
365
                return naming::invalid_gid;
×
366
            }
367

368
            if (paths.parameters_.empty())
×
369
            {
370
                HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
371
                    "average_time_between_parcels_counter_creator",
372
                    "invalid counter parameter for number of parcels: must "
373
                    "specify an action type");
374
                return naming::invalid_gid;
×
375
            }
376

377
            // ask registry
378
            hpx::function<std::int64_t(bool)> f =
379
                coalescing_counter_registry::instance()
×
380
                    .get_average_time_between_parcels_counter(
×
381
                        paths.parameters_);
×
382

383
            if (!f.empty())
×
384
            {
385
                return performance_counters::detail::create_raw_counter(
×
386
                    info, HPX_MOVE(f), ec);
×
387
            }
388

389
            // the counter is not available yet, create surrogate function
390
            return performance_counters::detail::create_raw_counter(info,
×
391
                average_time_between_parcels_counter_surrogate(
×
392
                    paths.parameters_),
×
393
                ec);
×
394
        }
×
395
        break;
396

397
        default:
398
            HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
399
                "average_time_between_parcels_counter_creator",
400
                "invalid counter type requested");
401
            return naming::invalid_gid;
×
402
        }
403
    }
×
404

405
    ///////////////////////////////////////////////////////////////////////////
406
    struct time_between_parcels_histogram_counter_surrogate
×
407
    {
408
        time_between_parcels_histogram_counter_surrogate(
×
409
            std::string const& action_name, std::int64_t min_boundary,
410
            std::int64_t max_boundary, std::int64_t num_buckets)
411
          : action_name_(action_name)
×
412
          , min_boundary_(min_boundary)
×
413
          , max_boundary_(max_boundary)
×
414
          , num_buckets_(num_buckets)
×
415
        {
416
        }
×
417

418
        time_between_parcels_histogram_counter_surrogate(
×
419
            time_between_parcels_histogram_counter_surrogate const& rhs)
420
          : action_name_(rhs.action_name_)
×
421
          , min_boundary_(rhs.min_boundary_)
×
422
          , max_boundary_(rhs.max_boundary_)
×
423
          , num_buckets_(rhs.num_buckets_)
×
424
        {
425
        }
×
426

427
        std::vector<std::int64_t> operator()(bool reset)
×
428
        {
429
            {
430
                std::lock_guard<hpx::spinlock> l(mtx_);
×
431
                if (counter_.empty())
×
432
                {
433
                    counter_ = coalescing_counter_registry::instance()
×
434
                                   .get_time_between_parcels_histogram_counter(
×
435
                                       action_name_, min_boundary_,
×
436
                                       max_boundary_, num_buckets_);
×
437

438
                    // no counter available yet
439
                    if (counter_.empty())
×
440
                        return coalescing_counter_registry::empty_histogram(
×
441
                            reset);
×
442
                }
×
443
            }
×
444

445
            // dispatch to actual counter
446
            return counter_(reset);
×
447
        }
×
448

449
        hpx::spinlock mtx_;
450
        hpx::function<std::vector<std::int64_t>(bool)> counter_;
451
        std::string action_name_;
452
        std::int64_t min_boundary_;
453
        std::int64_t max_boundary_;
454
        std::int64_t num_buckets_;
455
    };
456

457
    hpx::naming::gid_type time_between_parcels_histogram_counter_creator(
×
458
        hpx::performance_counters::counter_info const& info,
459
        hpx::error_code& ec)
460
    {
461
        switch (info.type_)
×
462
        {
463
        case performance_counters::counter_type::histogram:
464
        {
465
            performance_counters::counter_path_elements paths;
×
466
            performance_counters::get_counter_path_elements(
×
467
                info.fullname_, paths, ec);
×
468
            if (ec)
×
469
                return naming::invalid_gid;
×
470

471
            if (paths.parentinstance_is_basename_)
×
472
            {
473
                HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
474
                    "time_between_parcels_histogram_counter_creator",
475
                    "invalid counter name for "
476
                    "time-between-parcels histogram (instance "
477
                    "name must not be a valid base counter name)");
478
                return naming::invalid_gid;
×
479
            }
480

481
            if (paths.parameters_.empty())
×
482
            {
483
                HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
484
                    "time_between_parcels_histogram_counter_creator",
485
                    "invalid counter parameter for "
486
                    "time-between-parcels histogram: must "
487
                    "specify an action type");
488
                return naming::invalid_gid;
×
489
            }
490

491
            // split parameters, extract separate values
492
            std::vector<std::string> params;
×
493
            hpx::string_util::split(params, paths.parameters_,
×
494
                hpx::string_util::is_any_of(","),
×
495
                hpx::string_util::token_compress_mode::off);
496

497
            std::int64_t min_boundary = 0;
×
498
            std::int64_t max_boundary = 1000000;    // 1ms
×
499
            std::int64_t num_buckets = 20;
×
500

501
            if (params.empty() || params[0].empty())
×
502
            {
503
                HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
504
                    "time_between_parcels_histogram_counter_creator",
505
                    "invalid counter parameter for "
506
                    "time-between-parcels histogram: "
507
                    "must specify an action type");
508
                return naming::invalid_gid;
×
509
            }
510

511
            if (params.size() > 1 && !params[1].empty())
×
512
                min_boundary = util::from_string<std::int64_t>(params[1]);
×
513
            if (params.size() > 2 && !params[2].empty())
×
514
                max_boundary = util::from_string<std::int64_t>(params[2]);
×
515
            if (params.size() > 3 && !params[3].empty())
×
516
                num_buckets = util::from_string<std::int64_t>(params[3]);
×
517

518
            // ask registry
519
            hpx::function<std::vector<std::int64_t>(bool)> f =
520
                coalescing_counter_registry::instance()
×
521
                    .get_time_between_parcels_histogram_counter(
×
522
                        params[0], min_boundary, max_boundary, num_buckets);
×
523

524
            if (!f.empty())
×
525
            {
526
                return performance_counters::detail::create_raw_counter(
×
527
                    info, HPX_MOVE(f), ec);
×
528
            }
529

530
            // the counter is not available yet, create surrogate function
531
            return performance_counters::detail::create_raw_counter(info,
×
532
                time_between_parcels_histogram_counter_surrogate(
×
533
                    params[0], min_boundary, max_boundary, num_buckets),
×
534
                ec);
×
535
        }
×
536
        break;
537

538
        default:
539
            HPX_THROWS_IF(ec, hpx::error::bad_parameter,
×
540
                "time_between_parcels_histogram_counter_creator",
541
                "invalid counter type requested");
542
            return naming::invalid_gid;
×
543
        }
544
    }
×
545

546
    ///////////////////////////////////////////////////////////////////////////
547
    // This function will be registered as a startup function for HPX below.
548
    //
549
    // That means it will be executed in a HPX-thread before hpx_main, but after
550
    // the runtime has been initialized and started.
551
    void startup()
581✔
552
    {
553
        using namespace hpx::performance_counters;
554

555
        // define the counter types
556
        generic_counter_type_data const counter_types[] = {
3,486✔
557
            // /coalescing(locality#<locality_id>/total)/count/parcels@action-name
558
            {"/coalescing/count/parcels",
581✔
559
                counter_type::monotonically_increasing,
560
                "returns the number of parcels handled by the message handler "
581✔
561
                "associated with the action which is given by the counter "
562
                "parameter",
563
                HPX_PERFORMANCE_COUNTER_V1, &num_parcels_counter_creator,
581✔
564
                &counter_discoverer, ""},
581✔
565
            // /coalescing(locality#<locality_id>/total)/count/messages@action-name
566
            {"/coalescing/count/messages",
581✔
567
                counter_type::monotonically_increasing,
568
                "returns the number of messages creates as the result of "
581✔
569
                "coalescing parcels of the action which is given by the "
570
                "counter "
571
                "parameter",
572
                HPX_PERFORMANCE_COUNTER_V1, &num_messages_counter_creator,
581✔
573
                &counter_discoverer, ""},
581✔
574
            // /coalescing(...)/count/average-parcels-per-message@action-name
575
            {"/coalescing/count/average-parcels-per-message",
581✔
576
                counter_type::average_count,
577
                "returns the average number of parcels sent in a message "
581✔
578
                "generated by the message handler associated with the action "
579
                "which is given by the counter parameter",
580
                HPX_PERFORMANCE_COUNTER_V1,
581
                &num_parcels_per_message_counter_creator, &counter_discoverer,
581✔
582
                ""},
581✔
583
            // /coalescing(...)/time/between-parcels-average@action-name
584
            {"/coalescing/time/between-parcels-average",
581✔
585
                counter_type::average_timer,
586
                "returns the average time between parcels for the "
581✔
587
                "action which is given by the counter parameter",
588
                HPX_PERFORMANCE_COUNTER_V1,
589
                &average_time_between_parcels_counter_creator,
581✔
590
                &counter_discoverer, "ns"},
581✔
591
            // /coalescing(...)/time/between-parcels-histogram@action-name,min,max,buckets
592
            {"/coalescing/time/between-parcels-histogram",
581✔
593
                counter_type::histogram,
594
                "returns the histogram for the times between parcels for "
581✔
595
                "the action which is given by the counter parameter",
596
                HPX_PERFORMANCE_COUNTER_V1,
597
                &time_between_parcels_histogram_counter_creator,
581✔
598
                &counter_discoverer, "ns/0.1%"}};
581✔
599

600
        // Install the counter types, un-installation of the types is handled
601
        // automatically.
602
        install_counter_types(
581✔
603
            counter_types, sizeof(counter_types) / sizeof(counter_types[0]));
581✔
604
    }
2,905✔
605

606
    ///////////////////////////////////////////////////////////////////////////
607
    bool get_startup(
581✔
608
        hpx::startup_function_type& startup_func, bool& pre_startup)
609
    {
610
        // return our startup-function if performance counters are required
611
        startup_func = startup;    // function to run during startup
581✔
612
        pre_startup = true;        // run 'startup' as pre-startup function
581✔
613
        return true;
581✔
614
    }
615
}    // namespace hpx::plugins::parcel
616

617
///////////////////////////////////////////////////////////////////////////////
618
// Register a startup function which will be called as a HPX-thread during
619
// runtime startup. We use this function to register our performance counter
620
// type and performance counter instances.
621
//
622
// Note that this macro can be used not more than once in one module.
623
HPX_REGISTER_STARTUP_MODULE_DYNAMIC(hpx::plugins::parcel::get_startup)
11,949✔
624

625
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc