Skip to content

Commit

Permalink
add InterningMembershipOracle
Browse files Browse the repository at this point in the history
  • Loading branch information
mtf90 committed Jan 24, 2024
1 parent 338d0d7 commit 59d0f87
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added

* LearnLib now supports JPMS modules. All artifacts now provide a `module-info` descriptor except of the distribution artifacts (for Maven-less environments) which only provide an `Automatic-Module-Name` due to non-modular dependencies. Note that while this is a Java 9+ feature, LearnLib still supports Java 8 byte code for the remaining class files.
* Added an `InterningMembershipOracle` (including refinements) to the `learnlib-cache` artifact that interns query responses to reduce memory consumption of large data structures. This exports the internal concepts of the DHC learner (which no longer interns query responses automatically).

### Changed

Expand Down
1 change: 1 addition & 0 deletions build-parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ limitations under the License.
<exclude>de/learnlib/filter/reuse/**/Reuse*Builder.class</exclude>

<!-- generated refinements for oracles -->
<exclude>de/learnlib/filter/cache/**/*Interning*.class</exclude>
<exclude>de/learnlib/filter/statistic/**/DFA*.class</exclude>
<exclude>de/learnlib/filter/statistic/**/Mealy*.class</exclude>
<exclude>de/learnlib/filter/statistic/**/Moore*.class</exclude>
Expand Down
6 changes: 6 additions & 0 deletions filters/cache/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ limitations under the License.
<artifactId>slf4j-api</artifactId>
</dependency>

<!-- build -->
<dependency>
<groupId>de.learnlib.tooling</groupId>
<artifactId>annotations</artifactId>
</dependency>

<!-- only required in java doc and tests -->
<dependency>
<groupId>de.learnlib</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/* Copyright (C) 2013-2024 TU Dortmund University
* This file is part of LearnLib, http://www.learnlib.de/.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.learnlib.filter.cache;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

import de.learnlib.oracle.MembershipOracle;
import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle;
import de.learnlib.oracle.MembershipOracle.MooreMembershipOracle;
import de.learnlib.query.DefaultQuery;
import de.learnlib.query.Query;
import de.learnlib.tooling.annotation.refinement.GenerateRefinement;
import de.learnlib.tooling.annotation.refinement.Generic;
import de.learnlib.tooling.annotation.refinement.Interface;
import de.learnlib.tooling.annotation.refinement.Mapping;
import net.automatalib.word.Word;

/**
* A {@link MembershipOracle} that interns query outputs. May be used to reduce memory consumption of data structures
* that store a lot of query responses. Typically, this oracle only makes sense for output types that are not already
* interned by the JVM (such as {@link Boolean}s in case of {@link DFAMembershipOracle}s).
*/
@GenerateRefinement(name = "InterningMealyMembershipOracle",
packageName = "de.learnlib.filter.cache.mealy",
generics = {@Generic(value = "I", desc = "input symbol type"),
@Generic(value = "O", desc = "output symbol type")},
parentGenerics = {@Generic("I"), @Generic(clazz = Word.class, generics = "O")},
interfaces = @Interface(clazz = MealyMembershipOracle.class,
generics = {@Generic("I"), @Generic("O")}),
typeMappings = @Mapping(from = MembershipOracle.class,
to = MealyMembershipOracle.class,
generics = {@Generic("I"), @Generic("O")}))
@GenerateRefinement(name = "InterningMooreMembershipOracle",
packageName = "de.learnlib.filter.cache.moore",
generics = {@Generic(value = "I", desc = "input symbol type"),
@Generic(value = "O", desc = "output symbol type")},
parentGenerics = {@Generic("I"), @Generic(clazz = Word.class, generics = "O")},
interfaces = @Interface(clazz = MooreMembershipOracle.class,
generics = {@Generic("I"), @Generic("O")}),
typeMappings = @Mapping(from = MembershipOracle.class,
to = MooreMembershipOracle.class,
generics = {@Generic("I"), @Generic("O")}))
public class InterningMembershipOracle<I, D> implements MembershipOracle<I, D> {

private final MembershipOracle<I, D> delegate;
private final Map<D, WeakReference<D>> cache;

public InterningMembershipOracle(MembershipOracle<I, D> delegate) {
this.delegate = delegate;
this.cache = new WeakHashMap<>();
}

@Override
public void processQueries(Collection<? extends Query<I, D>> queries) {
final List<DefaultQuery<I, D>> delegates = new ArrayList<>(queries.size());

for (Query<I, D> q : queries) {
delegates.add(new DefaultQuery<>(q));
}

this.delegate.processQueries(delegates);

final Iterator<? extends Query<I, D>> origIter = queries.iterator();
final Iterator<DefaultQuery<I, D>> delegateIter = delegates.iterator();

while (origIter.hasNext() && delegateIter.hasNext()) {
final Query<I, D> origNext = origIter.next();
final DefaultQuery<I, D> delegateNext = delegateIter.next();
final D delegateOutput = delegateNext.getOutput();

// Since the GC may free our references during the lookup, repeat until we have a (non-null) cache hit.
D origOutput;
do {
origOutput = cache.computeIfAbsent(delegateOutput, k -> new WeakReference<>(delegateOutput)).get();
} while (origOutput == null);

origNext.answer(origOutput);
}

assert !origIter.hasNext() && !delegateIter.hasNext();
}
}
1 change: 1 addition & 0 deletions filters/cache/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

// only required by documentation
requires static de.learnlib.oracle.parallelism;
requires static de.learnlib.tooling.annotation;

exports de.learnlib.filter.cache;
exports de.learnlib.filter.cache.dfa;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* Copyright (C) 2013-2024 TU Dortmund University
* This file is part of LearnLib, http://www.learnlib.de/.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.learnlib.filter.cache;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import de.learnlib.oracle.SingleQueryOracle.SingleQueryOracleMealy;
import de.learnlib.query.DefaultQuery;
import net.automatalib.common.util.Pair;
import net.automatalib.word.Word;
import org.testng.Assert;
import org.testng.annotations.Test;

public class InterningMembershipOracleTest {

@Test
public void testInterning() {
final DefaultQuery<Character, Word<Character>> q1 = new DefaultQuery<>(Word.epsilon(), Word.fromString("qwe"));
final DefaultQuery<Character, Word<Character>> q2 = new DefaultQuery<>(Word.fromString("asd"), Word.epsilon());

final Oracle<Character> oracle = new Oracle<>();
final InterningMembershipOracle<Character, Word<Character>> interning = new InterningMembershipOracle<>(oracle);

interning.processQueries(Arrays.asList(q1, q2));

final Word<Character> o1 = q1.getOutput();

Assert.assertSame(q1.getOutput(), o1);
Assert.assertSame(q2.getOutput(), o1);

// repeated queries
interning.processQueries(Arrays.asList(q1, q2));

Assert.assertSame(q1.getOutput(), o1);
Assert.assertSame(q2.getOutput(), o1);

// check executed queries
Assert.assertEquals(oracle.count, 4);
Assert.assertEquals(oracle.queries,
Set.of(Pair.of(Word.epsilon(), Word.fromString("qwe")),
Pair.of(Word.fromString("asd"), Word.epsilon())));
// check that oracle actually returns different objects
Assert.assertNotSame(oracle.answerQuery(q1.getInput()), o1);
Assert.assertEquals(oracle.answerQuery(q1.getInput()), o1);
}

private static class Oracle<I> implements SingleQueryOracleMealy<I, Character> {

private int count;
private final Set<Pair<Word<I>, Word<I>>> queries;

Oracle() {
this.count = 0;
this.queries = new HashSet<>();
}

@Override
public Word<Character> answerQuery(Word<I> prefix, Word<I> suffix) {
count++;
queries.add(Pair.of(prefix, suffix));
return Word.fromString("abcdef");
}
}

}

0 comments on commit 59d0f87

Please sign in to comment.