Skip to content

Commit 2e17feb

Browse files
committed
refactor: Rework unevaluatedProperties
Signed-off-by: Dmitry Dygalo <[email protected]>
1 parent 52b939e commit 2e17feb

File tree

7 files changed

+706
-914
lines changed

7 files changed

+706
-914
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
- Recursive and regular `$ref` compilation deduplicates validator nodes, which decreases the memory usage and improves performance.
2222
- Validator compilation restores the regex cache for faster builds on regex-heavy schemas and precomputes absolute schema locations, trading a bit of compile time for faster `apply` on location-heavy workloads.
2323
- Large schema compilation is significantly faster. [#755](https://github.com/Stranger6667/jsonschema/issues/755)
24+
- `unevaluatedProperties` validation is 25-35% faster through optimized property marking and early-exit paths.
25+
- `unevaluatedProperties` memory usage drastically reduced by eliminating redundant registry clones during compilation.
2426

2527
### Removed
2628

crates/jsonschema-py/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
- Recursive and regular `$ref` compilation deduplicates validator nodes, which decreases the memory usage and improves performance.
1616
- Validator compilation restores the regex cache for faster builds on regex-heavy schemas and precomputes absolute schema locations, trading a bit of compile time for faster `apply` on location-heavy workloads.
1717
- Large schema compilation is significantly faster. [#755](https://github.com/Stranger6667/jsonschema/issues/755)
18+
- `unevaluatedProperties` validation is 25-35% faster through optimized property marking and early-exit paths.
19+
- `unevaluatedProperties` memory usage drastically reduced by eliminating redundant registry clones during compilation.
1820

1921
## [0.32.1] - 2025-08-24
2022

crates/jsonschema/benches/unevaluated_properties.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,6 @@ mod bench {
140140
}
141141

142142
fn run_benchmarks(c: &mut Criterion) {
143-
// Only benchmark large schema with invalid instance (most interesting case)
144143
let large = large_schema();
145144
bench_build(c, "unevaluated_properties", &large);
146145

crates/jsonschema/src/compiler.rs

Lines changed: 91 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::{
66
self,
77
custom::{CustomKeyword, KeywordFactory},
88
format::Format,
9+
unevaluated_properties::PendingPropertyValidators,
910
BoxedValidator, BuiltinKeyword, Keyword,
1011
},
1112
node::{PendingSchemaNode, SchemaNode},
@@ -28,11 +29,24 @@ pub(crate) const DEFAULT_BASE_URI: &str = "json-schema:///";
2829

2930
#[derive(Hash, PartialEq, Eq, Clone, Debug)]
3031
pub(crate) struct LocationCacheKey {
31-
base_uri: Arc<Uri<String>>,
32+
pub(crate) base_uri: Arc<Uri<String>>,
3233
location: Arc<str>,
3334
dynamic_scope: List<Uri<String>>,
3435
}
3536

37+
#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
38+
struct PropertyValidatorsPendingKey {
39+
schema_ptr: usize,
40+
}
41+
42+
impl PropertyValidatorsPendingKey {
43+
fn new(schema: &Map<String, Value>) -> Self {
44+
Self {
45+
schema_ptr: schema as *const _ as usize,
46+
}
47+
}
48+
}
49+
3650
#[derive(Hash, PartialEq, Eq, Clone, Debug)]
3751
pub(crate) struct AliasCacheKey {
3852
uri: Arc<Uri<String>>,
@@ -47,6 +61,9 @@ struct SharedContextState {
4761
alias_nodes: Rc<RefCell<AHashMap<AliasCacheKey, SchemaNode>>>,
4862
pending_nodes: Rc<RefCell<AHashMap<LocationCacheKey, PendingSchemaNode>>>,
4963
alias_placeholders: Rc<RefCell<AHashMap<Arc<Uri<String>>, PendingSchemaNode>>>,
64+
pending_property_validators: Rc<RefCell<AHashMap<LocationCacheKey, PendingPropertyValidators>>>,
65+
pending_property_validators_by_schema:
66+
Rc<RefCell<AHashMap<PropertyValidatorsPendingKey, PendingPropertyValidators>>>,
5067
pattern_cache: Rc<RefCell<AHashMap<Arc<str>, PatternCacheEntry>>>,
5168
uri_buffer: Rc<RefCell<String>>,
5269
}
@@ -66,19 +83,12 @@ impl SharedContextState {
6683
alias_nodes: Rc::new(RefCell::new(AHashMap::new())),
6784
pending_nodes: Rc::new(RefCell::new(AHashMap::new())),
6885
alias_placeholders: Rc::new(RefCell::new(AHashMap::new())),
86+
pending_property_validators: Rc::new(RefCell::new(AHashMap::new())),
87+
pending_property_validators_by_schema: Rc::new(RefCell::new(AHashMap::new())),
6988
pattern_cache: Rc::new(RefCell::new(AHashMap::new())),
7089
uri_buffer: Rc::new(RefCell::new(String::new())),
7190
}
7291
}
73-
74-
fn clear(&self) {
75-
self.location_nodes.borrow_mut().clear();
76-
self.alias_nodes.borrow_mut().clear();
77-
self.pending_nodes.borrow_mut().clear();
78-
self.alias_placeholders.borrow_mut().clear();
79-
self.pattern_cache.borrow_mut().clear();
80-
self.uri_buffer.borrow_mut().clear();
81-
}
8292
}
8393

8494
/// Per-location view used while compiling schemas into validators.
@@ -159,11 +169,7 @@ impl<'a> Context<'a> {
159169
self.resolver.lookup(reference)
160170
}
161171

162-
pub(crate) fn scopes(&self) -> List<Uri<String>> {
163-
self.resolver.dynamic_scope()
164-
}
165-
166-
fn location_cache_key(&self) -> LocationCacheKey {
172+
pub(crate) fn location_cache_key(&self) -> LocationCacheKey {
167173
LocationCacheKey {
168174
base_uri: self.resolver.base_uri(),
169175
location: self.location.as_arc(),
@@ -213,9 +219,6 @@ impl<'a> Context<'a> {
213219
Ok(translated)
214220
}
215221

216-
pub(crate) fn clear_shared_state(&self) {
217-
self.shared.clear();
218-
}
219222
fn is_known_keyword(&self, keyword: &str) -> bool {
220223
self.draft.is_known_keyword(keyword)
221224
}
@@ -348,6 +351,74 @@ impl<'a> Context<'a> {
348351
self.shared.pending_nodes.borrow_mut().remove(key);
349352
}
350353

354+
pub(crate) fn get_pending_property_validators(
355+
&self,
356+
key: &LocationCacheKey,
357+
) -> Option<PendingPropertyValidators> {
358+
self.shared
359+
.pending_property_validators
360+
.borrow()
361+
.get(key)
362+
.cloned()
363+
}
364+
365+
pub(crate) fn cache_pending_property_validators(
366+
&self,
367+
key: LocationCacheKey,
368+
pending: PendingPropertyValidators,
369+
) {
370+
self.shared
371+
.pending_property_validators
372+
.borrow_mut()
373+
.insert(key, pending);
374+
}
375+
376+
pub(crate) fn remove_pending_property_validators(&self, key: &LocationCacheKey) {
377+
self.shared
378+
.pending_property_validators
379+
.borrow_mut()
380+
.remove(key);
381+
}
382+
383+
fn property_schema_key(schema: &Map<String, Value>) -> PropertyValidatorsPendingKey {
384+
PropertyValidatorsPendingKey::new(schema)
385+
}
386+
387+
pub(crate) fn get_pending_property_validators_for_schema(
388+
&self,
389+
schema: &Map<String, Value>,
390+
) -> Option<PendingPropertyValidators> {
391+
let key = Self::property_schema_key(schema);
392+
self.shared
393+
.pending_property_validators_by_schema
394+
.borrow()
395+
.get(&key)
396+
.cloned()
397+
}
398+
399+
pub(crate) fn cache_pending_property_validators_for_schema(
400+
&self,
401+
schema: &Map<String, Value>,
402+
pending: PendingPropertyValidators,
403+
) {
404+
let key = Self::property_schema_key(schema);
405+
self.shared
406+
.pending_property_validators_by_schema
407+
.borrow_mut()
408+
.insert(key, pending);
409+
}
410+
411+
pub(crate) fn remove_pending_property_validators_for_schema(
412+
&self,
413+
schema: &Map<String, Value>,
414+
) {
415+
let key = Self::property_schema_key(schema);
416+
self.shared
417+
.pending_property_validators_by_schema
418+
.borrow_mut()
419+
.remove(&key);
420+
}
421+
351422
pub(crate) fn cached_alias_placeholder(
352423
&self,
353424
alias: &Arc<Uri<String>>,
@@ -473,10 +544,6 @@ impl<'a> Context<'a> {
473544
&self.location
474545
}
475546

476-
pub(crate) fn vocabularies(&self) -> &VocabularySet {
477-
&self.vocabularies
478-
}
479-
480547
pub(crate) fn has_vocabulary(&self, vocabulary: &Vocabulary) -> bool {
481548
if self.draft() < Draft::Draft201909 || vocabulary == &Vocabulary::Core {
482549
true
@@ -535,16 +602,7 @@ pub(crate) fn build_validator(
535602
}
536603

537604
// Finally, compile the validator
538-
let root = match compile(&ctx, resource_ref).map_err(ValidationError::to_owned) {
539-
Ok(node) => {
540-
ctx.clear_shared_state();
541-
node
542-
}
543-
Err(err) => {
544-
ctx.clear_shared_state();
545-
return Err(err);
546-
}
547-
};
605+
let root = compile(&ctx, resource_ref).map_err(ValidationError::to_owned)?;
548606
let draft = config.draft();
549607
Ok(Validator { root, draft })
550608
}
@@ -604,16 +662,7 @@ pub(crate) async fn build_validator_async(
604662
validate_schema(draft, schema)?;
605663
}
606664

607-
let root = match compile(&ctx, resource_ref).map_err(ValidationError::to_owned) {
608-
Ok(node) => {
609-
ctx.clear_shared_state();
610-
node
611-
}
612-
Err(err) => {
613-
ctx.clear_shared_state();
614-
return Err(err);
615-
}
616-
};
665+
let root = compile(&ctx, resource_ref).map_err(ValidationError::to_owned)?;
617666
let draft = config.draft();
618667
Ok(Validator { root, draft })
619668
}

0 commit comments

Comments
 (0)