Hi.
I've been doing some pipe performance tests (for heavy inter-thread
communication, so I can have callbacks called only when there's data to
be handled) and I've figured out some strange libevent overhead.
The event_base_loop() take 22% of self time and event_queue_remove()
plus event_queue_insert() take 14% of self time each.
Am I doing something wrong?
Libevent version - 1.4.1beta.
Gprof output and source code are in attachment.
Be sure to compile libevent with -pg flag and statically link too to
have libevent's functions included into timing.
Let me know if it's not reproducible, I'll appreciate any help.
--
- Eugene 'HMage' Bujak.
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdint.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <time.h>
#include <sys/time.h>
#include <signal.h>
#include <sys/types.h> // required for event.h
#include <event.h>
// C++
#include <string>
using namespace std;
pthread_mutex_t stdout_mutex = PTHREAD_MUTEX_INITIALIZER;
int filedes1[2];
int filedes2[2];
int thread1readcount = 0;
int thread2readcount = 0;
int lastread = 0;
struct timeval tv_bench;
struct event ev_bench;
#define BENCH_INTERVAL 1000 // in ms
#define IN
#define OUT
#ifdef _WIN32
typedef uint32_t socket_t;
#endif
#ifdef unix
typedef int socket_t;
#endif
void
ms_to_timeval (uint32_t IN ms, struct timeval OUT * tv)
{
tv->tv_sec = (ms) / 1000; //(long)floor(ms * 0.001);
tv->tv_usec = (ms - (tv->tv_sec * 1000)) * 1000;
}
int
fd_nonblock (socket_t socket)
{
#ifndef _WIN32
int yes = 1;
return ioctl (socket, FIONBIO, &yes);
#else
unsigned long yes = 1;
return ioctlsocket (socket, FIONBIO, (unsigned long *) &yes);
#endif
}
inline int
net_errno (void)
{
#ifdef _WIN32
return WSAGetLastError ();
#else
return errno;
#endif
}
void
sig_term (int i)
{
static int si = 0;
i = i;
// Save the settings before returning back to default signal handler
signal (SIGINT, SIG_DFL);
signal (SIGTERM, SIG_DFL);
#ifndef _WIN32
// Windows doesn't define this signal
signal (SIGHUP, SIG_DFL);
signal (SIGPIPE, SIG_DFL);
#endif
if (!si)
{
si++;
// cleanup ();
#ifdef _WIN32
_exit (0);
#else
exit (0);
#endif
}
}
void
on_bench (int fd, short ev, void *arg)
{
fprintf (stdout, "read_per_sec = %i\n", thread1readcount - lastread);
lastread = thread1readcount;
ms_to_timeval (BENCH_INTERVAL, &tv_bench);
event_add (&ev_bench, &tv_bench);
}
typedef struct thread_s
{
thread_s ()
{
base = NULL;
buf_read = evbuffer_new ();
buf_write = evbuffer_new ();
readcount = NULL;
}
event_base *base;
struct event ev_read;
struct event ev_write;
int fd_read;
int fd_write;
struct evbuffer *buf_read;
struct evbuffer *buf_write;
int *readcount;
} thread_t;
void
on_thread_read (int fd_read, short ev, void *arg)
{
thread_t *thread = (thread_t *) arg;
// fprintf(stdout, "on_thread_read(%p)\n", thread);
int remtoread = 4 - EVBUFFER_LENGTH (thread->buf_read);
if (remtoread)
{
int retval = evbuffer_read (thread->buf_read, fd_read, remtoread);
if ((retval < 0) && (net_errno () == EINTR))
return; // call would block, try later
if ((retval < 0) && (net_errno () == EWOULDBLOCK))
return; // call would block, try later
if ((retval < 0) && (net_errno () == EAGAIN))
return; // call would block, try later
if (retval < 0)
{
int error = net_errno ();
perror ("read(pipe)");
event_del (&thread->ev_read);
event_del (&thread->ev_write);
return;
}
if (retval == 0)
{
return;
}
}
if (EVBUFFER_LENGTH (thread->buf_read) != 4)
{
fprintf (stderr, "buf_read length != 4\n");
// event_del(&thread->ev_read);
// event_del(&thread->ev_write);
return;
}
int recvint = ((int *) EVBUFFER_DATA (thread->buf_read))[0];
if (recvint != *thread->readcount)
{
// fprintf(stderr, "received data is not valid = %i, expected %i\n",
recvint, *thread->readcount);
// event_del(&thread->ev_read);
// event_del(&thread->ev_write);
// return;
}
evbuffer_add (thread->buf_write, thread->readcount, 4);
evbuffer_drain (thread->buf_read, 4);
event_add (&thread->ev_write, NULL);
(*thread->readcount)++;
}
void
on_thread_write (int fd_write, short ev, void *arg)
{
thread_t *thread = (thread_t *) arg;
// fprintf(stdout, "on_thread_write(%p)\n", thread);
if (EVBUFFER_LENGTH (thread->buf_write) == 0)
{
event_del (&thread->ev_write);
return;
}
int retval = evbuffer_write (thread->buf_write, fd_write);
if ((retval < 0) && (net_errno () == EINTR))
return; // call would block, try later
if ((retval < 0) && (net_errno () == EWOULDBLOCK))
return; // call would block, try later
if ((retval < 0) && (net_errno () == EAGAIN))
return; // call would block, try later
if (retval == 0)
{
// That's strange, send() function was successful but we've written
nothing
fprintf (stderr,
"SHOULD NOT HAPPEN -- Had send() ready, but zero bytes were
written\n");
return;
}
if (retval <= 0)
{
int error = net_errno ();
perror ("write(pipe)");
event_del (&thread->ev_read);
event_del (&thread->ev_write);
return;
}
if (EVBUFFER_LENGTH (thread->buf_write) == 0)
{
// no more data to send
event_del (&thread->ev_write);
}
}
void
thread_initevent (thread_t * thread)
{
int retval = 0;
thread->base = (event_base *) event_init ();
event_set (&thread->ev_read, thread->fd_read, EV_READ | EV_PERSIST,
on_thread_read, thread);
event_set (&thread->ev_write, thread->fd_write, EV_WRITE | EV_PERSIST,
on_thread_write, thread);
retval = event_base_set (thread->base, &thread->ev_read);
if (retval < 0)
perror ("event_base_set");
retval = event_base_set (thread->base, &thread->ev_write);
if (retval < 0)
perror ("event_base_set");
retval = event_add (&thread->ev_read, NULL);
if (retval < 0)
perror ("event_add");
}
void *
thread1_main (void *arg)
{
thread_t thread;
thread.fd_read = filedes2[0];
thread.fd_write = filedes1[1];
thread.readcount = &thread1readcount;
thread_initevent (&thread);
event_base_dispatch (thread.base);
return NULL;
}
void *
thread2_main (void *arg)
{
thread_t thread;
thread.fd_read = filedes1[0];
thread.fd_write = filedes2[1];
thread.readcount = &thread2readcount;
thread_initevent (&thread);
evbuffer_add (thread.buf_write, thread.readcount, 4);
event_add (&thread.ev_write, NULL);
event_base_dispatch (thread.base);
return NULL;
}
int
main (int argc, char *argv[])
{
int retval = 0;
pthread_t threads[2];
signal(SIGINT, sig_term);
signal(SIGTERM, sig_term);
#ifndef _WIN32
// Windows doesn't define these signals
signal(SIGHUP, sig_term);
signal(SIGPIPE, sig_term);
#endif
retval = pipe (filedes1);
if (retval < 0)
{
perror ("pipe");
return 1;
}
retval = pipe (filedes2);
if (retval < 0)
{
perror ("pipe");
return 1;
}
retval = fd_nonblock (filedes1[0]);
if (retval < 0)
{
perror ("fd_nonblock");
return 1;
}
retval = fd_nonblock (filedes1[1]);
if (retval < 0)
{
perror ("fd_nonblock");
return 1;
}
retval = fd_nonblock (filedes2[0]);
if (retval < 0)
{
perror ("fd_nonblock");
return 1;
}
retval = fd_nonblock (filedes2[1]);
if (retval < 0)
{
perror ("fd_nonblock");
return 1;
}
retval = pthread_create (&threads[0], NULL, thread1_main, NULL);
if (retval)
{
fprintf (stderr, "Couldn't create thread.\n");
return 1;
}
retval = pthread_create (&threads[1], NULL, thread2_main, NULL);
if (retval)
{
fprintf (stderr, "Couldn't create thread 2.\n");
pthread_cancel (threads[0]);
return 1;
}
event_base *base = (event_base *) event_init ();
event_set (&ev_bench, -1, 0, on_bench, NULL);
event_base_set (base, &ev_bench);
ms_to_timeval (BENCH_INTERVAL, &tv_bench);
event_add (&ev_bench, &tv_bench);
event_base_dispatch (base);
}
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
22.58 0.07 0.07 3 23.34 100.02 event_base_loop
14.52 0.12 0.05 1327431 0.00 0.00 event_queue_remove
14.52 0.16 0.05 1324910 0.00 0.00 event_queue_insert
9.68 0.19 0.03 836768 0.00 0.00 epoll_dispatch
9.68 0.22 0.03 835087 0.00 0.00 epoll_wait
6.45 0.24 0.02 362924 0.00 0.00 evbuffer_read
3.23 0.25 0.01 442825 0.00 0.00 on_thread_write(int,
short, void*)
3.23 0.26 0.01 442774 0.00 0.00 event_add
3.23 0.27 0.01 442763 0.00 0.00 epoll_add
3.23 0.28 0.01 442606 0.00 0.00 on_thread_read(int,
short, void*)
3.23 0.29 0.01 442505 0.00 0.00 event_del
1.61 0.30 0.01 882060 0.00 0.00 event_active
1.61 0.30 0.01 442824 0.00 0.00 evbuffer_write
1.61 0.31 0.01 evbuffer_expand
1.61 0.31 0.01 event_loopbreak
0.00 0.31 0.00 885234 0.00 0.00 epoll_ctl
0.00 0.31 0.00 442759 0.00 0.00 evbuffer_drain
0.00 0.31 0.00 442726 0.00 0.00 evbuffer_add
0.00 0.31 0.00 442542 0.00 0.00 epoll_del
0.00 0.31 0.00 12 0.00 0.00 evsignal_process
0.00 0.31 0.00 8 0.00 0.00 event_set
0.00 0.31 0.00 7 0.00 0.00 on_bench(int, short,
void*)
0.00 0.31 0.00 5 0.00 0.00 event_base_set
0.00 0.31 0.00 3 0.00 0.00 epoll_create
0.00 0.31 0.00 3 0.00 0.00 epoll_init
0.00 0.31 0.00 3 0.00 0.00 evbuffer_new
0.00 0.31 0.00 3 0.00 100.02 event_base_dispatch
0.00 0.31 0.00 3 0.00 0.00 event_base_new
0.00 0.31 0.00 3 0.00 0.00 event_base_priority_init
0.00 0.31 0.00 3 0.00 0.00 event_init
0.00 0.31 0.00 3 0.00 0.00 evsignal_init
0.00 0.31 0.00 3 0.00 0.00
evutil_make_socket_nonblocking
0.00 0.31 0.00 3 0.00 0.00 evutil_socketpair
0.00 0.31 0.00 2 0.00 0.00
thread_initevent(thread_s*)
% the percentage of the total running time of the
time program used by this function.
cumulative a running sum of the number of seconds accounted
seconds for by this function and those listed above it.
self the number of seconds accounted for by this
seconds function alone. This is the major sort for this
listing.
calls the number of times this function was invoked, if
this function is profiled, else blank.
self the average number of milliseconds spent in this
ms/call function per call, if this function is profiled,
else blank.
total the average number of milliseconds spent in this
ms/call function and its descendents per call, if this
function is profiled, else blank.
name the name of the function. This is the minor sort
for this listing. The index shows the location of
the function in the gprof listing. If the index is
in parenthesis it shows where it would appear in
the gprof listing if it were to be printed.
Call graph (explanation follows)
granularity: each sample hit covers 2 byte(s) for 3.23% of 0.31 seconds
index % time self children called name
0.00 0.10 1/3 thread2_main(void*) [3]
0.00 0.10 1/3 thread1_main(void*) [4]
0.00 0.10 1/3 main [5]
[1] 96.8 0.00 0.30 3 event_base_dispatch [1]
0.07 0.23 3/3 event_base_loop [2]
-----------------------------------------------
0.07 0.23 3/3 event_base_dispatch [1]
[2] 96.8 0.07 0.23 3 event_base_loop [2]
0.03 0.06 836768/836768 epoll_dispatch [6]
0.01 0.06 442606/442606 on_thread_read(int, short,
void*) [7]
0.03 0.00 884926/1327431 event_queue_remove [8]
0.01 0.02 442505/442505 event_del [12]
0.01 0.01 442825/442825 on_thread_write(int, short,
void*) [14]
0.00 0.00 7/442774 event_add [11]
0.00 0.00 7/1324910 event_queue_insert [9]
0.00 0.00 7/7 on_bench(int, short, void*)
[119]
-----------------------------------------------
<spontaneous>
[3] 32.3 0.00 0.10 thread2_main(void*) [3]
0.00 0.10 1/3 event_base_dispatch [1]
0.00 0.00 1/2 thread_initevent(thread_s*)
[20]
0.00 0.00 1/442774 event_add [11]
0.00 0.00 1/442763 epoll_add [15]
0.00 0.00 1/3 evbuffer_new [30]
-----------------------------------------------
<spontaneous>
[4] 32.3 0.00 0.10 thread1_main(void*) [4]
0.00 0.10 1/3 event_base_dispatch [1]
0.00 0.00 1/2 thread_initevent(thread_s*)
[20]
0.00 0.00 2/3 evbuffer_new [30]
-----------------------------------------------
<spontaneous>
[5] 32.3 0.00 0.10 main [5]
0.00 0.10 1/3 event_base_dispatch [1]
0.00 0.00 1/442774 event_add [11]
0.00 0.00 1/3 event_init [33]
0.00 0.00 1/8 event_set [26]
0.00 0.00 1/5 event_base_set [27]
-----------------------------------------------
0.03 0.06 836768/836768 event_base_loop [2]
[6] 30.6 0.03 0.06 836768 epoll_dispatch [6]
0.03 0.00 835087/835087 epoll_wait [10]
0.03 0.00 882125/1324910 event_queue_insert [9]
0.01 0.00 882060/882060 event_active [16]
0.00 0.00 12/12 evsignal_process [25]
-----------------------------------------------
0.01 0.06 442606/442606 event_base_loop [2]
[7] 21.0 0.01 0.06 442606 on_thread_read(int, short, void*)
[7]
0.01 0.02 442763/442774 event_add [11]
0.02 0.00 362924/362924 evbuffer_read [13]
0.01 0.00 442760/442763 epoll_add [15]
0.00 0.00 442759/442759 evbuffer_drain [22]
0.00 0.00 442726/442726 evbuffer_add [23]
-----------------------------------------------
0.02 0.00 442505/1327431 event_del [12]
0.03 0.00 884926/1327431 event_base_loop [2]
[8] 14.5 0.05 0.00 1327431 event_queue_remove [8]
-----------------------------------------------
0.00 0.00 7/1324910 event_base_loop [2]
0.02 0.00 442778/1324910 event_add [11]
0.03 0.00 882125/1324910 epoll_dispatch [6]
[9] 14.5 0.05 0.00 1324910 event_queue_insert [9]
-----------------------------------------------
0.03 0.00 835087/835087 epoll_dispatch [6]
[10] 9.7 0.03 0.00 835087 epoll_wait [10]
-----------------------------------------------
0.00 0.00 1/442774 thread2_main(void*) [3]
0.00 0.00 1/442774 main [5]
0.00 0.00 2/442774 thread_initevent(thread_s*)
[20]
0.00 0.00 7/442774 event_base_loop [2]
0.01 0.02 442763/442774 on_thread_read(int, short,
void*) [7]
[11] 8.1 0.01 0.02 442774 event_add [11]
0.02 0.00 442778/1324910 event_queue_insert [9]
-----------------------------------------------
0.01 0.02 442505/442505 event_base_loop [2]
[12] 8.1 0.01 0.02 442505 event_del [12]
0.02 0.00 442505/1327431 event_queue_remove [8]
0.00 0.00 442542/442542 epoll_del [24]
-----------------------------------------------
0.02 0.00 362924/362924 on_thread_read(int, short,
void*) [7]
[13] 6.5 0.02 0.00 362924 evbuffer_read [13]
-----------------------------------------------
0.01 0.01 442825/442825 event_base_loop [2]
[14] 4.8 0.01 0.01 442825 on_thread_write(int, short, void*)
[14]
0.01 0.00 442824/442824 evbuffer_write [17]
-----------------------------------------------
0.00 0.00 1/442763 thread2_main(void*) [3]
0.00 0.00 2/442763 thread_initevent(thread_s*)
[20]
0.01 0.00 442760/442763 on_thread_read(int, short,
void*) [7]
[15] 3.2 0.01 0.00 442763 epoll_add [15]
0.00 0.00 442734/885234 epoll_ctl [21]
-----------------------------------------------
0.01 0.00 882060/882060 epoll_dispatch [6]
[16] 1.6 0.01 0.00 882060 event_active [16]
-----------------------------------------------
0.01 0.00 442824/442824 on_thread_write(int, short,
void*) [14]
[17] 1.6 0.01 0.00 442824 evbuffer_write [17]
-----------------------------------------------
<spontaneous>
[18] 1.6 0.01 0.00 evbuffer_expand [18]
-----------------------------------------------
<spontaneous>
[19] 1.6 0.01 0.00 event_loopbreak [19]
-----------------------------------------------
0.00 0.00 1/2 thread2_main(void*) [3]
0.00 0.00 1/2 thread1_main(void*) [4]
[20] 0.0 0.00 0.00 2 thread_initevent(thread_s*) [20]
0.00 0.00 2/442774 event_add [11]
0.00 0.00 2/442763 epoll_add [15]
0.00 0.00 4/8 event_set [26]
0.00 0.00 4/5 event_base_set [27]
0.00 0.00 2/3 event_init [33]
-----------------------------------------------
0.00 0.00 442500/885234 epoll_del [24]
0.00 0.00 442734/885234 epoll_add [15]
[21] 0.0 0.00 0.00 885234 epoll_ctl [21]
-----------------------------------------------
0.00 0.00 442759/442759 on_thread_read(int, short,
void*) [7]
[22] 0.0 0.00 0.00 442759 evbuffer_drain [22]
-----------------------------------------------
0.00 0.00 442726/442726 on_thread_read(int, short,
void*) [7]
[23] 0.0 0.00 0.00 442726 evbuffer_add [23]
-----------------------------------------------
0.00 0.00 442542/442542 event_del [12]
[24] 0.0 0.00 0.00 442542 epoll_del [24]
0.00 0.00 442500/885234 epoll_ctl [21]
-----------------------------------------------
0.00 0.00 12/12 epoll_dispatch [6]
[25] 0.0 0.00 0.00 12 evsignal_process [25]
-----------------------------------------------
0.00 0.00 1/8 main [5]
0.00 0.00 3/8 evsignal_init [34]
0.00 0.00 4/8 thread_initevent(thread_s*)
[20]
[26] 0.0 0.00 0.00 8 event_set [26]
-----------------------------------------------
0.00 0.00 1/5 main [5]
0.00 0.00 4/5 thread_initevent(thread_s*)
[20]
[27] 0.0 0.00 0.00 5 event_base_set [27]
-----------------------------------------------
0.00 0.00 3/3 epoll_init [29]
[28] 0.0 0.00 0.00 3 epoll_create [28]
-----------------------------------------------
0.00 0.00 3/3 event_base_new [31]
[29] 0.0 0.00 0.00 3 epoll_init [29]
0.00 0.00 3/3 epoll_create [28]
0.00 0.00 3/3 evsignal_init [34]
-----------------------------------------------
0.00 0.00 1/3 thread2_main(void*) [3]
0.00 0.00 2/3 thread1_main(void*) [4]
[30] 0.0 0.00 0.00 3 evbuffer_new [30]
-----------------------------------------------
0.00 0.00 3/3 event_init [33]
[31] 0.0 0.00 0.00 3 event_base_new [31]
0.00 0.00 3/3 epoll_init [29]
0.00 0.00 3/3 event_base_priority_init [32]
-----------------------------------------------
0.00 0.00 3/3 event_base_new [31]
[32] 0.0 0.00 0.00 3 event_base_priority_init [32]
-----------------------------------------------
0.00 0.00 1/3 main [5]
0.00 0.00 2/3 thread_initevent(thread_s*)
[20]
[33] 0.0 0.00 0.00 3 event_init [33]
0.00 0.00 3/3 event_base_new [31]
-----------------------------------------------
0.00 0.00 3/3 epoll_init [29]
[34] 0.0 0.00 0.00 3 evsignal_init [34]
0.00 0.00 3/3 evutil_socketpair [36]
0.00 0.00 3/3 evutil_make_socket_nonblocking
[35]
0.00 0.00 3/8 event_set [26]
-----------------------------------------------
0.00 0.00 3/3 evsignal_init [34]
[35] 0.0 0.00 0.00 3 evutil_make_socket_nonblocking [35]
-----------------------------------------------
0.00 0.00 3/3 evsignal_init [34]
[36] 0.0 0.00 0.00 3 evutil_socketpair [36]
-----------------------------------------------
0.00 0.00 7/7 event_base_loop [2]
[119] 0.0 0.00 0.00 7 on_bench(int, short, void*) [119]
-----------------------------------------------
This table describes the call tree of the program, and was sorted by
the total amount of time spent in each function and its children.
Each entry in this table consists of several lines. The line with the
index number at the left hand margin lists the current function.
The lines above it list the functions that called this function,
and the lines below it list the functions this one called.
This line lists:
index A unique number given to each element of the table.
Index numbers are sorted numerically.
The index number is printed next to every function name so
it is easier to look up where the function in the table.
% time This is the percentage of the `total' time that was spent
in this function and its children. Note that due to
different viewpoints, functions excluded by options, etc,
these numbers will NOT add up to 100%.
self This is the total amount of time spent in this function.
children This is the total amount of time propagated into this
function by its children.
called This is the number of times the function was called.
If the function called itself recursively, the number
only includes non-recursive calls, and is followed by
a `+' and the number of recursive calls.
name The name of the current function. The index number is
printed after it. If the function is a member of a
cycle, the cycle number is printed between the
function's name and the index number.
For the function's parents, the fields have the following meanings:
self This is the amount of time that was propagated directly
from the function into this parent.
children This is the amount of time that was propagated from
the function's children into this parent.
called This is the number of times this parent called the
function `/' the total number of times the function
was called. Recursive calls to the function are not
included in the number after the `/'.
name This is the name of the parent. The parent's index
number is printed after it. If the parent is a
member of a cycle, the cycle number is printed between
the name and the index number.
If the parents of the function cannot be determined, the word
`<spontaneous>' is printed in the `name' field, and all the other
fields are blank.
For the function's children, the fields have the following meanings:
self This is the amount of time that was propagated directly
from the child into the function.
children This is the amount of time that was propagated from the
child's children to the function.
called This is the number of times the function called
this child `/' the total number of times the child
was called. Recursive calls by the child are not
listed in the number after the `/'.
name This is the name of the child. The child's index
number is printed after it. If the child is a
member of a cycle, the cycle number is printed
between the name and the index number.
If there are any cycles (circles) in the call graph, there is an
entry for the cycle-as-a-whole. This entry shows who called the
cycle (as parents) and the members of the cycle (as children.)
The `+' recursive calls entry shows the number of function calls that
were internal to the cycle, and the calls entry for each member shows,
for that member, how many times it was called from other members of
the cycle.
Index by function name
[7] on_thread_read(int, short, void*) [22] evbuffer_drain [12] event_del
[14] on_thread_write(int, short, void*) [18] evbuffer_expand [33] event_init
[20] thread_initevent(thread_s*) [30] evbuffer_new [19] event_loopbreak
[119] on_bench(int, short, void*) [13] evbuffer_read [9]
event_queue_insert (event.c)
[15] epoll_add [17] evbuffer_write [8]
event_queue_remove (event.c)
[28] epoll_create [16] event_active [26] event_set
[21] epoll_ctl [11] event_add [34] evsignal_init
[24] epoll_del [1] event_base_dispatch [25] evsignal_process
[6] epoll_dispatch [2] event_base_loop [35]
evutil_make_socket_nonblocking
[29] epoll_init [31] event_base_new [36] evutil_socketpair
[10] epoll_wait [32] event_base_priority_init
[23] evbuffer_add [27] event_base_set
_______________________________________________
Libevent-users mailing list
Libevent-users@monkey.org
http://monkeymail.org/mailman/listinfo/libevent-users