Skip to content

Commit

Permalink
Refactor the IOCP event loop (timers, ...) (#15238)
Browse files Browse the repository at this point in the history
Upgrades the IOCP event loop for Windows to be on par with the Polling event loops (epoll, kqueue) on UNIX. After a few low hanging fruits (enqueue multiple fibers on each call, for example) the last commit completely rewrites the `#run` method:

- store events in pairing heaps;
- high resolution timers (`CreateWaitableTimer`);
- block forever/never (no need for timeout);
- cancelling timeouts (no more dead fibers);
- thread safety (parallel timer de/enqueues) for [RFC #2];
- interrupt run using completion key instead of an UserAPC for [RFC #2] (untested).

[RFC #2]: crystal-lang/rfcs#2
  • Loading branch information
ysbaddaden authored Dec 6, 2024
1 parent 0e80a60 commit f8db68e
Show file tree
Hide file tree
Showing 20 changed files with 526 additions and 200 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/mingw-w64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
cc crystal.obj -o .build/crystal.exe -municode \
$(pkg-config bdw-gc libpcre2-8 iconv zlib libffi --libs) \
$(llvm-config --libs --system-libs --ldflags) \
-lole32 -lWS2_32 -Wl,--stack,0x800000
-lole32 -lWS2_32 -lntdll -Wl,--stack,0x800000
- name: Package Crystal
shell: msys2 {0}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
{% skip_file unless Crystal::EventLoop.has_constant?(:Polling) %}

require "spec"
require "crystal/event_loop/timers"

private struct Timer
include Crystal::PointerPairingHeap::Node

property! wake_at : Time::Span

def initialize(timeout : Time::Span? = nil)
@wake_at = Time.monotonic + timeout if timeout
end

def heap_compare(other : Pointer(self)) : Bool
wake_at < other.value.wake_at
end
end

describe Crystal::EventLoop::Polling::Timers do
describe Crystal::EventLoop::Timers do
it "#empty?" do
timers = Crystal::EventLoop::Polling::Timers.new
timers = Crystal::EventLoop::Timers(Timer).new
timers.empty?.should be_true

event = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 7.seconds)
event = Timer.new(7.seconds)
timers.add(pointerof(event))
timers.empty?.should be_false

Expand All @@ -17,13 +30,13 @@ describe Crystal::EventLoop::Polling::Timers do

it "#next_ready?" do
# empty
timers = Crystal::EventLoop::Polling::Timers.new
timers = Crystal::EventLoop::Timers(Timer).new
timers.next_ready?.should be_nil

# with events
event1s = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 1.second)
event3m = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 3.minutes)
event5m = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 5.minutes)
event1s = Timer.new(1.second)
event3m = Timer.new(3.minutes)
event5m = Timer.new(5.minutes)

timers.add(pointerof(event5m))
timers.next_ready?.should eq(event5m.wake_at?)
Expand All @@ -36,24 +49,24 @@ describe Crystal::EventLoop::Polling::Timers do
end

it "#dequeue_ready" do
timers = Crystal::EventLoop::Polling::Timers.new
timers = Crystal::EventLoop::Timers(Timer).new

event1 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 0.seconds)
event2 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 0.seconds)
event3 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 1.minute)
event1 = Timer.new(0.seconds)
event2 = Timer.new(0.seconds)
event3 = Timer.new(1.minute)

# empty
called = 0
timers.dequeue_ready { called += 1 }
called.should eq(0)

# add events in non chronological order
timers = Crystal::EventLoop::Polling::Timers.new
timers = Crystal::EventLoop::Timers(Timer).new
timers.add(pointerof(event1))
timers.add(pointerof(event3))
timers.add(pointerof(event2))

events = [] of Crystal::EventLoop::Polling::Event*
events = [] of Timer*
timers.dequeue_ready { |event| events << event }

events.should eq([
Expand All @@ -64,12 +77,12 @@ describe Crystal::EventLoop::Polling::Timers do
end

it "#add" do
timers = Crystal::EventLoop::Polling::Timers.new
timers = Crystal::EventLoop::Timers(Timer).new

event0 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current)
event1 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 0.seconds)
event2 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 2.minutes)
event3 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 1.minute)
event0 = Timer.new
event1 = Timer.new(0.seconds)
event2 = Timer.new(2.minutes)
event3 = Timer.new(1.minute)

# add events in non chronological order
timers.add(pointerof(event1)).should be_true # added to the head (next ready)
Expand All @@ -81,13 +94,13 @@ describe Crystal::EventLoop::Polling::Timers do
end

it "#delete" do
event1 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 0.seconds)
event2 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 0.seconds)
event3 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 1.minute)
event4 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 4.minutes)
event1 = Timer.new(0.seconds)
event2 = Timer.new(0.seconds)
event3 = Timer.new(1.minute)
event4 = Timer.new(4.minutes)

# add events in non chronological order
timers = Crystal::EventLoop::Polling::Timers.new
timers = Crystal::EventLoop::Timers(Timer).new
timers.add(pointerof(event1))
timers.add(pointerof(event3))
timers.add(pointerof(event2))
Expand Down
Loading

0 comments on commit f8db68e

Please sign in to comment.