Repository navigation
Conversation
|
@Roman3349 really nice to see this improvement. This is saving quite some time on larger schema instances. It seems the gains only apply to where the strict mode is being used, which by itself is already a reason to merge this PR. Although the description seems to hint the benefits apply to draft 3 and 4 as well. My assumption is that this is still the mostly used drafts. I was wondering how certain you are this would benefit Draft 3/4? |
|
Thanks for this. The fix does what it says for the per-draft constraints (strict mode). The Draft 3/4 path (normal mode, Why
// src/JsonSchema/Constraints/CollectionConstraint.php:76-80
foreach ($value as $k => &$v) {
$initErrors = $this->getErrors(); // second handle on $this->errors
$this->checkUndefined($v, $schema->items, $path, $k); // -> addErrors() -> $this->errors[] = ...
BenchmarkAn array of n integers against
Benchmark script<?php
require __DIR__ . '/vendor/autoload.php';
use JsonSchema\Constraints\Constraint;
use JsonSchema\Validator;
foreach (['draft-04' => Constraint::CHECK_MODE_NORMAL, 'draft-07' => Constraint::CHECK_MODE_STRICT] as $d => $mode) {
foreach ([20000, 40000, 80000] as $n) {
$schema = json_decode(json_encode(['$schema' => "http://json-schema.org/$d/schema#", 'type' => 'array', 'items' => ['type' => 'string']]));
$data = range(1, $n);
$v = new Validator();
$t = hrtime(true);
$v->validate($data, $schema, $mode);
printf("%s n=%-6d %6.2fs errs=%d\n", $d, $n, (hrtime(true) - $t) / 1e9, count($v->getErrors()));
}
}Possible direction (not complete)Keeping a count instead of a copy is enough for the plain foreach ($value as $k => &$v) {
- $initErrors = $this->getErrors();
+ $initCount = count($this->errors);
// First check if its defined in "items"
$this->checkUndefined($v, $schema->items, $path, $k);
// Recheck with "additionalItems" if the first test fails
- if (count($initErrors) < count($this->getErrors()) && (isset($schema->additionalItems) && $schema->additionalItems !== false)) {
+ if ($initCount < count($this->errors) && (isset($schema->additionalItems) && $schema->additionalItems !== false)) {This diff is only the proof of concept I used for the benchmark. It is not ready to apply:
Would you like to take this on in this PR, or should we merge this one as is (with the description noting it covers the strict / per-draft path) and handle Draft 3/4 in a follow-up? |
|
Thanks for the thorough analysis and the benchmark, I could reproduce it. One clarification first: the normal-mode path isn't a Draft 3/4 implementation. It's the legacy validator ( That said, you're right that the legacy validator stayed quadratic, and the fix turned out to be small, so I added it to this PR (bf8bba5). Only The new test pins the reported errors (pointer and constraint) for Your benchmark, PHP 8.5, Xdebug and PCOV off:
|
ErrorBag::addErrors() and BaseConstraint::addErrors() merged new errors with array_merge(), which copies every error collected so far. Constraints such as items and properties add the errors of their sub-schemas one by one, so collecting n errors took O(n^2) time. A failing oneOf branch over a long array is enough to hit this: the 11 MB GeoJSON instance of jsonschema-benchmark took 194 s to validate in strict mode. Append the new errors in place instead. For lists of errors, which is what every caller in the library passes, the result is unchanged: the same errors in the same order and the same error mask. An array with string keys is now renumbered instead of keeping its keys. Signed-off-by: Roman Ondráček <mail@romanondracek.cz>
CollectionConstraint::validateItems() copied all errors before each item, which made arrays with many invalid items quadratic. Remember the error count instead and truncate with array_splice().
bf8bba5 to
faffc09
Compare
Description
ErrorBag::addErrors()andBaseConstraint::addErrors()merged new errors witharray_merge(), which copies every error collected so far. Constraints such asitemsandpropertiesadd the errors of their sub-schemas one by one, so collecting n errors took O(n²) time.A failing
oneOfbranch over a long array is enough to hit this. Thegeojsoncorpus of jsonschema-benchmark validatesFeatureCollections against aoneOfof the geometry types, and every branch except the matching one reports an error for each coordinate. Its largest instance (11 MB) took 194 s in strict mode, 49 times as long as an instance a fifth of its size. Profiling showed the extra time inaddErrors().The errors are now appended in place. For lists of errors (
ErrorList), which is what every caller in the library passes, the result is the same as before: the same errors in the same order, keyed0..n-1, and the same error mask. The only difference is for an array with string keys, whicharray_merge()kept (overwriting an earlier error with the same key) and which is now renumbered.Without strict mode, the legacy validator had a second quadratic path:
CollectionConstraint::validateItems()copied the errors collected so far before checking each item, to restore them if the item matchedadditionalItems. It now remembers the number of errors and truncates on restore. The legacy validator is not an implementation of Draft 3/4 (or any other draft, see #991), so this speeds up normal mode as a whole rather than a particular draft.Measured on the
geojsoncorpus in strict mode (one run, PHP 8.4):How to reproduce
The largest instance of the
geojsoncorpus, pinned to the commit of jsonschema-benchmark it was measured with:Then, in a checkout of this repository with
composer installdone, run it onmainand on this branch:Turning off Xdebug and PCOV matters, as they slow the validation down several times over (Xdebug in
developmode about seven times, PCOV about two times) and the difference gets lost.On my machine (PHP 8.5, Xdebug and PCOV off),
maintakes 244.5 s and this branch 66.9 s. Both report the instance as valid.Related Issue
None
Type of Change
Checklist
Additional Notes
The new test pins the errors reported for
itemswithadditionalItems, the only behaviour the second commit touches. No timing assertion is added, as it would be unreliable on CI.