rusteerio 0.1.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +7 -0
- package/THIRD_PARTY_NOTICES.md +32 -1
- package/dist/{chunk-DKULX6VO.mjs → chunk-DMRD3FGC.mjs} +1 -1
- package/dist/chunk-DMRD3FGC.mjs.map +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/slim.cjs.map +1 -1
- package/dist/slim.mjs +1 -1
- package/native/src/arena.rs +54 -535
- package/native/src/codec.rs +62 -18
- package/native/src/hook_sink.rs +77 -3
- package/native/src/lib.rs +245 -18
- package/native/src/markup_tokenizer.rs +668 -0
- package/native/src/tokenizer.rs +355 -199
- package/native/vendor/README.md +3 -2
- package/native/vendor/html5ever-0.35.0/src/tokenizer/mod.rs +23 -13
- package/package.json +1 -1
- package/rusteerio.darwin-arm64.node +0 -0
- package/rusteerio.darwin-x64.node +0 -0
- package/rusteerio.linux-arm64-gnu.node +0 -0
- package/rusteerio.linux-arm64-musl.node +0 -0
- package/rusteerio.linux-x64-gnu.node +0 -0
- package/rusteerio.linux-x64-musl.node +0 -0
- package/rusteerio.win32-arm64-msvc.node +0 -0
- package/rusteerio.win32-x64-msvc.node +0 -0
- package/scripts/generate-notices.mjs +11 -0
- package/scripts/notices/htmlparser2-MIT.txt +18 -0
- package/scripts/notices/sources.json +6 -0
- package/src/router.ts +3 -2
- package/dist/chunk-DKULX6VO.mjs.map +0 -1
package/native/src/codec.rs
CHANGED
|
@@ -10,6 +10,9 @@ pub(crate) fn has_encoding(value: &str) -> bool {
|
|
|
10
10
|
value.chars().any(is_reserved)
|
|
11
11
|
}
|
|
12
12
|
pub(crate) fn from_units(units: &[u16]) -> String {
|
|
13
|
+
if units.iter().all(|unit| !(0xd800..=0xe800).contains(unit)) {
|
|
14
|
+
return String::from_utf16_lossy(units);
|
|
15
|
+
}
|
|
13
16
|
let mut output = String::with_capacity(units.len());
|
|
14
17
|
let mut index = 0;
|
|
15
18
|
while index < units.len() {
|
|
@@ -38,28 +41,69 @@ pub(crate) fn from_units(units: &[u16]) -> String {
|
|
|
38
41
|
}
|
|
39
42
|
output
|
|
40
43
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
/// Receives the UTF-16 units of a string from [`visit_units`].
|
|
45
|
+
pub(crate) trait UnitSink {
|
|
46
|
+
/// A run of ASCII bytes starting at byte offset `start`, one unit each.
|
|
47
|
+
fn ascii(&mut self, start: usize, run: &[u8]);
|
|
48
|
+
/// One unit and the byte offset of the character that starts there, or
|
|
49
|
+
/// `None` for the low half of a surrogate pair.
|
|
50
|
+
fn unit(&mut self, offset: Option<usize>, unit: u16);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl UnitSink for Vec<u16> {
|
|
54
|
+
fn ascii(&mut self, _: usize, run: &[u8]) {
|
|
55
|
+
self.extend(run.iter().map(|value| u16::from(*value)));
|
|
56
|
+
}
|
|
57
|
+
fn unit(&mut self, _: Option<usize>, unit: u16) {
|
|
58
|
+
self.push(unit);
|
|
53
59
|
}
|
|
54
|
-
output
|
|
55
60
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
|
|
62
|
+
/// Feeds the UTF-16 units of `value` to `sink`. With `framed`, lossless frames
|
|
63
|
+
/// decode to their original units.
|
|
64
|
+
pub(crate) fn visit_units(value: &str, framed: bool, sink: &mut impl UnitSink) {
|
|
65
|
+
let bytes = value.as_bytes();
|
|
66
|
+
let mut byte = 0;
|
|
67
|
+
while byte < bytes.len() {
|
|
68
|
+
let ascii = bytes[byte..]
|
|
69
|
+
.iter()
|
|
70
|
+
.position(|value| !value.is_ascii())
|
|
71
|
+
.unwrap_or(bytes.len() - byte);
|
|
72
|
+
sink.ascii(byte, &bytes[byte..byte + ascii]);
|
|
73
|
+
byte += ascii;
|
|
74
|
+
let Some(ch) = value[byte..].chars().next() else {
|
|
75
|
+
break;
|
|
76
|
+
};
|
|
77
|
+
let start = byte;
|
|
78
|
+
byte += ch.len_utf8();
|
|
79
|
+
if framed && ch == ESCAPE {
|
|
80
|
+
if let Some(next) = value[byte..]
|
|
81
|
+
.chars()
|
|
82
|
+
.next()
|
|
83
|
+
.filter(|next| is_reserved(*next))
|
|
84
|
+
{
|
|
85
|
+
sink.unit(Some(start), next as u16);
|
|
86
|
+
byte += next.len_utf8();
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if framed && ('\u{e000}'..ESCAPE).contains(&ch) {
|
|
91
|
+
sink.unit(Some(start), 0xd800 + (ch as u16 - 0xe000));
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
let mut encoded = [0; 2];
|
|
95
|
+
let encoded = ch.encode_utf16(&mut encoded);
|
|
96
|
+
sink.unit(Some(start), encoded[0]);
|
|
97
|
+
if let Some(low) = encoded.get(1) {
|
|
98
|
+
sink.unit(None, *low);
|
|
99
|
+
}
|
|
61
100
|
}
|
|
62
101
|
}
|
|
102
|
+
pub(crate) fn to_units(value: &str) -> Vec<u16> {
|
|
103
|
+
let mut output = Vec::with_capacity(value.len());
|
|
104
|
+
visit_units(value, has_encoding(value), &mut output);
|
|
105
|
+
output
|
|
106
|
+
}
|
|
63
107
|
pub(crate) fn to_usv(value: &str) -> String {
|
|
64
108
|
if has_encoding(value) {
|
|
65
109
|
String::from_utf16_lossy(&to_units(value))
|
package/native/src/hook_sink.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//! An active user-supplied tree adapter. Rust runs the HTML5 algorithm; the
|
|
2
2
|
//! adapter supplies every construction, mutation and tree-inspection decision.
|
|
3
3
|
use std::borrow::Cow;
|
|
4
|
-
use std::cell::{Ref, RefCell};
|
|
4
|
+
use std::cell::{Cell, Ref, RefCell};
|
|
5
5
|
use std::collections::HashMap;
|
|
6
6
|
use std::rc::Rc;
|
|
7
7
|
|
|
@@ -196,6 +196,26 @@ struct HookSink {
|
|
|
196
196
|
context: Option<Rc<crate::diagnostics::DiagnosticContext>>,
|
|
197
197
|
diagnostics: RefCell<Vec<Value>>,
|
|
198
198
|
locations: Option<crate::tracker::Locations<u32>>,
|
|
199
|
+
pending_escape: Cell<Option<TextTarget>>,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Where a text chunk is inserted: appended to `parent`, or before `sibling`.
|
|
203
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
204
|
+
struct TextTarget {
|
|
205
|
+
parent: u32,
|
|
206
|
+
sibling: Option<u32>,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Whether the final character is a codec escape whose character has not
|
|
210
|
+
/// arrived yet. Escapes pair with the following character.
|
|
211
|
+
fn ends_with_unpaired_escape(text: &str) -> bool {
|
|
212
|
+
let mut chars = text.chars();
|
|
213
|
+
while let Some(ch) = chars.next() {
|
|
214
|
+
if ch == crate::codec::ESCAPE && chars.next().is_none() {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
false
|
|
199
219
|
}
|
|
200
220
|
|
|
201
221
|
impl HookSink {
|
|
@@ -203,6 +223,17 @@ impl HookSink {
|
|
|
203
223
|
if self.error.borrow().is_some() {
|
|
204
224
|
return Value::Null;
|
|
205
225
|
}
|
|
226
|
+
if matches!(
|
|
227
|
+
method,
|
|
228
|
+
"appendChild"
|
|
229
|
+
| "insertBefore"
|
|
230
|
+
| "detachNode"
|
|
231
|
+
| "setDocumentType"
|
|
232
|
+
| "setTemplateContent"
|
|
233
|
+
| "__snapshot"
|
|
234
|
+
) {
|
|
235
|
+
self.flush_escape();
|
|
236
|
+
}
|
|
206
237
|
let result = self.bridge.call(method, args);
|
|
207
238
|
match result {
|
|
208
239
|
Ok(value) => value,
|
|
@@ -215,6 +246,38 @@ impl HookSink {
|
|
|
215
246
|
fn node(&self, method: &str, args: Value) -> u32 {
|
|
216
247
|
self.call(method, args).as_u64().unwrap_or(0) as u32
|
|
217
248
|
}
|
|
249
|
+
/// The tokenizer may split a codec escape from its character across text
|
|
250
|
+
/// chunks. Adapters decode each chunk separately, so a trailing escape is
|
|
251
|
+
/// held until the next chunk for the same target.
|
|
252
|
+
fn insert_text(&self, target: TextTarget, text: &str) {
|
|
253
|
+
let mut data = String::with_capacity(text.len() + crate::codec::ESCAPE.len_utf8());
|
|
254
|
+
if let Some(pending) = self.pending_escape.take() {
|
|
255
|
+
if pending == target {
|
|
256
|
+
data.push(crate::codec::ESCAPE);
|
|
257
|
+
} else {
|
|
258
|
+
self.send_text(pending, &crate::codec::ESCAPE.to_string());
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
data.push_str(text);
|
|
262
|
+
if ends_with_unpaired_escape(&data) {
|
|
263
|
+
data.pop();
|
|
264
|
+
self.pending_escape.set(Some(target));
|
|
265
|
+
}
|
|
266
|
+
if !data.is_empty() {
|
|
267
|
+
self.send_text(target, &data);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
fn send_text(&self, target: TextTarget, data: &str) {
|
|
271
|
+
match target.sibling {
|
|
272
|
+
None => self.call("insertText", json!([target.parent, data])),
|
|
273
|
+
Some(sibling) => self.call("insertTextBefore", json!([target.parent, data, sibling])),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
fn flush_escape(&self) {
|
|
277
|
+
if let Some(pending) = self.pending_escape.take() {
|
|
278
|
+
self.send_text(pending, &crate::codec::ESCAPE.to_string());
|
|
279
|
+
}
|
|
280
|
+
}
|
|
218
281
|
fn attrs(attrs: Vec<Attribute>) -> Value {
|
|
219
282
|
Value::Array(
|
|
220
283
|
attrs
|
|
@@ -463,7 +526,11 @@ impl TreeSink for HookSink {
|
|
|
463
526
|
}
|
|
464
527
|
}
|
|
465
528
|
NodeOrText::AppendText(text) => {
|
|
466
|
-
|
|
529
|
+
let target = TextTarget {
|
|
530
|
+
parent: *parent,
|
|
531
|
+
sibling: None,
|
|
532
|
+
};
|
|
533
|
+
self.insert_text(target, text.as_ref());
|
|
467
534
|
self.mark_text(*parent, None, text.as_ref());
|
|
468
535
|
}
|
|
469
536
|
}
|
|
@@ -530,7 +597,11 @@ impl TreeSink for HookSink {
|
|
|
530
597
|
self.call("insertBefore", json!([parent, node, sibling]));
|
|
531
598
|
}
|
|
532
599
|
NodeOrText::AppendText(text) => {
|
|
533
|
-
|
|
600
|
+
let target = TextTarget {
|
|
601
|
+
parent,
|
|
602
|
+
sibling: Some(*sibling),
|
|
603
|
+
};
|
|
604
|
+
self.insert_text(target, text.as_ref());
|
|
534
605
|
self.mark_text(parent, Some(*sibling), text.as_ref());
|
|
535
606
|
}
|
|
536
607
|
}
|
|
@@ -589,6 +660,7 @@ fn parser(
|
|
|
589
660
|
context: None,
|
|
590
661
|
diagnostics: RefCell::new(Vec::new()),
|
|
591
662
|
locations: None,
|
|
663
|
+
pending_escape: Cell::new(None),
|
|
592
664
|
};
|
|
593
665
|
sink.call(
|
|
594
666
|
if is_document {
|
|
@@ -659,6 +731,7 @@ pub(crate) fn parse(
|
|
|
659
731
|
diagnostics: RefCell::new(Vec::new()),
|
|
660
732
|
locations: (options.get("sourceCodeLocationInfo") == Some(&Value::Bool(true)))
|
|
661
733
|
.then(|| crate::tracker::Locations::new(context.clone())),
|
|
734
|
+
pending_escape: Cell::new(None),
|
|
662
735
|
};
|
|
663
736
|
sink.call(
|
|
664
737
|
if is_document {
|
|
@@ -706,6 +779,7 @@ where
|
|
|
706
779
|
diagnostics: RefCell::new(Vec::new()),
|
|
707
780
|
locations: (options.get("sourceCodeLocationInfo") == Some(&Value::Bool(true)))
|
|
708
781
|
.then(|| crate::tracker::Locations::new(context.clone())),
|
|
782
|
+
pending_escape: Cell::new(None),
|
|
709
783
|
};
|
|
710
784
|
sink.call(
|
|
711
785
|
if is_document {
|
package/native/src/lib.rs
CHANGED
|
@@ -23,6 +23,7 @@ mod gc;
|
|
|
23
23
|
mod hook_sink;
|
|
24
24
|
mod hook_stream;
|
|
25
25
|
mod location;
|
|
26
|
+
mod markup_tokenizer;
|
|
26
27
|
mod runtime;
|
|
27
28
|
mod selector;
|
|
28
29
|
mod sink;
|
|
@@ -35,8 +36,90 @@ pub use runtime::{configure_worker_pool, worker_pool_info, WorkerPoolInfo};
|
|
|
35
36
|
pub use stream::NativeStream;
|
|
36
37
|
|
|
37
38
|
const HTML_NS: &str = "http://www.w3.org/1999/xhtml";
|
|
39
|
+
const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
|
|
40
|
+
const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/";
|
|
41
|
+
const XLINK_NS: &str = "http://www.w3.org/1999/xlink";
|
|
42
|
+
|
|
43
|
+
/// Writes an attribute's serialized name. Like parse5, the namespace rather
|
|
44
|
+
/// than the stored prefix selects the qualifier: HTML5 gives foreign `xmlns`
|
|
45
|
+
/// attributes an empty prefix, which must not become `:xmlns`. Other namespaces
|
|
46
|
+
/// keep a non-empty prefix; an empty or missing one writes the bare name.
|
|
47
|
+
pub(crate) fn push_attribute_name(
|
|
48
|
+
output: &mut String,
|
|
49
|
+
local: &str,
|
|
50
|
+
namespace: Option<&str>,
|
|
51
|
+
prefix: Option<&str>,
|
|
52
|
+
) {
|
|
53
|
+
match namespace.filter(|namespace| !namespace.is_empty()) {
|
|
54
|
+
None => {}
|
|
55
|
+
Some(XML_NS) => output.push_str("xml:"),
|
|
56
|
+
Some(XMLNS_NS) => {
|
|
57
|
+
if local != "xmlns" {
|
|
58
|
+
output.push_str("xmlns:");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
Some(XLINK_NS) => output.push_str("xlink:"),
|
|
62
|
+
Some(_) => {
|
|
63
|
+
if let Some(prefix) = prefix.filter(|prefix| !prefix.is_empty()) {
|
|
64
|
+
output.push_str(prefix);
|
|
65
|
+
output.push(':');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
output.push_str(local);
|
|
70
|
+
}
|
|
38
71
|
|
|
39
|
-
|
|
72
|
+
/// Orders attributes the way a JavaScript object enumerates its keys: integer
|
|
73
|
+
/// keys first, ascending, then insertion order. Cheerio serializes attributes
|
|
74
|
+
/// from such an object.
|
|
75
|
+
pub(crate) fn js_property_order<'a, T, I>(entries: I) -> JsPropertyOrder<I>
|
|
76
|
+
where
|
|
77
|
+
I: Iterator<Item = (&'a str, T)> + Clone,
|
|
78
|
+
{
|
|
79
|
+
if !entries.clone().any(|(name, _)| array_index(name).is_some()) {
|
|
80
|
+
return JsPropertyOrder::Unchanged(entries);
|
|
81
|
+
}
|
|
82
|
+
let mut keyed: Vec<_> = entries
|
|
83
|
+
.map(|entry| {
|
|
84
|
+
(
|
|
85
|
+
array_index(entry.0).map_or((1, 0), |index| (0, index)),
|
|
86
|
+
entry,
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
.collect();
|
|
90
|
+
keyed.sort_by_key(|(key, _)| *key);
|
|
91
|
+
let sorted: Vec<_> = keyed.into_iter().map(|(_, entry)| entry).collect();
|
|
92
|
+
JsPropertyOrder::Sorted(sorted.into_iter())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pub(crate) enum JsPropertyOrder<I: Iterator> {
|
|
96
|
+
Unchanged(I),
|
|
97
|
+
Sorted(std::vec::IntoIter<I::Item>),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl<I: Iterator> Iterator for JsPropertyOrder<I> {
|
|
101
|
+
type Item = I::Item;
|
|
102
|
+
fn next(&mut self) -> Option<Self::Item> {
|
|
103
|
+
match self {
|
|
104
|
+
Self::Unchanged(entries) => entries.next(),
|
|
105
|
+
Self::Sorted(entries) => entries.next(),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fn array_index(name: &str) -> Option<u32> {
|
|
111
|
+
let is_canonical = !name.is_empty()
|
|
112
|
+
&& name.len() <= 10
|
|
113
|
+
&& name.bytes().all(|byte| byte.is_ascii_digit())
|
|
114
|
+
&& (name == "0" || !name.starts_with('0'));
|
|
115
|
+
is_canonical
|
|
116
|
+
.then(|| name.parse::<u64>().ok())
|
|
117
|
+
.flatten()
|
|
118
|
+
.filter(|value| *value < u64::from(u32::MAX))
|
|
119
|
+
.map(|value| value as u32)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#[napi(custom_finalize)]
|
|
40
123
|
pub struct NativeDocument {
|
|
41
124
|
html: RefCell<Option<Html>>,
|
|
42
125
|
// Stable public IDs are preorder positions, rather than parser allocation
|
|
@@ -51,10 +134,43 @@ pub struct NativeDocument {
|
|
|
51
134
|
managed: Cell<bool>,
|
|
52
135
|
diagnostics: RefCell<Vec<Value>>,
|
|
53
136
|
locations: RefCell<HashMap<u32, Value>>,
|
|
137
|
+
// V8 cannot see native allocations. Reporting them lets its GC collect
|
|
138
|
+
// unreachable documents under native memory pressure.
|
|
139
|
+
content_bytes: Cell<usize>,
|
|
140
|
+
reported_heap_bytes: Cell<i64>,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
impl ObjectFinalize for NativeDocument {
|
|
144
|
+
fn finalize(self, mut env: Env) -> Result<()> {
|
|
145
|
+
let reported = self.reported_heap_bytes.get();
|
|
146
|
+
if reported != 0 {
|
|
147
|
+
// Accounting is advisory; a failure must not throw from a finalizer.
|
|
148
|
+
let _ = env.adjust_external_memory(-reported);
|
|
149
|
+
}
|
|
150
|
+
Ok(())
|
|
151
|
+
}
|
|
54
152
|
}
|
|
55
153
|
|
|
154
|
+
fn html_node_content_bytes(node: &Node) -> usize {
|
|
155
|
+
match node {
|
|
156
|
+
Node::Text(text) => text.len(),
|
|
157
|
+
Node::Comment(comment) => comment.len(),
|
|
158
|
+
Node::Element(element) => element.attrs.iter().map(|(_, value)| value.len()).sum(),
|
|
159
|
+
_ => 0,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Approximate retained bytes per node in each representation, excluding the
|
|
164
|
+
/// text and attribute content counted separately. V8 treats the total only as
|
|
165
|
+
/// allocation pressure, so these err on the high side.
|
|
166
|
+
const IMMUTABLE_NODE_BYTES: usize = 256;
|
|
167
|
+
const ARENA_NODE_BYTES: usize = 512;
|
|
168
|
+
/// Smaller changes to an already reported size are skipped to keep operations cheap.
|
|
169
|
+
const HEAP_REPORT_STEP: i64 = 64 * 1024;
|
|
170
|
+
|
|
56
171
|
impl NativeDocument {
|
|
57
172
|
pub(crate) fn from_arena(arena: arena::DomArena, options: Value) -> Self {
|
|
173
|
+
let content_bytes = arena.nodes.iter().map(arena::DomNode::content_bytes).sum();
|
|
58
174
|
Self {
|
|
59
175
|
html: RefCell::new(None),
|
|
60
176
|
ids: RefCell::new(Vec::new()),
|
|
@@ -67,6 +183,8 @@ impl NativeDocument {
|
|
|
67
183
|
managed: Cell::new(false),
|
|
68
184
|
diagnostics: RefCell::new(Vec::new()),
|
|
69
185
|
locations: RefCell::new(HashMap::new()),
|
|
186
|
+
content_bytes: Cell::new(content_bytes),
|
|
187
|
+
reported_heap_bytes: Cell::new(0),
|
|
70
188
|
}
|
|
71
189
|
}
|
|
72
190
|
pub(crate) fn set_diagnostic_records(&self, records: Vec<Value>) {
|
|
@@ -82,6 +200,39 @@ impl NativeDocument {
|
|
|
82
200
|
Self::parse_options(source, is_document, serde_json::json!({}), None)
|
|
83
201
|
}
|
|
84
202
|
|
|
203
|
+
fn estimated_heap_bytes(&self) -> i64 {
|
|
204
|
+
let live_arena_nodes = self.arena.borrow().as_ref().map_or(0, |arena| {
|
|
205
|
+
arena.nodes.len().saturating_sub(arena.free.len())
|
|
206
|
+
});
|
|
207
|
+
let immutable_nodes = self.ids.borrow().len();
|
|
208
|
+
let retained_source = self.source.borrow().as_ref().map_or(0, String::capacity);
|
|
209
|
+
let content = if live_arena_nodes + immutable_nodes == 0 {
|
|
210
|
+
0
|
|
211
|
+
} else {
|
|
212
|
+
self.content_bytes.get()
|
|
213
|
+
};
|
|
214
|
+
let bytes = content
|
|
215
|
+
+ retained_source
|
|
216
|
+
+ immutable_nodes * IMMUTABLE_NODE_BYTES
|
|
217
|
+
+ live_arena_nodes * ARENA_NODE_BYTES;
|
|
218
|
+
i64::try_from(bytes).unwrap_or(i64::MAX)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fn report_heap_bytes(&self, mut env: Env) {
|
|
222
|
+
let reported = self.reported_heap_bytes.get();
|
|
223
|
+
let estimated = self.estimated_heap_bytes();
|
|
224
|
+
let change = estimated - reported;
|
|
225
|
+
let is_small_update = reported != 0 && estimated != 0 && change.abs() < HEAP_REPORT_STEP;
|
|
226
|
+
if change != 0 && !is_small_update && env.adjust_external_memory(change).is_ok() {
|
|
227
|
+
self.reported_heap_bytes.set(estimated);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn reported<T>(&self, env: Env, value: T) -> T {
|
|
232
|
+
self.report_heap_bytes(env);
|
|
233
|
+
value
|
|
234
|
+
}
|
|
235
|
+
|
|
85
236
|
pub(crate) fn parse_options(
|
|
86
237
|
source: &str,
|
|
87
238
|
is_document: bool,
|
|
@@ -262,6 +413,7 @@ impl NativeDocument {
|
|
|
262
413
|
&& value != &&serde_json::json!("")
|
|
263
414
|
})
|
|
264
415
|
.and(source);
|
|
416
|
+
let content_bytes = html.tree.values().map(html_node_content_bytes).sum();
|
|
265
417
|
Self {
|
|
266
418
|
html: RefCell::new(Some(html)),
|
|
267
419
|
ids: RefCell::new(ids),
|
|
@@ -274,6 +426,8 @@ impl NativeDocument {
|
|
|
274
426
|
managed: Cell::new(false),
|
|
275
427
|
diagnostics: RefCell::new(diagnostics),
|
|
276
428
|
locations: RefCell::new(locations),
|
|
429
|
+
content_bytes: Cell::new(content_bytes),
|
|
430
|
+
reported_heap_bytes: Cell::new(0),
|
|
277
431
|
}
|
|
278
432
|
}
|
|
279
433
|
|
|
@@ -583,7 +737,16 @@ impl NativeDocument {
|
|
|
583
737
|
}
|
|
584
738
|
}
|
|
585
739
|
}
|
|
586
|
-
#[napi]
|
|
740
|
+
#[napi(js_name = "collectGarbage")]
|
|
741
|
+
pub fn collect_garbage_and_report(&self, env: Env) -> MemoryStats {
|
|
742
|
+
let stats = self.collect_garbage();
|
|
743
|
+
self.report_heap_bytes(env);
|
|
744
|
+
MemoryStats {
|
|
745
|
+
external_bytes: self.reported_heap_bytes.get(),
|
|
746
|
+
..stats
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
587
750
|
pub fn collect_garbage(&self) -> MemoryStats {
|
|
588
751
|
self.needs_collect.set(false);
|
|
589
752
|
if self.arena.borrow().is_none() && self.managed.get() && self.retained.borrow().is_empty()
|
|
@@ -599,6 +762,7 @@ impl NativeDocument {
|
|
|
599
762
|
retained: 0,
|
|
600
763
|
reclaimed,
|
|
601
764
|
immutable_nodes: 0,
|
|
765
|
+
external_bytes: self.reported_heap_bytes.get(),
|
|
602
766
|
};
|
|
603
767
|
}
|
|
604
768
|
if let Some(arena) = self.arena.borrow_mut().as_mut() {
|
|
@@ -610,6 +774,7 @@ impl NativeDocument {
|
|
|
610
774
|
retained: stats.retained,
|
|
611
775
|
reclaimed: stats.reclaimed,
|
|
612
776
|
immutable_nodes: 0,
|
|
777
|
+
external_bytes: self.reported_heap_bytes.get(),
|
|
613
778
|
}
|
|
614
779
|
} else {
|
|
615
780
|
let count = self.ids.borrow().len() as u32;
|
|
@@ -619,6 +784,7 @@ impl NativeDocument {
|
|
|
619
784
|
retained: self.retained.borrow().len() as u32,
|
|
620
785
|
reclaimed: 0,
|
|
621
786
|
immutable_nodes: count,
|
|
787
|
+
external_bytes: self.reported_heap_bytes.get(),
|
|
622
788
|
}
|
|
623
789
|
}
|
|
624
790
|
}
|
|
@@ -629,19 +795,30 @@ impl NativeDocument {
|
|
|
629
795
|
}
|
|
630
796
|
|
|
631
797
|
#[napi]
|
|
632
|
-
pub fn root_id(&self) -> u32 {
|
|
798
|
+
pub fn root_id(&self, env: Env) -> u32 {
|
|
799
|
+
self.report_heap_bytes(env);
|
|
633
800
|
0
|
|
634
801
|
}
|
|
635
802
|
|
|
636
|
-
#[napi]
|
|
803
|
+
#[napi(js_name = "select")]
|
|
804
|
+
pub fn select_and_report(
|
|
805
|
+
&self,
|
|
806
|
+
env: Env,
|
|
807
|
+
selector: String,
|
|
808
|
+
contexts: Option<Vec<u32>>,
|
|
809
|
+
) -> Result<Vec<u32>> {
|
|
810
|
+
self.reported(env, self.select(selector, contexts))
|
|
811
|
+
}
|
|
812
|
+
|
|
637
813
|
pub fn select(&self, selector: String, contexts: Option<Vec<u32>>) -> Result<Vec<u32>> {
|
|
638
814
|
self.select_ids(&selector, contexts.as_deref())
|
|
639
815
|
}
|
|
640
816
|
|
|
641
817
|
#[napi]
|
|
642
|
-
pub fn matches(&self, selector: String, ids: Vec<u32>) -> Result<Vec<u32>> {
|
|
818
|
+
pub fn matches(&self, env: Env, selector: String, ids: Vec<u32>) -> Result<Vec<u32>> {
|
|
643
819
|
self.ensure_arena()?;
|
|
644
|
-
selector::filter(self.arena.borrow().as_ref().unwrap(), &selector, &ids)
|
|
820
|
+
let matched = selector::filter(self.arena.borrow().as_ref().unwrap(), &selector, &ids);
|
|
821
|
+
self.reported(env, matched)
|
|
645
822
|
}
|
|
646
823
|
|
|
647
824
|
#[napi]
|
|
@@ -671,7 +848,11 @@ impl NativeDocument {
|
|
|
671
848
|
self.text_ids(&ids)
|
|
672
849
|
}
|
|
673
850
|
|
|
674
|
-
#[napi]
|
|
851
|
+
#[napi(js_name = "innerHtml")]
|
|
852
|
+
pub fn inner_html_and_report(&self, env: Env, id: u32) -> String {
|
|
853
|
+
self.reported(env, self.inner_html(id))
|
|
854
|
+
}
|
|
855
|
+
|
|
675
856
|
pub fn inner_html(&self, id: u32) -> String {
|
|
676
857
|
if (self.tag_name(id).as_deref() == Some("template") || self.arena.borrow().is_some())
|
|
677
858
|
&& self.ensure_arena().is_ok()
|
|
@@ -704,7 +885,11 @@ impl NativeDocument {
|
|
|
704
885
|
.flatten()
|
|
705
886
|
}
|
|
706
887
|
|
|
707
|
-
#[napi]
|
|
888
|
+
#[napi(js_name = "exportTree")]
|
|
889
|
+
pub fn export_tree_and_report(&self, env: Env) -> Result<String> {
|
|
890
|
+
self.reported(env, self.export_tree())
|
|
891
|
+
}
|
|
892
|
+
|
|
708
893
|
pub fn export_tree(&self) -> Result<String> {
|
|
709
894
|
if self.source.borrow().is_some() {
|
|
710
895
|
self.ensure_arena()?;
|
|
@@ -718,7 +903,11 @@ impl NativeDocument {
|
|
|
718
903
|
serde_json::to_string(&records).map_err(json_error)
|
|
719
904
|
}
|
|
720
905
|
|
|
721
|
-
#[napi]
|
|
906
|
+
#[napi(js_name = "node")]
|
|
907
|
+
pub fn node_and_report(&self, env: Env, id: u32) -> Result<String> {
|
|
908
|
+
self.reported(env, self.node(id))
|
|
909
|
+
}
|
|
910
|
+
|
|
722
911
|
pub fn node(&self, id: u32) -> Result<String> {
|
|
723
912
|
if self.source.borrow().is_some() {
|
|
724
913
|
self.ensure_arena()?;
|
|
@@ -729,13 +918,26 @@ impl NativeDocument {
|
|
|
729
918
|
serde_json::to_string(&self.arena.borrow().as_ref().unwrap().node(id)).map_err(json_error)
|
|
730
919
|
}
|
|
731
920
|
|
|
732
|
-
#[napi]
|
|
921
|
+
#[napi(js_name = "create")]
|
|
922
|
+
pub fn create_and_report(
|
|
923
|
+
&self,
|
|
924
|
+
env: Env,
|
|
925
|
+
html: String,
|
|
926
|
+
context_id: Option<u32>,
|
|
927
|
+
options_json: Option<String>,
|
|
928
|
+
) -> Result<Vec<u32>> {
|
|
929
|
+
let created = self.create(html, context_id, options_json);
|
|
930
|
+
self.reported(env, created)
|
|
931
|
+
}
|
|
932
|
+
|
|
733
933
|
pub fn create(
|
|
734
934
|
&self,
|
|
735
935
|
html: String,
|
|
736
936
|
context_id: Option<u32>,
|
|
737
937
|
_options_json: Option<String>,
|
|
738
938
|
) -> Result<Vec<u32>> {
|
|
939
|
+
self.content_bytes
|
|
940
|
+
.set(self.content_bytes.get().saturating_add(html.len()));
|
|
739
941
|
self.ensure_arena()?;
|
|
740
942
|
self.arena
|
|
741
943
|
.borrow_mut()
|
|
@@ -746,7 +948,19 @@ impl NativeDocument {
|
|
|
746
948
|
|
|
747
949
|
/// Move a branch between native documents. JavaScript only redirects its
|
|
748
950
|
/// existing stable handles using the returned complete descendant mapping.
|
|
749
|
-
#[napi]
|
|
951
|
+
#[napi(js_name = "adopt")]
|
|
952
|
+
pub fn adopt_and_report(
|
|
953
|
+
&self,
|
|
954
|
+
env: Env,
|
|
955
|
+
source: &NativeDocument,
|
|
956
|
+
ids: Vec<u32>,
|
|
957
|
+
detach: Option<bool>,
|
|
958
|
+
) -> Result<String> {
|
|
959
|
+
let adopted = self.adopt(source, ids, detach);
|
|
960
|
+
source.report_heap_bytes(env);
|
|
961
|
+
self.reported(env, adopted)
|
|
962
|
+
}
|
|
963
|
+
|
|
750
964
|
pub fn adopt(
|
|
751
965
|
&self,
|
|
752
966
|
source: &NativeDocument,
|
|
@@ -840,7 +1054,11 @@ impl NativeDocument {
|
|
|
840
1054
|
.map_err(json_error)
|
|
841
1055
|
}
|
|
842
1056
|
|
|
843
|
-
#[napi]
|
|
1057
|
+
#[napi(js_name = "op")]
|
|
1058
|
+
pub fn op_and_report(&self, env: Env, command: String, args_json: String) -> Result<String> {
|
|
1059
|
+
self.reported(env, self.op(command, args_json))
|
|
1060
|
+
}
|
|
1061
|
+
|
|
844
1062
|
pub fn op(&self, command: String, args_json: String) -> Result<String> {
|
|
845
1063
|
self.maybe_collect();
|
|
846
1064
|
let args: Value = serde_json::from_str(&args_json).map_err(json_error)?;
|
|
@@ -964,6 +1182,8 @@ pub struct MemoryStats {
|
|
|
964
1182
|
pub retained: u32,
|
|
965
1183
|
pub reclaimed: u32,
|
|
966
1184
|
pub immutable_nodes: u32,
|
|
1185
|
+
/// Native bytes currently reported to V8 for this document.
|
|
1186
|
+
pub external_bytes: i64,
|
|
967
1187
|
}
|
|
968
1188
|
|
|
969
1189
|
/// The Cheerio DOM adapter exposes foreign attributes by local name, whereas
|
|
@@ -1154,13 +1374,20 @@ fn serialize_node(node: NodeRef<'_, Node>, output: &mut String) {
|
|
|
1154
1374
|
Node::Element(element) => {
|
|
1155
1375
|
output.push('<');
|
|
1156
1376
|
output.push_str(element.name());
|
|
1157
|
-
|
|
1377
|
+
let attributes = js_property_order(
|
|
1378
|
+
element
|
|
1379
|
+
.attrs
|
|
1380
|
+
.iter()
|
|
1381
|
+
.map(|(name, value)| (name.local.as_ref(), (name, value))),
|
|
1382
|
+
);
|
|
1383
|
+
for (_, (name, value)) in attributes {
|
|
1158
1384
|
output.push(' ');
|
|
1159
|
-
|
|
1160
|
-
output
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1385
|
+
push_attribute_name(
|
|
1386
|
+
output,
|
|
1387
|
+
&name.local,
|
|
1388
|
+
Some(name.ns.as_ref()),
|
|
1389
|
+
name.prefix.as_deref(),
|
|
1390
|
+
);
|
|
1164
1391
|
output.push_str("=\"");
|
|
1165
1392
|
escape_html(value, true, output);
|
|
1166
1393
|
output.push('"');
|