Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
+63
View File
@@ -0,0 +1,63 @@
## Dual `_select` without GVL:
Always release GVL:
```
Warming up --------------------------------------
KQueue 55.896k i/100ms
Select 17.023k i/100ms
Calculating -------------------------------------
KQueue 532.515k (± 8.0%) i/s - 2.683M in 5.071193s
Select 177.956k (± 3.4%) i/s - 902.219k in 5.075817s
Comparison:
KQueue: 532515.3 i/s
Select: 177956.1 i/s - 2.99x (± 0.00) slower
```
Only release GVL with non-zero timeout, with selector.elect(1) (so always hitting slow path):
```
Warming up --------------------------------------
KQueue 39.628k i/100ms
Select 18.330k i/100ms
Calculating -------------------------------------
KQueue 381.868k (± 6.5%) i/s - 1.902M in 5.004267s
Select 171.623k (± 3.0%) i/s - 861.510k in 5.024308s
Comparison:
KQueue: 381867.8 i/s
Select: 171622.5 i/s - 2.23x (± 0.00) slower
```
Only release GVL with non-zero timeout, with selector.select(0) so always hitting fast path:
```
Warming up --------------------------------------
KQueue 56.240k i/100ms
Select 17.888k i/100ms
Calculating -------------------------------------
KQueue 543.042k (± 7.8%) i/s - 2.700M in 5.003790s
Select 171.866k (± 4.3%) i/s - 858.624k in 5.005785s
Comparison:
KQueue: 543041.5 i/s
Select: 171866.2 i/s - 3.16x (± 0.00) slower
```
Only release GVL when no events are ready and non-zero timeout, with selector.select(1):
```
Warming up --------------------------------------
KQueue 53.401k i/100ms
Select 16.691k i/100ms
Calculating -------------------------------------
KQueue 524.564k (± 6.1%) i/s - 2.617M in 5.006996s
Select 179.329k (± 2.4%) i/s - 901.314k in 5.029136s
Comparison:
KQueue: 524564.0 i/s
Select: 179329.1 i/s - 2.93x (± 0.00) slower
```
So this approach seems to be a net win of about 1.5x throughput.
+269
View File
@@ -0,0 +1,269 @@
SHELL = /bin/sh
# V=0 quiet, V=1 verbose. other values don't work.
V = 0
V0 = $(V:0=)
Q1 = $(V:1=)
Q = $(Q1:0=@)
ECHO1 = $(V:1=@ :)
ECHO = $(ECHO1:0=@ echo)
NULLCMD = :
#### Start of system configuration section. ####
srcdir = .
topdir = /usr/include/ruby-3.2.0
hdrdir = $(topdir)
arch_hdrdir = /usr/include/x86_64-linux-gnu/ruby-3.2.0
PATH_SEPARATOR = :
VPATH = $(srcdir):$(arch_hdrdir)/ruby:$(hdrdir)/ruby:$(srcdir)/io/event:$(srcdir)/io/event/selector
prefix = $(DESTDIR)/usr
rubysitearchprefix = $(sitearchlibdir)/$(RUBY_BASE_NAME)
rubyarchprefix = $(archlibdir)/$(RUBY_BASE_NAME)
rubylibprefix = $(libdir)/$(RUBY_BASE_NAME)
exec_prefix = $(prefix)
vendorarchhdrdir = $(sitearchincludedir)/$(RUBY_VERSION_NAME)/vendor_ruby
sitearchhdrdir = $(sitearchincludedir)/$(RUBY_VERSION_NAME)/site_ruby
rubyarchhdrdir = $(archincludedir)/$(RUBY_VERSION_NAME)
vendorhdrdir = $(rubyhdrdir)/vendor_ruby
sitehdrdir = $(rubyhdrdir)/site_ruby
rubyhdrdir = $(includedir)/$(RUBY_VERSION_NAME)
vendorarchdir = $(rubysitearchprefix)/vendor_ruby/$(ruby_version)
vendorlibdir = $(vendordir)/$(ruby_version)
vendordir = $(rubylibprefix)/vendor_ruby
sitearchdir = $(DESTDIR)/usr/local/lib/x86_64-linux-gnu/site_ruby
sitelibdir = $(sitedir)/$(ruby_version)
sitedir = $(DESTDIR)/usr/local/lib/site_ruby
rubyarchdir = $(rubyarchprefix)/$(ruby_version)
rubylibdir = $(rubylibprefix)/$(ruby_version)
sitearchincludedir = $(includedir)/$(sitearch)
archincludedir = $(includedir)/$(arch)
sitearchlibdir = $(libdir)/$(sitearch)
archlibdir = $(libdir)/$(arch)
ridir = $(datarootdir)/$(RI_BASE_NAME)
mandir = $(datarootdir)/man
localedir = $(datarootdir)/locale
libdir = $(exec_prefix)/lib
psdir = $(docdir)
pdfdir = $(docdir)
dvidir = $(docdir)
htmldir = $(docdir)
infodir = $(datarootdir)/info
docdir = $(datarootdir)/doc/$(PACKAGE)
oldincludedir = $(DESTDIR)/usr/include
includedir = $(prefix)/include
runstatedir = $(DESTDIR)/var/run
localstatedir = $(DESTDIR)/var
sharedstatedir = $(prefix)/com
sysconfdir = $(DESTDIR)/etc
datadir = $(datarootdir)
datarootdir = $(prefix)/share
libexecdir = $(exec_prefix)/libexec
sbindir = $(exec_prefix)/sbin
bindir = $(exec_prefix)/bin
archdir = $(rubyarchdir)
CC_WRAPPER =
CC = x86_64-linux-gnu-gcc
CXX = x86_64-linux-gnu-g++
LIBRUBY = $(LIBRUBY_SO)
LIBRUBY_A = lib$(RUBY_SO_NAME)-static.a
LIBRUBYARG_SHARED = -l$(RUBY_SO_NAME)
LIBRUBYARG_STATIC = -l$(RUBY_SO_NAME)-static $(MAINLIBS)
empty =
OUTFLAG = -o $(empty)
COUTFLAG = -o $(empty)
CSRCFLAG = $(empty)
RUBY_EXTCONF_H = extconf.h
cflags = $(optflags) $(debugflags) $(warnflags)
cxxflags =
optflags = -O3 -fno-fast-math
debugflags = -ggdb3
warnflags = -Wall -Wextra -Wdeprecated-declarations -Wdiv-by-zero -Wduplicated-cond -Wimplicit-function-declaration -Wimplicit-int -Wmisleading-indentation -Wpointer-arith -Wwrite-strings -Wold-style-definition -Wimplicit-fallthrough=0 -Wmissing-noreturn -Wno-cast-function-type -Wno-constant-logical-operand -Wno-long-long -Wno-missing-field-initializers -Wno-overlength-strings -Wno-packed-bitfield-compat -Wno-parentheses-equality -Wno-self-assign -Wno-tautological-compare -Wno-unused-parameter -Wno-unused-value -Wsuggest-attribute=format -Wsuggest-attribute=noreturn -Wunused-variable -Wundef
cppflags =
CCDLFLAGS = -fPIC
CFLAGS = $(CCDLFLAGS) -g -O2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffile-prefix-map=BUILDDIR=. -fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security -fcf-protection -fdebug-prefix-map=BUILDDIR=/usr/src/ruby3.2-3.2.3-1ubuntu0.24.04.8 -fPIC -Wall -Wno-unknown-pragmas -std=c99 $(ARCH_FLAG)
INCFLAGS = -I. -I$(arch_hdrdir) -I$(hdrdir)/ruby/backward -I$(hdrdir) -I$(srcdir)
DEFS =
CPPFLAGS = -DRUBY_EXTCONF_H=\"$(RUBY_EXTCONF_H)\" -Wdate-time -D_FORTIFY_SOURCE=3 $(DEFS) $(cppflags)
CXXFLAGS = $(CCDLFLAGS) -g -O2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffile-prefix-map=BUILDDIR=. -fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security -fcf-protection -fdebug-prefix-map=BUILDDIR=/usr/src/ruby3.2-3.2.3-1ubuntu0.24.04.8 $(ARCH_FLAG)
ldflags = -L. -Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -fstack-protector-strong -rdynamic -Wl,-export-dynamic -Wl,--no-as-needed
dldflags = -Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now
ARCH_FLAG =
DLDFLAGS = $(ldflags) $(dldflags) $(ARCH_FLAG)
LDSHARED = $(CC) -shared
LDSHAREDXX = $(CXX) -shared
AR = x86_64-linux-gnu-gcc-ar
EXEEXT =
RUBY_INSTALL_NAME = $(RUBY_BASE_NAME)3.2
RUBY_SO_NAME = ruby-3.2
RUBYW_INSTALL_NAME =
RUBY_VERSION_NAME = $(RUBY_BASE_NAME)-$(ruby_version)
RUBYW_BASE_NAME = rubyw
RUBY_BASE_NAME = ruby
arch = x86_64-linux-gnu
sitearch = $(arch)
ruby_version = 3.2.0
ruby = $(bindir)/$(RUBY_BASE_NAME)3.2
RUBY = $(ruby)
BUILTRUBY = $(bindir)/$(RUBY_BASE_NAME)3.2
ruby_headers = $(hdrdir)/ruby.h $(hdrdir)/ruby/backward.h $(hdrdir)/ruby/ruby.h $(hdrdir)/ruby/defines.h $(hdrdir)/ruby/missing.h $(hdrdir)/ruby/intern.h $(hdrdir)/ruby/st.h $(hdrdir)/ruby/subst.h $(arch_hdrdir)/ruby/config.h $(RUBY_EXTCONF_H)
RM = rm -f
RM_RF = rm -fr
RMDIRS = rmdir --ignore-fail-on-non-empty -p
MAKEDIRS = /bin/mkdir -p
INSTALL = /usr/bin/install -c
INSTALL_PROG = $(INSTALL) -m 0755
INSTALL_DATA = $(INSTALL) -m 644
COPY = cp
TOUCH = exit >
#### End of system configuration section. ####
preload =
libpath = . $(archlibdir)
LIBPATH = -L. -L$(archlibdir)
DEFFILE =
CLEANFILES = mkmf.log
DISTCLEANFILES =
DISTCLEANDIRS =
extout =
extout_prefix =
target_prefix =
LOCAL_LIBS =
LIBS = $(LIBRUBYARG_SHARED) -lm -lpthread -lc
ORIG_SRCS =
SRCS = $(ORIG_SRCS) event.c time.c fiber.c profiler.c selector.c epoll.c interrupt.c
OBJS = epoll.o event.o fiber.o interrupt.o profiler.o selector.o time.o
HDRS = $(srcdir)/extconf.h
LOCAL_HDRS =
TARGET = IO_Event
TARGET_NAME = IO_Event
TARGET_ENTRY = Init_$(TARGET_NAME)
DLLIB = $(TARGET).so
EXTSTATIC =
STATIC_LIB =
TIMESTAMP_DIR = .
BINDIR = $(bindir)
RUBYCOMMONDIR = $(sitedir)$(target_prefix)
RUBYLIBDIR = $(sitelibdir)$(target_prefix)
RUBYARCHDIR = $(sitearchdir)$(target_prefix)
HDRDIR = $(sitehdrdir)$(target_prefix)
ARCHHDRDIR = $(sitearchhdrdir)$(target_prefix)
TARGET_SO_DIR =
TARGET_SO = $(TARGET_SO_DIR)$(DLLIB)
CLEANLIBS = $(TARGET_SO) false
CLEANOBJS = $(OBJS) *.bak
TARGET_SO_DIR_TIMESTAMP = $(TIMESTAMP_DIR)/.sitearchdir.time
all: $(DLLIB)
static: $(STATIC_LIB)
.PHONY: all install static install-so install-rb
.PHONY: clean clean-so clean-static clean-rb
clean-static::
clean-rb-default::
clean-rb::
clean-so::
clean: clean-so clean-static clean-rb-default clean-rb
-$(Q)$(RM_RF) $(CLEANLIBS) $(CLEANOBJS) $(CLEANFILES) .*.time
distclean-rb-default::
distclean-rb::
distclean-so::
distclean-static::
distclean: clean distclean-so distclean-static distclean-rb-default distclean-rb
-$(Q)$(RM) Makefile $(RUBY_EXTCONF_H) conftest.* mkmf.log
-$(Q)$(RM) core ruby$(EXEEXT) *~ $(DISTCLEANFILES)
-$(Q)$(RMDIRS) $(DISTCLEANDIRS) 2> /dev/null || true
realclean: distclean
install: install-so install-rb
install-so: $(DLLIB) $(TARGET_SO_DIR_TIMESTAMP)
$(INSTALL_PROG) $(DLLIB) $(RUBYARCHDIR)
clean-static::
-$(Q)$(RM) $(STATIC_LIB)
install-rb: pre-install-rb do-install-rb install-rb-default
install-rb-default: pre-install-rb-default do-install-rb-default
pre-install-rb: Makefile
pre-install-rb-default: Makefile
do-install-rb:
do-install-rb-default:
pre-install-rb-default:
@$(NULLCMD)
$(TARGET_SO_DIR_TIMESTAMP):
$(Q) $(MAKEDIRS) $(@D) $(RUBYARCHDIR)
$(Q) $(TOUCH) $@
site-install: site-install-so site-install-rb
site-install-so: install-so
site-install-rb: install-rb
.SUFFIXES: .c .m .cc .mm .cxx .cpp .o .S
.cc.o:
$(ECHO) compiling $(<)
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
.cc.S:
$(ECHO) translating $(<)
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
.mm.o:
$(ECHO) compiling $(<)
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
.mm.S:
$(ECHO) translating $(<)
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
.cxx.o:
$(ECHO) compiling $(<)
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
.cxx.S:
$(ECHO) translating $(<)
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
.cpp.o:
$(ECHO) compiling $(<)
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
.cpp.S:
$(ECHO) translating $(<)
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
.c.o:
$(ECHO) compiling $(<)
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
.c.S:
$(ECHO) translating $(<)
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
.m.o:
$(ECHO) compiling $(<)
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
.m.S:
$(ECHO) translating $(<)
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
$(TARGET_SO): $(OBJS) Makefile
$(ECHO) linking shared-object $(DLLIB)
-$(Q)$(RM) $(@)
$(Q) $(LDSHARED) -o $@ $(OBJS) $(LIBPATH) $(DLDFLAGS) $(LOCAL_LIBS) $(LIBS)
$(OBJS): $(HDRS) $(ruby_headers)
+13
View File
@@ -0,0 +1,13 @@
#ifndef EXTCONF_H
#define EXTCONF_H
#define HAVE_RB_EXT_RACTOR_SAFE 1
#define HAVE__RB_FIBER_TRANSFER 1
#define HAVE_SYS_EPOLL_H 1
#define HAVE_SYS_WAIT_H 1
#define HAVE_SYS_EVENTFD_H 1
#define HAVE_RB_IO_DESCRIPTOR 1
#define HAVE_RB_FIBER_CURRENT 1
#define HAVE__RB_FIBER_RAISE 1
#define HAVE_EPOLL_PWAIT2 1
#define HAVE_RUBY_IO_BUFFER_H 1
#endif
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021-2025, by Samuel Williams.
# Copyright, 2023, by Math Ieu.
return if RUBY_DESCRIPTION =~ /jruby/
require "mkmf"
gem_name = File.basename(__dir__)
extension_name = "IO_Event"
# dir_config(extension_name)
$CFLAGS << " -Wall -Wno-unknown-pragmas -std=c99"
if ENV.key?("RUBY_DEBUG")
$stderr.puts "Enabling debug mode..."
$CFLAGS << " -DRUBY_DEBUG -O0"
end
$srcs = ["io/event/event.c", "io/event/time.c", "io/event/fiber.c", "io/event/profiler.c", "io/event/selector/selector.c"]
$VPATH << "$(srcdir)/io/event"
$VPATH << "$(srcdir)/io/event/selector"
have_func("rb_ext_ractor_safe")
have_func("&rb_fiber_transfer")
if have_library("uring") and have_header("liburing.h")
# We might want to consider using this in the future:
# have_func("io_uring_submit_and_wait_timeout", "liburing.h")
$srcs << "io/event/selector/uring.c"
end
if have_header("sys/epoll.h")
$srcs << "io/event/selector/epoll.c"
end
if have_header("sys/event.h")
$srcs << "io/event/selector/kqueue.c"
end
have_header("sys/wait.h")
have_header("sys/eventfd.h")
$srcs << "io/event/interrupt.c"
have_func("rb_io_descriptor")
have_func("&rb_process_status_wait")
have_func("rb_fiber_current")
have_func("&rb_fiber_raise")
have_func("epoll_pwait2")
have_header("ruby/io/buffer.h")
if ENV.key?("RUBY_SANITIZE")
$stderr.puts "Enabling sanitizers..."
# Add address and undefined behaviour sanitizers:
$CFLAGS << " -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer"
$LDFLAGS << " -fsanitize=address -fsanitize=undefined"
end
create_header
# Generate the makefile to compile the native binary into `lib`:
create_makefile(extension_name)
@@ -0,0 +1,192 @@
// Released under the MIT License.
// Copyright, 2023, by Samuel Williams.
// Provides a simple implementation of unique pointers to elements of the given size.
#include <ruby.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>
static const size_t IO_EVENT_ARRAY_MAXIMUM_COUNT = SIZE_MAX / sizeof(void*);
static const size_t IO_EVENT_ARRAY_DEFAULT_COUNT = 128;
struct IO_Event_Array {
// The array of pointers to elements:
void **base;
// The allocated size of the array:
size_t count;
// The biggest item we've seen so far:
size_t limit;
// The size of each element that is allocated:
size_t element_size;
void (*element_initialize)(void*);
void (*element_free)(void*);
};
inline static int IO_Event_Array_initialize(struct IO_Event_Array *array, size_t count, size_t element_size)
{
array->limit = 0;
array->element_size = element_size;
if (count) {
array->base = (void**)calloc(count, sizeof(void*));
if (array->base == NULL) {
return -1;
}
array->count = count;
return 1;
} else {
array->base = NULL;
array->count = 0;
return 0;
}
}
inline static size_t IO_Event_Array_memory_size(const struct IO_Event_Array *array)
{
// Upper bound.
return array->count * (sizeof(void*) + array->element_size);
}
inline static void IO_Event_Array_free(struct IO_Event_Array *array)
{
if (array->base) {
void **base = array->base;
size_t limit = array->limit;
array->base = NULL;
array->count = 0;
array->limit = 0;
for (size_t i = 0; i < limit; i += 1) {
void *element = base[i];
if (element) {
array->element_free(element);
free(element);
}
}
free(base);
}
}
inline static int IO_Event_Array_resize(struct IO_Event_Array *array, size_t count)
{
if (count <= array->count) {
// Already big enough:
return 0;
}
if (count > IO_EVENT_ARRAY_MAXIMUM_COUNT) {
errno = ENOMEM;
return -1;
}
size_t new_count = array->count;
// If the array is empty, we need to set the initial size:
if (new_count == 0) new_count = IO_EVENT_ARRAY_DEFAULT_COUNT;
else while (new_count < count) {
// Ensure we don't overflow:
if (new_count > (IO_EVENT_ARRAY_MAXIMUM_COUNT / 2)) {
new_count = IO_EVENT_ARRAY_MAXIMUM_COUNT;
break;
}
// Compute the next multiple (ideally a power of 2):
new_count *= 2;
}
void **new_base = (void**)realloc(array->base, new_count * sizeof(void*));
if (new_base == NULL) {
return -1;
}
// Zero out the new memory:
memset(new_base + array->count, 0, (new_count - array->count) * sizeof(void*));
array->base = (void**)new_base;
array->count = new_count;
// Resizing sucessful:
return 1;
}
inline static void* IO_Event_Array_lookup(struct IO_Event_Array *array, size_t index)
{
size_t count = index + 1;
// Resize the array if necessary:
if (count > array->count) {
if (IO_Event_Array_resize(array, count) == -1) {
return NULL;
}
}
// Get the element:
void **element = array->base + index;
// Allocate the element if it doesn't exist:
if (*element == NULL) {
*element = malloc(array->element_size);
assert(*element);
if (array->element_initialize) {
array->element_initialize(*element);
}
// Update the limit:
if (count > array->limit) array->limit = count;
}
return *element;
}
inline static void* IO_Event_Array_last(struct IO_Event_Array *array)
{
if (array->limit == 0) return NULL;
else return array->base[array->limit - 1];
}
inline static void IO_Event_Array_truncate(struct IO_Event_Array *array, size_t limit)
{
if (limit < array->limit) {
for (size_t i = limit; i < array->limit; i += 1) {
void **element = array->base + i;
if (*element) {
array->element_free(*element);
free(*element);
*element = NULL;
}
}
array->limit = limit;
}
}
// Push a new element onto the end of the array.
inline static void* IO_Event_Array_push(struct IO_Event_Array *array)
{
return IO_Event_Array_lookup(array, array->limit);
}
inline static void IO_Event_Array_each(struct IO_Event_Array *array, void (*callback)(void*))
{
for (size_t i = 0; i < array->limit; i += 1) {
void *element = array->base[i];
if (element) {
callback(element);
}
}
}
@@ -0,0 +1,35 @@
// Released under the MIT License.
// Copyright, 2021-2025, by Samuel Williams.
#include "event.h"
#include "fiber.h"
#include "profiler.h"
#include "selector/selector.h"
#include <complex.h>
void Init_IO_Event(void)
{
#ifdef HAVE_RB_EXT_RACTOR_SAFE
rb_ext_ractor_safe(true);
#endif
VALUE IO_Event = rb_define_module_under(rb_cIO, "Event");
Init_IO_Event_Fiber(IO_Event);
Init_IO_Event_Profiler(IO_Event);
VALUE IO_Event_Selector = rb_define_module_under(IO_Event, "Selector");
Init_IO_Event_Selector(IO_Event_Selector);
#ifdef IO_EVENT_SELECTOR_URING
Init_IO_Event_Selector_URing(IO_Event_Selector);
#endif
#ifdef IO_EVENT_SELECTOR_EPOLL
Init_IO_Event_Selector_EPoll(IO_Event_Selector);
#endif
#ifdef IO_EVENT_SELECTOR_KQUEUE
Init_IO_Event_Selector_KQueue(IO_Event_Selector);
#endif
}
@@ -0,0 +1,20 @@
// Released under the MIT License.
// Copyright, 2021-2025, by Samuel Williams.
#pragma once
#include <ruby.h>
void Init_IO_Event(void);
#ifdef HAVE_LIBURING_H
#include "selector/uring.h"
#endif
#ifdef HAVE_SYS_EPOLL_H
#include "selector/epoll.h"
#endif
#ifdef HAVE_SYS_EVENT_H
#include "selector/kqueue.h"
#endif
@@ -0,0 +1,63 @@
// Released under the MIT License.
// Copyright, 2025, by Samuel Williams.
#include "fiber.h"
static ID id_transfer, id_alive_p;
VALUE IO_Event_Fiber_transfer(VALUE fiber, int argc, VALUE *argv) {
// TODO Consider introducing something like `rb_fiber_scheduler_transfer(...)`.
#ifdef HAVE__RB_FIBER_TRANSFER
if (RTEST(rb_obj_is_fiber(fiber))) {
if (RTEST(rb_fiber_alive_p(fiber))) {
return rb_fiber_transfer(fiber, argc, argv);
}
// If it's a fiber, but dead, we are done.
return Qnil;
}
#endif
if (RTEST(rb_funcall(fiber, id_alive_p, 0))) {
return rb_funcallv(fiber, id_transfer, argc, argv);
}
return Qnil;
}
#ifndef HAVE__RB_FIBER_RAISE
static ID id_raise;
VALUE IO_Event_Fiber_raise(VALUE fiber, int argc, VALUE *argv) {
return rb_funcallv(fiber, id_raise, argc, argv);
}
#endif
#ifndef HAVE_RB_FIBER_CURRENT
static ID id_current;
static VALUE IO_Event_Fiber_current(void) {
return rb_funcall(rb_cFiber, id_current, 0);
}
#endif
// There is no public interface for this... yet.
static ID id_blocking_p;
int IO_Event_Fiber_blocking(VALUE fiber) {
return RTEST(rb_funcall(fiber, id_blocking_p, 0));
}
void Init_IO_Event_Fiber(VALUE IO_Event) {
id_transfer = rb_intern("transfer");
id_alive_p = rb_intern("alive?");
#ifndef HAVE__RB_FIBER_RAISE
id_raise = rb_intern("raise");
#endif
#ifndef HAVE_RB_FIBER_CURRENT
id_current = rb_intern("current");
#endif
id_blocking_p = rb_intern("blocking?");
}
@@ -0,0 +1,23 @@
// Released under the MIT License.
// Copyright, 2025, by Samuel Williams.
#pragma once
#include <ruby.h>
VALUE IO_Event_Fiber_transfer(VALUE fiber, int argc, VALUE *argv);
#ifdef HAVE__RB_FIBER_RAISE
#define IO_Event_Fiber_raise(fiber, argc, argv) rb_fiber_raise(fiber, argc, argv)
#else
VALUE IO_Event_Fiber_raise(VALUE fiber, int argc, VALUE *argv);
#endif
#ifdef HAVE_RB_FIBER_CURRENT
#define IO_Event_Fiber_current() rb_fiber_current()
#else
VALUE IO_Event_Fiber_current(void);
#endif
int IO_Event_Fiber_blocking(VALUE fiber);
void Init_IO_Event_Fiber(VALUE IO_Event);
@@ -0,0 +1,100 @@
// Released under the MIT License.
// Copyright, 2021-2025, by Samuel Williams.
#include "interrupt.h"
#include <unistd.h>
#include "selector/selector.h"
#ifdef HAVE_RUBY_WIN32_H
#include <ruby/win32.h>
#if !defined(HAVE_PIPE) && !defined(pipe)
#define pipe(p) rb_w32_pipe(p)
#endif
#endif
#ifdef HAVE_SYS_EVENTFD_H
#include <sys/eventfd.h>
void IO_Event_Interrupt_open(struct IO_Event_Interrupt *interrupt)
{
interrupt->descriptor = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
rb_update_max_fd(interrupt->descriptor);
}
void IO_Event_Interrupt_close(struct IO_Event_Interrupt *interrupt)
{
close(interrupt->descriptor);
}
void IO_Event_Interrupt_signal(struct IO_Event_Interrupt *interrupt)
{
uint64_t value = 1;
ssize_t result = write(interrupt->descriptor, &value, sizeof(value));
if (result == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) return;
rb_sys_fail("IO_Event_Interrupt_signal:write");
}
}
void IO_Event_Interrupt_clear(struct IO_Event_Interrupt *interrupt)
{
uint64_t value = 0;
ssize_t result = read(interrupt->descriptor, &value, sizeof(value));
if (result == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) return;
rb_sys_fail("IO_Event_Interrupt_clear:read");
}
}
#else
void IO_Event_Interrupt_open(struct IO_Event_Interrupt *interrupt)
{
#ifdef __linux__
pipe2(interrupt->descriptor, O_CLOEXEC | O_NONBLOCK);
#else
pipe(interrupt->descriptor);
IO_Event_Selector_nonblock_set(interrupt->descriptor[0]);
IO_Event_Selector_nonblock_set(interrupt->descriptor[1]);
#endif
rb_update_max_fd(interrupt->descriptor[0]);
rb_update_max_fd(interrupt->descriptor[1]);
}
void IO_Event_Interrupt_close(struct IO_Event_Interrupt *interrupt)
{
close(interrupt->descriptor[0]);
close(interrupt->descriptor[1]);
}
void IO_Event_Interrupt_signal(struct IO_Event_Interrupt *interrupt)
{
ssize_t result = write(interrupt->descriptor[1], ".", 1);
if (result == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// If we can't write to the pipe, it means the other end is full. In that case, we can be sure that the other end has already been woken up or is about to be woken up.
} else {
rb_sys_fail("IO_Event_Interrupt_signal:write");
}
}
}
void IO_Event_Interrupt_clear(struct IO_Event_Interrupt *interrupt)
{
char buffer[128];
ssize_t result = read(interrupt->descriptor[0], buffer, sizeof(buffer));
if (result == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// If we can't read from the pipe, it means the other end is empty. In that case, we can be sure that the other end is already clear.
} else {
rb_sys_fail("IO_Event_Interrupt_clear:read");
}
}
}
#endif
@@ -0,0 +1,30 @@
// Released under the MIT License.
// Copyright, 2021-2025, by Samuel Williams.
#pragma once
#include <ruby.h>
#ifdef HAVE_SYS_EVENTFD_H
struct IO_Event_Interrupt {
int descriptor;
};
static inline int IO_Event_Interrupt_descriptor(struct IO_Event_Interrupt *interrupt) {
return interrupt->descriptor;
}
#else
struct IO_Event_Interrupt {
int descriptor[2];
};
static inline int IO_Event_Interrupt_descriptor(struct IO_Event_Interrupt *interrupt) {
return interrupt->descriptor[0];
}
#endif
void IO_Event_Interrupt_open(struct IO_Event_Interrupt *interrupt);
void IO_Event_Interrupt_close(struct IO_Event_Interrupt *interrupt);
void IO_Event_Interrupt_signal(struct IO_Event_Interrupt *interrupt);
void IO_Event_Interrupt_clear(struct IO_Event_Interrupt *interrupt);
+107
View File
@@ -0,0 +1,107 @@
// Released under the MIT License.
// Copyright, 2023-2025, by Samuel Williams.
#include <ruby.h>
#include <stdio.h>
#include <assert.h>
struct IO_Event_List_Type {
};
struct IO_Event_List {
struct IO_Event_List *head, *tail;
struct IO_Event_List_Type *type;
};
inline static void IO_Event_List_initialize(struct IO_Event_List *list)
{
list->head = list->tail = list;
list->type = 0;
}
inline static void IO_Event_List_clear(struct IO_Event_List *list)
{
list->head = list->tail = NULL;
list->type = 0;
}
// Append an item to the end of the list.
inline static void IO_Event_List_append(struct IO_Event_List *list, struct IO_Event_List *node)
{
assert(node->head == NULL);
assert(node->tail == NULL);
struct IO_Event_List *head = list->head;
node->tail = list;
node->head = head;
list->head = node;
head->tail = node;
}
// Prepend an item to the beginning of the list.
inline static void IO_Event_List_prepend(struct IO_Event_List *list, struct IO_Event_List *node)
{
assert(node->head == NULL);
assert(node->tail == NULL);
struct IO_Event_List *tail = list->tail;
node->head = list;
node->tail = tail;
list->tail = node;
tail->head = node;
}
// Pop an item from the list.
inline static void IO_Event_List_pop(struct IO_Event_List *node)
{
assert(node->head != NULL);
assert(node->tail != NULL);
struct IO_Event_List *head = node->head;
struct IO_Event_List *tail = node->tail;
head->tail = tail;
tail->head = head;
node->head = node->tail = NULL;
}
// Remove an item from the list, if it is in a list.
inline static void IO_Event_List_free(struct IO_Event_List *node)
{
if (node->head && node->tail) {
IO_Event_List_pop(node);
}
}
// Calculate the memory size of the list nodes.
inline static size_t IO_Event_List_memory_size(const struct IO_Event_List *list)
{
size_t memsize = 0;
const struct IO_Event_List *node = list->tail;
while (node != list) {
memsize += sizeof(struct IO_Event_List);
node = node->tail;
}
return memsize;
}
// Return true if the list is empty.
inline static int IO_Event_List_empty(const struct IO_Event_List *list)
{
return list->head == list->tail;
}
// Enumerate all items in the list, assuming the list will not be modified during iteration.
inline static void IO_Event_List_immutable_each(struct IO_Event_List *list, void (*callback)(struct IO_Event_List *node))
{
struct IO_Event_List *node = list->tail;
while (node != list) {
if (node->type)
callback(node);
node = node->tail;
}
}
@@ -0,0 +1,505 @@
// Released under the MIT License.
// Copyright, 2025, by Samuel Williams.
#include "profiler.h"
#include "time.h"
#include "fiber.h"
#include "array.h"
#include <ruby/debug.h>
#include <stdio.h>
VALUE IO_Event_Profiler = Qnil;
struct IO_Event_Profiler_Call {
struct timespec enter_time;
struct timespec exit_time;
size_t nesting;
rb_event_flag_t event_flag;
ID id;
VALUE klass;
const char *path;
int line;
struct IO_Event_Profiler_Call *parent;
};
struct IO_Event_Profiler {
// Configuration:
float log_threshold;
int track_calls;
// Whether or not the profiler is currently running:
int running;
// Whether or not to capture call data:
int capture;
size_t stalls;
// From this point on, the state of any profile in progress:
struct timespec start_time;
struct timespec stop_time;
// The depth of the call stack:
size_t nesting;
// The current call frame:
struct IO_Event_Profiler_Call *current;
struct IO_Event_Array calls;
};
void IO_Event_Profiler_reset(struct IO_Event_Profiler *profiler) {
profiler->nesting = 0;
profiler->current = NULL;
IO_Event_Array_truncate(&profiler->calls, 0);
}
void IO_Event_Profiler_Call_initialize(struct IO_Event_Profiler_Call *call) {
call->enter_time.tv_sec = 0;
call->enter_time.tv_nsec = 0;
call->exit_time.tv_sec = 0;
call->exit_time.tv_nsec = 0;
call->nesting = 0;
call->event_flag = 0;
call->id = 0;
call->path = NULL;
call->line = 0;
}
void IO_Event_Profiler_Call_free(struct IO_Event_Profiler_Call *call) {
if (call->path) {
free((void*)call->path);
call->path = NULL;
}
}
static void IO_Event_Profiler_mark(void *ptr) {
struct IO_Event_Profiler *profiler = (struct IO_Event_Profiler*)ptr;
// If `klass` is stored as a VALUE in calls, we need to mark them here:
for (size_t i = 0; i < profiler->calls.limit; i += 1) {
struct IO_Event_Profiler_Call *call = profiler->calls.base[i];
rb_gc_mark_movable(call->klass);
}
}
static void IO_Event_Profiler_compact(void *ptr) {
struct IO_Event_Profiler *profiler = (struct IO_Event_Profiler*)ptr;
// If `klass` is stored as a VALUE in calls, we need to update their locations here:
for (size_t i = 0; i < profiler->calls.limit; i += 1) {
struct IO_Event_Profiler_Call *call = profiler->calls.base[i];
call->klass = rb_gc_location(call->klass);
}
}
static void IO_Event_Profiler_free(void *ptr) {
struct IO_Event_Profiler *profiler = (struct IO_Event_Profiler*)ptr;
IO_Event_Array_free(&profiler->calls);
free(profiler);
}
static size_t IO_Event_Profiler_memsize(const void *ptr) {
const struct IO_Event_Profiler *profiler = (const struct IO_Event_Profiler*)ptr;
return sizeof(*profiler) + IO_Event_Array_memory_size(&profiler->calls);
}
const rb_data_type_t IO_Event_Profiler_Type = {
.wrap_struct_name = "IO::Event::Profiler",
.function = {
.dmark = IO_Event_Profiler_mark,
.dcompact = IO_Event_Profiler_compact,
.dfree = IO_Event_Profiler_free,
.dsize = IO_Event_Profiler_memsize,
},
.flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED,
};
struct IO_Event_Profiler *IO_Event_Profiler_get(VALUE self) {
struct IO_Event_Profiler *profiler;
TypedData_Get_Struct(self, struct IO_Event_Profiler, &IO_Event_Profiler_Type, profiler);
return profiler;
}
VALUE IO_Event_Profiler_allocate(VALUE klass) {
struct IO_Event_Profiler *profiler = ALLOC(struct IO_Event_Profiler);
// Initialize the profiler state:
profiler->running = 0;
profiler->capture = 0;
profiler->stalls = 0;
profiler->nesting = 0;
profiler->current = NULL;
profiler->calls.element_initialize = (void (*)(void*))IO_Event_Profiler_Call_initialize;
profiler->calls.element_free = (void (*)(void*))IO_Event_Profiler_Call_free;
IO_Event_Array_initialize(&profiler->calls, 0, sizeof(struct IO_Event_Profiler_Call));
return TypedData_Wrap_Struct(klass, &IO_Event_Profiler_Type, profiler);
}
int IO_Event_Profiler_p(void) {
const char *enabled = getenv("IO_EVENT_PROFILER");
if (enabled && strcmp(enabled, "true") == 0) {
return 1;
}
return 0;
}
float IO_Event_Profiler_default_log_threshold(void) {
const char *log_threshold = getenv("IO_EVENT_PROFILER_LOG_THRESHOLD");
if (log_threshold) {
return strtof(log_threshold, NULL);
} else {
return 0.01;
}
}
int IO_Event_Profiler_default_track_calls(void) {
const char *track_calls = getenv("IO_EVENT_PROFILER_TRACK_CALLS");
if (track_calls && strcmp(track_calls, "false") == 0) {
return 0;
} else {
return 1;
}
}
VALUE IO_Event_Profiler_initialize(int argc, VALUE *argv, VALUE self) {
struct IO_Event_Profiler *profiler = IO_Event_Profiler_get(self);
VALUE log_threshold, track_calls;
rb_scan_args(argc, argv, "02", &log_threshold, &track_calls);
if (RB_NIL_P(log_threshold)) {
profiler->log_threshold = IO_Event_Profiler_default_log_threshold();
} else {
profiler->log_threshold = NUM2DBL(log_threshold);
}
if (RB_NIL_P(track_calls)) {
profiler->track_calls = IO_Event_Profiler_default_track_calls();
} else {
profiler->track_calls = RB_TEST(track_calls);
}
return self;
}
VALUE IO_Event_Profiler_default(VALUE klass) {
if (!IO_Event_Profiler_p()) {
return Qnil;
}
VALUE profiler = IO_Event_Profiler_allocate(klass);
struct IO_Event_Profiler *profiler_data = IO_Event_Profiler_get(profiler);
profiler_data->log_threshold = IO_Event_Profiler_default_log_threshold();
profiler_data->track_calls = IO_Event_Profiler_default_track_calls();
return profiler;
}
VALUE IO_Event_Profiler_new(float log_threshold, int track_calls) {
VALUE profiler = IO_Event_Profiler_allocate(IO_Event_Profiler);
struct IO_Event_Profiler *profiler_data = IO_Event_Profiler_get(profiler);
profiler_data->log_threshold = log_threshold;
profiler_data->track_calls = track_calls;
return profiler;
}
int event_flag_call_p(rb_event_flag_t event_flags) {
return event_flags & (RUBY_EVENT_CALL | RUBY_EVENT_C_CALL | RUBY_EVENT_B_CALL);
}
int event_flag_return_p(rb_event_flag_t event_flags) {
return event_flags & (RUBY_EVENT_RETURN | RUBY_EVENT_C_RETURN | RUBY_EVENT_B_RETURN);
}
const char *event_flag_name(rb_event_flag_t event_flag) {
switch (event_flag) {
case RUBY_EVENT_CALL: return "call";
case RUBY_EVENT_C_CALL: return "c-call";
case RUBY_EVENT_B_CALL: return "b-call";
case RUBY_EVENT_RETURN: return "return";
case RUBY_EVENT_C_RETURN: return "c-return";
case RUBY_EVENT_B_RETURN: return "b-return";
default: return "unknown";
}
}
static struct IO_Event_Profiler_Call* profiler_event_record_call(struct IO_Event_Profiler *profiler, rb_event_flag_t event_flag, ID id, VALUE klass) {
struct IO_Event_Profiler_Call *call = IO_Event_Array_push(&profiler->calls);
call->event_flag = event_flag;
call->parent = profiler->current;
profiler->current = call;
call->nesting = profiler->nesting;
profiler->nesting += 1;
if (id) {
call->id = id;
call->klass = klass;
} else {
rb_frame_method_id_and_class(&call->id, &call->klass);
}
const char *path = rb_sourcefile();
if (path) {
call->path = strdup(path);
}
call->line = rb_sourceline();
return call;
}
void IO_Event_Profiler_fiber_switch(struct IO_Event_Profiler *profiler);
static void IO_Event_Profiler_callback(rb_event_flag_t event_flag, VALUE data, VALUE self, ID id, VALUE klass) {
struct IO_Event_Profiler *profiler = IO_Event_Profiler_get(data);
if (event_flag & RUBY_EVENT_FIBER_SWITCH) {
IO_Event_Profiler_fiber_switch(profiler);
return;
}
// We don't want to capture data if we're not running:
if (!profiler->capture) return;
if (event_flag_call_p(event_flag)) {
struct IO_Event_Profiler_Call *call = profiler_event_record_call(profiler, event_flag, id, klass);
IO_Event_Time_current(&call->enter_time);
}
else if (event_flag_return_p(event_flag)) {
struct IO_Event_Profiler_Call *call = profiler->current;
// We may encounter returns without a preceeding call. This isn't an error, but we should pretend like the call started at the beginning of the profiling session:
if (call == NULL) {
struct IO_Event_Profiler_Call *last_call = IO_Event_Array_last(&profiler->calls);
call = profiler_event_record_call(profiler, event_flag, id, klass);
if (last_call) {
call->enter_time = last_call->enter_time;
} else {
call->enter_time = profiler->start_time;
}
}
IO_Event_Time_current(&call->exit_time);
profiler->current = call->parent;
// We may encounter returns without a preceeding call.
if (profiler->nesting > 0)
profiler->nesting -= 1;
}
}
VALUE IO_Event_Profiler_start(VALUE self) {
struct IO_Event_Profiler *profiler = IO_Event_Profiler_get(self);
if (profiler->running) return Qfalse;
profiler->running = 1;
IO_Event_Profiler_reset(profiler);
IO_Event_Time_current(&profiler->start_time);
rb_event_flag_t event_flags = RUBY_EVENT_FIBER_SWITCH;
if (profiler->track_calls) {
event_flags |= RUBY_EVENT_CALL | RUBY_EVENT_RETURN;
event_flags |= RUBY_EVENT_C_CALL | RUBY_EVENT_C_RETURN;
// event_flags |= RUBY_EVENT_B_CALL | RUBY_EVENT_B_RETURN;
}
VALUE thread = rb_thread_current();
rb_thread_add_event_hook(thread, IO_Event_Profiler_callback, event_flags, self);
return self;
}
VALUE IO_Event_Profiler_stop(VALUE self) {
struct IO_Event_Profiler *profiler = IO_Event_Profiler_get(self);
if (!profiler->running) return Qfalse;
profiler->running = 0;
VALUE thread = rb_thread_current();
rb_thread_remove_event_hook_with_data(thread, IO_Event_Profiler_callback, self);
IO_Event_Time_current(&profiler->stop_time);
IO_Event_Profiler_reset(profiler);
return self;
}
static inline float IO_Event_Profiler_duration(struct IO_Event_Profiler *profiler) {
struct timespec duration;
IO_Event_Time_current(&profiler->stop_time);
IO_Event_Time_elapsed(&profiler->start_time, &profiler->stop_time, &duration);
return IO_Event_Time_duration(&duration);
}
void IO_Event_Profiler_print(struct IO_Event_Profiler *profiler, FILE *restrict stream);
void IO_Event_Profiler_finish(struct IO_Event_Profiler *profiler) {
profiler->capture = 0;
struct IO_Event_Profiler_Call *current = profiler->current;
while (current) {
IO_Event_Time_current(&current->exit_time);
current = current->parent;
}
}
void IO_Event_Profiler_fiber_switch(struct IO_Event_Profiler *profiler)
{
float duration = IO_Event_Profiler_duration(profiler);
if (profiler->capture) {
IO_Event_Profiler_finish(profiler);
if (duration > profiler->log_threshold) {
profiler->stalls += 1;
IO_Event_Profiler_print(profiler, stderr);
}
}
IO_Event_Profiler_reset(profiler);
if (!IO_Event_Fiber_blocking(IO_Event_Fiber_current())) {
// Reset the start time:
IO_Event_Time_current(&profiler->start_time);
profiler->capture = 1;
}
}
static const float IO_EVENT_PROFILER_PRINT_MINIMUM_PROPORTION = 0.01;
void IO_Event_Profiler_print_tty(struct IO_Event_Profiler *profiler, FILE *restrict stream) {
struct timespec total_duration = {};
IO_Event_Time_elapsed(&profiler->start_time, &profiler->stop_time, &total_duration);
fprintf(stderr, "Fiber stalled for %.3f seconds\n", IO_Event_Time_duration(&total_duration));
size_t skipped = 0;
for (size_t i = 0; i < profiler->calls.limit; i += 1) {
struct IO_Event_Profiler_Call *call = profiler->calls.base[i];
struct timespec duration = {};
IO_Event_Time_elapsed(&call->enter_time, &call->exit_time, &duration);
// Skip calls that are too short to be meaningful:
if (IO_Event_Time_proportion(&duration, &total_duration) < IO_EVENT_PROFILER_PRINT_MINIMUM_PROPORTION) {
skipped += 1;
continue;
}
for (size_t i = 0; i < call->nesting; i += 1) {
fputc('\t', stream);
}
VALUE class_inspect = rb_inspect(call->klass);
const char *name = rb_id2name(call->id);
fprintf(stream, "%s:%d in %s '%s#%s' (" IO_EVENT_TIME_PRINTF_TIMESPEC "s)\n", call->path, call->line, event_flag_name(call->event_flag), RSTRING_PTR(class_inspect), name, IO_EVENT_TIME_PRINTF_TIMESPEC_ARGUMENTS(duration));
}
if (skipped > 0) {
fprintf(stream, "Skipped %zu calls that were too short to be meaningful.\n", skipped);
}
}
void IO_Event_Profiler_print_json(struct IO_Event_Profiler *profiler, FILE *restrict stream) {
struct timespec total_duration = {};
IO_Event_Time_elapsed(&profiler->start_time, &profiler->stop_time, &total_duration);
fputc('{', stream);
fprintf(stream, "\"duration\":" IO_EVENT_TIME_PRINTF_TIMESPEC, IO_EVENT_TIME_PRINTF_TIMESPEC_ARGUMENTS(total_duration));
size_t skipped = 0;
fprintf(stream, ",\"calls\":[");
int first = 1;
for (size_t i = 0; i < profiler->calls.limit; i += 1) {
struct IO_Event_Profiler_Call *call = profiler->calls.base[i];
struct timespec duration = {};
IO_Event_Time_elapsed(&call->enter_time, &call->exit_time, &duration);
// Skip calls that are too short to be meaningful:
if (IO_Event_Time_proportion(&duration, &total_duration) < IO_EVENT_PROFILER_PRINT_MINIMUM_PROPORTION) {
skipped += 1;
continue;
}
VALUE class_inspect = rb_inspect(call->klass);
const char *name = rb_id2name(call->id);
fprintf(stream, "%s{\"path\":\"%s\",\"line\":%d,\"class\":\"%s\",\"method\":\"%s\",\"duration\":" IO_EVENT_TIME_PRINTF_TIMESPEC ",\"nesting\":%zu}", first ? "" : ",", call->path, call->line, RSTRING_PTR(class_inspect), name, IO_EVENT_TIME_PRINTF_TIMESPEC_ARGUMENTS(duration), call->nesting);
first = 0;
}
fprintf(stream, "]");
if (skipped > 0) {
fprintf(stream, ",\"skipped\":%zu", skipped);
}
fprintf(stream, "}\n");
}
void IO_Event_Profiler_print(struct IO_Event_Profiler *profiler, FILE *restrict stream) {
if (isatty(fileno(stream))) {
IO_Event_Profiler_print_tty(profiler, stream);
} else {
IO_Event_Profiler_print_json(profiler, stream);
}
}
VALUE IO_Event_Profiler_stalls(VALUE self) {
struct IO_Event_Profiler *profiler = IO_Event_Profiler_get(self);
return SIZET2NUM(profiler->stalls);
}
void Init_IO_Event_Profiler(VALUE IO_Event) {
IO_Event_Profiler = rb_define_class_under(IO_Event, "Profiler", rb_cObject);
rb_define_alloc_func(IO_Event_Profiler, IO_Event_Profiler_allocate);
rb_define_singleton_method(IO_Event_Profiler, "default", IO_Event_Profiler_default, 0);
rb_define_method(IO_Event_Profiler, "initialize", IO_Event_Profiler_initialize, -1);
rb_define_method(IO_Event_Profiler, "start", IO_Event_Profiler_start, 0);
rb_define_method(IO_Event_Profiler, "stop", IO_Event_Profiler_stop, 0);
rb_define_method(IO_Event_Profiler, "stalls", IO_Event_Profiler_stalls, 0);
}
@@ -0,0 +1,8 @@
// Released under the MIT License.
// Copyright, 2025, by Samuel Williams.
#pragma once
#include <ruby.h>
void Init_IO_Event_Profiler(VALUE IO_Event);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,10 @@
// Released under the MIT License.
// Copyright, 2021-2025, by Samuel Williams.
#pragma once
#include <ruby.h>
#define IO_EVENT_SELECTOR_EPOLL
void Init_IO_Event_Selector_EPoll(VALUE IO_Event_Selector);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,10 @@
// Released under the MIT License.
// Copyright, 2021-2025, by Samuel Williams.
#pragma once
#include <ruby.h>
#define IO_EVENT_SELECTOR_KQUEUE
void Init_IO_Event_Selector_KQueue(VALUE IO_Event_Selector);
@@ -0,0 +1,21 @@
// Released under the MIT License.
// Copyright, 2021-2025, by Samuel Williams.
#include <ruby.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <poll.h>
#include <stdlib.h>
#include <stdio.h>
#ifndef __NR_pidfd_open
#define __NR_pidfd_open 434 /* System call # on most architectures */
#endif
static int
pidfd_open(pid_t pid, unsigned int flags)
{
return syscall(__NR_pidfd_open, pid, flags);
}
@@ -0,0 +1,298 @@
// Released under the MIT License.
// Copyright, 2021-2025, by Samuel Williams.
#include "selector.h"
#include <fcntl.h>
#include <stdlib.h>
static const int DEBUG = 0;
#ifndef HAVE_RB_IO_DESCRIPTOR
static ID id_fileno;
int IO_Event_Selector_io_descriptor(VALUE io) {
return RB_NUM2INT(rb_funcall(io, id_fileno, 0));
}
#endif
#ifndef HAVE_RB_PROCESS_STATUS_WAIT
static ID id_wait;
static VALUE rb_Process_Status = Qnil;
VALUE IO_Event_Selector_process_status_wait(rb_pid_t pid, int flags)
{
return rb_funcall(rb_Process_Status, id_wait, 2, PIDT2NUM(pid), INT2NUM(flags | WNOHANG));
}
#endif
int IO_Event_Selector_nonblock_set(int file_descriptor)
{
#ifdef _WIN32
u_long nonblock = 1;
ioctlsocket(file_descriptor, FIONBIO, &nonblock);
// Windows does not provide any way to know this, so we always restore it back to unset:
return 0;
#else
// Get the current mode:
int flags = fcntl(file_descriptor, F_GETFL, 0);
// Set the non-blocking flag if it isn't already:
if (!(flags & O_NONBLOCK)) {
fcntl(file_descriptor, F_SETFL, flags | O_NONBLOCK);
}
return flags;
#endif
}
void IO_Event_Selector_nonblock_restore(int file_descriptor, int flags)
{
#ifdef _WIN32
// Yolo...
u_long nonblock = flags;
ioctlsocket(file_descriptor, FIONBIO, &nonblock);
#else
// The flags didn't have O_NONBLOCK set, so it would have been set, so we need to restore it:
if (!(flags & O_NONBLOCK)) {
fcntl(file_descriptor, F_SETFL, flags);
}
#endif
}
struct IO_Event_Selector_nonblock_arguments {
int file_descriptor;
int flags;
};
static VALUE IO_Event_Selector_nonblock_ensure(VALUE _arguments) {
struct IO_Event_Selector_nonblock_arguments *arguments = (struct IO_Event_Selector_nonblock_arguments *)_arguments;
IO_Event_Selector_nonblock_restore(arguments->file_descriptor, arguments->flags);
return Qnil;
}
static VALUE IO_Event_Selector_nonblock(VALUE class, VALUE io)
{
struct IO_Event_Selector_nonblock_arguments arguments = {
.file_descriptor = IO_Event_Selector_io_descriptor(io),
.flags = IO_Event_Selector_nonblock_set(arguments.file_descriptor)
};
return rb_ensure(rb_yield, io, IO_Event_Selector_nonblock_ensure, (VALUE)&arguments);
}
void Init_IO_Event_Selector(VALUE IO_Event_Selector) {
#ifndef HAVE_RB_IO_DESCRIPTOR
id_fileno = rb_intern("fileno");
#endif
#ifndef HAVE_RB_PROCESS_STATUS_WAIT
id_wait = rb_intern("wait");
rb_Process_Status = rb_const_get_at(rb_mProcess, rb_intern("Status"));
rb_gc_register_mark_object(rb_Process_Status);
#endif
rb_define_singleton_method(IO_Event_Selector, "nonblock", IO_Event_Selector_nonblock, 1);
}
void IO_Event_Selector_initialize(struct IO_Event_Selector *backend, VALUE self, VALUE loop) {
RB_OBJ_WRITE(self, &backend->self, self);
RB_OBJ_WRITE(self, &backend->loop, loop);
backend->waiting = NULL;
backend->ready = NULL;
}
VALUE IO_Event_Selector_loop_resume(struct IO_Event_Selector *backend, VALUE fiber, int argc, VALUE *argv) {
return IO_Event_Fiber_transfer(fiber, argc, argv);
}
VALUE IO_Event_Selector_loop_yield(struct IO_Event_Selector *backend)
{
// TODO Why is this assertion failing in async?
// RUBY_ASSERT(backend->loop != IO_Event_Fiber_current());
return IO_Event_Fiber_transfer(backend->loop, 0, NULL);
}
struct wait_and_transfer_arguments {
int argc;
VALUE *argv;
struct IO_Event_Selector *backend;
struct IO_Event_Selector_Queue *waiting;
};
static void queue_pop(struct IO_Event_Selector *backend, struct IO_Event_Selector_Queue *waiting) {
if (waiting->head) {
waiting->head->tail = waiting->tail;
} else {
// We must have been at the head of the queue:
backend->waiting = waiting->tail;
}
if (waiting->tail) {
waiting->tail->head = waiting->head;
} else {
// We must have been at the tail of the queue:
backend->ready = waiting->head;
}
waiting->head = NULL;
waiting->tail = NULL;
}
static void queue_push(struct IO_Event_Selector *backend, struct IO_Event_Selector_Queue *waiting) {
assert(waiting->head == NULL);
assert(waiting->tail == NULL);
if (backend->waiting) {
// If there was an item in the queue already, we shift it along:
backend->waiting->head = waiting;
waiting->tail = backend->waiting;
} else {
// If the queue was empty, we update the tail too:
backend->ready = waiting;
}
// We always push to the front/head:
backend->waiting = waiting;
}
static VALUE wait_and_transfer(VALUE _arguments) {
struct wait_and_transfer_arguments *arguments = (struct wait_and_transfer_arguments *)_arguments;
VALUE fiber = arguments->argv[0];
int argc = arguments->argc - 1;
VALUE *argv = arguments->argv + 1;
return IO_Event_Selector_loop_resume(arguments->backend, fiber, argc, argv);
}
static VALUE wait_and_transfer_ensure(VALUE _arguments) {
struct wait_and_transfer_arguments *arguments = (struct wait_and_transfer_arguments *)_arguments;
queue_pop(arguments->backend, arguments->waiting);
return Qnil;
}
VALUE IO_Event_Selector_resume(struct IO_Event_Selector *backend, int argc, VALUE *argv)
{
rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
struct IO_Event_Selector_Queue waiting = {
.head = NULL,
.tail = NULL,
.flags = IO_EVENT_SELECTOR_QUEUE_FIBER,
.fiber = IO_Event_Fiber_current()
};
RB_OBJ_WRITTEN(backend->self, Qundef, waiting.fiber);
queue_push(backend, &waiting);
struct wait_and_transfer_arguments arguments = {
.argc = argc,
.argv = argv,
.backend = backend,
.waiting = &waiting,
};
return rb_ensure(wait_and_transfer, (VALUE)&arguments, wait_and_transfer_ensure, (VALUE)&arguments);
}
static VALUE wait_and_raise(VALUE _arguments) {
struct wait_and_transfer_arguments *arguments = (struct wait_and_transfer_arguments *)_arguments;
VALUE fiber = arguments->argv[0];
int argc = arguments->argc - 1;
VALUE *argv = arguments->argv + 1;
return IO_Event_Fiber_raise(fiber, argc, argv);
}
VALUE IO_Event_Selector_raise(struct IO_Event_Selector *backend, int argc, VALUE *argv)
{
rb_check_arity(argc, 2, UNLIMITED_ARGUMENTS);
struct IO_Event_Selector_Queue waiting = {
.head = NULL,
.tail = NULL,
.flags = IO_EVENT_SELECTOR_QUEUE_FIBER,
.fiber = IO_Event_Fiber_current()
};
RB_OBJ_WRITTEN(backend->self, Qundef, waiting.fiber);
queue_push(backend, &waiting);
struct wait_and_transfer_arguments arguments = {
.argc = argc,
.argv = argv,
.backend = backend,
.waiting = &waiting,
};
return rb_ensure(wait_and_raise, (VALUE)&arguments, wait_and_transfer_ensure, (VALUE)&arguments);
}
void IO_Event_Selector_ready_push(struct IO_Event_Selector *backend, VALUE fiber)
{
struct IO_Event_Selector_Queue *waiting = malloc(sizeof(struct IO_Event_Selector_Queue));
assert(waiting);
waiting->head = NULL;
waiting->tail = NULL;
waiting->flags = IO_EVENT_SELECTOR_QUEUE_INTERNAL;
RB_OBJ_WRITE(backend->self, &waiting->fiber, fiber);
queue_push(backend, waiting);
}
static inline
void IO_Event_Selector_ready_pop(struct IO_Event_Selector *backend, struct IO_Event_Selector_Queue *ready)
{
if (DEBUG) fprintf(stderr, "IO_Event_Selector_ready_pop -> %p\n", (void*)ready->fiber);
VALUE fiber = ready->fiber;
if (ready->flags & IO_EVENT_SELECTOR_QUEUE_INTERNAL) {
// This means that the fiber was added to the ready queue by the selector itself, and we need to transfer control to it, but before we do that, we need to remove it from the queue, as there is no expectation that returning from `transfer` will remove it.
queue_pop(backend, ready);
free(ready);
} else if (ready->flags & IO_EVENT_SELECTOR_QUEUE_FIBER) {
// This means the fiber added itself to the ready queue, and we need to transfer control back to it. Transferring control back to the fiber will call `queue_pop` and remove it from the queue.
} else {
rb_raise(rb_eRuntimeError, "Unknown queue type!");
}
IO_Event_Selector_loop_resume(backend, fiber, 0, NULL);
}
int IO_Event_Selector_ready_flush(struct IO_Event_Selector *backend)
{
int count = 0;
// During iteration of the queue, the same item may be re-queued. If we don't handle this correctly, we may end up in an infinite loop. So, to avoid this situation, we keep note of the current head of the queue and break the loop if we reach the same item again.
// Get the current tail and head of the queue:
struct IO_Event_Selector_Queue *waiting = backend->waiting;
if (DEBUG) fprintf(stderr, "IO_Event_Selector_ready_flush waiting = %p\n", waiting);
// Process from head to tail in order:
// During this, more items may be appended to tail.
while (backend->ready) {
if (DEBUG) fprintf(stderr, "backend->ready = %p\n", backend->ready);
struct IO_Event_Selector_Queue *ready = backend->ready;
count += 1;
IO_Event_Selector_ready_pop(backend, ready);
if (ready == waiting) break;
}
return count;
}
@@ -0,0 +1,158 @@
// Released under the MIT License.
// Copyright, 2021-2025, by Samuel Williams.
#pragma once
#include <ruby.h>
#include <ruby/thread.h>
#include <ruby/io.h>
#include "../time.h"
#include "../fiber.h"
#ifdef HAVE_RUBY_IO_BUFFER_H
#include <ruby/io/buffer.h>
#include <ruby/fiber/scheduler.h>
#endif
#ifndef RUBY_FIBER_SCHEDULER_VERSION
#define RUBY_FIBER_SCHEDULER_VERSION 1
#endif
#ifdef HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif
enum IO_Event {
IO_EVENT_READABLE = 1,
IO_EVENT_PRIORITY = 2,
IO_EVENT_WRITABLE = 4,
IO_EVENT_ERROR = 8,
IO_EVENT_HANGUP = 16,
// Used by kqueue to differentiate between process exit and file descriptor events:
IO_EVENT_EXIT = 32,
};
void Init_IO_Event_Selector(VALUE IO_Event_Selector);
static inline int IO_Event_try_again(int error) {
return error == EAGAIN || error == EWOULDBLOCK;
}
#ifdef HAVE_RB_IO_DESCRIPTOR
#define IO_Event_Selector_io_descriptor(io) rb_io_descriptor(io)
#else
int IO_Event_Selector_io_descriptor(VALUE io);
#endif
// Reap a process without hanging.
#ifdef HAVE_RB_PROCESS_STATUS_WAIT
#define IO_Event_Selector_process_status_wait(pid, flags) rb_process_status_wait(pid, flags | WNOHANG)
#else
VALUE IO_Event_Selector_process_status_wait(rb_pid_t pid, int flags);
#endif
int IO_Event_Selector_nonblock_set(int file_descriptor);
void IO_Event_Selector_nonblock_restore(int file_descriptor, int flags);
enum IO_Event_Selector_Queue_Flags {
IO_EVENT_SELECTOR_QUEUE_FIBER = 1,
IO_EVENT_SELECTOR_QUEUE_INTERNAL = 2,
};
struct IO_Event_Selector_Queue {
struct IO_Event_Selector_Queue *head;
struct IO_Event_Selector_Queue *tail;
enum IO_Event_Selector_Queue_Flags flags;
VALUE fiber;
};
// The internal state of the event selector.
// The event selector is responsible for managing the scheduling of fibers, as well as selecting for events.
struct IO_Event_Selector {
VALUE self;
VALUE loop;
// The ready queue is a list of fibers that are ready to be resumed from the event loop fiber.
// Append to waiting (front/head of queue).
struct IO_Event_Selector_Queue *waiting;
// Process from ready (back/tail of queue).
struct IO_Event_Selector_Queue *ready;
};
void IO_Event_Selector_initialize(struct IO_Event_Selector *backend, VALUE self, VALUE loop);
static inline
void IO_Event_Selector_mark(struct IO_Event_Selector *backend) {
rb_gc_mark_movable(backend->self);
rb_gc_mark_movable(backend->loop);
// Walk backwards through the ready queue:
struct IO_Event_Selector_Queue *ready = backend->ready;
while (ready) {
rb_gc_mark_movable(ready->fiber);
ready = ready->head;
}
}
static inline
void IO_Event_Selector_compact(struct IO_Event_Selector *backend) {
backend->self = rb_gc_location(backend->self);
backend->loop = rb_gc_location(backend->loop);
struct IO_Event_Selector_Queue *ready = backend->ready;
while (ready) {
ready->fiber = rb_gc_location(ready->fiber);
ready = ready->head;
}
}
// Transfer control from the event loop to a user fiber.
// This is used to transfer control to a user fiber when it may proceed.
// Strictly speaking, it's not a scheduling operation (does not schedule the current fiber).
VALUE IO_Event_Selector_loop_resume(struct IO_Event_Selector *backend, VALUE fiber, int argc, VALUE *argv);
// Transfer from a user fiber back to the event loop.
// This is used to transfer control back to the event loop in order to wait for events.
// Strictly speaking, it's not a scheduling operation (does not schedule the current fiber).
VALUE IO_Event_Selector_loop_yield(struct IO_Event_Selector *backend);
// Resume a specific fiber. This is a scheduling operation.
// The first argument is the fiber, the rest are the arguments to the resume.
//
// The implementation has two possible strategies:
// 1. Add the current fiber to the ready queue and transfer control to the target fiber.
// 2. Schedule the target fiber to be resumed by the event loop later on.
//
// We currently only implement the first strategy.
VALUE IO_Event_Selector_resume(struct IO_Event_Selector *backend, int argc, VALUE *argv);
// Raise an exception on a specific fiber.
// The first argument is the fiber, the rest are the arguments to the exception.
//
// The implementation has two possible strategies:
// 1. Add the current fiber to the ready queue and transfer control to the target fiber.
// 2. Schedule the target fiber to be resumed by the event loop with an exception later on.
//
// We currently only implement the first strategy.
VALUE IO_Event_Selector_raise(struct IO_Event_Selector *backend, int argc, VALUE *argv);
// Yield control to the event loop. This is a scheduling operation.
//
// The implementation adds the current fiber to the ready queue and transfers control to the event loop.
static inline
VALUE IO_Event_Selector_yield(struct IO_Event_Selector *backend)
{
return IO_Event_Selector_resume(backend, 1, &backend->loop);
}
// Append a specific fiber to the ready queue.
// The fiber can be an actual fiber or an object that responds to `alive?` and `transfer`.
// The implementation will transfer control to the fiber later on.
void IO_Event_Selector_ready_push(struct IO_Event_Selector *backend, VALUE fiber);
// Flush the ready queue by transferring control one at a time.
int IO_Event_Selector_ready_flush(struct IO_Event_Selector *backend);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,10 @@
// Released under the MIT License.
// Copyright, 2021-2025, by Samuel Williams.
#pragma once
#include <ruby.h>
#define IO_EVENT_SELECTOR_URING
void Init_IO_Event_Selector_URing(VALUE IO_Event_Selector);
@@ -0,0 +1,35 @@
// Released under the MIT License.
// Copyright, 2025, by Samuel Williams.
#include "time.h"
void IO_Event_Time_elapsed(const struct timespec* start, const struct timespec* stop, struct timespec *duration)
{
if ((stop->tv_nsec - start->tv_nsec) < 0) {
duration->tv_sec = stop->tv_sec - start->tv_sec - 1;
duration->tv_nsec = stop->tv_nsec - start->tv_nsec + 1000000000;
} else {
duration->tv_sec = stop->tv_sec - start->tv_sec;
duration->tv_nsec = stop->tv_nsec - start->tv_nsec;
}
}
float IO_Event_Time_duration(const struct timespec *duration)
{
return duration->tv_sec + duration->tv_nsec / 1000000000.0;
}
void IO_Event_Time_current(struct timespec *time) {
clock_gettime(CLOCK_MONOTONIC, time);
}
float IO_Event_Time_proportion(const struct timespec *duration, const struct timespec *total_duration) {
return IO_Event_Time_duration(duration) / IO_Event_Time_duration(total_duration);
}
float IO_Event_Time_delta(const struct timespec *start, const struct timespec *stop) {
struct timespec duration;
IO_Event_Time_elapsed(start, stop, &duration);
return IO_Event_Time_duration(&duration);
}
@@ -0,0 +1,17 @@
// Released under the MIT License.
// Copyright, 2025, by Samuel Williams.
#pragma once
#include <ruby.h>
#include <time.h>
void IO_Event_Time_elapsed(const struct timespec* start, const struct timespec* stop, struct timespec *duration);
float IO_Event_Time_duration(const struct timespec *duration);
void IO_Event_Time_current(struct timespec *time);
float IO_Event_Time_delta(const struct timespec *start, const struct timespec *stop);
float IO_Event_Time_proportion(const struct timespec *duration, const struct timespec *total_duration);
#define IO_EVENT_TIME_PRINTF_TIMESPEC "%.3g"
#define IO_EVENT_TIME_PRINTF_TIMESPEC_ARGUMENTS(ts) ((double)(ts).tv_sec + (ts).tv_nsec / 1e9)
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021-2025, by Samuel Williams.
require_relative "event/version"
require_relative "event/selector"
require_relative "event/timers"
require_relative "event/native"
@@ -0,0 +1,171 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021-2024, by Samuel Williams.
require_relative "../support"
module IO::Event
# @namespace
module Debug
# Enforces the selector interface and delegates operations to a wrapped selector instance.
#
# You can enable this in the default selector by setting the `IO_EVENT_DEBUG_SELECTOR` environment variable. In addition, you can log all selector operations to a file by setting the `IO_EVENT_DEBUG_SELECTOR_LOG` environment variable. This is useful for debugging and understanding the behavior of the event loop.
class Selector
# Wrap the given selector with debugging.
#
# @parameter selector [Selector] The selector to wrap.
# @parameter env [Hash] The environment to read configuration from.
def self.wrap(selector, env = ENV)
log = nil
if log_path = env["IO_EVENT_DEBUG_SELECTOR_LOG"]
log = File.open(log_path, "w")
end
return self.new(selector, log: log)
end
# Initialize the debug selector with the given selector and optional log.
#
# @parameter selector [Selector] The selector to wrap.
# @parameter log [IO] The log to write debug messages to.
def initialize(selector, log: nil)
@selector = selector
@readable = {}
@writable = {}
@priority = {}
unless Fiber.current == selector.loop
Kernel::raise "Selector must be initialized on event loop fiber!"
end
@log = log
end
# The idle duration of the underlying selector.
#
# @returns [Numeric] The idle duration.
def idle_duration
@selector.idle_duration
end
# The current time.
#
# @returns [Numeric] The current time.
def now
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
# Log the given message.
#
# @asynchronous Will block the calling fiber and the entire event loop.
def log(message)
return unless @log
Fiber.blocking do
@log.puts("T+%10.1f; %s" % [now, message])
end
end
# Wakeup the the selector.
def wakeup
@selector.wakeup
end
# Close the selector.
def close
log("Closing selector")
if @selector.nil?
Kernel::raise "Selector already closed!"
end
@selector.close
@selector = nil
end
# Transfer from the calling fiber to the selector.
def transfer
log("Transfering to event loop")
@selector.transfer
end
# Resume the given fiber with the given arguments.
def resume(*arguments)
log("Resuming fiber with #{arguments.inspect}")
@selector.resume(*arguments)
end
# Yield to the selector.
def yield
log("Yielding to event loop")
@selector.yield
end
# Push the given fiber to the selector ready list, such that it will be resumed on the next call to {select}.
#
# @parameter fiber [Fiber] The fiber that is ready.
def push(fiber)
log("Pushing fiber #{fiber.inspect} to ready list")
@selector.push(fiber)
end
# Raise the given exception on the given fiber.
#
# @parameter fiber [Fiber] The fiber to raise the exception on.
# @parameter arguments [Array] The arguments to use when raising the exception.
def raise(fiber, *arguments)
log("Raising exception on fiber #{fiber.inspect} with #{arguments.inspect}")
@selector.raise(fiber, *arguments)
end
# Check if the selector is ready.
#
# @returns [Boolean] Whether the selector is ready.
def ready?
@selector.ready?
end
# Wait for the given process, forwarded to the underlying selector.
def process_wait(*arguments)
log("Waiting for process with #{arguments.inspect}")
@selector.process_wait(*arguments)
end
# Wait for the given IO, forwarded to the underlying selector.
def io_wait(fiber, io, events)
log("Waiting for IO #{io.inspect} for events #{events.inspect}")
@selector.io_wait(fiber, io, events)
end
# Read from the given IO, forwarded to the underlying selector.
def io_read(fiber, io, buffer, length, offset = 0)
log("Reading from IO #{io.inspect} with buffer #{buffer}; length #{length} offset #{offset}")
@selector.io_read(fiber, io, buffer, length, offset)
end
# Write to the given IO, forwarded to the underlying selector.
def io_write(fiber, io, buffer, length, offset = 0)
log("Writing to IO #{io.inspect} with buffer #{buffer}; length #{length} offset #{offset}")
@selector.io_write(fiber, io, buffer, length, offset)
end
# Forward the given method to the underlying selector.
def respond_to?(name, include_private = false)
@selector.respond_to?(name, include_private)
end
# Select for the given duration, forwarded to the underlying selector.
def select(duration = nil)
log("Selecting for #{duration.inspect}")
unless Fiber.current == @selector.loop
Kernel::raise "Selector must be run on event loop fiber!"
end
@selector.select(duration)
end
end
end
end
@@ -0,0 +1,44 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021-2024, by Samuel Williams.
module IO::Event
# A thread safe synchronisation primative.
class Interrupt
def self.attach(selector)
self.new(selector)
end
def initialize(selector)
@selector = selector
@input, @output = ::IO.pipe
@fiber = Fiber.new do
while true
if @selector.io_wait(@fiber, @input, IO::READABLE)
@input.read_nonblock(1)
end
end
end
@fiber.transfer
end
# Send a sigle byte interrupt.
def signal
@output.write(".")
@output.flush
rescue IOError
# Ignore.
end
def close
@input.close
@output.close
# @fiber.raise(::Interrupt)
end
end
private_constant :Interrupt
end
@@ -0,0 +1,11 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.
begin
require "IO_Event"
rescue LoadError => error
warn "Could not load native event selector: #{error}"
require_relative "selector/nonblock"
end
@@ -0,0 +1,147 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021, by Wander Hillen.
# Copyright, 2021-2024, by Samuel Williams.
class IO
module Event
# A priority queue implementation using a standard binary minheap. It uses straight comparison
# of its contents to determine priority.
# See <https://en.wikipedia.org/wiki/Binary_heap> for explanations of the main methods.
class PriorityHeap
# Initializes the heap.
def initialize
# The heap is represented with an array containing a binary tree. See
# https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation for how this array
# is built up.
@contents = []
end
# @returns [Object | Nil] the smallest element in the heap without removing it, or nil if the heap is empty.
def peek
@contents[0]
end
# @returns [Integer] the number of elements in the heap.
def size
@contents.size
end
# Removes and returns the smallest element in the heap, or nil if the heap is empty.
#
# @returns [Object | Nil] The smallest element in the heap, or nil if the heap is empty.
def pop
# If the heap is empty:
if @contents.empty?
return nil
end
# If we have only one item, no swapping is required:
if @contents.size == 1
return @contents.pop
end
# Take the root of the tree:
value = @contents[0]
# Remove the last item in the tree:
last = @contents.pop
# Overwrite the root of the tree with the item:
@contents[0] = last
# Bubble it down into place:
bubble_down(0)
# validate!
return value
end
# Add a new element to the heap, then rearrange elements until the heap invariant is true again.
#
# @parameter element [Object] The element to add to the heap.
def push(element)
# Insert the item at the end of the heap:
@contents.push(element)
# Bubble it up into position:
bubble_up(@contents.size - 1)
# validate!
return self
end
# Empties out the heap, discarding all elements
def clear!
@contents = []
end
# Validate the heap invariant. Every element except the root must not be smaller than its parent element. Note that it MAY be equal.
def valid?
# Notice we skip index 0 on purpose, because it has no parent
(1..(@contents.size - 1)).all? { |e| @contents[e] >= @contents[(e - 1) / 2] }
end
private
# Left here for reference, but unused.
# def swap(i, j)
# @contents[i], @contents[j] = @contents[j], @contents[i]
# end
def bubble_up(index)
parent_index = (index - 1) / 2 # watch out, integer division!
while index > 0 && @contents[index] < @contents[parent_index]
# If the node has a smaller value than its parent, swap these nodes to uphold the minheap invariant and update the index of the 'current' node. If the node is already at index 0, we can also stop because that is the root of the heap.
# swap(index, parent_index)
@contents[index], @contents[parent_index] = @contents[parent_index], @contents[index]
index = parent_index
parent_index = (index - 1) / 2 # watch out, integer division!
end
end
def bubble_down(index)
swap_value = 0
swap_index = nil
while true
left_index = (2 * index) + 1
left_value = @contents[left_index]
if left_value.nil?
# This node has no children so it can't bubble down any further. We're done here!
return
end
# Determine which of the child nodes has the smallest value:
right_index = left_index + 1
right_value = @contents[right_index]
if right_value.nil? or right_value > left_value
swap_value = left_value
swap_index = left_index
else
swap_value = right_value
swap_index = right_index
end
if @contents[index] < swap_value
# No need to swap, the minheap invariant is already satisfied:
return
else
# At least one of the child node has a smaller value than the current node, swap current node with that child and update current node for if it might need to bubble down even further:
# swap(index, swap_index)
@contents[index], @contents[swap_index] = @contents[swap_index], @contents[index]
index = swap_index
end
end
end
end
end
end
@@ -0,0 +1,18 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.
require_relative "native"
module IO::Event
unless self.const_defined?(:Profiler)
module Profiler
# The default profiler, if the platform supports it.
# Use `IO_EVENT_PROFILER=true` to enable it.
def self.default
nil
end
end
end
end
@@ -0,0 +1,48 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021-2024, by Samuel Williams.
require_relative "selector/select"
require_relative "debug/selector"
require_relative "support"
module IO::Event
# @namespace
module Selector
# The default selector implementation, which is chosen based on the environment and available implementations.
#
# @parameter env [Hash] The environment to read configuration from.
# @returns [Class] The default selector implementation.
def self.default(env = ENV)
if name = env["IO_EVENT_SELECTOR"]&.to_sym
return const_get(name)
end
if self.const_defined?(:URing)
URing
elsif self.const_defined?(:EPoll)
EPoll
elsif self.const_defined?(:KQueue)
KQueue
else
Select
end
end
# Create a new selector instance, according to the best available implementation.
#
# @parameter loop [Fiber] The event loop fiber.
# @parameter env [Hash] The environment to read configuration from.
# @returns [Selector] The new selector instance.
def self.new(loop, env = ENV)
selector = default(env).new(loop)
if debug = env["IO_EVENT_DEBUG_SELECTOR"]
selector = Debug::Selector.wrap(selector, env)
end
return selector
end
end
end
@@ -0,0 +1,21 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2022-2024, by Samuel Williams.
require "io/nonblock"
module IO::Event
module Selector
# Execute the given block in non-blocking mode.
#
# @parameter io [IO] The IO object to operate on.
# @yields {...} The block to execute.
def self.nonblock(io, &block)
io.nonblock(&block)
rescue Errno::EBADF
# Windows.
yield
end
end
end
@@ -0,0 +1,495 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021-2024, by Samuel Williams.
# Copyright, 2023, by Math Ieu.
require_relative "../interrupt"
require_relative "../support"
module IO::Event
module Selector
# A pure-Ruby implementation of the event selector.
class Select
# Initialize the selector with the given event loop fiber.
def initialize(loop)
@loop = loop
@waiting = Hash.new.compare_by_identity
@blocked = false
@ready = Queue.new
@interrupt = Interrupt.attach(self)
@idle_duration = 0.0
end
# @attribute [Fiber] The event loop fiber.
attr :loop
# @attribute [Float] This is the amount of time the event loop was idle during the last select call.
attr :idle_duration
# Wake up the event loop if it is currently sleeping.
def wakeup
if @blocked
@interrupt.signal
return true
end
return false
end
# Close the selector and release any resources.
def close
@interrupt.close
@loop = nil
@waiting = nil
end
Optional = Struct.new(:fiber) do
def transfer(*arguments)
fiber&.transfer(*arguments)
end
def alive?
fiber&.alive?
end
def nullify
self.fiber = nil
end
end
# Transfer from the current fiber to the event loop.
def transfer
@loop.transfer
end
# Transfer from the current fiber to the specified fiber. Put the current fiber into the ready list.
def resume(fiber, *arguments)
optional = Optional.new(Fiber.current)
@ready.push(optional)
fiber.transfer(*arguments)
ensure
optional.nullify
end
# Yield from the current fiber back to the event loop. Put the current fiber into the ready list.
def yield
optional = Optional.new(Fiber.current)
@ready.push(optional)
@loop.transfer
ensure
optional.nullify
end
# Append the given fiber into the ready list.
def push(fiber)
@ready.push(fiber)
end
# Transfer to the given fiber and raise an exception. Put the current fiber into the ready list.
def raise(fiber, *arguments)
optional = Optional.new(Fiber.current)
@ready.push(optional)
fiber.raise(*arguments)
ensure
optional.nullify
end
# @returns [Boolean] Whether the ready list is not empty, i.e. there are fibers ready to be resumed.
def ready?
!@ready.empty?
end
Waiter = Struct.new(:fiber, :events, :tail) do
def alive?
self.fiber&.alive?
end
# Dispatch the given events to the list of waiting fibers. If the fiber was not waiting for the given events, it is reactivated by calling the given block.
def dispatch(events, &reactivate)
# We capture the tail here, because calling reactivate might modify it:
tail = self.tail
if fiber = self.fiber
if fiber.alive?
revents = events & self.events
if revents.zero?
reactivate.call(self)
else
self.fiber = nil
fiber.transfer(revents)
end
else
self.fiber = nil
end
end
tail&.dispatch(events, &reactivate)
end
def invalidate
self.fiber = nil
end
def each(&block)
if fiber = self.fiber
yield fiber, self.events
end
self.tail&.each(&block)
end
end
# Wait for the given IO to become readable or writable.
#
# @parameter fiber [Fiber] The fiber that is waiting.
# @parameter io [IO] The IO object to wait on.
# @parameter events [Integer] The events to wait for.
def io_wait(fiber, io, events)
waiter = @waiting[io] = Waiter.new(fiber, events, @waiting[io])
@loop.transfer
ensure
waiter&.invalidate
end
# Wait for multiple IO objects to become readable or writable.
#
# @parameter readable [Array(IO)] The list of IO objects to wait for readability.
# @parameter writable [Array(IO)] The list of IO objects to wait for writability.
# @parameter priority [Array(IO)] The list of IO objects to wait for priority events.
def io_select(readable, writable, priority, timeout)
Thread.new do
IO.select(readable, writable, priority, timeout)
end.value
end
EAGAIN = -Errno::EAGAIN::Errno
EWOULDBLOCK = -Errno::EWOULDBLOCK::Errno
# Whether the given error code indicates that the operation should be retried.
protected def again?(errno)
errno == EAGAIN or errno == EWOULDBLOCK
end
if Support.fiber_scheduler_v3?
# Ruby 3.3+, full IO::Buffer support.
# Read from the given IO to the buffer.
#
# @parameter length [Integer] The minimum number of bytes to read.
# @parameter offset [Integer] The offset into the buffer to read to.
def io_read(fiber, io, buffer, length, offset = 0)
total = 0
Selector.nonblock(io) do
while true
result = Fiber.blocking{buffer.read(io, 0, offset)}
if result < 0
if again?(result)
self.io_wait(fiber, io, IO::READABLE)
else
return result
end
elsif result == 0
break
else
total += result
break if total >= length
offset += result
end
end
end
return total
end
# Write to the given IO from the buffer.
#
# @parameter length [Integer] The minimum number of bytes to write.
# @parameter offset [Integer] The offset into the buffer to write from.
def io_write(fiber, io, buffer, length, offset = 0)
total = 0
Selector.nonblock(io) do
while true
result = Fiber.blocking{buffer.write(io, 0, offset)}
if result < 0
if again?(result)
self.io_wait(fiber, io, IO::READABLE)
else
return result
end
elsif result == 0
break result
else
total += result
break if total >= length
offset += result
end
end
end
return total
end
elsif Support.fiber_scheduler_v2?
# Ruby 3.2, most IO::Buffer support, but slightly clunky read/write methods.
def io_read(fiber, io, buffer, length, offset = 0)
total = 0
Selector.nonblock(io) do
maximum_size = buffer.size - offset
while maximum_size > 0
result = Fiber.blocking{buffer.read(io, maximum_size, offset)}
if again?(result)
if length > 0
self.io_wait(fiber, io, IO::READABLE)
else
return result
end
elsif result < 0
return result
else
total += result
offset += result
break if total >= length
end
maximum_size = buffer.size - offset
end
end
return total
end
def io_write(fiber, io, buffer, length, offset = 0)
total = 0
Selector.nonblock(io) do
maximum_size = buffer.size - offset
while maximum_size > 0
result = Fiber.blocking{buffer.write(io, maximum_size, offset)}
if again?(result)
if length > 0
self.io_wait(fiber, io, IO::READABLE)
else
return result
end
elsif result < 0
return result
else
total += result
offset += result
break if total >= length
end
maximum_size = buffer.size - offset
end
end
return total
end
elsif Support.fiber_scheduler_v1?
# Ruby <= 3.1, limited IO::Buffer support.
def io_read(fiber, _io, buffer, length, offset = 0)
# We need to avoid any internal buffering, so we use a duplicated IO object:
io = IO.for_fd(_io.fileno, autoclose: false)
total = 0
maximum_size = buffer.size - offset
while maximum_size > 0
case result = blocking{io.read_nonblock(maximum_size, exception: false)}
when :wait_readable
if length > 0
self.io_wait(fiber, io, IO::READABLE)
else
return EWOULDBLOCK
end
when :wait_writable
if length > 0
self.io_wait(fiber, io, IO::WRITABLE)
else
return EWOULDBLOCK
end
when nil
break
else
buffer.set_string(result, offset)
size = result.bytesize
total += size
offset += size
break if size >= length
length -= size
end
maximum_size = buffer.size - offset
end
return total
rescue IOError => error
return -Errno::EBADF::Errno
rescue SystemCallError => error
return -error.errno
end
def io_write(fiber, _io, buffer, length, offset = 0)
# We need to avoid any internal buffering, so we use a duplicated IO object:
io = IO.for_fd(_io.fileno, autoclose: false)
total = 0
maximum_size = buffer.size - offset
while maximum_size > 0
chunk = buffer.get_string(offset, maximum_size)
case result = blocking{io.write_nonblock(chunk, exception: false)}
when :wait_readable
if length > 0
self.io_wait(fiber, io, IO::READABLE)
else
return EWOULDBLOCK
end
when :wait_writable
if length > 0
self.io_wait(fiber, io, IO::WRITABLE)
else
return EWOULDBLOCK
end
else
total += result
offset += result
break if result >= length
length -= result
end
maximum_size = buffer.size - offset
end
return total
rescue IOError => error
return -Errno::EBADF::Errno
rescue SystemCallError => error
return -error.errno
end
def blocking(&block)
fiber = Fiber.new(blocking: true, &block)
return fiber.resume(fiber)
end
end
def process_wait(fiber, pid, flags)
Thread.new do
Process::Status.wait(pid, flags)
end.value
end
private def pop_ready
unless @ready.empty?
count = @ready.size
count.times do
fiber = @ready.pop
fiber.transfer if fiber.alive?
end
return true
end
end
def select(duration = nil)
if pop_ready
# If we have popped items from the ready list, they may influence the duration calculation, so we don't delay the event loop:
duration = 0
end
readable = Array.new
writable = Array.new
priority = Array.new
@waiting.each do |io, waiter|
waiter.each do |fiber, events|
if (events & IO::READABLE) > 0
readable << io
end
if (events & IO::WRITABLE) > 0
writable << io
end
if (events & IO::PRIORITY) > 0
priority << io
end
end
end
duration = 0 unless @ready.empty?
error = nil
if duration&.>(0)
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
else
@idle_duration = 0.0
end
# We need to handle interrupts on blocking IO. Every other implementation uses EINTR, but that doesn't work with `::IO.select` as it will retry the call on EINTR.
Thread.handle_interrupt(::Exception => :on_blocking) do
@blocked = true
readable, writable, priority = ::IO.select(readable, writable, priority, duration)
rescue ::Exception => error
# Requeue below...
ensure
@blocked = false
if start_time
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@idle_duration = end_time - start_time
end
end
if error
# Requeue the error into the pending exception queue:
Thread.current.raise(error)
return 0
end
ready = Hash.new(0).compare_by_identity
readable&.each do |io|
ready[io] |= IO::READABLE
end
writable&.each do |io|
ready[io] |= IO::WRITABLE
end
priority&.each do |io|
ready[io] |= IO::PRIORITY
end
ready.each do |io, events|
@waiting.delete(io).dispatch(events) do |waiter|
# Re-schedule the waiting IO:
waiter.tail = @waiting[io]
@waiting[io] = waiter
end
end
return ready.size
end
end
end
end
@@ -0,0 +1,57 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2022-2024, by Samuel Williams.
class IO
module Event
# Helper methods for detecting support for various features.
module Support
# Some features are only availble if the IO::Buffer class is available.
#
# @returns [Boolean] Whether the IO::Buffer class is available.
def self.buffer?
IO.const_defined?(:Buffer)
end
# The basic fiber scheduler was introduced along side the IO::Buffer class.
#
# @returns [Boolean] Whether the IO::Buffer class is available.
#
# To be removed on 31 Mar 2025.
def self.fiber_scheduler_v1?
IO.const_defined?(:Buffer)
end
# More advanced read/write methods and blocking controls were introduced in Ruby 3.2.
#
# To be removed on 31 Mar 2026.
def self.fiber_scheduler_v2?
# Some interface changes were back-ported incorrectly:
# https://github.com/ruby/ruby/pull/10778
# Specifically "Improvements to IO::Buffer read/write/pread/pwrite."
# Missing correct size calculation.
return false if RUBY_VERSION >= "3.2.5"
IO.const_defined?(:Buffer) and Fiber.respond_to?(:blocking) and IO::Buffer.instance_method(:read).arity == -1
end
# Updated inferfaces for read/write and IO::Buffer were introduced in Ruby 3.3, including pread/pwrite.
#
# To become the default 31 Mar 2026.
def self.fiber_scheduler_v3?
if fiber_scheduler_v2?
return true if RUBY_VERSION >= "3.3"
# Feature detection if required:
begin
IO::Buffer.new.slice(0, 0).write(STDOUT)
return true
rescue
return false
end
end
end
end
end
end
@@ -0,0 +1,149 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2024, by Samuel Williams.
require_relative "priority_heap"
class IO
module Event
# An efficient sorted set of timers.
class Timers
# A handle to a scheduled timer.
class Handle
# Initialize the handle with the given time and block.
#
# @parameter time [Float] The time at which the block should be called.
# @parameter block [Proc] The block to call.
def initialize(time, block)
@time = time
@block = block
end
# @attribute [Float] The time at which the block should be called.
attr :time
# @attribute [Proc | Nil] The block to call when the timer fires.
attr :block
# Compare the handle with another handle.
#
# @parameter other [Handle] The other handle to compare with.
# @returns [Boolean] Whether the handle is less than the other handle.
def < other
@time < other.time
end
# Compare the handle with another handle.
#
# @parameter other [Handle] The other handle to compare with.
# @returns [Boolean] Whether the handle is greater than the other handle.
def > other
@time > other.time
end
# Invoke the block.
def call(...)
@block.call(...)
end
# Cancel the timer.
def cancel!
@block = nil
end
# @returns [Boolean] Whether the timer has been cancelled.
def cancelled?
@block.nil?
end
end
# Initialize the timers.
def initialize
@heap = PriorityHeap.new
@scheduled = []
end
# @returns [Integer] The number of timers in the heap.
def size
flush!
return @heap.size
end
# Schedule a block to be called at a specific time in the future.
#
# @parameter time [Float] The time at which the block should be called, relative to {#now}.
# @parameter block [Proc] The block to call.
def schedule(time, block)
handle = Handle.new(time, block)
@scheduled << handle
return handle
end
# Schedule a block to be called after a specific time offset, relative to the current time as returned by {#now}.
#
# @parameter offset [#to_f] The time offset from the current time at which the block should be called.
# @yields {|now| ...} When the timer fires.
def after(offset, &block)
schedule(self.now + offset.to_f, block)
end
# Compute the time interval until the next timer fires.
#
# @parameter now [Float] The current time.
# @returns [Float | Nil] The time interval until the next timer fires, if any.
def wait_interval(now = self.now)
flush!
while handle = @heap.peek
if handle.cancelled?
@heap.pop
else
return handle.time - now
end
end
end
# @returns [Float] The current time.
def now
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
end
# Fire all timers that are ready to fire.
#
# @parameter now [Float] The current time.
def fire(now = self.now)
# Flush scheduled timers into the heap:
flush!
# Get the earliest timer:
while handle = @heap.peek
if handle.cancelled?
@heap.pop
elsif handle.time <= now
# Remove the earliest timer from the heap:
@heap.pop
# Call the block:
handle.call(now)
else
break
end
end
end
# Flush all scheduled timers into the heap.
#
# This is a small optimization which assumes that most timers (timeouts) will be cancelled.
protected def flush!
while handle = @scheduled.pop
@heap.push(handle) unless handle.cancelled?
end
end
end
end
end
@@ -0,0 +1,12 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021-2025, by Samuel Williams.
# @namespace
class IO
# @namespace
module Event
VERSION = "1.9.0"
end
end
+31
View File
@@ -0,0 +1,31 @@
# MIT License
Copyright, 2021, by Wander Hillen.
Copyright, 2021-2025, by Samuel Williams.
Copyright, 2021, by Delton Ding.
Copyright, 2021-2024, by Benoit Daloze.
Copyright, 2022, by Alex Matchneer.
Copyright, 2022, by Bruno Sutic.
Copyright, 2023, by Math Ieu.
Copyright, 2024, by Pavel Rosický.
Copyright, 2024, by Anthony Ross.
Copyright, 2024, by Shizuo Fujita.
Copyright, 2024, by Jean Boussier.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+49
View File
@@ -0,0 +1,49 @@
# ![Event](logo.svg)
Provides low level cross-platform primitives for constructing event loops, with support for `select`, `kqueue`, `epoll` and `io_uring`.
[![Development Status](https://github.com/socketry/io-event/workflows/Test/badge.svg)](https://github.com/socketry/io-event/actions?workflow=Test)
## Motivation
The initial proof-of-concept [Async](https://github.com/socketry/async) was built on [NIO4r](https://github.com/socketry/nio4r). It was perfectly acceptable and well tested in production, however being built on `libev` was a little bit limiting. I wanted to directly build my fiber scheduler into the fabric of the event loop, which is what this gem exposes - it is specifically implemented to support building event loops beneath the fiber scheduler interface, providing an efficient C implementation of all the core operations.
## Usage
Please see the [project documentation](https://socketry.github.io/io-event/) for more details.
- [Getting Started](https://socketry.github.io/io-event/guides/getting-started/index) - This guide explains how to use `io-event` for non-blocking IO.
## Releases
Please see the [project releases](https://socketry.github.io/io-event/releases/index) for all releases.
### v1.9.0
- [Improved `IO::Event::Profiler` for detecting stalls.](https://socketry.github.io/io-event/releases/index#improved-io::event::profiler-for-detecting-stalls.)
### v1.8.0
- [Detecting fibers that are stalling the event loop.](https://socketry.github.io/io-event/releases/index#detecting-fibers-that-are-stalling-the-event-loop.)
### v1.7.5
- Fix `process_wait` race condition on EPoll that could cause a hang.
## Contributing
We welcome contributions to this project.
1. Fork it.
2. Create your feature branch (`git checkout -b my-new-feature`).
3. Commit your changes (`git commit -am 'Add some feature'`).
4. Push to the branch (`git push origin my-new-feature`).
5. Create new Pull Request.
### Developer Certificate of Origin
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
### Community Guidelines
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
+57
View File
@@ -0,0 +1,57 @@
# Releases
## v1.9.0
### Improved `IO::Event::Profiler` for detecting stalls.
A new `IO::Event::Profiler` class has been added to help detect stalls in the event loop. The previous approach was insufficient to detect all possible stalls. This new approach uses the `RUBY_EVENT_FIBER_SWITCH` event to track context switching by the scheduler, and can detect stalls no matter how they occur.
``` ruby
profiler = IO::Event::Profiler.new
profiler.start
Fiber.new do
sleep 1.0
end.transfer
profiler.stop
```
A default profiler is exposed using `IO::Event::Profiler.default` which is controlled by the following environment variables:
- `IO_EVENT_PROFILER=true` - Enable the profiler, otherwise `IO::Event::Profiler.default` will return `nil`.
- `IO_EVENT_PROFILER_LOG_THRESHOLD` - Specify the threshold in seconds for logging a stall. Defaults to `0.01`.
- `IO_EVENT_PROFILER_TRACK_CALLS` - Track the method call for each event, in order to log specifically which method is causing the stall. Defaults to `true`.
The previous environment variables `IO_EVENT_SELECTOR_STALL_LOG_THRESHOLD` and `IO_EVENT_SELECTOR_STALL_LOG` no longer have any effect.
## v1.8.0
### Detecting fibers that are stalling the event loop.
A new (experimental) feature for detecting fiber stalls has been added. This feature is disabled by default and can be enabled by setting the `IO_EVENT_SELECTOR_STALL_LOG_THRESHOLD` to `true` or a floating point number representing the threshold in seconds.
When enabled, the event loop will measure and profile user code when resuming a fiber. If the fiber takes too long to return back to the event loop, the event loop will log a warning message with a profile of the fiber's execution.
> cat test.rb
#!/usr/bin/env ruby
require_relative "lib/async"
Async do
Fiber.blocking do
sleep 1
end
end
> IO_EVENT_SELECTOR_STALL_LOG_THRESHOLD=true bundle exec ./test.rb
Fiber stalled for 1.003 seconds
/home/samuel/Developer/socketry/async/test.rb:6 in '#<Class:Fiber>#blocking' (1s)
/home/samuel/Developer/socketry/async/test.rb:7 in 'Kernel#sleep' (1s)
There is a performance overhead to this feature, so it is recommended to only enable it when debugging performance issues.
## v1.7.5
- Fix `process_wait` race condition on EPoll that could cause a hang.