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,272 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teek
|
|
4
|
+
# Thread-based background work for Teek applications.
|
|
5
|
+
# Always available, works on all Ruby versions.
|
|
6
|
+
#
|
|
7
|
+
# Best for I/O-bound work (network, file downloads, database queries) where the
|
|
8
|
+
# worker releases the GVL during blocking calls, allowing real concurrency with
|
|
9
|
+
# the UI thread. For CPU-bound work, the GVL serializes execution and thread
|
|
10
|
+
# overhead makes this slower than synchronous mode. Use :ractor for CPU-bound
|
|
11
|
+
# parallelism (Ruby 4.x+).
|
|
12
|
+
module BackgroundThread
|
|
13
|
+
|
|
14
|
+
# High-level API for background work with messaging support.
|
|
15
|
+
#
|
|
16
|
+
# Example:
|
|
17
|
+
# task = Teek::BackgroundThread::BackgroundWork.new(app, data) do |t|
|
|
18
|
+
# data.each do |item|
|
|
19
|
+
# break if t.check_message == :stop
|
|
20
|
+
# t.yield(process(item))
|
|
21
|
+
# end
|
|
22
|
+
# end.on_progress { |r| update_ui(r) }
|
|
23
|
+
# .on_done { puts "Done!" }
|
|
24
|
+
#
|
|
25
|
+
# task.send_message(:pause)
|
|
26
|
+
# task.send_message(:resume)
|
|
27
|
+
# task.stop
|
|
28
|
+
#
|
|
29
|
+
# Poll interval when paused (slower to save CPU)
|
|
30
|
+
PAUSED_POLL_MS = 10000
|
|
31
|
+
|
|
32
|
+
class BackgroundWork
|
|
33
|
+
def initialize(app, data, worker: nil, &block)
|
|
34
|
+
# Thread mode supports both block and worker class for API consistency
|
|
35
|
+
@app = app
|
|
36
|
+
@data = data
|
|
37
|
+
@work_block = block || (worker && proc { |t, d| worker.new.call(t, d) })
|
|
38
|
+
@callbacks = { progress: nil, done: nil, message: nil }
|
|
39
|
+
@started = false
|
|
40
|
+
@done = false
|
|
41
|
+
@paused = false
|
|
42
|
+
|
|
43
|
+
# Communication channels
|
|
44
|
+
@output_queue = Thread::Queue.new # Worker -> Main
|
|
45
|
+
@message_queue = Thread::Queue.new # Main -> Worker
|
|
46
|
+
@worker_thread = nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def on_progress(&block)
|
|
50
|
+
@callbacks[:progress] = block
|
|
51
|
+
maybe_start
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def on_done(&block)
|
|
56
|
+
@callbacks[:done] = block
|
|
57
|
+
maybe_start
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Called when worker sends a non-result message back
|
|
62
|
+
def on_message(&block)
|
|
63
|
+
@callbacks[:message] = block
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Send a message to the worker (pause, resume, stop, or custom)
|
|
68
|
+
def send_message(msg)
|
|
69
|
+
@message_queue << msg
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Convenience methods
|
|
74
|
+
def pause
|
|
75
|
+
@paused = true
|
|
76
|
+
send_message(:pause)
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def resume
|
|
81
|
+
@paused = false
|
|
82
|
+
send_message(:resume)
|
|
83
|
+
# Restart polling (was stopped when paused)
|
|
84
|
+
@app.after(0, &@poll_proc) if @poll_proc && !@done
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def stop
|
|
89
|
+
send_message(:stop)
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def close
|
|
94
|
+
@done = true
|
|
95
|
+
@worker_thread&.kill
|
|
96
|
+
self
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def done?
|
|
100
|
+
@done
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def paused?
|
|
104
|
+
@paused
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def start
|
|
108
|
+
return self if @started
|
|
109
|
+
@started = true
|
|
110
|
+
|
|
111
|
+
@worker_thread = Thread.new do
|
|
112
|
+
Thread.current[:tk_in_background_work] = true
|
|
113
|
+
task = TaskContext.new(@output_queue, @message_queue)
|
|
114
|
+
begin
|
|
115
|
+
@work_block.call(task, @data)
|
|
116
|
+
@output_queue << [:done]
|
|
117
|
+
rescue StopIteration
|
|
118
|
+
@output_queue << [:done]
|
|
119
|
+
rescue => e
|
|
120
|
+
@output_queue << [:error, "#{e.class}: #{e.message}\n#{e.backtrace.first(3).join("\n")}"]
|
|
121
|
+
@output_queue << [:done]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
start_polling
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def maybe_start
|
|
132
|
+
start unless @started
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def start_polling
|
|
136
|
+
@dropped_count = 0
|
|
137
|
+
@choke_warned = false
|
|
138
|
+
|
|
139
|
+
@poll_proc = proc do
|
|
140
|
+
next if @done
|
|
141
|
+
|
|
142
|
+
drop_intermediate = Teek::BackgroundWork.drop_intermediate
|
|
143
|
+
# Drain queue. If drop_intermediate, only use LATEST progress value.
|
|
144
|
+
# This prevents UI choking when worker yields faster than UI polls.
|
|
145
|
+
last_progress = nil
|
|
146
|
+
results_this_poll = 0
|
|
147
|
+
until @output_queue.empty?
|
|
148
|
+
msg = @output_queue.pop(true)
|
|
149
|
+
type, value = msg
|
|
150
|
+
case type
|
|
151
|
+
when :done
|
|
152
|
+
@done = true
|
|
153
|
+
# Call progress with final value before done callback
|
|
154
|
+
@callbacks[:progress]&.call(last_progress) if last_progress
|
|
155
|
+
last_progress = nil # Prevent duplicate call after loop
|
|
156
|
+
warn_if_choked
|
|
157
|
+
@callbacks[:done]&.call
|
|
158
|
+
break
|
|
159
|
+
when :result
|
|
160
|
+
results_this_poll += 1
|
|
161
|
+
if drop_intermediate
|
|
162
|
+
last_progress = value # Keep only latest
|
|
163
|
+
else
|
|
164
|
+
@callbacks[:progress]&.call(value) # Call for every value
|
|
165
|
+
end
|
|
166
|
+
when :message
|
|
167
|
+
@callbacks[:message]&.call(value)
|
|
168
|
+
when :error
|
|
169
|
+
warn "[Thread] Background work error: #{value}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Track dropped messages (all but the last one we processed)
|
|
174
|
+
if drop_intermediate && results_this_poll > 1
|
|
175
|
+
dropped = results_this_poll - 1
|
|
176
|
+
@dropped_count += dropped
|
|
177
|
+
warn_choke_start(dropped) unless @choke_warned
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Call progress callback once with latest value (only if dropping)
|
|
181
|
+
@callbacks[:progress]&.call(last_progress) if drop_intermediate && last_progress && !@done
|
|
182
|
+
|
|
183
|
+
unless @done || @paused
|
|
184
|
+
@app.after(Teek::BackgroundWork.poll_ms, &@poll_proc)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
@app.after(0, &@poll_proc)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def warn_choke_start(dropped)
|
|
192
|
+
@choke_warned = true
|
|
193
|
+
warn "[Teek::BackgroundWork] UI choking: worker yielding faster than UI can poll. " \
|
|
194
|
+
"#{dropped} progress values dropped this cycle. " \
|
|
195
|
+
"Consider yielding less frequently or increasing Tk.background_work_poll_ms."
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def warn_if_choked
|
|
199
|
+
return unless @dropped_count > 0
|
|
200
|
+
warn "[Teek::BackgroundWork] Total #{@dropped_count} progress values dropped during task. " \
|
|
201
|
+
"Only latest values were shown to UI."
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Context passed to the work block
|
|
205
|
+
class TaskContext
|
|
206
|
+
def initialize(output_queue, message_queue)
|
|
207
|
+
@output_queue = output_queue
|
|
208
|
+
@message_queue = message_queue
|
|
209
|
+
@paused = false
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Yield a result to the main thread.
|
|
213
|
+
# Calls Thread.pass to give main thread a chance to process events.
|
|
214
|
+
def yield(value)
|
|
215
|
+
@output_queue << [:result, value]
|
|
216
|
+
Thread.pass
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Non-blocking check for messages from main thread.
|
|
220
|
+
# Returns the message or nil if none.
|
|
221
|
+
def check_message
|
|
222
|
+
return nil if @message_queue.empty?
|
|
223
|
+
msg = @message_queue.pop(true)
|
|
224
|
+
handle_control_message(msg)
|
|
225
|
+
msg
|
|
226
|
+
rescue ThreadError
|
|
227
|
+
nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Blocking wait for next message
|
|
231
|
+
def wait_message
|
|
232
|
+
msg = @message_queue.pop
|
|
233
|
+
handle_control_message(msg)
|
|
234
|
+
msg
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Send a message back to main thread (not a result)
|
|
238
|
+
def send_message(msg)
|
|
239
|
+
@output_queue << [:message, msg]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Check pause state, blocking if paused
|
|
243
|
+
def check_pause
|
|
244
|
+
# First drain any pending messages (non-blocking)
|
|
245
|
+
until @message_queue.empty?
|
|
246
|
+
msg = @message_queue.pop(true)
|
|
247
|
+
handle_control_message(msg)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Then block while paused
|
|
251
|
+
while @paused
|
|
252
|
+
msg = @message_queue.pop # Blocking wait
|
|
253
|
+
handle_control_message(msg)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
private
|
|
258
|
+
|
|
259
|
+
def handle_control_message(msg)
|
|
260
|
+
case msg
|
|
261
|
+
when :pause
|
|
262
|
+
@paused = true
|
|
263
|
+
when :resume
|
|
264
|
+
@paused = false
|
|
265
|
+
when :stop
|
|
266
|
+
raise StopIteration
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|