gioser

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 1061HTML 997Markdown 12TOML 83120 Sutichaykuna

Enable compilation of crate for no_std again.

Several regressions caused the no_std support to bitrot.  This adds back
support for no_std environments.

As part of this change, I had to revamp the Id re-use functionality.  The
good news?  Creating new IDs is now atomically lock-free.  The bad news?
Freeing IDs uses a spin lock.  Performance could be greatly improved by
freeing more IDs at the same time, as the signature would already suggest;
however it seems like no code is taking advantage of that right now.

80d832d1390b9d070ab601ffa7615ef4ef9e6220 · ruwaq David Flemström · (7 wata ñawpaq) · tayta ab572088b · kay commitpi sach'ata qhaway

Semántico sayk'uy · 17

src/algo.rs

src/id.rs

src/lib.rs

  • silq'uyninpi t'ikrakurqan enum Error pub

src/node.rs

8 qillqakuna · +135 −49

diff --git a/Cargo.toml b/Cargo.toml
index 3bb8e14b3..c6f4599a8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,8 +16,9 @@ circle-ci = { repository = "vislyhq/stretch", branch = "master" }
 maintenance = { status = "experimental" }
 
 [dependencies]
-libm = "0.1.2"
+hashbrown = "0.6.0"
 lazy_static = "1.3"
+spin = "0.5.2"
 
 [dependencies.serde]
 version = "1.0.99"
diff --git a/src/algo.rs b/src/algo.rs
index 18bb933d6..d4bde363f 100644
--- a/src/algo.rs
+++ b/src/algo.rs
@@ -1,7 +1,5 @@
 #[cfg(not(feature = "std"))]
-use alloc::{vec, vec::Vec};
-#[cfg(not(feature = "std"))]
-use libm::F32Ext;
+use alloc::{boxed::Box, vec, vec::Vec};
 
 use core::any::Any;
 use core::f32;
@@ -59,7 +57,7 @@ struct FlexLine {
 }
 
 impl Forest {
-    pub(crate) fn compute(&mut self, root: NodeId, size: Size<Number>) -> Result<(), Box<Any>> {
+    pub(crate) fn compute(&mut self, root: NodeId, size: Size<Number>) -> Result<(), Box<dyn Any>> {
         let style = self.nodes[root].style;
         let has_root_min_max = style.min_size.width.is_defined()
             || style.min_size.height.is_defined()
@@ -132,7 +130,7 @@ impl Forest {
         node_size: Size<Number>,
         parent_size: Size<Number>,
         perform_layout: bool,
-    ) -> Result<ComputeResult, Box<Any>> {
+    ) -> Result<ComputeResult, Box<dyn Any>> {
         self.nodes[node].is_dirty = false;
 
         // First we check if we have a result for the given input
@@ -276,7 +274,7 @@ impl Forest {
 
         // TODO - this does not follow spec. See commented out code below
         // 3. Determine the flex base size and hypothetical main size of each item:
-        flex_items.iter_mut().try_for_each(|child| -> Result<(), Box<Any>> {
+        flex_items.iter_mut().try_for_each(|child| -> Result<(), Box<dyn Any>> {
             let child_style = self.nodes[child.node].style;
 
             // A. If the item has a definite used flex basis, that’s the flex base size.
@@ -363,7 +361,7 @@ impl Forest {
         // The hypothetical main size is the item’s flex base size clamped according to its
         // used min and max main sizes (and flooring the content box size at zero).
 
-        flex_items.iter_mut().try_for_each(|child| -> Result<(), Box<Any>> {
+        flex_items.iter_mut().try_for_each(|child| -> Result<(), Box<dyn Any>> {
             child.inner_flex_basis = child.flex_basis - child.padding.main(dir) - child.border.main(dir);
 
             // TODO - not really spec abiding but needs to be done somewhere. probably somewhere else though.
@@ -440,7 +438,7 @@ impl Forest {
         //
         // 9.7. Resolving Flexible Lengths
 
-        flex_lines.iter_mut().try_for_each(|line| -> Result<(), Box<Any>> {
+        flex_lines.iter_mut().try_for_each(|line| -> Result<(), Box<dyn Any>> {
             // 1. Determine the used flex factor. Sum the outer hypothetical main sizes of all
             //    items on the line. If the sum is less than the flex container’s inner main size,
             //    use the flex grow factor for the rest of this algorithm; otherwise, use the
@@ -457,7 +455,7 @@ impl Forest {
             //    - If using the flex shrink factor: any item that has a flex base size
             //      smaller than its hypothetical main size
 
-            line.items.iter_mut().try_for_each(|child| -> Result<(), Box<Any>> {
+            line.items.iter_mut().try_for_each(|child| -> Result<(), Box<dyn Any>> {
                 // TODO - This is not found by reading the spec. Maybe this can be done in some other place
                 // instead. This was found by trail and error fixing tests to align with webkit output.
                 if node_inner_size.main(dir).is_undefined() && is_row {
@@ -615,7 +613,7 @@ impl Forest {
                 //    item’s target main size was made smaller by this, it’s a max violation.
                 //    If the item’s target main size was made larger by this, it’s a min violation.
 
-                let total_violation = unfrozen.iter_mut().try_fold(0.0, |acc, child| -> Result<f32, Box<Any>> {
+                let total_violation = unfrozen.iter_mut().try_fold(0.0, |acc, child| -> Result<f32, Box<dyn Any>> {
                     // TODO - not really spec abiding but needs to be done somewhere. probably somewhere else though.
                     // The following logic was developed not from the spec but by trail and error looking into how
                     // webkit handled various scenarios. Can probably be solved better by passing in
@@ -692,7 +690,7 @@ impl Forest {
         //    used main size and the available space, treating auto as fit-content.
 
         flex_lines.iter_mut().try_for_each(|line| {
-            line.items.iter_mut().try_for_each(|child| -> Result<(), Box<Any>> {
+            line.items.iter_mut().try_for_each(|child| -> Result<(), Box<dyn Any>> {
                 let child_cross =
                     child.size.cross(dir).maybe_max(child.min_size.cross(dir)).maybe_min(child.max_size.cross(dir));
 
@@ -738,7 +736,7 @@ impl Forest {
 
         if has_baseline_child {
             flex_lines.iter_mut().try_for_each(|line| {
-                line.items.iter_mut().try_for_each(|child| -> Result<(), Box<Any>> {
+                line.items.iter_mut().try_for_each(|child| -> Result<(), Box<dyn Any>> {
                     let result = self.compute_internal(
                         child.node,
                         Size {
@@ -1149,12 +1147,12 @@ impl Forest {
             let mut lines: Vec<Vec<result::Layout>> = vec![];
             let mut total_offset_cross = padding_border.cross_start(dir);
 
-            let layout_line = |line: &mut FlexLine| -> Result<(), Box<Any>> {
+            let layout_line = |line: &mut FlexLine| -> Result<(), Box<dyn Any>> {
                 let mut children: Vec<result::Layout> = vec![];
                 let mut total_offset_main = padding_border.main_start(dir);
                 let line_offset_cross = line.offset_cross;
 
-                let layout_item = |child: &mut FlexItem| -> Result<(), Box<Any>> {
+                let layout_item = |child: &mut FlexItem| -> Result<(), Box<dyn Any>> {
                     let result = self.compute_internal(
                         child.node,
                         child.target_size.map(|s| s.to_number()),
diff --git a/src/forest.rs b/src/forest.rs
index cce8bd842..06422c805 100644
--- a/src/forest.rs
+++ b/src/forest.rs
@@ -2,6 +2,9 @@
 //!
 //! Backing datastructure for `Stretch` structs.
 
+#[cfg(not(feature = "std"))]
+use alloc::vec::Vec;
+
 use crate::geometry::Size;
 use crate::id::NodeId;
 use crate::node::MeasureFunc;
diff --git a/src/id.rs b/src/id.rs
index 0b76c8934..db1085913 100644
--- a/src/id.rs
+++ b/src/id.rs
@@ -1,6 +1,9 @@
 //! Identifier for a Node
 //!
 //!
+#[cfg(not(feature = "std"))]
+use alloc::vec::Vec;
+use core::sync::atomic;
 
 /// Internal node id.
 pub(crate) type NodeId = usize;
@@ -12,28 +15,110 @@ pub(crate) struct Id {
 }
 
 pub(crate) struct Allocator {
-    new_id: u32,
-    free_ids: Vec<Id>,
+    new_id: atomic::AtomicU32,
+    free_ids: spin::RwLock<IdCache>,
 }
 
 impl Allocator {
     pub fn new() -> Self {
-        Allocator { new_id: 0, free_ids: Vec::new() }
-    }
-
-    pub fn allocate(&mut self) -> Id {
-        // TODO: better balancing
-        match self.free_ids.pop() {
-            Some(id) => Id { id: id.id, generation: id.generation + 1 },
-            None => {
-                let id = self.new_id;
-                self.new_id += 1;
-                Id { id, generation: 0 }
-            }
+        Allocator { new_id: atomic::AtomicU32::new(0), free_ids: spin::RwLock::new(IdCache::default()) }
+    }
+
+    pub fn allocate(&self) -> Id {
+        self.free_ids
+            .read()
+            .pop_atomic()
+            .map(|Id { id, generation }| Id { id, generation: generation + 1 })
+            .unwrap_or_else(|| Id {
+                id: atomic_increment(&self.new_id).expect("No entity left to allocate"),
+                generation: 0,
+            })
+    }
+
+    pub fn free(&self, ids: &[Id]) {
+        self.free_ids.write().extend(ids.iter().copied());
+    }
+}
+
+// Code below this line is based on code from the `specs` library, in the `src/world/entity.rs`
+// file, which has the following copyright notice (MIT license):
+
+/*
+Copyright (c) 2017 The Specs Project Developers
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+*/
+
+#[derive(Default, Debug)]
+struct IdCache {
+    cache: Vec<Id>,
+    len: atomic::AtomicUsize,
+}
+
+impl IdCache {
+    fn pop_atomic(&self) -> Option<Id> {
+        atomic_decrement(&self.len).map(|x| self.cache[x - 1])
+    }
+
+    fn maintain(&mut self) {
+        self.cache.truncate(*(self.len.get_mut()));
+    }
+}
+
+impl Extend<Id> for IdCache {
+    fn extend<T: IntoIterator<Item = Id>>(&mut self, iter: T) {
+        self.maintain();
+        self.cache.extend(iter);
+        *self.len.get_mut() = self.cache.len();
+    }
+}
+
+/// Increments `i` atomically without wrapping on overflow.
+/// Resembles a `fetch_add(1, Ordering::Relaxed)` with
+/// checked overflow, returning `None` instead.
+fn atomic_increment(i: &atomic::AtomicU32) -> Option<u32> {
+    let mut prev = i.load(atomic::Ordering::Relaxed);
+    while prev != core::u32::MAX {
+        match i.compare_exchange_weak(prev, prev + 1, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) {
+            Ok(x) => return Some(x),
+            Err(next_prev) => prev = next_prev,
         }
     }
+    None
+}
 
-    pub fn free(&mut self, ids: &[Id]) {
-        self.free_ids.extend(ids);
+/// Decrements `i` atomically without wrapping on overflow.
+/// Resembles a `fetch_sub(1, Ordering::Relaxed)` with
+/// checked underflow, returning `None` instead.
+fn atomic_decrement(i: &atomic::AtomicUsize) -> Option<usize> {
+    let mut prev = i.load(atomic::Ordering::Relaxed);
+    while prev != 0 {
+        match i.compare_exchange_weak(prev, prev - 1, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) {
+            Ok(x) => return Some(x),
+            Err(next_prev) => prev = next_prev,
+        }
     }
+    None
 }
diff --git a/src/lib.rs b/src/lib.rs
index 5ffa0ac94..2e1284112 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,8 +1,10 @@
 #![cfg_attr(not(feature = "std"), no_std)]
-#![cfg_attr(not(feature = "std"), feature(alloc))]
 #[cfg(not(feature = "std"))]
 extern crate alloc;
 
+#[cfg(not(feature = "std"))]
+use alloc::boxed::Box;
+
 #[macro_use]
 extern crate lazy_static;
 
@@ -27,9 +29,10 @@ use core::any::Any;
 #[derive(Debug)]
 pub enum Error {
     InvalidNode(node::Node),
-    Measure(Box<Any>),
+    Measure(Box<dyn Any>),
 }
 
+#[cfg(feature = "std")]
 impl std::fmt::Display for Error {
     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
         match *self {
@@ -39,6 +42,7 @@ impl std::fmt::Display for Error {
     }
 }
 
+#[cfg(feature = "std")]
 impl std::error::Error for Error {
     fn description(&self) -> &str {
         match *self {
diff --git a/src/node.rs b/src/node.rs
index 0ab316000..4158f8894 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -1,11 +1,12 @@
 #[cfg(not(feature = "std"))]
-use alloc::{vec, vec::Vec};
+use alloc::{boxed::Box, vec::Vec};
+#[cfg(not(feature = "std"))]
+use hashbrown::HashMap;
+#[cfg(feature = "std")]
+use std::collections::HashMap;
 
 use core::any::Any;
-
-use std::collections::HashMap;
-use std::ops::Drop;
-use std::sync::Mutex;
+use core::ops::Drop;
 
 use crate::forest::Forest;
 use crate::geometry::Size;
@@ -15,11 +16,11 @@ use crate::result::Layout;
 use crate::style::*;
 use crate::Error;
 
-pub type MeasureFunc = Box<Fn(Size<Number>) -> Result<Size<f32>, Box<Any>>>;
+pub type MeasureFunc = Box<dyn Fn(Size<Number>) -> Result<Size<f32>, Box<dyn Any>>>;
 
 lazy_static! {
     /// Global stretch instance id allocator.
-    static ref INSTANCE_ALLOCATOR: Mutex<id::Allocator> = Mutex::new(id::Allocator::new());
+    static ref INSTANCE_ALLOCATOR: id::Allocator = id::Allocator::new();
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
@@ -45,7 +46,7 @@ impl Stretch {
 
     pub fn with_capacity(capacity: usize) -> Self {
         Stretch {
-            id: INSTANCE_ALLOCATOR.lock().unwrap().allocate(),
+            id: INSTANCE_ALLOCATOR.allocate(),
             nodes: id::Allocator::new(),
             nodes_to_ids: HashMap::with_capacity(capacity),
             ids_to_nodes: HashMap::with_capacity(capacity),
@@ -167,7 +168,7 @@ impl Stretch {
         // TODO: index check
 
         self.forest.parents[child_id].push(node_id);
-        let old_child = std::mem::replace(&mut self.forest.children[node_id][index], child_id);
+        let old_child = core::mem::replace(&mut self.forest.children[node_id][index], child_id);
         self.forest.parents[old_child].retain(|p| *p != node_id);
 
         self.forest.mark_dirty(node_id);
@@ -221,6 +222,6 @@ impl Stretch {
 
 impl Drop for Stretch {
     fn drop(&mut self) {
-        INSTANCE_ALLOCATOR.lock().unwrap().free(&[self.id]);
+        INSTANCE_ALLOCATOR.free(&[self.id]);
     }
 }
diff --git a/src/result.rs b/src/result.rs
index 6b464ade9..7f69e8a5f 100644
--- a/src/result.rs
+++ b/src/result.rs
@@ -1,6 +1,3 @@
-#[cfg(not(feature = "std"))]
-use alloc::vec::Vec;
-
 use crate::algo::ComputeResult;
 use crate::geometry::{Point, Size};
 use crate::number::Number;
diff --git a/src/style.rs b/src/style.rs
index 0a30256c3..3d2463cc5 100644
--- a/src/style.rs
+++ b/src/style.rs
@@ -1,6 +1,3 @@
-#[cfg(not(feature = "std"))]
-use alloc::boxed::Box;
-
 use crate::geometry::{Rect, Size};
 use crate::number::Number;