Skip to content

Commit f8db68e

Browse files
authored
Refactor the IOCP event loop (timers, ...) (#15238)
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
1 parent 0e80a60 commit f8db68e

File tree

20 files changed

+526
-200
lines changed

20 files changed

+526
-200
lines changed

.github/workflows/mingw-w64.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
cc crystal.obj -o .build/crystal.exe -municode \
8181
$(pkg-config bdw-gc libpcre2-8 iconv zlib libffi --libs) \
8282
$(llvm-config --libs --system-libs --ldflags) \
83-
-lole32 -lWS2_32 -Wl,--stack,0x800000
83+
-lole32 -lWS2_32 -lntdll -Wl,--stack,0x800000
8484
8585
- name: Package Crystal
8686
shell: msys2 {0}

spec/std/crystal/event_loop/polling/timers_spec.cr renamed to spec/std/crystal/event_loop/timers_spec.cr

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
1-
{% skip_file unless Crystal::EventLoop.has_constant?(:Polling) %}
2-
31
require "spec"
2+
require "crystal/event_loop/timers"
3+
4+
private struct Timer
5+
include Crystal::PointerPairingHeap::Node
6+
7+
property! wake_at : Time::Span
8+
9+
def initialize(timeout : Time::Span? = nil)
10+
@wake_at = Time.monotonic + timeout if timeout
11+
end
12+
13+
def heap_compare(other : Pointer(self)) : Bool
14+
wake_at < other.value.wake_at
15+
end
16+
end
417

5-
describe Crystal::EventLoop::Polling::Timers do
18+
describe Crystal::EventLoop::Timers do
619
it "#empty?" do
7-
timers = Crystal::EventLoop::Polling::Timers.new
20+
timers = Crystal::EventLoop::Timers(Timer).new
821
timers.empty?.should be_true
922

10-
event = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 7.seconds)
23+
event = Timer.new(7.seconds)
1124
timers.add(pointerof(event))
1225
timers.empty?.should be_false
1326

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

1831
it "#next_ready?" do
1932
# empty
20-
timers = Crystal::EventLoop::Polling::Timers.new
33+
timers = Crystal::EventLoop::Timers(Timer).new
2134
timers.next_ready?.should be_nil
2235

2336
# with events
24-
event1s = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 1.second)
25-
event3m = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 3.minutes)
26-
event5m = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 5.minutes)
37+
event1s = Timer.new(1.second)
38+
event3m = Timer.new(3.minutes)
39+
event5m = Timer.new(5.minutes)
2740

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

3851
it "#dequeue_ready" do
39-
timers = Crystal::EventLoop::Polling::Timers.new
52+
timers = Crystal::EventLoop::Timers(Timer).new
4053

41-
event1 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 0.seconds)
42-
event2 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 0.seconds)
43-
event3 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 1.minute)
54+
event1 = Timer.new(0.seconds)
55+
event2 = Timer.new(0.seconds)
56+
event3 = Timer.new(1.minute)
4457

4558
# empty
4659
called = 0
4760
timers.dequeue_ready { called += 1 }
4861
called.should eq(0)
4962

5063
# add events in non chronological order
51-
timers = Crystal::EventLoop::Polling::Timers.new
64+
timers = Crystal::EventLoop::Timers(Timer).new
5265
timers.add(pointerof(event1))
5366
timers.add(pointerof(event3))
5467
timers.add(pointerof(event2))
5568

56-
events = [] of Crystal::EventLoop::Polling::Event*
69+
events = [] of Timer*
5770
timers.dequeue_ready { |event| events << event }
5871

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

6679
it "#add" do
67-
timers = Crystal::EventLoop::Polling::Timers.new
80+
timers = Crystal::EventLoop::Timers(Timer).new
6881

69-
event0 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current)
70-
event1 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 0.seconds)
71-
event2 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 2.minutes)
72-
event3 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 1.minute)
82+
event0 = Timer.new
83+
event1 = Timer.new(0.seconds)
84+
event2 = Timer.new(2.minutes)
85+
event3 = Timer.new(1.minute)
7386

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

8396
it "#delete" do
84-
event1 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 0.seconds)
85-
event2 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 0.seconds)
86-
event3 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 1.minute)
87-
event4 = Crystal::EventLoop::Polling::Event.new(:sleep, Fiber.current, timeout: 4.minutes)
97+
event1 = Timer.new(0.seconds)
98+
event2 = Timer.new(0.seconds)
99+
event3 = Timer.new(1.minute)
100+
event4 = Timer.new(4.minutes)
88101

89102
# add events in non chronological order
90-
timers = Crystal::EventLoop::Polling::Timers.new
103+
timers = Crystal::EventLoop::Timers(Timer).new
91104
timers.add(pointerof(event1))
92105
timers.add(pointerof(event3))
93106
timers.add(pointerof(event2))

0 commit comments

Comments
 (0)