lib/io/event/debug/selector.rb
# 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