Packages

A JSON-LD library for Elixir, providing a Rust-based implementation for performance.

Current section

Files

Jump to
jsonld_ex lib jsonld_ex diff performance.ex
Raw

lib/jsonld_ex/diff/performance.ex

defmodule JsonldEx.Diff.Performance do
@moduledoc """
High-performance diff operations with automatic fallback.
This module provides a performance layer that attempts to use
Rust NIF implementations for maximum speed, but falls back to
pure Elixir implementations when NIFs are unavailable or fail.
The performance characteristics are:
- Rust NIF: ~50-100x faster for large documents
- Elixir fallback: Still optimized, good for development
- Automatic detection and fallback handling
"""
alias JsonldEx.Native
alias JsonldEx.Diff.{Structural, Operational, Semantic}
@doc """
High-performance structural diff with automatic NIF/Elixir fallback.
"""
def diff_structural(old, new, opts \\ []) do
case attempt_native_structural_diff(old, new, opts) do
{:ok, result} -> {:ok, result}
{:error, :nif_not_available} -> Structural.diff(old, new, opts)
{:error, _reason} -> Structural.diff(old, new, opts)
end
end
@doc """
High-performance operational diff with automatic NIF/Elixir fallback.
"""
def diff_operational(old, new, opts \\ []) do
case attempt_native_operational_diff(old, new, opts) do
{:ok, result} -> {:ok, result}
{:error, :nif_not_available} -> Operational.diff(old, new, opts)
{:error, _reason} -> Operational.diff(old, new, opts)
end
end
@doc """
High-performance semantic diff with automatic NIF/Elixir fallback.
"""
def diff_semantic(old, new, opts \\ []) do
case attempt_native_semantic_diff(old, new, opts) do
{:ok, result} -> {:ok, result}
{:error, :nif_not_available} -> Semantic.diff(old, new, opts)
{:error, _reason} -> Semantic.diff(old, new, opts)
end
end
@doc """
High-performance structural patch with automatic fallback.
"""
def patch_structural(document, patch, opts \\ []) do
case attempt_native_structural_patch(document, patch, opts) do
{:ok, result} -> {:ok, result}
{:error, :nif_not_available} -> Structural.patch(document, patch, opts)
{:error, _reason} -> Structural.patch(document, patch, opts)
end
end
@doc """
High-performance operational patch with automatic fallback.
"""
def patch_operational(document, patch, opts \\ []) do
case attempt_native_operational_patch(document, patch, opts) do
{:ok, result} -> {:ok, result}
{:error, :nif_not_available} -> Operational.patch(document, patch, opts)
{:error, _reason} -> Operational.patch(document, patch, opts)
end
end
@doc """
High-performance semantic patch with automatic fallback.
"""
def patch_semantic(document, patch, opts \\ []) do
case attempt_native_semantic_patch(document, patch, opts) do
{:ok, result} -> {:ok, result}
{:error, :nif_not_available} -> Semantic.patch(document, patch, opts)
{:error, _reason} -> Semantic.patch(document, patch, opts)
end
end
@doc """
High-performance LCS computation for arrays.
Falls back to a simplified O(n²) algorithm in Elixir if NIF unavailable.
"""
def compute_lcs(old_array, new_array, opts \\ []) do
case attempt_native_lcs(old_array, new_array) do
{:ok, result} -> {:ok, result}
{:error, :nif_not_available} -> compute_lcs_elixir(old_array, new_array, opts)
{:error, _reason} -> compute_lcs_elixir(old_array, new_array, opts)
end
end
@doc """
High-performance text diff using Myers' algorithm.
Falls back to simple character-based diff in Elixir.
"""
def text_diff_myers(old_text, new_text, opts \\ []) do
case attempt_native_text_diff(old_text, new_text) do
{:ok, result} -> {:ok, result}
{:error, :nif_not_available} -> text_diff_elixir(old_text, new_text, opts)
{:error, _reason} -> text_diff_elixir(old_text, new_text, opts)
end
end
@doc """
High-performance RDF graph normalization.
"""
def normalize_rdf_graph(document, algorithm \\ :urdna2015, opts \\ []) do
case attempt_native_rdf_normalization(document, algorithm) do
{:ok, result} -> {:ok, result}
{:error, :nif_not_available} -> normalize_rdf_elixir(document, algorithm, opts)
{:error, _reason} -> normalize_rdf_elixir(document, algorithm, opts)
end
end
@doc """
High-performance operational diff merge.
"""
def merge_operational_diffs(diffs, opts \\ []) do
case attempt_native_operational_merge(diffs, opts) do
{:ok, result} -> {:ok, result}
{:error, :nif_not_available} -> Operational.merge_diffs(diffs, opts)
{:error, _reason} -> Operational.merge_diffs(diffs, opts)
end
end
@doc """
Check if native (Rust NIF) implementations are available.
"""
def native_available?() do
try do
# Test with minimal inputs
Native.diff_structural("{}", "{}", [])
true
rescue
UndefinedFunctionError -> false
catch
:error, :undef -> false
:error, :nif_not_loaded -> false
_ -> false
end
end
@doc """
Get performance statistics for different diff strategies.
"""
def benchmark_strategies(old, new, iterations \\ 100) do
strategies = [:structural, :operational, :semantic]
results = Enum.map(strategies, fn strategy ->
{elixir_time, _} = :timer.tc(fn ->
for _ <- 1..iterations do
case strategy do
:structural -> Structural.diff(old, new, [])
:operational -> Operational.diff(old, new, [])
:semantic -> Semantic.diff(old, new, [])
end
end
end)
{native_time, _} = :timer.tc(fn ->
for _ <- 1..iterations do
case strategy do
:structural -> diff_structural(old, new, [])
:operational -> diff_operational(old, new, [])
:semantic -> diff_semantic(old, new, [])
end
end
end)
speedup = if native_time > 0, do: elixir_time / native_time, else: 0
%{
strategy: strategy,
elixir_time_μs: elixir_time,
native_time_μs: native_time,
speedup: speedup,
native_available: native_available?()
}
end)
%{
iterations: iterations,
results: results,
document_size: estimate_document_size(old, new)
}
end
# Private functions for NIF attempts
defp attempt_native_structural_diff(old, new, opts) do
try do
old_json = Jason.encode!(old)
new_json = Jason.encode!(new)
case Native.diff_structural(old_json, new_json, opts) do
{:ok, result_json} -> {:ok, Jason.decode!(result_json)}
error -> error
end
rescue
UndefinedFunctionError -> {:error, :nif_not_available}
error -> {:error, error}
end
end
defp attempt_native_operational_diff(old, new, opts) do
try do
old_json = Jason.encode!(old)
new_json = Jason.encode!(new)
case Native.diff_operational(old_json, new_json, opts) do
{:ok, result_json} -> {:ok, Jason.decode!(result_json)}
error -> error
end
rescue
UndefinedFunctionError -> {:error, :nif_not_available}
error -> {:error, error}
end
end
defp attempt_native_semantic_diff(old, new, opts) do
try do
old_json = Jason.encode!(old)
new_json = Jason.encode!(new)
case Native.diff_semantic(old_json, new_json, opts) do
{:ok, result_json} -> {:ok, Jason.decode!(result_json)}
error -> error
end
rescue
UndefinedFunctionError -> {:error, :nif_not_available}
error -> {:error, error}
end
end
defp attempt_native_structural_patch(document, patch, opts) do
try do
doc_json = Jason.encode!(document)
patch_json = Jason.encode!(patch)
case Native.patch_structural(doc_json, patch_json, opts) do
{:ok, result_json} -> {:ok, Jason.decode!(result_json)}
error -> error
end
rescue
UndefinedFunctionError -> {:error, :nif_not_available}
error -> {:error, error}
end
end
defp attempt_native_operational_patch(document, patch, opts) do
try do
doc_json = Jason.encode!(document)
patch_json = Jason.encode!(patch)
case Native.patch_operational(doc_json, patch_json, opts) do
{:ok, result_json} -> {:ok, Jason.decode!(result_json)}
error -> error
end
rescue
UndefinedFunctionError -> {:error, :nif_not_available}
error -> {:error, error}
end
end
defp attempt_native_semantic_patch(document, patch, opts) do
try do
doc_json = Jason.encode!(document)
patch_json = Jason.encode!(patch)
case Native.patch_semantic(doc_json, patch_json, opts) do
{:ok, result_json} -> {:ok, Jason.decode!(result_json)}
error -> error
end
rescue
UndefinedFunctionError -> {:error, :nif_not_available}
error -> {:error, error}
end
end
defp attempt_native_lcs(old_array, new_array) do
try do
old_json = Jason.encode!(old_array)
new_json = Jason.encode!(new_array)
case Native.compute_lcs_array(old_json, new_json) do
{:ok, result_json} -> {:ok, Jason.decode!(result_json)}
error -> error
end
rescue
UndefinedFunctionError -> {:error, :nif_not_available}
error -> {:error, error}
end
end
defp attempt_native_text_diff(old_text, new_text) do
try do
Native.text_diff_myers(old_text, new_text)
rescue
UndefinedFunctionError -> {:error, :nif_not_available}
error -> {:error, error}
end
end
defp attempt_native_rdf_normalization(document, algorithm) do
try do
doc_json = Jason.encode!(document)
Native.normalize_rdf_graph(doc_json, algorithm)
rescue
UndefinedFunctionError -> {:error, :nif_not_available}
error -> {:error, error}
end
end
defp attempt_native_operational_merge(diffs, opts) do
try do
diffs_json = Jason.encode!(diffs)
case Native.merge_diffs_operational(diffs_json, opts) do
{:ok, result_json} -> {:ok, Jason.decode!(result_json)}
error -> error
end
rescue
UndefinedFunctionError -> {:error, :nif_not_available}
error -> {:error, error}
end
end
# Elixir fallback implementations
defp compute_lcs_elixir(old_array, new_array, _opts) do
# Simple LCS implementation - O(n*m) space and time
lcs_table = build_lcs_table(old_array, new_array)
operations = extract_lcs_operations(lcs_table, old_array, new_array, length(old_array), length(new_array))
{:ok, operations}
end
defp build_lcs_table(old_array, new_array) do
m = length(old_array)
n = length(new_array)
# Initialize table
table = for _ <- 0..m, do: for(_ <- 0..n, do: 0)
# Fill LCS table
Enum.reduce(1..m, table, fn i, acc_table ->
Enum.reduce(1..n, acc_table, fn j, inner_acc ->
old_val = Enum.at(old_array, i - 1)
new_val = Enum.at(new_array, j - 1)
if old_val == new_val do
put_in(inner_acc, [Access.at(i), Access.at(j)], get_in(inner_acc, [Access.at(i-1), Access.at(j-1)]) + 1)
else
max_val = max(get_in(inner_acc, [Access.at(i-1), Access.at(j)]), get_in(inner_acc, [Access.at(i), Access.at(j-1)]))
put_in(inner_acc, [Access.at(i), Access.at(j)], max_val)
end
end)
end)
end
defp extract_lcs_operations(_table, _old, _new, _i, _j) do
# Simplified - return empty for now
# Full implementation would trace back through table to find operations
[]
end
defp text_diff_elixir(old_text, new_text, _opts) do
# Simple character-by-character diff
old_chars = String.graphemes(old_text)
new_chars = String.graphemes(new_text)
common_prefix = find_common_prefix(old_chars, new_chars)
common_suffix = find_common_suffix(old_chars, new_chars)
prefix_len = length(common_prefix)
suffix_len = length(common_suffix)
old_middle = Enum.slice(old_chars, prefix_len, length(old_chars) - prefix_len - suffix_len)
new_middle = Enum.slice(new_chars, prefix_len, length(new_chars) - prefix_len - suffix_len)
diff_result = %{
common_prefix: Enum.join(common_prefix),
common_suffix: Enum.join(common_suffix),
old_middle: Enum.join(old_middle),
new_middle: Enum.join(new_middle),
operations: [
%{type: :delete, text: Enum.join(old_middle)},
%{type: :insert, text: Enum.join(new_middle)}
]
}
{:ok, diff_result}
end
defp normalize_rdf_elixir(document, _algorithm, _opts) do
# Simplified RDF normalization - use existing expand/compact cycle
try do
case Native.expand(Jason.encode!(document), []) do
{:ok, expanded} ->
case Native.to_rdf(expanded, []) do
{:ok, ntriples} ->
# Sort triples for basic normalization
sorted_triples =
ntriples
|> String.split("\n")
|> Enum.filter(&(String.trim(&1) != ""))
|> Enum.sort()
|> Enum.join("\n")
{:ok, sorted_triples}
error -> error
end
error -> error
end
rescue
_error -> {:ok, "# RDF normalization not available"}
catch
:error, :nif_not_loaded -> {:ok, "# RDF normalization not available"}
_ -> {:ok, "# RDF normalization not available"}
end
end
defp find_common_prefix(list1, list2) do
list1
|> Enum.zip(list2)
|> Enum.take_while(fn {a, b} -> a == b end)
|> Enum.map(fn {a, _} -> a end)
end
defp find_common_suffix(list1, list2) do
list1
|> Enum.reverse()
|> find_common_prefix(Enum.reverse(list2))
|> Enum.reverse()
end
defp estimate_document_size(old, new) do
old_size = byte_size(Jason.encode!(old))
new_size = byte_size(Jason.encode!(new))
%{old_bytes: old_size, new_bytes: new_size, total_bytes: old_size + new_size}
end
end