rufio 0.33.0 → 0.34.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.
@@ -1,9 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fiddle'
4
+ require 'fiddle/import'
5
+
3
6
  module Rufio
4
- # Zig拡張のラッパー
5
- # FFIを使わずに直接Rubyオブジェクトとして扱える
6
- module NativeScannerZigLoader
7
+ # Zig拡張の非同期FFI層
8
+ # Ruby側は「ハンドル(u64)だけ」を持つ
9
+ module NativeScannerZigFFI
10
+ extend Fiddle::Importer
11
+
7
12
  LIB_PATH = File.expand_path('native/rufio_zig.bundle', __dir__)
8
13
 
9
14
  @loaded = false
@@ -17,12 +22,24 @@ module Rufio
17
22
 
18
23
  if File.exist?(LIB_PATH)
19
24
  begin
20
- # .bundleファイルを直接ロード
21
- # 拡張子を外してrequireする
22
- lib_path_without_ext = LIB_PATH.sub(/\.bundle$/, '')
23
- require lib_path_without_ext
24
- @available = defined?(Rufio::NativeScannerZig)
25
- rescue LoadError => e
25
+ # 動的ライブラリをロード
26
+ dlload LIB_PATH
27
+
28
+ # ABI Boundary: Ruby ABI非依存のC関数(非同期版)
29
+ extern 'uint64_t core_async_create()'
30
+ extern 'int32_t core_async_scan(uint64_t, const char*)'
31
+ extern 'int32_t core_async_scan_fast(uint64_t, const char*, size_t)'
32
+ extern 'uint8_t core_async_get_state(uint64_t)'
33
+ extern 'void core_async_get_progress(uint64_t, void*, void*)'
34
+ extern 'void core_async_cancel(uint64_t)'
35
+ extern 'size_t core_async_get_count(uint64_t)'
36
+ extern 'size_t core_async_get_name(uint64_t, size_t, void*, size_t)'
37
+ extern 'int32_t core_async_get_attrs(uint64_t, size_t, void*, void*, void*, void*, void*)'
38
+ extern 'void core_async_destroy(uint64_t)'
39
+ extern 'char* core_async_version()'
40
+
41
+ @available = true
42
+ rescue StandardError => e
26
43
  warn "Failed to load zig extension: #{e.message}" if ENV['RUFIO_DEBUG']
27
44
  @available = false
28
45
  end
@@ -37,12 +54,150 @@ module Rufio
37
54
  load! unless @loaded
38
55
  @available
39
56
  end
57
+
58
+ # バージョン取得
59
+ def version
60
+ core_async_version.to_s
61
+ end
62
+ end
63
+ end
64
+
65
+ # 非同期スキャナークラス(ポーリングベース)
66
+ class NativeScannerZigCore
67
+ POLL_INTERVAL = 0.01 # 10ms
68
+
69
+ def initialize
70
+ @handle = NativeScannerZigFFI.core_async_create
71
+ raise StandardError, "Failed to create scanner" if @handle.zero?
72
+ end
73
+
74
+ # 非同期スキャン開始
75
+ def scan_async(path)
76
+ result = NativeScannerZigFFI.core_async_scan(@handle, path)
77
+ raise StandardError, "Failed to start scan" if result != 0
78
+ self
79
+ end
80
+
81
+ # 高速スキャン(エントリ数制限付き)
82
+ def scan_fast_async(path, max_entries)
83
+ result = NativeScannerZigFFI.core_async_scan_fast(@handle, path, max_entries)
84
+ raise StandardError, "Failed to start scan" if result != 0
85
+ self
86
+ end
87
+
88
+ # ポーリングして完了待ち
89
+ def wait(timeout: nil)
90
+ start_time = Time.now
91
+ loop do
92
+ state = get_state
93
+
94
+ case state
95
+ when :done
96
+ return get_results
97
+ when :failed
98
+ raise StandardError, "Scan failed"
99
+ when :cancelled
100
+ raise StandardError, "Scan cancelled"
101
+ end
102
+
103
+ if timeout && (Time.now - start_time) > timeout
104
+ raise StandardError, "Timeout"
105
+ end
106
+
107
+ sleep POLL_INTERVAL
108
+ end
109
+ end
110
+
111
+ # 進捗報告付きで完了待ち
112
+ def wait_with_progress(&block)
113
+ loop do
114
+ state = get_state
115
+ progress = get_progress
116
+
117
+ yield(progress[:current], progress[:total]) if block_given?
118
+
119
+ case state
120
+ when :done
121
+ return get_results
122
+ when :failed
123
+ raise StandardError, "Scan failed"
124
+ when :cancelled
125
+ raise StandardError, "Scan cancelled"
126
+ end
127
+
128
+ sleep POLL_INTERVAL
129
+ end
130
+ end
131
+
132
+ # 状態確認
133
+ def get_state
134
+ state_code = NativeScannerZigFFI.core_async_get_state(@handle)
135
+ [:idle, :scanning, :done, :cancelled, :failed][state_code] || :failed
136
+ end
137
+
138
+ # 進捗取得
139
+ def get_progress
140
+ current = Fiddle::Pointer.malloc(8)
141
+ total = Fiddle::Pointer.malloc(8)
142
+ NativeScannerZigFFI.core_async_get_progress(@handle, current, total)
143
+ {
144
+ current: current[0, 8].unpack1('Q'),
145
+ total: total[0, 8].unpack1('Q')
146
+ }
147
+ end
148
+
149
+ # キャンセル
150
+ def cancel
151
+ NativeScannerZigFFI.core_async_cancel(@handle)
152
+ end
153
+
154
+ # 結果取得(完了後)
155
+ def get_results
156
+ count = NativeScannerZigFFI.core_async_get_count(@handle)
157
+ entries = []
158
+ count.times { |i| entries << get_entry(i) }
159
+ entries
160
+ end
161
+
162
+ # スキャナーを明示的に破棄
163
+ def close
164
+ return if @handle.zero?
165
+
166
+ NativeScannerZigFFI.core_async_destroy(@handle)
167
+ @handle = 0
168
+ end
169
+
170
+ private
171
+
172
+ # 指定インデックスのエントリを取得
173
+ def get_entry(index)
174
+ # 名前を取得
175
+ name_buf = Fiddle::Pointer.malloc(256)
176
+ name_len = NativeScannerZigFFI.core_async_get_name(@handle, index, name_buf, 256)
177
+ name = name_buf[0, name_len].force_encoding('UTF-8')
178
+
179
+ # 属性を取得
180
+ is_dir = Fiddle::Pointer.malloc(1)
181
+ size = Fiddle::Pointer.malloc(8)
182
+ mtime = Fiddle::Pointer.malloc(8)
183
+ executable = Fiddle::Pointer.malloc(1)
184
+ hidden = Fiddle::Pointer.malloc(1)
185
+
186
+ NativeScannerZigFFI.core_async_get_attrs(@handle, index, is_dir, size, mtime, executable, hidden)
187
+
188
+ {
189
+ name: name,
190
+ is_dir: is_dir[0, 1].unpack1('C') != 0,
191
+ size: size[0, 8].unpack1('Q'),
192
+ mtime: mtime[0, 8].unpack1('q'),
193
+ executable: executable[0, 1].unpack1('C') != 0,
194
+ hidden: hidden[0, 1].unpack1('C') != 0
195
+ }
40
196
  end
41
197
  end
42
198
 
43
- # Zig拡張が利用可能な場合のみロード
44
- if NativeScannerZigLoader.load!
45
- # NativeScannerにzigモードを追加
199
+ # Zig拡張が利用可能な場合のみ、NativeScannerに統合
200
+ if NativeScannerZigFFI.load!
46
201
  class NativeScanner
47
202
  class << self
48
203
  # zigモードを追加
@@ -51,9 +206,9 @@ module Rufio
51
206
  def mode=(value)
52
207
  case value
53
208
  when 'zig'
54
- if NativeScannerZigLoader.available?
209
+ if NativeScannerZigFFI.available?
55
210
  @mode = 'zig'
56
- @current_library = nil # zig は FFI を使わない
211
+ @current_library = nil
57
212
  else
58
213
  @mode = 'ruby'
59
214
  @current_library = nil
@@ -63,44 +218,57 @@ module Rufio
63
218
  end
64
219
  end
65
220
 
66
- # zigスキャン
221
+ # zigスキャン(ポーリング方式)
67
222
  def scan_directory_with_zig(path)
68
223
  raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
69
224
 
70
- entries = NativeScannerZig.scan_directory(path)
71
-
72
- # 結果の形式を統一
73
- entries.map do |entry|
74
- {
75
- name: entry[:name],
76
- type: entry[:is_dir] ? 'directory' : 'file',
77
- size: entry[:size],
78
- mtime: entry[:mtime],
79
- mode: 0,
80
- executable: entry[:executable],
81
- hidden: entry[:hidden]
82
- }
225
+ # 非同期スキャナーを作成してスキャン
226
+ scanner = NativeScannerZigCore.new
227
+ begin
228
+ scanner.scan_async(path)
229
+ entries = scanner.wait(timeout: 60)
230
+
231
+ # 結果の形式を統一(type フィールドを追加)
232
+ entries.map do |entry|
233
+ {
234
+ name: entry[:name],
235
+ type: entry[:is_dir] ? 'directory' : 'file',
236
+ size: entry[:size],
237
+ mtime: entry[:mtime],
238
+ mode: 0,
239
+ executable: entry[:executable],
240
+ hidden: entry[:hidden]
241
+ }
242
+ end
243
+ ensure
244
+ scanner.close
83
245
  end
84
246
  rescue StandardError => e
85
247
  raise StandardError, "Zig scan failed: #{e.message}"
86
248
  end
87
249
 
88
- # zigで高速スキャン
250
+ # zigで高速スキャン(ポーリング方式)
89
251
  def scan_directory_fast_with_zig(path, max_entries)
90
252
  raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
91
253
 
92
- entries = NativeScannerZig.scan_directory_fast(path, max_entries)
93
-
94
- entries.map do |entry|
95
- {
96
- name: entry[:name],
97
- type: entry[:is_dir] ? 'directory' : 'file',
98
- size: entry[:size],
99
- mtime: entry[:mtime],
100
- mode: 0,
101
- executable: entry[:executable],
102
- hidden: entry[:hidden]
103
- }
254
+ scanner = NativeScannerZigCore.new
255
+ begin
256
+ scanner.scan_fast_async(path, max_entries)
257
+ entries = scanner.wait(timeout: 60)
258
+
259
+ entries.map do |entry|
260
+ {
261
+ name: entry[:name],
262
+ type: entry[:is_dir] ? 'directory' : 'file',
263
+ size: entry[:size],
264
+ mtime: entry[:mtime],
265
+ mode: 0,
266
+ executable: entry[:executable],
267
+ hidden: entry[:hidden]
268
+ }
269
+ end
270
+ ensure
271
+ scanner.close
104
272
  end
105
273
  rescue StandardError => e
106
274
  raise StandardError, "Zig fast scan failed: #{e.message}"
@@ -115,13 +283,6 @@ module Rufio
115
283
  case @mode
116
284
  when 'zig'
117
285
  scan_directory_with_zig(path)
118
- when 'magnus'
119
- # magnusモードの場合は、native_scanner_magnus.rbで定義されたメソッドを呼ぶ
120
- if defined?(NativeScannerMagnusLoader) && respond_to?(:scan_directory_with_magnus, true)
121
- scan_directory_with_magnus(path)
122
- else
123
- original_scan_directory(path)
124
- end
125
286
  else
126
287
  original_scan_directory(path)
127
288
  end
@@ -136,13 +297,6 @@ module Rufio
136
297
  case @mode
137
298
  when 'zig'
138
299
  scan_directory_fast_with_zig(path, max_entries)
139
- when 'magnus'
140
- # magnusモードの場合は、native_scanner_magnus.rbで定義されたメソッドを呼ぶ
141
- if defined?(NativeScannerMagnusLoader) && respond_to?(:scan_directory_fast_with_magnus, true)
142
- scan_directory_fast_with_magnus(path, max_entries)
143
- else
144
- original_scan_directory_fast(path, max_entries)
145
- end
146
300
  else
147
301
  original_scan_directory_fast(path, max_entries)
148
302
  end
@@ -156,7 +310,7 @@ module Rufio
156
310
 
157
311
  case @mode
158
312
  when 'zig'
159
- NativeScannerZig.version
313
+ NativeScannerZigFFI.version
160
314
  else
161
315
  original_version
162
316
  end
@@ -167,50 +321,29 @@ module Rufio
167
321
 
168
322
  def available_libraries
169
323
  original = original_available_libraries
170
- result = original.merge(zig: NativeScannerZigLoader.available?)
171
- # magnusが既に追加されていなければ追加
172
- if defined?(NativeScannerMagnusLoader) && !result.key?(:magnus)
173
- result = result.merge(magnus: NativeScannerMagnusLoader.available?)
174
- end
175
- result
324
+ original.merge(zig: NativeScannerZigFFI.available?)
176
325
  end
177
326
 
178
- # autoモードの優先順位を更新(magnus > zig > rust > go > ruby)
327
+ # autoモードの優先順位を更新(zig > ruby)
179
328
  def mode=(value)
180
329
  case value
181
330
  when 'auto'
182
- # 優先順位: Magnus > Zig > Rust > Go > Ruby
183
- if defined?(NativeScannerMagnusLoader) && NativeScannerMagnusLoader.available?
184
- @mode = 'magnus'
185
- @current_library = nil
186
- elsif NativeScannerZigLoader.available?
331
+ # 優先順位: Zig > Ruby
332
+ if NativeScannerZigFFI.available?
187
333
  @mode = 'zig'
188
334
  @current_library = nil
189
- elsif defined?(RustLib) && RustLib.available?
190
- @mode = 'rust'
191
- @current_library = RustLib
192
- elsif defined?(GoLib) && GoLib.available?
193
- @mode = 'go'
194
- @current_library = GoLib
195
335
  else
196
336
  @mode = 'ruby'
197
337
  @current_library = nil
198
338
  end
199
339
  when 'zig'
200
- if NativeScannerZigLoader.available?
340
+ if NativeScannerZigFFI.available?
201
341
  @mode = 'zig'
202
342
  @current_library = nil
203
343
  else
204
344
  @mode = 'ruby'
205
345
  @current_library = nil
206
346
  end
207
- when 'magnus'
208
- if defined?(NativeScannerMagnusLoader) && NativeScannerMagnusLoader.available?
209
- @mode = 'magnus'
210
- @current_library = nil
211
- else
212
- send(:original_mode=, value)
213
- end
214
347
  else
215
348
  send(:original_mode=, value)
216
349
  end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thread'
4
+
5
+ module Rufio
6
+ # 並列スキャン最適化クラス
7
+ #
8
+ # 複数のディレクトリを並列にスキャンし、結果をマージします。
9
+ # スレッドプールを使用して効率的に並列処理を行います。
10
+ #
11
+ # 使用例:
12
+ # parallel_scanner = ParallelScanner.new(max_workers: 4)
13
+ # results = parallel_scanner.scan_all(['/path1', '/path2', '/path3'])
14
+ # all_entries = parallel_scanner.scan_all_merged(['/path1', '/path2'])
15
+ #
16
+ class ParallelScanner
17
+ DEFAULT_MAX_WORKERS = 4
18
+
19
+ # @param max_workers [Integer] 最大ワーカー数
20
+ # @param backend [Symbol] バックエンド (:ruby or :zig)
21
+ def initialize(max_workers: DEFAULT_MAX_WORKERS, backend: :ruby)
22
+ @max_workers = max_workers
23
+ @backend = backend
24
+ end
25
+
26
+ # 複数のディレクトリを並列スキャン
27
+ #
28
+ # @param paths [Array<String>] スキャンするディレクトリパスのリスト
29
+ # @return [Array<Hash>] 各ディレクトリのスキャン結果
30
+ # [{path:, entries:, success:, error:}, ...]
31
+ def scan_all(paths)
32
+ return [] if paths.empty?
33
+
34
+ results = []
35
+ mutex = Mutex.new
36
+ queue = Queue.new
37
+
38
+ # キューにパスを追加
39
+ paths.each { |path| queue << path }
40
+
41
+ # ワーカースレッドを作成
42
+ workers = []
43
+ worker_count = [@max_workers, paths.length].min
44
+
45
+ worker_count.times do
46
+ workers << Thread.new do
47
+ loop do
48
+ path = queue.pop(true) rescue nil
49
+ break if path.nil?
50
+
51
+ result = scan_single_directory(path)
52
+ mutex.synchronize { results << result }
53
+ end
54
+ end
55
+ end
56
+
57
+ # 全ワーカーの完了を待つ
58
+ workers.each(&:join)
59
+
60
+ results
61
+ end
62
+
63
+ # 複数のディレクトリを並列スキャンし、結果をマージ
64
+ #
65
+ # @param paths [Array<String>] スキャンするディレクトリパスのリスト
66
+ # @yield [entry] 各エントリをフィルタリングするブロック(オプション)
67
+ # @return [Array<Hash>] 全エントリのマージされた配列
68
+ def scan_all_merged(paths, &filter)
69
+ results = scan_all(paths)
70
+
71
+ # 成功した結果のみを取得
72
+ all_entries = results
73
+ .select { |r| r[:success] }
74
+ .flat_map { |r| r[:entries] }
75
+
76
+ # フィルタが指定されている場合は適用
77
+ all_entries = all_entries.select(&filter) if block_given?
78
+
79
+ all_entries
80
+ end
81
+
82
+ # 進捗報告付き並列スキャン
83
+ #
84
+ # @param paths [Array<String>] スキャンするディレクトリパスのリスト
85
+ # @yield [completed, total] 進捗情報を受け取るブロック
86
+ # @return [Array<Hash>] 各ディレクトリのスキャン結果
87
+ def scan_all_with_progress(paths, &block)
88
+ return [] if paths.empty?
89
+
90
+ results = []
91
+ mutex = Mutex.new
92
+ queue = Queue.new
93
+ completed = 0
94
+ total = paths.length
95
+
96
+ # キューにパスを追加
97
+ paths.each { |path| queue << path }
98
+
99
+ # ワーカースレッドを作成
100
+ workers = []
101
+ worker_count = [@max_workers, paths.length].min
102
+
103
+ worker_count.times do
104
+ workers << Thread.new do
105
+ loop do
106
+ path = queue.pop(true) rescue nil
107
+ break if path.nil?
108
+
109
+ result = scan_single_directory(path)
110
+
111
+ mutex.synchronize do
112
+ results << result
113
+ completed += 1
114
+ yield(completed, total) if block_given?
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ # 全ワーカーの完了を待つ
121
+ workers.each(&:join)
122
+
123
+ results
124
+ end
125
+
126
+ private
127
+
128
+ # 単一のディレクトリをスキャン
129
+ #
130
+ # @param path [String] スキャンするディレクトリパス
131
+ # @return [Hash] スキャン結果 {path:, entries:, success:, error:}
132
+ def scan_single_directory(path)
133
+ scanner = create_scanner
134
+
135
+ begin
136
+ scanner.scan_async(path)
137
+ entries = scanner.wait(timeout: 60)
138
+
139
+ {
140
+ path: path,
141
+ entries: entries,
142
+ success: true
143
+ }
144
+ rescue StandardError => e
145
+ {
146
+ path: path,
147
+ entries: [],
148
+ success: false,
149
+ error: e.message
150
+ }
151
+ ensure
152
+ scanner.close
153
+ end
154
+ end
155
+
156
+ # バックエンドに応じたスキャナーを作成
157
+ #
158
+ # @return [NativeScannerRubyCore, NativeScannerZigCore] スキャナーインスタンス
159
+ def create_scanner
160
+ case @backend
161
+ when :zig
162
+ if defined?(NativeScannerZigFFI) && NativeScannerZigFFI.available?
163
+ NativeScannerZigCore.new
164
+ else
165
+ # Zigが利用できない場合はRubyにフォールバック
166
+ NativeScannerRubyCore.new
167
+ end
168
+ else
169
+ NativeScannerRubyCore.new
170
+ end
171
+ end
172
+ end
173
+ end
data/lib/rufio/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rufio
4
- VERSION = '0.33.0'
4
+ VERSION = '0.34.0'
5
5
  end
data/lib/rufio.rb CHANGED
@@ -33,8 +33,10 @@ require_relative "rufio/shell_command_completion"
33
33
  require_relative "rufio/command_logger"
34
34
  require_relative "rufio/background_command_executor"
35
35
  require_relative "rufio/native_scanner"
36
- require_relative "rufio/native_scanner_magnus"
37
36
  require_relative "rufio/native_scanner_zig"
37
+ require_relative "rufio/async_scanner_promise"
38
+ require_relative "rufio/async_scanner_fiber"
39
+ require_relative "rufio/parallel_scanner"
38
40
 
39
41
  # プロジェクトモード
40
42
  require_relative "rufio/project_mode"
@@ -7,13 +7,14 @@ ZIG := zig
7
7
  RUBY_INCLUDE := $(shell $(RUBY) -e "puts RbConfig::CONFIG['rubyhdrdir']")
8
8
  RUBY_ARCH_INCLUDE := $(shell $(RUBY) -e "puts RbConfig::CONFIG['rubyarchhdrdir']")
9
9
  RUBY_LIB_DIR := $(shell $(RUBY) -e "puts RbConfig::CONFIG['libdir']")
10
+ RUBY_VERSION := $(shell $(RUBY) -e "puts RbConfig::CONFIG['MAJOR'] + '.' + RbConfig::CONFIG['MINOR']")
10
11
 
11
12
  # コンパイルフラグ
12
13
  ZIG_FLAGS := -O ReleaseFast -dynamic -lc
13
14
  ZIG_FLAGS += -I$(RUBY_INCLUDE)
14
15
  ZIG_FLAGS += -I$(RUBY_ARCH_INCLUDE)
15
16
  ZIG_FLAGS += -L$(RUBY_LIB_DIR)
16
- ZIG_FLAGS += -lruby.3.4
17
+ ZIG_FLAGS += -lruby.$(RUBY_VERSION)
17
18
 
18
19
  # ターゲット
19
20
  TARGET := librufio_zig.dylib