mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
perf(diagnostics): build reporter line index incrementally
This commit is contained in:
1 parent
041085af90
commit
5d12c9c1d0
4 files changed
+200
-38
No files matched your search
@@ -1,18 +1,33 @@
|
||||
use std::{fmt, sync::OnceLock};
|
||||
use std::{
|
||||
fmt,
|
||||
sync::{Mutex, OnceLock},
|
||||
};
|
||||
|
||||
use crate::{SourceCode, source_impls};
|
||||
|
||||
/// Utility struct for when you have a regular [`SourceCode`] type that doesn't
|
||||
/// implement `name`. For example [`String`]. Or if you want to override the
|
||||
/// `name` returned by the `SourceCode`.
|
||||
#[derive(Clone)]
|
||||
pub struct NamedSource<S: SourceCode> {
|
||||
source: S,
|
||||
name: String,
|
||||
/// Line starts of `source`, computed on first use. Reporters resolve every
|
||||
/// diagnostic's line and column through this, so a file's many diagnostics
|
||||
/// share one scan instead of each rescanning the file.
|
||||
/// Complete index, built only when explicitly requested.
|
||||
line_starts: OnceLock<Option<Box<[u32]>>>,
|
||||
/// Position queries retain only the line starts scanned so far.
|
||||
line_index: Mutex<source_impls::SourceLineIndex>,
|
||||
}
|
||||
|
||||
impl<S: SourceCode + Clone> Clone for NamedSource<S> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
source: self.source.clone(),
|
||||
name: self.name.clone(),
|
||||
line_starts: self.line_starts.clone(),
|
||||
line_index: Mutex::new(
|
||||
self.line_index.lock().unwrap_or_else(std::sync::PoisonError::into_inner).clone(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: SourceCode> fmt::Debug for NamedSource<S> {
|
||||
@@ -29,7 +44,12 @@ impl<S: SourceCode> NamedSource<S> {
|
||||
/// a name.
|
||||
#[must_use]
|
||||
pub fn new(name: impl AsRef<str>, source: S) -> Self {
|
||||
Self { source, name: name.as_ref().to_string(), line_starts: OnceLock::new() }
|
||||
Self {
|
||||
source,
|
||||
name: name.as_ref().to_string(),
|
||||
line_starts: OnceLock::new(),
|
||||
line_index: Mutex::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +65,63 @@ impl<S: SourceCode> SourceCode for NamedSource<S> {
|
||||
fn line_starts(&self) -> Option<&[u32]> {
|
||||
self.line_starts.get_or_init(|| source_impls::line_starts(self.source.data())).as_deref()
|
||||
}
|
||||
|
||||
fn line_column(&self, offset: usize) -> Option<(usize, usize)> {
|
||||
let input = self.source.data();
|
||||
if let Some(line_starts) = self.line_starts.get().and_then(Option::as_deref) {
|
||||
return source_impls::line_column(input, Some(line_starts), offset);
|
||||
}
|
||||
if u32::try_from(input.len()).is_err() {
|
||||
return source_impls::line_column(input, None, offset);
|
||||
}
|
||||
self.line_index
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.line_column(input, offset)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use oxc_span::Span;
|
||||
|
||||
use crate::{OxcDiagnostic, reporter::Info};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn early_diagnostic_does_not_initialize_complete_index() {
|
||||
let source = Arc::new(NamedSource::new("file.js", "line\n".repeat(200_000)));
|
||||
let diagnostic = OxcDiagnostic::error("test")
|
||||
.with_label(Span::new(0, 1))
|
||||
.with_source_code(Arc::clone(&source));
|
||||
let info = Info::new(&diagnostic);
|
||||
assert_eq!((info.start.line, info.start.column), (1, 1));
|
||||
assert_eq!((info.end.line, info.end.column), (1, 2));
|
||||
assert!(source.line_starts.get().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloned_and_shared_indexes_match_direct_scan() {
|
||||
let text = "first\r\nsecond\n\u{00e9}\r\u{1f980}\n";
|
||||
let source = NamedSource::new("file.js", text);
|
||||
source.line_column(8).unwrap();
|
||||
let cloned = Arc::new(source.clone());
|
||||
std::thread::scope(|scope| {
|
||||
for _ in 0..4 {
|
||||
scope.spawn(|| {
|
||||
for offset in (0..=text.len() + 1).rev() {
|
||||
assert_eq!(cloned.line_column(offset), text.line_column(offset));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
assert!(source.line_starts.get().is_none());
|
||||
cloned.line_starts().unwrap();
|
||||
for offset in 0..=text.len() + 1 {
|
||||
assert_eq!(source.line_column(offset), cloned.line_column(offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,4 +84,12 @@ pub trait SourceCode: Send + Sync {
|
||||
fn line_starts(&self) -> Option<&[u32]> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the one-based line and byte column at `offset`.
|
||||
///
|
||||
/// Returns `None` if the offset is past the end of the source. An offset
|
||||
/// inside a CRLF pair refers to the position of its CR.
|
||||
fn line_column(&self, offset: usize) -> Option<(usize, usize)> {
|
||||
crate::source_impls::line_column(self.data(), self.line_starts(), offset)
|
||||
}
|
||||
}
|
||||
@@ -140,36 +140,8 @@ pub struct InfoPosition {
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
fn line_column(source: &dyn SourceCode, mut offset: usize) -> Option<InfoPosition> {
|
||||
let data = source.data();
|
||||
if offset > data.len() {
|
||||
return None;
|
||||
}
|
||||
if offset > 0 && offset < data.len() && data[offset - 1] == b'\r' && data[offset] == b'\n' {
|
||||
offset -= 1;
|
||||
}
|
||||
|
||||
if let Some(line_starts) = source.line_starts() {
|
||||
// `line_starts[0] == 0`, so at least one line starts at or before `offset`.
|
||||
let line = line_starts.partition_point(|&start| start as usize <= offset);
|
||||
let line_start = line_starts[line - 1] as usize;
|
||||
return Some(InfoPosition { line, column: offset - line_start + 1 });
|
||||
}
|
||||
|
||||
let mut line = 1;
|
||||
let mut line_start = 0;
|
||||
for index in memchr::memchr2_iter(b'\r', b'\n', &data[..offset]) {
|
||||
if data[index] == b'\n' && index > 0 && data[index - 1] == b'\r' {
|
||||
continue;
|
||||
}
|
||||
line += 1;
|
||||
line_start = if data[index] == b'\r' && index + 1 < offset && data[index + 1] == b'\n' {
|
||||
index + 2
|
||||
} else {
|
||||
index + 1
|
||||
};
|
||||
}
|
||||
Some(InfoPosition { line, column: offset - line_start + 1 })
|
||||
fn line_column(source: &dyn SourceCode, offset: usize) -> Option<InfoPosition> {
|
||||
source.line_column(offset).map(|(line, column)| InfoPosition { line, column })
|
||||
}
|
||||
|
||||
impl Info {
|
||||
@@ -212,7 +184,9 @@ impl Info {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{Error, NamedSource, OxcDiagnostic};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{Error, NamedSource, OxcDiagnostic, SourceCode};
|
||||
|
||||
use super::{Info, line_column};
|
||||
|
||||
@@ -228,7 +202,8 @@ mod test {
|
||||
text.push(ALPHABET[code % 3] as char);
|
||||
code /= 3;
|
||||
}
|
||||
let indexed = NamedSource::new("file.js", text.clone());
|
||||
let indexed = Arc::new(NamedSource::new("file.js", text.clone()));
|
||||
let cloned = indexed.as_ref().clone();
|
||||
for offset in 0..=text.len() + 1 {
|
||||
assert_eq!(
|
||||
line_column(&indexed, offset),
|
||||
@@ -236,6 +211,14 @@ mod test {
|
||||
"{text:?} at {offset}"
|
||||
);
|
||||
}
|
||||
for offset in (0..=text.len() + 1).rev() {
|
||||
assert_eq!(line_column(&indexed, offset), line_column(&text, offset));
|
||||
assert_eq!(line_column(&cloned, offset), line_column(&text, offset));
|
||||
}
|
||||
indexed.line_starts().unwrap();
|
||||
for offset in 0..=text.len() + 1 {
|
||||
assert_eq!(line_column(&indexed, offset), line_column(&text, offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,6 +803,74 @@ impl<T: ?Sized + SourceCode> SourceCode for Arc<T> {
|
||||
fn line_starts(&self) -> Option<&[u32]> {
|
||||
self.as_ref().line_starts()
|
||||
}
|
||||
|
||||
fn line_column(&self, offset: usize) -> Option<(usize, usize)> {
|
||||
self.as_ref().line_column(offset)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a position using a complete index or a direct prefix scan.
|
||||
pub fn line_column(
|
||||
input: &[u8],
|
||||
line_starts: Option<&[u32]>,
|
||||
offset: usize,
|
||||
) -> Option<(usize, usize)> {
|
||||
if offset > input.len() {
|
||||
return None;
|
||||
}
|
||||
let offset = normalize_offset(input, offset);
|
||||
if let Some(line_starts) = line_starts {
|
||||
let line = line_starts.partition_point(|&start| start as usize <= offset);
|
||||
return Some((line, offset - line_starts[line - 1] as usize + 1));
|
||||
}
|
||||
let mut line = 1;
|
||||
let mut line_start = 0;
|
||||
for index in memchr::memchr2_iter(b'\r', b'\n', &input[..offset]) {
|
||||
if input[index] == b'\n' && index > 0 && input[index - 1] == b'\r' {
|
||||
continue;
|
||||
}
|
||||
line += 1;
|
||||
line_start = if input[index] == b'\r' && index + 1 < offset && input[index + 1] == b'\n' {
|
||||
index + 2
|
||||
} else {
|
||||
index + 1
|
||||
};
|
||||
}
|
||||
Some((line, offset - line_start + 1))
|
||||
}
|
||||
|
||||
fn normalize_offset(input: &[u8], offset: usize) -> usize {
|
||||
if offset > 0 && offset < input.len() && input[offset - 1] == b'\r' && input[offset] == b'\n' {
|
||||
offset - 1
|
||||
} else {
|
||||
offset
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared reporters extend this index only through their requested positions.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SourceLineIndex {
|
||||
line_starts: Vec<u32>,
|
||||
frontier: usize,
|
||||
}
|
||||
|
||||
impl SourceLineIndex {
|
||||
pub fn line_column(&mut self, input: &[u8], offset: usize) -> Option<(usize, usize)> {
|
||||
if offset > input.len() {
|
||||
return None;
|
||||
}
|
||||
let offset = normalize_offset(input, offset);
|
||||
if offset > self.frontier {
|
||||
for line_break in LineBreaks::new(&input[self.frontier..offset]) {
|
||||
self.line_starts
|
||||
.push(u32::try_from(self.frontier + line_break.next_line_start()).ok()?);
|
||||
}
|
||||
self.frontier = offset;
|
||||
}
|
||||
let line = self.line_starts.partition_point(|&start| start as usize <= offset);
|
||||
let line_start = line.checked_sub(1).map_or(0, |index| self.line_starts[index] as usize);
|
||||
Some((line + 1, offset - line_start + 1))
|
||||
}
|
||||
}
|
||||
|
||||
/// Byte offsets at which each line of `input` starts, with CRLF counted as one line break.
|
||||
@@ -820,6 +888,30 @@ pub fn line_starts(input: &[u8]) -> Option<Box<[u32]>> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn source_line_index_only_scans_requested_prefixes() {
|
||||
let input = b"first\r\nsecond\nthird\rfourth";
|
||||
let mut index = SourceLineIndex::default();
|
||||
assert_eq!(index.line_column(input, 1), Some((1, 2)));
|
||||
assert_eq!(index.frontier, 1);
|
||||
assert_eq!(index.line_starts, [] as [u32; 0]);
|
||||
|
||||
// An offset on the LF keeps the whole CRLF beyond the frontier.
|
||||
assert_eq!(index.line_column(input, 6), Some((1, 6)));
|
||||
assert_eq!(index.frontier, 5);
|
||||
assert_eq!(index.line_starts, [] as [u32; 0]);
|
||||
assert_eq!(index.line_column(input, 8), Some((2, 2)));
|
||||
assert_eq!(index.frontier, 8);
|
||||
assert_eq!(index.line_starts, [7]);
|
||||
|
||||
assert_eq!(index.line_column(input, 0), Some((1, 1)));
|
||||
assert_eq!(index.frontier, 8);
|
||||
assert_eq!(index.line_column(input, input.len() + 1), None);
|
||||
assert_eq!(index.frontier, 8);
|
||||
assert_eq!(index.line_column(input, input.len()), Some((4, 7)));
|
||||
assert_eq!(index.line_starts, [7, 14, 20]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_context_prefix_matches_generic_path() {
|
||||
for input in [
|
||||
|
||||
Reference in new issue
Block a user