blob: 8c78e5408963a5fb50ee3342ec97feb02e0ae147 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
super::{cache::Cache, Asset, AssetId, AssetLoader},
anyhow::Error,
fidl_fuchsia_fonts::CacheMissPolicy,
fidl_fuchsia_io as io, fidl_fuchsia_mem as mem,
fuchsia_inspect::{self as finspect, Property},
fuchsia_url::pkg_url::PkgUrl,
futures::{join, lock::Mutex},
manifest::v2,
std::{collections::BTreeMap, hash::Hash, path::PathBuf},
thiserror::Error,
};
/// A complete asset location, including the file name. (`AssetLocation` only includes the
/// directory or package.)
#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
struct FullAssetLocation {
/// Directory or package location the file.
location: v2::AssetLocation,
/// Name of the font asset file.
file_name: String,
}
impl FullAssetLocation {
/// If the asset represents a local file, returns the package-relative path to the file.
/// Otherwise, returns `None`.
pub fn local_path(&self) -> Option<PathBuf> {
match &self.location {
v2::AssetLocation::LocalFile(locator) => {
Some(locator.directory.join(self.file_name.clone()))
}
_ => None,
}
}
/// Returns a string representing either an absolute local path or an absolute package resource
/// URL, e.g. "/config/data/Alpha.ttf" or
/// "fuchsia-pkg://fuchsia.com/font-package-alpha-ttf#Alpha.ttf".
fn path_or_url(&self) -> String {
match &self.location {
v2::AssetLocation::LocalFile(locator) => {
locator.directory.join(&self.file_name).to_string_lossy().to_string()
}
v2::AssetLocation::Package(locator) => PkgUrl::new_resource(
locator.url.host().to_owned(),
locator.url.path().to_owned(),
locator.url.package_hash().map(|s| s.to_owned()),
self.file_name.to_owned(),
)
.unwrap()
.to_string(),
}
}
}
/// Builder for [`AssetCollection`]. Allows populating all the fields that are then immutable for
/// the lifetime of the `AssetCollection`.
///
/// Create an instance with [`new()`](AssetCollectionBuilder::new), then populate using
/// [`add_or_get_asset_id()`](AssetCollectionBuilder::add_or_get_asset_id), and finally construct
/// an `AssetCollection` using [`build()`](AssetCollectionBuilder::build).
///
/// The builder is consumed when `build()` is called.
#[derive(Debug)]
pub struct AssetCollectionBuilder<L>
where
L: AssetLoader,
{
asset_loader: L,
cache_capacity_bytes: u64,
id_to_location_map: BTreeMap<AssetId, FullAssetLocation>,
/// Used for quickly looking up assets that have already been added.
location_to_id_map: BTreeMap<FullAssetLocation, AssetId>,
/// Used for looking up assets for the fallback chain.
file_name_to_id_map: BTreeMap<String, AssetId>,
next_id: u32,
inspect_node: finspect::Node,
}
impl<L> AssetCollectionBuilder<L>
where
L: AssetLoader,
{
/// Creates a new, empty `AssetCollectionBuilder` with the given asset loader, cache capacity,
/// and parent Inspect node.
///
/// A child node for the asset collection is created immediately in the builder, to avoid having
/// to manage the lifetime of the borrowed parent node. (If the builder is dropped without
/// calling `build()`, the node is also dropped.)
pub fn new(
asset_loader: L,
cache_capacity_bytes: u64,
parent_inspect_node: &finspect::Node,
) -> AssetCollectionBuilder<L> {
AssetCollectionBuilder {
asset_loader,
cache_capacity_bytes,
id_to_location_map: BTreeMap::new(),
location_to_id_map: BTreeMap::new(),
file_name_to_id_map: BTreeMap::new(),
next_id: 0,
// TODO(fxbug.dev/45391): Factor this out into AssectCollectionInspectData
inspect_node: AssetCollectionInspectData::make_node(parent_inspect_node),
}
}
/// Adds a [manifest `Asset`](manifest::v2::Asset) to the builder and returns a new or existing
/// asset ID.
pub fn add_or_get_asset_id(&mut self, manifest_asset: &v2::Asset) -> AssetId {
let full_location = FullAssetLocation {
file_name: manifest_asset.file_name.clone(),
location: manifest_asset.location.clone(),
};
if let Some(id) = self.location_to_id_map.get(&full_location) {
return *id;
}
let id = AssetId(self.next_id);
self.id_to_location_map.insert(id, full_location.clone());
self.location_to_id_map.insert(full_location.clone(), id);
self.file_name_to_id_map.insert(manifest_asset.file_name.clone(), id);
self.next_id += 1;
id
}
/// Looks up the existing `AssetId` for a given asset file name.
pub fn get_asset_id_by_name(&self, file_name: &String) -> Option<AssetId> {
self.file_name_to_id_map.get(file_name).map(|asset_id| *asset_id)
}
/// Updates the capacity of the in-memory cache.
pub fn set_cache_capacity(&mut self, cache_capacity_bytes: u64) {
self.cache_capacity_bytes = cache_capacity_bytes;
}
/// Consumes the builder and creates an [`AssetCollection`].
pub fn build(self) -> AssetCollection<L> {
let inspect_data =
AssetCollectionInspectData::new(self.inspect_node, &self.id_to_location_map);
AssetCollection {
asset_loader: self.asset_loader,
id_to_location_map: self.id_to_location_map,
id_to_dir_map: Mutex::new(BTreeMap::new()),
cache: Mutex::new(Cache::new(self.cache_capacity_bytes, &inspect_data.node)),
inspect_data: Mutex::new(inspect_data),
}
}
}
/// A lazy collection of `mem::Buffer`s, each representing a single font asset (file).
///
/// Assets can be retrieved by [`AssetId`] (which are assigned at service startup). The collection
/// knows the location (local file or Fuchsia package) of every `AssetId`.
#[derive(Debug)]
pub struct AssetCollection<L: AssetLoader> {
asset_loader: L,
/// Maps asset ID to its location. Immutable over the lifetime of the collection.
id_to_location_map: BTreeMap<AssetId, FullAssetLocation>,
/// Maps asset IDs to previously-resolved directory handles for packages.
///
/// `DirectoryProxy` contains an `Arc`, so it is safe to clone. We can use a directory for an
/// extended period without keeping the entire map locked.
//
// TODO(fxbug.dev/8904): If resource use becomes a concern, consider replacing with a LRU cache.
id_to_dir_map: Mutex<BTreeMap<AssetId, io::DirectoryProxy>>,
/// Cache of memory buffers
cache: Mutex<Cache>,
/// Inspect nodes
inspect_data: Mutex<AssetCollectionInspectData>,
}
impl<L: AssetLoader> AssetCollection<L> {
/// Gets a `Buffer` holding the `Vmo` for the [`Asset`] corresponding to `id`, using the cache
/// if possible. If the `Buffer` is not cached, this may involve reading a file on disk or even
/// resolving an ephemeral package.
pub async fn get_asset(
&self,
id: AssetId,
cache_miss_policy: CacheMissPolicy,
) -> Result<mem::Buffer, AssetCollectionError> {
let location =
self.id_to_location_map.get(&id).ok_or_else(|| AssetCollectionError::UnknownId(id))?;
// Note that we don't keep the cache locked while fetching.
let mut cache_lock = self.cache.lock().await;
let buffer = match cache_lock.get(id) {
Some(cached_asset) => {
drop(cache_lock);
cached_asset.buffer
}
None => {
drop(cache_lock);
let buffer = match &location.location {
v2::AssetLocation::LocalFile(_) => {
let file_path = location.local_path().unwrap();
self.asset_loader.load_vmo_from_path(&file_path)
}
v2::AssetLocation::Package(package_locator) => {
self.get_package_asset(
id,
package_locator,
&location.file_name,
cache_miss_policy,
)
.await
}
}?;
let (mut cache_lock, mut inspect_data_lock) =
join!(self.cache.lock(), self.inspect_data.lock());
let (asset, is_cached, popped_assets) = cache_lock.push(Asset { id, buffer });
inspect_data_lock.update_after_cache_push(&asset, is_cached, &popped_assets);
asset.buffer
}
};
Ok(buffer)
}
/// Looks up the local path or URL for a given asset ID.
pub fn get_asset_path_or_url(&self, asset_id: AssetId) -> Option<String> {
self.id_to_location_map.get(&asset_id).map(FullAssetLocation::path_or_url)
}
/// Returns the number of assets in the collection
#[allow(dead_code)]
#[cfg(test)]
pub fn len(&self) -> usize {
self.id_to_location_map.len()
}
/// Returns the capacity of the in-memory cache in bytes.
#[cfg(test)]
pub async fn cache_capacity_bytes(&self) -> u64 {
self.cache.lock().await.capacity_bytes()
}
/// Gets a `Buffer` for the [`Asset`] corresponding to `id` from a Fuchsia package, using the
/// directory proxy cache if possible.
async fn get_package_asset(
&self,
id: AssetId,
package_locator: &v2::PackageLocator,
file_name: &str,
policy: CacheMissPolicy,
) -> Result<mem::Buffer, AssetCollectionError> {
// Note that we don't keep the cache locked while fetching.
let directory_cache_lock = self.id_to_dir_map.lock().await;
let directory_proxy = match directory_cache_lock.get(&id) {
Some(dir_proxy) => {
let dir_proxy = Clone::clone(dir_proxy);
drop(directory_cache_lock);
Ok(dir_proxy)
}
None => {
match policy {
CacheMissPolicy::BlockUntilDownloaded => {
drop(directory_cache_lock);
self.fetch_and_cache_package_directory(id, package_locator).await
}
// TODO(fxbug.dev/8904): Implement async fetching and notification
_ => Err(AssetCollectionError::UncachedEphemeral {
file_name: file_name.to_string(),
package_locator: package_locator.clone(),
}),
}
}
}?;
self.asset_loader
.load_vmo_from_directory_proxy(directory_proxy, package_locator, file_name)
.await
}
/// Resolves the requested package and adds its `Directory` to our cache.
async fn fetch_and_cache_package_directory(
&self,
asset_id: AssetId,
package_locator: &v2::PackageLocator,
) -> Result<io::DirectoryProxy, AssetCollectionError> {
let dir_proxy = self.asset_loader.fetch_package_directory(package_locator).await?;
// Cache directory handle
let mut directory_cache = self.id_to_dir_map.lock().await;
directory_cache.insert(asset_id, Clone::clone(&dir_proxy));
// TODO(fxbug.dev/8904): For "universe" fonts, send event to clients.
Ok(dir_proxy)
}
}
/// Possible errors when trying to retrieve an asset `Buffer` from the collection.
#[derive(Debug, Error)]
pub enum AssetCollectionError {
/// Unknown `AssetId`
#[error("No asset found with id {:?}", _0)]
UnknownId(AssetId),
/// A file within the font service's namespace could not be accessed
#[error("Local file not accessible: {:?}", _0)]
LocalFileNotAccessible(PathBuf, #[source] Error),
/// Failed to connect to the Font Resolver service
#[error("Service connection error: {:?}", _0)]
ServiceConnectionError(#[source] Error),
/// The Font Resolver could not resolve the requested package
#[error("Package resolver error when resolving {:?}: {:?}", _0, _1)]
PackageResolverError(v2::PackageLocator, #[source] Error),
/// The requested package was resolved, but its font file could not be read
#[error("Error when reading file {:?} from {:?}: {:?}", file_name, package_locator, cause)]
PackagedFileError {
/// Name of the file
file_name: String,
/// Package that we're trying to read
package_locator: v2::PackageLocator,
#[source]
cause: Error,
},
/// The client requested an empty response if the asset wasn't cached, and the asset is indeed
/// not cached
#[allow(dead_code)]
#[error("Requested asset needs to be loaded ephemerally but is not yet cached")]
UncachedEphemeral {
/// Name of the file
file_name: String,
/// Package that we want to read
package_locator: v2::PackageLocator,
},
}
/// Inspect data for a single asset.
#[allow(dead_code)]
struct AssetInspectData {
node: finspect::Node,
caching: finspect::StringProperty,
size: Option<finspect::UintProperty>,
}
impl AssetInspectData {
const CACHING_CACHED: &'static str = "cached";
const CACHING_UNCACHED: &'static str = "uncached";
fn new(parent_node: &finspect::Node, asset_id: &AssetId, location: &FullAssetLocation) -> Self {
let node = parent_node.create_child(format!("{}", asset_id.0));
node.record_string("location", location.path_or_url());
let caching = node.create_string("caching", Self::CACHING_UNCACHED);
let size = None;
AssetInspectData { node, caching, size }
}
}
/// Inspect data for a [`Collection`].
#[allow(dead_code)]
pub struct AssetCollectionInspectData {
node: finspect::Node,
assets_node: finspect::Node,
assets: BTreeMap<AssetId, AssetInspectData>,
}
impl AssetCollectionInspectData {
/// Creates a new inspect data container with the given _self_ node (not parent node) and asset
/// locations.
fn new(
node: finspect::Node,
id_to_location_map: &BTreeMap<AssetId, FullAssetLocation>,
) -> Self {
node.record_uint("count", id_to_location_map.len() as u64);
let assets_node = node.create_child("assets");
let mut assets = BTreeMap::new();
for (id, location) in id_to_location_map.iter() {
let asset_data = AssetInspectData::new(&assets_node, id, location);
assets.insert(id.clone(), asset_data);
}
AssetCollectionInspectData { node, assets_node, assets }
}
/// Creates an Inspect node to be passed into `new`.
fn make_node(parent_node: &finspect::Node) -> finspect::Node {
parent_node.create_child("asset_collection")
}
/// Updates the state of the inspect data based on the result of `Cache::push`.
fn update_after_cache_push(&mut self, asset: &Asset, is_cached: bool, popped_assets: &[Asset]) {
if is_cached {
self.set_file_cached(asset.id, asset.buffer.size as usize);
}
for popped_asset in popped_assets {
self.set_not_cached(popped_asset.id);
}
}
/// Marks the given asset as cached and keeps track of its size in bytes.
fn set_file_cached(&mut self, asset_id: AssetId, size: usize) {
self.assets.entry(asset_id).and_modify(|asset_data| {
asset_data.caching.set(AssetInspectData::CACHING_CACHED);
let node = &mut asset_data.node;
asset_data.size.get_or_insert_with(|| node.create_uint("size_bytes", size as u64));
});
}
/// Marks the given asset as no longer cached. (The size data is kept.)
fn set_not_cached(&mut self, asset_id: AssetId) {
self.assets.entry(asset_id).and_modify(|asset_data| {
asset_data.caching.set(AssetInspectData::CACHING_UNCACHED);
});
}
}
#[cfg(test)]
mod tests {
use {
super::*, async_trait::async_trait, finspect::assert_inspect_tree, fuchsia_async as fasync,
fuchsia_zircon as zx, std::path::Path,
};
fn mock_vmo(vmo_size: u64, buffer_size: u64) -> mem::Buffer {
mem::Buffer { vmo: zx::Vmo::create(vmo_size).unwrap(), size: buffer_size }
}
#[fasync::run_singlethreaded(test)]
async fn test_inspect_data() -> Result<(), Error> {
struct FakeAssetLoader {}
#[async_trait]
#[allow(dead_code, unused_variables)]
impl AssetLoader for FakeAssetLoader {
async fn fetch_package_directory(
&self,
package_locator: &v2::PackageLocator,
) -> Result<io::DirectoryProxy, AssetCollectionError> {
unimplemented!()
}
fn load_vmo_from_path(&self, path: &Path) -> Result<mem::Buffer, AssetCollectionError> {
let size = match path.file_name().unwrap().to_str().unwrap() {
"Alpha-Regular.ttf" => 2345,
"Beta-Regular.ttf" => 4000,
_ => unreachable!(),
};
Ok(mock_vmo(size * 2, size))
}
async fn load_vmo_from_directory_proxy(
&self,
directory_proxy: io::DirectoryProxy,
package_locator: &v2::PackageLocator,
file_name: &str,
) -> Result<mem::Buffer, AssetCollectionError> {
unimplemented!()
}
}
let inspector = finspect::Inspector::new();
let assets = vec![
v2::Asset {
file_name: "Alpha-Regular.ttf".to_string(),
location: v2::AssetLocation::LocalFile(v2::LocalFileLocator {
directory: "/config/data/assets".into(),
}),
typefaces: vec![],
},
v2::Asset {
file_name: "Beta-Regular.ttf".to_string(),
location: v2::AssetLocation::LocalFile(v2::LocalFileLocator {
directory: "/config/data/assets".into(),
}),
typefaces: vec![],
},
v2::Asset {
file_name: "Alpha-Condensed.ttf".to_string(),
location: v2::AssetLocation::Package(v2::PackageLocator {
url: PkgUrl::parse(
"fuchsia-pkg://fuchsia.com/font-package-alpha-condensed-ttf",
)?,
}),
typefaces: vec![],
},
];
let cache_capacity_bytes = 5000;
let asset_loader = FakeAssetLoader {};
let mut builder =
AssetCollectionBuilder::new(asset_loader, cache_capacity_bytes, inspector.root());
for asset in &assets {
builder.add_or_get_asset_id(asset);
}
let collection = builder.build();
// Note partial `contains` match. Cache is tested separately.
assert_inspect_tree!(inspector, root: {
asset_collection: {
assets: {
"0": {
caching: "uncached",
location: "/config/data/assets/Alpha-Regular.ttf"
},
"1": {
caching: "uncached",
location: "/config/data/assets/Beta-Regular.ttf",
},
"2": {
caching: "uncached",
location: "fuchsia-pkg://fuchsia.com/font-package-alpha-condensed-ttf#Alpha-Condensed.ttf"
}
},
asset_cache: contains {},
count: 3u64
}
});
collection.get_asset(AssetId(0), CacheMissPolicy::BlockUntilDownloaded).await?;
assert_inspect_tree!(inspector, root: {
asset_collection: contains {
assets: {
"0": {
caching: "cached",
location: "/config/data/assets/Alpha-Regular.ttf",
size_bytes: 2345u64,
},
"1": {
caching: "uncached",
location: "/config/data/assets/Beta-Regular.ttf",
},
"2": {
caching: "uncached",
location: "fuchsia-pkg://fuchsia.com/font-package-alpha-condensed-ttf#Alpha-Condensed.ttf"
}
}
}
});
collection.get_asset(AssetId(1), CacheMissPolicy::BlockUntilDownloaded).await?;
// Asset 1 gets cached, asset 1 is evicted but size info is kept.
assert_inspect_tree!(inspector, root: {
asset_collection: contains {
assets: {
"0": {
caching: "uncached",
location: "/config/data/assets/Alpha-Regular.ttf",
size_bytes: 2345u64,
},
"1": {
caching: "cached",
location: "/config/data/assets/Beta-Regular.ttf",
size_bytes: 4000u64,
},
"2": {
caching: "uncached",
location: "fuchsia-pkg://fuchsia.com/font-package-alpha-condensed-ttf#Alpha-Condensed.ttf"
}
}
}
});
Ok(())
}
}