1use 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#[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 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 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 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 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}