按需(惰性)输入
如果你可以很轻松地在一开始就提供全部输入, 那么 Salsa 的 input 会用得最顺手。 但有时候,输入集合并不是预先已知的。
一个典型例子就是从磁盘读取文件。 你当然可以预先扫描某个目录, 把整棵文件树作为 Salsa input struct 建出来; 但更直接的做法通常是惰性地读取文件。 也就是说,当某个查询第一次请求某个文件的文本时:
- 从磁盘读取文件并把它缓存起来。
- 为这个路径安装文件系统监听器。
- 当监听器收到文件变化通知时,更新缓存中的文件内容。
在 Salsa 里,这种方案是可行的: 你可以把 input 缓存在数据库结构体中, 再给数据库 trait 增加一个方法,从这个缓存里按需取出它们。
一个完整、可运行的文件监听示例可以在 lazy-input 示例 中找到。
它的大致结构如下:
#[salsa::input]
struct File {
path: PathBuf,
#[returns(ref)]
contents: String,
}
#[salsa::db]
trait Db: salsa::Database {
fn input(&self, path: PathBuf) -> Result<File>;
}
#[salsa::db]
#[derive(Clone)]
struct LazyInputDatabase {
storage: Storage<Self>,
logs: Arc<Mutex<Vec<String>>>,
files: DashMap<PathBuf, File>,
file_watcher: Arc<Mutex<Debouncer<RecommendedWatcher>>>,
}
impl LazyInputDatabase {
fn new(tx: Sender<DebounceEventResult>) -> Self {
let logs: Arc<Mutex<Vec<String>>> = Default::default();
Self {
storage: Storage::new(Some(Box::new({
let logs = logs.clone();
move |event| {
// don't log boring events
if let salsa::EventKind::WillExecute { .. } = event.kind {
logs.lock().unwrap().push(format!("{event:?}"));
}
}
}))),
logs,
files: DashMap::new(),
file_watcher: Arc::new(Mutex::new(
new_debouncer(Duration::from_secs(1), tx).unwrap(),
)),
}
}
}
#[salsa::db]
impl salsa::Database for LazyInputDatabase {}
#[salsa::db]
impl Db for LazyInputDatabase {
fn input(&self, path: PathBuf) -> Result<File> {
let path = path
.canonicalize()
.wrap_err_with(|| format!("Failed to read {}", path.display()))?;
Ok(match self.files.entry(path.clone()) {
// If the file already exists in our cache then just return it.
Entry::Occupied(entry) => *entry.get(),
// If we haven't read this file yet set up the watch, read the
// contents, store it in the cache, and return it.
Entry::Vacant(entry) => {
// Set up the watch before reading the contents to try to avoid
// race conditions.
let watcher = &mut *self.file_watcher.lock().unwrap();
watcher
.watcher()
.watch(&path, RecursiveMode::NonRecursive)
.unwrap();
let contents = std::fs::read_to_string(&path)
.wrap_err_with(|| format!("Failed to read {}", path.display()))?;
*entry.insert(File::new(self, path, contents))
}
})
}
}
- 我们在
Dbtrait 上声明一个按需获取Fileinput 的方法 (它只需要&dyn Db,不需要&mut dyn Db)。 - 每个文件应该只对应一个 input struct,
所以我们会用一个缓存来实现这个方法(
DashMap可以理解为类似RwLock<HashMap>的结构)。
负责发起顶层查询的驱动代码,在收到文件变化通知时, 就要负责更新文件内容。 它更新 Salsa input 的方式,和更新其他 input 没有什么区别。
这里我们实现了一个简单的驱动循环: 每当文件发生变化,就重新编译代码。 你可以通过日志观察到, 只有那些可能受到影响的查询才会被重新计算。
fn main() -> Result<()> {
// Create the channel to receive file change events.
let (tx, rx) = unbounded();
let mut db = LazyInputDatabase::new(tx);
let initial_file_path = std::env::args_os()
.nth(1)
.ok_or_else(|| eyre!("Usage: ./lazy-input <input-file>"))?;
// Create the initial input using the input method so that changes to it
// will be watched like the other files.
let initial = db.input(initial_file_path.into())?;
loop {
// Compile the code starting at the provided input, this will read other
// needed files using the on-demand mechanism.
let sum = compile(&db, initial);
let diagnostics = compile::accumulated::<Diagnostic>(&db, initial);
if diagnostics.is_empty() {
println!("Sum is: {sum}");
} else {
for diagnostic in diagnostics {
println!("{}", diagnostic.0);
}
}
for log in db.logs.lock().unwrap().drain(..) {
eprintln!("{log}");
}
// Wait for file change events, the output can't change unless the
// inputs change.
for event in rx.recv()?.unwrap() {
let path = event.path.canonicalize().wrap_err_with(|| {
format!("Failed to canonicalize path {}", event.path.display())
})?;
let file = match db.files.get(&path) {
Some(file) => *file,
None => continue,
};
// `path` has changed, so read it and update the contents to match.
// This creates a new revision and causes the incremental algorithm
// to kick in, just like any other update to a salsa input.
let contents = std::fs::read_to_string(path)
.wrap_err_with(|| format!("Failed to read file {}", event.path.display()))?;
file.set_contents(&mut db).to(contents);
}
}
}