Skip to content

Commit 07c2e81

Browse files
authored
perf: merged concat source children (#186)
1 parent fbb9493 commit 07c2e81

File tree

4 files changed

+199
-32
lines changed

4 files changed

+199
-32
lines changed

src/concat_source.rs

Lines changed: 194 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::{
22
borrow::Cow,
33
cell::RefCell,
44
hash::{Hash, Hasher},
5+
sync::{Mutex, OnceLock},
56
};
67

78
use rustc_hash::FxHashMap as HashMap;
@@ -10,7 +11,8 @@ use crate::{
1011
helpers::{get_map, GeneratedInfo, OnChunk, OnName, OnSource, StreamChunks},
1112
linear_map::LinearMap,
1213
source::{Mapping, OriginalLocation},
13-
BoxSource, MapOptions, Rope, Source, SourceExt, SourceMap, SourceValue,
14+
BoxSource, MapOptions, RawStringSource, Rope, Source, SourceExt, SourceMap,
15+
SourceValue,
1416
};
1517

1618
/// Concatenate multiple [Source]s to a single [Source].
@@ -55,9 +57,26 @@ use crate::{
5557
/// .unwrap()
5658
/// );
5759
/// ```
58-
#[derive(Default, Clone)]
60+
#[derive(Default)]
5961
pub struct ConcatSource {
60-
children: Vec<BoxSource>,
62+
children: Mutex<Vec<BoxSource>>,
63+
is_optimized: OnceLock<Vec<BoxSource>>,
64+
}
65+
66+
impl Clone for ConcatSource {
67+
fn clone(&self) -> Self {
68+
Self {
69+
children: Mutex::new(self.children.lock().unwrap().clone()),
70+
is_optimized: match self.is_optimized.get() {
71+
Some(children) => {
72+
let once_lock = OnceLock::new();
73+
once_lock.get_or_init(|| children.clone());
74+
once_lock
75+
}
76+
None => OnceLock::default(),
77+
},
78+
}
79+
}
6180
}
6281

6382
impl std::fmt::Debug for ConcatSource {
@@ -66,7 +85,13 @@ impl std::fmt::Debug for ConcatSource {
6685
let indent_str = format!("{:indent$}", "", indent = indent);
6786

6887
writeln!(f, "{indent_str}ConcatSource::new(vec![")?;
69-
for child in self.children.iter() {
88+
89+
let original_children = self.children.lock().unwrap();
90+
let children = match self.is_optimized.get() {
91+
Some(optimized_children) => optimized_children,
92+
None => original_children.as_ref(),
93+
};
94+
for child in children {
7095
writeln!(f, "{:indent$?},", child, indent = indent + 2)?;
7196
}
7297
write!(f, "{indent_str}]).boxed()")
@@ -87,19 +112,33 @@ impl ConcatSource {
87112
concat_source
88113
}
89114

90-
fn children(&self) -> &Vec<BoxSource> {
91-
&self.children
115+
fn optimized_children(&self) -> &[BoxSource] {
116+
self.is_optimized.get_or_init(|| {
117+
let mut children = self.children.lock().unwrap();
118+
optimize(&mut children)
119+
})
92120
}
93121

94122
/// Add a [Source] to concat.
95123
pub fn add<S: Source + 'static>(&mut self, source: S) {
124+
let children = &mut *self.children.lock().unwrap();
125+
126+
if let Some(optimized_children) = self.is_optimized.take() {
127+
*children = optimized_children;
128+
}
129+
96130
// First check if it's already a BoxSource containing a ConcatSource
97131
if let Some(box_source) = source.as_any().downcast_ref::<BoxSource>() {
98132
if let Some(concat_source) =
99133
box_source.as_ref().as_any().downcast_ref::<ConcatSource>()
100134
{
101135
// Extend with existing children (cheap clone due to Arc)
102-
self.children.extend(concat_source.children.iter().cloned());
136+
let original_children = concat_source.children.lock().unwrap();
137+
let other_children = match concat_source.is_optimized.get() {
138+
Some(optimized_children) => optimized_children,
139+
None => original_children.as_ref(),
140+
};
141+
children.extend(other_children.iter().cloned());
103142
return;
104143
}
105144
}
@@ -108,30 +147,27 @@ impl ConcatSource {
108147
if let Some(concat_source) = source.as_any().downcast_ref::<ConcatSource>()
109148
{
110149
// Extend with existing children (cheap clone due to Arc)
111-
self.children.extend(concat_source.children.iter().cloned());
150+
let original_children = concat_source.children.lock().unwrap();
151+
let other_children = match concat_source.is_optimized.get() {
152+
Some(optimized_children) => optimized_children,
153+
None => original_children.as_ref(),
154+
};
155+
children.extend(other_children.iter().cloned());
112156
} else {
113157
// Regular source - box it and add to children
114-
self.children.push(SourceExt::boxed(source));
158+
children.push(source.boxed());
115159
}
116160
}
117161
}
118162

119163
impl Source for ConcatSource {
120164
fn source(&self) -> SourceValue {
121-
let children = self.children();
122-
if children.len() == 1 {
123-
children[0].source()
124-
} else {
125-
let mut content = String::new();
126-
for child in self.children() {
127-
content.push_str(child.source().into_string_lossy().as_ref());
128-
}
129-
SourceValue::String(Cow::Owned(content))
130-
}
165+
let rope = self.rope();
166+
SourceValue::String(Cow::Owned(rope.to_string()))
131167
}
132168

133169
fn rope(&self) -> Rope<'_> {
134-
let children = self.children();
170+
let children = self.optimized_children();
135171
if children.len() == 1 {
136172
children[0].rope()
137173
} else {
@@ -145,7 +181,7 @@ impl Source for ConcatSource {
145181
}
146182

147183
fn buffer(&self) -> Cow<[u8]> {
148-
let children = self.children();
184+
let children = self.optimized_children();
149185
if children.len() == 1 {
150186
children[0].buffer()
151187
} else {
@@ -156,15 +192,19 @@ impl Source for ConcatSource {
156192
}
157193

158194
fn size(&self) -> usize {
159-
self.children().iter().map(|child| child.size()).sum()
195+
self
196+
.optimized_children()
197+
.iter()
198+
.map(|child| child.size())
199+
.sum()
160200
}
161201

162202
fn map(&self, options: &MapOptions) -> Option<SourceMap> {
163203
get_map(self, options)
164204
}
165205

166206
fn to_writer(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> {
167-
for child in self.children() {
207+
for child in self.optimized_children() {
168208
child.to_writer(writer)?;
169209
}
170210
Ok(())
@@ -174,15 +214,15 @@ impl Source for ConcatSource {
174214
impl Hash for ConcatSource {
175215
fn hash<H: Hasher>(&self, state: &mut H) {
176216
"ConcatSource".hash(state);
177-
for child in self.children().iter() {
217+
for child in self.optimized_children().iter() {
178218
child.hash(state);
179219
}
180220
}
181221
}
182222

183223
impl PartialEq for ConcatSource {
184224
fn eq(&self, other: &Self) -> bool {
185-
self.children() == other.children()
225+
self.optimized_children() == other.optimized_children()
186226
}
187227
}
188228
impl Eq for ConcatSource {}
@@ -195,9 +235,10 @@ impl StreamChunks for ConcatSource {
195235
on_source: OnSource<'_, 'a>,
196236
on_name: OnName<'_, 'a>,
197237
) -> crate::helpers::GeneratedInfo {
198-
if self.children().len() == 1 {
199-
return self.children[0]
200-
.stream_chunks(options, on_chunk, on_source, on_name);
238+
let children = self.optimized_children();
239+
240+
if children.len() == 1 {
241+
return children[0].stream_chunks(options, on_chunk, on_source, on_name);
201242
}
202243
let mut current_line_offset = 0;
203244
let mut current_column_offset = 0;
@@ -210,7 +251,7 @@ impl StreamChunks for ConcatSource {
210251
let name_index_mapping: RefCell<LinearMap<u32>> =
211252
RefCell::new(LinearMap::default());
212253

213-
for item in self.children() {
254+
for item in children {
214255
source_index_mapping.borrow_mut().clear();
215256
name_index_mapping.borrow_mut().clear();
216257
let mut last_mapping_line = 0;
@@ -356,6 +397,56 @@ impl StreamChunks for ConcatSource {
356397
}
357398
}
358399

400+
fn optimize(children: &mut Vec<BoxSource>) -> Vec<BoxSource> {
401+
let original_children = std::mem::take(children);
402+
403+
if original_children.len() <= 1 {
404+
return original_children; // Nothing to optimize
405+
}
406+
407+
let mut new_children = Vec::new();
408+
let mut current_raw_sources = Vec::new();
409+
410+
for child in original_children {
411+
if child.as_ref().as_any().is::<RawStringSource>() {
412+
current_raw_sources.push(child);
413+
} else {
414+
// Flush any pending raw sources before adding the non-raw source
415+
merge_raw_sources(&mut current_raw_sources, &mut new_children);
416+
new_children.push(child);
417+
}
418+
}
419+
420+
// Flush any remaining pending raw sources
421+
merge_raw_sources(&mut current_raw_sources, &mut new_children);
422+
423+
new_children
424+
}
425+
426+
/// Helper function to merge and flush pending raw sources.
427+
fn merge_raw_sources(
428+
raw_sources: &mut Vec<BoxSource>,
429+
new_children: &mut Vec<BoxSource>,
430+
) {
431+
match raw_sources.len() {
432+
0 => {} // Nothing to flush
433+
1 => {
434+
// Single source - move it directly
435+
new_children.push(raw_sources.pop().unwrap());
436+
}
437+
_ => {
438+
// Multiple sources - merge them
439+
let capacity = raw_sources.iter().map(|s| s.size()).sum();
440+
let mut merged_content = String::with_capacity(capacity);
441+
for source in raw_sources.drain(..) {
442+
merged_content.push_str(source.source().into_string_lossy().as_ref());
443+
}
444+
let merged_source = RawStringSource::from(merged_content);
445+
new_children.push(merged_source.boxed());
446+
}
447+
}
448+
}
449+
359450
#[cfg(test)]
360451
mod tests {
361452
use crate::{OriginalSource, RawBufferSource, RawStringSource};
@@ -640,6 +731,79 @@ mod tests {
640731
);
641732
// The key test: verify that nested ConcatSources are flattened
642733
// Should have 6 direct children instead of nested structure
643-
assert_eq!(outer_concat.children.len(), 6);
734+
assert_eq!(outer_concat.optimized_children().len(), 1);
735+
}
736+
737+
#[test]
738+
fn test_self_equality_no_deadlock() {
739+
let concat_source = ConcatSource::new([
740+
RawStringSource::from("Hello "),
741+
RawStringSource::from("World"),
742+
])
743+
.boxed();
744+
assert_eq!(concat_source.as_ref(), concat_source.as_ref());
745+
746+
concat_source.source();
747+
748+
assert_eq!(concat_source.as_ref(), concat_source.as_ref());
749+
}
750+
751+
#[test]
752+
fn test_debug_output() {
753+
let inner_concat = ConcatSource::new([
754+
RawStringSource::from("Hello "),
755+
RawStringSource::from("World"),
756+
]);
757+
758+
let mut outer_concat = ConcatSource::new([
759+
inner_concat.boxed(),
760+
RawStringSource::from("!").boxed(),
761+
ConcatSource::new([
762+
RawStringSource::from(" How"),
763+
RawStringSource::from(" are"),
764+
])
765+
.boxed(),
766+
RawStringSource::from(" you?\n").boxed(),
767+
]);
768+
769+
assert_eq!(
770+
format!("{:?}", outer_concat),
771+
r#"ConcatSource::new(vec![
772+
RawStringSource::from_static("Hello ").boxed(),
773+
RawStringSource::from_static("World").boxed(),
774+
RawStringSource::from_static("!").boxed(),
775+
RawStringSource::from_static(" How").boxed(),
776+
RawStringSource::from_static(" are").boxed(),
777+
RawStringSource::from_static(" you?\n").boxed(),
778+
]).boxed()"#
779+
);
780+
781+
outer_concat.source();
782+
783+
assert_eq!(
784+
format!("{:?}", outer_concat),
785+
r#"ConcatSource::new(vec![
786+
RawStringSource::from_static("Hello World! How are you?\n").boxed(),
787+
]).boxed()"#
788+
);
789+
790+
outer_concat.add(RawStringSource::from("I'm fine."));
791+
792+
assert_eq!(
793+
format!("{:?}", outer_concat),
794+
r#"ConcatSource::new(vec![
795+
RawStringSource::from_static("Hello World! How are you?\n").boxed(),
796+
RawStringSource::from_static("I'm fine.").boxed(),
797+
]).boxed()"#
798+
);
799+
800+
outer_concat.source();
801+
802+
assert_eq!(
803+
format!("{:?}", outer_concat),
804+
r#"ConcatSource::new(vec![
805+
RawStringSource::from_static("Hello World! How are you?\nI'm fine.").boxed(),
806+
]).boxed()"#
807+
);
644808
}
645809
}

src/replace_source.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1247,7 +1247,7 @@ return <div>{data.foo}</div>
12471247
}
12481248

12491249
#[test]
1250-
fn debug() {
1250+
fn test_debug_output() {
12511251
let mut source =
12521252
ReplaceSource::new(OriginalSource::new("hello", "file.txt").boxed());
12531253
source.replace(0, 0, "println!(\"", None);

src/source.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,9 @@ pub trait SourceExt {
242242

243243
impl<T: Source + 'static> SourceExt for T {
244244
fn boxed(self) -> BoxSource {
245+
if let Some(source) = self.as_any().downcast_ref::<BoxSource>() {
246+
return source.clone();
247+
}
245248
Arc::new(self)
246249
}
247250
}

src/source_map_source.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ mod tests {
774774
}
775775

776776
#[test]
777-
fn debug() {
777+
fn test_debug_output() {
778778
let source = SourceMapSource::new(SourceMapSourceOptions {
779779
value: "hello world",
780780
name: "hello.txt",

0 commit comments

Comments
 (0)