Skip to main content

apace_dom/
resources.rs

1//! Bounded document resources. Local patch for long-lived virtualized views.
2use std::collections::HashMap;
3
4use crate::{BaseDocument, NodeId, node::ImageData, util::ImageType};
5#[cfg(test)]
6#[path = "image_observation_tests.rs"]
7mod image_observation_tests;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub struct ResourceLimits {
11    pub requests: usize,
12    pub completion_bytes: usize,
13    pub encoded_bytes: usize,
14    pub image_cache_entries: usize,
15    pub image_cache_bytes: usize,
16    pub image_decoded_bytes: u64,
17    pub image_dimension: u32,
18    pub pending_images: usize,
19    pub image_waiters: usize,
20    pub font_entries: usize,
21    pub font_bytes: usize,
22}
23impl Default for ResourceLimits {
24    fn default() -> Self {
25        Self {
26            requests: 256,
27            completion_bytes: 32 * 1024 * 1024,
28            encoded_bytes: 8 * 1024 * 1024,
29            image_cache_entries: 64,
30            image_cache_bytes: 16 * 1024 * 1024,
31            image_decoded_bytes: 16 * 1024 * 1024,
32            image_dimension: 4096,
33            pending_images: 128,
34            image_waiters: 4096,
35            font_entries: 64,
36            font_bytes: 32 * 1024 * 1024,
37        }
38    }
39}
40
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub struct ResourceStats {
43    pub requests: usize,
44    pub completions: usize,
45    pub completion_bytes: usize,
46    pub decode_waiters: usize,
47    pub cached_images: usize,
48    pub cached_image_bytes: usize,
49    pub image_evictions: u64,
50    pub pending_images: usize,
51    pub image_waiters: usize,
52    pub registered_fonts: usize,
53    pub registered_font_bytes: usize,
54    pub rejected: u64,
55    pub failures: u64,
56}
57
58/// One live element consumer, independently of a shared URL fetch.
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub struct ImageRequestId(pub u64);
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum ImageLoadStatus {
63    Loaded,
64    Failed,
65}
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub struct ImageLoadEvent {
68    pub node: NodeId,
69    pub request: ImageRequestId,
70    pub status: ImageLoadStatus,
71}
72#[derive(Default)]
73pub(crate) struct ImageObservations {
74    next: u64,
75    live: HashMap<NodeId, ImageConsumer>,
76    sequence: u64,
77    ready: HashMap<NodeId, (u64, ImageLoadEvent)>,
78}
79struct ImageConsumer {
80    request: ImageRequestId,
81    terminal: bool,
82}
83impl ImageObservations {
84    pub fn begin(&mut self, node: NodeId) -> Option<ImageRequestId> {
85        self.ready.remove(&node);
86        self.live.remove(&node);
87        let next = self.next.checked_add(1)?;
88        self.next = next;
89        let request = ImageRequestId(next);
90        self.live.insert(
91            node,
92            ImageConsumer {
93                request,
94                terminal: false,
95            },
96        );
97        Some(request)
98    }
99    pub fn retire(&mut self, node: NodeId) {
100        self.live.remove(&node);
101        self.ready.remove(&node);
102    }
103    pub fn complete(&mut self, node: NodeId, request: ImageRequestId, status: ImageLoadStatus) {
104        if let Some(consumer) = self.live.get_mut(&node)
105            && consumer.request == request
106            && !consumer.terminal
107        {
108            consumer.terminal = true;
109            self.sequence = self
110                .sequence
111                .checked_add(1)
112                .expect("image completion count is bounded by admitted requests");
113            self.ready.insert(
114                node,
115                (
116                    self.sequence,
117                    ImageLoadEvent {
118                        node,
119                        request,
120                        status,
121                    },
122                ),
123            );
124        }
125    }
126}
127
128pub(crate) struct PendingImage {
129    pub request_id: usize,
130    pub waiters: Vec<(NodeId, ImageType, Option<ImageRequestId>)>,
131}
132
133#[derive(Default)]
134pub(crate) struct ImageCache {
135    entries: HashMap<String, (ImageData, usize, u128)>,
136    bytes: usize,
137    clock: u128,
138    evictions: u64,
139    limits: ResourceLimits,
140}
141impl ImageCache {
142    fn remove(&mut self, url: &str) {
143        if let Some((_, bytes, _)) = self.entries.remove(url) {
144            self.bytes -= bytes;
145            self.evictions = self.evictions.saturating_add(1);
146        }
147    }
148    pub fn get(&mut self, url: &str) -> Option<&ImageData> {
149        self.clock = self.clock.saturating_add(1);
150        let entry = self.entries.get_mut(url)?;
151        entry.2 = self.clock;
152        Some(&entry.0)
153    }
154    pub fn insert(&mut self, url: String, image: ImageData) {
155        // SVG owns an opaque tree: do not pretend its allocation is RGBA size.
156        // Live elements can still hold it; the optional reuse cache skips it.
157        let ImageData::Raster(raster) = &image else {
158            return;
159        };
160        let Some(bytes) = raster.data.len().checked_add(url.len()) else {
161            return;
162        };
163        if let Some(old) = self.entries.remove(&url) {
164            self.bytes -= old.1;
165        }
166        if self.limits.image_cache_entries == 0 || bytes > self.limits.image_cache_bytes {
167            return;
168        }
169        while self.entries.len() >= self.limits.image_cache_entries
170            || self.bytes > self.limits.image_cache_bytes - bytes
171        {
172            self.evict();
173        }
174        self.clock = self.clock.saturating_add(1);
175        self.bytes += bytes;
176        self.entries.insert(url, (image, bytes, self.clock));
177    }
178    fn evict(&mut self) {
179        let Some(key) = self
180            .entries
181            .iter()
182            .min_by_key(|(_, value)| value.2)
183            .map(|(key, _)| key.clone())
184        else {
185            return;
186        };
187        self.bytes -= self.entries.remove(&key).expect("cache entry").1;
188        self.evictions = self.evictions.saturating_add(1);
189    }
190    fn set_limits(&mut self, limits: ResourceLimits) {
191        self.limits = limits;
192        while self.entries.len() > limits.image_cache_entries
193            || self.bytes > limits.image_cache_bytes
194        {
195            self.evict();
196        }
197    }
198}
199
200impl BaseDocument {
201    pub(crate) fn cancel_replaced_image_consumers(&mut self, node: NodeId) {
202        let provider = &self.net_provider;
203        let document = self.id();
204        self.pending_images.retain(|url, pending| {
205            pending
206                .waiters
207                .retain(|(id, _, request)| *id != node || request.is_none());
208            if pending.waiters.is_empty() {
209                self.tx.pool.cancel(pending.request_id);
210                provider.cancel(document, url);
211                false
212            } else {
213                true
214            }
215        });
216    }
217    /// Current element-consumer identity; changes even for an explicit same-URL reload.
218    pub fn image_request(&self, node: NodeId) -> Option<ImageRequestId> {
219        self.image_observations
220            .live
221            .get(&node)
222            .map(|consumer| consumer.request)
223    }
224    /// Terminal observations recorded after actual resource consumption, including cache hits.
225    /// Storage contains at most one result per live image element; no URL/history scan.
226    pub fn take_image_events(&mut self) -> Vec<ImageLoadEvent> {
227        let mut events: Vec<_> = self
228            .image_observations
229            .ready
230            .drain()
231            .map(|(_, event)| event)
232            .collect();
233        events.sort_by_key(|(sequence, _)| *sequence);
234        events.into_iter().map(|(_, event)| event).collect()
235    }
236
237    pub fn resource_stats(&self) -> ResourceStats {
238        let pool = self.tx.pool.stats();
239        ResourceStats {
240            requests: pool.0,
241            completions: pool.1,
242            completion_bytes: pool.2,
243            decode_waiters: pool.3,
244            cached_images: self.image_cache.entries.len(),
245            cached_image_bytes: self.image_cache.bytes,
246            image_evictions: self.image_cache.evictions,
247            pending_images: self.pending_images.len(),
248            image_waiters: self
249                .pending_images
250                .values()
251                .map(|image| image.waiters.len())
252                .sum(),
253            registered_fonts: self.resource_counters.registered_fonts,
254            registered_font_bytes: self.resource_counters.registered_font_bytes,
255            rejected: self.resource_counters.rejected.saturating_add(pool.4),
256            failures: self.resource_counters.failures,
257        }
258    }
259    pub fn set_resource_limits(&mut self, limits: ResourceLimits) -> Result<(), &'static str> {
260        if self.resource_counters.registered_fonts > limits.font_entries
261            || self.resource_counters.registered_font_bytes > limits.font_bytes
262            || self.pending_images.len() > limits.pending_images
263            || self
264                .pending_images
265                .values()
266                .any(|pending| pending.waiters.len() > limits.image_waiters)
267        {
268            return Err("new resource limits are below live non-evictable resources");
269        }
270        self.tx.pool.set_limits(limits)?;
271        self.resource_limits = limits;
272        self.image_cache.set_limits(limits);
273        Ok(())
274    }
275    /// Cancel image requests without a live consumer. Work depends only on the
276    /// bounded pending table, never the source data or all visited URLs.
277    /// Revoke a resource's reusable decoded image and pending delivery. Current
278    /// elements may retain pixels already delivered; future consumers must go
279    /// through the provider again. No mounted-tree scan or URL history is kept.
280    pub fn revoke_image_url(&mut self, url: &str) {
281        self.image_cache.remove(url);
282        if let Some(pending) = self.pending_images.get(url) {
283            let request_id = pending.request_id;
284            self.tx.pool.cancel(request_id);
285            self.net_provider.cancel(self.id(), url);
286            self.load_resource(crate::net::ResourceLoadResponse {
287                request_id,
288                node_id: None,
289                resolved_url: Some(url.to_owned()),
290                result: Err("image resource revoked".into()),
291                ticket: None,
292            });
293        }
294    }
295
296    pub fn prune_resource_interests(&mut self) {
297        let nodes = &self.nodes;
298        let base = &self.url;
299        let provider = &self.net_provider;
300        let doc_id = self.id();
301        let mut removed = Vec::new();
302        self.pending_images.retain(|url, pending| {
303            pending.waiters.retain(|(id, kind, request)| {
304                if request.is_some()
305                    && self
306                        .image_observations
307                        .live
308                        .get(id)
309                        .map(|consumer| consumer.request)
310                        != *request
311                {
312                    return false;
313                }
314                let Some(node) = nodes.get(*id).filter(|node| node.flags.is_in_document()) else {
315                    removed.push((*id, *kind, url.clone()));
316                    return false;
317                };
318                let Some(element) = node.element_data() else {
319                    return false;
320                };
321                match kind {
322                    ImageType::Image => element
323                        .attr(crate::local_name!("src"))
324                        .and_then(|value| base.resolve_relative(value))
325                        .is_some_and(|value| value.as_str() == url),
326                    ImageType::Background(index) => element
327                        .background_images
328                        .get(*index)
329                        .and_then(|value| value.as_ref())
330                        .is_some_and(|value| value.url.as_str() == url),
331                    ImageType::Mask(index) => element
332                        .mask_images
333                        .get(*index)
334                        .and_then(|value| value.as_ref())
335                        .is_some_and(|value| value.url.as_str() == url),
336                }
337            });
338            if pending.waiters.is_empty() {
339                self.tx.pool.cancel(pending.request_id);
340                provider.cancel(doc_id, url);
341                false
342            } else {
343                true
344            }
345        });
346        for (id, kind, url) in removed {
347            let Some(element) = self
348                .nodes
349                .get_mut(id)
350                .and_then(|node| node.element_data_mut())
351            else {
352                continue;
353            };
354            let layer = match kind {
355                ImageType::Background(index) => element.background_images.get_mut(index),
356                ImageType::Mask(index) => element.mask_images.get_mut(index),
357                ImageType::Image => None,
358            };
359            if let Some(Some(layer)) = layer
360                && layer.url.as_str() == url
361            {
362                layer.status = crate::node::Status::Error;
363            }
364        }
365    }
366}
367
368impl Drop for BaseDocument {
369    fn drop(&mut self) {
370        self.tx.pool.close();
371        drop(self.rx.take());
372        self.net_provider.close(self.id());
373    }
374}