按需(惰性)输入

如果你可以轻松地预先提供所有输入,则 Salsa 输入工作得最好。然而,有时输入集是事先不知道的。

一个典型的例子是从磁盘读取文件。虽然 Salsa 可以急切地扫描特定目录并把内存中创建的文件树作为输入结构体,但更直接的方法是延迟读取文件。

也就是说,当某个查询第一次请求文件的文本时:

  1. 从磁盘读取文件并缓存它
  2. 为此路径设置文件系统监视器 (file-system watcher)
  3. 在监视器发送更改通知的时候,更新缓存的文件

这可以在 Salsa 中使用数据库中的缓存输入和增加数据库 trait 方法来从缓存中获得。

一个完整的可运行的文件监视示例见 lazy-input

#[salsa::input]
struct File {
    path: PathBuf,
    #[return_ref]
    contents: String,
}

trait Db: salsa::DbWithJar<Jar> {
    fn input(&self, path: PathBuf) -> Result<File>;
}

#[salsa::db(Jar)]
struct Database {
    storage: salsa::Storage<Self>,
    logs: Mutex<Vec<String>>,
    files: DashMap<PathBuf, File>,
    file_watcher: Mutex<Debouncer<RecommendedWatcher>>,
}

impl Database {
    fn new(tx: Sender<DebounceEventResult>) -> Self {
        let storage = Default::default();
        Self {
            storage,
            logs: Default::default(),
            files: DashMap::new(),
            file_watcher: Mutex::new(new_debouncer(Duration::from_secs(1), None, tx).unwrap()),
        }
    }
}

impl Db for Database {
    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))
            }
        })
    }
}
  • Db trait 上定义一个按需获取 File 的方法(它只需要 &dyn Db 而不需要 &mut dyn Db
  • 每个文件应该只有一个输入结构体,所以给方法实现缓存(DashMap 就像 RwLock<HashMap> 一样)

然后,执行顶级查询的驱动代码负责在文件更改通知到达时更新文件内容。它更新 Salsa 输入的方式与更新任何其他输入的方式相同。

这里,我们实现了一个简单的驱动循环,每当文件发生变化时,它都会重新编译代码。你可以使用日志来检查是否只重新计算了可能已更改的查询。

{{#include ../../../examples-2022/lazy-input/src/main.rs:main}}