dsh-ops 0.0.0-stage → 0.2.1
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.
- package/CHANGELOG.md +189 -0
- package/LICENSE +30 -0
- package/NOTICE +106 -0
- package/PROVENANCE.md +417 -0
- package/README.en.md +121 -0
- package/README.md +113 -2
- package/README.zh.md +114 -0
- package/bin/dsh-ops.mjs +1216 -0
- package/cordis.patch.yml +160 -0
- package/docs/manual-validation.md +53 -0
- package/docs/schema-baseline.json +64 -0
- package/docs/schema-current.json +84 -0
- package/docs/schema-measurement.md +17 -0
- package/dsh-plugin.json +88 -0
- package/icon.svg +12 -0
- package/lib/binary.js +409 -0
- package/lib/config.js +198 -0
- package/lib/handshake.js +252 -0
- package/lib/index.js +108 -0
- package/lib/jobs.js +42 -0
- package/lib/policy.js +64 -0
- package/lib/presentation.js +63 -0
- package/lib/profile-install.js +61 -0
- package/lib/rust.js +194 -0
- package/lib/session-shells.js +78 -0
- package/lib/shells.js +993 -0
- package/lib/tools.js +657 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +114 -4
- package/vendor/fastctx/Cargo.lock +3210 -0
- package/vendor/fastctx/Cargo.toml +94 -0
- package/vendor/fastctx/FORK.md +119 -0
- package/vendor/fastctx/LICENSE-APACHE +201 -0
- package/vendor/fastctx/NOTICE +40 -0
- package/vendor/fastctx/README.md +439 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
- package/vendor/fastctx/UPSTREAM.md +49 -0
- package/vendor/fastctx/build.rs +413 -0
- package/vendor/fastctx/src/background_status.rs +403 -0
- package/vendor/fastctx/src/binary.rs +75 -0
- package/vendor/fastctx/src/bounded_sort.rs +500 -0
- package/vendor/fastctx/src/budget.rs +781 -0
- package/vendor/fastctx/src/cli/mod.rs +110 -0
- package/vendor/fastctx/src/context_guard.rs +289 -0
- package/vendor/fastctx/src/control/mod.rs +6 -0
- package/vendor/fastctx/src/control/paths.rs +49 -0
- package/vendor/fastctx/src/control/settings.rs +753 -0
- package/vendor/fastctx/src/control/transaction.rs +531 -0
- package/vendor/fastctx/src/edit/document.rs +535 -0
- package/vendor/fastctx/src/edit/locks.rs +371 -0
- package/vendor/fastctx/src/edit/mod.rs +213 -0
- package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
- package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
- package/vendor/fastctx/src/edit/private_storage.rs +234 -0
- package/vendor/fastctx/src/edit/replace.rs +1030 -0
- package/vendor/fastctx/src/edit_server.rs +53 -0
- package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
- package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
- package/vendor/fastctx/src/encoding.rs +1118 -0
- package/vendor/fastctx/src/file_executor.rs +1151 -0
- package/vendor/fastctx/src/file_snapshot.rs +1491 -0
- package/vendor/fastctx/src/glob_filter.rs +98 -0
- package/vendor/fastctx/src/glob_tool.rs +653 -0
- package/vendor/fastctx/src/grep_sink.rs +1162 -0
- package/vendor/fastctx/src/grep_tool.rs +2449 -0
- package/vendor/fastctx/src/lib.rs +45 -0
- package/vendor/fastctx/src/main.rs +15 -0
- package/vendor/fastctx/src/model.rs +51 -0
- package/vendor/fastctx/src/model_guidance.rs +62 -0
- package/vendor/fastctx/src/operation.rs +356 -0
- package/vendor/fastctx/src/ordered_window.rs +1235 -0
- package/vendor/fastctx/src/os_environment.rs +414 -0
- package/vendor/fastctx/src/path_codec.rs +850 -0
- package/vendor/fastctx/src/paths.rs +244 -0
- package/vendor/fastctx/src/process_identity.rs +763 -0
- package/vendor/fastctx/src/process_policy.rs +74 -0
- package/vendor/fastctx/src/read_tool/batch.rs +496 -0
- package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
- package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
- package/vendor/fastctx/src/read_tool/mod.rs +245 -0
- package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
- package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
- package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
- package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
- package/vendor/fastctx/src/render_plan.rs +468 -0
- package/vendor/fastctx/src/runtime/activity.rs +159 -0
- package/vendor/fastctx/src/runtime/hosts.rs +99 -0
- package/vendor/fastctx/src/runtime/journal.rs +556 -0
- package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
- package/vendor/fastctx/src/runtime/mod.rs +746 -0
- package/vendor/fastctx/src/runtime/protocol.rs +296 -0
- package/vendor/fastctx/src/runtime/session.rs +536 -0
- package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
- package/vendor/fastctx/src/search_parallelism.rs +106 -0
- package/vendor/fastctx/src/search_text.rs +227 -0
- package/vendor/fastctx/src/server.rs +359 -0
- package/vendor/fastctx/src/server_manifest.rs +468 -0
- package/vendor/fastctx/src/server_support.rs +826 -0
- package/vendor/fastctx/src/session.rs +629 -0
- package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
- package/vendor/fastctx/src/shell/bash.rs +263 -0
- package/vendor/fastctx/src/shell/buffer.rs +108 -0
- package/vendor/fastctx/src/shell/encoding.rs +403 -0
- package/vendor/fastctx/src/shell/foreground.rs +115 -0
- package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
- package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
- package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
- package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
- package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
- package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
- package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
- package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
- package/vendor/fastctx/src/shell/mod.rs +345 -0
- package/vendor/fastctx/src/shell/normalize.rs +389 -0
- package/vendor/fastctx/src/shell/output.rs +406 -0
- package/vendor/fastctx/src/shell/process.rs +493 -0
- package/vendor/fastctx/src/shell_server.rs +156 -0
- package/vendor/fastctx/src/skip_report.rs +83 -0
- package/vendor/fastctx/src/stdio_transport.rs +177 -0
- package/vendor/fastctx/src/tool_schema.rs +204 -0
- package/vendor/fastctx/src/traversal.rs +846 -0
- package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/zlib.txt +29 -0
|
@@ -0,0 +1,1151 @@
|
|
|
1
|
+
//! Process-wide, try-only extra CPU capacity for grep and glob.
|
|
2
|
+
|
|
3
|
+
use crate::search_parallelism::{self, MAX_SEARCH_PARALLELISM};
|
|
4
|
+
use rayon::{ThreadPool, ThreadPoolBuilder};
|
|
5
|
+
use std::fmt;
|
|
6
|
+
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
7
|
+
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
8
|
+
use std::sync::{Arc, OnceLock};
|
|
9
|
+
|
|
10
|
+
#[cfg(test)]
|
|
11
|
+
use std::collections::BTreeSet;
|
|
12
|
+
#[cfg(test)]
|
|
13
|
+
use std::sync::{Condvar, Mutex};
|
|
14
|
+
|
|
15
|
+
/// The bounded subsystem currently borrowing one extra CPU credit.
|
|
16
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
17
|
+
pub(crate) enum BurstUse {
|
|
18
|
+
SearchSpeculation,
|
|
19
|
+
TraversalExtra,
|
|
20
|
+
SortExtra,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
struct CreditState {
|
|
24
|
+
capacity: usize,
|
|
25
|
+
available: AtomicUsize,
|
|
26
|
+
#[cfg(test)]
|
|
27
|
+
ledger: OwnershipLedger,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl CreditState {
|
|
31
|
+
fn new(capacity: usize) -> Self {
|
|
32
|
+
Self {
|
|
33
|
+
capacity,
|
|
34
|
+
available: AtomicUsize::new(capacity),
|
|
35
|
+
#[cfg(test)]
|
|
36
|
+
ledger: OwnershipLedger::default(),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn try_acquire(&self) -> bool {
|
|
41
|
+
let mut available = self.available.load(Ordering::Acquire);
|
|
42
|
+
loop {
|
|
43
|
+
if available == 0 {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
match self.available.compare_exchange_weak(
|
|
47
|
+
available,
|
|
48
|
+
available - 1,
|
|
49
|
+
Ordering::AcqRel,
|
|
50
|
+
Ordering::Acquire,
|
|
51
|
+
) {
|
|
52
|
+
Ok(_) => return true,
|
|
53
|
+
Err(observed) => available = observed,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn release(&self) {
|
|
59
|
+
let previous = self.available.fetch_add(1, Ordering::AcqRel);
|
|
60
|
+
debug_assert!(previous < self.capacity, "credit released more than once");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fn available(&self) -> usize {
|
|
64
|
+
self.available.load(Ordering::Acquire)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Exactly `P-1` non-waiting credits shared by search, traversal, and sorting.
|
|
69
|
+
pub(crate) struct CpuBurstCredits {
|
|
70
|
+
state: Arc<CreditState>,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
impl CpuBurstCredits {
|
|
74
|
+
fn new(capacity: usize) -> Self {
|
|
75
|
+
Self {
|
|
76
|
+
state: Arc::new(CreditState::new(capacity)),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Tries once and returns immediately when every extra CPU lane is occupied.
|
|
81
|
+
pub(crate) fn try_one(&self, use_: BurstUse) -> Option<BurstPermit> {
|
|
82
|
+
if !self.state.try_acquire() {
|
|
83
|
+
return None;
|
|
84
|
+
}
|
|
85
|
+
Some(BurstPermit {
|
|
86
|
+
state: Arc::clone(&self.state),
|
|
87
|
+
use_,
|
|
88
|
+
#[cfg(test)]
|
|
89
|
+
permit_id: self.state.ledger.allocate(),
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Acquires at most `maximum` currently free credits without registering a waiter.
|
|
94
|
+
pub(crate) fn try_up_to(&self, maximum: usize, use_: BurstUse) -> Vec<BurstPermit> {
|
|
95
|
+
let mut permits = Vec::with_capacity(maximum.min(self.state.capacity));
|
|
96
|
+
for _ in 0..maximum {
|
|
97
|
+
let Some(permit) = self.try_one(use_) else {
|
|
98
|
+
break;
|
|
99
|
+
};
|
|
100
|
+
permits.push(permit);
|
|
101
|
+
}
|
|
102
|
+
permits
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
pub(crate) fn capacity(&self) -> usize {
|
|
106
|
+
self.state.capacity
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pub(crate) fn available(&self) -> usize {
|
|
110
|
+
self.state.available()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
impl fmt::Debug for CpuBurstCredits {
|
|
115
|
+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
116
|
+
formatter
|
|
117
|
+
.debug_struct("CpuBurstCredits")
|
|
118
|
+
.field("capacity", &self.capacity())
|
|
119
|
+
.field("available", &self.available())
|
|
120
|
+
.finish()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// The sole linear owner of one shared extra CPU credit.
|
|
125
|
+
#[must_use = "dropping the permit is the only way to return its extra CPU credit"]
|
|
126
|
+
pub(crate) struct BurstPermit {
|
|
127
|
+
state: Arc<CreditState>,
|
|
128
|
+
use_: BurstUse,
|
|
129
|
+
#[cfg(test)]
|
|
130
|
+
permit_id: u64,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[cfg_attr(
|
|
134
|
+
not(test),
|
|
135
|
+
expect(
|
|
136
|
+
dead_code,
|
|
137
|
+
reason = "the ordered-window and traversal batches consume this B5 foundation"
|
|
138
|
+
)
|
|
139
|
+
)]
|
|
140
|
+
impl BurstPermit {
|
|
141
|
+
pub(crate) const fn use_(&self) -> BurstUse {
|
|
142
|
+
self.use_
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
impl fmt::Debug for BurstPermit {
|
|
147
|
+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
148
|
+
let mut debug = formatter.debug_struct("BurstPermit");
|
|
149
|
+
debug.field("use", &self.use_);
|
|
150
|
+
#[cfg(test)]
|
|
151
|
+
debug.field("permit_id", &self.permit_id);
|
|
152
|
+
debug.finish_non_exhaustive()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
impl Drop for BurstPermit {
|
|
157
|
+
fn drop(&mut self) {
|
|
158
|
+
#[cfg(test)]
|
|
159
|
+
self.state.ledger.release(self.permit_id);
|
|
160
|
+
self.state.release();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// A ready speculative value that keeps its extra CPU credit until ordered consumption.
|
|
165
|
+
pub(crate) struct BurstEnvelope<T> {
|
|
166
|
+
_permit: BurstPermit,
|
|
167
|
+
value: T,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
impl<T> BurstEnvelope<T> {
|
|
171
|
+
/// Moves a running lease's sole permit owner into a ready result.
|
|
172
|
+
pub(crate) fn new(permit: BurstPermit, value: T) -> Self {
|
|
173
|
+
Self {
|
|
174
|
+
_permit: permit,
|
|
175
|
+
value,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Releases the ready credit and returns the value to the ordered reducer.
|
|
180
|
+
pub(crate) fn into_value(self) -> T {
|
|
181
|
+
let Self { _permit, value } = self;
|
|
182
|
+
drop(_permit);
|
|
183
|
+
value
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// Exactly `P-1` non-waiting tickets for queued plus running private-pool leaves.
|
|
188
|
+
pub(crate) struct ExtraTaskTickets {
|
|
189
|
+
state: Arc<CreditState>,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
impl ExtraTaskTickets {
|
|
193
|
+
fn new(capacity: usize) -> Self {
|
|
194
|
+
Self {
|
|
195
|
+
state: Arc::new(CreditState::new(capacity)),
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// Tries once and returns immediately when the private leaf queue is full.
|
|
200
|
+
pub(crate) fn try_one(&self) -> Option<RunnerTicket> {
|
|
201
|
+
if !self.state.try_acquire() {
|
|
202
|
+
return None;
|
|
203
|
+
}
|
|
204
|
+
Some(RunnerTicket {
|
|
205
|
+
state: Arc::clone(&self.state),
|
|
206
|
+
#[cfg(test)]
|
|
207
|
+
ticket_id: self.state.ledger.allocate(),
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// Acquires at most `maximum` currently free tickets without waiting.
|
|
212
|
+
pub(crate) fn try_up_to(&self, maximum: usize) -> Vec<RunnerTicket> {
|
|
213
|
+
let mut tickets = Vec::with_capacity(maximum.min(self.state.capacity));
|
|
214
|
+
for _ in 0..maximum {
|
|
215
|
+
let Some(ticket) = self.try_one() else {
|
|
216
|
+
break;
|
|
217
|
+
};
|
|
218
|
+
tickets.push(ticket);
|
|
219
|
+
}
|
|
220
|
+
tickets
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
pub(crate) fn capacity(&self) -> usize {
|
|
224
|
+
self.state.capacity
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
pub(crate) fn available(&self) -> usize {
|
|
228
|
+
self.state.available()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
impl fmt::Debug for ExtraTaskTickets {
|
|
233
|
+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
234
|
+
formatter
|
|
235
|
+
.debug_struct("ExtraTaskTickets")
|
|
236
|
+
.field("capacity", &self.capacity())
|
|
237
|
+
.field("available", &self.available())
|
|
238
|
+
.finish()
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// A linear queue slot that lives from successful submission until leaf exit.
|
|
243
|
+
#[must_use = "the ticket must move into a submitted leaf or be dropped by the coordinator"]
|
|
244
|
+
pub(crate) struct RunnerTicket {
|
|
245
|
+
state: Arc<CreditState>,
|
|
246
|
+
#[cfg(test)]
|
|
247
|
+
ticket_id: u64,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
impl fmt::Debug for RunnerTicket {
|
|
251
|
+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
252
|
+
let mut debug = formatter.debug_struct("RunnerTicket");
|
|
253
|
+
#[cfg(test)]
|
|
254
|
+
debug.field("ticket_id", &self.ticket_id);
|
|
255
|
+
debug.finish_non_exhaustive()
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
impl Drop for RunnerTicket {
|
|
260
|
+
fn drop(&mut self) {
|
|
261
|
+
#[cfg(test)]
|
|
262
|
+
self.state.ledger.release(self.ticket_id);
|
|
263
|
+
self.state.release();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// A private, process-wide extra pool plus its two orthogonal try-only quotas.
|
|
268
|
+
pub(crate) struct GrepGlobExecutor {
|
|
269
|
+
parallelism: usize,
|
|
270
|
+
extra_threads: usize,
|
|
271
|
+
extra_pool: OnceLock<Result<ThreadPool, Arc<str>>>,
|
|
272
|
+
extra_disabled: AtomicBool,
|
|
273
|
+
burst: Arc<CpuBurstCredits>,
|
|
274
|
+
leaf_tasks: Arc<ExtraTaskTickets>,
|
|
275
|
+
#[cfg(test)]
|
|
276
|
+
probe: Arc<ExecutorProbe>,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
impl GrepGlobExecutor {
|
|
280
|
+
/// Returns the one process-wide executor shared by every production entry point.
|
|
281
|
+
pub(crate) fn shared() -> Arc<Self> {
|
|
282
|
+
static SHARED: OnceLock<Arc<GrepGlobExecutor>> = OnceLock::new();
|
|
283
|
+
Arc::clone(SHARED.get_or_init(|| Arc::new(Self::new())))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/// Uses the machine's capped file parallelism without initializing a pool.
|
|
287
|
+
pub(crate) fn new() -> Self {
|
|
288
|
+
Self::with_parallelism(search_parallelism::detected_available())
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// Builds an executor for an already validated `P`, including the request-local base lane.
|
|
292
|
+
pub(crate) fn with_parallelism(parallelism: usize) -> Self {
|
|
293
|
+
let parallelism = parallelism.clamp(1, MAX_SEARCH_PARALLELISM);
|
|
294
|
+
let extra_threads = parallelism - 1;
|
|
295
|
+
Self {
|
|
296
|
+
parallelism,
|
|
297
|
+
extra_threads,
|
|
298
|
+
extra_pool: OnceLock::new(),
|
|
299
|
+
extra_disabled: AtomicBool::new(false),
|
|
300
|
+
burst: Arc::new(CpuBurstCredits::new(extra_threads)),
|
|
301
|
+
leaf_tasks: Arc::new(ExtraTaskTickets::new(extra_threads)),
|
|
302
|
+
#[cfg(test)]
|
|
303
|
+
probe: Arc::new(ExecutorProbe::default()),
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// Returns `P`, including the coordinator's request-local base lane.
|
|
308
|
+
pub(crate) const fn parallelism(&self) -> usize {
|
|
309
|
+
self.parallelism
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/// Returns `E=P-1`, the maximum number of process-wide extra lanes.
|
|
313
|
+
pub(crate) const fn extra_capacity(&self) -> usize {
|
|
314
|
+
self.extra_threads
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// Tries to borrow one extra CPU lane without waiting.
|
|
318
|
+
pub(crate) fn try_burst(&self, use_: BurstUse) -> Option<BurstPermit> {
|
|
319
|
+
self.extra_enabled()
|
|
320
|
+
.then(|| self.burst.try_one(use_))
|
|
321
|
+
.flatten()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/// Tries to borrow up to `maximum` extra CPU lanes without waiting.
|
|
325
|
+
pub(crate) fn try_bursts(&self, maximum: usize, use_: BurstUse) -> Vec<BurstPermit> {
|
|
326
|
+
if self.extra_enabled() {
|
|
327
|
+
self.burst.try_up_to(maximum, use_)
|
|
328
|
+
} else {
|
|
329
|
+
Vec::new()
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/// Tries to reserve one queued/running leaf slot without waiting.
|
|
334
|
+
pub(crate) fn try_runner_ticket(&self) -> Option<RunnerTicket> {
|
|
335
|
+
self.extra_enabled()
|
|
336
|
+
.then(|| self.leaf_tasks.try_one())
|
|
337
|
+
.flatten()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// Tries to reserve up to `maximum` queued/running leaf slots without waiting.
|
|
341
|
+
pub(crate) fn try_runner_tickets(&self, maximum: usize) -> Vec<RunnerTicket> {
|
|
342
|
+
if self.extra_enabled() {
|
|
343
|
+
self.leaf_tasks.try_up_to(maximum)
|
|
344
|
+
} else {
|
|
345
|
+
Vec::new()
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/// Submits one already-ticketed FastCtx leaf, or returns it intact for inline work.
|
|
350
|
+
pub(crate) fn try_spawn<J>(
|
|
351
|
+
self: &Arc<Self>,
|
|
352
|
+
ticket: RunnerTicket,
|
|
353
|
+
job: J,
|
|
354
|
+
) -> Result<(), (RunnerTicket, J)>
|
|
355
|
+
where
|
|
356
|
+
J: FnOnce() + Send + 'static,
|
|
357
|
+
{
|
|
358
|
+
if !self.extra_enabled() {
|
|
359
|
+
return Err((ticket, job));
|
|
360
|
+
}
|
|
361
|
+
let pool = match self.pool() {
|
|
362
|
+
Ok(pool) => pool,
|
|
363
|
+
Err(_) => return Err((ticket, job)),
|
|
364
|
+
};
|
|
365
|
+
if !self.extra_enabled() {
|
|
366
|
+
return Err((ticket, job));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let executor = Arc::downgrade(self);
|
|
370
|
+
#[cfg(test)]
|
|
371
|
+
let probe = Arc::clone(&self.probe);
|
|
372
|
+
pool.spawn(move || {
|
|
373
|
+
let ticket = ticket;
|
|
374
|
+
#[cfg(test)]
|
|
375
|
+
probe.record_leaf_started();
|
|
376
|
+
let outcome = catch_unwind(AssertUnwindSafe(job));
|
|
377
|
+
if outcome.is_err()
|
|
378
|
+
&& let Some(executor) = executor.upgrade()
|
|
379
|
+
{
|
|
380
|
+
executor.disable_after_panic();
|
|
381
|
+
}
|
|
382
|
+
drop(ticket);
|
|
383
|
+
#[cfg(test)]
|
|
384
|
+
probe.record_leaf_finished();
|
|
385
|
+
});
|
|
386
|
+
Ok(())
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/// Submits a ticketed leaf with recoverable payload, returning every owner
|
|
390
|
+
/// intact when private-pool admission is unavailable.
|
|
391
|
+
pub(crate) fn try_spawn_with_payload<P, J>(
|
|
392
|
+
self: &Arc<Self>,
|
|
393
|
+
ticket: RunnerTicket,
|
|
394
|
+
payload: P,
|
|
395
|
+
job: J,
|
|
396
|
+
) -> Result<(), (RunnerTicket, P, J)>
|
|
397
|
+
where
|
|
398
|
+
P: Send + 'static,
|
|
399
|
+
J: FnOnce(P) + Send + 'static,
|
|
400
|
+
{
|
|
401
|
+
if !self.extra_enabled() {
|
|
402
|
+
return Err((ticket, payload, job));
|
|
403
|
+
}
|
|
404
|
+
let pool = match self.pool() {
|
|
405
|
+
Ok(pool) => pool,
|
|
406
|
+
Err(_) => return Err((ticket, payload, job)),
|
|
407
|
+
};
|
|
408
|
+
if !self.extra_enabled() {
|
|
409
|
+
return Err((ticket, payload, job));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let executor = Arc::downgrade(self);
|
|
413
|
+
#[cfg(test)]
|
|
414
|
+
let probe = Arc::clone(&self.probe);
|
|
415
|
+
pool.spawn(move || {
|
|
416
|
+
let ticket = ticket;
|
|
417
|
+
#[cfg(test)]
|
|
418
|
+
probe.record_leaf_started();
|
|
419
|
+
let outcome = catch_unwind(AssertUnwindSafe(|| job(payload)));
|
|
420
|
+
if outcome.is_err()
|
|
421
|
+
&& let Some(executor) = executor.upgrade()
|
|
422
|
+
{
|
|
423
|
+
executor.disable_after_panic();
|
|
424
|
+
}
|
|
425
|
+
drop(ticket);
|
|
426
|
+
#[cfg(test)]
|
|
427
|
+
probe.record_leaf_finished();
|
|
428
|
+
});
|
|
429
|
+
Ok(())
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
fn extra_enabled(&self) -> bool {
|
|
433
|
+
self.extra_threads > 0 && !self.extra_disabled.load(Ordering::Acquire)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
fn pool(self: &Arc<Self>) -> Result<&ThreadPool, Arc<str>> {
|
|
437
|
+
if self.extra_threads == 0 {
|
|
438
|
+
return Err(Arc::from("extra pool is disabled at P=1"));
|
|
439
|
+
}
|
|
440
|
+
let pool = self.extra_pool.get_or_init(|| self.build_pool());
|
|
441
|
+
if pool.is_err() {
|
|
442
|
+
self.extra_disabled.store(true, Ordering::Release);
|
|
443
|
+
}
|
|
444
|
+
pool.as_ref().map_err(Arc::clone)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
fn build_pool(self: &Arc<Self>) -> Result<ThreadPool, Arc<str>> {
|
|
448
|
+
#[cfg(test)]
|
|
449
|
+
{
|
|
450
|
+
self.probe.record_pool_build_attempt();
|
|
451
|
+
if self.probe.force_build_failure.load(Ordering::Acquire) {
|
|
452
|
+
return Err(Arc::from("injected private-pool build failure"));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let weak_executor = Arc::downgrade(self);
|
|
457
|
+
let builder = ThreadPoolBuilder::new()
|
|
458
|
+
.num_threads(self.extra_threads)
|
|
459
|
+
.thread_name(|index| format!("fastctx-extra-{index}"))
|
|
460
|
+
.panic_handler(move |_| {
|
|
461
|
+
if let Some(executor) = weak_executor.upgrade() {
|
|
462
|
+
executor.disable_after_panic();
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
#[cfg(test)]
|
|
466
|
+
let builder = {
|
|
467
|
+
let start_probe = Arc::clone(&self.probe);
|
|
468
|
+
let exit_probe = Arc::clone(&self.probe);
|
|
469
|
+
builder
|
|
470
|
+
.start_handler(move |_| start_probe.record_worker_started())
|
|
471
|
+
.exit_handler(move |_| exit_probe.record_worker_exited())
|
|
472
|
+
};
|
|
473
|
+
builder
|
|
474
|
+
.build()
|
|
475
|
+
.map_err(|error| Arc::<str>::from(error.to_string()))
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
fn disable_after_panic(&self) {
|
|
479
|
+
self.extra_disabled.store(true, Ordering::Release);
|
|
480
|
+
#[cfg(test)]
|
|
481
|
+
self.probe.record_leaf_panic();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
#[cfg(test)]
|
|
485
|
+
pub(crate) fn with_test_parallelism(parallelism: usize) -> Self {
|
|
486
|
+
Self::with_parallelism(parallelism)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
#[cfg(test)]
|
|
490
|
+
pub(crate) fn test_burst_available(&self) -> usize {
|
|
491
|
+
self.burst.available()
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
#[cfg(test)]
|
|
495
|
+
pub(crate) fn test_ticket_available(&self) -> usize {
|
|
496
|
+
self.leaf_tasks.available()
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
#[cfg(test)]
|
|
500
|
+
pub(crate) fn test_burst_ledger(&self) -> LedgerSnapshot {
|
|
501
|
+
self.burst.state.ledger.snapshot()
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
#[cfg(test)]
|
|
505
|
+
pub(crate) fn test_ticket_ledger(&self) -> LedgerSnapshot {
|
|
506
|
+
self.leaf_tasks.state.ledger.snapshot()
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
#[cfg(test)]
|
|
510
|
+
pub(crate) fn wait_for_test_quiescence(&self) {
|
|
511
|
+
self.probe.wait_for(|state| {
|
|
512
|
+
state.leaves_finished == state.leaves_started
|
|
513
|
+
&& self.leaf_tasks.available() == self.extra_threads
|
|
514
|
+
&& self.burst.available() == self.extra_threads
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
impl Default for GrepGlobExecutor {
|
|
520
|
+
fn default() -> Self {
|
|
521
|
+
Self::new()
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
impl fmt::Debug for GrepGlobExecutor {
|
|
526
|
+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
527
|
+
formatter
|
|
528
|
+
.debug_struct("GrepGlobExecutor")
|
|
529
|
+
.field("parallelism", &self.parallelism)
|
|
530
|
+
.field("extra_threads", &self.extra_threads)
|
|
531
|
+
.field("pool_initialized", &self.extra_pool.get().is_some())
|
|
532
|
+
.field(
|
|
533
|
+
"extra_disabled",
|
|
534
|
+
&self.extra_disabled.load(Ordering::Acquire),
|
|
535
|
+
)
|
|
536
|
+
.field("burst", &self.burst)
|
|
537
|
+
.field("leaf_tasks", &self.leaf_tasks)
|
|
538
|
+
.finish_non_exhaustive()
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
#[cfg(test)]
|
|
543
|
+
#[derive(Default)]
|
|
544
|
+
struct OwnershipLedger {
|
|
545
|
+
state: Mutex<OwnershipLedgerState>,
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
#[cfg(test)]
|
|
549
|
+
#[derive(Default)]
|
|
550
|
+
struct OwnershipLedgerState {
|
|
551
|
+
next_id: u64,
|
|
552
|
+
allocated: usize,
|
|
553
|
+
released: usize,
|
|
554
|
+
live: BTreeSet<u64>,
|
|
555
|
+
duplicate_releases: usize,
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
#[cfg(test)]
|
|
559
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
560
|
+
pub(crate) struct LedgerSnapshot {
|
|
561
|
+
pub(crate) allocated: usize,
|
|
562
|
+
pub(crate) released: usize,
|
|
563
|
+
pub(crate) live: usize,
|
|
564
|
+
pub(crate) duplicate_releases: usize,
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
#[cfg(test)]
|
|
568
|
+
impl OwnershipLedger {
|
|
569
|
+
fn allocate(&self) -> u64 {
|
|
570
|
+
let mut state = self.state.lock().unwrap();
|
|
571
|
+
state.next_id = state
|
|
572
|
+
.next_id
|
|
573
|
+
.checked_add(1)
|
|
574
|
+
.expect("test ledger id overflow");
|
|
575
|
+
let id = state.next_id;
|
|
576
|
+
state.allocated += 1;
|
|
577
|
+
assert!(state.live.insert(id));
|
|
578
|
+
id
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
fn release(&self, id: u64) {
|
|
582
|
+
let mut state = self.state.lock().unwrap();
|
|
583
|
+
if state.live.remove(&id) {
|
|
584
|
+
state.released += 1;
|
|
585
|
+
} else {
|
|
586
|
+
state.duplicate_releases += 1;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
fn snapshot(&self) -> LedgerSnapshot {
|
|
591
|
+
let state = self.state.lock().unwrap();
|
|
592
|
+
LedgerSnapshot {
|
|
593
|
+
allocated: state.allocated,
|
|
594
|
+
released: state.released,
|
|
595
|
+
live: state.live.len(),
|
|
596
|
+
duplicate_releases: state.duplicate_releases,
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
#[cfg(test)]
|
|
602
|
+
#[derive(Default)]
|
|
603
|
+
struct ExecutorProbe {
|
|
604
|
+
force_build_failure: AtomicBool,
|
|
605
|
+
state: Mutex<ExecutorProbeState>,
|
|
606
|
+
changed: Condvar,
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
#[cfg(test)]
|
|
610
|
+
#[derive(Clone, Copy, Debug, Default)]
|
|
611
|
+
struct ExecutorProbeState {
|
|
612
|
+
pool_build_attempts: usize,
|
|
613
|
+
workers_started: usize,
|
|
614
|
+
workers_exited: usize,
|
|
615
|
+
leaves_started: usize,
|
|
616
|
+
leaves_finished: usize,
|
|
617
|
+
active_leaves: usize,
|
|
618
|
+
peak_active_leaves: usize,
|
|
619
|
+
leaf_panics: usize,
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
#[cfg(test)]
|
|
623
|
+
impl ExecutorProbe {
|
|
624
|
+
fn update(&self, update: impl FnOnce(&mut ExecutorProbeState)) {
|
|
625
|
+
let mut state = self.state.lock().unwrap();
|
|
626
|
+
update(&mut state);
|
|
627
|
+
self.changed.notify_all();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
fn record_pool_build_attempt(&self) {
|
|
631
|
+
self.update(|state| state.pool_build_attempts += 1);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
fn record_worker_started(&self) {
|
|
635
|
+
self.update(|state| state.workers_started += 1);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
fn record_worker_exited(&self) {
|
|
639
|
+
self.update(|state| state.workers_exited += 1);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
fn record_leaf_started(&self) {
|
|
643
|
+
self.update(|state| {
|
|
644
|
+
state.leaves_started += 1;
|
|
645
|
+
state.active_leaves += 1;
|
|
646
|
+
state.peak_active_leaves = state.peak_active_leaves.max(state.active_leaves);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
fn record_leaf_finished(&self) {
|
|
651
|
+
self.update(|state| {
|
|
652
|
+
debug_assert!(state.active_leaves > 0);
|
|
653
|
+
state.active_leaves -= 1;
|
|
654
|
+
state.leaves_finished += 1;
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
fn record_leaf_panic(&self) {
|
|
659
|
+
self.update(|state| state.leaf_panics += 1);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
fn snapshot(&self) -> ExecutorProbeState {
|
|
663
|
+
*self.state.lock().unwrap()
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
fn wait_for(&self, predicate: impl Fn(ExecutorProbeState) -> bool) -> ExecutorProbeState {
|
|
667
|
+
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
|
|
668
|
+
let mut state = self.state.lock().unwrap();
|
|
669
|
+
while !predicate(*state) {
|
|
670
|
+
let remaining = deadline
|
|
671
|
+
.checked_duration_since(std::time::Instant::now())
|
|
672
|
+
.expect("executor probe timed out");
|
|
673
|
+
let (next, timeout) = self.changed.wait_timeout(state, remaining).unwrap();
|
|
674
|
+
state = next;
|
|
675
|
+
assert!(!timeout.timed_out(), "executor probe timed out: {state:?}");
|
|
676
|
+
}
|
|
677
|
+
*state
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
#[cfg(test)]
|
|
682
|
+
mod tests {
|
|
683
|
+
use super::{BurstUse, GrepGlobExecutor, LedgerSnapshot, MAX_SEARCH_PARALLELISM};
|
|
684
|
+
use std::collections::BTreeSet;
|
|
685
|
+
use std::process::{Command, Stdio};
|
|
686
|
+
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
687
|
+
use std::sync::{Arc, Barrier, Condvar, Mutex, mpsc};
|
|
688
|
+
use std::time::Duration;
|
|
689
|
+
|
|
690
|
+
fn executor(parallelism: usize) -> Arc<GrepGlobExecutor> {
|
|
691
|
+
Arc::new(GrepGlobExecutor::with_parallelism(parallelism))
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
fn run_inline_base_work(values: &[usize]) -> usize {
|
|
695
|
+
values.iter().sum()
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
fn released(allocated: usize) -> LedgerSnapshot {
|
|
699
|
+
LedgerSnapshot {
|
|
700
|
+
allocated,
|
|
701
|
+
released: allocated,
|
|
702
|
+
live: 0,
|
|
703
|
+
duplicate_releases: 0,
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
struct OpenGateOnDrop {
|
|
708
|
+
gate: Arc<(Mutex<bool>, Condvar)>,
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
impl OpenGateOnDrop {
|
|
712
|
+
fn new() -> Self {
|
|
713
|
+
Self {
|
|
714
|
+
gate: Arc::new((Mutex::new(false), Condvar::new())),
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
fn clone_gate(&self) -> Arc<(Mutex<bool>, Condvar)> {
|
|
719
|
+
Arc::clone(&self.gate)
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
impl Drop for OpenGateOnDrop {
|
|
724
|
+
fn drop(&mut self) {
|
|
725
|
+
let (lock, changed) = &*self.gate;
|
|
726
|
+
*lock.lock().unwrap() = true;
|
|
727
|
+
changed.notify_all();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
fn wait_for_open_gate(gate: &Arc<(Mutex<bool>, Condvar)>) {
|
|
732
|
+
let (lock, changed) = &**gate;
|
|
733
|
+
let mut released = lock.lock().unwrap();
|
|
734
|
+
while !*released {
|
|
735
|
+
released = changed.wait(released).unwrap();
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
#[test]
|
|
740
|
+
fn p1_has_no_extra_capacity_and_never_initializes_rayon() {
|
|
741
|
+
let executor = executor(1);
|
|
742
|
+
assert_eq!(executor.parallelism(), 1);
|
|
743
|
+
assert_eq!(executor.extra_capacity(), 0);
|
|
744
|
+
assert!(executor.try_burst(BurstUse::SearchSpeculation).is_none());
|
|
745
|
+
assert!(executor.try_runner_ticket().is_none());
|
|
746
|
+
assert!(executor.try_bursts(10, BurstUse::TraversalExtra).is_empty());
|
|
747
|
+
assert!(executor.try_runner_tickets(10).is_empty());
|
|
748
|
+
assert!(executor.extra_pool.get().is_none());
|
|
749
|
+
assert_eq!(executor.probe.snapshot().pool_build_attempts, 0);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
#[test]
|
|
753
|
+
fn credits_and_tickets_are_bounded_try_only_linear_owners() {
|
|
754
|
+
let executor = executor(4);
|
|
755
|
+
let mut permits = executor.try_bursts(99, BurstUse::SortExtra);
|
|
756
|
+
let mut tickets = executor.try_runner_tickets(99);
|
|
757
|
+
assert_eq!(permits.len(), 3);
|
|
758
|
+
assert_eq!(tickets.len(), 3);
|
|
759
|
+
assert_eq!(executor.burst.available(), 0);
|
|
760
|
+
assert_eq!(executor.leaf_tasks.available(), 0);
|
|
761
|
+
assert!(executor.try_burst(BurstUse::TraversalExtra).is_none());
|
|
762
|
+
assert!(executor.try_runner_ticket().is_none());
|
|
763
|
+
assert!(
|
|
764
|
+
permits
|
|
765
|
+
.iter()
|
|
766
|
+
.all(|permit| permit.use_() == BurstUse::SortExtra)
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
drop(permits.pop());
|
|
770
|
+
drop(tickets.pop());
|
|
771
|
+
permits.push(executor.try_burst(BurstUse::TraversalExtra).unwrap());
|
|
772
|
+
tickets.push(executor.try_runner_ticket().unwrap());
|
|
773
|
+
assert_eq!(executor.burst.available(), 0);
|
|
774
|
+
assert_eq!(executor.leaf_tasks.available(), 0);
|
|
775
|
+
drop(permits);
|
|
776
|
+
drop(tickets);
|
|
777
|
+
|
|
778
|
+
assert_eq!(executor.burst.available(), 3);
|
|
779
|
+
assert_eq!(executor.leaf_tasks.available(), 3);
|
|
780
|
+
assert_eq!(executor.burst.state.ledger.snapshot(), released(4));
|
|
781
|
+
assert_eq!(executor.leaf_tasks.state.ledger.snapshot(), released(4));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
#[test]
|
|
785
|
+
fn configured_p_bounds_real_workers_credits_and_one_request_base_lane() {
|
|
786
|
+
let middle = (MAX_SEARCH_PARALLELISM / 2).max(1);
|
|
787
|
+
for parallelism in [1, middle, MAX_SEARCH_PARALLELISM] {
|
|
788
|
+
let extras = parallelism - 1;
|
|
789
|
+
let executor = executor(parallelism);
|
|
790
|
+
let release = OpenGateOnDrop::new();
|
|
791
|
+
let permits = executor.try_bursts(parallelism + 7, BurstUse::SearchSpeculation);
|
|
792
|
+
let tickets = executor.try_runner_tickets(parallelism + 7);
|
|
793
|
+
|
|
794
|
+
assert_eq!(executor.parallelism(), parallelism);
|
|
795
|
+
assert_eq!(executor.extra_capacity(), extras);
|
|
796
|
+
assert_eq!(permits.len(), extras);
|
|
797
|
+
assert_eq!(tickets.len(), extras);
|
|
798
|
+
assert!(executor.try_burst(BurstUse::TraversalExtra).is_none());
|
|
799
|
+
assert!(executor.try_runner_ticket().is_none());
|
|
800
|
+
|
|
801
|
+
for (ticket, permit) in tickets.into_iter().zip(permits) {
|
|
802
|
+
let gate = release.clone_gate();
|
|
803
|
+
assert!(
|
|
804
|
+
executor
|
|
805
|
+
.try_spawn(ticket, move || {
|
|
806
|
+
let _permit = permit;
|
|
807
|
+
wait_for_open_gate(&gate);
|
|
808
|
+
})
|
|
809
|
+
.is_ok()
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if parallelism == 1 {
|
|
814
|
+
assert!(executor.extra_pool.get().is_none());
|
|
815
|
+
assert_eq!(executor.probe.snapshot().pool_build_attempts, 0);
|
|
816
|
+
} else {
|
|
817
|
+
let held = executor.probe.wait_for(|state| {
|
|
818
|
+
state.workers_started == extras && state.active_leaves == extras
|
|
819
|
+
});
|
|
820
|
+
assert_eq!(held.pool_build_attempts, 1);
|
|
821
|
+
assert_eq!(held.workers_started, extras);
|
|
822
|
+
assert_eq!(held.peak_active_leaves, extras);
|
|
823
|
+
assert_eq!(1 + held.active_leaves, parallelism);
|
|
824
|
+
assert_eq!(executor.test_ticket_ledger().live, extras);
|
|
825
|
+
assert_eq!(executor.test_burst_ledger().live, extras);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
drop(release);
|
|
829
|
+
executor.wait_for_test_quiescence();
|
|
830
|
+
assert_eq!(executor.test_ticket_ledger(), released(extras));
|
|
831
|
+
assert_eq!(executor.test_burst_ledger(), released(extras));
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
#[test]
|
|
836
|
+
fn concurrent_credit_attempts_never_cross_the_shared_capacity() {
|
|
837
|
+
let executor = executor(4);
|
|
838
|
+
let contenders = 32;
|
|
839
|
+
let acquired = Arc::new(Barrier::new(contenders + 1));
|
|
840
|
+
let release = Arc::new(Barrier::new(contenders + 1));
|
|
841
|
+
let holders = Arc::new(AtomicUsize::new(0));
|
|
842
|
+
let peak = Arc::new(AtomicUsize::new(0));
|
|
843
|
+
let mut threads = Vec::new();
|
|
844
|
+
for _ in 0..contenders {
|
|
845
|
+
let executor = Arc::clone(&executor);
|
|
846
|
+
let acquired = Arc::clone(&acquired);
|
|
847
|
+
let release = Arc::clone(&release);
|
|
848
|
+
let holders = Arc::clone(&holders);
|
|
849
|
+
let peak = Arc::clone(&peak);
|
|
850
|
+
threads.push(std::thread::spawn(move || {
|
|
851
|
+
let permit = executor.try_burst(BurstUse::SearchSpeculation);
|
|
852
|
+
if permit.is_some() {
|
|
853
|
+
let current = holders.fetch_add(1, Ordering::AcqRel) + 1;
|
|
854
|
+
peak.fetch_max(current, Ordering::AcqRel);
|
|
855
|
+
}
|
|
856
|
+
acquired.wait();
|
|
857
|
+
release.wait();
|
|
858
|
+
if permit.is_some() {
|
|
859
|
+
holders.fetch_sub(1, Ordering::AcqRel);
|
|
860
|
+
}
|
|
861
|
+
drop(permit);
|
|
862
|
+
}));
|
|
863
|
+
}
|
|
864
|
+
acquired.wait();
|
|
865
|
+
assert_eq!(holders.load(Ordering::Acquire), 3);
|
|
866
|
+
assert_eq!(peak.load(Ordering::Acquire), 3);
|
|
867
|
+
release.wait();
|
|
868
|
+
for thread in threads {
|
|
869
|
+
thread.join().unwrap();
|
|
870
|
+
}
|
|
871
|
+
assert_eq!(executor.burst.available(), 3);
|
|
872
|
+
assert_eq!(executor.burst.state.ledger.snapshot(), released(3));
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
#[test]
|
|
876
|
+
fn build_failure_is_latched_and_returns_the_job_for_inline_progress() {
|
|
877
|
+
let executor = executor(4);
|
|
878
|
+
executor
|
|
879
|
+
.probe
|
|
880
|
+
.force_build_failure
|
|
881
|
+
.store(true, Ordering::Release);
|
|
882
|
+
let ran_inline = Arc::new(AtomicBool::new(false));
|
|
883
|
+
let ran_inline_job = Arc::clone(&ran_inline);
|
|
884
|
+
let ticket = executor.try_runner_ticket().unwrap();
|
|
885
|
+
let (ticket, job) = executor
|
|
886
|
+
.try_spawn(ticket, move || {
|
|
887
|
+
ran_inline_job.store(true, Ordering::Release);
|
|
888
|
+
})
|
|
889
|
+
.expect_err("the injected pool build must return the leaf intact");
|
|
890
|
+
assert!(!ran_inline.load(Ordering::Acquire));
|
|
891
|
+
drop(ticket);
|
|
892
|
+
job();
|
|
893
|
+
assert!(ran_inline.load(Ordering::Acquire));
|
|
894
|
+
assert!(executor.extra_disabled.load(Ordering::Acquire));
|
|
895
|
+
assert!(executor.extra_pool.get().unwrap().is_err());
|
|
896
|
+
assert_eq!(executor.probe.snapshot().pool_build_attempts, 1);
|
|
897
|
+
assert!(executor.try_runner_ticket().is_none());
|
|
898
|
+
assert!(executor.try_burst(BurstUse::TraversalExtra).is_none());
|
|
899
|
+
assert_eq!(executor.leaf_tasks.state.ledger.snapshot(), released(1));
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
#[test]
|
|
903
|
+
fn leaf_panic_returns_its_ticket_and_disables_later_extra_admission() {
|
|
904
|
+
let executor = executor(3);
|
|
905
|
+
let ticket = executor.try_runner_ticket().unwrap();
|
|
906
|
+
assert!(
|
|
907
|
+
executor
|
|
908
|
+
.try_spawn(ticket, || panic!("injected leaf panic"))
|
|
909
|
+
.is_ok(),
|
|
910
|
+
"the private pool must accept the leaf"
|
|
911
|
+
);
|
|
912
|
+
let state = executor.probe.wait_for(|state| state.leaves_finished == 1);
|
|
913
|
+
assert_eq!(state.leaf_panics, 1);
|
|
914
|
+
assert!(executor.extra_disabled.load(Ordering::Acquire));
|
|
915
|
+
assert!(executor.try_runner_ticket().is_none());
|
|
916
|
+
assert!(executor.try_burst(BurstUse::SearchSpeculation).is_none());
|
|
917
|
+
assert_eq!(executor.leaf_tasks.available(), 2);
|
|
918
|
+
assert_eq!(executor.leaf_tasks.state.ledger.snapshot(), released(1));
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
#[test]
|
|
922
|
+
fn saturated_private_pool_does_not_gate_inline_base_work() {
|
|
923
|
+
let executor = executor(3);
|
|
924
|
+
let permits = executor.try_bursts(2, BurstUse::SearchSpeculation);
|
|
925
|
+
let gate = Arc::new((Mutex::new(false), Condvar::new()));
|
|
926
|
+
let (started_tx, started_rx) = mpsc::channel();
|
|
927
|
+
for ticket in executor.try_runner_tickets(2) {
|
|
928
|
+
let gate = Arc::clone(&gate);
|
|
929
|
+
let started_tx = started_tx.clone();
|
|
930
|
+
assert!(
|
|
931
|
+
executor
|
|
932
|
+
.try_spawn(ticket, move || {
|
|
933
|
+
started_tx.send(()).unwrap();
|
|
934
|
+
let (lock, changed) = &*gate;
|
|
935
|
+
let mut released = lock.lock().unwrap();
|
|
936
|
+
while !*released {
|
|
937
|
+
released = changed.wait(released).unwrap();
|
|
938
|
+
}
|
|
939
|
+
})
|
|
940
|
+
.is_ok()
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
drop(started_tx);
|
|
944
|
+
for _ in 0..2 {
|
|
945
|
+
started_rx.recv_timeout(Duration::from_secs(3)).unwrap();
|
|
946
|
+
}
|
|
947
|
+
assert!(executor.try_burst(BurstUse::SortExtra).is_none());
|
|
948
|
+
assert!(executor.try_runner_ticket().is_none());
|
|
949
|
+
|
|
950
|
+
assert_eq!(run_inline_base_work(&[1, 2, 3, 4]), 10);
|
|
951
|
+
let (lock, changed) = &*gate;
|
|
952
|
+
*lock.lock().unwrap() = true;
|
|
953
|
+
changed.notify_all();
|
|
954
|
+
executor.probe.wait_for(|state| state.leaves_finished == 2);
|
|
955
|
+
drop(permits);
|
|
956
|
+
assert_eq!(executor.burst.state.ledger.snapshot(), released(2));
|
|
957
|
+
assert_eq!(executor.leaf_tasks.state.ledger.snapshot(), released(2));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
#[test]
|
|
961
|
+
fn eight_request_base_lanes_share_constant_extras_and_later_small_progresses() {
|
|
962
|
+
const REQUESTS: usize = 8;
|
|
963
|
+
const PARALLELISM: usize = 4;
|
|
964
|
+
const EXTRAS: usize = PARALLELISM - 1;
|
|
965
|
+
|
|
966
|
+
let executor = executor(PARALLELISM);
|
|
967
|
+
let release = OpenGateOnDrop::new();
|
|
968
|
+
let base_active = Arc::new(AtomicUsize::new(0));
|
|
969
|
+
let peak_base_active = Arc::new(AtomicUsize::new(0));
|
|
970
|
+
let accepted_leaves = Arc::new(AtomicUsize::new(0));
|
|
971
|
+
let (ready_tx, ready_rx) = mpsc::channel();
|
|
972
|
+
let mut requests = Vec::new();
|
|
973
|
+
|
|
974
|
+
for request_id in 0..REQUESTS {
|
|
975
|
+
let executor_for_request = Arc::clone(&executor);
|
|
976
|
+
let gate_for_request = release.clone_gate();
|
|
977
|
+
let base_active_for_request = Arc::clone(&base_active);
|
|
978
|
+
let peak_base_for_request = Arc::clone(&peak_base_active);
|
|
979
|
+
let accepted_for_request = Arc::clone(&accepted_leaves);
|
|
980
|
+
let ready_for_request = ready_tx.clone();
|
|
981
|
+
requests.push(std::thread::spawn(move || {
|
|
982
|
+
let active = base_active_for_request.fetch_add(1, Ordering::AcqRel) + 1;
|
|
983
|
+
peak_base_for_request.fetch_max(active, Ordering::AcqRel);
|
|
984
|
+
|
|
985
|
+
if let Some(ticket) = executor_for_request.try_runner_ticket() {
|
|
986
|
+
if let Some(permit) =
|
|
987
|
+
executor_for_request.try_burst(BurstUse::SearchSpeculation)
|
|
988
|
+
{
|
|
989
|
+
let gate_for_leaf = Arc::clone(&gate_for_request);
|
|
990
|
+
let spawned = executor_for_request
|
|
991
|
+
.try_spawn(ticket, move || {
|
|
992
|
+
let _permit = permit;
|
|
993
|
+
wait_for_open_gate(&gate_for_leaf);
|
|
994
|
+
})
|
|
995
|
+
.is_ok();
|
|
996
|
+
assert!(
|
|
997
|
+
spawned,
|
|
998
|
+
"the shared private pool must accept a ticketed leaf"
|
|
999
|
+
);
|
|
1000
|
+
accepted_for_request.fetch_add(1, Ordering::AcqRel);
|
|
1001
|
+
} else {
|
|
1002
|
+
drop(ticket);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
ready_for_request.send(request_id).unwrap();
|
|
1007
|
+
wait_for_open_gate(&gate_for_request);
|
|
1008
|
+
base_active_for_request.fetch_sub(1, Ordering::AcqRel);
|
|
1009
|
+
}));
|
|
1010
|
+
}
|
|
1011
|
+
drop(ready_tx);
|
|
1012
|
+
let ready = (0..REQUESTS)
|
|
1013
|
+
.map(|_| ready_rx.recv_timeout(Duration::from_secs(5)).unwrap())
|
|
1014
|
+
.collect::<BTreeSet<_>>();
|
|
1015
|
+
assert_eq!(ready, (0..REQUESTS).collect());
|
|
1016
|
+
let held = executor
|
|
1017
|
+
.probe
|
|
1018
|
+
.wait_for(|state| state.workers_started == EXTRAS && state.active_leaves == EXTRAS);
|
|
1019
|
+
let held_tickets = executor.test_ticket_ledger();
|
|
1020
|
+
let held_bursts = executor.test_burst_ledger();
|
|
1021
|
+
let held_base = base_active.load(Ordering::Acquire);
|
|
1022
|
+
let later_small = run_inline_base_work(&[1, 2, 3, 4]);
|
|
1023
|
+
|
|
1024
|
+
drop(release);
|
|
1025
|
+
for request in requests {
|
|
1026
|
+
request.join().unwrap();
|
|
1027
|
+
}
|
|
1028
|
+
executor.wait_for_test_quiescence();
|
|
1029
|
+
|
|
1030
|
+
assert_eq!(accepted_leaves.load(Ordering::Acquire), EXTRAS);
|
|
1031
|
+
assert_eq!(held.pool_build_attempts, 1);
|
|
1032
|
+
assert_eq!(held.workers_started, EXTRAS);
|
|
1033
|
+
assert_eq!(held.active_leaves, EXTRAS);
|
|
1034
|
+
assert_eq!(held.peak_active_leaves, EXTRAS);
|
|
1035
|
+
assert_eq!(held_tickets.live, EXTRAS);
|
|
1036
|
+
assert_eq!(held_bursts.live, EXTRAS);
|
|
1037
|
+
assert_eq!(held_base, REQUESTS);
|
|
1038
|
+
assert_eq!(peak_base_active.load(Ordering::Acquire), REQUESTS);
|
|
1039
|
+
assert!(held_base + held.active_leaves <= REQUESTS + EXTRAS);
|
|
1040
|
+
assert_eq!(later_small, 10);
|
|
1041
|
+
assert_eq!(base_active.load(Ordering::Acquire), 0);
|
|
1042
|
+
assert_eq!(executor.test_ticket_ledger(), released(EXTRAS));
|
|
1043
|
+
assert_eq!(executor.test_burst_ledger(), released(EXTRAS));
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
#[test]
|
|
1047
|
+
fn private_workers_exit_after_the_last_executor_owner_is_dropped() {
|
|
1048
|
+
let executor = executor(4);
|
|
1049
|
+
let probe = Arc::clone(&executor.probe);
|
|
1050
|
+
let ticket = executor.try_runner_ticket().unwrap();
|
|
1051
|
+
assert!(executor.try_spawn(ticket, || {}).is_ok());
|
|
1052
|
+
probe.wait_for(|state| state.workers_started == 3 && state.leaves_finished == 1);
|
|
1053
|
+
drop(executor);
|
|
1054
|
+
let state = probe.wait_for(|state| state.workers_exited == 3);
|
|
1055
|
+
assert_eq!(state.pool_build_attempts, 1);
|
|
1056
|
+
assert_eq!(state.workers_started, 3);
|
|
1057
|
+
assert_eq!(state.workers_exited, 3);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
#[test]
|
|
1061
|
+
fn private_pool_size_ignores_rayon_num_threads() {
|
|
1062
|
+
const CHILD_MARKER: &str = "FASTCTX_TEST_RAYON_ENV_CHILD";
|
|
1063
|
+
if std::env::var_os(CHILD_MARKER).is_some() {
|
|
1064
|
+
assert_private_pool_has_three_overlapping_workers();
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
let mut child = Command::new(std::env::current_exe().unwrap());
|
|
1069
|
+
child
|
|
1070
|
+
.arg("--exact")
|
|
1071
|
+
.arg("file_executor::tests::private_pool_size_ignores_rayon_num_threads")
|
|
1072
|
+
.arg("--nocapture")
|
|
1073
|
+
.env(CHILD_MARKER, "1")
|
|
1074
|
+
.env("RAYON_NUM_THREADS", "1")
|
|
1075
|
+
.stdout(Stdio::piped())
|
|
1076
|
+
.stderr(Stdio::piped());
|
|
1077
|
+
let mut child = child.spawn().unwrap();
|
|
1078
|
+
let deadline = std::time::Instant::now() + Duration::from_secs(10);
|
|
1079
|
+
loop {
|
|
1080
|
+
if child.try_wait().unwrap().is_some() {
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
if std::time::Instant::now() >= deadline {
|
|
1084
|
+
child.kill().unwrap();
|
|
1085
|
+
let output = child.wait_with_output().unwrap();
|
|
1086
|
+
panic!(
|
|
1087
|
+
"RAYON_NUM_THREADS child exceeded its hard deadline\nstdout:\n{}\nstderr:\n{}",
|
|
1088
|
+
String::from_utf8_lossy(&output.stdout),
|
|
1089
|
+
String::from_utf8_lossy(&output.stderr)
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
std::thread::sleep(Duration::from_millis(10));
|
|
1093
|
+
}
|
|
1094
|
+
let output = child.wait_with_output().unwrap();
|
|
1095
|
+
assert!(
|
|
1096
|
+
output.status.success(),
|
|
1097
|
+
"child failed\nstdout:\n{}\nstderr:\n{}",
|
|
1098
|
+
String::from_utf8_lossy(&output.stdout),
|
|
1099
|
+
String::from_utf8_lossy(&output.stderr)
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
fn assert_private_pool_has_three_overlapping_workers() {
|
|
1104
|
+
let executor = executor(4);
|
|
1105
|
+
let gate = Arc::new((Mutex::new(false), Condvar::new()));
|
|
1106
|
+
let (started_tx, started_rx) = mpsc::channel();
|
|
1107
|
+
for ticket in executor.try_runner_tickets(3) {
|
|
1108
|
+
let gate = Arc::clone(&gate);
|
|
1109
|
+
let started_tx = started_tx.clone();
|
|
1110
|
+
assert!(
|
|
1111
|
+
executor
|
|
1112
|
+
.try_spawn(ticket, move || {
|
|
1113
|
+
started_tx
|
|
1114
|
+
.send(rayon::current_thread_index().unwrap())
|
|
1115
|
+
.unwrap();
|
|
1116
|
+
let (lock, changed) = &*gate;
|
|
1117
|
+
let mut released = lock.lock().unwrap();
|
|
1118
|
+
while !*released {
|
|
1119
|
+
released = changed.wait(released).unwrap();
|
|
1120
|
+
}
|
|
1121
|
+
})
|
|
1122
|
+
.is_ok()
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
drop(started_tx);
|
|
1126
|
+
|
|
1127
|
+
let mut indices = BTreeSet::new();
|
|
1128
|
+
let mut receive_failure = None;
|
|
1129
|
+
for _ in 0..3 {
|
|
1130
|
+
match started_rx.recv_timeout(Duration::from_secs(3)) {
|
|
1131
|
+
Ok(index) => {
|
|
1132
|
+
indices.insert(index);
|
|
1133
|
+
}
|
|
1134
|
+
Err(error) => {
|
|
1135
|
+
receive_failure = Some(error);
|
|
1136
|
+
break;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
let (lock, changed) = &*gate;
|
|
1141
|
+
*lock.lock().unwrap() = true;
|
|
1142
|
+
changed.notify_all();
|
|
1143
|
+
executor.probe.wait_for(|state| state.leaves_finished == 3);
|
|
1144
|
+
assert!(
|
|
1145
|
+
receive_failure.is_none(),
|
|
1146
|
+
"private workers did not overlap: {receive_failure:?}"
|
|
1147
|
+
);
|
|
1148
|
+
assert_eq!(indices, BTreeSet::from([0, 1, 2]));
|
|
1149
|
+
assert_eq!(executor.probe.snapshot().workers_started, 3);
|
|
1150
|
+
}
|
|
1151
|
+
}
|