rufio 1.1.0 → 1.3.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 +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +74 -3
- data/README_ja.md +71 -2
- data/Rakefile +14 -1
- data/bin/rufio +31 -2
- data/docs/fast_zip_design_ja.md +167 -0
- data/info/help.md +2 -0
- data/info/keybindings.md +3 -1
- data/info/welcome.md +1 -1
- data/lib/rufio/application.rb +12 -1
- data/lib/rufio/archive_controller.rb +211 -0
- data/lib/rufio/background_command_executor.rb +9 -0
- data/lib/rufio/background_status.rb +49 -0
- data/lib/rufio/bookmark_controller.rb +38 -121
- data/lib/rufio/bookmark_manager.rb +77 -331
- data/lib/rufio/command_completion.rb +6 -1
- data/lib/rufio/command_history.rb +5 -2
- data/lib/rufio/command_mode.rb +40 -134
- data/lib/rufio/command_mode_ui.rb +4 -42
- data/lib/rufio/config.rb +88 -136
- data/lib/rufio/config_loader.rb +3 -11
- data/lib/rufio/dialog_renderer.rb +64 -152
- data/lib/rufio/dialog_support.rb +69 -0
- data/lib/rufio/document_text_extractor.rb +167 -0
- data/lib/rufio/file_operation_controller.rb +49 -222
- data/lib/rufio/file_preview.rb +46 -2
- data/lib/rufio/global_search.rb +121 -9
- data/lib/rufio/help_menu.rb +8 -2
- data/lib/rufio/interpreter_resolver.rb +6 -11
- data/lib/rufio/job_manager.rb +2 -5
- data/lib/rufio/key_input.rb +77 -0
- data/lib/rufio/keybind_handler.rb +81 -115
- data/lib/rufio/local_script_scanner.rb +6 -18
- data/lib/rufio/native/aarch64-linux-gnu/librufio_zig.so +0 -0
- data/lib/rufio/native/aarch64-linux-musl/librufio_zig.so +0 -0
- data/lib/rufio/native/aarch64-macos/librufio_zig.dylib +0 -0
- data/lib/rufio/native/aarch64-windows/rufio_zig.dll +0 -0
- data/lib/rufio/native/x86_64-linux-gnu/librufio_zig.so +0 -0
- data/lib/rufio/native/x86_64-linux-musl/librufio_zig.so +0 -0
- data/lib/rufio/native/x86_64-macos/librufio_zig.dylib +0 -0
- data/lib/rufio/native/x86_64-windows/rufio_zig.dll +0 -0
- data/lib/rufio/native_content_search.rb +153 -0
- data/lib/rufio/native_scanner.rb +13 -68
- data/lib/rufio/native_scanner_zig.rb +106 -56
- data/lib/rufio/native_zip.rb +121 -0
- data/lib/rufio/navigation_controller.rb +18 -20
- data/lib/rufio/screen.rb +2 -12
- data/lib/rufio/script_executor.rb +6 -73
- data/lib/rufio/script_file.rb +44 -0
- data/lib/rufio/script_path_manager.rb +9 -43
- data/lib/rufio/script_runner.rb +13 -50
- data/lib/rufio/search_controller.rb +1 -1
- data/lib/rufio/shell_command_completion.rb +1 -5
- data/lib/rufio/shell_init.rb +90 -0
- data/lib/rufio/task_status.rb +5 -1
- data/lib/rufio/terminal_ui.rb +55 -294
- data/lib/rufio/text_utils.rb +26 -3
- data/lib/rufio/ui_renderer.rb +63 -40
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio/zip_archive.rb +314 -0
- data/lib/rufio/zip_extractor.rb +346 -0
- data/lib/rufio/zip_writer.rb +268 -0
- data/lib/rufio/zoxide_integration.rb +5 -43
- data/lib/rufio.rb +5 -0
- data/lib_zig/rufio_native/Makefile +54 -17
- data/lib_zig/rufio_native/bench.sh +36 -0
- data/lib_zig/rufio_native/build.zig +32 -35
- data/lib_zig/rufio_native/src/cli.zig +99 -0
- data/lib_zig/rufio_native/src/main.zig +187 -83
- data/lib_zig/rufio_native/src/os.zig +889 -0
- data/lib_zig/rufio_native/src/search.zig +919 -0
- data/lib_zig/rufio_native/src/search_test.zig +88 -0
- data/lib_zig/rufio_native/src/zip.zig +482 -0
- metadata +125 -3
- data/lib/rufio/native/rufio_zig.bundle +0 -0
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
//! G mode のコンテンツ検索エンジン。
|
|
2
|
+
//!
|
|
3
|
+
//! Ruby 実装(GlobalSearch#scan_roots / #scan_content)と同じ結果を返しつつ、
|
|
4
|
+
//! 次の手法で ripgrep 以上の速度を狙う:
|
|
5
|
+
//! - 全 CPU コアでディレクトリを並列走査(ワークスタックで負荷分散)
|
|
6
|
+
//! - getdirentries64 による大きなバッファでの一括ディレクトリ読み込み(macOS)
|
|
7
|
+
//! - openat/fstatat で親ディレクトリ fd から相対オープン(パス解決を省略)
|
|
8
|
+
//! - 希少バイト 2 点を SIMD で同時比較する大文字小文字無視リテラル検索
|
|
9
|
+
//! - 行番号はマッチ発生時のみ SIMD で改行を数える(非マッチファイルは数えない)
|
|
10
|
+
//! - スレッドローカル出力バッファでロック回数を削減し、上限超過時は背圧で待機
|
|
11
|
+
const std = @import("std");
|
|
12
|
+
const c = std.c;
|
|
13
|
+
const os = @import("os.zig");
|
|
14
|
+
|
|
15
|
+
pub const FileMeta = os.FileMeta;
|
|
16
|
+
|
|
17
|
+
pub const MAX_NEEDLE = 1024;
|
|
18
|
+
pub const MAX_THREADS = 64;
|
|
19
|
+
|
|
20
|
+
const VLEN = 32;
|
|
21
|
+
const Vec = @Vector(VLEN, u8);
|
|
22
|
+
|
|
23
|
+
/// Ruby 実装と同じく先頭 8KB に NUL があればバイナリとみなす
|
|
24
|
+
const BINARY_SAMPLE = 8192;
|
|
25
|
+
const READ_BUFFER = 256 * 1024;
|
|
26
|
+
/// サイズ不明時の初回読み込み量(多くのソースファイルが 1 回で収まる)
|
|
27
|
+
const FIRST_READ = 16 * 1024;
|
|
28
|
+
const LOCAL_FLUSH = 64 * 1024;
|
|
29
|
+
/// Ruby 側が取り出していない出力がこれを超えたらワーカーは待機する
|
|
30
|
+
const MAX_PENDING = 8 * 1024 * 1024;
|
|
31
|
+
|
|
32
|
+
/// スニペット用に返すマッチ前後のバイト幅(Ruby 側で 30 文字前 + 200 文字に切り出す)
|
|
33
|
+
const SNIPPET_BEFORE = 30 * 4;
|
|
34
|
+
const SNIPPET_WINDOW = 1024;
|
|
35
|
+
|
|
36
|
+
pub const RECORD_HEADER = 25;
|
|
37
|
+
|
|
38
|
+
pub const Kind = enum(u8) {
|
|
39
|
+
match = 1,
|
|
40
|
+
document = 2,
|
|
41
|
+
err = 3,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
pub const State = enum(u8) {
|
|
45
|
+
idle = 0,
|
|
46
|
+
scanning = 1,
|
|
47
|
+
done = 2,
|
|
48
|
+
cancelled = 3,
|
|
49
|
+
failed = 4,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
inline fn fold(b: u8) u8 {
|
|
53
|
+
return if (b >= 'A' and b <= 'Z') b | 0x20 else b;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// 一般的なソースコード/テキストでの出現頻度の目安(大きいほど頻出)
|
|
57
|
+
fn byteRank(b: u8) u8 {
|
|
58
|
+
const letters = "etaoinsrhldcumfpgwybvkxjqz";
|
|
59
|
+
if (b >= 'a' and b <= 'z') {
|
|
60
|
+
const idx = std.mem.indexOfScalar(u8, letters, b).?;
|
|
61
|
+
return @intCast(230 - idx * 6);
|
|
62
|
+
}
|
|
63
|
+
return switch (b) {
|
|
64
|
+
' ' => 255,
|
|
65
|
+
'\t', '\n' => 200,
|
|
66
|
+
'0'...'9' => 130,
|
|
67
|
+
'_', '(', ')', ',', '.', ';', '=', '"', '\'', '/', '-', '*', ':' => 140,
|
|
68
|
+
0xE3 => 150, // ひらがな・カタカナの UTF-8 先頭バイト
|
|
69
|
+
0x80...0xBF => 120, // UTF-8 継続バイト
|
|
70
|
+
else => 60,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// 大文字小文字を無視するリテラル検索(ASCII のみ畳み込み、それ以外は完全一致)
|
|
75
|
+
pub const Matcher = struct {
|
|
76
|
+
needle: [MAX_NEEDLE]u8,
|
|
77
|
+
len: usize,
|
|
78
|
+
i1: usize,
|
|
79
|
+
i2: usize,
|
|
80
|
+
m1: Vec,
|
|
81
|
+
t1: Vec,
|
|
82
|
+
m2: Vec,
|
|
83
|
+
t2: Vec,
|
|
84
|
+
|
|
85
|
+
pub fn init(needle: []const u8) Matcher {
|
|
86
|
+
var self: Matcher = undefined;
|
|
87
|
+
self.len = @min(needle.len, MAX_NEEDLE);
|
|
88
|
+
for (needle[0..self.len], 0..) |b, i| self.needle[i] = fold(b);
|
|
89
|
+
|
|
90
|
+
// 最も希少な 2 バイトを候補判定に使う
|
|
91
|
+
var best: usize = 0;
|
|
92
|
+
var i: usize = 1;
|
|
93
|
+
while (i < self.len) : (i += 1) {
|
|
94
|
+
if (byteRank(self.needle[i]) < byteRank(self.needle[best])) best = i;
|
|
95
|
+
}
|
|
96
|
+
var second: usize = best;
|
|
97
|
+
i = 0;
|
|
98
|
+
while (i < self.len) : (i += 1) {
|
|
99
|
+
if (i == best) continue;
|
|
100
|
+
if (second == best or byteRank(self.needle[i]) < byteRank(self.needle[second])) second = i;
|
|
101
|
+
}
|
|
102
|
+
self.i1 = best;
|
|
103
|
+
self.i2 = second;
|
|
104
|
+
|
|
105
|
+
const b1 = if (self.len > 0) self.needle[best] else 0;
|
|
106
|
+
const b2 = if (self.len > 0) self.needle[second] else 0;
|
|
107
|
+
self.m1 = @splat(foldMask(b1));
|
|
108
|
+
self.t1 = @splat(b1);
|
|
109
|
+
self.m2 = @splat(foldMask(b2));
|
|
110
|
+
self.t2 = @splat(b2);
|
|
111
|
+
return self;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn foldMask(b: u8) u8 {
|
|
115
|
+
return if (b >= 'a' and b <= 'z') 0x20 else 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
inline fn verify(self: *const Matcher, at: []const u8) bool {
|
|
119
|
+
for (self.needle[0..self.len], at[0..self.len]) |n, h| {
|
|
120
|
+
if (fold(h) != n) return false;
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
pub fn find(self: *const Matcher, hay: []const u8) ?usize {
|
|
126
|
+
const n = self.len;
|
|
127
|
+
if (n == 0) return 0;
|
|
128
|
+
if (hay.len < n) return null;
|
|
129
|
+
const last = hay.len - n;
|
|
130
|
+
const hi = @max(self.i1, self.i2);
|
|
131
|
+
const zero: Vec = @splat(0);
|
|
132
|
+
|
|
133
|
+
var p: usize = 0;
|
|
134
|
+
if (hay.len >= hi + 2 * VLEN) {
|
|
135
|
+
const end = hay.len - hi - 2 * VLEN;
|
|
136
|
+
while (p <= end) : (p += 2 * VLEN) {
|
|
137
|
+
const a0: Vec = hay[p + self.i1 ..][0..VLEN].*;
|
|
138
|
+
const b0: Vec = hay[p + self.i2 ..][0..VLEN].*;
|
|
139
|
+
const a1: Vec = hay[p + VLEN + self.i1 ..][0..VLEN].*;
|
|
140
|
+
const b1: Vec = hay[p + VLEN + self.i2 ..][0..VLEN].*;
|
|
141
|
+
const x0 = ((a0 | self.m1) ^ self.t1) | ((b0 | self.m2) ^ self.t2);
|
|
142
|
+
const x1 = ((a1 | self.m1) ^ self.t1) | ((b1 | self.m2) ^ self.t2);
|
|
143
|
+
// 候補なしの判定を 1 命令に寄せる(NEON では umin で済む)
|
|
144
|
+
if (@reduce(.Min, @min(x0, x1)) != 0) continue;
|
|
145
|
+
const lo_mask: u32 = @bitCast(x0 == zero);
|
|
146
|
+
const hi_mask: u32 = @bitCast(x1 == zero);
|
|
147
|
+
var mask: u64 = (@as(u64, hi_mask) << 32) | lo_mask;
|
|
148
|
+
while (mask != 0) : (mask &= mask - 1) {
|
|
149
|
+
const cand = p + @ctz(mask);
|
|
150
|
+
if (cand <= last and self.verify(hay[cand..])) return cand;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
while (p <= last) : (p += 1) {
|
|
155
|
+
if (fold(hay[p + self.i1]) == self.needle[self.i1] and self.verify(hay[p..])) return p;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
pub fn countNewlines(bytes: []const u8) usize {
|
|
162
|
+
const nl: Vec = @splat('\n');
|
|
163
|
+
const one: Vec = @splat(1);
|
|
164
|
+
const zero: Vec = @splat(0);
|
|
165
|
+
var total: usize = 0;
|
|
166
|
+
var i: usize = 0;
|
|
167
|
+
while (i + VLEN <= bytes.len) {
|
|
168
|
+
var acc: Vec = zero;
|
|
169
|
+
var rounds: usize = 0;
|
|
170
|
+
while (rounds < 255 and i + VLEN <= bytes.len) : ({
|
|
171
|
+
rounds += 1;
|
|
172
|
+
i += VLEN;
|
|
173
|
+
}) {
|
|
174
|
+
const v: Vec = bytes[i..][0..VLEN].*;
|
|
175
|
+
acc += @select(u8, v == nl, one, zero);
|
|
176
|
+
}
|
|
177
|
+
total += @reduce(.Add, @as(@Vector(VLEN, u16), acc));
|
|
178
|
+
}
|
|
179
|
+
for (bytes[i..]) |b| total += @intFromBool(b == '\n');
|
|
180
|
+
return total;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
pub fn hasSuffixIgnoreCase(name: []const u8, suffix: []const u8) bool {
|
|
184
|
+
if (suffix.len > name.len) return false;
|
|
185
|
+
for (name[name.len - suffix.len ..], suffix) |a, b| {
|
|
186
|
+
if (fold(a) != fold(b)) return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Ruby の File.extname と同じ規則(先頭のドットは拡張子とみなさない)
|
|
192
|
+
pub fn extname(name: []const u8) []const u8 {
|
|
193
|
+
var start: usize = 0;
|
|
194
|
+
while (start < name.len and name[start] == '.') start += 1;
|
|
195
|
+
const dot = std.mem.lastIndexOfScalar(u8, name[start..], '.') orelse return "";
|
|
196
|
+
return name[start + dot ..];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// NUL 区切りの文字列リスト
|
|
200
|
+
const StringList = struct {
|
|
201
|
+
data: []u8 = &.{},
|
|
202
|
+
items: [][]const u8 = &.{},
|
|
203
|
+
|
|
204
|
+
fn init(packed_bytes: []const u8) !StringList {
|
|
205
|
+
if (packed_bytes.len == 0) return .{};
|
|
206
|
+
const alloc = std.heap.c_allocator;
|
|
207
|
+
const data = try alloc.dupe(u8, packed_bytes);
|
|
208
|
+
const count = std.mem.count(u8, data, "\x00") + 1;
|
|
209
|
+
const items = try alloc.alloc([]const u8, count);
|
|
210
|
+
var it = std.mem.splitScalar(u8, data, 0);
|
|
211
|
+
var n: usize = 0;
|
|
212
|
+
while (it.next()) |item| : (n += 1) items[n] = item;
|
|
213
|
+
return .{ .data = data, .items = items };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
fn deinit(self: *StringList) void {
|
|
217
|
+
const alloc = std.heap.c_allocator;
|
|
218
|
+
if (self.data.len > 0) alloc.free(self.data);
|
|
219
|
+
if (self.items.len > 0) alloc.free(self.items);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/// malloc ベースの伸長バッファ
|
|
224
|
+
const Buf = struct {
|
|
225
|
+
ptr: [*]u8 = undefined,
|
|
226
|
+
len: usize = 0,
|
|
227
|
+
cap: usize = 0,
|
|
228
|
+
|
|
229
|
+
fn reserve(self: *Buf, need: usize) !void {
|
|
230
|
+
if (need <= self.cap) return;
|
|
231
|
+
const new_cap = @max(need, @max(self.cap * 2, 4096));
|
|
232
|
+
const p = c.realloc(if (self.cap == 0) null else self.ptr, new_cap) orelse return error.OutOfMemory;
|
|
233
|
+
self.ptr = @ptrCast(p);
|
|
234
|
+
self.cap = new_cap;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
fn append(self: *Buf, bytes: []const u8) !void {
|
|
238
|
+
try self.reserve(self.len + bytes.len);
|
|
239
|
+
@memcpy(self.ptr[self.len..][0..bytes.len], bytes);
|
|
240
|
+
self.len += bytes.len;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fn items(self: *const Buf) []u8 {
|
|
244
|
+
return if (self.cap == 0) &.{} else self.ptr[0..self.len];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
fn deinit(self: *Buf) void {
|
|
248
|
+
if (self.cap > 0) c.free(self.ptr);
|
|
249
|
+
self.* = .{};
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const Job = struct {
|
|
254
|
+
next: ?*Job,
|
|
255
|
+
root: u32,
|
|
256
|
+
len: usize,
|
|
257
|
+
|
|
258
|
+
fn path(self: *Job) [:0]u8 {
|
|
259
|
+
const base: [*]u8 = @ptrCast(self);
|
|
260
|
+
return base[@sizeOf(Job)..][0..self.len :0];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn create(root: u32, parent: []const u8, name: []const u8) ?*Job {
|
|
264
|
+
const len = if (name.len == 0) parent.len else parent.len + 1 + name.len;
|
|
265
|
+
const mem = c.malloc(@sizeOf(Job) + len + 1) orelse return null;
|
|
266
|
+
const job: *Job = @ptrCast(@alignCast(mem));
|
|
267
|
+
job.* = .{ .next = null, .root = root, .len = len };
|
|
268
|
+
const p = job.path();
|
|
269
|
+
@memcpy(p[0..parent.len], parent);
|
|
270
|
+
if (name.len > 0) {
|
|
271
|
+
p[parent.len] = '/';
|
|
272
|
+
@memcpy(p[parent.len + 1 ..][0..name.len], name);
|
|
273
|
+
}
|
|
274
|
+
p[len] = 0;
|
|
275
|
+
return job;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fn destroy(self: *Job) void {
|
|
279
|
+
c.free(self);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
pub const Search = struct {
|
|
284
|
+
matcher: Matcher,
|
|
285
|
+
roots: StringList,
|
|
286
|
+
excluded: StringList,
|
|
287
|
+
documents: StringList,
|
|
288
|
+
dedup: bool,
|
|
289
|
+
cache: ?*Cache = null,
|
|
290
|
+
|
|
291
|
+
mutex: os.Mutex = .{},
|
|
292
|
+
work_cond: os.Cond = .{},
|
|
293
|
+
stack: ?*Job = null,
|
|
294
|
+
active: usize = 0,
|
|
295
|
+
next_root: usize = 0,
|
|
296
|
+
finished: bool = false,
|
|
297
|
+
running: usize = 0,
|
|
298
|
+
|
|
299
|
+
visit_mutex: os.Mutex = .{},
|
|
300
|
+
visited: std.AutoHashMapUnmanaged(os.DirKey, void) = .empty,
|
|
301
|
+
|
|
302
|
+
out_mutex: os.Mutex = .{},
|
|
303
|
+
space_cond: os.Cond = .{},
|
|
304
|
+
out: Buf = .{},
|
|
305
|
+
taken: Buf = .{},
|
|
306
|
+
|
|
307
|
+
state: std.atomic.Value(u8) = .init(@intFromEnum(State.idle)),
|
|
308
|
+
cancelled: std.atomic.Value(bool) = .init(false),
|
|
309
|
+
skipped: std.atomic.Value(u64) = .init(0),
|
|
310
|
+
|
|
311
|
+
threads: [MAX_THREADS]std.Thread = undefined,
|
|
312
|
+
thread_count: usize = 0,
|
|
313
|
+
|
|
314
|
+
pub fn create(roots: []const u8, query: []const u8, excluded: []const u8, documents: []const u8, threads: usize, cache: ?*Cache) !*Search {
|
|
315
|
+
if (query.len == 0 or query.len > MAX_NEEDLE or roots.len == 0) return error.InvalidArgument;
|
|
316
|
+
const self = try std.heap.c_allocator.create(Search);
|
|
317
|
+
errdefer std.heap.c_allocator.destroy(self);
|
|
318
|
+
self.* = .{
|
|
319
|
+
.matcher = Matcher.init(query),
|
|
320
|
+
.roots = try StringList.init(roots),
|
|
321
|
+
.excluded = try StringList.init(excluded),
|
|
322
|
+
.documents = try StringList.init(documents),
|
|
323
|
+
.dedup = false,
|
|
324
|
+
};
|
|
325
|
+
self.dedup = self.roots.items.len > 1;
|
|
326
|
+
// 使用中のキャッシュは共有せず、キャッシュなしで検索する
|
|
327
|
+
if (cache) |cc| {
|
|
328
|
+
if (cc.begin()) self.cache = cc;
|
|
329
|
+
}
|
|
330
|
+
// 拡張子は Ruby 側で小文字化済みだが念のため揃える
|
|
331
|
+
for (self.excluded.data) |*b| b.* = fold(b.*);
|
|
332
|
+
for (self.documents.data) |*b| b.* = fold(b.*);
|
|
333
|
+
|
|
334
|
+
const want = if (threads == 0) os.defaultThreads() else threads;
|
|
335
|
+
const count = std.math.clamp(want, 1, MAX_THREADS);
|
|
336
|
+
|
|
337
|
+
self.state.store(@intFromEnum(State.scanning), .release);
|
|
338
|
+
// 起動したワーカーは nextJob でこのロックを待つので、running の確定前に終了することはない
|
|
339
|
+
self.mutex.lock();
|
|
340
|
+
self.advanceRootLocked();
|
|
341
|
+
var spawned: usize = 0;
|
|
342
|
+
while (spawned < count) : (spawned += 1) {
|
|
343
|
+
self.threads[spawned] = std.Thread.spawn(.{ .stack_size = 512 * 1024 }, workerMain, .{self}) catch break;
|
|
344
|
+
}
|
|
345
|
+
self.thread_count = spawned;
|
|
346
|
+
self.running = spawned;
|
|
347
|
+
self.mutex.unlock();
|
|
348
|
+
if (spawned == 0) self.state.store(@intFromEnum(State.failed), .release);
|
|
349
|
+
return self;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
pub fn cancel(self: *Search) void {
|
|
353
|
+
self.cancelled.store(true, .release);
|
|
354
|
+
self.mutex.lock();
|
|
355
|
+
self.work_cond.broadcast();
|
|
356
|
+
self.mutex.unlock();
|
|
357
|
+
self.out_mutex.lock();
|
|
358
|
+
self.space_cond.broadcast();
|
|
359
|
+
self.out_mutex.unlock();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
pub fn destroy(self: *Search) void {
|
|
363
|
+
self.cancel();
|
|
364
|
+
for (self.threads[0..self.thread_count]) |t| t.join();
|
|
365
|
+
if (self.cache) |cc| cc.end(self.getState() == .done);
|
|
366
|
+
while (self.stack) |job| {
|
|
367
|
+
self.stack = job.next;
|
|
368
|
+
job.destroy();
|
|
369
|
+
}
|
|
370
|
+
self.visited.deinit(std.heap.c_allocator);
|
|
371
|
+
self.out.deinit();
|
|
372
|
+
self.taken.deinit();
|
|
373
|
+
self.roots.deinit();
|
|
374
|
+
self.excluded.deinit();
|
|
375
|
+
self.documents.deinit();
|
|
376
|
+
std.heap.c_allocator.destroy(self);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
pub fn getState(self: *Search) State {
|
|
380
|
+
return @enumFromInt(self.state.load(.acquire));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/// 溜まった出力を取り出す(返したバッファは次の drain まで有効)
|
|
384
|
+
pub fn drain(self: *Search) []const u8 {
|
|
385
|
+
self.taken.len = 0;
|
|
386
|
+
self.out_mutex.lock();
|
|
387
|
+
std.mem.swap(Buf, &self.out, &self.taken);
|
|
388
|
+
self.space_cond.broadcast();
|
|
389
|
+
self.out_mutex.unlock();
|
|
390
|
+
return self.taken.items();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
fn advanceRootLocked(self: *Search) void {
|
|
394
|
+
// 複数ブックマークは Ruby 実装同様に順番に走査し、重複ディレクトリは先勝ちにする
|
|
395
|
+
while (self.next_root < self.roots.items.len) {
|
|
396
|
+
const idx = self.next_root;
|
|
397
|
+
self.next_root += 1;
|
|
398
|
+
if (Job.create(@intCast(idx), self.roots.items[idx], "")) |job| {
|
|
399
|
+
job.next = self.stack;
|
|
400
|
+
self.stack = job;
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
self.finished = true;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
fn nextJob(self: *Search) ?*Job {
|
|
408
|
+
self.mutex.lock();
|
|
409
|
+
defer self.mutex.unlock();
|
|
410
|
+
while (true) {
|
|
411
|
+
if (self.cancelled.load(.acquire)) return null;
|
|
412
|
+
if (self.stack) |job| {
|
|
413
|
+
self.stack = job.next;
|
|
414
|
+
self.active += 1;
|
|
415
|
+
return job;
|
|
416
|
+
}
|
|
417
|
+
if (self.finished) return null;
|
|
418
|
+
self.work_cond.wait(&self.mutex);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
fn finishJob(self: *Search, subdirs: ?*Job, subdirs_tail: ?*Job, subdir_count: usize) void {
|
|
423
|
+
self.mutex.lock();
|
|
424
|
+
defer self.mutex.unlock();
|
|
425
|
+
if (subdirs) |head| {
|
|
426
|
+
subdirs_tail.?.next = self.stack;
|
|
427
|
+
self.stack = head;
|
|
428
|
+
}
|
|
429
|
+
self.active -= 1;
|
|
430
|
+
if (self.active == 0 and self.stack == null) {
|
|
431
|
+
self.advanceRootLocked();
|
|
432
|
+
self.work_cond.broadcast();
|
|
433
|
+
} else if (subdir_count > 1) {
|
|
434
|
+
self.work_cond.broadcast();
|
|
435
|
+
} else if (subdir_count == 1) {
|
|
436
|
+
// 自スレッドが続けて取れるので起こすのは 1 つで足りる
|
|
437
|
+
self.work_cond.signal();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
fn workerExit(self: *Search) void {
|
|
442
|
+
self.mutex.lock();
|
|
443
|
+
self.running -= 1;
|
|
444
|
+
const last = self.running == 0;
|
|
445
|
+
self.mutex.unlock();
|
|
446
|
+
if (last) {
|
|
447
|
+
const final: State = if (self.cancelled.load(.acquire)) .cancelled else .done;
|
|
448
|
+
self.state.store(@intFromEnum(final), .release);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
fn publish(self: *Search, local: *Buf) void {
|
|
453
|
+
if (local.len == 0) return;
|
|
454
|
+
self.out_mutex.lock();
|
|
455
|
+
while (self.out.len > MAX_PENDING and !self.cancelled.load(.acquire)) {
|
|
456
|
+
self.space_cond.wait(&self.out_mutex);
|
|
457
|
+
}
|
|
458
|
+
self.out.append(local.items()) catch {};
|
|
459
|
+
self.out_mutex.unlock();
|
|
460
|
+
local.len = 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
fn claimDirectory(self: *Search, dir: *os.Dir) bool {
|
|
464
|
+
if (!self.dedup) return true;
|
|
465
|
+
const key = dir.identity() orelse return true;
|
|
466
|
+
self.visit_mutex.lock();
|
|
467
|
+
defer self.visit_mutex.unlock();
|
|
468
|
+
const gop = self.visited.getOrPut(std.heap.c_allocator, key) catch return true;
|
|
469
|
+
return !gop.found_existing;
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
fn workerMain(s: *Search) void {
|
|
474
|
+
var w = Worker{ .s = s };
|
|
475
|
+
defer w.deinit();
|
|
476
|
+
if (w.init()) {
|
|
477
|
+
while (s.nextJob()) |job| {
|
|
478
|
+
w.processDir(job);
|
|
479
|
+
job.destroy();
|
|
480
|
+
s.finishJob(w.subdirs, w.subdirs_tail, w.subdir_count);
|
|
481
|
+
w.subdirs = null;
|
|
482
|
+
w.subdirs_tail = null;
|
|
483
|
+
w.subdir_count = 0;
|
|
484
|
+
if (w.out.len >= LOCAL_FLUSH) s.publish(&w.out);
|
|
485
|
+
}
|
|
486
|
+
} else |_| {}
|
|
487
|
+
s.publish(&w.out);
|
|
488
|
+
s.workerExit();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const Worker = struct {
|
|
492
|
+
s: *Search,
|
|
493
|
+
read: Buf = .{},
|
|
494
|
+
scratch: os.Scratch = .{},
|
|
495
|
+
out: Buf = .{},
|
|
496
|
+
subdirs: ?*Job = null,
|
|
497
|
+
subdirs_tail: ?*Job = null,
|
|
498
|
+
subdir_count: usize = 0,
|
|
499
|
+
key: Buf = .{},
|
|
500
|
+
|
|
501
|
+
fn init(self: *Worker) !void {
|
|
502
|
+
try self.read.reserve(READ_BUFFER);
|
|
503
|
+
try self.scratch.init();
|
|
504
|
+
try self.out.reserve(LOCAL_FLUSH * 2);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
fn deinit(self: *Worker) void {
|
|
508
|
+
while (self.subdirs) |job| {
|
|
509
|
+
self.subdirs = job.next;
|
|
510
|
+
job.destroy();
|
|
511
|
+
}
|
|
512
|
+
self.read.deinit();
|
|
513
|
+
self.scratch.deinit();
|
|
514
|
+
self.out.deinit();
|
|
515
|
+
self.key.deinit();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
fn cancelled(self: *Worker) bool {
|
|
519
|
+
return self.s.cancelled.load(.monotonic);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
fn record(self: *Worker, kind: Kind, root: u32, line: u64, offset: u32, dir: []const u8, name: []const u8, extra: []const u8) void {
|
|
523
|
+
const path_len = if (name.len == 0) dir.len else dir.len + 1 + name.len;
|
|
524
|
+
self.out.reserve(self.out.len + RECORD_HEADER + path_len + extra.len) catch return;
|
|
525
|
+
var hdr: [RECORD_HEADER]u8 = undefined;
|
|
526
|
+
hdr[0] = @intFromEnum(kind);
|
|
527
|
+
std.mem.writeInt(u32, hdr[1..5], root, .little);
|
|
528
|
+
std.mem.writeInt(u64, hdr[5..13], line, .little);
|
|
529
|
+
std.mem.writeInt(u32, hdr[13..17], offset, .little);
|
|
530
|
+
std.mem.writeInt(u32, hdr[17..21], @intCast(path_len), .little);
|
|
531
|
+
std.mem.writeInt(u32, hdr[21..25], @intCast(extra.len), .little);
|
|
532
|
+
self.out.append(&hdr) catch unreachable;
|
|
533
|
+
self.out.append(dir) catch unreachable;
|
|
534
|
+
if (name.len > 0) {
|
|
535
|
+
self.out.append("/") catch unreachable;
|
|
536
|
+
self.out.append(name) catch unreachable;
|
|
537
|
+
}
|
|
538
|
+
self.out.append(extra) catch unreachable;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
fn recordErrno(self: *Worker, root: u32, dir: []const u8, name: []const u8) void {
|
|
542
|
+
self.record(.err, root, 0, 0, dir, name, os.lastErrorMessage());
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
fn pushSubdir(self: *Worker, job: *Job, name: []const u8) void {
|
|
546
|
+
const sub = Job.create(job.root, job.path(), name) orelse return;
|
|
547
|
+
sub.next = self.subdirs;
|
|
548
|
+
if (self.subdirs == null) self.subdirs_tail = sub;
|
|
549
|
+
self.subdirs = sub;
|
|
550
|
+
self.subdir_count += 1;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
fn processDir(self: *Worker, job: *Job) void {
|
|
554
|
+
const dir_path = job.path();
|
|
555
|
+
var dir = os.Dir.open(dir_path, &self.scratch) orelse {
|
|
556
|
+
self.recordErrno(job.root, dir_path, "");
|
|
557
|
+
return;
|
|
558
|
+
};
|
|
559
|
+
defer dir.close();
|
|
560
|
+
if (!self.s.claimDirectory(&dir)) return;
|
|
561
|
+
|
|
562
|
+
while (!self.cancelled()) {
|
|
563
|
+
const entry = dir.next() catch {
|
|
564
|
+
self.recordErrno(job.root, dir_path, "");
|
|
565
|
+
return;
|
|
566
|
+
} orelse return;
|
|
567
|
+
switch (entry.kind) {
|
|
568
|
+
.dir => self.pushSubdir(job, entry.name),
|
|
569
|
+
.file => self.handleFile(&dir, job, entry.name, if (entry.meta) |m| m.size else null, entry.meta),
|
|
570
|
+
// ファイルへのシンボリックリンクは検索し、ディレクトリへのリンクは辿らない
|
|
571
|
+
.symlink => if (dir.stat(entry.name, true)) |info| {
|
|
572
|
+
if (info.kind == .file) self.handleFile(&dir, job, entry.name, null, null);
|
|
573
|
+
},
|
|
574
|
+
.unknown => if (dir.stat(entry.name, false)) |info| switch (info.kind) {
|
|
575
|
+
.dir => self.pushSubdir(job, entry.name),
|
|
576
|
+
.file => self.handleFile(&dir, job, entry.name, info.size, null),
|
|
577
|
+
.symlink => if (dir.stat(entry.name, true)) |target| {
|
|
578
|
+
if (target.kind == .file) self.handleFile(&dir, job, entry.name, null, null);
|
|
579
|
+
},
|
|
580
|
+
else => {},
|
|
581
|
+
},
|
|
582
|
+
.other => {},
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
fn handleFile(self: *Worker, dir: *os.Dir, job: *Job, name: [:0]const u8, size: ?u64, meta: ?FileMeta) void {
|
|
588
|
+
for (self.s.excluded.items) |ext| {
|
|
589
|
+
if (ext.len > 0 and hasSuffixIgnoreCase(name, ext)) {
|
|
590
|
+
_ = self.s.skipped.fetchAdd(1, .monotonic);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const ext = extname(name);
|
|
595
|
+
if (ext.len > 0) {
|
|
596
|
+
for (self.s.documents.items) |doc| {
|
|
597
|
+
if (doc.len == ext.len and hasSuffixIgnoreCase(ext, doc)) {
|
|
598
|
+
self.record(.document, job.root, 0, 0, job.path(), name, "");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// 空ファイルはマッチし得ないので open すら省く
|
|
604
|
+
if (size == 0) return;
|
|
605
|
+
|
|
606
|
+
// キャッシュ済みで属性が一致すれば open/read を一切行わない
|
|
607
|
+
var key: []const u8 = &.{};
|
|
608
|
+
const cache = if (meta != null and meta.?.size <= MAX_CACHED_FILE) self.s.cache else null;
|
|
609
|
+
if (cache) |cc| {
|
|
610
|
+
key = self.cacheKey(job.path(), name) orelse return;
|
|
611
|
+
if (cc.lookup(key, meta.?)) |hit| {
|
|
612
|
+
switch (hit) {
|
|
613
|
+
.binary => _ = self.s.skipped.fetchAdd(1, .monotonic),
|
|
614
|
+
.text => |content| {
|
|
615
|
+
var line_no: u64 = 1;
|
|
616
|
+
self.searchLines(content, job, name, &line_no);
|
|
617
|
+
},
|
|
618
|
+
}
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const file = dir.openFile(name) orelse {
|
|
624
|
+
self.recordErrno(job.root, job.path(), name);
|
|
625
|
+
return;
|
|
626
|
+
};
|
|
627
|
+
defer file.close();
|
|
628
|
+
if (cache) |cc| {
|
|
629
|
+
self.searchWholeFile(file, job, name, meta.?, cc, key);
|
|
630
|
+
} else {
|
|
631
|
+
self.searchFile(file, job, name, size, 0);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
fn cacheKey(self: *Worker, dir: []const u8, name: []const u8) ?[]const u8 {
|
|
636
|
+
self.key.len = 0;
|
|
637
|
+
self.key.append(dir) catch return null;
|
|
638
|
+
self.key.append("/") catch return null;
|
|
639
|
+
self.key.append(name) catch return null;
|
|
640
|
+
return self.key.items();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/// dst を埋めるまで読む。通常ファイルの短い read は EOF なのでそこで止める(EOF 確認の read を省く)。
|
|
644
|
+
/// エラー時は null
|
|
645
|
+
fn readUpTo(file: os.File, dst: []u8) ?usize {
|
|
646
|
+
var got: usize = 0;
|
|
647
|
+
while (got < dst.len) {
|
|
648
|
+
const want = dst.len - got;
|
|
649
|
+
const n = file.read(dst[got..]) orelse return null;
|
|
650
|
+
if (n == 0) break;
|
|
651
|
+
got += n;
|
|
652
|
+
if (n < want) break;
|
|
653
|
+
}
|
|
654
|
+
return got;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/// キャッシュ対象のファイルを丸ごと読み、判定結果と内容をキャッシュしてから検索する
|
|
658
|
+
fn searchWholeFile(self: *Worker, file: os.File, job: *Job, name: []const u8, meta: FileMeta, cache: *Cache, key: []const u8) void {
|
|
659
|
+
const size: usize = @intCast(meta.size);
|
|
660
|
+
// 1 バイト余分に要求してファイルの伸長を検知する
|
|
661
|
+
self.read.reserve(size + 1) catch return;
|
|
662
|
+
const buf = self.read.ptr[0 .. size + 1];
|
|
663
|
+
const first = if (size < FIRST_READ) size + 1 else BINARY_SAMPLE;
|
|
664
|
+
var got = readUpTo(file, buf[0..first]) orelse return self.recordErrno(job.root, job.path(), name);
|
|
665
|
+
if (std.mem.indexOfScalar(u8, buf[0..@min(got, BINARY_SAMPLE)], 0) != null) {
|
|
666
|
+
cache.store(key, meta, null);
|
|
667
|
+
_ = self.s.skipped.fetchAdd(1, .monotonic);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (got == first and first < buf.len) {
|
|
671
|
+
got += readUpTo(file, buf[got..]) orelse return self.recordErrno(job.root, job.path(), name);
|
|
672
|
+
}
|
|
673
|
+
if (got > size) {
|
|
674
|
+
// 走査後にファイルが伸びた: キャッシュせず残りをストリーミングで処理
|
|
675
|
+
self.searchFile(file, job, name, null, got);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
cache.store(key, meta, buf[0..got]);
|
|
679
|
+
var line_no: u64 = 1;
|
|
680
|
+
self.searchLines(buf[0..got], job, name, &line_no);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/// 完全な行の並び(末尾は改行または EOF)を検索してマッチを記録する
|
|
684
|
+
fn searchLines(self: *Worker, buf: []const u8, job: *Job, name: []const u8, line_no: *u64) void {
|
|
685
|
+
const m = &self.s.matcher;
|
|
686
|
+
var pos: usize = 0;
|
|
687
|
+
var counted: usize = 0;
|
|
688
|
+
while (pos < buf.len) {
|
|
689
|
+
const rel = m.find(buf[pos..]) orelse break;
|
|
690
|
+
const hit = pos + rel;
|
|
691
|
+
line_no.* += countNewlines(buf[counted..hit]);
|
|
692
|
+
counted = hit;
|
|
693
|
+
const line_start = if (std.mem.lastIndexOfScalar(u8, buf[0..hit], '\n')) |i| i + 1 else 0;
|
|
694
|
+
const line_end = if (std.mem.indexOfScalarPos(u8, buf, hit, '\n')) |i| i + 1 else buf.len;
|
|
695
|
+
|
|
696
|
+
var start = if (hit - line_start > SNIPPET_BEFORE) hit - SNIPPET_BEFORE else line_start;
|
|
697
|
+
while (start < hit and (buf[start] & 0xC0) == 0x80) start += 1;
|
|
698
|
+
const stop = @min(line_end, start + SNIPPET_WINDOW);
|
|
699
|
+
self.record(.match, job.root, line_no.*, @intCast(hit - start), job.path(), name, buf[start..stop]);
|
|
700
|
+
if (self.out.len >= LOCAL_FLUSH) self.s.publish(&self.out);
|
|
701
|
+
pos = line_end;
|
|
702
|
+
}
|
|
703
|
+
line_no.* += countNewlines(buf[counted..]);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/// ストリーミング検索。preloaded > 0 のときは read バッファ先頭に読み込み済み(バイナリ判定済み)
|
|
707
|
+
fn searchFile(self: *Worker, file: os.File, job: *Job, name: []const u8, size: ?u64, preloaded: usize) void {
|
|
708
|
+
var len: usize = preloaded;
|
|
709
|
+
var eof = false;
|
|
710
|
+
var checked_binary = preloaded > 0;
|
|
711
|
+
var line_no: u64 = 1;
|
|
712
|
+
// 小さいファイルは 1 回の read で読み切り、短い read を EOF とみなして 2 回目を省く。
|
|
713
|
+
// 大きいファイルは先頭 8KB だけ読んでバイナリなら即座に打ち切る。
|
|
714
|
+
var want: usize = if (size) |sz|
|
|
715
|
+
(if (sz < FIRST_READ) @as(usize, @intCast(sz)) + 1 else BINARY_SAMPLE)
|
|
716
|
+
else
|
|
717
|
+
FIRST_READ;
|
|
718
|
+
|
|
719
|
+
while (true) {
|
|
720
|
+
if (self.cancelled()) return;
|
|
721
|
+
if (!eof) {
|
|
722
|
+
if (len == self.read.cap) {
|
|
723
|
+
// 1 行がバッファより長い場合のみ拡張する
|
|
724
|
+
self.read.reserve(self.read.cap * 2) catch return;
|
|
725
|
+
}
|
|
726
|
+
const req = @min(want, self.read.cap - len);
|
|
727
|
+
want = self.read.cap;
|
|
728
|
+
const n = file.read(self.read.ptr[len..][0..req]) orelse {
|
|
729
|
+
self.recordErrno(job.root, job.path(), name);
|
|
730
|
+
return;
|
|
731
|
+
};
|
|
732
|
+
if (n == 0) {
|
|
733
|
+
eof = true;
|
|
734
|
+
} else {
|
|
735
|
+
len += n;
|
|
736
|
+
if (size != null and n < req) eof = true;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const buf = self.read.ptr[0..len];
|
|
740
|
+
|
|
741
|
+
if (!checked_binary) {
|
|
742
|
+
if (!eof and len < BINARY_SAMPLE) continue;
|
|
743
|
+
if (std.mem.indexOfScalar(u8, buf[0..@min(len, BINARY_SAMPLE)], 0) != null) {
|
|
744
|
+
_ = self.s.skipped.fetchAdd(1, .monotonic);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
checked_binary = true;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
var end = len;
|
|
751
|
+
if (!eof) {
|
|
752
|
+
const last_nl = std.mem.lastIndexOfScalar(u8, buf, '\n') orelse continue;
|
|
753
|
+
end = last_nl + 1;
|
|
754
|
+
}
|
|
755
|
+
self.searchLines(buf[0..end], job, name, &line_no);
|
|
756
|
+
if (eof) return;
|
|
757
|
+
|
|
758
|
+
const rest = len - end;
|
|
759
|
+
if (rest > 0) std.mem.copyForwards(u8, self.read.ptr[0..rest], buf[end..len]);
|
|
760
|
+
len = rest;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
/// 1 ファイルあたりのキャッシュ上限(これを超えるファイルは毎回ストリーミングで読む)
|
|
767
|
+
const MAX_CACHED_FILE = 16 * 1024 * 1024;
|
|
768
|
+
|
|
769
|
+
/// 検索をまたいで保持するファイル内容キャッシュ。
|
|
770
|
+
/// G mode はキー入力のたびに全体を再検索するため、2 回目以降は open/read を省いて
|
|
771
|
+
/// メモリ上で検索する。走査時に取得する mtime/inode/サイズで毎回検証する。
|
|
772
|
+
pub const Cache = struct {
|
|
773
|
+
const SHARDS = 64;
|
|
774
|
+
|
|
775
|
+
const Entry = struct {
|
|
776
|
+
meta: FileMeta,
|
|
777
|
+
/// null はバイナリ判定済み
|
|
778
|
+
data: ?[*]u8,
|
|
779
|
+
len: usize,
|
|
780
|
+
gen: u32,
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
const Shard = struct {
|
|
784
|
+
mutex: os.Mutex = .{},
|
|
785
|
+
map: std.StringHashMapUnmanaged(Entry) = .empty,
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
pub const Hit = union(enum) {
|
|
789
|
+
binary,
|
|
790
|
+
text: []const u8,
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
shards: [SHARDS]Shard = @splat(.{}),
|
|
794
|
+
max_bytes: u64,
|
|
795
|
+
bytes: std.atomic.Value(u64) = .init(0),
|
|
796
|
+
gen: u32 = 0,
|
|
797
|
+
busy: std.atomic.Value(bool) = .init(false),
|
|
798
|
+
|
|
799
|
+
pub fn create(max_bytes: u64) !*Cache {
|
|
800
|
+
const self = try std.heap.c_allocator.create(Cache);
|
|
801
|
+
self.* = .{ .max_bytes = max_bytes };
|
|
802
|
+
return self;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
pub fn destroy(self: *Cache) void {
|
|
806
|
+
for (&self.shards) |*shard| {
|
|
807
|
+
var it = shard.map.iterator();
|
|
808
|
+
while (it.next()) |kv| freeEntry(kv.key_ptr.*, kv.value_ptr.*);
|
|
809
|
+
shard.map.deinit(std.heap.c_allocator);
|
|
810
|
+
}
|
|
811
|
+
std.heap.c_allocator.destroy(self);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
pub fn cachedBytes(self: *Cache) u64 {
|
|
815
|
+
return self.bytes.load(.acquire);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/// 1 つのキャッシュを同時に使える検索は 1 つだけ
|
|
819
|
+
pub fn begin(self: *Cache) bool {
|
|
820
|
+
if (self.busy.cmpxchgStrong(false, true, .acq_rel, .acquire) != null) return false;
|
|
821
|
+
self.gen +%= 1;
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/// 完走した検索のあとは、今回見つからなかった(削除・範囲外の)エントリを捨てる
|
|
826
|
+
pub fn end(self: *Cache, completed: bool) void {
|
|
827
|
+
if (completed) {
|
|
828
|
+
var stale: std.ArrayList([]const u8) = .empty;
|
|
829
|
+
defer stale.deinit(std.heap.c_allocator);
|
|
830
|
+
for (&self.shards) |*shard| {
|
|
831
|
+
stale.clearRetainingCapacity();
|
|
832
|
+
var it = shard.map.iterator();
|
|
833
|
+
while (it.next()) |kv| {
|
|
834
|
+
if (kv.value_ptr.gen != self.gen) stale.append(std.heap.c_allocator, kv.key_ptr.*) catch break;
|
|
835
|
+
}
|
|
836
|
+
for (stale.items) |key| {
|
|
837
|
+
const kv = shard.map.fetchRemove(key).?;
|
|
838
|
+
self.release(kv.value);
|
|
839
|
+
freeEntry(kv.key, kv.value);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
self.busy.store(false, .release);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
fn shardFor(self: *Cache, key: []const u8) *Shard {
|
|
847
|
+
return &self.shards[std.hash.Wyhash.hash(0, key) % SHARDS];
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
fn freeEntry(key: []const u8, entry: Entry) void {
|
|
851
|
+
if (entry.data) |p| c.free(p);
|
|
852
|
+
std.heap.c_allocator.free(key);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
fn release(self: *Cache, entry: Entry) void {
|
|
856
|
+
if (entry.data != null) _ = self.bytes.fetchSub(entry.len, .monotonic);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
pub fn lookup(self: *Cache, key: []const u8, meta: FileMeta) ?Hit {
|
|
860
|
+
const shard = self.shardFor(key);
|
|
861
|
+
shard.mutex.lock();
|
|
862
|
+
defer shard.mutex.unlock();
|
|
863
|
+
const entry = shard.map.getPtr(key) orelse return null;
|
|
864
|
+
if (!entry.meta.eql(meta)) {
|
|
865
|
+
// 変更されたファイル: 古い内容を捨てて読み直させる
|
|
866
|
+
const kv = shard.map.fetchRemove(key).?;
|
|
867
|
+
self.release(kv.value);
|
|
868
|
+
freeEntry(kv.key, kv.value);
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
entry.gen = self.gen;
|
|
872
|
+
// 内容は同じ検索中に他スレッドから解放されない(同一パスは 1 スレッドしか扱わない)
|
|
873
|
+
return if (entry.data) |p| .{ .text = p[0..entry.len] } else .binary;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
pub fn store(self: *Cache, key: []const u8, meta: FileMeta, content: ?[]const u8) void {
|
|
877
|
+
var data: ?[*]u8 = null;
|
|
878
|
+
var len: usize = 0;
|
|
879
|
+
if (content) |bytes| {
|
|
880
|
+
// 予算を超える場合は保存しない(次回もディスクから読む)
|
|
881
|
+
const prev = self.bytes.fetchAdd(bytes.len, .monotonic);
|
|
882
|
+
if (prev + bytes.len > self.max_bytes) {
|
|
883
|
+
_ = self.bytes.fetchSub(bytes.len, .monotonic);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const p: [*]u8 = @ptrCast(c.malloc(@max(bytes.len, 1)) orelse {
|
|
887
|
+
_ = self.bytes.fetchSub(bytes.len, .monotonic);
|
|
888
|
+
return;
|
|
889
|
+
});
|
|
890
|
+
@memcpy(p[0..bytes.len], bytes);
|
|
891
|
+
data = p;
|
|
892
|
+
len = bytes.len;
|
|
893
|
+
}
|
|
894
|
+
const owned_key = std.heap.c_allocator.dupe(u8, key) catch {
|
|
895
|
+
if (data) |p| {
|
|
896
|
+
c.free(p);
|
|
897
|
+
_ = self.bytes.fetchSub(len, .monotonic);
|
|
898
|
+
}
|
|
899
|
+
return;
|
|
900
|
+
};
|
|
901
|
+
const shard = self.shardFor(key);
|
|
902
|
+
shard.mutex.lock();
|
|
903
|
+
defer shard.mutex.unlock();
|
|
904
|
+
const gop = shard.map.getOrPut(std.heap.c_allocator, owned_key) catch {
|
|
905
|
+
if (data) |p| {
|
|
906
|
+
c.free(p);
|
|
907
|
+
_ = self.bytes.fetchSub(len, .monotonic);
|
|
908
|
+
}
|
|
909
|
+
std.heap.c_allocator.free(owned_key);
|
|
910
|
+
return;
|
|
911
|
+
};
|
|
912
|
+
if (gop.found_existing) {
|
|
913
|
+
self.release(gop.value_ptr.*);
|
|
914
|
+
freeEntry(gop.key_ptr.*, gop.value_ptr.*);
|
|
915
|
+
gop.key_ptr.* = owned_key;
|
|
916
|
+
}
|
|
917
|
+
gop.value_ptr.* = .{ .meta = meta, .data = data, .len = len, .gen = self.gen };
|
|
918
|
+
}
|
|
919
|
+
};
|