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

tarantool / luajit / 5784628350

07 Aug 2023 11:39AM UTC coverage: 87.373% (+2.2%) from 85.197%
5784628350

push

github

ligurio
setup-gcovr [TO SQUASH]

5317 of 6002 branches covered (88.59%)

Branch coverage included in aggregate %.

20381 of 23410 relevant lines covered (87.06%)

242527.58 hits per line

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

64.44
/src/lj_sysprof.c
1
/*
2
** Implementation of sysprof - platform and Lua profiler.
3
*/
4

5
#define lj_sysprof_c
6
#define LUA_CORE
7

8
#include "lj_arch.h"
9
#include "lj_sysprof.h"
10

11
#if LJ_HASSYSPROF
12

13
#include "lj_obj.h"
14
#include "lj_debug.h"
15
#include "lj_dispatch.h"
16
#include "lj_frame.h"
17

18
#if LJ_HASJIT
19
#include "lj_jit.h"
20
#include "lj_trace.h"
21
#endif
22

23
#include "lj_wbuf.h"
24
#include "lj_profile_timer.h"
25
#include "lj_symtab.h"
26

27
#include <pthread.h>
28
#include <errno.h>
29
#include <execinfo.h>
30

31
/*
32
** Number of profiler frames we need to omit during stack
33
** unwinding.
34
**   +------------------------+
35
** 0 | default_backtrace_host |
36
**   +------------------------+
37
** 1 | stream_backtrace_host  |
38
**   +------------------------+
39
** 2 |  stream_{guest/host}   |
40
**   +------------------------+
41
** 3 |      stream_event      |
42
**   +------------------------+
43
** 4 | sysprof_record_sample  |
44
**   +------------------------+
45
** 5 | sysprof_signal_handler |
46
**   +------------------------+
47
*/
48
#define SYSPROF_HANDLER_STACK_DEPTH 6
49
#define SYSPROF_BACKTRACE_FRAME_MAX 512
50

51
/* Check that vmstate fits in 4 bits (see streaming format) */
52
#define vmstfit4(st) ((st & ~(uint32_t)((1 << 4) - 1)) == 0)
53

54
enum sysprof_state {
55
  /* Profiler is not running. */
56
  SPS_IDLE,
57
  /* Profiler is running. */
58
  SPS_PROFILE,
59
  /*
60
  ** Stopped in case of stopped or failed stream.
61
  ** Saved errno is set at luaM_sysprof_stop.
62
  */
63
  SPS_HALT
64
};
65

66
struct sysprof {
67
  global_State *g; /* Profiled VM. */
68
  pthread_t thread; /* Profiled thread. */
69
  volatile sig_atomic_t state; /* Internal state. */
70
  struct lj_wbuf out; /* Output accumulator. */
71
  struct luam_Sysprof_Counters counters; /* Profiling counters. */
72
  struct luam_Sysprof_Options opt; /* Profiling options. */
73
  luam_Sysprof_writer writer; /* Writer function for profile events. */
74
  luam_Sysprof_on_stop on_stop; /* Callback on profiling stopping. */
75
  luam_Sysprof_backtracer backtracer; /* Backtracing function for the host stack. */
76
  lj_profile_timer timer; /* Profiling timer. */
77
  int saved_errno; /* Saved errno when profiler failed. */
78
  uint32_t lib_adds; /* Number of libs loaded. Monotonic. */
79
};
80
/*
81
** XXX: Only one VM can be profiled at a time.
82
*/
83

84
static struct sysprof sysprof = {0};
85

86
/* --- Stream ------------------------------------------------------------- */
87

88
static const uint8_t ljp_header[] = {'l', 'j', 'p', LJP_FORMAT_VERSION,
89
                                      0x0, 0x0, 0x0};
90

91
static int stream_is_needed(struct sysprof *sp)
16✔
92
{
93
  return sp->opt.mode != LUAM_SYSPROF_DEFAULT;
16✔
94
}
95

96
static int is_unconfigured(struct sysprof *sp)
659✔
97
{
98
  return sp->backtracer == NULL || sp->on_stop == NULL || sp->writer == NULL;
221✔
99
}
100

101
static void stream_prologue(struct sysprof *sp)
2✔
102
{
103
  lj_symtab_dump(&sp->out, sp->g, &sp->lib_adds);
2✔
104
  lj_wbuf_addn(&sp->out, ljp_header, sizeof(ljp_header));
2✔
105
}
2✔
106

107
static void stream_epilogue(struct sysprof *sp)
2✔
108
{
109
  lj_wbuf_addbyte(&sp->out, LJP_EPILOGUE_BYTE);
2✔
110
}
111

112
static void stream_lfunc(struct lj_wbuf *buf, const GCfunc *func)
113
{
114
  lua_assert(isluafunc(func));
115
  const GCproto *pt = funcproto(func);
116
  lua_assert(pt != NULL);
117
  lj_wbuf_addbyte(buf, LJP_FRAME_LFUNC);
118
  lj_wbuf_addu64(buf, (uintptr_t)pt);
119
  lj_wbuf_addu64(buf, (uint64_t)pt->firstline);
120
}
121

122
static void stream_cfunc(struct lj_wbuf *buf, const GCfunc *func)
×
123
{
124
  lua_assert(iscfunc(func));
×
125
  lj_wbuf_addbyte(buf, LJP_FRAME_CFUNC);
×
126
  lj_wbuf_addu64(buf, (uintptr_t)func->c.f);
×
127
}
×
128

129
static void stream_ffunc(struct lj_wbuf *buf, const GCfunc *func)
×
130
{
131
  lua_assert(isffunc(func));
×
132
  lj_wbuf_addbyte(buf, LJP_FRAME_FFUNC);
×
133
  lj_wbuf_addu64(buf, func->c.ffid);
×
134
}
×
135

136
static void stream_frame_lua(struct lj_wbuf *buf, const cTValue *frame)
×
137
{
138
  const GCfunc *func = frame_func(frame);
×
139
  lua_assert(func != NULL);
×
140
  if (isluafunc(func))
×
141
    stream_lfunc(buf, func);
×
142
  else if (isffunc(func))
×
143
    stream_ffunc(buf, func);
×
144
  else if (iscfunc(func))
×
145
    stream_cfunc(buf, func);
×
146
  else
147
    /* Unreachable. */
148
    lua_assert(0);
×
149
}
×
150

151
static void stream_backtrace_lua(struct sysprof *sp)
×
152
{
153
  global_State *g = sp->g;
×
154
  struct lj_wbuf *buf = &sp->out;
×
155
  cTValue *top_frame = NULL, *frame = NULL, *bot = NULL;
×
156
  lua_State *L = NULL;
×
157

158
  lua_assert(g != NULL);
×
159
  L = gco2th(gcref(g->cur_L));
×
160
  lua_assert(L != NULL);
×
161

162
  top_frame = g->top_frame - 1; //(1 + LJ_FR2)
×
163

164
  bot = tvref(L->stack) + LJ_FR2;
×
165
  /* Traverse frames backwards */
166
  for (frame = top_frame; frame > bot; frame = frame_prev(frame)) {
×
167
    if (frame_gc(frame) == obj2gco(L) || frame_isvarg(frame))
×
168
      continue;  /* Skip dummy frames. See lj_err_optype_call(). */
×
169
    stream_frame_lua(buf, frame);
×
170
  }
171

172
  lj_wbuf_addbyte(buf, LJP_FRAME_LUA_LAST);
×
173
}
×
174

175
static void *stream_frame_host(int frame_no, void *addr)
8✔
176
{
177
  struct sysprof *sp = &sysprof;
8✔
178
  /*
179
  ** We don't want the profiler stack to be streamed, as it will
180
  ** burden the profile with unnecessary information.
181
  */
182
  if (LJ_UNLIKELY(frame_no <= SYSPROF_HANDLER_STACK_DEPTH))
8✔
183
    return addr;
184
  else if (LJ_UNLIKELY(sp->opt.mode == LUAM_SYSPROF_LEAF &&
1✔
185
                         frame_no > SYSPROF_HANDLER_STACK_DEPTH))
186
    return NULL;
187

188
  lj_wbuf_addu64(&sp->out, (uintptr_t)addr);
1✔
189
  return addr;
1✔
190
}
191

192
static void default_backtrace_host(void *(writer)(int frame_no, void *addr))
2✔
193
{
194
  static void *backtrace_buf[SYSPROF_BACKTRACE_FRAME_MAX] = {};
2✔
195

196
  struct sysprof *sp = &sysprof;
2✔
197
  int max_depth = sp->opt.mode == LUAM_SYSPROF_LEAF
4✔
198
                  ? SYSPROF_HANDLER_STACK_DEPTH + 1
199
                  : SYSPROF_BACKTRACE_FRAME_MAX;
2✔
200
  const int depth = backtrace(backtrace_buf, max_depth);
2✔
201
  int level;
2✔
202

203
  lua_assert(depth <= max_depth);
2✔
204
  for (level = SYSPROF_HANDLER_STACK_DEPTH; level < depth; ++level) {
12✔
205
    if (!writer(level - SYSPROF_HANDLER_STACK_DEPTH + 1, backtrace_buf[level]))
8✔
206
      return;
207
  }
208
}
209

210
static void stream_backtrace_host(struct sysprof *sp)
2✔
211
{
212
  lua_assert(sp->backtracer != NULL);
2✔
213
  sp->backtracer(stream_frame_host);
2✔
214
  lj_wbuf_addu64(&sp->out, (uintptr_t)LJP_FRAME_HOST_LAST);
2✔
215
}
216

217
#if LJ_HASJIT
218
static void stream_trace(struct sysprof *sp, uint32_t vmstate)
×
219
{
220
  lj_wbuf_addbyte(&sp->out, (uint8_t)vmstate);
×
221
  struct lj_wbuf *out = &sp->out;
×
222
  uint32_t traceno = sp->g->vmstate;
×
223
  jit_State *J = G2J(sp->g);
×
224
  GCtrace *trace = traceref(J, traceno);
×
225

226
  GCproto *startpt = gco2pt(gcref(trace->startpt));
×
227

228
  lj_wbuf_addu64(out, traceno);
×
229
  lj_wbuf_addu64(out, (uintptr_t)startpt);
×
230
  lj_wbuf_addu64(out, startpt->firstline);
×
231
}
×
232
#endif
233

234
static void stream_guest(struct sysprof *sp, uint32_t vmstate)
×
235
{
236
  lj_wbuf_addbyte(&sp->out, (uint8_t)vmstate);
×
237
  stream_backtrace_lua(sp);
×
238
  stream_backtrace_host(sp);
×
239
}
×
240

241
static void stream_host(struct sysprof *sp, uint32_t vmstate)
2✔
242
{
243
  struct lua_State *L = gco2th(gcref(sp->g->cur_L));
2✔
244
  lj_symtab_dump_newc(&sp->lib_adds, &sp->out, LJP_SYMTAB_CFUNC_EVENT, L);
2✔
245
  lj_wbuf_addbyte(&sp->out, (uint8_t)vmstate);
2✔
246
  stream_backtrace_host(sp);
2✔
247
}
2✔
248

249
typedef void (*event_streamer)(struct sysprof *sp, uint32_t vmstate);
250

251
static event_streamer event_streamers[] = {
252
  /* XXX: order is important */
253
  stream_host,  /* LJ_VMST_INTERP */
254
  stream_guest, /* LJ_VMST_LFUNC */
255
  stream_guest, /* LJ_VMST_FFUNC */
256
  stream_guest, /* LJ_VMST_CFUNC */
257
  stream_host,  /* LJ_VMST_GC */
258
  stream_host,  /* LJ_VMST_EXIT */
259
  stream_host,  /* LJ_VMST_RECORD */
260
  stream_host,  /* LJ_VMST_OPT */
261
  stream_host,  /* LJ_VMST_ASM */
262
#if LJ_HASJIT
263
  stream_trace  /* LJ_VMST_TRACE */
264
#endif
265
};
266

267
static void stream_event(struct sysprof *sp, uint32_t vmstate)
268
{
269
  event_streamer stream = NULL;
270

271
  lua_assert(vmstfit4(vmstate));
272
  stream = event_streamers[vmstate];
273
  lua_assert(NULL != stream);
274
  stream(sp, vmstate);
275
}
276

277
/* -- Signal handler ------------------------------------------------------ */
278

279
static void sysprof_record_sample(struct sysprof *sp, siginfo_t *info)
280
{
281
  global_State *g = sp->g;
282
  uint32_t _vmstate = ~(uint32_t)(g->vmstate);
283
  uint32_t vmstate = _vmstate < LJ_VMST_TRACE ? _vmstate : LJ_VMST_TRACE;
284

285
  lua_assert(pthread_self() == sp->thread);
286

287
  /* Caveat: order of counters must match vmstate order in <lj_obj.h>. */
288
  ((uint64_t *)&sp->counters)[vmstate]++;
289

290
  sp->counters.samples++;
291

292
  if (!stream_is_needed(sp))
293
    return;
294

295
  stream_event(sp, vmstate);
296
  if (LJ_UNLIKELY(lj_wbuf_test_flag(&sp->out, STREAM_ERRIO|STREAM_STOP))) {
297
    sp->saved_errno = lj_wbuf_errno(&sp->out);
298
    lj_wbuf_terminate(&sp->out);
299
    sp->state = SPS_HALT;
300
  }
301
}
302

303
static void sysprof_signal_handler(int sig, siginfo_t *info, void *ctx)
3✔
304
{
305
  struct sysprof *sp = &sysprof;
3✔
306
  UNUSED(sig);
3✔
307
  UNUSED(ctx);
3✔
308

309
  switch (sp->state) {
3✔
310
    case SPS_PROFILE:
3✔
311
      sysprof_record_sample(sp, info);
3✔
312
      break;
3✔
313

314
    case SPS_IDLE:
315
    case SPS_HALT:
316
      /* noop */
317
      break;
318

319
    default:
320
      lua_assert(0);
321
      break;
322
  }
323
}
3✔
324

325
/* -- Internal ------------------------------------------------------------ */
326

327
static int sysprof_validate(struct sysprof *sp,
13✔
328
                            const struct luam_Sysprof_Options *opt)
329
{
330
  switch (sp->state) {
13✔
331
    case SPS_IDLE:
11✔
332
      if (opt->mode > LUAM_SYSPROF_CALLGRAPH) {
11✔
333
        return PROFILE_ERRUSE;
334
      } else if (opt->mode != LUAM_SYSPROF_DEFAULT &&
10✔
335
                 (opt->buf == NULL || opt->len == 0 || is_unconfigured(sp))) {
3✔
336
        return PROFILE_ERRUSE;
337
      } else if (opt->interval == 0) {
9✔
338
        return PROFILE_ERRUSE;
1✔
339
      }
340
      break;
341

342
    case SPS_PROFILE:
343
    case SPS_HALT:
344
      return PROFILE_ERRRUN;
345

346
    default:
347
      lua_assert(0);
348
      break;
349
  }
350

351
  return PROFILE_SUCCESS;
352
}
353

354
static int sysprof_init(struct sysprof *sp, lua_State *L,
355
                        const struct luam_Sysprof_Options *opt)
356
{
357
  const int status = sysprof_validate(sp, opt);
358
  if (PROFILE_SUCCESS != status)
359
    return status;
360

361
  /* Copy validated options to sysprof state. */
362
  memcpy(&sp->opt, opt, sizeof(sp->opt));
363

364
  /* Init general fields. */
365
  sp->g = G(L);
366
  sp->thread = pthread_self();
367

368
  /* Reset counters. */
369
  memset(&sp->counters, 0, sizeof(sp->counters));
370

371
  /* Reset saved errno. */
372
  sp->saved_errno = 0;
373

374
  if (stream_is_needed(sp))
375
    lj_wbuf_init(&sp->out, sp->writer, opt->ctx, opt->buf, opt->len);
376

377
  return PROFILE_SUCCESS;
378
}
379

380
/* -- Public profiling API ------------------------------------------------ */
381

382
int lj_sysprof_set_writer(luam_Sysprof_writer writer) {
219✔
383
  struct sysprof *sp = &sysprof;
219✔
384

385
  if (sp->state != SPS_IDLE || writer == NULL)
219✔
386
    return PROFILE_ERRUSE;
387

388
  sp->writer = writer;
219✔
389
  if (!is_unconfigured(sp)) {
219✔
390
    sp->state = SPS_IDLE;
×
391
  }
392
  return PROFILE_SUCCESS;
393
}
394

395
int lj_sysprof_set_on_stop(luam_Sysprof_on_stop on_stop) {
219✔
396
  struct sysprof *sp = &sysprof;
219✔
397

398
  if (sp->state != SPS_IDLE || on_stop == NULL)
219✔
399
    return PROFILE_ERRUSE;
400

401
  sp->on_stop = on_stop;
219✔
402
  if (!is_unconfigured(sp)) {
219✔
403
    sp->state = SPS_IDLE;
×
404
  }
405
  return PROFILE_SUCCESS;
406
}
407

408
int lj_sysprof_set_backtracer(luam_Sysprof_backtracer backtracer) {
219✔
409
  struct sysprof *sp = &sysprof;
219✔
410

411
  if (sp->state != SPS_IDLE)
219✔
412
    return PROFILE_ERRUSE;
413
  if (backtracer == NULL) {
219✔
414
    sp->backtracer = default_backtrace_host;
219✔
415
    /*
416
    ** XXX: `backtrace` is not signal-safe, according to man,
417
    ** because it is lazy loaded on the first call, which triggers
418
    ** allocations. We need to call `backtrace` before starting profiling
419
    ** to avoid lazy loading.
420
    */
421
    void *dummy = NULL;
219✔
422
    backtrace(&dummy, 1);
219✔
423
  }
424
  else {
425
    sp->backtracer = backtracer;
×
426
  }
427
  if (!is_unconfigured(sp)) {
219✔
428
    sp->state = SPS_IDLE;
219✔
429
  }
430
  return PROFILE_SUCCESS;
431
}
432

433
int lj_sysprof_start(lua_State *L, const struct luam_Sysprof_Options *opt)
13✔
434
{
435
  struct sysprof *sp = &sysprof;
13✔
436

437
  int status = sysprof_init(sp, L, opt);
13✔
438
  if (PROFILE_SUCCESS != status) {
13✔
439
    if (NULL != sp->on_stop) {
5✔
440
      /*
441
      ** Initialization may fail in case of unconfigured sysprof,
442
      ** so we cannot guarantee cleaning up resources in this case.
443
      */
444
      sp->on_stop(opt->ctx, opt->buf);
5✔
445
    }
446
    return status;
5✔
447
  }
448

449
  sp->state = SPS_PROFILE;
8✔
450

451
  if (stream_is_needed(sp)) {
8✔
452
    stream_prologue(sp);
2✔
453
    if (LJ_UNLIKELY(lj_wbuf_test_flag(&sp->out, STREAM_ERRIO|STREAM_STOP))) {
2✔
454
      /* on_stop call may change errno value. */
455
      const int saved_errno = lj_wbuf_errno(&sp->out);
×
456
      /* Ignore possible errors. mp->out.buf may be NULL here. */
457
      sp->on_stop(opt->ctx, sp->out.buf);
×
458
      lj_wbuf_terminate(&sp->out);
×
459
      sp->state = SPS_IDLE;
×
460
      errno = saved_errno;
×
461
      return PROFILE_ERRIO;
×
462
    }
463
  }
464

465
  sp->timer.opt.interval_msec = opt->interval;
8✔
466
  sp->timer.opt.handler = sysprof_signal_handler;
8✔
467
  lj_profile_timer_start(&sp->timer);
8✔
468

469
  return PROFILE_SUCCESS;
8✔
470
}
471

472
int lj_sysprof_stop(lua_State *L)
227✔
473
{
474
  struct sysprof *sp = &sysprof;
227✔
475
  global_State *g = sp->g;
227✔
476
  struct lj_wbuf *out = &sp->out;
227✔
477

478
  if (SPS_IDLE == sp->state)
227✔
479
    return PROFILE_ERRRUN;
480
  else if (G(L) != g)
8✔
481
    return PROFILE_ERRUSE;
482

483
  lj_profile_timer_stop(&sp->timer);
8✔
484

485
  if (SPS_HALT == sp->state) {
8✔
486
    errno = sp->saved_errno;
×
487
    sp->state = SPS_IDLE;
×
488
    /* wbuf was terminated when error occured. */
489
    return PROFILE_ERRIO;
×
490
  }
491

492
  sp->state = SPS_IDLE;
8✔
493

494
  if (stream_is_needed(sp)) {
8✔
495
    int cb_status = 0;
2✔
496

497
    stream_epilogue(sp);
2✔
498
    lj_wbuf_flush(out);
2✔
499

500
    cb_status = sp->on_stop(sp->opt.ctx, out->buf);
2✔
501
    if (LJ_UNLIKELY(lj_wbuf_test_flag(out, STREAM_ERRIO | STREAM_STOP)) ||
2✔
502
        cb_status != 0) {
503
      errno = lj_wbuf_errno(out);
×
504
      lj_wbuf_terminate(out);
×
505
      return PROFILE_ERRIO;
×
506
    }
507

508
    lj_wbuf_terminate(out);
2✔
509
  }
510

511
  return PROFILE_SUCCESS;
512
}
513

514
int lj_sysprof_report(struct luam_Sysprof_Counters *counters)
3✔
515
{
516
  const struct sysprof *sp = &sysprof;
3✔
517
  if (sp->state != SPS_IDLE)
3✔
518
    return PROFILE_ERRUSE;
519
  memcpy(counters, &sp->counters, sizeof(sp->counters));
3✔
520
  return PROFILE_SUCCESS;
3✔
521
}
522

523
void lj_sysprof_add_proto(const struct GCproto *pt)
52,080✔
524
{
525
  struct sysprof *sp = &sysprof;
52,080✔
526

527
  if (sp->state != SPS_PROFILE || sp->opt.mode == LUAM_SYSPROF_DEFAULT)
52,080✔
528
    return;
529

530
  /*
531
  ** XXX: Avoid sampling during the symtab extension. That shouldn't have any
532
  ** significant effect on profile precision, but if it does, it's better to
533
  ** implement an async-safe queue for the symtab events.
534
  */
535
  sp->state = SPS_IDLE;
×
536
  lj_wbuf_addbyte(&sp->out, LJP_SYMTAB_LFUNC_EVENT);
×
537
  lj_symtab_dump_proto(&sp->out, pt);
×
538
  sp->state = SPS_PROFILE;
×
539
}
540

541
#if LJ_HASJIT
542
void lj_sysprof_add_trace(const struct GCtrace *tr)
2,123✔
543
{
544
  struct sysprof *sp = &sysprof;
2,123✔
545

546
  if (sp->state != SPS_PROFILE || sp->opt.mode == LUAM_SYSPROF_DEFAULT)
2,123✔
547
    return;
548

549
  /* See the comment about the sysprof state above. */
550
  sp->state = SPS_IDLE;
×
551
  lj_wbuf_addbyte(&sp->out, LJP_SYMTAB_TRACE_EVENT);
×
552
  lj_symtab_dump_trace(&sp->out, tr);
×
553
  sp->state = SPS_PROFILE;
×
554
}
555
#endif /* LJ_HASJIT */
556

557
#else /* LJ_HASSYSPROF */
558

559
int lj_sysprof_set_writer(luam_Sysprof_writer writer) {
560
  UNUSED(writer);
561
  return PROFILE_ERRUSE;
562
}
563

564
int lj_sysprof_set_on_stop(luam_Sysprof_on_stop on_stop) {
565
  UNUSED(on_stop);
566
  return PROFILE_ERRUSE;
567
}
568

569
int lj_sysprof_set_backtracer(luam_Sysprof_backtracer backtracer) {
570
  UNUSED(backtracer);
571
  return PROFILE_ERRUSE;
572
}
573

574
int lj_sysprof_start(lua_State *L, const struct luam_Sysprof_Options *opt)
575
{
576
  UNUSED(L);
577
  return PROFILE_ERRUSE;
578
}
579

580
int lj_sysprof_stop(lua_State *L)
581
{
582
  UNUSED(L);
583
  return PROFILE_ERRUSE;
584
}
585

586
int lj_sysprof_report(struct luam_Sysprof_Counters *counters)
587
{
588
  UNUSED(counters);
589
  return PROFILE_ERRUSE;
590
}
591

592
void lj_sysprof_add_proto(const struct GCproto *pt)
593
{
594
  UNUSED(pt);
595
}
596

597
#if LJ_HASJIT
598
void lj_sysprof_add_trace(const struct GCtrace *tr)
599
{
600
  UNUSED(tr);
601
}
602
#endif /* LJ_HASJIT */
603

604
#endif /* LJ_HASSYSPROF */
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

© 2026 Coveralls, Inc