Skip to content

Commit d5d631b

Browse files
committed
fix: source map
1 parent edddae1 commit d5d631b

File tree

12 files changed

+405
-609
lines changed

12 files changed

+405
-609
lines changed

benches/bench.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ use rspack_sources::{
1818

1919
use bench_complex_replace_source::benchmark_complex_replace_source;
2020
use bench_source_map::{
21-
benchmark_parse_source_map_from_json, benchmark_source_map_as_borrowed,
22-
benchmark_stringify_source_map_to_json,
21+
benchmark_parse_source_map_from_json, benchmark_stringify_source_map_to_json,
2322
};
2423

2524
const HELLOWORLD_JS: &str = include_str!(concat!(
@@ -253,9 +252,6 @@ fn bench_rspack_sources(criterion: &mut Criterion) {
253252
benchmark_parse_source_map_from_json,
254253
);
255254

256-
group
257-
.bench_function("source_map_as_borrowed", benchmark_source_map_as_borrowed);
258-
259255
group.bench_function(
260256
"stringify_source_map_to_json",
261257
benchmark_stringify_source_map_to_json,

benches/bench_source_map.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,6 @@ pub fn benchmark_parse_source_map_from_json(b: &mut Bencher) {
1919
})
2020
}
2121

22-
pub fn benchmark_source_map_as_borrowed(b: &mut Bencher) {
23-
let source = SourceMap::from_json(ANTD_MIN_JS_MAP).unwrap();
24-
b.iter(|| {
25-
let _ = black_box(source.as_borrowed());
26-
})
27-
}
28-
2922
pub fn benchmark_stringify_source_map_to_json(b: &mut Bencher) {
3023
let source = SourceMap::from_json(ANTD_MIN_JS_MAP).unwrap();
3124
b.iter(|| {

src/cached_source.rs

Lines changed: 94 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use std::{
22
borrow::Cow,
3-
hash::{Hash, Hasher},
4-
sync::OnceLock,
3+
hash::{BuildHasherDefault, Hash, Hasher},
4+
sync::{Arc, OnceLock},
55
};
66

7+
use dashmap::{mapref::entry::Entry, DashMap};
78
use rustc_hash::FxHasher;
89

910
use crate::{
@@ -12,7 +13,7 @@ use crate::{
1213
stream_chunks_of_source_map, StreamChunks,
1314
},
1415
rope::Rope,
15-
BoxSource, MapOptions, Source, SourceExt, SourceMap,
16+
MapOptions, Source, SourceMap,
1617
};
1718

1819
/// It tries to reused cached results from other methods to avoid calculations,
@@ -48,61 +49,36 @@ use crate::{
4849
/// "Hello World\nconsole.log('test');\nconsole.log('test2');\nHello2\n"
4950
/// );
5051
/// ```
51-
52-
#[derive(Debug)]
53-
struct CachedSourceOwner {
54-
inner: BoxSource,
55-
cached_hash: OnceLock<u64>,
56-
}
57-
58-
#[derive(Debug)]
59-
struct CachedSourceDependent<'a> {
60-
cached_colomns_map: OnceLock<Option<Cow<'static, SourceMap<'static>>>>,
61-
cached_line_only_map: OnceLock<Option<Cow<'static, SourceMap<'static>>>>,
62-
phantom: std::marker::PhantomData<&'a ()>,
52+
pub struct CachedSource<T> {
53+
inner: Arc<T>,
54+
cached_hash: Arc<OnceLock<u64>>,
55+
cached_maps:
56+
Arc<DashMap<MapOptions, Option<SourceMap>, BuildHasherDefault<FxHasher>>>,
6357
}
6458

65-
self_cell::self_cell!(
66-
struct CachedSourceCell {
67-
owner: CachedSourceOwner,
68-
69-
#[covariant]
70-
dependent: CachedSourceDependent,
71-
}
72-
73-
impl { Debug }
74-
);
75-
76-
/// A wrapper around any [`Source`] that caches expensive computations to improve performance.
77-
pub struct CachedSource(CachedSourceCell);
78-
79-
impl CachedSource {
59+
impl<T> CachedSource<T> {
8060
/// Create a [CachedSource] with the original [Source].
81-
pub fn new<T: SourceExt>(inner: T) -> Self {
82-
let owner = CachedSourceOwner {
83-
inner: inner.boxed(),
84-
cached_hash: OnceLock::new(),
85-
};
86-
Self(CachedSourceCell::new(owner, |_| CachedSourceDependent {
87-
cached_colomns_map: Default::default(),
88-
cached_line_only_map: Default::default(),
89-
phantom: std::marker::PhantomData,
90-
}))
61+
pub fn new(inner: T) -> Self {
62+
Self {
63+
inner: Arc::new(inner),
64+
cached_hash: Default::default(),
65+
cached_maps: Default::default(),
66+
}
9167
}
9268

9369
/// Get the original [Source].
94-
pub fn original(&self) -> &BoxSource {
95-
&self.0.borrow_owner().inner
70+
pub fn original(&self) -> &T {
71+
&self.inner
9672
}
9773
}
9874

99-
impl Source for CachedSource {
75+
impl<T: Source + Hash + PartialEq + Eq + 'static> Source for CachedSource<T> {
10076
fn source(&self) -> Cow<str> {
101-
self.0.borrow_owner().inner.source()
77+
self.inner.source()
10278
}
10379

10480
fn rope(&self) -> Rope<'_> {
105-
self.0.borrow_owner().inner.rope()
81+
self.inner.rope()
10682
}
10783

10884
fn buffer(&self) -> Cow<[u8]> {
@@ -112,134 +88,101 @@ impl Source for CachedSource {
11288
}
11389

11490
fn size(&self) -> usize {
115-
self.0.borrow_owner().inner.size()
91+
self.inner.size()
11692
}
11793

118-
fn map<'a>(&'a self, options: &MapOptions) -> Option<Cow<'a, SourceMap<'a>>> {
119-
fn get_or_init_cache<'b>(
120-
owner: &'b CachedSourceOwner,
121-
cache: &'b OnceLock<Option<Cow<'static, SourceMap<'static>>>>,
122-
options: &MapOptions,
123-
) -> Option<Cow<'b, SourceMap<'b>>> {
124-
cache
125-
.get_or_init(|| {
126-
let map = owner.inner.map(options);
127-
// SAFETY: This transmute is safe because:
128-
// 1. BoxSource is an immutable wrapper around Arc<dyn Source>, ensuring the underlying
129-
// data remains stable throughout the CachedSource's lifetime
130-
// 2. The SourceMap references string data that lives in the BoxSource, which is owned
131-
// by the CachedSourceOwner and guaranteed to outlive any cached references
132-
// 3. The self_cell structure ensures that the dependent (cache) cannot outlive the
133-
// owner (BoxSource), maintaining memory safety invariants
134-
// 4. We're extending the lifetime to 'static for caching purposes, but the actual
135-
// data lifetime is managed by the self-referential structure
136-
#[allow(unsafe_code)]
137-
unsafe { std::mem::transmute::<_, Option<Cow<'static, SourceMap<'static>>>>(map) }
138-
})
139-
.as_ref()
140-
.map(|map| {
141-
Cow::Borrowed(map.as_ref())
142-
})
143-
}
144-
145-
if options.columns {
146-
self.0.with_dependent(|owner, dependent| {
147-
get_or_init_cache(owner, &dependent.cached_colomns_map, options)
148-
})
94+
fn map(&self, options: &MapOptions) -> Option<SourceMap> {
95+
if let Some(map) = self.cached_maps.get(options) {
96+
map.clone()
14997
} else {
150-
self.0.with_dependent(|owner, dependent| {
151-
get_or_init_cache(owner, &dependent.cached_line_only_map, options)
152-
})
98+
let map = self.inner.map(options);
99+
self.cached_maps.insert(options.clone(), map.clone());
100+
map
153101
}
154102
}
155103

156104
fn to_writer(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> {
157-
self.0.borrow_owner().inner.to_writer(writer)
105+
self.inner.to_writer(writer)
158106
}
159107
}
160108

161-
impl StreamChunks for CachedSource {
109+
impl<T: Source + Hash + PartialEq + Eq + 'static> StreamChunks
110+
for CachedSource<T>
111+
{
162112
fn stream_chunks<'a>(
163113
&'a self,
164114
options: &MapOptions,
165115
on_chunk: crate::helpers::OnChunk<'_, 'a>,
166116
on_source: crate::helpers::OnSource<'_, 'a>,
167117
on_name: crate::helpers::OnName<'_, 'a>,
168118
) -> crate::helpers::GeneratedInfo {
169-
let cached = if options.columns {
170-
self.0.borrow_dependent().cached_colomns_map.get()
171-
} else {
172-
self.0.borrow_dependent().cached_line_only_map.get()
173-
};
174-
match cached {
175-
Some(Some(map)) => {
176-
let source = self.0.borrow_owner().inner.rope();
177-
stream_chunks_of_source_map(
178-
source, map, on_chunk, on_source, on_name, options,
179-
)
180-
}
181-
Some(None) => {
182-
let source = self.0.borrow_owner().inner.rope();
183-
stream_chunks_of_raw_source(
184-
source, options, on_chunk, on_source, on_name,
185-
)
186-
}
187-
None => {
188-
if options.columns {
189-
self.0.with_dependent(|owner, dependent| {
190-
let (generated_info, map) = stream_and_get_source_and_map(
191-
&owner.inner,
192-
options,
193-
on_chunk,
194-
on_source,
195-
on_name,
196-
);
197-
dependent.cached_colomns_map.get_or_init(|| {
198-
unsafe { std::mem::transmute::<Option<SourceMap>, Option<SourceMap<'static>>>(map) }.map(Cow::Owned)
199-
});
200-
generated_info
201-
})
119+
let cached_map = self.cached_maps.entry(options.clone());
120+
match cached_map {
121+
Entry::Occupied(entry) => {
122+
let source = self.rope();
123+
if let Some(map) = entry.get() {
124+
#[allow(unsafe_code)]
125+
// SAFETY: We guarantee that once a `SourceMap` is stored in the cache, it will never be removed.
126+
// Therefore, even if we force its lifetime to be longer, the reference remains valid.
127+
// This is based on the following assumptions:
128+
// 1. `SourceMap` will be valid for the entire duration of the application.
129+
// 2. The cached `SourceMap` will not be manually removed or replaced, ensuring the reference's safety.
130+
let map =
131+
unsafe { std::mem::transmute::<&SourceMap, &'a SourceMap>(map) };
132+
stream_chunks_of_source_map(
133+
source, map, on_chunk, on_source, on_name, options,
134+
)
202135
} else {
203-
self.0.with_dependent(|owner, dependent| {
204-
let (generated_info, map) = stream_and_get_source_and_map(
205-
&owner.inner,
136+
stream_chunks_of_raw_source(
137+
source, options, on_chunk, on_source, on_name,
138+
)
139+
}
140+
}
141+
Entry::Vacant(entry) => {
142+
let (generated_info, map) = stream_and_get_source_and_map(
143+
&self.inner as &T,
206144
options,
207145
on_chunk,
208146
on_source,
209147
on_name,
210148
);
211-
dependent.cached_line_only_map.get_or_init(|| {
212-
unsafe { std::mem::transmute::<Option<SourceMap>, Option<SourceMap<'static>>>(map) }.map(Cow::Owned)
213-
});
149+
entry.insert(map);
214150
generated_info
215-
})
216-
}
217151
}
218152
}
219153
}
220154
}
221155

222-
impl Hash for CachedSource {
156+
impl<T> Clone for CachedSource<T> {
157+
fn clone(&self) -> Self {
158+
Self {
159+
inner: self.inner.clone(),
160+
cached_hash: self.cached_hash.clone(),
161+
cached_maps: self.cached_maps.clone(),
162+
}
163+
}
164+
}
165+
166+
impl<T: Source + Hash + PartialEq + Eq + 'static> Hash for CachedSource<T> {
223167
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
224-
let owner = self.0.borrow_owner();
225-
(owner.cached_hash.get_or_init(|| {
168+
(self.cached_hash.get_or_init(|| {
226169
let mut hasher = FxHasher::default();
227-
owner.inner.hash(&mut hasher);
170+
self.inner.hash(&mut hasher);
228171
hasher.finish()
229172
}))
230173
.hash(state);
231174
}
232175
}
233176

234-
impl PartialEq for CachedSource {
177+
impl<T: PartialEq> PartialEq for CachedSource<T> {
235178
fn eq(&self, other: &Self) -> bool {
236-
&self.0.borrow_owner().inner == &other.0.borrow_owner().inner
179+
self.inner == other.inner
237180
}
238181
}
239182

240-
impl Eq for CachedSource {}
183+
impl<T: Eq> Eq for CachedSource<T> {}
241184

242-
impl std::fmt::Debug for CachedSource {
185+
impl<T: std::fmt::Debug> std::fmt::Debug for CachedSource<T> {
243186
fn fmt(
244187
&self,
245188
f: &mut std::fmt::Formatter<'_>,
@@ -251,7 +194,7 @@ impl std::fmt::Debug for CachedSource {
251194
writeln!(
252195
f,
253196
"{indent_str}{:indent$?}",
254-
self.0.borrow_owner().inner,
197+
self.inner,
255198
indent = indent + 2
256199
)?;
257200
write!(f, "{indent_str}).boxed()")
@@ -287,6 +230,25 @@ mod tests {
287230
assert_eq!(map.mappings(), ";;AACA");
288231
}
289232

233+
#[test]
234+
fn should_allow_to_store_and_share_cached_data() {
235+
let original = OriginalSource::new("Hello World", "test.txt");
236+
let source = CachedSource::new(original);
237+
let clone = source.clone();
238+
239+
// fill up cache
240+
let map_options = MapOptions::default();
241+
source.source();
242+
source.buffer();
243+
source.size();
244+
source.map(&map_options);
245+
246+
assert_eq!(
247+
*clone.cached_maps.get(&map_options).unwrap().value(),
248+
source.map(&map_options)
249+
);
250+
}
251+
290252
#[test]
291253
fn should_return_the_correct_size_for_binary_files() {
292254
let source = OriginalSource::new(

0 commit comments

Comments
 (0)