Skip to content
Merged
175 changes: 164 additions & 11 deletions WPDS/src/main/java/wpds/impl/WeightedPAutomaton.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@
import com.google.common.collect.Table;
import de.fraunhofer.iem.Location;
import java.util.ArrayList;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Objects;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
Expand All @@ -51,11 +56,18 @@
public abstract class WeightedPAutomaton<N extends Location, D extends State, W extends Weight>
implements LabeledGraph<D, N> {
private static final Logger LOGGER = LoggerFactory.getLogger(WeightedPAutomaton.class);
private final Map<Transition<N, D>, W> transitionToWeights = new HashMap<>();
/**
* Insertion-ordered because its key set now *is* the set of transitions: the separate
* LinkedHashSet that used to hold them stored exactly the same elements (every transition added
* there is put here in the same call, and a fresh transition always has a null old weight, so the
* put always fires), at 40 bytes per transition for nothing. A LinkedHashMap costs 8 bytes more
* per entry than a HashMap but removes a whole duplicate entry, and it preserves the iteration
* order callers of getTransitions() previously relied on.
*/
private final Map<Transition<N, D>, W> transitionToWeights = new LinkedHashMap<>();
// Set Q is implicit
// Weighted Pushdown Systems and their Application to Interprocedural
// Dataflow Analysis
protected Set<Transition<N, D>> transitions = new LinkedHashSet<>();
// set F in paper [Reps2003]
protected Set<D> finalState = new LinkedHashSet<>();
protected SimpleSetMultimap<D, D> initialStatesToSource = new SimpleSetMultimap<>();
Expand Down Expand Up @@ -104,7 +116,7 @@ public abstract class WeightedPAutomaton<N extends Location, D extends State, W
public abstract boolean isGeneratedState(D d);

public Collection<Transition<N, D>> getTransitions() {
return Lists.newArrayList(transitions);
return Lists.newArrayList(transitionToWeights.keySet());
}

public boolean addTransition(Transition<N, D> trans) {
Expand Down Expand Up @@ -218,7 +230,7 @@ private String toDotString(Set<WeightedPAutomaton<N, D, W>> visited) {
}
}

s += "Transitions: " + transitions.size() + " Nested: " + nestedAutomatons.size() + "\n";
s += "Transitions: " + transitionToWeights.size() + " Nested: " + nestedAutomatons.size() + "\n";
for (WeightedPAutomaton<N, D, W> nested : nestedAutomatons) {
s += "NESTED -> \n";
s += nested.toDotString(visited);
Expand All @@ -237,7 +249,7 @@ private String escapeQuotes(String string) {

public String toLabelGroupedDotString() {
HashBasedTable<D, N, Collection<D>> groupedByTargetAndLabel = HashBasedTable.create();
for (Transition<N, D> t : transitions) {
for (Transition<N, D> t : transitionToWeights.keySet()) {
Collection<D> collection = groupedByTargetAndLabel.get(t.getTarget(), t.getLabel());
if (collection == null) collection = new LinkedHashSet<>();
collection.add(t.getStart());
Expand All @@ -254,7 +266,7 @@ public String toLabelGroupedDotString() {
}
}
s += "}\n";
s += "Transitions: " + transitions.size() + "\n";
s += "Transitions: " + transitionToWeights.size() + "\n";
for (WeightedPAutomaton<N, D, W> nested : nestedAutomatons) {
s += "NESTED -> \n";
s += nested.toDotString();
Expand Down Expand Up @@ -292,7 +304,7 @@ public Set<D> getStates() {

public Set<Edge<D, N>> getEdges() {
Set<Edge<D, N>> trans = new LinkedHashSet<>();
for (Edge<D, N> tran : transitions) {
for (Edge<D, N> tran : transitionToWeights.keySet()) {
if (!tran.getLabel().equals(epsilon())) {
trans.add(new Transition<N, D>(tran.getTarget(), tran.getLabel(), tran.getStart()));
}
Expand Down Expand Up @@ -323,8 +335,8 @@ public boolean addWeightForTransition(Transition<N, D> trans, W weight) {
stateCreatingTransition.put(trans.getTarget(), trans);
}
states.add(trans.getStart());
boolean added = transitions.add(trans);
W oldWeight = transitionToWeights.get(trans);
boolean added = oldWeight == null;
W newWeight = (W) (oldWeight == null ? weight : oldWeight.combineWith(weight));

if (!newWeight.equals(oldWeight)) {
Expand Down Expand Up @@ -908,19 +920,19 @@ public Collection<D> getUnbalancedStartOf(D target) {
* preserved via the per-key LinkedHashSet.
*/
private static final class SimpleSetMultimap<K, V> {
private final Map<K, LinkedHashSet<V>> map = new HashMap<>();
private final Map<K, SmallOrderedSet<V>> map = new HashMap<>();

Set<V> get(K key) {
Set<V> values = map.get(key);
return values != null ? values : Collections.emptySet();
}

boolean put(K key, V value) {
return map.computeIfAbsent(key, k -> new LinkedHashSet<>()).add(value);
return map.computeIfAbsent(key, k -> new SmallOrderedSet<>()).add(value);
}

void putAll(K key, Collection<? extends V> values) {
map.computeIfAbsent(key, k -> new LinkedHashSet<>()).addAll(values);
map.computeIfAbsent(key, k -> new SmallOrderedSet<>()).addAll(values);
}

boolean containsKey(K key) {
Expand All @@ -943,4 +955,145 @@ void clear() {
map.clear();
}
}

/**
* An insertion-ordered set sized for how these multimaps are actually used: measured over a full
* analysis, 88.5% of the per-key sets hold exactly one element and 96% hold eight or fewer. A
* LinkedHashSet costs about 190 bytes for a single element (the set wrapper, a LinkedHashMap, its
* table array and one 40-byte entry); holding that element in a field costs 24.
*
* <p>The representation of {@code data} is decided by {@code size} alone, never by instanceof, so
* an element that is itself an array or a set cannot be misread:
*
* <ul>
* <li>{@code size == 0}: {@code data} is null
* <li>{@code size == 1}: {@code data} is the element
* <li>{@code 2 <= size <= ARRAY_LIMIT}: {@code data} is an Object[], scanned linearly, which
* beats hashing at these sizes
* <li>{@code size > ARRAY_LIMIT}: {@code data} is a LinkedHashSet
* </ul>
*
* <p>Insertion order and Set.add()'s "false if already present" contract are preserved in every
* representation, so callers cannot tell the difference.
*/
private static final class SmallOrderedSet<V> extends AbstractSet<V> {

private static final int ARRAY_LIMIT = 8;

private Object data;
private int size;

@Override
public int size() {
return size;
}

@Override
public boolean isEmpty() {
return size == 0;
}

@Override
@SuppressWarnings("unchecked")
public boolean contains(Object o) {
if (size == 0) {
return false;
}
if (size == 1) {
return Objects.equals(data, o);
}
if (size <= ARRAY_LIMIT) {
Object[] array = (Object[]) data;
for (int i = 0; i < size; i++) {
if (Objects.equals(array[i], o)) {
return true;
}
}
return false;
}
return ((LinkedHashSet<V>) data).contains(o);
}

@Override
@SuppressWarnings("unchecked")
public boolean add(V value) {
if (size == 0) {
data = value;
size = 1;
return true;
}
if (size == 1) {
if (Objects.equals(data, value)) {
return false;
}
Object[] array = new Object[4];
array[0] = data;
array[1] = value;
data = array;
size = 2;
return true;
}
if (size <= ARRAY_LIMIT) {
Object[] array = (Object[]) data;
for (int i = 0; i < size; i++) {
if (Objects.equals(array[i], value)) {
return false;
}
}
if (size == ARRAY_LIMIT) {
LinkedHashSet<V> promoted = new LinkedHashSet<>();
for (int i = 0; i < size; i++) {
promoted.add((V) array[i]);
}
promoted.add(value);
data = promoted;
size = promoted.size();
return true;
}
if (size == array.length) {
array = Arrays.copyOf(array, array.length * 2);
data = array;
}
array[size++] = value;
return true;
}
LinkedHashSet<V> set = (LinkedHashSet<V>) data;
if (set.add(value)) {
size++;
return true;
}
return false;
}

@Override
@SuppressWarnings("unchecked")
public Iterator<V> iterator() {
if (size == 0) {
return Collections.emptyIterator();
}
if (size == 1) {
return Collections.singletonList((V) data).iterator();
}
if (size <= ARRAY_LIMIT) {
Object[] array = (Object[]) data;
return new Iterator<>() {
private int index;

@Override
public boolean hasNext() {
return index < size;
}

@Override
public V next() {
if (index >= size) {
throw new java.util.NoSuchElementException();
}
return (V) array[index++];
}
};
}
return ((LinkedHashSet<V>) data).iterator();
}
}
}
25 changes: 16 additions & 9 deletions boomerangPDS/src/main/java/boomerang/WeightedBoomerang.java
Original file line number Diff line number Diff line change
Expand Up @@ -988,15 +988,22 @@ public ForwardBoomerangResults<W> solve(ForwardQuery query) {
if (analysisWatch.isRunning()) {
analysisWatch.stop();
}
return new ForwardBoomerangResults<>(
query,
icfg(),
cfg(),
timedout,
this.queryToSolvers,
getStats(),
analysisWatch,
visitedMethods);
ForwardBoomerangResults<W> forwardResults =
new ForwardBoomerangResults<>(
query,
icfg(),
cfg(),
timedout,
this.queryToSolvers,
getStats(),
analysisWatch,
visitedMethods);
// Only the top-level query releases: solveUnderScope(..) runs nested inside an outer analysis
// that still needs these solvers.
if (options.releaseSolversAfterQuery()) {
forwardResults.releaseSolvers();
}
return forwardResults;
}

public BackwardBoomerangResults<W> solve(BackwardQuery query) {
Expand Down
17 changes: 17 additions & 0 deletions boomerangPDS/src/main/java/boomerang/options/BoomerangOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ public boolean allowMultipleQueries() {
return builder.allowMultipleQueries;
}

/**
* When enabled, a completed forward query materializes all of its query-scoped results and then
* drops the solvers, so the automata do not stay reachable from the results object. Off by
* default: it makes {@link boomerang.results.ForwardBoomerangResults#getContext} unavailable, and
* it computes every accessor whether or not the client uses them.
*/
public boolean releaseSolversAfterQuery() {
return builder.releaseSolversAfterQuery;
}

public boolean handleSpecialInvokeAsNormalPropagation() {
return builder.handleSpecialInvokeAsNormalPropagation;
}
Expand Down Expand Up @@ -184,6 +194,7 @@ public static class OptionsBuilder {
private boolean trackStaticFieldAtEntryPointToClinit;
private boolean handleMaps;
private boolean allowMultipleQueries;
private boolean releaseSolversAfterQuery;
private boolean handleSpecialInvokeAsNormalPropagation;
private boolean ignoreSparsificationAfterQuery;

Expand All @@ -209,6 +220,7 @@ protected OptionsBuilder() {
this.trackStaticFieldAtEntryPointToClinit = false;
this.handleMaps = true;
this.allowMultipleQueries = false;
this.releaseSolversAfterQuery = false;
this.handleSpecialInvokeAsNormalPropagation = false;
this.ignoreSparsificationAfterQuery = true;
}
Expand Down Expand Up @@ -335,6 +347,11 @@ public OptionsBuilder enableAllowMultipleQueries(boolean allowMultipleQueries) {
return this;
}

public OptionsBuilder enableReleaseSolversAfterQuery(boolean releaseSolversAfterQuery) {
this.releaseSolversAfterQuery = releaseSolversAfterQuery;
return this;
}

public OptionsBuilder enableHandleSpecialInvokeAsNormalPropagation(
boolean handleSpecialInvokeAsNormalPropagation) {
this.handleSpecialInvokeAsNormalPropagation = handleSpecialInvokeAsNormalPropagation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@
public class AbstractBoomerangResults<W extends Weight> {

protected final Logger LOGGER = LoggerFactory.getLogger(AbstractBoomerangResults.class);
protected final DefaultValueMap<ForwardQuery, ForwardBoomerangSolver<W>> queryToSolvers;
/**
* Not final: {@link boomerang.results.ForwardBoomerangResults#releaseSolvers()} drops this
* reference once every query-scoped result has been materialized, so the automata can be
* collected. The map itself is owned by the WeightedBoomerang instance and is never mutated here.
*/
protected DefaultValueMap<ForwardQuery, ForwardBoomerangSolver<W>> queryToSolvers;

public AbstractBoomerangResults(
DefaultValueMap<ForwardQuery, ForwardBoomerangSolver<W>> solverMap) {
Expand Down
Loading
Loading