Repository: denoland/deno
Pull request: perf(cache): single cache file for typescript emit

File format:

```
<content>\n// denoCacheMetadata=<source-hash>,<emit-hash>
```

* `<content>` - Emitted js file
* `<source-hash>` - xxHash of the original TypeScript file. This keeps it in sync with the original source.
* `<emit-hash>` - xxHash of content and cli version.

File format considerations:

1. Want to cache bust when:
   1. Someone modifies the source.
   1. Someone edits this file such that its out of sync with the source, though they could edit the emit hash if they wanted, but this discourages the practice (a hash of the content is stored in the emit hash).
   1. The CLI version changes (this is stored as part of the emit hash)
1. Want to store the content first so the loaded bytes can be truncated to the content.
1. Want this file to work with tools that forget to strip off the last line or deal with it (though they really should because not reading it means they might be using stale information)
  
Loading 10k typescript modules:

```
Benchmark 1: ./deno_main run main.ts
  Time (mean ± σ):     763.3 ms ±   5.4 ms    [User: 322.0 ms, System: 444.2 ms]
  Range (min … max):   755.8 ms … 774.6 ms    10 runs
 
Benchmark 2: ./deno_new run main.ts
  Time (mean ± σ):     596.1 ms ±  12.2 ms    [User: 296.1 ms, System: 302.7 ms]
  Range (min … max):   583.6 ms … 615.6 ms    10 runs
 
Summary
  ./deno_new run main.ts ran
    1.28 ± 0.03 times faster than ./deno_main run main.ts
```

Part of https://github.com/denoland/deno/issues/17703

Closes #24979

Changed files:
filename: cli/cache/emit.rs
patch: @@ -5,33 +5,25 @@ use std::path::PathBuf;
 use deno_ast::ModuleSpecifier;
 use deno_core::anyhow::anyhow;
 use deno_core::error::AnyError;
-use deno_core::serde_json;
 use deno_core::unsync::sync::AtomicFlag;
-use serde::Deserialize;
-use serde::Serialize;
 
 use super::DiskCache;
-use super::FastInsecureHasher;
-
-#[derive(Debug, Deserialize, Serialize)]
-struct EmitMetadata {
-  pub source_hash: u64,
-  pub emit_hash: u64,
-}
 
 /// The cache that stores previously emitted files.
 pub struct EmitCache {
   disk_cache: DiskCache,
-  cli_version: &'static str,
   emit_failed_flag: AtomicFlag,
+  file_serializer: EmitFileSerializer,
 }
 
 impl EmitCache {
   pub fn new(disk_cache: DiskCache) -> Self {
     Self {
       disk_cache,
-      cli_version: crate::version::DENO_VERSION_INFO.deno,
       emit_failed_flag: Default::default(),
+      file_serializer: EmitFileSerializer {
+        cli_version: crate::version::DENO_VERSION_INFO.deno,
+      },
     }
   }
 
@@ -48,37 +40,11 @@ impl EmitCache {
     specifier: &ModuleSpecifier,
     expected_source_hash: u64,
   ) -> Option<Vec<u8>> {
-    let meta_filename = self.get_meta_filename(specifier)?;
     let emit_filename = self.get_emit_filename(specifier)?;
-
-    // load and verify the meta data file is for this source and CLI version
-    let bytes = self.disk_cache.get(&meta_filename).ok()?;
-    let meta: EmitMetadata = serde_json::from_slice(&bytes).ok()?;
-    if meta.source_hash != expected_source_hash {
-      return None;
-    }
-
-    // load and verify the emit is for the meta data
-    let emit_bytes = self.disk_cache.get(&emit_filename).ok()?;
-    if meta.emit_hash != compute_emit_hash(&emit_bytes, self.cli_version) {
-      return None;
-    }
-
-    // everything looks good, return it
-    Some(emit_bytes)
-  }
-
-  /// Gets the filepath which stores the emit.
-  pub fn get_emit_filepath(
-    &self,
-    specifier: &ModuleSpecifier,
-  ) -> Option<PathBuf> {
-    Some(
-      self
-        .disk_cache
-        .location
-        .join(self.get_emit_filename(specifier)?),
-    )
+    let bytes = self.disk_cache.get(&emit_filename).ok()?;
+    self
+      .file_serializer
+      .deserialize(bytes, expected_source_hash)
   }
 
   /// Sets the emit code in the cache.
@@ -107,32 +73,26 @@ impl EmitCache {
       return Ok(());
     }
 
-    let meta_filename = self
-      .get_meta_filename(specifier)
-      .ok_or_else(|| anyhow!("Could not get meta filename."))?;
     let emit_filename = self
       .get_emit_filename(specifier)
       .ok_or_else(|| anyhow!("Could not get emit filename."))?;
-
-    // save the metadata
-    let metadata = EmitMetadata {
-      source_hash,
-      emit_hash: compute_emit_hash(code, self.cli_version),
-    };
-    self
-      .disk_cache
-      .set(&meta_filename, &serde_json::to_vec(&metadata)?)?;
-
-    // save the emit source
-    self.disk_cache.set(&emit_filename, code)?;
+    let cache_data = self.file_serializer.serialize(code, source_hash);
+    self.disk_cache.set(&emit_filename, &cache_data)?;
 
     Ok(())
   }
 
-  fn get_meta_filename(&self, specifier: &ModuleSpecifier) -> Option<PathBuf> {
-    self
-      .disk_cache
-      .get_cache_filename_with_extension(specifier, "meta")
+  /// Gets the filepath which stores the emit.
+  pub fn get_emit_filepath(
+    &self,
+    specifier: &ModuleSpecifier,
+  ) -> Option<PathBuf> {
+    Some(
+      self
+        .disk_cache
+        .location
+        .join(self.get_emit_filename(specifier)?),
+    )
   }
 
   fn get_emit_filename(&self, specifier: &ModuleSpecifier) -> Option<PathBuf> {
@@ -142,15 +102,68 @@ impl EmitCache {
   }
 }
 
-fn compute_emit_hash(bytes: &[u8], cli_version: &str) -> u64 {
-  // it's ok to use an insecure hash here because
-  // if someone can change the emit source then they
-  // can also change the version hash
-  FastInsecureHasher::new_without_deno_version() // use cli_version param instead
-    .write(bytes)
-    // emit should not be re-used between cli versions
-    .write_str(cli_version)
-    .finish()
+const LAST_LINE_PREFIX: &str = "\n// denoCacheMetadata=";
+
+struct EmitFileSerializer {
+  cli_version: &'static str,
+}
+
+impl EmitFileSerializer {
+  pub fn deserialize(
+    &self,
+    mut bytes: Vec<u8>,
+    expected_source_hash: u64,
+  ) -> Option<Vec<u8>> {
+    let last_newline_index = bytes.iter().rposition(|&b| b == b'\n')?;
+    let (content, last_line) = bytes.split_at(last_newline_index);
+    let hashes = last_line.strip_prefix(LAST_LINE_PREFIX.as_bytes())?;
+    let hashes = String::from_utf8_lossy(hashes);
+    let (source_hash, emit_hash) = hashes.split_once(',')?;
+
+    // verify the meta data file is for this source and CLI version
+    let source_hash = source_hash.parse::<u64>().ok()?;
+    if source_hash != expected_source_hash {
+      return None;
+    }
+    let emit_hash = emit_hash.parse::<u64>().ok()?;
+    // prevent using an emit from a different cli version or emits that were tampered with
+    if emit_hash != self.compute_emit_hash(content) {
+      return None;
+    }
+
+    // everything looks good, truncate and return it
+    bytes.truncate(content.len());
+    Some(bytes)
+  }
+
+  pub fn serialize(&self, code: &[u8], source_hash: u64) -> Vec<u8> {
+    let source_hash = source_hash.to_string();
+    let emit_hash = self.compute_emit_hash(code).to_string();
+    let capacity = code.len()
+      + LAST_LINE_PREFIX.len()
+      + source_hash.len()
+      + 1
+      + emit_hash.len();
+    let mut cache_data = Vec::with_capacity(capacity);
+    cache_data.extend(code);
+    cache_data.extend(LAST_LINE_PREFIX.as_bytes());
+    cache_data.extend(source_hash.as_bytes());
+    cache_data.push(b',');
+    cache_data.extend(emit_hash.as_bytes());
+    debug_assert_eq!(cache_data.len(), capacity);
+    cache_data
+  }
+
+  fn compute_emit_hash(&self, bytes: &[u8]) -> u64 {
+    // it's ok to use an insecure hash here because
+    // if someone can change the emit source then they
+    // can also change the version hash
+    crate::cache::FastInsecureHasher::new_without_deno_version() // use cli_version property instead
+      .write(bytes)
+      // emit should not be re-used between cli versions
+      .write_str(self.cli_version)
+      .finish()
+  }
 }
 
 #[cfg(test)]
@@ -165,7 +178,9 @@ mod test {
     let disk_cache = DiskCache::new(temp_dir.path().as_path());
     let cache = EmitCache {
       disk_cache: disk_cache.clone(),
-      cli_version: "1.0.0",
+      file_serializer: EmitFileSerializer {
+        cli_version: "1.0.0",
+      },
       emit_failed_flag: Default::default(),
     };
     let to_string =
@@ -197,7 +212,9 @@ mod test {
     // try changing the cli version (should not load previous ones)
     let cache = EmitCache {
       disk_cache: disk_cache.clone(),
-      cli_version: "2.0.0",
+      file_serializer: EmitFileSerializer {
+        cli_version: "2.0.0",
+      },
       emit_failed_flag: Default::default(),
     };
     assert_eq!(cache.get_emit_code(&specifier1, 10), None);
@@ -206,7 +223,9 @@ mod test {
     // recreating the cache should still load the data because the CLI version is the same
     let cache = EmitCache {
       disk_cache,
-      cli_version: "2.0.0",
+      file_serializer: EmitFileSerializer {
+        cli_version: "2.0.0",
+      },
       emit_failed_flag: Default::default(),
     };
     assert_eq!(