Skip to main content

astrelis_ui/
layout_engine.rs

1//! Configurable layout engine supporting synchronous and asynchronous computation.
2//!
3//! The layout engine provides:
4//! - Synchronous mode: Layout computed on main thread (current behavior)
5//! - Asynchronous mode: Layout computed on background thread with double-buffering
6//! - Hybrid mode: Small subtrees sync, large subtrees async
7//!
8//! # Architecture
9//!
10//! In async mode, the engine maintains two layout caches:
11//! - Front buffer: Read by renderer (last completed layout)
12//! - Back buffer: Written by worker thread (in-progress layout)
13//!
14//! When layout completes, buffers are swapped atomically.
15//!
16//! # Example
17//!
18//! ```ignore
19//! use astrelis_ui::layout_engine::{LayoutEngine, LayoutMode};
20//!
21//! let mut engine = LayoutEngine::new(LayoutMode::Asynchronous {
22//!     max_stale_frames: 2,
23//! });
24//!
25//! // Request layout computation
26//! engine.request_layout(&tree, viewport_size);
27//!
28//! // In render loop: get current layout (may be slightly stale in async mode)
29//! let layout = engine.get_layout(node_id);
30//!
31//! // Poll for completed async results
32//! let completed = engine.poll_results();
33//! ```
34
35use std::collections::HashMap;
36use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
37use std::sync::{Arc, Mutex, RwLock};
38use std::thread::{self, JoinHandle};
39use std::time::Instant;
40
41use astrelis_core::geometry::Size;
42use astrelis_core::profiling::profile_function;
43
44use crate::plugin::registry::WidgetTypeRegistry;
45use crate::tree::{LayoutRect, NodeId, UiTree};
46
47/// Return type for `spawn_worker`: (request sender, result receiver, thread handle).
48type WorkerChannels = (
49    Option<std::sync::mpsc::Sender<WorkerMessage>>,
50    Option<std::sync::mpsc::Receiver<LayoutResult>>,
51    Option<JoinHandle<()>>,
52);
53
54/// Layout computation mode.
55#[derive(Debug, Clone, Default)]
56pub enum LayoutMode {
57    /// Compute layout synchronously on the main thread.
58    /// This is the default and simplest mode.
59    #[default]
60    Synchronous,
61
62    /// Compute layout asynchronously on a background thread.
63    /// Layout results may be 1-N frames stale.
64    Asynchronous {
65        /// Maximum number of frames layout can be stale before
66        /// falling back to synchronous computation.
67        max_stale_frames: u32,
68    },
69
70    /// Hybrid mode: Use sync for small subtrees, async for large ones.
71    Hybrid {
72        /// Node count threshold for async processing.
73        async_threshold: usize,
74        /// Maximum stale frames for async portion.
75        max_stale_frames: u32,
76    },
77}
78
79/// Snapshot of node data for async layout computation.
80#[derive(Debug, Clone)]
81pub struct NodeSnapshot {
82    /// Node identifier.
83    pub node_id: NodeId,
84    /// Node's Taffy style.
85    pub style: taffy::Style,
86    /// Parent node (if any).
87    pub parent: Option<usize>,
88    /// Child node indices.
89    pub children: Vec<usize>,
90    /// Whether measurement results should be cached for this widget type.
91    pub caches_measurement: bool,
92    /// Cached text measurement (width, height) if available.
93    pub text_measurement: Option<(f32, f32)>,
94}
95
96/// Complete snapshot of tree state for async layout.
97#[derive(Debug, Clone)]
98pub struct TreeSnapshot {
99    /// All nodes in the snapshot.
100    pub nodes: Vec<NodeSnapshot>,
101    /// Root node index.
102    pub root: Option<usize>,
103    /// Set of dirty node indices.
104    pub dirty_nodes: Vec<usize>,
105    /// Mapping from NodeId to index.
106    pub id_to_index: HashMap<NodeId, usize>,
107}
108
109impl TreeSnapshot {
110    /// Create a snapshot from a UiTree.
111    pub fn from_tree(tree: &UiTree, widget_registry: &WidgetTypeRegistry) -> Self {
112        let mut nodes = Vec::new();
113        let mut id_to_index = HashMap::new();
114        let mut dirty_nodes = Vec::new();
115
116        // First pass: collect all nodes
117        for (node_id, node) in tree.iter() {
118            let index = nodes.len();
119            id_to_index.insert(node_id, index);
120
121            if !node.dirty_flags.is_empty() {
122                dirty_nodes.push(index);
123            }
124
125            // Check if this widget type caches measurements (via registry)
126            let caches_measurement =
127                widget_registry.caches_measurement(node.widget.as_any().type_id());
128
129            nodes.push(NodeSnapshot {
130                node_id,
131                style: node.widget.style().layout.clone(),
132                parent: None,         // Will be set in second pass
133                children: Vec::new(), // Will be set in second pass
134                caches_measurement,
135                text_measurement: node.text_measurement,
136            });
137        }
138
139        // Second pass: set parent/child relationships
140        for (node_id, node) in tree.iter() {
141            if let Some(&index) = id_to_index.get(&node_id) {
142                // Set parent
143                if let Some(parent_id) = node.parent
144                    && let Some(&parent_index) = id_to_index.get(&parent_id)
145                {
146                    nodes[index].parent = Some(parent_index);
147                }
148
149                // Set children
150                let child_indices: Vec<usize> = node
151                    .children
152                    .iter()
153                    .filter_map(|child_id| id_to_index.get(child_id).copied())
154                    .collect();
155                nodes[index].children = child_indices;
156            }
157        }
158
159        let root = tree.root().and_then(|id| id_to_index.get(&id).copied());
160
161        Self {
162            nodes,
163            root,
164            dirty_nodes,
165            id_to_index,
166        }
167    }
168
169    /// Get node count.
170    pub fn node_count(&self) -> usize {
171        self.nodes.len()
172    }
173}
174
175/// Request for layout computation.
176#[derive(Debug)]
177pub struct LayoutRequest {
178    /// Tree snapshot to compute layout for.
179    tree_snapshot: TreeSnapshot,
180    /// Viewport size.
181    viewport_size: Size<f32>,
182    /// Frame ID for this request.
183    frame_id: u64,
184    /// Timestamp when request was made.
185    _timestamp: Instant,
186}
187
188/// Result of layout computation.
189#[derive(Debug, Clone)]
190pub struct LayoutResult {
191    /// Frame ID this result is for.
192    pub frame_id: u64,
193    /// Computed layouts by node ID.
194    pub layouts: HashMap<NodeId, LayoutRect>,
195    /// Computation time.
196    pub compute_time: std::time::Duration,
197    /// Whether this was a full or partial update.
198    pub is_partial: bool,
199}
200
201/// Cache for layout results (double-buffered).
202struct LayoutCache {
203    /// Primary layout data (read by renderer).
204    front: RwLock<HashMap<NodeId, LayoutRect>>,
205    /// Secondary layout data (written by worker).
206    back: Mutex<HashMap<NodeId, LayoutRect>>,
207    /// Frame ID of front buffer.
208    front_frame_id: AtomicU64,
209    /// Whether a swap is pending.
210    swap_pending: AtomicBool,
211}
212
213impl LayoutCache {
214    fn new() -> Self {
215        Self {
216            front: RwLock::new(HashMap::new()),
217            back: Mutex::new(HashMap::new()),
218            front_frame_id: AtomicU64::new(0),
219            swap_pending: AtomicBool::new(false),
220        }
221    }
222
223    /// Get layout from front buffer.
224    fn get(&self, node_id: NodeId) -> Option<LayoutRect> {
225        self.front.read().ok()?.get(&node_id).copied()
226    }
227
228    /// Write layout to back buffer.
229    fn write_back(&self, node_id: NodeId, layout: LayoutRect) {
230        if let Ok(mut back) = self.back.lock() {
231            back.insert(node_id, layout);
232        }
233    }
234
235    /// Swap front and back buffers.
236    fn swap(&self, frame_id: u64) {
237        if let (Ok(mut front), Ok(mut back)) = (self.front.write(), self.back.lock()) {
238            std::mem::swap(&mut *front, &mut *back);
239            self.front_frame_id.store(frame_id, Ordering::SeqCst);
240            back.clear();
241            self.swap_pending.store(false, Ordering::SeqCst);
242        }
243    }
244
245    /// Mark swap as pending.
246    fn mark_swap_pending(&self) {
247        self.swap_pending.store(true, Ordering::SeqCst);
248    }
249
250    /// Check if swap is pending.
251    fn is_swap_pending(&self) -> bool {
252        self.swap_pending.load(Ordering::SeqCst)
253    }
254
255    /// Get frame ID of front buffer.
256    fn front_frame_id(&self) -> u64 {
257        self.front_frame_id.load(Ordering::SeqCst)
258    }
259}
260
261/// Message types for worker thread.
262enum WorkerMessage {
263    /// Request layout computation.
264    Compute(LayoutRequest),
265    /// Shut down the worker.
266    Shutdown,
267}
268
269/// Configurable layout engine.
270pub struct LayoutEngine {
271    /// Current layout mode.
272    mode: LayoutMode,
273    /// Double-buffered layout cache.
274    cache: Arc<LayoutCache>,
275    /// Current frame ID.
276    frame_id: u64,
277    /// Frame ID of last completed layout.
278    last_completed_frame: u64,
279
280    // Async mode fields
281    /// Sender for layout requests.
282    request_sender: Option<std::sync::mpsc::Sender<WorkerMessage>>,
283    /// Receiver for layout results.
284    result_receiver: Option<std::sync::mpsc::Receiver<LayoutResult>>,
285    /// Worker thread handle.
286    worker_handle: Option<JoinHandle<()>>,
287    /// Whether async layout is in progress.
288    layout_in_progress: Arc<AtomicBool>,
289}
290
291impl LayoutEngine {
292    /// Create a new layout engine with the specified mode.
293    pub fn new(mode: LayoutMode) -> Self {
294        let cache = Arc::new(LayoutCache::new());
295        let layout_in_progress = Arc::new(AtomicBool::new(false));
296
297        let (request_sender, result_receiver, worker_handle) = match &mode {
298            LayoutMode::Synchronous => (None, None, None),
299            LayoutMode::Asynchronous { .. } | LayoutMode::Hybrid { .. } => {
300                Self::spawn_worker(cache.clone(), layout_in_progress.clone())
301            }
302        };
303
304        Self {
305            mode,
306            cache,
307            frame_id: 0,
308            last_completed_frame: 0,
309            request_sender,
310            result_receiver,
311            worker_handle,
312            layout_in_progress,
313        }
314    }
315
316    /// Spawn the layout worker thread.
317    fn spawn_worker(cache: Arc<LayoutCache>, in_progress: Arc<AtomicBool>) -> WorkerChannels {
318        let (request_tx, request_rx) = std::sync::mpsc::channel::<WorkerMessage>();
319        let (result_tx, result_rx) = std::sync::mpsc::channel::<LayoutResult>();
320
321        let handle = thread::Builder::new()
322            .name("layout-worker".to_string())
323            .spawn(move || {
324                Self::worker_loop(request_rx, result_tx, cache, in_progress);
325            })
326            .expect("Failed to spawn layout worker thread");
327
328        (Some(request_tx), Some(result_rx), Some(handle))
329    }
330
331    /// Worker thread main loop.
332    fn worker_loop(
333        request_rx: std::sync::mpsc::Receiver<WorkerMessage>,
334        result_tx: std::sync::mpsc::Sender<LayoutResult>,
335        cache: Arc<LayoutCache>,
336        in_progress: Arc<AtomicBool>,
337    ) {
338        while let Ok(msg) = request_rx.recv() {
339            match msg {
340                WorkerMessage::Compute(request) => {
341                    in_progress.store(true, Ordering::SeqCst);
342                    let start = Instant::now();
343
344                    // Perform layout computation
345                    let layouts =
346                        Self::compute_layout_sync(&request.tree_snapshot, request.viewport_size);
347
348                    // Write results to back buffer
349                    for (node_id, layout) in &layouts {
350                        cache.write_back(*node_id, *layout);
351                    }
352
353                    // Mark swap pending
354                    cache.mark_swap_pending();
355
356                    let result = LayoutResult {
357                        frame_id: request.frame_id,
358                        layouts,
359                        compute_time: start.elapsed(),
360                        is_partial: false,
361                    };
362
363                    let _ = result_tx.send(result);
364                    in_progress.store(false, Ordering::SeqCst);
365                }
366                WorkerMessage::Shutdown => break,
367            }
368        }
369    }
370
371    /// Compute layout synchronously from a snapshot.
372    fn compute_layout_sync(
373        snapshot: &TreeSnapshot,
374        viewport_size: Size<f32>,
375    ) -> HashMap<NodeId, LayoutRect> {
376        let mut taffy = taffy::TaffyTree::new();
377        let mut taffy_nodes: HashMap<usize, taffy::NodeId> = HashMap::new();
378        let mut results = HashMap::new();
379
380        // Build Taffy tree from snapshot
381        for (index, node) in snapshot.nodes.iter().enumerate() {
382            let taffy_node = if node.children.is_empty() {
383                taffy.new_leaf(node.style.clone()).unwrap()
384            } else {
385                taffy.new_with_children(node.style.clone(), &[]).unwrap()
386            };
387            taffy_nodes.insert(index, taffy_node);
388        }
389
390        // Set up parent-child relationships
391        for (index, node) in snapshot.nodes.iter().enumerate() {
392            if !node.children.is_empty() {
393                let children: Vec<taffy::NodeId> = node
394                    .children
395                    .iter()
396                    .filter_map(|&child_idx| taffy_nodes.get(&child_idx).copied())
397                    .collect();
398
399                if let Some(&parent_taffy) = taffy_nodes.get(&index) {
400                    let _ = taffy.set_children(parent_taffy, &children);
401                }
402            }
403        }
404
405        // Compute layout
406        if let Some(root_idx) = snapshot.root
407            && let Some(&root_taffy) = taffy_nodes.get(&root_idx)
408        {
409            let available = taffy::Size {
410                width: taffy::AvailableSpace::Definite(viewport_size.width),
411                height: taffy::AvailableSpace::Definite(viewport_size.height),
412            };
413
414            // Simple measure function using cached measurements
415            let measure_fn = |known_dimensions: taffy::Size<Option<f32>>,
416                              _available_space: taffy::Size<taffy::AvailableSpace>,
417                              node_id: taffy::NodeId,
418                              _node_context: Option<&mut ()>,
419                              _style: &taffy::Style|
420             -> taffy::Size<f32> {
421                // Find the snapshot node for this taffy node
422                for (idx, &tn) in &taffy_nodes {
423                    if tn == node_id
424                        && let Some(node) = snapshot.nodes.get(*idx)
425                        && let Some((w, h)) = node.text_measurement
426                    {
427                        return taffy::Size {
428                            width: known_dimensions.width.unwrap_or(w),
429                            height: known_dimensions.height.unwrap_or(h),
430                        };
431                    }
432                }
433                taffy::Size {
434                    width: known_dimensions.width.unwrap_or(0.0),
435                    height: known_dimensions.height.unwrap_or(0.0),
436                }
437            };
438
439            let _ = taffy.compute_layout_with_measure(root_taffy, available, measure_fn);
440        }
441
442        // Extract layouts
443        for (index, node) in snapshot.nodes.iter().enumerate() {
444            if let Some(&taffy_node) = taffy_nodes.get(&index)
445                && let Ok(layout) = taffy.layout(taffy_node)
446            {
447                results.insert(
448                    node.node_id,
449                    LayoutRect {
450                        x: layout.location.x,
451                        y: layout.location.y,
452                        width: layout.size.width,
453                        height: layout.size.height,
454                    },
455                );
456            }
457        }
458
459        results
460    }
461
462    /// Set the layout mode.
463    pub fn set_mode(&mut self, mode: LayoutMode) {
464        // Shut down existing worker if switching away from async
465        if let Some(sender) = self.request_sender.take() {
466            let _ = sender.send(WorkerMessage::Shutdown);
467        }
468        if let Some(handle) = self.worker_handle.take() {
469            let _ = handle.join();
470        }
471        self.result_receiver = None;
472
473        // Spawn new worker if switching to async
474        let (request_sender, result_receiver, worker_handle) = match &mode {
475            LayoutMode::Synchronous => (None, None, None),
476            LayoutMode::Asynchronous { .. } | LayoutMode::Hybrid { .. } => {
477                Self::spawn_worker(self.cache.clone(), self.layout_in_progress.clone())
478            }
479        };
480
481        self.mode = mode;
482        self.request_sender = request_sender;
483        self.result_receiver = result_receiver;
484        self.worker_handle = worker_handle;
485    }
486
487    /// Get the current layout mode.
488    pub fn mode(&self) -> &LayoutMode {
489        &self.mode
490    }
491
492    /// Compute layout for the tree.
493    ///
494    /// In synchronous mode, this blocks until layout is complete.
495    /// In async mode, this queues a layout request and returns immediately.
496    pub fn compute_layout(
497        &mut self,
498        tree: &UiTree,
499        viewport_size: Size<f32>,
500        widget_registry: &WidgetTypeRegistry,
501    ) {
502        profile_function!();
503        self.frame_id += 1;
504
505        match &self.mode {
506            LayoutMode::Synchronous => {
507                self.compute_layout_synchronous(tree, viewport_size, widget_registry);
508            }
509            LayoutMode::Asynchronous { max_stale_frames } => {
510                let frames_stale = self.frame_id.saturating_sub(self.last_completed_frame);
511                if frames_stale > *max_stale_frames as u64 {
512                    // Too stale, fall back to sync
513                    self.compute_layout_synchronous(tree, viewport_size, widget_registry);
514                } else {
515                    self.compute_layout_async(tree, viewport_size, widget_registry);
516                }
517            }
518            LayoutMode::Hybrid {
519                async_threshold,
520                max_stale_frames,
521            } => {
522                let node_count = tree.iter().count();
523                if node_count < *async_threshold {
524                    self.compute_layout_synchronous(tree, viewport_size, widget_registry);
525                } else {
526                    let frames_stale = self.frame_id.saturating_sub(self.last_completed_frame);
527                    if frames_stale > *max_stale_frames as u64 {
528                        self.compute_layout_synchronous(tree, viewport_size, widget_registry);
529                    } else {
530                        self.compute_layout_async(tree, viewport_size, widget_registry);
531                    }
532                }
533            }
534        }
535    }
536
537    /// Compute layout synchronously.
538    fn compute_layout_synchronous(
539        &mut self,
540        tree: &UiTree,
541        viewport_size: Size<f32>,
542        widget_registry: &WidgetTypeRegistry,
543    ) {
544        let snapshot = TreeSnapshot::from_tree(tree, widget_registry);
545        let layouts = Self::compute_layout_sync(&snapshot, viewport_size);
546
547        // Write directly to front buffer
548        if let Ok(mut front) = self.cache.front.write() {
549            front.clear();
550            front.extend(layouts);
551        }
552
553        self.cache
554            .front_frame_id
555            .store(self.frame_id, Ordering::SeqCst);
556        self.last_completed_frame = self.frame_id;
557    }
558
559    /// Queue async layout computation.
560    fn compute_layout_async(
561        &mut self,
562        tree: &UiTree,
563        viewport_size: Size<f32>,
564        widget_registry: &WidgetTypeRegistry,
565    ) {
566        // Don't queue if already in progress
567        if self.layout_in_progress.load(Ordering::SeqCst) {
568            return;
569        }
570
571        if let Some(sender) = &self.request_sender {
572            let snapshot = TreeSnapshot::from_tree(tree, widget_registry);
573            let request = LayoutRequest {
574                tree_snapshot: snapshot,
575                viewport_size,
576                frame_id: self.frame_id,
577                _timestamp: Instant::now(),
578            };
579            let _ = sender.send(WorkerMessage::Compute(request));
580        }
581    }
582
583    /// Poll for async layout results.
584    ///
585    /// Returns the number of results processed.
586    pub fn poll_results(&mut self) -> usize {
587        let mut count = 0;
588
589        if let Some(receiver) = &self.result_receiver {
590            while let Ok(result) = receiver.try_recv() {
591                self.last_completed_frame = result.frame_id;
592                count += 1;
593            }
594        }
595
596        // Swap buffers if pending
597        if self.cache.is_swap_pending() {
598            self.cache.swap(self.last_completed_frame);
599        }
600
601        count
602    }
603
604    /// Get layout for a node.
605    ///
606    /// In async mode, this may return a slightly stale layout.
607    pub fn get_layout(&self, node_id: NodeId) -> Option<LayoutRect> {
608        self.cache.get(node_id)
609    }
610
611    /// Check if layout is current (not stale).
612    pub fn is_layout_current(&self) -> bool {
613        self.cache.front_frame_id() >= self.frame_id
614    }
615
616    /// Check if async layout is in progress.
617    pub fn is_layout_in_progress(&self) -> bool {
618        self.layout_in_progress.load(Ordering::SeqCst)
619    }
620
621    /// Get the number of frames layout is stale by.
622    pub fn frames_stale(&self) -> u64 {
623        self.frame_id.saturating_sub(self.cache.front_frame_id())
624    }
625
626    /// Clear the layout cache.
627    pub fn clear(&mut self) {
628        if let Ok(mut front) = self.cache.front.write() {
629            front.clear();
630        }
631        if let Ok(mut back) = self.cache.back.lock() {
632            back.clear();
633        }
634    }
635}
636
637impl Drop for LayoutEngine {
638    fn drop(&mut self) {
639        // Shut down worker thread
640        if let Some(sender) = self.request_sender.take() {
641            let _ = sender.send(WorkerMessage::Shutdown);
642        }
643        if let Some(handle) = self.worker_handle.take() {
644            let _ = handle.join();
645        }
646    }
647}
648
649impl Default for LayoutEngine {
650    fn default() -> Self {
651        Self::new(LayoutMode::default())
652    }
653}
654
655#[cfg(test)]
656mod tests {
657    use super::*;
658
659    #[test]
660    fn test_layout_mode_default() {
661        let mode = LayoutMode::default();
662        assert!(matches!(mode, LayoutMode::Synchronous));
663    }
664
665    #[test]
666    fn test_tree_snapshot() {
667        let registry = WidgetTypeRegistry::new();
668        let mut tree = UiTree::new();
669        let root = tree.add_widget(Box::new(crate::widgets::Container::new()));
670        let child = tree.add_widget(Box::new(crate::widgets::Container::new()));
671        tree.add_child(root, child);
672        tree.set_root(root);
673
674        let snapshot = TreeSnapshot::from_tree(&tree, &registry);
675        assert_eq!(snapshot.node_count(), 2);
676        assert!(snapshot.root.is_some());
677    }
678
679    #[test]
680    fn test_layout_engine_sync() {
681        let registry = WidgetTypeRegistry::new();
682        let mut engine = LayoutEngine::new(LayoutMode::Synchronous);
683
684        let mut tree = UiTree::new();
685        let root = tree.add_widget(Box::new(crate::widgets::Container::new()));
686        tree.set_root(root);
687
688        engine.compute_layout(&tree, Size::new(800.0, 600.0), &registry);
689
690        assert!(engine.is_layout_current());
691        assert!(!engine.is_layout_in_progress());
692    }
693
694    #[test]
695    fn test_layout_engine_mode_switch() {
696        let mut engine = LayoutEngine::new(LayoutMode::Synchronous);
697        assert!(matches!(engine.mode(), LayoutMode::Synchronous));
698
699        engine.set_mode(LayoutMode::Asynchronous {
700            max_stale_frames: 2,
701        });
702        assert!(matches!(engine.mode(), LayoutMode::Asynchronous { .. }));
703
704        engine.set_mode(LayoutMode::Synchronous);
705        assert!(matches!(engine.mode(), LayoutMode::Synchronous));
706    }
707
708    #[test]
709    fn test_layout_cache() {
710        let cache = LayoutCache::new();
711        let node_id = NodeId(1);
712        let layout = LayoutRect {
713            x: 0.0,
714            y: 0.0,
715            width: 100.0,
716            height: 50.0,
717        };
718
719        // Write to back buffer
720        cache.write_back(node_id, layout);
721
722        // Not in front buffer yet
723        assert!(cache.get(node_id).is_none());
724
725        // Swap
726        cache.mark_swap_pending();
727        cache.swap(1);
728
729        // Now in front buffer
730        let result = cache.get(node_id);
731        assert!(result.is_some());
732        assert_eq!(result.unwrap().width, 100.0);
733    }
734
735    #[test]
736    fn test_frames_stale() {
737        let registry = WidgetTypeRegistry::new();
738        let mut engine = LayoutEngine::new(LayoutMode::Synchronous);
739
740        let mut tree = UiTree::new();
741        let root = tree.add_widget(Box::new(crate::widgets::Container::new()));
742        tree.set_root(root);
743
744        engine.compute_layout(&tree, Size::new(800.0, 600.0), &registry);
745        assert_eq!(engine.frames_stale(), 0);
746    }
747}