Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use std::cell::RefCell;
use std::ffi::OsString;
use std::fs::{create_dir_all, read_dir, read_to_string, File};
use std::io::{Error, ErrorKind};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use nsstring::nsAString;
use rayon::ThreadPool;
use webrender::{ProgramBinary, ProgramCache, ProgramCacheObserver, ProgramSourceDigest};
const MAX_LOAD_TIME_MS: u64 = 400;
fn deserialize_program_binary(path: &Path) -> Result<Arc<ProgramBinary>, Error> {
let mut buf = vec![];
let mut file = File::open(path)?;
file.read_to_end(&mut buf)?;
if buf.len() <= 8 + 4 {
return Err(Error::new(ErrorKind::InvalidData, "File size is too small"));
}
let magic = &buf[0..4];
let hash = &buf[4..8 + 4];
let data = &buf[8 + 4..];
// Check if magic + version are correct.
let mv: u32 = bincode::deserialize(&magic).unwrap();
if mv != MAGIC_AND_VERSION {
return Err(Error::new(
ErrorKind::InvalidData,
"File data is invalid (magic+version)",
));
}
// Check if hash is correct
let hash: u64 = bincode::deserialize(&hash).unwrap();
let hash_data = fxhash::hash64(&data);
if hash != hash_data {
return Err(Error::new(ErrorKind::InvalidData, "File data is invalid (hash)"));
}
// Deserialize ProgramBinary
let binary = match bincode::deserialize(&data) {
Ok(binary) => binary,
Err(_) => {
return Err(Error::new(
ErrorKind::InvalidData,
"Failed to deserialize ProgramBinary",
))
},
};
Ok(Arc::new(binary))
}
#[cfg(target_os = "windows")]
fn get_cache_path_from_prof_path(prof_path: &nsAString) -> Option<PathBuf> {
if prof_path.is_empty() {
// Empty means that we do not use disk cache.
return None;
}
use std::os::windows::prelude::*;
let prof_path = OsString::from_wide(prof_path.as_ref());
let mut cache_path = PathBuf::from(&prof_path);
cache_path.push("shader-cache");
Some(cache_path)
}
#[cfg(not(target_os = "windows"))]
fn get_cache_path_from_prof_path(prof_path: &nsAString) -> Option<PathBuf> {
if prof_path.is_empty() {
// Empty means that we do not use disk cache.
return None;
}
let utf8 = String::from_utf16(prof_path.as_ref()).unwrap();
let prof_path = OsString::from(utf8);
let mut cache_path = PathBuf::from(&prof_path);
cache_path.push("shader-cache");
Some(cache_path)
}
struct WrProgramBinaryDiskCache {
cache_path: PathBuf,
workers: Arc<ThreadPool>,
cached_shader_filenames: Vec<OsString>,
}
// Magic number + version. Increment the version when the binary format changes.
const MAGIC: u32 = 0xB154AD30; // BI-SHADE + version.
const VERSION: u32 = 2;
const MAGIC_AND_VERSION: u32 = MAGIC + VERSION;
const WHITELIST_FILENAME: &str = "startup_shaders";
const WHITELIST_SEPARATOR: &str = "\n";
/// Helper to convert a closure returning a `Result` to one that returns void.
/// This allows the enclosed code to use the question-mark operator in a
/// context where the calling function doesn't expect a `Result`.
#[allow(unused_must_use)]
fn result_to_void<F: FnOnce() -> Result<(), ()>>(f: F) {
f();
}
impl WrProgramBinaryDiskCache {
#[allow(dead_code)]
fn new(cache_path: PathBuf, workers: &Arc<ThreadPool>) -> Self {
WrProgramBinaryDiskCache {
cache_path,
workers: Arc::clone(workers),
cached_shader_filenames: Vec::new(),
}
}
/// Saves the specified binaries to the on-disk shader cache.
fn save_shaders_to_disk(&mut self, entries: Vec<Arc<ProgramBinary>>) {
info!("Saving binaries to on-disk shader cache");
// Write the entries to disk on a worker thread.
for entry in entries {
let file_name = entry.source_digest().to_string();
let file_path = self.cache_path.join(&file_name);
self.workers.spawn(move || {
result_to_void(move || {
info!("Writing shader: {}", file_name);
use std::time::Instant;
let start = Instant::now();
let data: Vec<u8> =
bincode::serialize(&*entry).map_err(|e| error!("shader-cache: Failed to serialize: {}", e))?;
let mut file =
File::create(&file_path).map_err(|e| error!("shader-cache: Failed to create file: {}", e))?;
// Write magic + version.
let mv = MAGIC_AND_VERSION;
let mv = bincode::serialize(&mv).unwrap();
assert!(mv.len() == 4);
file.write_all(&mv)
.map_err(|e| error!("shader-cache: Failed to write magic + version: {}", e))?;
// Write hash
let hash = fxhash::hash64(&data);
let hash = bincode::serialize(&hash).unwrap();
assert!(hash.len() == 8);
file.write_all(&hash)
.map_err(|e| error!("shader-cache: Failed to write hash: {}", e))?;
// Write serialized data
file.write_all(&data)
.map_err(|e| error!("shader-cache: Failed to write program binary: {}", e))?;
info!("Wrote shader {} in {:?}", file_name, start.elapsed());
Ok(())
})
});
}
}
/// Writes the whitelist containing the set of startup shaders to disk.
fn set_startup_shaders(&mut self, entries: Vec<Arc<ProgramBinary>>) {
let whitelist = entries
.iter()
.map(|e| e.source_digest().to_string())
.collect::<Vec<String>>()
.join(WHITELIST_SEPARATOR);
let mut whitelist_path = self.cache_path.clone();
whitelist_path.push(WHITELIST_FILENAME);
self.workers.spawn(move || {
result_to_void(move || {
info!("Writing startup shader whitelist");
File::create(&whitelist_path)
.and_then(|mut file| file.write_all(whitelist.as_bytes()))
.map_err(|e| error!("shader-cache: Failed to write startup whitelist: {}", e))?;
Ok(())
})
});
}
pub fn try_load_shader_from_disk(&mut self, filename: &str, program_cache: &Rc<ProgramCache>) {
if let Some(index) = self.cached_shader_filenames.iter().position(|e| e == filename) {
let mut path = self.cache_path.clone();
path.push(filename);
self.cached_shader_filenames.swap_remove(index);
info!("Loading shader: {}", filename);
match deserialize_program_binary(&path) {
Ok(program) => {
program_cache.load_program_binary(program);
},
Err(err) => {
error!("shader-cache: Failed to deserialize program binary: {}", err);
},
};
} else {
info!("shader-cache: Program binary not found in disk cache");
}
}
pub fn try_load_startup_shaders_from_disk(&mut self, program_cache: &Rc<ProgramCache>) {
use std::time::Instant;
let start = Instant::now();
// Load and parse the whitelist if it exists
let mut whitelist_path = self.cache_path.clone();
whitelist_path.push(WHITELIST_FILENAME);
let whitelist = match read_to_string(&whitelist_path) {
Ok(whitelist) => whitelist
.split(WHITELIST_SEPARATOR)
.map(|s| s.to_string())
.collect::<Vec<String>>(),
Err(err) => {
info!("shader-cache: Could not read startup whitelist: {}", err);
Vec::new()
},
};
info!("Loaded startup shader whitelist in {:?}", start.elapsed());
self.cached_shader_filenames = read_dir(&self.cache_path)
.map_err(|err| {
error!(
"shader-cache: Error reading directory whilst loading startup shaders: {}",
err
)
})
.into_iter()
.flatten()
.filter_map(Result::ok)
.map(|entry| entry.file_name())
.filter(|filename| filename != WHITELIST_FILENAME)
.collect::<Vec<OsString>>();
// Load whitelisted program binaries if they exist
for entry in &whitelist {
self.try_load_shader_from_disk(&entry, program_cache);
let elapsed = start.elapsed();
info!("Loaded shader in {:?}", elapsed);
let elapsed_ms = (elapsed.as_secs() * 1_000) + elapsed.subsec_millis() as u64;
if elapsed_ms > MAX_LOAD_TIME_MS {
// Loading the startup shaders is taking too long, so bail out now.
// Additionally clear the list of remaining shaders cached on disk,
// so that we do not attempt to load any on demand during rendering.
error!("shader-cache: Timed out before finishing loads");
self.cached_shader_filenames.clear();
break;
}
}
}
}
pub struct WrProgramCacheObserver {
disk_cache: Rc<RefCell<WrProgramBinaryDiskCache>>,
}
impl WrProgramCacheObserver {
#[allow(dead_code)]
fn new(disk_cache: Rc<RefCell<WrProgramBinaryDiskCache>>) -> Self {
WrProgramCacheObserver { disk_cache }
}
}
impl ProgramCacheObserver for WrProgramCacheObserver {
fn save_shaders_to_disk(&self, entries: Vec<Arc<ProgramBinary>>) {
self.disk_cache.borrow_mut().save_shaders_to_disk(entries);
}
fn set_startup_shaders(&self, entries: Vec<Arc<ProgramBinary>>) {
self.disk_cache.borrow_mut().set_startup_shaders(entries);
}
fn try_load_shader_from_disk(&self, digest: &ProgramSourceDigest, program_cache: &Rc<ProgramCache>) {
let filename = digest.to_string();
self.disk_cache
.borrow_mut()
.try_load_shader_from_disk(&filename, program_cache);
}
fn notify_program_binary_failed(&self, _program_binary: &Arc<ProgramBinary>) {
error!("shader-cache: Failed program_binary");
}
}
pub struct WrProgramCache {
pub program_cache: Rc<ProgramCache>,
disk_cache: Option<Rc<RefCell<WrProgramBinaryDiskCache>>>,
}
impl WrProgramCache {
pub fn new(prof_path: &nsAString, workers: &Arc<ThreadPool>) -> Self {
let cache_path = get_cache_path_from_prof_path(prof_path);
let use_disk_cache = cache_path.as_ref().map_or(false, |p| create_dir_all(p).is_ok());
let (disk_cache, program_cache_observer) = if use_disk_cache {
let cache = Rc::new(RefCell::new(WrProgramBinaryDiskCache::new(
cache_path.unwrap(),
workers,
)));
let obs = Box::new(WrProgramCacheObserver::new(Rc::clone(&cache))) as Box<dyn ProgramCacheObserver>;
(Some(cache), Some(obs))
} else {
(None, None)
};
let program_cache = ProgramCache::new(program_cache_observer);
WrProgramCache {
program_cache,
disk_cache,
}
}
pub fn rc_get(&self) -> &Rc<ProgramCache> {
&self.program_cache
}
pub fn try_load_startup_shaders_from_disk(&self) {
if let Some(ref disk_cache) = self.disk_cache {
disk_cache
.borrow_mut()
.try_load_startup_shaders_from_disk(&self.program_cache);
} else {
error!("shader-cache: Shader disk cache is not supported");
}
}
}
pub fn remove_disk_cache(prof_path: &nsAString) -> Result<(), Error> {
use std::time::Instant;
if let Some(cache_path) = get_cache_path_from_prof_path(prof_path) {
if cache_path.exists() {
let start = Instant::now();
remove_dir_all::remove_dir_all(&cache_path)?;
info!("removed all disk cache shaders in {:?}", start.elapsed());
}
}
Ok(())
}