rusteerio 1.0.1 → 1.1.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.
Files changed (57) hide show
  1. package/Cargo.lock +42 -1
  2. package/Cargo.toml +8 -1
  3. package/README.md +90 -51
  4. package/THIRD_PARTY_NOTICES.md +107 -2
  5. package/dist/{chunk-DMRD3FGC.mjs → chunk-FPRP6UIX.mjs} +2233 -751
  6. package/dist/chunk-FPRP6UIX.mjs.map +1 -0
  7. package/dist/index.cjs +2232 -750
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.mts +2 -2
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.mjs +1 -1
  12. package/dist/slim.cjs +2226 -745
  13. package/dist/slim.cjs.map +1 -1
  14. package/dist/slim.d.mts +2 -2
  15. package/dist/slim.d.ts +2 -2
  16. package/dist/slim.mjs +1 -1
  17. package/dist/{types-BP6TpxE5.d.mts → types-Cs9UP1Vv.d.mts} +3 -3
  18. package/dist/{types-BP6TpxE5.d.ts → types-Cs9UP1Vv.d.ts} +3 -3
  19. package/dist/utils.d.mts +1 -1
  20. package/dist/utils.d.ts +1 -1
  21. package/native/src/arena.rs +219 -80
  22. package/native/src/attributes.rs +11 -5
  23. package/native/src/document_order.rs +711 -0
  24. package/native/src/extraction.rs +15 -3
  25. package/native/src/gc.rs +12 -3
  26. package/native/src/id_hash.rs +42 -0
  27. package/native/src/immutable.rs +256 -0
  28. package/native/src/lib.rs +1039 -171
  29. package/native/src/runtime.rs +189 -33
  30. package/native/src/selector.rs +346 -44
  31. package/native/src/sink.rs +14 -1
  32. package/native/src/snapshot.rs +527 -0
  33. package/native/src/stream.rs +2 -2
  34. package/native/src/tokenizer.rs +29 -12
  35. package/native/vendor/html5ever-0.35.0/src/tokenizer/char_ref/mod.rs +79 -0
  36. package/package.json +3 -1
  37. package/rusteerio.darwin-arm64.node +0 -0
  38. package/rusteerio.darwin-x64.node +0 -0
  39. package/rusteerio.linux-arm64-gnu.node +0 -0
  40. package/rusteerio.linux-arm64-musl.node +0 -0
  41. package/rusteerio.linux-x64-gnu.node +0 -0
  42. package/rusteerio.linux-x64-musl.node +0 -0
  43. package/rusteerio.win32-arm64-msvc.node +0 -0
  44. package/rusteerio.win32-x64-msvc.node +0 -0
  45. package/src/attributes.ts +78 -44
  46. package/src/codec.ts +22 -3
  47. package/src/extract.ts +86 -2
  48. package/src/hooks.ts +148 -2
  49. package/src/index.ts +51 -40
  50. package/src/match.ts +207 -0
  51. package/src/native.ts +9 -1
  52. package/src/nodes.ts +531 -183
  53. package/src/router.ts +792 -229
  54. package/src/selection.ts +239 -125
  55. package/src/snapshot.ts +255 -0
  56. package/src/transport.ts +51 -42
  57. package/dist/chunk-DMRD3FGC.mjs.map +0 -1
@@ -2,8 +2,8 @@
2
2
  use napi::{Error, Result, Status};
3
3
  use napi_derive::napi;
4
4
  use rayon::prelude::*;
5
- use std::sync::atomic::{AtomicU32, Ordering};
6
- use std::sync::{Arc, Condvar, Mutex, OnceLock};
5
+ use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
6
+ use std::sync::{mpsc, Arc, Condvar, Mutex, OnceLock};
7
7
 
8
8
  struct Configuration {
9
9
  threads: usize,
@@ -81,6 +81,10 @@ pub fn configure_worker_pool(threads: u32) -> Result<WorkerPoolInfo> {
81
81
  "Configure the worker budget before starting native asynchronous work",
82
82
  ));
83
83
  }
84
+ if config.threads != threads as usize {
85
+ // A pool that only internal work has created yet is rebuilt at the new size.
86
+ config.pool = None;
87
+ }
84
88
  config.threads = threads as usize;
85
89
  config.error = None;
86
90
  }
@@ -114,6 +118,144 @@ impl Drop for Permit {
114
118
  }
115
119
  }
116
120
 
121
+ /// The process-wide worker pool, created on first use with the configured size.
122
+ fn shared_pool() -> Result<Arc<rayon::ThreadPool>> {
123
+ pool(true)
124
+ }
125
+
126
+ /// The pool for work JavaScript did not ask for, which must not fix the
127
+ /// configured budget: the caller may configure it later.
128
+ fn internal_pool() -> Result<Arc<rayon::ThreadPool>> {
129
+ pool(false)
130
+ }
131
+
132
+ fn pool(is_requested: bool) -> Result<Arc<rayon::ThreadPool>> {
133
+ let mut config = runtime().configuration.lock().unwrap();
134
+ if let Some(error) = &config.error {
135
+ return Err(Error::new(Status::InvalidArg, error.clone()));
136
+ }
137
+ config.started |= is_requested;
138
+ if config.pool.is_none() {
139
+ config.pool = Some(Arc::new(
140
+ rayon::ThreadPoolBuilder::new()
141
+ .num_threads(config.threads)
142
+ .build()
143
+ .map_err(|error| Error::new(Status::GenericFailure, error.to_string()))?,
144
+ ));
145
+ }
146
+ Ok(config.pool.as_ref().unwrap().clone())
147
+ }
148
+
149
+ /// Values the pool has yet to drop. Past the limit they drop here, so busy
150
+ /// workers cannot let freed memory pile up.
151
+ static PENDING_RELEASES: AtomicU32 = AtomicU32::new(0);
152
+ const MAXIMUM_PENDING_RELEASES: u32 = 2;
153
+
154
+ /// Drops `value` on the shared pool or the release thread if either exists
155
+ /// and is keeping up, or here. It never starts either: finalizers call it
156
+ /// during garbage collection.
157
+ pub(crate) fn release_later<T: Send + 'static>(value: T) {
158
+ if PENDING_RELEASES.load(Ordering::Relaxed) >= MAXIMUM_PENDING_RELEASES {
159
+ return drop(value);
160
+ }
161
+ let pool = runtime().configuration.lock().unwrap().pool.clone();
162
+ if let Some(pool) = pool {
163
+ PENDING_RELEASES.fetch_add(1, Ordering::Relaxed);
164
+ return pool.spawn(move || {
165
+ drop(value);
166
+ PENDING_RELEASES.fetch_sub(1, Ordering::Relaxed);
167
+ });
168
+ }
169
+ if let Some(release_thread) = RELEASE_THREAD.get() {
170
+ PENDING_RELEASES.fetch_add(1, Ordering::Relaxed);
171
+ if let Err(unsent) = release_thread.send(Box::new(value)) {
172
+ PENDING_RELEASES.fetch_sub(1, Ordering::Relaxed);
173
+ drop(unsent);
174
+ }
175
+ return;
176
+ }
177
+ drop(value)
178
+ }
179
+
180
+ static RELEASE_THREAD: OnceLock<mpsc::Sender<Box<dyn Send>>> = OnceLock::new();
181
+
182
+ /// Starts the thread that frees released documents while no worker pool
183
+ /// exists, so freeing large trees does not stall JavaScript. Documents large
184
+ /// enough to be released later start it when they are created, because
185
+ /// finalizers must not create threads.
186
+ pub(crate) fn start_release_thread() {
187
+ RELEASE_THREAD.get_or_init(|| {
188
+ let (sender, receiver) = mpsc::channel::<Box<dyn Send>>();
189
+ // A thread that cannot start drops its receiver; sends then fail and
190
+ // values drop on the caller.
191
+ let _ = std::thread::Builder::new()
192
+ .name("rusteerio-release".into())
193
+ .spawn(move || {
194
+ for value in receiver {
195
+ drop(value);
196
+ PENDING_RELEASES.fetch_sub(1, Ordering::Relaxed);
197
+ }
198
+ });
199
+ sender
200
+ });
201
+ }
202
+
203
+ /// Permits for up to `wanted` workers that are free now, without waiting.
204
+ fn available_permits(wanted: usize) -> Vec<Permit> {
205
+ let shared = runtime();
206
+ let limit = {
207
+ let config = shared.configuration.lock().unwrap();
208
+ if config.error.is_some() {
209
+ return Vec::new();
210
+ }
211
+ config.threads as u32
212
+ };
213
+ let mut used = shared.used.lock().unwrap();
214
+ let count = (limit.saturating_sub(*used) as usize).min(wanted);
215
+ *used += count as u32;
216
+ shared.peak.fetch_max(*used, Ordering::Relaxed);
217
+ (0..count).map(|_| Permit).collect()
218
+ }
219
+
220
+ /// Maps chunks of `0..count` in order. It uses only the shared pool's workers
221
+ /// that are idle now, so work on the JavaScript thread never waits on other
222
+ /// batches; with none idle, or little work, it runs on the calling thread.
223
+ pub(crate) fn chunked_map<U: Send>(
224
+ count: usize,
225
+ chunk: usize,
226
+ operation: impl Fn(std::ops::Range<usize>) -> Vec<U> + Sync + Send,
227
+ ) -> Vec<U> {
228
+ // A pool worker already holds its batch's permit; waiting there on jobs
229
+ // installed behind other batches' work could leave every worker waiting.
230
+ if rayon::current_thread_index().is_some() {
231
+ return operation(0..count);
232
+ }
233
+ let chunks = count.div_ceil(chunk.max(1));
234
+ let permits = if chunks > 1 {
235
+ available_permits(chunks)
236
+ } else {
237
+ Vec::new()
238
+ };
239
+ let pool = (permits.len() > 1).then(internal_pool).and_then(Result::ok);
240
+ let Some(pool) = pool else {
241
+ drop(permits);
242
+ return operation(0..count);
243
+ };
244
+ let _batch = batch();
245
+ let size = count.div_ceil(permits.len());
246
+ let ranges: Vec<_> = (0..count)
247
+ .step_by(size)
248
+ .map(|start| start..(start + size).min(count))
249
+ .collect();
250
+ let results: Vec<Vec<U>> = pool.install(|| ranges.into_par_iter().map(&operation).collect());
251
+ drop(permits);
252
+ let mut merged = Vec::with_capacity(results.iter().map(Vec::len).sum());
253
+ for chunk in results {
254
+ merged.extend(chunk);
255
+ }
256
+ merged
257
+ }
258
+
117
259
  pub(crate) struct Batch;
118
260
  pub(crate) fn batch() -> Batch {
119
261
  runtime().batches.fetch_add(1, Ordering::Relaxed);
@@ -125,44 +267,58 @@ impl Drop for Batch {
125
267
  }
126
268
  }
127
269
 
270
+ /// Maps `items` in order, on at most `threads` threads within the
271
+ /// process-wide budget. The calling thread waits for one permit and maps
272
+ /// items itself; pool workers help only with permits that are free now.
273
+ /// Pool jobs therefore never wait for permits, and a free permit always has
274
+ /// an idle worker: a worker that waited could hold the thread a permit's
275
+ /// holder needs in order to finish.
128
276
  pub(crate) fn parallel_map<T: Sync, U: Send>(
129
277
  items: &[T],
130
278
  threads: usize,
131
279
  operation: impl Fn(&T) -> Result<U> + Sync + Send,
132
280
  ) -> Result<Vec<U>> {
133
281
  let _batch = batch();
134
- let pool = {
135
- let mut config = runtime().configuration.lock().unwrap();
136
- if let Some(error) = &config.error {
137
- return Err(Error::new(Status::InvalidArg, error.clone()));
138
- }
139
- config.started = true;
140
- if config.pool.is_none() {
141
- config.pool = Some(Arc::new(
142
- rayon::ThreadPoolBuilder::new()
143
- .num_threads(config.threads)
144
- .build()
145
- .map_err(|error| Error::new(Status::GenericFailure, error.to_string()))?,
146
- ));
147
- }
148
- config.pool.as_ref().unwrap().clone()
282
+ let results: Vec<Mutex<Option<Result<U>>>> = items.iter().map(|_| Mutex::new(None)).collect();
283
+ let next = AtomicUsize::new(0);
284
+ let map_remaining = || loop {
285
+ let index = next.fetch_add(1, Ordering::Relaxed);
286
+ let Some(item) = items.get(index) else {
287
+ break;
288
+ };
289
+ *results[index].lock().unwrap() = Some(operation(item));
149
290
  };
150
- let chunk_size = items.len().div_ceil(threads.max(1)).max(1);
151
- let chunks: Result<Vec<Vec<U>>> = pool.install(|| {
152
- items
153
- .par_chunks(chunk_size)
154
- .map(|chunk| {
155
- chunk
156
- .iter()
157
- .map(|item| {
158
- let _permit = permit()?;
159
- operation(item)
160
- })
161
- .collect()
162
- })
163
- .collect()
164
- });
165
- Ok(chunks?.into_iter().flatten().collect())
291
+ // A pool worker already holds the permit of the job it runs.
292
+ let own = if rayon::current_thread_index().is_some() {
293
+ None
294
+ } else {
295
+ Some(permit()?)
296
+ };
297
+ let wanted_helpers = threads.max(1).min(items.len()).saturating_sub(1);
298
+ let helpers = if own.is_some() && wanted_helpers > 0 {
299
+ available_permits(wanted_helpers)
300
+ } else {
301
+ Vec::new()
302
+ };
303
+ if helpers.is_empty() {
304
+ map_remaining();
305
+ } else {
306
+ let pool = shared_pool()?;
307
+ pool.in_place_scope(|scope| {
308
+ for helper in helpers {
309
+ scope.spawn(|_| {
310
+ map_remaining();
311
+ drop(helper);
312
+ });
313
+ }
314
+ map_remaining();
315
+ });
316
+ }
317
+ drop(own);
318
+ results
319
+ .into_iter()
320
+ .map(|result| result.into_inner().unwrap().expect("every item ran"))
321
+ .collect()
166
322
  }
167
323
 
168
324
  #[cfg(test)]