[fonts] Stop loading all fonts into memory at service startup

- AssetCollection now holds a bidirectional map of buffer IDs and paths to font files
- Removed AssetCollection.assets cache in favor of relying on filesystem behavior (VFS can already manage caching and handle-counting)
- Expect slightly more disk reads (previously: read all files exactly once; now: read all files once at startup + individually as needed)

I18N-15 #done

Change-Id: Ia0b1bfe2ed9336ffc140aaaf8d19d1bb6998b001
diff --git a/garnet/bin/fonts/src/font_service.rs b/garnet/bin/fonts/src/font_service.rs
index 5c20e59..2e7d06e 100644
--- a/garnet/bin/fonts/src/font_service.rs
+++ b/garnet/bin/fonts/src/font_service.rs
@@ -12,51 +12,33 @@
 use fidl_fuchsia_fonts as fonts;
 use fidl_fuchsia_mem as mem;
 use fuchsia_async as fasync;
-use fuchsia_zircon as zx;
-use fuchsia_zircon::HandleBased;
 use futures::prelude::*;
 use futures::{future, Future, FutureExt};
 use log::error;
 use std::collections::BTreeMap;
 use std::fs::File;
 use std::path::{Path, PathBuf};
-use std::sync::{Arc, RwLock};
+use std::sync::Arc;
 use unicase::UniCase;
 
-fn clone_buffer(buf: &mem::Buffer) -> Result<mem::Buffer, Error> {
-    let vmo_rights = zx::Rights::BASIC | zx::Rights::READ | zx::Rights::MAP;
-    let vmo = buf.vmo.duplicate_handle(vmo_rights).context("Failed to duplicate VMO handle.")?;
-    Ok(mem::Buffer { vmo, size: buf.size })
-}
-
-struct Asset {
-    path: PathBuf,
-    buffer: RwLock<Option<mem::Buffer>>,
-}
-
-/// Provides cached storage of [`Asset`]s. Should be initialized by [`FontService`].
+/// Stores the relationship between [`Asset`] paths and IDs.
+/// Should be initialized by [`FontService`].
 ///
-/// Each [`Asset`] is assigned an ID equal to its index in `assets`.
-///
-/// # Examples
+/// `path_to_id_map` and `id_to_path_map` form a bidirectional map, so this relation holds:
 /// ```
-/// let asset_collection = AssetCollection::new();
-/// // Load data into asset_collection, including "/path/to/font.ttf".
-/// let path: PathBuf = PathBuf::from("/path/to/font.ttf");
-/// let asset_id: u32 = asset_collection.assets_map.get(path).unwrap();
-/// let asset: &Asset = &asset_collection.assets[asset_id as usize];
+/// assert_eq!(self.path_to_id_map.get(&path), Some(&id));
+/// assert_eq!(self.id_to_path_map.get(&id), Some(&path));
 /// ```
 struct AssetCollection {
     /// Maps [`Asset`] path to ID.
-    assets_map: BTreeMap<PathBuf, u32>,
-
-    /// [`Asset`] cache. Indices correspond to values stored in `self.assets_map`.
-    ///
-    /// Currently, no cache eviction strategy is implemented (I18N-15).
-    assets: Vec<Asset>,
+    path_to_id_map: BTreeMap<PathBuf, u32>,
+    /// Inverse of `path_to_id_map`.
+    id_to_path_map: BTreeMap<u32, PathBuf>,
+    /// Next ID to assign, autoincremented from 0.
+    next_id: u32,
 }
 
-/// Load a `Vmo` from disk into a `Buffer`.
+/// Get `VMO` handle to the [`Asset`] at `path`.
 fn load_asset_to_vmo(path: &Path) -> Result<mem::Buffer, Error> {
     let file = File::open(path)?;
     let vmo = fdio::get_vmo_copy_from_file(&file)?;
@@ -66,48 +48,38 @@
 
 impl AssetCollection {
     fn new() -> AssetCollection {
-        AssetCollection { assets_map: BTreeMap::new(), assets: vec![] }
+        AssetCollection {
+            path_to_id_map: BTreeMap::new(),
+            id_to_path_map: BTreeMap::new(),
+            next_id: 0,
+        }
     }
 
-    /// Lazily add the [`Asset`] found at `path` to the collection and return its ID.
-    /// If it already exists, just return the ID.
+    /// Add the [`Asset`] found at `path` to the collection and return its ID.
+    /// If `path` is already in the collection, return the existing ID.
     ///
-    /// The push onto `assets` is lazy in the sense that the pushed [`Asset`] has an empty `buffer`.
+    /// TODO(seancuff): Switch to updating ID of existing entries. This would allow assets to be
+    /// updated without restarting the service (e.g. installing a newer version of a file). Clients
+    /// would need to check the ID of their currently-held asset against the response.
     fn add_or_get_asset_id(&mut self, path: &Path) -> u32 {
-        if let Some(id) = self.assets_map.get(path) {
+        if let Some(id) = self.path_to_id_map.get(&path.to_path_buf()) {
             return *id;
         }
-
-        let id = self.assets.len() as u32;
-        self.assets.push(Asset { path: path.to_path_buf(), buffer: RwLock::new(None) });
-        self.assets_map.insert(path.to_path_buf(), id);
+        let id = self.next_id;
+        self.id_to_path_map.insert(id, path.to_path_buf());
+        self.path_to_id_map.insert(path.to_path_buf(), id);
+        self.next_id += 1;
         id
     }
 
-    /// Get a `Buffer` holding the `Vmo` for the [`Asset`] corresponding to `id`, using the
-    /// cache if possible. On a cache miss, the [`Asset`] is cached unconditionally.
+    /// Get a `Buffer` holding the `Vmo` for the [`Asset`] corresponding to `id`.
     fn get_asset(&self, id: u32) -> Result<mem::Buffer, Error> {
-        assert!(id < self.assets.len() as u32);
-
-        let asset = &self.assets[id as usize];
-
-        if let Some(cached) = asset.buffer.read().unwrap().as_ref() {
-            return clone_buffer(cached);
+        if let Some(path) = self.id_to_path_map.get(&id) {
+            let buf = load_asset_to_vmo(path)
+                .with_context(|_| format!("Failed to load {}", path.to_string_lossy()))?;
+            return Ok(buf);
         }
-
-        let buf = load_asset_to_vmo(&asset.path)
-            .with_context(|_| format!("Failed to load {}", asset.path.to_string_lossy()))?;
-        let buf_clone = clone_buffer(&buf)?;
-        *asset.buffer.write().unwrap() = Some(buf);
-        Ok(buf_clone)
-    }
-
-    /// Clear the cache by deallocating all [`Asset`]'s `buffer`s.
-    /// Does not reduce the size of `assets`.
-    fn reset_cache(&mut self) {
-        for asset in self.assets.iter_mut() {
-            *asset.buffer.write().unwrap() = None;
-        }
+        Err(format_err!("No asset found with id {}", id))
     }
 }
 
@@ -259,10 +231,6 @@
                 }
             }
         }
-
-        // Flush the cache. Font files will be loaded again when they are needed.
-        self.assets.reset_cache();
-
         Ok(())
     }
 
diff --git a/garnet/bin/fonts/tests/font_provider_test.rs b/garnet/bin/fonts/tests/font_provider_test.rs
index b2a8226..621545b 100644
--- a/garnet/bin/fonts/tests/font_provider_test.rs
+++ b/garnet/bin/fonts/tests/font_provider_test.rs
@@ -14,6 +14,19 @@
 use fuchsia_zircon as zx;
 use fuchsia_zircon::AsHandleRef;
 
+macro_rules! assert_buf_eq {
+    ($font_info_a:ident, $font_info_b:ident) => {
+        assert!(
+            $font_info_a.buffer_id == $font_info_b.buffer_id,
+            "{}.buffer_id == {}.buffer_id\n{0}: {:?}\n{1}: {:?}",
+            stringify!($font_info_a),
+            stringify!($font_info_b),
+            $font_info_a,
+            $font_info_b
+        )
+    };
+}
+
 #[derive(Debug, Eq, PartialEq)]
 struct FontInfo {
     vmo_koid: zx::Koid,
@@ -23,7 +36,9 @@
 }
 
 async fn get_font_info(
-    font_provider: &fonts::ProviderProxy, name: Option<String>, language: Option<Vec<String>>,
+    font_provider: &fonts::ProviderProxy,
+    name: Option<String>,
+    language: Option<Vec<String>>,
     character: char,
 ) -> Result<FontInfo, Error> {
     let font = await!(font_provider.get_font(&mut fonts::Request {
@@ -51,7 +66,8 @@
 }
 
 async fn get_font_info_basic(
-    font_provider: &fonts::ProviderProxy, name: Option<String>,
+    font_provider: &fonts::ProviderProxy,
+    name: Option<String>,
 ) -> Result<FontInfo, Error> {
     await!(get_font_info(font_provider, name, None, '\0'))
 }
@@ -75,22 +91,16 @@
 
     let default =
         await!(get_font_info_basic(&font_provider, None)).context("Failed to load default font")?;
-    let roboto = await!(get_font_info_basic(
-        &font_provider,
-        Some("Roboto".to_string())
-    ))
-    .context("Failed to load Roboto")?;
-    let material_icons = await!(get_font_info_basic(
-        &font_provider,
-        Some("Material Icons".to_string())
-    ))
-    .context("Failed to load Material Icons")?;
+    let roboto = await!(get_font_info_basic(&font_provider, Some("Roboto".to_string())))
+        .context("Failed to load Roboto")?;
+    let material_icons =
+        await!(get_font_info_basic(&font_provider, Some("Material Icons".to_string())))
+            .context("Failed to load Material Icons")?;
 
     // Roboto should be returned by default.
-    assert!(default == roboto);
+    assert_buf_eq!(default, roboto);
 
     // Material Icons request should return a different font.
-    assert!(default.vmo_koid != material_icons.vmo_koid);
     assert!(default.buffer_id != material_icons.buffer_id);
 
     Ok(())
@@ -102,17 +112,14 @@
     let (_app, font_provider) = start_provider_with_default_fonts()?;
 
     // Both requests should return the same font.
-    let materialicons = await!(get_font_info_basic(
-        &font_provider,
-        Some("MaterialIcons".to_string())
-    ))
-    .context("Failed to load MaterialIcons")?;
-    let material_icons = await!(get_font_info_basic(
-        &font_provider,
-        Some("Material Icons".to_string())
-    ))
-    .context("Failed to load Material Icons")?;
-    assert!(materialicons == material_icons);
+    let materialicons =
+        await!(get_font_info_basic(&font_provider, Some("MaterialIcons".to_string())))
+            .context("Failed to load MaterialIcons")?;
+    let material_icons =
+        await!(get_font_info_basic(&font_provider, Some("Material Icons".to_string())))
+            .context("Failed to load Material Icons")?;
+
+    assert_buf_eq!(materialicons, material_icons);
 
     Ok(())
 }
@@ -163,8 +170,16 @@
     ))
     .context("Failed to load NotoSansCJK font")?;
 
-    assert!(noto_sans_cjk_ja.vmo_koid == noto_sans_cjk_sc.vmo_koid);
-    assert!(noto_sans_cjk_ja.index != noto_sans_cjk_sc.index);
+    assert_buf_eq!(noto_sans_cjk_ja, noto_sans_cjk_sc);
+
+    assert!(
+        noto_sans_cjk_ja.index != noto_sans_cjk_sc.index,
+        "noto_sans_cjk_ja.index != noto_sans_cjk_sc.index\n \
+         noto_sans_cjk_ja.index: {:?}\n \
+         noto_sans_cjk_sc.index: {:?}",
+        noto_sans_cjk_ja,
+        noto_sans_cjk_sc
+    );
 
     Ok(())
 }
@@ -191,7 +206,7 @@
     .context("Failed to load NotoSansCJK font")?;
 
     // Same font should be returned in both cases.
-    assert!(noto_sans_cjk_ja == noto_sans_cjk_ja_by_char);
+    assert_buf_eq!(noto_sans_cjk_ja, noto_sans_cjk_ja_by_char);
 
     Ok(())
 }
@@ -221,7 +236,7 @@
     // The query above requested Roboto Slab, so it's expected to return
     // Noto Serif CJK instead of Noto Sans CJK because Roboto Slab and
     // Noto Serif CJK are both in serif fallback group.
-    assert!(noto_serif_cjk_ja == noto_serif_cjk_ja_by_char);
+    assert_buf_eq!(noto_serif_cjk_ja, noto_serif_cjk_ja_by_char);
 
     Ok(())
 }