From 28461012a2ea6b1e91ce8da57152a1d76c5bc8b1 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sat, 2 Mar 2024 16:01:13 +1300 Subject: [PATCH] Add idler wip.Add `Scheduler#load` and `Async::Idler` for scheduling tasks when idle. - `Async::Idler` introduces a `maximum_load` and functions like a semaphore, in that it will schedule tasks until the maximum load is reached. --- async.gemspec | 2 +- examples/load/test.rb | 27 +++++++++++++++++++++++++++ lib/async/idler.rb | 39 +++++++++++++++++++++++++++++++++++++++ lib/async/scheduler.rb | 35 +++++++++++++++++++++++++++++++++++ test.rb | 0 5 files changed, 102 insertions(+), 1 deletion(-) create mode 100755 examples/load/test.rb create mode 100644 lib/async/idler.rb create mode 100755 test.rb diff --git a/async.gemspec b/async.gemspec index ca7f3515..101f68f1 100644 --- a/async.gemspec +++ b/async.gemspec @@ -26,6 +26,6 @@ Gem::Specification.new do |spec| spec.add_dependency "console", "~> 1.10" spec.add_dependency "fiber-annotation" - spec.add_dependency "io-event", "~> 1.1" + spec.add_dependency "io-event", "~> 1.5" spec.add_dependency "timers", "~> 4.1" end diff --git a/examples/load/test.rb b/examples/load/test.rb new file mode 100755 index 00000000..81efadf7 --- /dev/null +++ b/examples/load/test.rb @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby + +require_relative '../../lib/async' +require_relative '../../lib/async/idler' + +Async do + idler = Async::Idler.new(0.8) + + Async do + while true + idler.async do + puts "Starting load..." + while true + sleep 0.1 + end + end + end + end + + scheduler = Fiber.scheduler + while true + load = scheduler.load + + puts "Load: #{load}..." + sleep 1.0 + end +end diff --git a/lib/async/idler.rb b/lib/async/idler.rb new file mode 100644 index 00000000..054100e0 --- /dev/null +++ b/lib/async/idler.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + +module Async + class Idler + def initialize(maximum_load = 0.9, backoff: 0.001, parent: nil) + @maximum_load = maximum_load + @backoff = backoff + @parent = parent + + @waiting = 0 + end + + def async(*arguments, parent: (@parent or Task.current), **options, &block) + wait + + parent.async(*arguments, **options, &block) + end + + def wait + scheduler = Fiber.scheduler + + if @waiting > 0 + scheduler.yield + @waiting = 0 + else + @waiting += 1 + end + + while scheduler.load > @maximum_load + sleep(@backoff) + end + ensure + @waiting = 0 + end + end +end diff --git a/lib/async/scheduler.rb b/lib/async/scheduler.rb index 63e1245c..3b2703f1 100644 --- a/lib/async/scheduler.rb +++ b/lib/async/scheduler.rb @@ -37,9 +37,33 @@ def initialize(parent = nil, selector: nil) @blocked = 0 + @busy_time = 0.0 + @idle_time = 0.0 + @timers = ::Timers::Group.new end + # Compute the scheduler load according to the busy and idle times that are updated by the run loop. + # @returns [Float] The load of the scheduler. 0.0 means no load, 1.0 means fully loaded or over-loaded. + def load + total_time = @busy_time + @idle_time + + # If the total time is zero, then the load is zero: + return 0.0 if total_time.zero? + + # We normalize to a 1 second window: + if total_time > 1.0 + ratio = 1.0 / total_time + @busy_time *= ratio + @idle_time *= ratio + + # We don't need to divide here as we've already normalised it to a 1s window: + return @busy_time + else + return @busy_time / total_time + end + end + def scheduler_close # If the execution context (thread) was handling an exception, we want to exit as quickly as possible: unless $! @@ -267,6 +291,8 @@ def run_once(timeout = nil) # @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite. # @returns [Boolean] Whether there is more work to do. private def run_once!(timeout = 0) + start_time = Async::Clock.now + interval = @timers.wait_interval # If there is no interval to wait (thus no timers), and no tasks, we could be done: @@ -288,6 +314,15 @@ def run_once(timeout = nil) @timers.fire + # Compute load: + end_time = Async::Clock.now + total_duration = end_time - start_time + idle_duration = @selector.idle_duration + busy_duration = total_duration - idle_duration + + @busy_time += busy_duration + @idle_time += idle_duration + # The reactor still has work to do: return true end diff --git a/test.rb b/test.rb new file mode 100755 index 00000000..e69de29b