Faster galley cache (#699)
* Speed up galley cache by only using the hash as key This hashes the job but doesn't compare them with Eq, which speeds up demo_with_tessellate__realistic by 5-6%, winning back all the performance lost in https://github.com/emilk/egui/pull/682 * Remove custom Eq/PartialEq code for LayoutJob and friends * Silence clippy * Unrelated clippy fixes
This commit is contained in:
parent
3b75a84d3b
commit
5f88d89f74
8 changed files with 34 additions and 59 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -898,6 +898,7 @@ dependencies = [
|
||||||
"atomic_refcell",
|
"atomic_refcell",
|
||||||
"cint",
|
"cint",
|
||||||
"emath",
|
"emath",
|
||||||
|
"nohash-hasher",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -1492,6 +1493,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nohash-hasher"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "6.2.1"
|
version = "6.2.1"
|
||||||
|
|
|
@ -81,7 +81,7 @@ ui.label(format!("Hello '{}', age {}", name, age));
|
||||||
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/egui_demo_lib/src/apps/demo/toggle_switch.rs)
|
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/egui_demo_lib/src/apps/demo/toggle_switch.rs)
|
||||||
* Modular: You should be able to use small parts of egui and combine them in new ways
|
* Modular: You should be able to use small parts of egui and combine them in new ways
|
||||||
* Safe: there is no `unsafe` code in egui
|
* Safe: there is no `unsafe` code in egui
|
||||||
* Minimal dependencies: [`ab_glyph`](https://crates.io/crates/ab_glyph) [`ahash`](https://crates.io/crates/ahash) [`atomic_refcell`](https://crates.io/crates/atomic_refcell)
|
* Minimal dependencies: [`ab_glyph`](https://crates.io/crates/ab_glyph) [`ahash`](https://crates.io/crates/ahash) [`atomic_refcell`](https://crates.io/crates/atomic_refcell), [`nohash-hasher`](https://crates.io/crates/nohash-hasher)
|
||||||
|
|
||||||
egui is *not* a framework. egui is a library you call into, not an environment you program for.
|
egui is *not* a framework. egui is a library you call into, not an environment you program for.
|
||||||
|
|
||||||
|
|
|
@ -223,6 +223,7 @@ struct Highligher {}
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
#[cfg(not(feature = "syntect"))]
|
||||||
impl Highligher {
|
impl Highligher {
|
||||||
|
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||||
fn highlight(&self, is_dark_mode: bool, mut text: &str, _language: &str) -> Option<LayoutJob> {
|
fn highlight(&self, is_dark_mode: bool, mut text: &str, _language: &str) -> Option<LayoutJob> {
|
||||||
// Extremely simple syntax highlighter for when we compile without syntect
|
// Extremely simple syntax highlighter for when we compile without syntect
|
||||||
|
|
||||||
|
@ -269,7 +270,7 @@ impl Highligher {
|
||||||
|
|
||||||
while !text.is_empty() {
|
while !text.is_empty() {
|
||||||
if text.starts_with("//") {
|
if text.starts_with("//") {
|
||||||
let end = text.find('\n').unwrap_or(text.len());
|
let end = text.find('\n').unwrap_or_else(|| text.len());
|
||||||
job.append(&text[..end], 0.0, comment_format);
|
job.append(&text[..end], 0.0, comment_format);
|
||||||
text = &text[end..];
|
text = &text[end..];
|
||||||
} else if text.starts_with('"') {
|
} else if text.starts_with('"') {
|
||||||
|
@ -277,14 +278,14 @@ impl Highligher {
|
||||||
.find('"')
|
.find('"')
|
||||||
.map(|i| i + 2)
|
.map(|i| i + 2)
|
||||||
.or_else(|| text.find('\n'))
|
.or_else(|| text.find('\n'))
|
||||||
.unwrap_or(text.len());
|
.unwrap_or_else(|| text.len());
|
||||||
job.append(&text[..end], 0.0, quoted_string_format);
|
job.append(&text[..end], 0.0, quoted_string_format);
|
||||||
text = &text[end..];
|
text = &text[end..];
|
||||||
} else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) {
|
} else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) {
|
||||||
let end = text[1..]
|
let end = text[1..]
|
||||||
.find(|c: char| !c.is_ascii_alphanumeric())
|
.find(|c: char| !c.is_ascii_alphanumeric())
|
||||||
.map(|i| i + 1)
|
.map(|i| i + 1)
|
||||||
.unwrap_or(text.len());
|
.unwrap_or_else(|| text.len());
|
||||||
let word = &text[..end];
|
let word = &text[..end];
|
||||||
if is_keyword(word) {
|
if is_keyword(word) {
|
||||||
job.append(word, 0.0, keyword_format);
|
job.append(word, 0.0, keyword_format);
|
||||||
|
@ -296,7 +297,7 @@ impl Highligher {
|
||||||
let end = text[1..]
|
let end = text[1..]
|
||||||
.find(|c: char| !c.is_ascii_whitespace())
|
.find(|c: char| !c.is_ascii_whitespace())
|
||||||
.map(|i| i + 1)
|
.map(|i| i + 1)
|
||||||
.unwrap_or(text.len());
|
.unwrap_or_else(|| text.len());
|
||||||
job.append(&text[..end], 0.0, whitespace_format);
|
job.append(&text[..end], 0.0, whitespace_format);
|
||||||
text = &text[end..];
|
text = &text[end..];
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -31,6 +31,7 @@ ab_glyph = "0.2.11"
|
||||||
ahash = { version = "0.7", features = ["std"], default-features = false }
|
ahash = { version = "0.7", features = ["std"], default-features = false }
|
||||||
atomic_refcell = { version = "0.1", optional = true } # Used instead of parking_lot when you are always using epaint in a single thread. About as fast as parking_lot. Panics on multi-threaded use.
|
atomic_refcell = { version = "0.1", optional = true } # Used instead of parking_lot when you are always using epaint in a single thread. About as fast as parking_lot. Panics on multi-threaded use.
|
||||||
cint = { version = "^0.2.2", optional = true }
|
cint = { version = "^0.2.2", optional = true }
|
||||||
|
nohash-hasher = "0.2"
|
||||||
parking_lot = { version = "0.11", optional = true } # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
|
parking_lot = { version = "0.11", optional = true } # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
|
||||||
|
|
|
@ -189,12 +189,3 @@ pub(crate) fn f32_hash<H: std::hash::Hasher>(state: &mut H, f: f32) {
|
||||||
f.to_bits().hash(state)
|
f.to_bits().hash(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn f32_eq(a: f32, b: f32) -> bool {
|
|
||||||
if a.is_nan() && b.is_nan() {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
a == b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
#![allow(clippy::derive_hash_xor_eq)] // We need to impl Hash for f32, but we don't implement Eq, which is fine
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Describes the width and color of a line.
|
/// Describes the width and color of a line.
|
||||||
///
|
///
|
||||||
/// The default stroke is the same as [`Stroke::none`].
|
/// The default stroke is the same as [`Stroke::none`].
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct Stroke {
|
pub struct Stroke {
|
||||||
pub width: f32,
|
pub width: f32,
|
||||||
|
@ -44,12 +46,3 @@ impl std::hash::Hash for Stroke {
|
||||||
color.hash(state);
|
color.hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Stroke {
|
|
||||||
#[inline(always)]
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.color == other.color && crate::f32_eq(self.width, other.width)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::cmp::Eq for Stroke {}
|
|
||||||
|
|
|
@ -4,8 +4,6 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ahash::AHashMap;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
mutex::Mutex,
|
mutex::Mutex,
|
||||||
text::{
|
text::{
|
||||||
|
@ -321,8 +319,8 @@ impl Fonts {
|
||||||
/// [`Self::layout_delayed_color`].
|
/// [`Self::layout_delayed_color`].
|
||||||
///
|
///
|
||||||
/// The implementation uses memoization so repeated calls are cheap.
|
/// The implementation uses memoization so repeated calls are cheap.
|
||||||
pub fn layout_job(&self, job: impl Into<Arc<LayoutJob>>) -> Arc<Galley> {
|
pub fn layout_job(&self, job: LayoutJob) -> Arc<Galley> {
|
||||||
self.galley_cache.lock().layout(self, job.into())
|
self.galley_cache.lock().layout(self, job)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Will wrap text at the given width and line break at `\n`.
|
/// Will wrap text at the given width and line break at `\n`.
|
||||||
|
@ -400,19 +398,25 @@ struct CachedGalley {
|
||||||
struct GalleyCache {
|
struct GalleyCache {
|
||||||
/// Frame counter used to do garbage collection on the cache
|
/// Frame counter used to do garbage collection on the cache
|
||||||
generation: u32,
|
generation: u32,
|
||||||
cache: AHashMap<Arc<LayoutJob>, CachedGalley>,
|
cache: nohash_hasher::IntMap<u64, CachedGalley>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GalleyCache {
|
impl GalleyCache {
|
||||||
fn layout(&mut self, fonts: &Fonts, job: Arc<LayoutJob>) -> Arc<Galley> {
|
fn layout(&mut self, fonts: &Fonts, job: LayoutJob) -> Arc<Galley> {
|
||||||
match self.cache.entry(job.clone()) {
|
let hash = {
|
||||||
|
let mut hasher = ahash::AHasher::new_with_keys(123, 456); // TODO: even faster hasher?
|
||||||
|
job.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.cache.entry(hash) {
|
||||||
std::collections::hash_map::Entry::Occupied(entry) => {
|
std::collections::hash_map::Entry::Occupied(entry) => {
|
||||||
let cached = entry.into_mut();
|
let cached = entry.into_mut();
|
||||||
cached.last_used = self.generation;
|
cached.last_used = self.generation;
|
||||||
cached.galley.clone()
|
cached.galley.clone()
|
||||||
}
|
}
|
||||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||||
let galley = super::layout(fonts, job);
|
let galley = super::layout(fonts, job.into());
|
||||||
let galley = Arc::new(galley);
|
let galley = Arc::new(galley);
|
||||||
entry.insert(CachedGalley {
|
entry.insert(CachedGalley {
|
||||||
last_used: self.generation,
|
last_used: self.generation,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(clippy::derive_hash_xor_eq)] // We need to impl Hash for f32, but we don't implement Eq, which is fine
|
||||||
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -10,7 +12,7 @@ use emath::*;
|
||||||
/// This supports mixing different fonts, color and formats (underline etc).
|
/// This supports mixing different fonts, color and formats (underline etc).
|
||||||
///
|
///
|
||||||
/// Pass this to [`Fonts::layout_job]` or [`crate::text::layout`].
|
/// Pass this to [`Fonts::layout_job]` or [`crate::text::layout`].
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct LayoutJob {
|
pub struct LayoutJob {
|
||||||
/// The complete text of this job, referenced by `LayoutSection`.
|
/// The complete text of this job, referenced by `LayoutSection`.
|
||||||
pub text: String, // TODO: Cow<'static, str>
|
pub text: String, // TODO: Cow<'static, str>
|
||||||
|
@ -120,22 +122,9 @@ impl std::hash::Hash for LayoutJob {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for LayoutJob {
|
|
||||||
#[inline(always)]
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.text == other.text
|
|
||||||
&& self.sections == other.sections
|
|
||||||
&& crate::f32_eq(self.wrap_width, other.wrap_width)
|
|
||||||
&& crate::f32_eq(self.first_row_min_height, other.first_row_min_height)
|
|
||||||
&& self.break_on_newline == other.break_on_newline
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::cmp::Eq for LayoutJob {}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct LayoutSection {
|
pub struct LayoutSection {
|
||||||
/// Can be used for first row indentation.
|
/// Can be used for first row indentation.
|
||||||
pub leading_space: f32,
|
pub leading_space: f32,
|
||||||
|
@ -158,20 +147,9 @@ impl std::hash::Hash for LayoutSection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for LayoutSection {
|
|
||||||
#[inline(always)]
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
crate::f32_eq(self.leading_space, other.leading_space)
|
|
||||||
&& self.byte_range == other.byte_range
|
|
||||||
&& self.format == other.format
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::cmp::Eq for LayoutSection {}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Hash, PartialEq)]
|
||||||
pub struct TextFormat {
|
pub struct TextFormat {
|
||||||
pub style: TextStyle,
|
pub style: TextStyle,
|
||||||
/// Text color
|
/// Text color
|
||||||
|
|
Loading…
Reference in a new issue