From 9ab9748c0e4d401426f38bd21b6b4176ab6a017e Mon Sep 17 00:00:00 2001 From: xinz Date: Wed, 9 Sep 2026 20:37:05 +0800 Subject: [PATCH] Improve fragment bundle 1. Void repeated descendant metadata indexing in fragment bundling; 2. Improve fallback-anchor accumulation; 3. Skip identity-only external-reference rewrites. --- bench/bundle_scaling.exs | 48 ++++ lib/jsonschex/compiler/fragment/bundle.ex | 70 +++--- test/bundle_performance_regression_test.exs | 257 ++++++++++++++++++++ 3 files changed, 346 insertions(+), 29 deletions(-) create mode 100644 bench/bundle_scaling.exs create mode 100644 test/bundle_performance_regression_test.exs diff --git a/bench/bundle_scaling.exs b/bench/bundle_scaling.exs new file mode 100644 index 0000000..838687e --- /dev/null +++ b/bench/bundle_scaling.exs @@ -0,0 +1,48 @@ +# Run from the repository root: mix run bench/bundle_scaling.exs +# Measures bundling only; schema construction and warmup are outside timing. +IO.puts("Elixir #{System.version()}, OTP #{System.otp_release()}, schedulers #{System.schedulers_online()}") + +measure = fn label, bundle -> + Enum.each(1..5, fn _ -> bundle.() end) + samples = for _ <- 1..21 do + :erlang.garbage_collect() + {microseconds, _} = :timer.tc(fn -> Enum.each(1..10, fn _ -> bundle.() end) end) + microseconds / 10 / 1000 + end + median = samples |> Enum.sort() |> Enum.at(10) + IO.puts("#{label} median_ms=#{Float.round(median, 4)} (21 batches of 10)") +end + +for depth <- [100, 200, 400] do + schema = Enum.reduce(1..depth, %{"type" => "integer"}, fn _, child -> + %{"properties" => %{"child" => child}} + end) + + bundle = fn -> + {:ok, _} = JSONSchex.bundle_fragment(schema, entry: "#") + end + + measure.("depth=#{depth}", bundle) +end + +# Repeated names are intentionally outside schema keywords, exercising fallback discovery. +for count <- [500, 1000, 2000] do + document = %{"schema" => %{}, "x-candidates" => List.duplicate(%{"$anchor" => "same"}, count)} + measure.("anchors=#{count}", fn -> + {:ok, _} = JSONSchex.bundle_fragment(document, entry: "#/schema") + end) +end + +# Identical payloads and loaders except for the effective resource base. +# Unselected deep refs must still be rewritten in the redirected case. +for count <- [500, 1000, 2000], mode <- [:identity, :redirected] do + payload = List.duplicate(%{"nested" => [%{"$ref" => "/api/value.json#value"}]}, count) + document = %{"$ref" => "/api/value.json", "x-payload" => payload} + base = if mode == :identity, do: "/api/value.json", else: "/mirror/value.json" + loader = fn "/api/value.json" -> + {:ok, %{document: %{"type" => "integer", "x-payload" => payload}, base_uri: base}} + end + measure.("aliases=#{mode} payload=#{count}", fn -> + {:ok, _} = JSONSchex.bundle_fragment(document, entry: "#", base_uri: "/api/root.json", loader: loader) + end) +end diff --git a/lib/jsonschex/compiler/fragment/bundle.ex b/lib/jsonschex/compiler/fragment/bundle.ex index d1bc956..44d9f3d 100644 --- a/lib/jsonschex/compiler/fragment/bundle.ex +++ b/lib/jsonschex/compiler/fragment/bundle.ex @@ -61,12 +61,15 @@ defmodule JSONSchex.Compiler.Fragment.Bundle do bundle <- put_context_document(bundle, document, entry_resource, base_uri), {:ok, {external_resources, reachable_anchors, base_aliases}} <- collect_external_resources(document, entry_schema, entry_base, entry_resources, base_uri, opts) do - bundle = rewrite_external_refs(bundle, entry_base, base_aliases) + # Resolution retains every alias; rewriting only needs actual redirects. + # An empty map lets identity-only bundles skip all deep rewrite passes. + rewrite_aliases = Map.reject(base_aliases, fn {base, target} -> base == target end) + bundle = rewrite_external_refs(bundle, entry_base, rewrite_aliases) bundle = bundle - |> put_external_resources(external_resources, base_aliases) - |> put_reachable_anchors(reachable_anchors, base_aliases) + |> put_external_resources(external_resources, rewrite_aliases) + |> put_reachable_anchors(reachable_anchors, rewrite_aliases) {:ok, bundle} end @@ -177,10 +180,6 @@ defmodule JSONSchex.Compiler.Fragment.Bundle do defp index_schema_tree(state, _schema, _current_base), do: state - - - - # Anchors have no URI that can be loaded, so a containing non-schema document # needs a discovery fallback. These candidates never register `$id` resources # or dynamic scope and are consulted only after authoritative schema metadata. @@ -216,15 +215,13 @@ defmodule JSONSchex.Compiler.Fragment.Bundle do defp put_fallback_anchor(state, base_uri, anchor, schema, path) do anchor_uri = URIUtil.resolve(base_uri, "#" <> anchor) candidate_path = Enum.reverse(path) - candidate = {schema, base_uri, candidate_path} + candidate = {schema, base_uri} + # A location with both anchor keywords is one candidate; equal schemas at + # different locations remain distinct. Keys hash paths, never schema trees. candidates = - Map.update(state.fallback_anchors, anchor_uri, [candidate], fn existing -> - if Enum.any?(existing, fn {_schema, _base, path} -> path == candidate_path end) do - existing - else - existing ++ [candidate] - end + Map.update(state.fallback_anchors, anchor_uri, %{candidate_path => candidate}, fn existing -> + Map.put_new(existing, candidate_path, candidate) end) %{state | fallback_anchors: candidates} @@ -254,8 +251,26 @@ defmodule JSONSchex.Compiler.Fragment.Bundle do base end + # Entry and reference targets may come from arbitrary document locations, so + # index their metadata before walking. Structural children are already covered + # by this pass, including their inactive definitions and nested resource ids. defp walk_reachable(schema, current_base, dynamic_scope, state) when is_map(schema) do state = index_schema_tree(state, schema, current_base) + walk_indexed_reachable(schema, current_base, dynamic_scope, state) + end + + defp walk_reachable(list, current_base, dynamic_scope, state) when is_list(list) do + Enum.reduce_while(list, {:ok, state}, fn value, {:ok, acc} -> + case walk_reachable(value, current_base, dynamic_scope, acc) do + {:ok, next} -> {:cont, {:ok, next}} + {:error, error} -> {:halt, {:error, error}} + end + end) + end + + defp walk_reachable(_value, _current_base, _dynamic_scope, state), do: {:ok, state} + + defp walk_indexed_reachable(schema, current_base, dynamic_scope, state) when is_map(schema) do new_base = resolve_base_from_id(schema, current_base) dynamic_scope = put_dynamic_scope(dynamic_scope, resource_key(new_base)) @@ -263,7 +278,7 @@ defmodule JSONSchex.Compiler.Fragment.Bundle do schema |> SchemaTraversal.active_subschemas() |> Enum.reduce_while({:ok, state}, fn subschema, {:ok, acc} -> - case walk_reachable(subschema, new_base, dynamic_scope, acc) do + case walk_indexed_reachable(subschema, new_base, dynamic_scope, acc) do {:ok, next} -> {:cont, {:ok, next}} {:error, error} -> {:halt, {:error, error}} end @@ -271,16 +286,7 @@ defmodule JSONSchex.Compiler.Fragment.Bundle do end end - defp walk_reachable(list, current_base, dynamic_scope, state) when is_list(list) do - Enum.reduce_while(list, {:ok, state}, fn value, {:ok, acc} -> - case walk_reachable(value, current_base, dynamic_scope, acc) do - {:ok, next} -> {:cont, {:ok, next}} - {:error, error} -> {:halt, {:error, error}} - end - end) - end - - defp walk_reachable(_value, _current_base, _dynamic_scope, state), do: {:ok, state} + defp walk_indexed_reachable(_value, _current_base, _dynamic_scope, state), do: {:ok, state} defp put_dynamic_scope({stack, members} = dynamic_scope, base) do if Map.has_key?(members, base) do @@ -451,10 +457,16 @@ defmodule JSONSchex.Compiler.Fragment.Bundle do end defp find_fallback_anchor(anchor_uri, state) do - case Map.get(state.fallback_anchors, anchor_uri, []) do - [{target, inherited_base, _path}] -> {:ok, target, inherited_base} - [] -> nil - candidates -> {:ambiguous_anchor, anchor_uri, length(candidates)} + case Map.get(state.fallback_anchors, anchor_uri, %{}) do + candidates when map_size(candidates) == 0 -> + nil + + candidates when map_size(candidates) == 1 -> + [{target, inherited_base}] = Map.values(candidates) + {:ok, target, inherited_base} + + candidates -> + {:ambiguous_anchor, anchor_uri, map_size(candidates)} end end diff --git a/test/bundle_performance_regression_test.exs b/test/bundle_performance_regression_test.exs new file mode 100644 index 0000000..70609d0 --- /dev/null +++ b/test/bundle_performance_regression_test.exs @@ -0,0 +1,257 @@ +defmodule JSONSchex.Test.BundlePerformanceRegression do + use ExUnit.Case, async: true + + test "structural depth scales approximately linearly in reductions" do + small = chain(100) + large = chain(400) + + # Warm module loading before measuring process-local work, not wall-clock time. + assert {:ok, _} = JSONSchex.bundle_fragment(small, entry: "#") + small_work = bundle_reductions(small) + large_work = bundle_reductions(large) + + assert large_work < small_work * 6, + "quadrupling depth used #{large_work} vs #{small_work} reductions" + end + + test "structural children retain inactive metadata across nested relative ids" do + schema = %{ + "$id" => "/api/root.json", + "properties" => %{ + "child" => %{ + "$id" => "nested/child.json", + "$ref" => "value.json#value", + "$defs" => %{ + "Value" => %{"$id" => "value.json", "$anchor" => "value", "type" => "integer"}, + "Unused" => %{"$ref" => "missing.json"} + } + } + } + } + + assert {:ok, bundle} = JSONSchex.bundle_fragment(schema, entry: "#") + assert {:ok, compiled} = JSONSchex.compile(bundle) + assert :ok == JSONSchex.validate(compiled, %{"child" => 1}) + assert {:error, _} = JSONSchex.validate(compiled, %{"child" => "1"}) + end + + test "new arbitrary pointer targets index descendants before following their refs" do + document = %{ + "schema" => %{"$ref" => "#/x-target"}, + "x-target" => %{ + "$id" => "nested/target.json", + "properties" => %{ + "value" => %{"$ref" => "hidden.json#value"} + }, + "$defs" => %{ + "Hidden" => %{"$id" => "hidden.json", "$anchor" => "value", "$ref" => "leaf.json"}, + "Unused" => %{"$ref" => "missing.json"} + } + } + } + + parent = self() + loader = fn uri -> + send(parent, {:loaded, uri}) + case uri do + "/api/nested/leaf.json" -> {:ok, %{"type" => "integer"}} + _ -> {:error, :unexpected_load} + end + end + + assert {:ok, bundle} = JSONSchex.bundle_fragment(document, + entry: "#/schema", base_uri: "/api/document.json", loader: loader) + assert_received {:loaded, "/api/nested/leaf.json"} + refute_received {:loaded, _} + assert Enum.any?(bundle["$defs"], fn {_key, value} -> + value["$id"] == "/api/nested/leaf.json" and value["type"] == "integer" + end) + end + + test "entry resource metadata supplies a dynamic override to structural descendants" do + parent = self() + document = %{ + "container" => %{ + "$id" => "nested/container.json", + "$defs" => %{ + "Override" => %{"$dynamicAnchor" => "node", "$ref" => "value.json"} + }, + "schema" => %{ + "properties" => %{"child" => %{"$dynamicRef" => "static.json#node"}} + } + } + } + + loader = fn uri -> + send(parent, {:loaded, uri}) + case uri do + "/api/nested/static.json" -> + {:ok, %{"$dynamicAnchor" => "node", "$ref" => "missing.json"}} + "/api/nested/value.json" -> + {:ok, %{"type" => "integer"}} + _ -> + {:error, :unexpected_load} + end + end + + assert {:ok, bundle} = JSONSchex.bundle_fragment(document, + entry: "#/container/schema", base_uri: "/api/document.json", loader: loader) + assert bundle["$id"] == "/api/nested/container.json" + assert_received {:loaded, "/api/nested/static.json"} + assert_received {:loaded, "/api/nested/value.json"} + refute_received {:loaded, _} + assert {:ok, compiled} = JSONSchex.compile(bundle) + assert :ok == JSONSchex.validate(compiled, %{"child" => 1}) + assert {:error, _} = JSONSchex.validate(compiled, %{"child" => "1"}) + end + + test "repeated fallback names scale approximately linearly in reductions" do + small = fallback_document(500) + large = fallback_document(2000) + assert {:ok, _} = JSONSchex.bundle_fragment(small, entry: "#") + small_work = bundle_reductions(small) + large_work = bundle_reductions(large) + assert large_work < small_work * 6, + "quadrupling candidates used #{large_work} vs #{small_work} reductions" + end + + test "fallback ambiguity counts distinct paths, not keywords or equal values" do + candidate = %{"$anchor" => "same", "$dynamicAnchor" => "same", "type" => "integer"} + document = %{ + "schema" => %{"$ref" => "#same"}, + "x-list" => [candidate, candidate], + "x-map" => %{"0" => candidate} + } + assert {:error, error} = JSONSchex.bundle_fragment(document, + entry: "#/schema", base_uri: "/api/root.json") + assert error.context.contrast == "ambiguous_anchor" + assert error.context.input == "/api/root.json#same" + assert error.context.error_detail == {:candidate_count, 3} + end + + test "a single dual-keyword fallback candidate resolves and authoritative anchors win" do + candidate = %{"$anchor" => "same", "$dynamicAnchor" => "same", "type" => "integer"} + document = %{"schema" => %{"$ref" => "#same"}, "x-list" => [candidate]} + assert {:ok, bundle} = JSONSchex.bundle_fragment(document, + entry: "#/schema", base_uri: "/api/root.json") + assert {:ok, compiled} = JSONSchex.compile(bundle) + assert :ok == JSONSchex.validate(compiled, 1) + assert {:error, _} = JSONSchex.validate(compiled, "1") + + schema = %{"$ref" => "#same", "$defs" => %{"Value" => candidate}} + document = %{document | "schema" => schema, "x-list" => [candidate, candidate]} + assert {:ok, _} = JSONSchex.bundle_fragment(document, + entry: "#/schema", base_uri: "/api/root.json") + end + + test "identity aliases avoid deep rewrite work while redirects still traverse" do + payload = List.duplicate(%{"nested" => [%{"$ref" => "/api/value.json#value"}]}, 1000) + document = %{"$ref" => "/api/value.json", "x-payload" => payload} + run = fn base -> + loader = fn "/api/value.json" -> + {:ok, %{document: %{"type" => "integer", "x-payload" => payload}, base_uri: base}} + end + fn -> JSONSchex.bundle_fragment(document, + entry: "#", base_uri: "/api/root.json", loader: loader) end + end + identity = run.("/api/value.json") + redirected = run.("/mirror/value.json") + assert {:ok, _} = identity.() + assert {:ok, _} = redirected.() + identity_work = reductions(identity) + redirected_work = reductions(redirected) + assert identity_work * 2 < redirected_work, + "identity used #{identity_work} vs redirected #{redirected_work} reductions" + end + + test "identity and mixed aliases preserve resolution and deeply rewrite only redirects" do + parent = self() + payload = %{ + "$id" => "/api/nested/scope.json", + "values" => [%{ + "$ref" => "../value.json#value", + "$dynamicRef" => "../value.json#node", + "stable" => %{"$ref" => "../stable.json"}, + "unknown" => %{"$ref" => "../missing.json"} + }] + } + + for mode <- [:identity, :redirected] do + canonical = if mode == :identity, do: "/api/value.json", else: "/mirror/value.json" + external = %{"type" => "integer", "x-payload" => payload} + anchor = %{"$anchor" => "selected", "type" => "integer", "x-payload" => payload} + document = %{ + "schema" => %{"allOf" => [ + %{"$ref" => "/api/value.json"}, + %{"$ref" => canonical}, + %{"$ref" => "/api/stable.json"}, + %{"$ref" => "#selected"} + ]}, + "x-payload" => payload, + "x-anchor" => anchor + } + loader = fn uri -> + send(parent, {:loaded, uri}) + case uri do + "/api/value.json" -> {:ok, %{document: external, base_uri: canonical}} + "/api/stable.json" -> {:ok, %{"type" => "integer"}} + _ -> {:error, :unexpected_load} + end + end + assert {:ok, bundle} = JSONSchex.bundle_fragment(document, + entry: "#/schema", base_uri: "/api/root.json", loader: loader) + assert_received {:loaded, "/api/value.json"} + assert_received {:loaded, "/api/stable.json"} + refute_received {:loaded, _} + assert hd(bundle["allOf"])["$ref"] == canonical + + expected = if mode == :identity do + payload + else + put_in(payload, ["values"], [%{ + "$ref" => "/mirror/value.json#value", + "$dynamicRef" => "/mirror/value.json#node", + "stable" => %{"$ref" => "../stable.json"}, + "unknown" => %{"$ref" => "../missing.json"} + }]) + end + assert bundle["x-payload"] == expected + assert bundle["x-anchor"]["x-payload"] == expected + mounted = Enum.find_value(bundle["$defs"], fn {_key, value} -> + if value["$id"] == canonical, do: value + end) + assert mounted["x-payload"] == expected + reachable = Enum.find_value(bundle["$defs"], fn {key, value} -> + if String.starts_with?(key, "jsonschex_anchor_"), do: value + end) + assert reachable["x-payload"] == expected + assert {:ok, compiled} = JSONSchex.compile(bundle) + assert :ok == JSONSchex.validate(compiled, 1) + assert {:error, _} = JSONSchex.validate(compiled, "1") + end + end + + defp reductions(fun) do + {:reductions, before_count} = Process.info(self(), :reductions) + {:ok, _} = fun.() + {:reductions, after_count} = Process.info(self(), :reductions) + after_count - before_count + end + + defp fallback_document(count) do + %{"x-candidates" => List.duplicate(%{"$anchor" => "same"}, count)} + end + + defp chain(depth) do + Enum.reduce(1..depth, %{"type" => "integer"}, fn _, child -> + %{"properties" => %{"child" => child}} + end) + end + + defp bundle_reductions(schema) do + {:reductions, before_count} = Process.info(self(), :reductions) + {:ok, _} = JSONSchex.bundle_fragment(schema, entry: "#") + {:reductions, after_count} = Process.info(self(), :reductions) + after_count - before_count + end +end