include!("search_budget.inc.rs");
use inflight::{Charge, budget};
const FILE_INLINE_EVENTS: usize = crate::constants::SEARCH_FILE_INLINE_EVENTS;
const FILE_INLINE_MAX_BYTES: u64 = crate::constants::SEARCH_FILE_INLINE_MAX_BYTES;
const FILE_STREAM_CAP: usize = crate::constants::SEARCH_FILE_STREAM_CAP;
const FILE_FEED_CAP: usize = crate::constants::SEARCH_FILE_FEED_CAP;
type Charged = (SearchEvent, Charge);
fn event_cost(event: &SearchEvent) -> u64 {
let heap = match event {
SearchEvent::Begin(_) | SearchEvent::End { .. } => 0,
SearchEvent::Context { lines, .. } => lines.len(),
SearchEvent::Match {
lines, submatches, ..
} => {
lines.len()
+ submatches
.iter()
.map(|s| std::mem::size_of::<Submatch>() + s.r#match.len())
.sum::<usize>()
}
};
(std::mem::size_of::<Charged>() + heap) as u64
}
enum FileFeed {
Inline(Vec<Charged>),
Stream {
rx: crossbeam_channel::Receiver<Charged>,
id: u64,
own: Arc<std::sync::atomic::AtomicU64>,
},
}
struct FeedEvents {
source: FeedSource,
head: Option<u64>,
}
enum FeedSource {
Inline(std::vec::IntoIter<Charged>),
Stream {
events: crossbeam_channel::IntoIter<Charged>,
own: Arc<std::sync::atomic::AtomicU64>,
},
}
impl Iterator for FeedEvents {
type Item = SearchEvent;
fn next(&mut self) -> Option<SearchEvent> {
let (event, charge) = match &mut self.source {
FeedSource::Inline(it) => it.next()?,
FeedSource::Stream { events, own } => {
let item = events.next()?;
let before = own.fetch_sub(item.1.bytes(), std::sync::atomic::Ordering::AcqRel);
if before == item.1.bytes() {
budget().head_drained();
}
item
}
};
drop(charge);
Some(event)
}
}
impl Drop for FeedEvents {
fn drop(&mut self) {
if let Some(id) = self.head {
budget().clear_head(id);
}
}
}
impl IntoIterator for FileFeed {
type Item = SearchEvent;
type IntoIter = FeedEvents;
fn into_iter(self) -> FeedEvents {
match self {
Self::Inline(events) => FeedEvents {
source: FeedSource::Inline(events.into_iter()),
head: None,
},
Self::Stream { rx, id, own } => {
budget().set_head(id);
FeedEvents {
source: FeedSource::Stream {
events: rx.into_iter(),
own,
},
head: Some(id),
}
}
}
}
}
enum FileOut {
Inline { events: Vec<Charged>, bytes: u64 },
Stream {
tx: crossbeam_channel::Sender<Charged>,
id: u64,
own: Arc<std::sync::atomic::AtomicU64>,
},
}
struct FileSender<'a> {
feeds: &'a crossbeam_channel::Sender<FileFeed>,
out: FileOut,
}
impl<'a> FileSender<'a> {
fn new(feeds: &'a crossbeam_channel::Sender<FileFeed>) -> Self {
budget().wait_until_room(crate::signal::is_global_shutdown);
Self {
feeds,
out: FileOut::Inline {
events: Vec::new(),
bytes: 0,
},
}
}
fn send(&mut self, event: SearchEvent) -> bool {
let cost = event_cost(&event);
match &mut self.out {
FileOut::Stream { tx, id, own } => {
let Some(charge) = budget().acquire(
cost,
Some((*id, own.as_ref())),
crate::signal::is_global_shutdown,
) else {
return false;
};
own.fetch_add(cost, std::sync::atomic::Ordering::AcqRel);
tx.send((event, charge)).is_ok()
}
FileOut::Inline { events, bytes } => {
events.push((event, budget().charge(cost)));
*bytes += cost;
if events.len() <= FILE_INLINE_EVENTS && *bytes <= FILE_INLINE_MAX_BYTES {
return true;
}
self.open_stream()
}
}
}
fn open_stream(&mut self) -> bool {
let (tx, rx) = crossbeam_channel::bounded(FILE_STREAM_CAP);
let id = budget().next_stream_id();
let inline_bytes = match &self.out {
FileOut::Inline { bytes, .. } => *bytes,
FileOut::Stream { .. } => 0,
};
let own = Arc::new(std::sync::atomic::AtomicU64::new(inline_bytes));
let feed = FileFeed::Stream {
rx,
id,
own: Arc::clone(&own),
};
if self.feeds.send(feed).is_err() {
return false;
}
let out = FileOut::Stream {
tx: tx.clone(),
id,
own,
};
let FileOut::Inline { events, .. } = std::mem::replace(&mut self.out, out) else {
return true;
};
events.into_iter().all(|ev| tx.send(ev).is_ok())
}
fn finish(self) -> bool {
match self.out {
FileOut::Inline { events, .. } if events.is_empty() => true,
FileOut::Inline { events, .. } => self.feeds.send(FileFeed::Inline(events)).is_ok(),
FileOut::Stream { .. } => true,
}
}
}
fn line_text(bytes: &[u8], max_columns: usize) -> String {
let text = String::from_utf8_lossy(bytes);
if text.len() <= max_columns {
return text.into_owned();
}
let mut cut = max_columns;
while cut > 0 && !text.is_char_boundary(cut) {
cut -= 1;
}
format!("{}...[truncated]", &text[..cut])
}
fn trimmed_line_bytes(bytes: &[u8]) -> Vec<u8> {
let end = bytes.iter().rposition(|b| *b != b'\n').map_or(0, |i| i + 1);
bytes[..end].to_vec()
}
fn trim_line_end(line: &mut String) {
let keep = line.trim_end_matches('\n').len();
line.truncate(keep);
}
struct SearchSink<'a, 'b> {
matcher: &'a grep_regex::RegexMatcher,
path: Arc<std::path::Path>,
out: &'a mut FileSender<'b>,
cancel: crate::signal::CancelCheck,
max_columns: usize,
tally_only: bool,
max_count: Option<u64>,
suppressed: bool,
matches: u64,
lines: u64,
stopped: bool,
}
impl SearchSink<'_, '_> {
fn stop(&mut self) -> Result<bool, std::io::Error> {
self.stopped = true;
Ok(false)
}
fn deliver(&mut self, event: SearchEvent) -> Result<bool, std::io::Error> {
if self.out.send(event) {
Ok(true)
} else {
self.stop()
}
}
}
impl Sink for SearchSink<'_, '_> {
type Error = std::io::Error;
fn matched(
&mut self,
_searcher: &grep_searcher::Searcher,
mat: &SinkMatch<'_>,
) -> Result<bool, Self::Error> {
if self.max_count.is_some_and(|max| self.matches >= max) {
self.suppressed = true;
return Ok(false);
}
self.lines += 1;
self.matches += 1;
if self.cancel.advance(1).is_err() {
return self.stop();
}
if self.tally_only {
return Ok(true);
}
let mut lines = line_text(mat.bytes(), self.max_columns);
let submatches = extract_submatches(self.matcher, &lines);
trim_line_end(&mut lines);
self.deliver(SearchEvent::Match {
path: Arc::clone(&self.path),
line_number: mat.line_number().unwrap_or(0),
lines,
byte_offset: mat.absolute_byte_offset(),
submatches,
raw: trimmed_line_bytes(mat.bytes()),
})
}
fn context(
&mut self,
_searcher: &grep_searcher::Searcher,
ctx: &SinkContext<'_>,
) -> Result<bool, Self::Error> {
if self.cancel.advance(1).is_err() {
return self.stop();
}
if self.tally_only {
return Ok(true);
}
let mut lines = line_text(ctx.bytes(), self.max_columns);
trim_line_end(&mut lines);
self.deliver(SearchEvent::Context {
path: Arc::clone(&self.path),
line_number: ctx.line_number().unwrap_or(0),
lines,
raw_block: ctx.bytes().to_vec(),
})
}
}
struct CancelRead<R> {
inner: R,
flag: Arc<AtomicBool>,
last_call: std::time::Instant,
}
impl<R: std::io::Read> std::io::Read for CancelRead<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let gap = self.last_call.elapsed();
self.last_call = std::time::Instant::now();
if gap.as_millis() >= u128::from(crate::constants::SEARCH_READ_GAP_LOG_MS) {
tracing::debug!(
gap_ms = gap.as_millis() as u64,
requested_bytes = buf.len(),
raised_ago_ms = crate::signal::shutdown_raised_ago()
.map_or(0, |d| d.as_millis() as u64),
"searcher went this long without asking for bytes"
);
}
if self.flag.load(Ordering::Acquire) {
tracing::debug!(
requested_bytes = buf.len(),
raised_ago_ms = crate::signal::shutdown_raised_ago()
.map_or(0, |d| d.as_millis() as u64),
"cancel observed at read boundary"
);
return Err(std::io::Error::other(crate::signal::current_cancel_reason()));
}
self.inner.read(buf)
}
}
fn search_file_cancellable(
searcher: &mut grep_searcher::Searcher,
matcher: &grep_regex::RegexMatcher,
path: &std::path::Path,
flag: &Arc<AtomicBool>,
sink: &mut SearchSink<'_, '_>,
) -> std::io::Result<()> {
let file = std::fs::File::open(path)?;
let reader = CancelRead {
inner: file,
flag: Arc::clone(flag),
last_call: std::time::Instant::now(),
};
match searcher.search_reader(matcher, reader, sink) {
Err(_) if flag.load(Ordering::Acquire) => Ok(()),
other => other,
}
}
fn event_path(event: &SearchEvent) -> &std::path::Path {
match event {
SearchEvent::Begin(path)
| SearchEvent::Match { path, .. }
| SearchEvent::Context { path, .. }
| SearchEvent::End { path, .. } => path,
}
}
fn consume_streaming(
feeds: &crossbeam_channel::Receiver<FileFeed>,
writer: &mut NdjsonWriter<impl Write>,
args: &SearchArgs,
shutdown: &ShutdownSignal,
tally: &mut SearchTally,
text: &mut TextLayout,
) -> Result<bool> {
let mut has_matches = false;
let cap = crate::output::limit_cap();
let mut accepted: u64 = 0;
for feed in feeds {
if crate::output::limit_reached() {
break;
}
let mut answered = 0u64;
for event in feed {
if shutdown.is_shutdown() {
return Ok(has_matches);
}
let took =
emit_search_event(event, writer, args, &mut has_matches, tally, text)?;
answered = answered.saturating_add(took);
accepted = accepted.saturating_add(took);
if took > 0 && cap.is_some_and(|max| accepted >= max) {
crate::output::mark_limit_reached();
}
}
if answered > 0 {
tally.files_matched = tally.files_matched.saturating_add(1);
}
}
Ok(has_matches)
}
fn consume_sorted(
feeds: &crossbeam_channel::Receiver<FileFeed>,
writer: &mut NdjsonWriter<impl Write>,
args: &SearchArgs,
shutdown: &ShutdownSignal,
tally: &mut SearchTally,
text: &mut TextLayout,
) -> Result<bool> {
let mut buffer: std::collections::BTreeMap<std::path::PathBuf, Vec<SearchEvent>> =
std::collections::BTreeMap::new();
for feed in feeds {
let mut block = Vec::new();
for event in feed {
if shutdown.is_shutdown() {
return Ok(false);
}
block.push(event);
}
if let Some(first) = block.first() {
buffer
.entry(event_path(first).to_path_buf())
.or_default()
.append(&mut block);
}
}
let cap = crate::output::limit_cap();
let mut accepted: u64 = 0;
let mut has_matches = false;
let mut blocks: Vec<(std::path::PathBuf, Vec<SearchEvent>)> = buffer.into_iter().collect();
if args.format == Some(crate::cli::SearchFormat::Text) {
blocks.sort_by_cached_key(|(path, _)| text.order_key(path));
}
for (_, block) in blocks {
if crate::output::limit_reached() {
break;
}
let mut answered = 0u64;
for event in block {
if shutdown.is_shutdown() {
return Ok(has_matches);
}
let took =
emit_search_event(event, writer, args, &mut has_matches, tally, text)?;
answered = answered.saturating_add(took);
accepted = accepted.saturating_add(took);
if took > 0 && cap.is_some_and(|max| accepted >= max) {
crate::output::mark_limit_reached();
}
}
if answered > 0 {
tally.files_matched = tally.files_matched.saturating_add(1);
}
}
Ok(has_matches)
}