teek 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +139 -0
- data/Rakefile +316 -0
- data/ext/teek/extconf.rb +79 -0
- data/ext/teek/stubs.h +33 -0
- data/ext/teek/tcl9compat.h +211 -0
- data/ext/teek/tcltkbridge.c +1597 -0
- data/ext/teek/tcltkbridge.h +42 -0
- data/ext/teek/tkfont.c +218 -0
- data/ext/teek/tkphoto.c +477 -0
- data/ext/teek/tkwin.c +144 -0
- data/lib/teek/background_none.rb +158 -0
- data/lib/teek/background_ractor4x.rb +410 -0
- data/lib/teek/background_thread.rb +272 -0
- data/lib/teek/debugger.rb +742 -0
- data/lib/teek/demo_support.rb +150 -0
- data/lib/teek/ractor_support.rb +246 -0
- data/lib/teek/version.rb +5 -0
- data/lib/teek.rb +540 -0
- data/sample/calculator.rb +260 -0
- data/sample/debug_demo.rb +45 -0
- data/sample/goldberg.rb +1803 -0
- data/sample/goldberg_helpers.rb +170 -0
- data/sample/minesweeper/assets/MINESWEEPER_0.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_1.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_2.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_3.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_4.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_5.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_6.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_7.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_8.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_F.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_M.png +0 -0
- data/sample/minesweeper/assets/MINESWEEPER_X.png +0 -0
- data/sample/minesweeper/minesweeper.rb +452 -0
- data/sample/threading_demo.rb +499 -0
- data/teek.gemspec +32 -0
- metadata +179 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
# Synchronous "background" work - runs on main thread, blocks UI.
|
|
5
|
+
#
|
|
6
|
+
# FOR TESTING AND DEMONSTRATION ONLY.
|
|
7
|
+
#
|
|
8
|
+
# This mode exists to show what happens WITHOUT background processing:
|
|
9
|
+
# the UI freezes during work. Use it in demos to contrast with :thread
|
|
10
|
+
# and :ractor modes which keep the UI responsive.
|
|
11
|
+
#
|
|
12
|
+
# Not registered by default. To use:
|
|
13
|
+
# require 'teek/background_none'
|
|
14
|
+
# Teek::BackgroundWork.register_background_mode :none, Teek::BackgroundNone::BackgroundWork
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
module BackgroundNone
|
|
18
|
+
class BackgroundWork
|
|
19
|
+
def initialize(app, data, worker: nil, &block)
|
|
20
|
+
@app = app
|
|
21
|
+
@data = data
|
|
22
|
+
@work_block = block || (worker && proc { |t, d| worker.new.call(t, d) })
|
|
23
|
+
@callbacks = { progress: nil, done: nil, message: nil }
|
|
24
|
+
@message_queue = []
|
|
25
|
+
@started = false
|
|
26
|
+
@done = false
|
|
27
|
+
@paused = false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_progress(&block)
|
|
31
|
+
@callbacks[:progress] = block
|
|
32
|
+
maybe_start
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def on_done(&block)
|
|
37
|
+
@callbacks[:done] = block
|
|
38
|
+
maybe_start
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def on_message(&block)
|
|
43
|
+
@callbacks[:message] = block
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def send_message(msg)
|
|
48
|
+
@message_queue << msg
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def pause
|
|
53
|
+
@paused = true
|
|
54
|
+
send_message(:pause)
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def resume
|
|
59
|
+
@paused = false
|
|
60
|
+
send_message(:resume)
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def stop
|
|
65
|
+
send_message(:stop)
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def close
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def done?
|
|
74
|
+
@done
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def paused?
|
|
78
|
+
@paused
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def start
|
|
82
|
+
maybe_start
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def maybe_start
|
|
89
|
+
return if @started
|
|
90
|
+
@started = true
|
|
91
|
+
@app.after(0) { do_work }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def do_work
|
|
95
|
+
task = TaskContext.new(@app, @callbacks, @message_queue)
|
|
96
|
+
begin
|
|
97
|
+
@work_block.call(task, @data)
|
|
98
|
+
rescue StopIteration
|
|
99
|
+
# Worker requested stop
|
|
100
|
+
rescue => e
|
|
101
|
+
warn "[None] Background work error: #{e.class}: #{e.message}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
@done = true
|
|
105
|
+
@callbacks[:done]&.call
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Synchronous task context - callbacks fire immediately
|
|
109
|
+
class TaskContext
|
|
110
|
+
def initialize(app, callbacks, message_queue)
|
|
111
|
+
@app = app
|
|
112
|
+
@callbacks = callbacks
|
|
113
|
+
@message_queue = message_queue
|
|
114
|
+
@paused = false
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def yield(value)
|
|
118
|
+
@callbacks[:progress]&.call(value)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def check_message
|
|
122
|
+
msg = @message_queue.shift
|
|
123
|
+
handle_control_message(msg) if msg
|
|
124
|
+
msg
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def wait_message
|
|
128
|
+
check_message
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def send_message(msg)
|
|
132
|
+
@callbacks[:message]&.call(msg)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def check_pause
|
|
136
|
+
while @paused
|
|
137
|
+
@app.update
|
|
138
|
+
msg = check_message
|
|
139
|
+
break unless @paused
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def handle_control_message(msg)
|
|
146
|
+
case msg
|
|
147
|
+
when :pause
|
|
148
|
+
@paused = true
|
|
149
|
+
when :resume
|
|
150
|
+
@paused = false
|
|
151
|
+
when :stop
|
|
152
|
+
raise StopIteration
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
|
|
5
|
+
# Ruby 4.x Ractor-based background work for Teek applications.
|
|
6
|
+
# Uses Ractor::Port for streaming and Ractor.shareable_proc for blocks.
|
|
7
|
+
# Uses thread-inside-ractor pattern for non-blocking message handling.
|
|
8
|
+
module BackgroundRactor4x
|
|
9
|
+
# Poll interval when paused (slower to save CPU)
|
|
10
|
+
PAUSED_POLL_MS = 500
|
|
11
|
+
|
|
12
|
+
# Why Ractor mode requires Ruby 4.x:
|
|
13
|
+
#
|
|
14
|
+
# Ruby 3.x Ractor support was attempted but abandoned due to fundamental issues:
|
|
15
|
+
#
|
|
16
|
+
# | Aspect | 3.x Problem | 4.x Solution |
|
|
17
|
+
# |-----------------------------|------------------------------|-------------------------------|
|
|
18
|
+
# | Output mechanism | Ractor.yield BLOCKS caller | Port.send is non-blocking |
|
|
19
|
+
# | Block support | Cannot pass blocks to Ractor | Ractor.shareable_proc works |
|
|
20
|
+
# | close_incoming after yield | Bug: doesn't wake threads | Works correctly |
|
|
21
|
+
# | Orphaned threads on exit | Hangs in rb_ractor_terminate | Exits cleanly |
|
|
22
|
+
# | Non-blocking receive | No API exists | Ractor::Port with select |
|
|
23
|
+
#
|
|
24
|
+
# What we tried on Ruby 3.x (all failed):
|
|
25
|
+
#
|
|
26
|
+
# 1. Yielder thread pattern: Separate thread does blocking Ractor.yield while
|
|
27
|
+
# worker pushes to a Queue. Works but adds complexity and has shutdown bugs.
|
|
28
|
+
#
|
|
29
|
+
# 2. Timeout-based polling for messages: Create thread, call Ractor.receive,
|
|
30
|
+
# join with timeout, kill thread. Very expensive (~10ms per check) and
|
|
31
|
+
# causes severe performance degradation.
|
|
32
|
+
#
|
|
33
|
+
# 3. Long-lived receiver thread: One thread continuously receives and pushes
|
|
34
|
+
# to Queue. UI hangs due to thread interaction issues.
|
|
35
|
+
#
|
|
36
|
+
# 4. IO.pipe for signaling: Considered but IO objects aren't Ractor-shareable.
|
|
37
|
+
#
|
|
38
|
+
# The fundamental problem is Ruby 3.x has no non-blocking Ractor.receive,
|
|
39
|
+
# and all workarounds either kill performance or cause hangs/crashes.
|
|
40
|
+
#
|
|
41
|
+
# On Ruby 3.x, use :thread mode instead - it works reliably with the GVL.
|
|
42
|
+
# This 4.x implementation is simpler because Ruby 4.x Ractors just work.
|
|
43
|
+
|
|
44
|
+
# Ractor-based background work using Ruby 4.x Ractor::Port for streaming
|
|
45
|
+
# and Ractor.shareable_proc for blocks.
|
|
46
|
+
#
|
|
47
|
+
# @example Block form
|
|
48
|
+
# work = BackgroundWork.new(app, urls) do |task, data|
|
|
49
|
+
# data.each { |url| task.yield(fetch(url)) }
|
|
50
|
+
# end
|
|
51
|
+
# work.on_progress { |r| update_ui(r) }.on_done { puts "Done!" }
|
|
52
|
+
#
|
|
53
|
+
# @example Worker class form
|
|
54
|
+
# work = BackgroundWork.new(app, data, worker: MyWorker)
|
|
55
|
+
# work.on_progress { |r| update_ui(r) }
|
|
56
|
+
class BackgroundWork
|
|
57
|
+
# @param app [Teek::App] the application instance (for +after+ scheduling)
|
|
58
|
+
# @param data [Object] data passed to the worker block/class
|
|
59
|
+
# @param worker [Class, nil] optional worker class (must respond to +#call(task, data)+)
|
|
60
|
+
# @yield [task, data] block executed inside a Ractor
|
|
61
|
+
# @yieldparam task [TaskContext] context for yielding results and checking messages
|
|
62
|
+
# @yieldparam data [Object] the data passed to the constructor
|
|
63
|
+
def initialize(app, data, worker: nil, &block)
|
|
64
|
+
@app = app
|
|
65
|
+
@data = data
|
|
66
|
+
@work_block = block || (worker && proc { |t, d| worker.new.call(t, d) })
|
|
67
|
+
@callbacks = { progress: nil, done: nil, message: nil }
|
|
68
|
+
@started = false
|
|
69
|
+
@done = false
|
|
70
|
+
@paused = false
|
|
71
|
+
|
|
72
|
+
# Communication
|
|
73
|
+
@output_queue = Thread::Queue.new
|
|
74
|
+
@control_port = nil # Set by worker, received back
|
|
75
|
+
@pending_messages = [] # Queued until control_port ready
|
|
76
|
+
@worker_ractor = nil
|
|
77
|
+
@bridge_thread = nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Register a callback for progress updates from the worker.
|
|
81
|
+
# Auto-starts the task if not already started.
|
|
82
|
+
# @yield [value] called on the main thread each time the worker yields a result
|
|
83
|
+
# @return [self]
|
|
84
|
+
def on_progress(&block)
|
|
85
|
+
@callbacks[:progress] = block
|
|
86
|
+
maybe_start
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Register a callback for when the worker finishes.
|
|
91
|
+
# Auto-starts the task if not already started.
|
|
92
|
+
# @yield called on the main thread when the worker completes
|
|
93
|
+
# @return [self]
|
|
94
|
+
def on_done(&block)
|
|
95
|
+
@callbacks[:done] = block
|
|
96
|
+
maybe_start
|
|
97
|
+
self
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Register a callback for custom messages sent by the worker via
|
|
101
|
+
# {TaskContext#send_message}.
|
|
102
|
+
# @yield [msg] called on the main thread with the message
|
|
103
|
+
# @return [self]
|
|
104
|
+
def on_message(&block)
|
|
105
|
+
@callbacks[:message] = block
|
|
106
|
+
self
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Send a message to the worker. The worker can receive it via
|
|
110
|
+
# {TaskContext#check_message} or {TaskContext#wait_message}.
|
|
111
|
+
# Messages are queued if the worker's control port isn't ready yet.
|
|
112
|
+
# @param msg [Object] any Ractor-shareable value
|
|
113
|
+
# @return [self]
|
|
114
|
+
def send_message(msg)
|
|
115
|
+
if @control_port
|
|
116
|
+
begin
|
|
117
|
+
@control_port.send(msg)
|
|
118
|
+
rescue Ractor::ClosedError
|
|
119
|
+
# Port already closed, task is done - ignore
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
@pending_messages << msg
|
|
123
|
+
end
|
|
124
|
+
self
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Pause the worker. The worker will block on the next {TaskContext#yield}
|
|
128
|
+
# or {TaskContext#check_pause} until {#resume} is called.
|
|
129
|
+
# @return [self]
|
|
130
|
+
def pause
|
|
131
|
+
@paused = true
|
|
132
|
+
send_message(:pause)
|
|
133
|
+
self
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Resume a paused worker.
|
|
137
|
+
# @return [self]
|
|
138
|
+
def resume
|
|
139
|
+
@paused = false
|
|
140
|
+
send_message(:resume)
|
|
141
|
+
self
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Request the worker to stop. Raises +StopIteration+ inside the worker
|
|
145
|
+
# on the next {TaskContext#check_message} or {TaskContext#yield}.
|
|
146
|
+
# @return [self]
|
|
147
|
+
def stop
|
|
148
|
+
send_message(:stop)
|
|
149
|
+
self
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Force-close the Ractor and all associated resources.
|
|
153
|
+
# @return [self]
|
|
154
|
+
def close
|
|
155
|
+
@done = true
|
|
156
|
+
@control_port = nil # Prevent further message sends
|
|
157
|
+
begin
|
|
158
|
+
@worker_ractor&.close_incoming
|
|
159
|
+
@worker_ractor&.close_outgoing
|
|
160
|
+
rescue Ractor::ClosedError
|
|
161
|
+
# Already closed
|
|
162
|
+
end
|
|
163
|
+
self
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Explicitly start the background work. Called automatically by
|
|
167
|
+
# {#on_progress} and {#on_done}; only needed when using {#on_message}
|
|
168
|
+
# alone.
|
|
169
|
+
# @return [self]
|
|
170
|
+
def start
|
|
171
|
+
return self if @started
|
|
172
|
+
@started = true
|
|
173
|
+
|
|
174
|
+
# Wrap in isolated proc for Ractor sharing. The block can only access
|
|
175
|
+
# its parameters (task, data), not outer-scope variables.
|
|
176
|
+
shareable_block = Ractor.shareable_proc(&@work_block)
|
|
177
|
+
|
|
178
|
+
start_ractor(shareable_block)
|
|
179
|
+
start_polling
|
|
180
|
+
self
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# @return [Boolean] whether the worker has finished
|
|
184
|
+
def done?
|
|
185
|
+
@done
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# @return [Boolean] whether the worker is paused
|
|
189
|
+
def paused?
|
|
190
|
+
@paused
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
def maybe_start
|
|
196
|
+
start unless @started
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def start_ractor(shareable_block)
|
|
200
|
+
data = @data
|
|
201
|
+
output_port = Ractor::Port.new
|
|
202
|
+
|
|
203
|
+
@worker_ractor = Ractor.new(data, output_port, shareable_block) do |d, out, blk|
|
|
204
|
+
# Worker creates its own control port for receiving messages
|
|
205
|
+
control_port = Ractor::Port.new
|
|
206
|
+
msg_queue = Thread::Queue.new
|
|
207
|
+
|
|
208
|
+
# Send control port back to main thread
|
|
209
|
+
out.send([:control_port, control_port])
|
|
210
|
+
|
|
211
|
+
# Background thread receives from control port, forwards to queue
|
|
212
|
+
Thread.new do
|
|
213
|
+
loop do
|
|
214
|
+
begin
|
|
215
|
+
msg = control_port.receive
|
|
216
|
+
msg_queue << msg
|
|
217
|
+
break if msg == :stop
|
|
218
|
+
rescue Ractor::ClosedError
|
|
219
|
+
break
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
Thread.current[:tk_in_background_work] = true
|
|
225
|
+
task = TaskContext.new(out, msg_queue)
|
|
226
|
+
begin
|
|
227
|
+
blk.call(task, d)
|
|
228
|
+
out.send([:done])
|
|
229
|
+
rescue StopIteration
|
|
230
|
+
out.send([:done])
|
|
231
|
+
rescue => e
|
|
232
|
+
out.send([:error, "#{e.class}: #{e.message}\n#{e.backtrace.first(3).join("\n")}"])
|
|
233
|
+
out.send([:done])
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Bridge thread: Port.receive -> Queue
|
|
238
|
+
@bridge_thread = Thread.new do
|
|
239
|
+
loop do
|
|
240
|
+
begin
|
|
241
|
+
result = output_port.receive
|
|
242
|
+
if result.is_a?(Array) && result[0] == :control_port
|
|
243
|
+
@control_port = result[1]
|
|
244
|
+
@pending_messages.each { |m| @control_port.send(m) }
|
|
245
|
+
@pending_messages.clear
|
|
246
|
+
else
|
|
247
|
+
@output_queue << result
|
|
248
|
+
break if result[0] == :done
|
|
249
|
+
end
|
|
250
|
+
rescue Ractor::ClosedError
|
|
251
|
+
@output_queue << [:done]
|
|
252
|
+
break
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def start_polling
|
|
259
|
+
@dropped_count = 0
|
|
260
|
+
@choke_warned = false
|
|
261
|
+
|
|
262
|
+
poll = proc do
|
|
263
|
+
next if @done
|
|
264
|
+
|
|
265
|
+
drop_intermediate = Teek::BackgroundWork.drop_intermediate
|
|
266
|
+
# Drain queue. If drop_intermediate, only use LATEST progress value.
|
|
267
|
+
# This prevents UI choking when worker yields faster than UI polls.
|
|
268
|
+
last_progress = nil
|
|
269
|
+
results_this_poll = 0
|
|
270
|
+
until @output_queue.empty?
|
|
271
|
+
msg = @output_queue.pop(true)
|
|
272
|
+
type, value = msg
|
|
273
|
+
case type
|
|
274
|
+
when :done
|
|
275
|
+
@done = true
|
|
276
|
+
@control_port = nil # Clear to prevent send to closed port
|
|
277
|
+
# Call progress with final value before done callback
|
|
278
|
+
@callbacks[:progress]&.call(last_progress) if last_progress
|
|
279
|
+
last_progress = nil # Prevent duplicate call after loop
|
|
280
|
+
warn_if_choked
|
|
281
|
+
@callbacks[:done]&.call
|
|
282
|
+
break
|
|
283
|
+
when :result
|
|
284
|
+
results_this_poll += 1
|
|
285
|
+
if drop_intermediate
|
|
286
|
+
last_progress = value # Keep only latest
|
|
287
|
+
else
|
|
288
|
+
@callbacks[:progress]&.call(value) # Call for every value
|
|
289
|
+
end
|
|
290
|
+
when :message
|
|
291
|
+
@callbacks[:message]&.call(value)
|
|
292
|
+
when :error
|
|
293
|
+
if Teek::BackgroundWork.abort_on_error
|
|
294
|
+
raise RuntimeError, "[Ractor] Background work error: #{value}"
|
|
295
|
+
else
|
|
296
|
+
warn "[Ractor] Background work error: #{value}"
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Track dropped messages (all but the last one we processed)
|
|
302
|
+
if drop_intermediate && results_this_poll > 1
|
|
303
|
+
dropped = results_this_poll - 1
|
|
304
|
+
@dropped_count += dropped
|
|
305
|
+
warn_choke_start(dropped) unless @choke_warned
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Call progress callback once with latest value (only if dropping)
|
|
309
|
+
@callbacks[:progress]&.call(last_progress) if drop_intermediate && last_progress && !@done
|
|
310
|
+
|
|
311
|
+
unless @done
|
|
312
|
+
# Use slower polling when paused to save CPU
|
|
313
|
+
interval = @paused ? PAUSED_POLL_MS : Teek::BackgroundWork.poll_ms
|
|
314
|
+
@app.after(interval, &poll)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
@app.after(0, &poll)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def warn_choke_start(dropped)
|
|
322
|
+
@choke_warned = true
|
|
323
|
+
warn "[Teek::BackgroundWork] UI choking: worker yielding faster than UI can poll. " \
|
|
324
|
+
"#{dropped} progress values dropped this cycle. " \
|
|
325
|
+
"Consider yielding less frequently or increasing Tk.background_work_poll_ms."
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def warn_if_choked
|
|
329
|
+
return unless @dropped_count > 0
|
|
330
|
+
warn "[Teek::BackgroundWork] Total #{@dropped_count} progress values dropped during task. " \
|
|
331
|
+
"Only latest values were shown to UI."
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Context object passed to the worker block inside the Ractor.
|
|
335
|
+
# Provides methods for yielding results, sending/receiving messages,
|
|
336
|
+
# and responding to pause/stop signals.
|
|
337
|
+
class TaskContext
|
|
338
|
+
# @api private
|
|
339
|
+
def initialize(output_port, msg_queue)
|
|
340
|
+
@output_port = output_port
|
|
341
|
+
@msg_queue = msg_queue
|
|
342
|
+
@paused = false
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Send a result to the main thread. Blocks while paused.
|
|
346
|
+
# The value arrives in the {BackgroundWork#on_progress} callback.
|
|
347
|
+
# @param value [Object] any Ractor-shareable value
|
|
348
|
+
def yield(value)
|
|
349
|
+
check_pause_loop
|
|
350
|
+
@output_port.send([:result, value])
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Non-blocking check for a message from the main thread.
|
|
354
|
+
# Handles built-in control messages (+:pause+, +:resume+, +:stop+)
|
|
355
|
+
# automatically; +:stop+ raises +StopIteration+.
|
|
356
|
+
# @return [Object, nil] the message, or +nil+ if none pending
|
|
357
|
+
def check_message
|
|
358
|
+
return nil if @msg_queue.empty?
|
|
359
|
+
msg = @msg_queue.pop(true)
|
|
360
|
+
handle_control_message(msg)
|
|
361
|
+
msg
|
|
362
|
+
rescue ThreadError
|
|
363
|
+
nil
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Blocking wait for the next message from the main thread.
|
|
367
|
+
# Handles control messages automatically.
|
|
368
|
+
# @return [Object] the message
|
|
369
|
+
def wait_message
|
|
370
|
+
msg = @msg_queue.pop
|
|
371
|
+
handle_control_message(msg)
|
|
372
|
+
msg
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Send a custom message back to the main thread.
|
|
376
|
+
# Arrives in the {BackgroundWork#on_message} callback.
|
|
377
|
+
# @param msg [Object] any Ractor-shareable value
|
|
378
|
+
def send_message(msg)
|
|
379
|
+
@output_port.send([:message, msg])
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Block while paused, returning immediately if not paused.
|
|
383
|
+
# Call this periodically in long-running loops to honor pause requests.
|
|
384
|
+
def check_pause
|
|
385
|
+
check_pause_loop
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
private
|
|
389
|
+
|
|
390
|
+
def handle_control_message(msg)
|
|
391
|
+
case msg
|
|
392
|
+
when :pause
|
|
393
|
+
@paused = true
|
|
394
|
+
when :resume
|
|
395
|
+
@paused = false
|
|
396
|
+
when :stop
|
|
397
|
+
raise StopIteration
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def check_pause_loop
|
|
402
|
+
while @paused
|
|
403
|
+
msg = @msg_queue.pop
|
|
404
|
+
handle_control_message(msg)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|