Skip to content

Commit ae227de

Browse files
authored
Merge pull request #424 from MajewskiKrzysztof/fix/framing-reverse-performance
Fix O(n²) performance issue in JSON-LD framing with @reverse properties
2 parents d481eff + c8f175d commit ae227de

4 files changed

Lines changed: 265 additions & 23 deletions

File tree

src/main/java/com/apicatalog/jsonld/framing/Framing.java

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222
import java.util.Map;
2323
import java.util.Objects;
24+
import java.util.Set;
2425

2526
import com.apicatalog.jsonld.JsonLdEmbed;
2627
import com.apicatalog.jsonld.JsonLdError;
@@ -355,42 +356,31 @@ public void frame() throws JsonLdError {
355356

356357
if (JsonUtils.isObject(reverseObject)) {
357358

358-
for (final String reverseProperty : reverseObject.asJsonObject().keySet()) {
359+
for (final String reverseProperty : reverseObject.asJsonObject().keySet()) {
359360

360-
final Frame subframe = Frame.of((JsonStructure)reverseObject.asJsonObject().get(reverseProperty));
361+
final Frame subframe = Frame.of((JsonStructure) reverseObject.asJsonObject().get(reverseProperty));
361362

362-
for (final String subjectProperty : state.getGraphMap().get(state.getGraphName()).map(Map::keySet).orElseGet(() -> Collections.emptySet())) {
363+
final Set<String> subjectProperties = state.getReversePropertySubjects(state.getGraphName(), reverseProperty, id);
363364

364-
final JsonValue nodeValues = state.getGraphMap().get(state.getGraphName(), subjectProperty, reverseProperty);
365+
if (!subjectProperties.isEmpty()) {
365366

366-
if (nodeValues != null
367-
&& JsonUtils.toStream(nodeValues)
368-
.filter(JsonUtils::isObject)
369-
.map(JsonObject.class::cast)
370-
.filter(v -> v.containsKey(Keywords.ID))
371-
.map(v -> v.getString(Keywords.ID))
372-
.anyMatch(vid -> Objects.equals(vid, id))
373-
) {
367+
final JsonMapBuilder reverseResult = JsonMapBuilder.create();
374368

375-
final JsonMapBuilder reverseResult = JsonMapBuilder.create();
369+
final FramingState reverseState = new FramingState(state);
370+
reverseState.setEmbedded(true);
376371

377-
final FramingState reverseState = new FramingState(state);
378-
reverseState.setEmbedded(true);
379-
380-
Framing.with(
372+
Framing.with(
381373
reverseState,
382-
Arrays.asList(subjectProperty),
374+
new ArrayList<>(subjectProperties),
383375
subframe,
384376
reverseResult,
385377
null)
386-
.ordered(ordered)
387-
.frame();
378+
.ordered(ordered)
379+
.frame();
388380

389-
output
381+
output
390382
.getMapBuilder(Keywords.REVERSE)
391383
.add(reverseProperty, reverseResult.valuesToArray());
392-
393-
}
394384
}
395385
}
396386
}

src/main/java/com/apicatalog/jsonld/framing/FramingState.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616
package com.apicatalog.jsonld.framing;
1717

1818
import java.util.ArrayDeque;
19+
import java.util.Collections;
1920
import java.util.Deque;
2021
import java.util.HashMap;
22+
import java.util.HashSet;
2123
import java.util.Map;
24+
import java.util.Set;
2225

2326
import com.apicatalog.jsonld.JsonLdEmbed;
2427
import com.apicatalog.jsonld.flattening.NodeMap;
@@ -39,6 +42,8 @@ public final class FramingState {
3942

4043
private Deque<String> parents;
4144

45+
private Map<String, Map<String, Map<String, Set<String>>>> reversePropertyIndex;
46+
4247
public FramingState() {
4348
this.done = new HashMap<>();
4449
this.parents = new ArrayDeque<>();
@@ -54,6 +59,7 @@ public FramingState(FramingState state) {
5459
this.graphName = state.graphName;
5560
this.done = state.done;
5661
this.parents = state.parents;
62+
this.reversePropertyIndex = state.reversePropertyIndex;
5763
}
5864

5965
public JsonLdEmbed getEmbed() {
@@ -135,4 +141,27 @@ public void removeLastParent() {
135141
public void clearDone() {
136142
done.clear();
137143
}
144+
145+
public Set<String> getReversePropertySubjects(String graphName, String property, String value) {
146+
if (reversePropertyIndex == null) {
147+
return Collections.emptySet();
148+
}
149+
150+
final Map<String, Map<String, Set<String>>> graphIndex = reversePropertyIndex.get(graphName);
151+
if (graphIndex == null) {
152+
return Collections.emptySet();
153+
}
154+
155+
final Map<String, Set<String>> propertyIndex = graphIndex.get(property);
156+
if (propertyIndex == null) {
157+
return Collections.emptySet();
158+
}
159+
160+
final Set<String> subjects = propertyIndex.get(value);
161+
return subjects != null ? subjects : Collections.emptySet();
162+
}
163+
164+
public void setReversePropertyIndex(Map<String, Map<String, Map<String, Set<String>>>> index) {
165+
this.reversePropertyIndex = index;
166+
}
138167
}

src/main/java/com/apicatalog/jsonld/processor/FramingProcessor.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
import java.net.URI;
1919
import java.util.ArrayList;
2020
import java.util.HashMap;
21+
import java.util.HashSet;
2122
import java.util.List;
2223
import java.util.Map;
2324
import java.util.Map.Entry;
25+
import java.util.Set;
2426
import java.util.stream.Collectors;
2527
import java.util.stream.Stream;
2628

@@ -163,6 +165,9 @@ public static final JsonObject frame(final Document input, final Document frame,
163165
state.getGraphMap().merge();
164166
}
165167

168+
// Build reverse property index for efficient lookups
169+
state.setReversePropertyIndex(buildReversePropertyIndex(state.getGraphMap()));
170+
166171
// 15.
167172
final JsonMapBuilder resultMap = JsonMapBuilder.create();
168173

@@ -391,4 +396,61 @@ private static final void findBlankNodes(JsonValue value, final Map<String, Inte
391396
findBlankNodes(entry.getValue(), blankNodes);
392397
}
393398
}
399+
400+
private static Map<String, Map<String, Map<String, Set<String>>>> buildReversePropertyIndex(final NodeMap graphMap) {
401+
402+
final Map<String, Map<String, Map<String, Set<String>>>> index = new HashMap<>();
403+
404+
for (final String graphName : graphMap.graphs()) {
405+
406+
final Map<String, Map<String, Set<String>>> graphIndex = index.computeIfAbsent(graphName, k -> new HashMap<>());
407+
408+
for (final String subject : graphMap.subjects(graphName)) {
409+
410+
final Map<String, JsonValue> node = graphMap.get(graphName, subject);
411+
412+
if (node == null) {
413+
continue;
414+
}
415+
416+
for (final Entry<String, JsonValue> propEntry : node.entrySet()) {
417+
418+
final String property = propEntry.getKey();
419+
420+
if (Keywords.contains(property)) {
421+
continue;
422+
}
423+
424+
final JsonValue value = propEntry.getValue();
425+
426+
if (JsonUtils.isArray(value)) {
427+
428+
for (final JsonValue item : value.asJsonArray()) {
429+
430+
if (JsonUtils.isObject(item) && item.asJsonObject().containsKey(Keywords.ID)) {
431+
432+
final String targetId = item.asJsonObject().getString(Keywords.ID);
433+
434+
graphIndex
435+
.computeIfAbsent(property, k -> new HashMap<>())
436+
.computeIfAbsent(targetId, k -> new HashSet<>())
437+
.add(subject);
438+
}
439+
}
440+
441+
} else if (JsonUtils.isObject(value) && value.asJsonObject().containsKey(Keywords.ID)) {
442+
443+
final String targetId = value.asJsonObject().getString(Keywords.ID);
444+
445+
graphIndex
446+
.computeIfAbsent(property, k -> new HashMap<>())
447+
.computeIfAbsent(targetId, k -> new HashSet<>())
448+
.add(subject);
449+
}
450+
}
451+
}
452+
}
453+
454+
return index;
455+
}
394456
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.apicatalog.jsonld.api;
17+
18+
import static org.junit.jupiter.api.Assertions.assertNotNull;
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import com.apicatalog.jsonld.JsonLd;
24+
import com.apicatalog.jsonld.JsonLdError;
25+
import com.apicatalog.jsonld.document.JsonDocument;
26+
27+
import jakarta.json.Json;
28+
import jakarta.json.JsonArrayBuilder;
29+
import jakarta.json.JsonObject;
30+
import jakarta.json.JsonObjectBuilder;
31+
32+
class FramingReversePerformanceTest {
33+
34+
@Test
35+
void testFramingPerformance1000Elements() throws JsonLdError {
36+
37+
final long executionTime = measureFramingPerformance(1_000);
38+
39+
System.out.println("Framing 1_000 elements took: " + executionTime + " ms");
40+
41+
assertTrue(executionTime < 1_000L,
42+
"Framing 1_000 elements took too long: " + executionTime + " ms");
43+
}
44+
45+
@Test
46+
void testFramingPerformance10000Elements() throws JsonLdError {
47+
48+
final long executionTime = measureFramingPerformance(10_000);
49+
50+
System.out.println("Framing 10_000 elements took: " + executionTime + " ms");
51+
52+
assertTrue(executionTime < 5_000L,
53+
"Framing 10_000 elements took too long: " + executionTime + " ms");
54+
}
55+
56+
private long measureFramingPerformance(final int elementCount) throws JsonLdError {
57+
58+
final JsonObject document = buildTestDocument(elementCount);
59+
final JsonObject frame = buildTestFrame();
60+
61+
final long startTime = System.currentTimeMillis();
62+
63+
final JsonObject framed = JsonLd.frame(JsonDocument.of(document), JsonDocument.of(frame)).get();
64+
65+
final long endTime = System.currentTimeMillis();
66+
67+
assertNotNull(framed);
68+
69+
return endTime - startTime;
70+
}
71+
72+
private JsonObject buildTestDocument(final int elementCount) {
73+
74+
final JsonObjectBuilder contextBuilder = Json.createObjectBuilder()
75+
.add("@vocab", "http://example.com/vocab#");
76+
77+
final JsonArrayBuilder dataBuilder = Json.createArrayBuilder();
78+
79+
final int departmentCount = Math.max(5, elementCount / 100);
80+
final int personsPerDepartment = elementCount / departmentCount;
81+
82+
for (int departmentIndex = 0; departmentIndex < departmentCount; departmentIndex++) {
83+
84+
final String departmentId = "http://example.com/department" + departmentIndex;
85+
86+
dataBuilder.add(Json.createObjectBuilder()
87+
.add("@id", departmentId)
88+
.add("@type", "Department")
89+
.add("name", "Department " + departmentIndex));
90+
91+
for (int personOffset = 0; personOffset < personsPerDepartment; personOffset++) {
92+
93+
final int personIndex = departmentIndex * personsPerDepartment + personOffset;
94+
final JsonObjectBuilder personBuilder = Json.createObjectBuilder()
95+
.add("@id", "http://example.com/person" + personIndex)
96+
.add("@type", "Person")
97+
.add("name", "Person " + personIndex)
98+
.add("memberOf", departmentId);
99+
100+
if (personOffset == 0 && departmentIndex > 0) {
101+
personBuilder.add("manages", departmentId);
102+
}
103+
104+
if (personIndex > 0 && personIndex % 3 == 0) {
105+
final JsonArrayBuilder relatedBuilder = Json.createArrayBuilder();
106+
relatedBuilder.add("http://example.com/person" + (personIndex - 1));
107+
108+
if (personIndex < elementCount - 1) {
109+
relatedBuilder.add("http://example.com/person" + (personIndex + 1));
110+
}
111+
112+
if (personIndex >= 5 && personIndex % 5 == 0) {
113+
relatedBuilder.add("http://example.com/person" + (personIndex - 5));
114+
}
115+
116+
personBuilder.add("relatedTo", relatedBuilder);
117+
}
118+
119+
final int itemsPerPerson = 3;
120+
for (int itemOffset = 0; itemOffset < itemsPerPerson; itemOffset++) {
121+
122+
final int itemIndex = personIndex * itemsPerPerson + itemOffset;
123+
final JsonObjectBuilder itemBuilder = Json.createObjectBuilder()
124+
.add("@id", "http://example.com/item" + itemIndex)
125+
.add("@type", "Item")
126+
.add("name", "Item " + itemIndex)
127+
.add("parent", "http://example.com/person" + personIndex);
128+
129+
if (itemOffset > 0) {
130+
itemBuilder.add("child", "http://example.com/item" + (itemIndex - 1));
131+
}
132+
133+
dataBuilder.add(itemBuilder);
134+
}
135+
136+
dataBuilder.add(personBuilder);
137+
}
138+
}
139+
140+
return Json.createObjectBuilder()
141+
.add("@context", contextBuilder)
142+
.add("@graph", dataBuilder)
143+
.build();
144+
}
145+
146+
private JsonObject buildTestFrame() {
147+
148+
return Json.createObjectBuilder()
149+
.add("@context", Json.createObjectBuilder()
150+
.add("@vocab", "http://example.com/vocab#"))
151+
.add("@type", "Person")
152+
.add("@reverse", Json.createObjectBuilder()
153+
.add("memberOf", Json.createObjectBuilder()
154+
.add("@type", "Department"))
155+
.add("parent", Json.createObjectBuilder()
156+
.add("@type", "Item"))
157+
.add("relatedTo", Json.createObjectBuilder()
158+
.add("@type", "Person")))
159+
.build();
160+
}
161+
}

0 commit comments

Comments
 (0)