diff --git a/.gitignore b/.gitignore index 4af03587..216a8f74 100644 --- a/.gitignore +++ b/.gitignore @@ -94,6 +94,7 @@ cscope* /sample/signal-test /sample/time-test /sample/event-test +/sample/watch-timing /test-driver /test/bench diff --git a/CMakeLists.txt b/CMakeLists.txt index f1e5240b..aecff467 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -737,6 +737,7 @@ set(HDR_PUBLIC include/event2/event.h include/event2/event_compat.h include/event2/event_struct.h + include/event2/watch.h include/event2/http.h include/event2/http_compat.h include/event2/http_struct.h @@ -765,6 +766,7 @@ set(SRC_CORE evutil.c evutil_rand.c evutil_time.c + watch.c listener.c log.c signal.c @@ -953,11 +955,15 @@ if (NOT EVENT__DISABLE_SAMPLES) hello-world signal-test http-connect - time-test) + time-test + watch-timing) foreach(SAMPLE ${SAMPLES}) add_sample_prog(OFF ${SAMPLE} sample/${SAMPLE}.c) endforeach() + if (NOT WIN32) + target_link_libraries(watch-timing m) + endif() if (NOT EVENT__DISABLE_OPENSSL) add_sample_prog(ON https-client @@ -1061,6 +1067,7 @@ if (NOT EVENT__DISABLE_TESTS) test/regress_testutils.c test/regress_testutils.h test/regress_util.c + test/regress_watch.c test/tinytest.c) if (WIN32) diff --git a/Makefile.am b/Makefile.am index c2855956..acc5ecff 100644 --- a/Makefile.am +++ b/Makefile.am @@ -237,6 +237,7 @@ CORE_SRC = \ evutil.c \ evutil_rand.c \ evutil_time.c \ + watch.c \ listener.c \ log.c \ $(SYS_SRC) diff --git a/event-internal.h b/event-internal.h index 92941b71..ed36fb0b 100644 --- a/event-internal.h +++ b/event-internal.h @@ -32,6 +32,7 @@ extern "C" { #endif #include "event2/event-config.h" +#include "event2/watch.h" #include "evconfig-private.h" #include @@ -205,6 +206,52 @@ struct event_once { void *arg; }; +/** Contextual information passed from event_base_loop to the "prepare" watcher + * callbacks. We define this as a struct rather than individual parameters to + * the callback function for the sake of future extensibility. */ +struct evwatch_prepare_cb_info { + /** The timeout duration passed to the underlying implementation's `dispatch`. + * See evwatch_prepare_get_timeout. */ + const struct timeval *timeout; +}; + +/** Contextual information passed from event_base_loop to the "check" watcher + * callbacks. We define this as a struct rather than individual parameters to + * the callback function for the sake of future extensibility. */ +struct evwatch_check_cb_info { + /** Placeholder, since empty struct is not allowed by some compilers. */ + void *unused; +}; + +/** Watcher types (prepare and check, perhaps others in the future). */ +#define EVWATCH_PREPARE 0 +#define EVWATCH_CHECK 1 +#define EVWATCH_MAX 2 + +/** Handle to a "prepare" or "check" callback, registered in event_base. */ +union evwatch_cb { + evwatch_prepare_cb prepare; + evwatch_check_cb check; +}; +struct evwatch { + /** Tail queue pointers, called "next" by convention in libevent. + * See */ + TAILQ_ENTRY(evwatch) next; + + /** Pointer to owning event loop */ + struct event_base *base; + + /** Watcher type (see above) */ + unsigned type; + + /** Callback function */ + union evwatch_cb callback; + + /** User-defined argument for callback function */ + void *arg; +}; +TAILQ_HEAD(evwatch_list, evwatch); + struct event_base { /** Function pointers and other data to describe this event_base's * backend. */ @@ -346,6 +393,8 @@ struct event_base { /** List of event_onces that have not yet fired. */ LIST_HEAD(once_event_list, event_once) once_events; + /** "Prepare" and "check" watchers. */ + struct evwatch_list watchers[EVWATCH_MAX]; }; struct event_config_entry { diff --git a/event.c b/event.c index bfd94ebd..34f28ba2 100644 --- a/event.c +++ b/event.c @@ -59,6 +59,7 @@ #include "event2/event.h" #include "event2/event_struct.h" #include "event2/event_compat.h" +#include "event2/watch.h" #include "event-internal.h" #include "defer-internal.h" #include "evthread-internal.h" @@ -737,6 +738,10 @@ event_base_new_with_config(const struct event_config *cfg) event_base_start_iocp_(base, cfg->n_cpus_hint); #endif + /* initialize watcher lists */ + for (i = 0; i < EVWATCH_MAX; ++i) + TAILQ_INIT(&base->watchers[i]); + return (base); } @@ -839,6 +844,7 @@ event_base_free_(struct event_base *base, int run_finalizers) { int i, n_deleted=0; struct event *ev; + struct evwatch *watcher; /* XXXX grab the lock? If there is contention when one thread frees * the base, then the contending thread will be very sad soon. */ @@ -939,6 +945,15 @@ event_base_free_(struct event_base *base, int run_finalizers) EVTHREAD_FREE_LOCK(base->th_base_lock, 0); EVTHREAD_FREE_COND(base->current_event_cond); + /* Free all event watchers */ + for (i = 0; i < EVWATCH_MAX; ++i) { + while (!TAILQ_EMPTY(&base->watchers[i])) { + watcher = TAILQ_FIRST(&base->watchers[i]); + TAILQ_REMOVE(&base->watchers[i], watcher, next); + mm_free(watcher); + } + } + /* If we're freeing current_base, there won't be a current_base. */ if (base == current_base) current_base = NULL; @@ -1926,9 +1941,12 @@ event_base_loop(struct event_base *base, int flags) struct timeval tv; struct timeval *tv_p; int res, done, retval = 0; + struct evwatch_prepare_cb_info prepare_info; + struct evwatch_check_cb_info check_info; + struct evwatch *watcher; /* Grab the lock. We will release it inside evsel.dispatch, and again - * as we invoke user callbacks. */ + * as we invoke watchers and user callbacks. */ EVBASE_ACQUIRE_LOCK(base, th_base_lock); if (base->running_loop) { @@ -1987,6 +2005,13 @@ event_base_loop(struct event_base *base, int flags) event_queue_make_later_events_active(base); + /* Invoke prepare watchers before polling for events */ + EVBASE_RELEASE_LOCK(base, th_base_lock); + prepare_info.timeout = tv_p; + TAILQ_FOREACH(watcher, &base->watchers[EVWATCH_PREPARE], next) + (*watcher->callback.prepare)(watcher, &prepare_info, watcher->arg); + EVBASE_ACQUIRE_LOCK(base, th_base_lock); + clear_time_cache(base); res = evsel->dispatch(base, tv_p); @@ -2000,6 +2025,13 @@ event_base_loop(struct event_base *base, int flags) update_time_cache(base); + /* Invoke check watchers after polling for events, and before + * processing them */ + EVBASE_RELEASE_LOCK(base, th_base_lock); + TAILQ_FOREACH(watcher, &base->watchers[EVWATCH_CHECK], next) + (*watcher->callback.check)(watcher, &check_info, watcher->arg); + EVBASE_ACQUIRE_LOCK(base, th_base_lock); + timeout_process(base); if (N_ACTIVE_CALLBACKS(base)) { diff --git a/include/event2/watch.h b/include/event2/watch.h new file mode 100644 index 00000000..e3a6e609 --- /dev/null +++ b/include/event2/watch.h @@ -0,0 +1,134 @@ +/* + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#ifndef EVENT2_WATCH_H_INCLUDED_ +#define EVENT2_WATCH_H_INCLUDED_ + +/** @file event2/watch.h + + "Prepare" and "check" watchers. A "prepare" watcher is a callback that fires + immediately before polling for I/O. A "check" watcher is a callback that + fires immediately after polling and before processing any active events. This + may be useful for embedding other libraries' event loops (e.g. UI toolkits) + into libevent's. + + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct event_base; +struct evwatch; +struct evwatch_prepare_cb_info; +struct evwatch_check_cb_info; +struct timeval; + +/** + Prepare callback, invoked by event_base_loop immediately before polling for + I/O. + + @param watcher the prepare watcher that invoked this callback. + @param info contextual information passed from event_base_loop. + @param arg additional user-defined argument, set in `evwatch_prepare_new`. + */ +typedef void (*evwatch_prepare_cb)(struct evwatch *, const struct evwatch_prepare_cb_info *, void *); + +/** + Check callback, invoked by event_base_loop immediately after polling for I/O + and before processing any active events. + + @param watcher the check watcher that invoked this callback. + @param info contextual information passed from event_base_loop. + @param arg additional user-defined argument, set in `evwatch_check_new`. + */ +typedef void (*evwatch_check_cb)(struct evwatch *, const struct evwatch_check_cb_info *, void *); + +/** + Register a new "prepare" watcher, to be called in the event loop prior to + polling for events. Watchers will be called in the order they were + registered. + + @param base the event_base to operate on. + @param callback the callback function to invoke. + @param arg additional user-defined argument provided to the callback. + @return a pointer to the newly allocated event watcher. + */ +EVENT2_EXPORT_SYMBOL +struct evwatch *evwatch_prepare_new(struct event_base *base, evwatch_prepare_cb callback, void *arg); + +/** + Register a new "check" watcher, to be called in the event loop after polling + for events and before handling them. Watchers will be called in the order + they were registered. + + @param base the event_base to operate on. + @param callback the callback function to invoke. + @param arg additional user-defined argument provided to the callback. + @return a pointer to the newly allocated event watcher. + */ +EVENT2_EXPORT_SYMBOL +struct evwatch *evwatch_check_new(struct event_base *base, evwatch_check_cb callback, void *arg); + +/** + Get the event_base that a given evwatch is registered with. + + @param watcher the watcher to get the event_base for. + @return the event_base for the given watcher. + */ +EVENT2_EXPORT_SYMBOL +struct event_base *evwatch_base(struct evwatch *watcher); + +/** + Deregister and deallocate a watcher. Any watchers not freed using + evwatch_free will eventually be deallocated in event_base_free + (calling evwatch_free on a watcher after event_base_free has been + called on its corresponding event_base is an error). + + @param watcher the watcher to deregister and deallocate. + */ +EVENT2_EXPORT_SYMBOL +void evwatch_free(struct evwatch *watcher); + +/** + Get the timeout (the expected polling duration) passed to the underlying + implementation's `dispatch`. This value will only be set if there are pending + EV_TIMEOUT events and if the event_base isn't in EVLOOP_NONBLOCK mode. It may + be a useful performance statistic to compare the expected polling duration + against the actual polling duration (that is, the time difference measured + between this prepare callback and the following check callback). + + @param info the "prepare" callback info. + @param timeout address of a timeval to write the polling duration to. + @return 1 if a value was written to *timeout, or 0 if not. + */ +EVENT2_EXPORT_SYMBOL +int evwatch_prepare_get_timeout(const struct evwatch_prepare_cb_info *info, struct timeval *timeout); + +#ifdef __cplusplus +} +#endif + +#endif /* EVENT2_WATCH_H_INCLUDED_ */ diff --git a/include/include.am b/include/include.am index 9aad2dba..8c9d4f7f 100644 --- a/include/include.am +++ b/include/include.am @@ -19,6 +19,7 @@ EVENT2_EXPORT = \ include/event2/event.h \ include/event2/event_compat.h \ include/event2/event_struct.h \ + include/event2/watch.h \ include/event2/http.h \ include/event2/http_compat.h \ include/event2/http_struct.h \ diff --git a/sample/include.am b/sample/include.am index 266274d0..0343a24a 100644 --- a/sample/include.am +++ b/sample/include.am @@ -11,7 +11,8 @@ SAMPLES = \ sample/http-server \ sample/http-connect \ sample/signal-test \ - sample/time-test + sample/time-test \ + sample/watch-timing if OPENSSL SAMPLES += sample/le-proxy @@ -56,3 +57,5 @@ sample_http_server_SOURCES = sample/http-server.c sample_http_server_LDADD = $(LIBEVENT_GC_SECTIONS) libevent.la sample_http_connect_SOURCES = sample/http-connect.c sample_http_connect_LDADD = $(LIBEVENT_GC_SECTIONS) libevent.la +sample_watch_timing_SOURCES = sample/watch-timing.c +sample_watch_timing_LDADD = $(LIBEVENT_GC_SECTIONS) libevent.la -lm diff --git a/sample/watch-timing.c b/sample/watch-timing.c new file mode 100644 index 00000000..9babd32b --- /dev/null +++ b/sample/watch-timing.c @@ -0,0 +1,344 @@ +#include +#include +#include +#include +#ifdef EVENT__HAVE_SYS_TIME_H +#include +#endif +#include + +#include +#include +#include + +/** + An approximate histogram in constant space, based on Ben-Haim & Yom-Tov, "A + Streaming Parallel Decision Tree Algorithm" [1] and a previous implementation + in Java by Dan Rosen [2]. The histogram is represented as an array of + contiguous bins of non-uniform width. Each bin is centered on a certain point, + called its "centroid," and summarizes some "count" of observations. The bins + are ordered in the array by their centroids; an array is used rather than a + linked structure for CPU cache friendliness. + + When the histogram is updated with a new observation, a new bin is created for + it, and then the pair of bins with the closest centroids are merged. Since + bins are stored in contiguous memory, this update process requires bins to be + shifted in worst-case linear time. The novel contribution of this + implementation is to maintain an insertion gap adjacent to the most recently + merged bin, such that for "well behaved" input (such as a normal + distribution), the number of shift operations required by an update should be + much less than the total number of bins on average. + + This implementation is almost entirely untested. Don't trust it for + production code. + + [1] http://www.jmlr.org/papers/volume11/ben-haim10a/ben-haim10a.pdf + [2] https://github.com/mergeconflict/histogram + */ + +/** Compare two doubles for equality without the compiler warning. This is + * probably the wrong thing to do, but this is just sample code :) */ +static inline int +eq(double a, double b) +{ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wfloat-equal" + return a == b; +#pragma GCC diagnostic pop +} + +struct bin { + double centroid; + unsigned long count; +}; + +struct histogram { + struct bin *bins; + unsigned max_bins; + unsigned num_bins; + unsigned gap; + unsigned long count; + double min; + double max; +}; + +static struct histogram * +histogram_new(unsigned max_bins) +{ + struct histogram *h = malloc(sizeof(struct histogram)); + h->bins = calloc(max_bins + 1, sizeof(struct bin)); + h->max_bins = max_bins; + h->num_bins = 0; + h->gap = 0; + h->count = 0; + h->min = INFINITY; + h->max = -INFINITY; + return h; +} + +static void +histogram_free(struct histogram *h) +{ + free(h->bins); + free(h); +} + +static void +histogram_update(struct histogram *h, double observation) +{ + unsigned bin; + double delta; + double min_delta = INFINITY; + + /* Update population count, min and max */ + ++(h->count); + if (observation < h->min) + h->min = observation; + if (observation > h->max) + h->max = observation; + + /* Shift the insertion gap to the left or right so that the new bin + * containing the given observation as its centroid will be in the right + * order with respect to the other bins. */ + while (1) { + /* Look at the bin to the left of the gap... */ + if (h->gap != 0) { + /* If its centroid is greater than the observation, move + * the gap to the left and try again... */ + if (h->bins[h->gap - 1].centroid > observation) { + h->bins[h->gap] = h->bins[h->gap - 1]; + --(h->gap); + continue; + } + /* If its centroid is equal to the observation, just + * update its count in place. */ + if (eq(h->bins[h->gap - 1].centroid, observation)) { + ++(h->bins[h->gap - 1].count); + return; + } + } + + /* Look at the bin to the right of the gap... */ + if (h->gap != h->num_bins) { + /* If its centroid is less than the observation, move + * the gap to the right and try again... */ + if (h->bins[h->gap + 1].centroid < observation) { + h->bins[h->gap] = h->bins[h->gap + 1]; + ++(h->gap); + continue; + } + /* If its centroid is equal to the observation, just + * update its count in place. */ + if (eq(h->bins[h->gap + 1].centroid, observation)) { + ++(h->bins[h->gap + 1].count); + return; + } + } + + /* If the gap is in the right place, we're ready to insert. */ + break; + } + + /* Insert the observation into a new bin at the gap. */ + h->bins[h->gap].centroid = observation; + h->bins[h->gap].count = 1; + + /* If the histogram isn't full yet, don't bother merging bins, just + * stick the gap back at the end. */ + if (h->num_bins != h->max_bins) { + h->gap = ++(h->num_bins); + return; + } + + /* Find the two adjacent bins with the closest centroids and merge them. + * The choice whether to leave the gap on the left or right is + * arbitrary (we choose the left). */ + for (bin = 0; bin < h->num_bins; ++bin) { + delta = h->bins[bin + 1].centroid - h->bins[bin].centroid; + if (delta < min_delta) { + min_delta = delta; + h->gap = bin; + } + } + /* The merged centroid is the weighted average of the two, and the + * merged count is the sum of the two. */ + h->bins[h->gap + 1].centroid = + (h->bins[h->gap].centroid * h->bins[h->gap].count + + h->bins[h->gap + 1].centroid * h->bins[h->gap + 1].count) / + (h->bins[h->gap].count + h->bins[h->gap + 1].count); + h->bins[h->gap + 1].count += h->bins[h->gap].count; +} + +static double +histogram_query(const struct histogram *h, double quantile) +{ + unsigned lhs = 0, rhs = 0; + struct bin lhs_bin = { 0, 0 }, rhs_bin = { 0, 0 }; + double lhs_total = 0, rhs_total = 0; + double a = 0, b = 0, c = 0, z = 0; + + /* The "needle" is the n'th value represented by the histogram. For + * example, if the histogram summarizes 100 entries and we're querying + * for the 50th percentile, the needle is 50. */ + double needle = h->count * quantile; + if (quantile <= 0) + return h->min; + if (quantile >= 1) + return h->max; + + /* Divide the histogram into slices: the first slice starts at h->min + * and ends at h->bins[0].centroid, the last slice starts at + * h->bins[h->num_bins].centroid and ends at h->max, and the slices + * in the middle are between adjacent centroids (minding the gap). The + * "count" in each slice is the average of the count in the two bins + * that define it. Find the slice containing the needle by keeping a + * running total of the slice counts. */ + while (rhs_total < needle) { + /* Determine the left-hand side bin of the current slice. Note + * that the first slice has bin 0 on its right-hand side! */ + if (rhs == 0) { + lhs_bin.centroid = h->min; + lhs_bin.count = 0; + } else { + lhs_bin = h->bins[lhs]; + } + + /* Determine the right-hand side bin of the current slice... */ + if (rhs > h->num_bins) { + lhs_bin.centroid = h->max; + rhs_bin.count = 0; + } else { + rhs_bin = h->bins[rhs]; + } + + /* Update the running totals: the lhs total is whatever the rhs + * total was previously, and the new rhs total includes the + * count for this slice. */ + lhs_total = rhs_total; + rhs_total += 0.5 * (lhs_bin.count + rhs_bin.count); + + /* Next iteration's left-hand side is the current iteration's + * right-hand side, and next iteration's right-hand side is one + * bin further right (minding the gap). */ + lhs = rhs++; + if (rhs == h->gap) + rhs++; + } + + /* Approximate the value at the requested quantile... */ + a = rhs_bin.count - lhs_bin.count; + if (eq(a, 0)) { + b = rhs_total - lhs_total; + z = eq(b, 0) ? 0 : (needle - lhs_total) / b; + } else { + b = 2 * lhs_bin.count; + c = 2 * (lhs_total - needle); + z = (-b + sqrt(b * b - 4 * a * c)) / (2 * a); + } + return lhs_bin.centroid + (rhs_bin.centroid - lhs_bin.centroid) * z; +} + +/** + This is an example of one way in which "prepare" and "check" watchers can be + useful. We track histograms of two timing metrics: + + The first is "duration," which is the amount of time between a "check" and the + next "prepare" (in the next iteration of the event loop). This corresponds + pretty closely to the amount of time spent in event handlers (such as the + `on_timeout` handler in this example). In a real-world server, this would + provide a way to monitor whether any of your handlers are blocking or + otherwise performing heavy computation. + + The second is "delay," which is the difference between the actual and expected + polling duration. The actual polling duration is the amount of time between a + "prepare" and the next "check" (in the same iteration of the event loop), and + the expected duration is obtained from `evwatch_prepare_get_timeout`. In a + real-world server, this provides an indication of kernel scheduling delays. + For example, if your server is lightly loaded, this delay should usually be + close to your kernel's scheduling quantum (e.g. 1 millisecond). + */ + +static struct event_base *base; +static struct timeval + prepare_time = { 0, 0 }, + check_time = { 0, 0 }, + expected = { 0, 0 }; +static struct histogram *durations, *delays; + +static void on_prepare(struct evwatch *watcher, const struct evwatch_prepare_cb_info *info, void *arg) +{ + struct timeval duration; + evutil_gettimeofday(&prepare_time, NULL); + evwatch_prepare_get_timeout(info, &expected); + if (check_time.tv_sec != 0) { + evutil_timersub(&prepare_time, &check_time, &duration); + histogram_update(durations, duration.tv_sec + duration.tv_usec / 1000000.0l); + } +} + +static void on_check(struct evwatch *watcher, const struct evwatch_check_cb_info *info, void *arg) +{ + struct timeval actual, delay; + evutil_gettimeofday(&check_time, NULL); + evutil_timersub(&check_time, &prepare_time, &actual); + evutil_timersub(&actual, &expected, &delay); + if (delay.tv_sec >= 0) + histogram_update(delays, delay.tv_sec + delay.tv_usec / 1000000.0l); +} + +static void +on_timeout(evutil_socket_t fd, short events, void *arg) +{ + printf("durations: [p50 = %fs, p95 = %fs], delays: [p50 = %fs, p95 = %fs]\n", + histogram_query(durations, 0.5), + histogram_query(durations, 0.95), + histogram_query(delays, 0.5), + histogram_query(delays, 0.95)); +} + + +static void +on_sigint(evutil_socket_t sig, short events, void *arg) +{ + event_base_loopbreak(base); +} + +int +main(int argc, char **argv) +{ + struct timeval one_second = { 1, 0 }; + struct event *timeout_event, *sigint_event; + + base = event_base_new(); + durations = histogram_new(100); + delays = histogram_new(100); + + /* add prepare and check watchers; no need to hang on to their pointers, + * since they will be freed for us in event_base_free. */ + evwatch_prepare_new(base, &on_prepare, NULL); + evwatch_check_new(base, &on_check, NULL); + + /* set a persistent one second timeout */ + timeout_event = event_new(base, -1, EV_PERSIST, &on_timeout, NULL); + if (!timeout_event) + return EXIT_FAILURE; + event_add(timeout_event, &one_second); + + /* set a handler for interrupt, so we can quit cleanly */ + sigint_event = evsignal_new(base, SIGINT, &on_sigint, NULL); + if (!sigint_event) + return EXIT_FAILURE; + event_add(sigint_event, NULL); + + /* run the event loop until interrupted */ + event_base_dispatch(base); + + /* clean up */ + event_free(timeout_event); + event_free(sigint_event); + event_base_free(base); + histogram_free(durations); + histogram_free(delays); + + return EXIT_SUCCESS; +} diff --git a/test/include.am b/test/include.am index 04375247..ec11b6bf 100644 --- a/test/include.am +++ b/test/include.am @@ -120,6 +120,7 @@ test_regress_SOURCES = \ test/regress_testutils.c \ test/regress_testutils.h \ test/regress_util.c \ + test/regress_watch.c \ test/tinytest.c \ $(regress_thread_SOURCES) \ $(regress_zlib_SOURCES) diff --git a/test/regress.h b/test/regress.h index 643b82ba..8486e019 100644 --- a/test/regress.h +++ b/test/regress.h @@ -53,6 +53,7 @@ extern struct testcase_t ssl_testcases[]; extern struct testcase_t listener_testcases[]; extern struct testcase_t listener_iocp_testcases[]; extern struct testcase_t thread_testcases[]; +extern struct testcase_t watch_testcases[]; extern struct evutil_weakrand_state test_weakrand_state; diff --git a/test/regress_main.c b/test/regress_main.c index c9372825..6c64b0de 100644 --- a/test/regress_main.c +++ b/test/regress_main.c @@ -380,6 +380,7 @@ struct testgroup_t testgroups[] = { { "rpc/", rpc_testcases }, { "thread/", thread_testcases }, { "listener/", listener_testcases }, + { "watch/", watch_testcases }, #ifdef _WIN32 { "iocp/", iocp_testcases }, { "iocp/bufferevent/", bufferevent_iocp_testcases }, diff --git a/test/regress_watch.c b/test/regress_watch.c new file mode 100644 index 00000000..9e340618 --- /dev/null +++ b/test/regress_watch.c @@ -0,0 +1,243 @@ +/* + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#ifdef EVENT__HAVE_SYS_TIME_H +#include +#endif +#include + +#include "event2/event.h" +#include "event2/watch.h" +#include "regress.h" + +static int iteration = 0; +static int prepare_callback_1_count = 0; +static int prepare_callback_2_count = 0; +static int check_callback_1_count = 0; +static int check_callback_2_count = 0; +static struct timeval start_time = { 0, 0 }; +static struct timeval end_time = { 0, 0 }; +static int user_arg = 8675309; + +static void +prepare_callback_1(struct evwatch *watcher, const struct evwatch_prepare_cb_info *info, void *arg) +{ + struct timeval timeout; + int timeout_msec; + + /* user argument should be passed properly */ + tt_ptr_op(arg, ==, &user_arg); + + ++prepare_callback_1_count; + + /* prepare_callback_1 should always fire before prepare_callback_2, and + * before both check callbacks */ + tt_int_op(prepare_callback_1_count, >, prepare_callback_2_count); + tt_int_op(prepare_callback_1_count, >, check_callback_1_count); + tt_int_op(prepare_callback_1_count, >, check_callback_2_count); + + /* if we've just scheduled the timeout event at the beginning of the + * iteration, save the current time and assert that the timeout is + * roughly what we set (this won't be exact on some platforms) */ + if (start_time.tv_sec == 0) { + event_base_gettimeofday_cached(evwatch_base(watcher), &start_time); + tt_int_op(evwatch_prepare_get_timeout(info, &timeout), ==, 1); + + timeout_msec = (timeout.tv_sec * 1000) + (timeout.tv_usec / 1000); + tt_int_op(timeout_msec, >=, 995); + tt_int_op(timeout_msec, <=, 1005); + } +end: + ; +} + +static void +prepare_callback_2(struct evwatch *watcher, const struct evwatch_prepare_cb_info *info, void *arg) +{ + /* user argument should be passed properly */ + tt_ptr_op(arg, ==, &user_arg); + + ++prepare_callback_2_count; + + /* prepare_callback_2 should only fire on the first iteration, and + * should fire before both check callbacks */ + tt_int_op(iteration, ==, 0); + tt_int_op(prepare_callback_2_count, >, check_callback_1_count); + tt_int_op(prepare_callback_2_count, >, check_callback_2_count); +end: + ; +} + +static void +check_callback_1(struct evwatch *watcher, const struct evwatch_check_cb_info *info, void *arg) +{ + /* user argument should be passed properly */ + tt_ptr_op(arg, ==, &user_arg); + + ++check_callback_1_count; + + /* check_callback_1 should always fire before check_callback_2 */ + tt_int_op(check_callback_1_count, >, check_callback_2_count); + + /* save the end time, in case the timeout fires this time through the + * event loop */ + event_base_gettimeofday_cached(evwatch_base(watcher), &end_time); +end: + ; +} + +static void +check_callback_2(struct evwatch *watcher, const struct evwatch_check_cb_info *info, void *arg) +{ + /* user argument should be passed properly */ + tt_ptr_op(arg, ==, &user_arg); + + ++check_callback_2_count; + + /* check_callback_2 should only fire on the first iteration */ + tt_int_op(iteration, ==, 0); +end: + ; +} + +static void +timeout_callback(evutil_socket_t fd, short events, void *arg) +{ + /* the duration between the start and end times should be at least 1 + * second */ + tt_int_op(end_time.tv_sec, >=, start_time.tv_sec + 1); +end: + ; +} + +/** + This tests a few important properties of "prepare" and "check" watchers: + - Watchers should be called in the order they were registered. + - Prepare watchers should be called before check watchers. + - Freeing a watcher will stop callbacks to it, but not to other watchers. + - Reported durations should align with the registered timeouts. + - It should be possible to call back into libevent from a callback without a + recursive lock. + - If this test is compiled with ASAN or similar, this test also illustrates + that event_base_free will free any watchers not previously freed by + evwatch_free. + */ +static void +test_callback_ordering(void *ptr) +{ + struct basic_test_data *data = ptr; + struct event_base *base = data->base; + struct evwatch *prepare_callback_2_watcher; + struct evwatch *check_callback_2_watcher; + struct timeval timeout; + + /* install prepare and check watchers */ + evwatch_prepare_new(base, &prepare_callback_1, &user_arg); + evwatch_check_new(base, &check_callback_1, &user_arg); + prepare_callback_2_watcher = evwatch_prepare_new(base, &prepare_callback_2, &user_arg); + check_callback_2_watcher = evwatch_check_new(base, &check_callback_2, &user_arg); + + /* schedule an 1 second timeout event, and run the event loop until the + * timeout fires */ + timeout.tv_sec = 1; + timeout.tv_usec = 0; + event_base_once(base, -1, EV_TIMEOUT, &timeout_callback, 0, &timeout); + event_base_dispatch(base); + + /* second iteration: free two of the watchers, schedule a timeout and + * run the event loop again */ + iteration = 1; + start_time.tv_sec = 0; + evwatch_free(prepare_callback_2_watcher); + evwatch_free(check_callback_2_watcher); + event_base_once(base, -1, EV_TIMEOUT, &timeout_callback, 0, &timeout); + event_base_dispatch(base); +} + +static void +prepare_callback_3(struct evwatch *watcher, const struct evwatch_prepare_cb_info *info, void *arg) +{ + /* timeout should not be written to */ + struct timeval timeout = { 123, 456 }; + tt_int_op(evwatch_prepare_get_timeout(info, &timeout), ==, 0); + tt_int_op(timeout.tv_sec, ==, 123); + tt_int_op(timeout.tv_usec, ==, 456); +end: + ; +} + +/** + Test that evwatch_prepare_get_timeout behaves correctly when there is no + timeout. + */ +static void +test_timeout_unavailable(void *ptr) +{ + struct basic_test_data *data = ptr; + struct event_base *base = data->base; + + evwatch_prepare_new(base, &prepare_callback_3, NULL); + event_base_dispatch(base); +} + +#ifndef EVENT__DISABLE_MM_REPLACEMENT +static void * +bad_malloc(size_t sz) +{ + return NULL; +} + +/** + Test that creating prepare and check watchers fails gracefully if we can't + allocate memory. + */ +static void +test_malloc_failure(void *ptr) +{ + struct basic_test_data *data = ptr; + struct event_base *base = data->base; + struct evwatch *bad_prepare, *bad_check; + + event_set_mem_functions(bad_malloc, realloc, free); + bad_prepare = evwatch_prepare_new(base, &prepare_callback_1, NULL); + tt_ptr_op(bad_prepare, ==, NULL); + + bad_check = evwatch_check_new(base, &check_callback_1, NULL); + tt_ptr_op(bad_check, ==, NULL); + + event_set_mem_functions(malloc, realloc, free); +end: + ; +} +#endif + +struct testcase_t watch_testcases[] = { + BASIC(callback_ordering, TT_FORK|TT_NEED_BASE), + BASIC(timeout_unavailable, TT_FORK|TT_NEED_BASE), +#ifndef EVENT__DISABLE_MM_REPLACEMENT + BASIC(malloc_failure, TT_FORK|TT_NEED_BASE), +#endif + END_OF_TESTCASES +}; diff --git a/watch.c b/watch.c new file mode 100644 index 00000000..645cbd9a --- /dev/null +++ b/watch.c @@ -0,0 +1,82 @@ +/* + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "event2/watch.h" +#include "event-internal.h" +#include "evthread-internal.h" + +static inline struct evwatch * +evwatch_new(struct event_base *base, union evwatch_cb callback, void *arg, unsigned type) +{ + struct evwatch *watcher = mm_malloc(sizeof(struct evwatch)); + if (!watcher) + return NULL; + watcher->base = base; + watcher->type = type; + watcher->callback = callback; + watcher->arg = arg; + EVBASE_ACQUIRE_LOCK(base, th_base_lock); + TAILQ_INSERT_TAIL(&base->watchers[type], watcher, next); + EVBASE_RELEASE_LOCK(base, th_base_lock); + return watcher; +} + +struct evwatch * +evwatch_prepare_new(struct event_base *base, evwatch_prepare_cb callback, void *arg) +{ + union evwatch_cb cb = { .prepare = callback }; + return evwatch_new(base, cb, arg, EVWATCH_PREPARE); +} + +struct evwatch * +evwatch_check_new(struct event_base *base, evwatch_check_cb callback, void *arg) +{ + union evwatch_cb cb = { .check = callback }; + return evwatch_new(base, cb, arg, EVWATCH_CHECK); +} + +struct event_base * +evwatch_base(struct evwatch *watcher) +{ + return watcher->base; +} + +void +evwatch_free(struct evwatch *watcher) +{ + EVBASE_ACQUIRE_LOCK(watcher->base, th_base_lock); + TAILQ_REMOVE(&watcher->base->watchers[watcher->type], watcher, next); + EVBASE_RELEASE_LOCK(watcher->base, th_base_lock); + mm_free(watcher); +} + +int +evwatch_prepare_get_timeout(const struct evwatch_prepare_cb_info *info, struct timeval *timeout) +{ + if (info->timeout) { + *timeout = *(info->timeout); + return 1; + } + return 0; +} diff --git a/whatsnew-2.2.txt b/whatsnew-2.2.txt index 7e09e61f..c13d22fc 100644 --- a/whatsnew-2.2.txt +++ b/whatsnew-2.2.txt @@ -47,4 +47,24 @@ ac_configure_args=" LIBEVENT_LDFLAGS='${EXTRA_LDFLAGS}'${ac_configure_args}" AC_CONFIG_SUBDIRS([libevent]) ... -The space after the initial '"' is significant. \ No newline at end of file +The space after the initial '"' is significant. + +* "Prepare" and "check" watchers + +Libevent now has a new mechanism for hooking into the event loop: "prepare" and +"check" watchers. A "prepare" watcher is a callback that fires immediately +before polling for I/O. A "check" watcher is a callback that fires immediately +after polling and before processing any active events. This may be useful for +embedding other libraries' event loops (e.g. UI toolkits) into libevent's. It's +also useful for monitoring server performance. For example, if you measure the +time between "prepare" and "check," that is the polling duration; the difference +between the expected and actual polling duration provides an indication of +kernel scheduling delay. And if you measure the time between "check" and the +next "prepare" (in the next iteration of the event loop), that is a good +approximation of the amount of time handling events; this provides a convenient +way to monitor whether any event handlers are blocking or otherwise performing +heavy computation. + +The watcher API is defined in . A concrete example of how +watchers can help monitor server performance is available in +"sample/watch-timing.c".