Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/syntax_mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::Path;
use globset::{Candidate, GlobBuilder, GlobMatcher};

use crate::error::Result;
use builtin::BUILTIN_MAPPINGS;
use builtin::OffloadIter;
use ignored_suffixes::IgnoredSuffixes;

mod builtin;
Expand Down Expand Up @@ -91,8 +91,7 @@ impl<'a> SyntaxMapping<'a> {
pub fn builtin_mappings(
&self,
) -> impl Iterator<Item = (&'static GlobMatcher, &'static MappingTarget<'static>)> {
BUILTIN_MAPPINGS
.iter()
OffloadIter::new()
.filter_map(|(matcher, target)| matcher.as_ref().map(|glob| (glob, target)))
}

Expand Down
95 changes: 94 additions & 1 deletion src/syntax_mapping/builtin.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::env;
use std::{env, iter::Enumerate, slice, sync::mpsc, thread};

use globset::GlobMatcher;
use once_cell::sync::Lazy;
Expand Down Expand Up @@ -89,3 +89,96 @@ enum MatcherSegment {
Text(&'static str),
Env(&'static str),
}

/// The maximum number of offload workers.
const MAX_OFFLOAD_WORKERS: usize = 8;
/// The minimum number of built glob matchers remaining before offload workers
/// start building the next batch.
const OFFLOAD_PREEMPT_MARGIN: usize = 2;

/// An iterator over the builtin mappings that offloads glob matcher building to
/// worker threads.
#[derive(Debug)]
pub struct OffloadIter {
/// The iterator tracking the item returned by `next()`.
next_iter: Enumerate<slice::Iter<'static, (Lazy<Option<GlobMatcher>>, MappingTarget<'static>)>>,

/// The index of the next item that is to be fed to workers.
///
/// Used to determine whether the next offload batch should be triggered.
build_idx: usize,
/// The iterator tracking the next item that is to be fed to workers.
build_iter: slice::Iter<'static, (Lazy<Option<GlobMatcher>>, MappingTarget<'static>)>,

/// Control channels for worker threads.
workers: Vec<mpsc::Sender<&'static Lazy<Option<GlobMatcher>>>>,
}

impl OffloadIter {
pub fn new() -> Self {
let worker_count = match thread::available_parallelism() {
Ok(n) => (n.get() - 1).min(MAX_OFFLOAD_WORKERS), // leave 1 for main thread
Err(_) => 0,
};

let workers = (0..worker_count)
.map(|_| {
let (cmd_tx, cmd_rx) = mpsc::channel();
thread::spawn(move || loop {
match cmd_rx.recv() {
Ok(cell) => {
Lazy::force(cell);
}
Err(_) => break, // cmd_tx dropped; nothing more to do
}
}); // no need for the join handle; thread will halt when cmd_tx is dropped
cmd_tx
})
.collect();

Self {
next_iter: BUILTIN_MAPPINGS.iter().enumerate(),
build_idx: 0,
build_iter: BUILTIN_MAPPINGS.iter(),
workers,
}
}
}

impl Iterator for OffloadIter {
type Item = &'static (Lazy<Option<GlobMatcher>>, MappingTarget<'static>);

fn next(&mut self) -> Option<Self::Item> {
let (idx, item) = self.next_iter.next()?;

if idx + OFFLOAD_PREEMPT_MARGIN < self.build_idx
|| idx + OFFLOAD_PREEMPT_MARGIN >= BUILTIN_MAPPINGS.len()
{
// no further work needed at this point
return Some(item);
}

// feed jobs to workers
for cmd_tx in self.workers.iter() {
match self.build_iter.next() {
Some((unbuilt, _)) => {
cmd_tx
.send(unbuilt)
.expect("Offload worker should not hang up before main thread");
self.build_idx += 1;
}
None => {
break;
}
}
}

// halt workers if no longer needed
if self.build_idx >= BUILTIN_MAPPINGS.len() {
// workers stop when their current job is finished and cmd_tx is dropped
self.workers.clear();
}

Some(item)
}
}