diff --git a/common/src/main/java/de/uni_bremen/agst/mimesis/filter/DecayPhaseShareBasedFilter.java b/common/src/main/java/de/uni_bremen/agst/mimesis/filter/DecayPhaseShareBasedFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..c40d467f440ea3b57f9af1e0d1ecc19a9af5fab4 --- /dev/null +++ b/common/src/main/java/de/uni_bremen/agst/mimesis/filter/DecayPhaseShareBasedFilter.java @@ -0,0 +1,121 @@ +package de.uni_bremen.agst.mimesis.filter; + +import de.uni_bremen.agst.mimesis.persistence.events.*; +import de.uni_bremen.agst.mimesis.persistence.events.visitor.ExtractPhaseShareBasedProbabilityVisitor; + +import java.util.*; + +/** + * Filter which uses predetermined probabilities for each event to be in a given phase to calculate the phase probabilities. + * + * This filter works similarly to the SimplePhaseShareBasedFilter, but instead of only considering the last 100 events + * the values are just being added up to a continuous score which is multiplied by a decay factor each time a new event is added. + * + * Also, some event types are simply ignored, because these events seem to not have occured often enough to make a meaningful + * guess as to which phase they belong to. + */ +public class DecayPhaseShareBasedFilter extends ThreePhaseFiltering { + + /** + * Visitor responsible for extracting probabilities for given events. + */ + private final ExtractPhaseShareBasedProbabilityVisitor extractor; + + /** + * The previous state of the event buffer. Used to determine if the event buffer has changed. + */ + private Deque<Event> investigationBuffer; + + private Deque<Event> editingBuffer; + + private Deque<Event> verificationBuffer; + + private double investigationProbabilitySum = 0; + + private double editingProbabilitySum = 0; + + private double verificationProbabilitySum = 0; + + private static final double INVESTIGATION_DECAY_FACTOR = 0.99; + + private static final double EDITING_DECAY_FACTOR = 0.99; + + private static final double VERIFICATION_DECAY_FACTOR = 0.99; + + private Set<Class<? extends Event>> ignoredEventTypes = new HashSet<Class<? extends Event>>(); + + public DecayPhaseShareBasedFilter() { + super(); + + ignoredEventTypes.add(TreeViewerEvent.class); + ignoredEventTypes.add(TreeSelectionEvent.class); + ignoredEventTypes.add(EditorTextCursorEvent.class); + ignoredEventTypes.add(DebugEvent.class); + ignoredEventTypes.add(RecordingEvent.class); + ignoredEventTypes.add(EditorMouseEvent.class); + + extractor = new ExtractPhaseShareBasedProbabilityVisitor(); + investigationBuffer = new ArrayDeque<>(100); + editingBuffer = new ArrayDeque<>(100); + verificationBuffer = new ArrayDeque<>(100); + } + + @Override + public void handleEvent(Event event) { + if(!ignoredEventTypes.contains(event.getClass())) { + decay(); + super.handleEvent(event); + } + } + + private void decay() { + investigationProbabilitySum *= INVESTIGATION_DECAY_FACTOR; + editingProbabilitySum *= EDITING_DECAY_FACTOR; + verificationProbabilitySum *= VERIFICATION_DECAY_FACTOR; + } + + @Override + public void handleEvents(Collection<Event> events) { + for(Event event : events) { + if(!ignoredEventTypes.contains(event.getClass())) { + handleEvent(event); + } + } + } + + @Override + protected double getInvestigationProbability(Deque<Event> events, double currentProbability) { + if(!events.isEmpty() && !events.peekFirst().equals(investigationBuffer.peekFirst())) { + events.peekFirst().accept(extractor); + if(extractor.getInvestigationFrequencyScore() != 0) { + investigationProbabilitySum += extractor.getInvestigationProbability(); + investigationBuffer.addFirst(events.peekFirst()); + } + } + return investigationProbabilitySum/(investigationProbabilitySum+editingProbabilitySum+verificationProbabilitySum); + } + + @Override + protected double getEditingProbability(Deque<Event> events, double currentProbability) { + if(!events.isEmpty() && !events.peekFirst().equals(editingBuffer.peekFirst())) { + events.peekFirst().accept(extractor); + if(extractor.getEditingFrequencyScore() != 0) { + editingProbabilitySum += extractor.getEditingProbability(); + editingBuffer.addFirst(events.peekFirst()); + } + } + return editingProbabilitySum/(investigationProbabilitySum+editingProbabilitySum+verificationProbabilitySum); + } + + @Override + protected double getVerificationProbability(Deque<Event> events, double currentProbability) { + if(!events.isEmpty() && !events.peekFirst().equals(verificationBuffer.peekFirst())) { + events.peekFirst().accept(extractor); + if(extractor.getVerificationFrequencyScore() != 0) { + verificationProbabilitySum += extractor.getVerificationProbability(); + verificationBuffer.addFirst(events.peekFirst()); + } + } + return verificationProbabilitySum/(investigationProbabilitySum+editingProbabilitySum+verificationProbabilitySum); + } +} diff --git a/common/src/main/java/de/uni_bremen/agst/mimesis/filter/FrequencyAccountedPhaseShareBasedFilter.java b/common/src/main/java/de/uni_bremen/agst/mimesis/filter/FrequencyAccountedPhaseShareBasedFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..70f5e105e961897c260a5407544ed662c4420d6b --- /dev/null +++ b/common/src/main/java/de/uni_bremen/agst/mimesis/filter/FrequencyAccountedPhaseShareBasedFilter.java @@ -0,0 +1,128 @@ +package de.uni_bremen.agst.mimesis.filter; + +import de.uni_bremen.agst.mimesis.persistence.events.EditorMouseEvent; +import de.uni_bremen.agst.mimesis.persistence.events.Event; +import de.uni_bremen.agst.mimesis.persistence.events.visitor.ExtractPhaseShareBasedProbabilityVisitor; +import de.uni_bremen.agst.mimesis.persistence.events.visitor.ExtractProbabilityVisitor; + +import java.time.Duration; +import java.util.*; + +/** + * Filter which uses predetermined probabilities for each event to be in a given phase to calculate the phase probabilities. + * + * This filter works similarly to the SimplePhaseShareBasedFilter, but uses weights for the different event types based + * on their frequency in a certain phase. The aim of this method is to account for the fact that some events are inherently + * more common (like ScrollEvents) and therefore have more influence on the resulting value. This weighing must happen + * on the phase-level, not on the global level, because otherwise the filter is inherently biased towards shorter phases. + * + * Another difference is that the number of events considered is based on their weights and changes dynamically (and + * therefore also differs from phase to phase. + */ +public class FrequencyAccountedPhaseShareBasedFilter extends ThreePhaseFiltering { + + /** + * Visitor responsible for extracting probabilities for given events. + */ + private final ExtractPhaseShareBasedProbabilityVisitor extractor; + + /** + * The previous state of the event buffer. Used to determine if the event buffer has changed. + */ + private Deque<Event> investigationBuffer; + + private Deque<Event> editingBuffer; + + private Deque<Event> verificationBuffer; + + private double investigationProbabilitySum = 0; + + private double editingProbabilitySum = 0; + + private double verificationProbabilitySum = 0; + + private double investigationFrequencyScoreSum = 0; + + private double editingFrequencyScoreSum = 0; + + private double verificationFrequencyScoreSum = 0; + + private static final double bufferLimit = 100d/3d; + + public FrequencyAccountedPhaseShareBasedFilter() { + super(); + extractor = new ExtractPhaseShareBasedProbabilityVisitor(); + investigationBuffer = new ArrayDeque<>(100); + editingBuffer = new ArrayDeque<>(100); + verificationBuffer = new ArrayDeque<>(100); + } + + @Override + public void handleEvent(Event event) { + limitBuffers(); + super.handleEvent(event); + } + + private void limitBuffers() { + while(investigationFrequencyScoreSum > bufferLimit && !investigationBuffer.isEmpty()) { + investigationBuffer.pollLast().accept(extractor); + investigationProbabilitySum -= extractor.getInvestigationProbability()/extractor.getInvestigationFrequencyScore(); + investigationFrequencyScoreSum -= extractor.getInvestigationFrequencyScore(); + } + while(editingFrequencyScoreSum > bufferLimit && !editingBuffer.isEmpty()) { + editingBuffer.pollLast().accept(extractor); + editingProbabilitySum -= extractor.getEditingProbability()/extractor.getEditingFrequencyScore(); + editingFrequencyScoreSum -= extractor.getEditingFrequencyScore(); + } + while(verificationFrequencyScoreSum > bufferLimit && !verificationBuffer.isEmpty()) { + verificationBuffer.pollLast().accept(extractor); + verificationProbabilitySum -= extractor.getVerificationProbability()/extractor.getVerificationFrequencyScore(); + verificationFrequencyScoreSum -= extractor.getVerificationFrequencyScore(); + } + } + + @Override + public void handleEvents(Collection<Event> events) { + limitBuffers(); + super.handleEvents(events); + } + + @Override + protected double getInvestigationProbability(Deque<Event> events, double currentProbability) { + if(!events.isEmpty() && !events.peekFirst().equals(investigationBuffer.peekFirst()) && !(events.peekFirst() instanceof EditorMouseEvent)) { + events.peekFirst().accept(extractor); + if(extractor.getInvestigationFrequencyScore() != 0) { + investigationProbabilitySum += extractor.getInvestigationProbability()/extractor.getInvestigationFrequencyScore(); + investigationFrequencyScoreSum += extractor.getInvestigationFrequencyScore(); + investigationBuffer.addFirst(events.peekFirst()); + } + } + return investigationProbabilitySum/(investigationProbabilitySum+editingProbabilitySum+verificationProbabilitySum); + } + + @Override + protected double getEditingProbability(Deque<Event> events, double currentProbability) { + if(!events.isEmpty() && !events.peekFirst().equals(editingBuffer.peekFirst()) && !(events.peekFirst() instanceof EditorMouseEvent)) { + events.peekFirst().accept(extractor); + if(extractor.getEditingFrequencyScore() != 0) { + editingProbabilitySum += extractor.getEditingProbability()/extractor.getEditingFrequencyScore(); + editingFrequencyScoreSum += extractor.getEditingFrequencyScore(); + editingBuffer.addFirst(events.peekFirst()); + } + } + return editingProbabilitySum/(investigationProbabilitySum+editingProbabilitySum+verificationProbabilitySum); + } + + @Override + protected double getVerificationProbability(Deque<Event> events, double currentProbability) { + if(!events.isEmpty() && !events.peekFirst().equals(verificationBuffer.peekFirst()) && !(events.peekFirst() instanceof EditorMouseEvent)) { + events.peekFirst().accept(extractor); + if(extractor.getVerificationFrequencyScore() != 0) { + verificationProbabilitySum += extractor.getVerificationProbability()/extractor.getVerificationFrequencyScore(); + verificationFrequencyScoreSum += extractor.getVerificationFrequencyScore(); + verificationBuffer.addFirst(events.peekFirst()); + } + } + return verificationProbabilitySum/(investigationProbabilitySum+editingProbabilitySum+verificationProbabilitySum); + } +} diff --git a/common/src/main/java/de/uni_bremen/agst/mimesis/filter/Phase.java b/common/src/main/java/de/uni_bremen/agst/mimesis/filter/Phase.java index cc8995b5f2015afe018424db45a6933bac354a22..2cda0cc48d73f7d7796089f47e26f40b6004ef10 100644 --- a/common/src/main/java/de/uni_bremen/agst/mimesis/filter/Phase.java +++ b/common/src/main/java/de/uni_bremen/agst/mimesis/filter/Phase.java @@ -27,12 +27,12 @@ class Phase { /** * A function which takes in the buffer of recent events with the most recent event at the start as well as - * the duration between when the last event happened until this event happened, - * and returns a double with which the current probability will be multiplied. + * the current probability + * and returns a double with which the current probability will be replaced. * This will be applied every time a new Event occurs. * For the first event, the buffer will be empty and the duration will be zero. */ - private final ToDoubleBiFunction<Deque<Event>, Duration> modifierFunction; + private final ToDoubleBiFunction<Deque<Event>, Double> modifierFunction; /** * Constructs a new Phase with the given modifier function @@ -40,22 +40,17 @@ class Phase { * that the user is currently in this phase. * @param phaseName the name for this phase. */ - public Phase(ToDoubleBiFunction<Deque<Event>, Duration> modifierFunction, String phaseName) { + public Phase(ToDoubleBiFunction<Deque<Event>, Double> modifierFunction, String phaseName) { this.modifierFunction = modifierFunction; this.phaseName = phaseName; } /** * A function which takes in the buffer of recent events with the most recent event at the start, - * and returns a double with which the current probability will be multiplied. + * and replaced the current probability with a value calculated using the modifierFunction. * This will be applied every time a new Event occurs, so performance has to be taken into account. */ public void calculateModifier(Deque<Event> events) { - Duration duration = Duration.ZERO; - if (events.size() > 1) { - Iterator<Event> iter = events.iterator(); - duration = Duration.ofMillis(iter.next().getTimestamp() - iter.next().getTimestamp()).abs(); - } - probability *= modifierFunction.applyAsDouble(events, duration); + probability = modifierFunction.applyAsDouble(events, probability); } } diff --git a/common/src/main/java/de/uni_bremen/agst/mimesis/filter/SimplePhaseShareBasedFilter.java b/common/src/main/java/de/uni_bremen/agst/mimesis/filter/SimplePhaseShareBasedFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..828af61e77b1f25d9da11afb2ac1addf9bff6c2c --- /dev/null +++ b/common/src/main/java/de/uni_bremen/agst/mimesis/filter/SimplePhaseShareBasedFilter.java @@ -0,0 +1,95 @@ +package de.uni_bremen.agst.mimesis.filter; + +import de.uni_bremen.agst.mimesis.persistence.events.EditorMouseEvent; +import de.uni_bremen.agst.mimesis.persistence.events.Event; +import de.uni_bremen.agst.mimesis.persistence.events.visitor.ExtractPhaseShareBasedProbabilityVisitor; +import de.uni_bremen.agst.mimesis.persistence.events.visitor.ExtractProbabilityVisitor; + +import java.time.Duration; +import java.util.*; + +/** + * Filter which uses predetermined probabilities for each event to be in a given phase to calculate the phase probabilities. + * + * This filter calculates the phase probabilities based on the last 100 events. For each event/phase combination a + * probability has been provided, which has been determined using previous recordings. These probabilities are then + * summed up for the 100 considered events to determine a score for each phase. The probability is then divided by the + * sum of all three scores combined. + * + * This filter, even though it is very simple, manages to differentiate between investigation and editing phase pretty + * well, but has problems in finding verification phases. + * + * The probabilities used for this filter have not been calculated by just + * using the raw number of events of a given type divided by the total number of events of this type, because then there + * would be a heavy bias towards the editing phase (which is by far the longest phase). Instead, the values has been + * calculated by first determining the procentual share a given event type has in a given phase, and then dividing that + * by the sum of the procentual share of the given event type for all three phases. + */ +public class SimplePhaseShareBasedFilter extends ThreePhaseFiltering { + + /** + * Visitor responsible for extracting probabilities for given events. + */ + private final ExtractPhaseShareBasedProbabilityVisitor extractor; + + /** + * The previous state of the event buffer. Used to determine if the event buffer has changed. + */ + private Deque<Event> previousEvents; + + private double investigationProbabilitySum = 0; + + private double editingProbabilitySum = 0; + + private double verificationProbabilitySum = 0; + + public SimplePhaseShareBasedFilter() { + super(); + extractor = new ExtractPhaseShareBasedProbabilityVisitor(); + previousEvents = new ArrayDeque<>(100); + } + + /** + * Applies the extractor to the most recent event and sets necessary attributes. + * After this method is executed, each probability can be accessed. + * If the event queue did not change, the extractor will not be called again. + * @param events the recent events + */ + private void extractProbability(Deque<Event> events) { + if (!events.peekFirst().equals(previousEvents.peekFirst())) { + if(!(events.peekFirst() instanceof EditorMouseEvent)) { + if(previousEvents.size() == 100) { + previousEvents.pollLast().accept(extractor); + investigationProbabilitySum -= extractor.getInvestigationProbability(); + editingProbabilitySum -= extractor.getEditingProbability(); + verificationProbabilitySum -= extractor.getVerificationProbability(); + } + if (!events.isEmpty()) { + events.peekFirst().accept(extractor); + investigationProbabilitySum += extractor.getInvestigationProbability(); + editingProbabilitySum += extractor.getEditingProbability(); + verificationProbabilitySum += extractor.getVerificationProbability(); + } + this.previousEvents.addFirst(events.peekFirst()); + } + } + } + + @Override + protected double getInvestigationProbability(Deque<Event> events, double currentProbability) { + extractProbability(events); + return investigationProbabilitySum/(investigationProbabilitySum+editingProbabilitySum+verificationProbabilitySum); + } + + @Override + protected double getEditingProbability(Deque<Event> events, double currentProbability) { + extractProbability(events); + return editingProbabilitySum/(investigationProbabilitySum+editingProbabilitySum+verificationProbabilitySum); + } + + @Override + protected double getVerificationProbability(Deque<Event> events, double currentProbability) { + extractProbability(events); + return verificationProbabilitySum/(investigationProbabilitySum+editingProbabilitySum+verificationProbabilitySum); + } +} diff --git a/common/src/main/java/de/uni_bremen/agst/mimesis/filter/ThreePhaseFiltering.java b/common/src/main/java/de/uni_bremen/agst/mimesis/filter/ThreePhaseFiltering.java index 646399fd9ea6fae4616a63cba75d38080eb30897..b50356f9ae96c886d20f7b721f81557b38c72e10 100644 --- a/common/src/main/java/de/uni_bremen/agst/mimesis/filter/ThreePhaseFiltering.java +++ b/common/src/main/java/de/uni_bremen/agst/mimesis/filter/ThreePhaseFiltering.java @@ -4,10 +4,7 @@ import de.uni_bremen.agst.mimesis.persistence.events.Event; import de.uni_bremen.agst.mimesis.persistence.events.visitor.ExtractProbabilityVisitor; import java.time.Duration; -import java.util.ArrayDeque; -import java.util.Collection; -import java.util.Deque; -import java.util.Map; +import java.util.*; /** * Responsible for filtering incoming events using the "Three Phases" model, @@ -18,28 +15,17 @@ import java.util.Map; * <li>"Verification" phase</li> * </ol> */ -public class ThreePhaseFiltering { +public abstract class ThreePhaseFiltering { /** * The event filter responsible for calculating the probabilities implementing the three phase model. */ - private final EventFilter threePhaseFilter; + protected final EventFilter threePhaseFilter; - /** - * Visitor responsible for extracting probabilities for given events. - */ - private final ExtractProbabilityVisitor extractor; - - /** - * The previous state of the event buffer. Used to determine if the event buffer has changed. - */ - private Deque<Event> previousEvents; - - public ThreePhaseFiltering() { - extractor = new ExtractProbabilityVisitor(); - Phase investigation = new Phase(this::getInvestigationModifier, "Investigation"); - Phase editing = new Phase(this::getEditingModifier, "Editing"); - Phase verification = new Phase(this::getVerificationModifier, "Verification"); + protected ThreePhaseFiltering() { + Phase investigation = new Phase(this::getInvestigationProbability, "Investigation"); + Phase editing = new Phase(this::getEditingProbability, "Editing"); + Phase verification = new Phase(this::getVerificationProbability, "Verification"); this.threePhaseFilter = new EventFilter(investigation, editing, verification); } @@ -52,6 +38,14 @@ public class ThreePhaseFiltering { return threePhaseFilter.getAllProbabilities(); } + /** + * Returns the phase that this filter currently predicts to be the most likely one. + * @return the phase that this filter currently predicts to be the most likely one. + */ + public String getMostLikelyPhaseName() { + return threePhaseFilter.getMostLikelyPhase().getPhaseName(); + } + /** * Recalculates the probabilities based on the new event. * @param event the new event @@ -69,36 +63,9 @@ public class ThreePhaseFiltering { threePhaseFilter.handleEvents(events); } - /** - * Applies the extractor to the most recent event and sets necessary attributes. - * After this method is executed, each probability can be accessed. - * If the event queue did not change, the extractor will not be called again. - * @param events the recent events - * @param duration the duration between the second-to-last and the last event - */ - private void extractProbability(Deque<Event> events, Duration duration) { - if (!events.equals(previousEvents)) { - extractor.setRecentEvents(events); - extractor.setSinceLastEvent(duration); - if (!events.isEmpty()) { - events.peekFirst().accept(extractor); - } - this.previousEvents = new ArrayDeque<>(events); - } - } - - private double getInvestigationModifier(Deque<Event> events, Duration duration) { - extractProbability(events, duration); - return extractor.getInvestigationProbabilityModifier(); - } + protected abstract double getInvestigationProbability(Deque<Event> events, double currentProbability); - private double getEditingModifier(Deque<Event> events, Duration duration) { - extractProbability(events, duration); - return extractor.getEditingProbabilityModifier(); - } + protected abstract double getEditingProbability(Deque<Event> events, double currentProbability); - private double getVerificationModifier(Deque<Event> events, Duration duration) { - extractProbability(events, duration); - return extractor.getVerificationProbabilityModifier(); - } + protected abstract double getVerificationProbability(Deque<Event> events, double currentProbability); } diff --git a/common/src/main/java/de/uni_bremen/agst/mimesis/persistence/events/visitor/ExtractPhaseShareBasedProbabilityVisitor.java b/common/src/main/java/de/uni_bremen/agst/mimesis/persistence/events/visitor/ExtractPhaseShareBasedProbabilityVisitor.java new file mode 100644 index 0000000000000000000000000000000000000000..29cb6344e8b5904ccf5cd856e596e0ea2a56de30 --- /dev/null +++ b/common/src/main/java/de/uni_bremen/agst/mimesis/persistence/events/visitor/ExtractPhaseShareBasedProbabilityVisitor.java @@ -0,0 +1,268 @@ +package de.uni_bremen.agst.mimesis.persistence.events.visitor; + +import de.uni_bremen.agst.mimesis.persistence.events.*; +import lombok.Getter; + +/** + * Extracts the probability that a given event type + */ +public class ExtractPhaseShareBasedProbabilityVisitor implements EventVisitor { + + /** + * Probability modifier that the developer is currently in the investigation phase. + * Will be multiplied with the initial probability of this phase. + */ + @Getter + private double investigationProbability; + + /** + * Probability modifier that the developer is currently in the editing phase. + * Will be multiplied with the initial probability of this phase. + */ + @Getter + private double editingProbability; + + /** + * Probability modifier that the developer is currently in the verification phase. + * Will be multiplied with the initial probability of this phase. + */ + @Getter + private double verificationProbability; + + @Getter + private double investigationFrequencyScore; + + @Getter + private double editingFrequencyScore; + + @Getter + private double verificationFrequencyScore; + + /** + * Initializes a new visitor with the collection of recent events. + */ + public ExtractPhaseShareBasedProbabilityVisitor() { + reset(); + } + + /** + * Resets all values to the default. + */ + private void reset() { + investigationProbability = 0; + editingProbability = 0; + verificationProbability = 0; + investigationFrequencyScore = 0; + editingFrequencyScore = 0; + verificationFrequencyScore = 0; + } + + @Override + public void visit(CodeChangeEvent event) { + investigationProbability = 0.013146697456303; + editingProbability = 0.806578110483533; + verificationProbability = 0.180275192060164; + investigationFrequencyScore = 0.00333936283624377; + editingFrequencyScore = 0.216670469994924; + verificationFrequencyScore = 0.0625271557291902; + + } + + @Override + public void visit(CodeCompletionEvent event) { + investigationProbability = 0; + editingProbability = 0.821386262495585; + verificationProbability = 0.178613737504415; + investigationFrequencyScore = 0; + editingFrequencyScore = 0.00245227679335341; + verificationFrequencyScore = 0.00144410860523393; + + } + + @Override + public void visit(DebugEvent event) { + investigationProbability = 0; + editingProbability = 0.095633538708813; + verificationProbability = 0.904366461291187; + investigationFrequencyScore = 0; + editingFrequencyScore = 0.0000148104265402844; + verificationFrequencyScore = 0.000140056022408964; + + } + + @Override + public void visit(EditorMouseEvent event) { + investigationProbability = 0; + editingProbability = 0; + verificationProbability = 0; + investigationFrequencyScore = 0; + editingFrequencyScore = 0; + verificationFrequencyScore = 0; + + } + + @Override + public void visit(EditorTextCursorEvent event) { + investigationProbability = 0.104262985820664; + editingProbability = 0.678619341254429; + verificationProbability = 0.217117672924907; + investigationFrequencyScore = 0.0684842632728819; + editingFrequencyScore = 0.398904207483886; + verificationFrequencyScore = 0.155413564867674; + + } + + @Override + public void visit(LaunchEvent event) { + investigationProbability = 0.164140697056276; + editingProbability = 0.11448978059679; + verificationProbability = 0.721369522346934; + investigationFrequencyScore = 0.00477442401012157; + editingFrequencyScore = 0.00276436018434436; + verificationFrequencyScore = 0.0242490729727571; + } + + @Override + public void visit(PerspectiveEvent event) { + investigationProbability = 0.79882399829807; + editingProbability = 0.062640225722219; + verificationProbability = 0.138535775979711; + investigationFrequencyScore = 0.0722785293992259; + editingFrequencyScore = 0.00559398916090297; + verificationFrequencyScore = 0.015366732439432; + } + + @Override + public void visit(ProjectEvent event) { + investigationProbability = 0; + editingProbability = 0; + verificationProbability = 0; + investigationFrequencyScore = 0; + editingFrequencyScore = 0; + verificationFrequencyScore = 0; + } + + @Override + public void visit(RecordingEvent event) { + investigationProbability = 0.316245369656478; + editingProbability = 0; + verificationProbability = 0.683754630343522; + investigationFrequencyScore = 0.00277477291036844; + editingFrequencyScore = 0; + verificationFrequencyScore = 0.0111000580226078; + + } + + @Override + public void visit(ResourceEvent event) { + investigationProbability = 0.163247626316087; + editingProbability = 0.243999680623975; + verificationProbability = 0.592752693059938; + investigationFrequencyScore = 0.00696094437567606; + editingFrequencyScore = 0.00810130009812491; + verificationFrequencyScore = 0.0245494020878547; + } + + @Override + public void visit(SaveEvent event) { + investigationProbability = 0.010025986114882; + editingProbability = 0.248820768119129; + verificationProbability = 0.741153245765989; + investigationFrequencyScore = 0.000189753320683112; + editingFrequencyScore = 0.00310065176842037; + verificationFrequencyScore = 0.0116899481120217; + } + + @Override + public void visit(ScrollEvent event) { + investigationProbability = 0.452728912256228; + editingProbability = 0.193274043616377; + verificationProbability = 0.353997044127395; + investigationFrequencyScore = 0.524984950190141; + editingFrequencyScore = 0.215334164537235; + verificationFrequencyScore = 0.396731057255983; + } + + @Override + public void visit(SearchEvent event) { + investigationProbability = 0.173177102929252; + editingProbability = 0.349260287502385; + verificationProbability = 0.477562609568364; + investigationFrequencyScore = 0.00764163372859025; + editingFrequencyScore = 0.0011605765606206; + verificationFrequencyScore = 0.0216182355351368; + } + + @Override + public void visit(TextCommentEvent event) { + investigationProbability = 0; + editingProbability = 0; + verificationProbability = 0; + investigationFrequencyScore = 0; + editingFrequencyScore = 0; + verificationFrequencyScore = 0; + + } + + @Override + public void visit(TextSelectionEvent event) { + investigationProbability = 0.287503224076869; + editingProbability = 0.435153397743742; + verificationProbability = 0.277343378179389; + investigationFrequencyScore = 0.0671112695567684; + editingFrequencyScore = 0.093580599641985; + verificationFrequencyScore = 0.0681679534377027; + + } + + @Override + public void visit(TreeViewerEvent event) { + investigationProbability = 0.985883620689655; + editingProbability = 0.014116379310345; + verificationProbability = 0; + investigationFrequencyScore = 0.00465560075110805; + editingFrequencyScore = 0.0000289855072463768; + verificationFrequencyScore = 0; + } + + @Override + public void visit(TreeSelectionEvent event) { + investigationProbability = 0.935798330544671; + editingProbability = 0.03753421103529; + verificationProbability = 0.026667458420039; + investigationFrequencyScore = 0.0498458800200679; + editingFrequencyScore = 0.00344333304588117; + verificationFrequencyScore = 0.00588235294117647; + + } + + @Override + public void visit(ViewEvent event) { + investigationProbability = 0.654731978633943; + editingProbability = 0.126850618560833; + verificationProbability = 0.218417402805224; + investigationFrequencyScore = 0.170837008576606; + editingFrequencyScore = 0.0373430419583147; + verificationFrequencyScore = 0.093718193787574; + } + + @Override + public void visit(VoiceCommentEvent event) { + investigationProbability = 0; + editingProbability = 0; + verificationProbability = 0; + investigationFrequencyScore = 0; + editingFrequencyScore = 0; + verificationFrequencyScore = 0; + } + + @Override + public void visit(WindowEvent event) { + investigationProbability = 0.140393295469157; + editingProbability = 0.113100049642076; + verificationProbability = 0.746506654888767; + investigationFrequencyScore = 0.0161216070515173; + editingFrequencyScore = 0.0115072328382208; + verificationFrequencyScore = 0.107402108183246; + } +} diff --git a/visual/src/main/java/de/uni_bremen/agst/mimesis/visualization/model/Settings.java b/visual/src/main/java/de/uni_bremen/agst/mimesis/visualization/model/Settings.java index c2bfa41aaffab41ba24718e641b9d7ee76c76525..57c0c594ff7e2627846ae563a243ec0688c07159 100644 --- a/visual/src/main/java/de/uni_bremen/agst/mimesis/visualization/model/Settings.java +++ b/visual/src/main/java/de/uni_bremen/agst/mimesis/visualization/model/Settings.java @@ -4,6 +4,8 @@ import de.uni_bremen.agst.mimesis.visualization.model.settings.LineMappingMode; import de.uni_bremen.agst.mimesis.visualization.model.settings.SelectionExport; import de.uni_bremen.agst.mimesis.visualization.model.settings.TooltipDetail; import de.uni_bremen.agst.mimesis.visualization.model.settings.TooltipMode; +import de.uni_bremen.agst.mimesis.visualization.model.settings.Filter; +import org.primefaces.application.resource.DynamicResourcesPhaseListener; import javax.enterprise.context.SessionScoped; import java.io.Serializable; @@ -61,6 +63,9 @@ public class Settings implements Serializable { add("Selection Export", "Whether to export a file after selecting an event range.", SelectionExport.NO_DOWNLOAD, SelectionExport.class, SelectionExport::valueOf, null); + add("Phase filter", "Choose the filter used to determine the phase the developer is in.", + Filter.DECAY_PHASE_SHARE_BASED, Filter.class, Filter::valueOf, null); + } /** diff --git a/visual/src/main/java/de/uni_bremen/agst/mimesis/visualization/model/settings/Filter.java b/visual/src/main/java/de/uni_bremen/agst/mimesis/visualization/model/settings/Filter.java new file mode 100644 index 0000000000000000000000000000000000000000..9e0a769700496233ec54761ecdadabc53734a7ae --- /dev/null +++ b/visual/src/main/java/de/uni_bremen/agst/mimesis/visualization/model/settings/Filter.java @@ -0,0 +1,31 @@ +package de.uni_bremen.agst.mimesis.visualization.model.settings; + +import de.uni_bremen.agst.mimesis.filter.DecayPhaseShareBasedFilter; +import de.uni_bremen.agst.mimesis.filter.FrequencyAccountedPhaseShareBasedFilter; +import de.uni_bremen.agst.mimesis.filter.SimplePhaseShareBasedFilter; +import de.uni_bremen.agst.mimesis.filter.ThreePhaseFiltering; + +public enum Filter { + DECAY_PHASE_SHARE_BASED("Use a filter that scores the phases using a multiplication based decay for the current value each time a new Event is added.", new DecayPhaseShareBasedFilter()), + + FREQUENCY_ACCOUNTED_PHASE_SHARE_BASED("Uses a filter that attempts to compensate for the fact that some events are more frequent when determining phase scores.", new FrequencyAccountedPhaseShareBasedFilter()), + + SIMPLE_PHASE_SHARE_BASED("Uses a filter that simply scores up the probability that each event type is in a certain phase for the last 100 Events for each events to determine the current phase.", new SimplePhaseShareBasedFilter()); + + private String description; + private ThreePhaseFiltering instance; + + Filter(String description, ThreePhaseFiltering instance) { + this.description = description; + this.instance = instance; + } + + @Override + public String toString() { + return String.format("%s: %s", super.toString(), description); + } + + public ThreePhaseFiltering getFilterInstance() { + return instance; + } +} diff --git a/visual/src/main/java/de/uni_bremen/agst/mimesis/visualization/service/GraphGeneratorService.java b/visual/src/main/java/de/uni_bremen/agst/mimesis/visualization/service/GraphGeneratorService.java index 45b0d63e96237764d6325121fdbe1bacf4034b81..c088e6688052058fa8f672910c3cb9acddbaffa9 100644 --- a/visual/src/main/java/de/uni_bremen/agst/mimesis/visualization/service/GraphGeneratorService.java +++ b/visual/src/main/java/de/uni_bremen/agst/mimesis/visualization/service/GraphGeneratorService.java @@ -1,5 +1,8 @@ package de.uni_bremen.agst.mimesis.visualization.service; - +; +import de.uni_bremen.agst.mimesis.filter.DecayPhaseShareBasedFilter; +import de.uni_bremen.agst.mimesis.filter.FrequencyAccountedPhaseShareBasedFilter; +import de.uni_bremen.agst.mimesis.filter.SimplePhaseShareBasedFilter; import de.uni_bremen.agst.mimesis.filter.ThreePhaseFiltering; import de.uni_bremen.agst.mimesis.persistence.EventContext; import de.uni_bremen.agst.mimesis.persistence.LineMapping; @@ -9,6 +12,7 @@ import de.uni_bremen.agst.mimesis.persistence.locations.FileSpecificLocation; import de.uni_bremen.agst.mimesis.persistence.locations.LineSpecificLocation; import de.uni_bremen.agst.mimesis.visualization.model.EventListEntry; import de.uni_bremen.agst.mimesis.visualization.model.Settings; +import de.uni_bremen.agst.mimesis.visualization.model.settings.Filter; import de.uni_bremen.agst.mimesis.visualization.model.settings.LineMappingMode; import de.uni_bremen.agst.mimesis.visualization.model.settings.TooltipDetail; import lombok.Getter; @@ -36,7 +40,8 @@ public class GraphGeneratorService { boolean includeInconsistentEvents = false; return generateGraphForEventList(events, includeInconsistentEvents, fileName, settings.<TooltipDetail>getSetting("Tooltip Detail").getValue(), - settings.<LineMappingMode>getSetting("Line Mapping Mode").getValue()); + settings.<LineMappingMode>getSetting("Line Mapping Mode").getValue(), + ((Filter) settings.getSetting("Phase filter").getValue()).getFilterInstance()); } private static void applyFixes(Recording rec) { @@ -203,7 +208,7 @@ public class GraphGeneratorService { * @return RecordingGraph for the given events. */ private static RecordingGraph generateGraphForEventList(List<Event> events, boolean includeInconsistentEvents, - String fileName, TooltipDetail detail, LineMappingMode mapMode) { + String fileName, TooltipDetail detail, LineMappingMode mapMode, ThreePhaseFiltering filtering) { // This contains the maximum length each file requires to display all events within it HashMap<String, Integer> fileMaxLengths = getMaxFileLengths(events, includeInconsistentEvents, mapMode); @@ -224,16 +229,29 @@ public class GraphGeneratorService { // Time at which the recording ended long recordingEnd = events.get(events.size() - 1).getTimestamp(); - - List<EventDatapoint> eventData = getEventGraphDataset(events, offsets, includeInconsistentEvents, detail, mapMode); + // Generate event list entries and phase markings. + List<EventDatapoint> eventData = getEventGraphDataset(events, offsets, includeInconsistentEvents, detail, mapMode, filtering); List<EventListEntry> entries = new ArrayList<>(); + ArrayList<PhaseDatapoint> phases = new ArrayList<>(); + String currentPhase = null; + long currentStartY = 0; for (EventDatapoint datapoint : eventData) { EventListEntry entry = EventListEntry.valueOf(datapoint.event); entries.add(entry.withPhases(datapoint.getProbabilities())); + if(!datapoint.phaseName.equals(currentPhase)) { + if(currentPhase != null) { + phases.add(new PhaseDatapoint(currentPhase, currentStartY, datapoint.event.getTimestamp())); + } + currentStartY = datapoint.event.getTimestamp(); + currentPhase = datapoint.phaseName; + } + } + if(currentPhase != null && eventData.size() > 0) { + phases.add(new PhaseDatapoint(currentPhase, currentStartY, eventData.get(eventData.size()-1).event.getTimestamp())); } // fileOffset is the maximum line number used in the graph - return new RecordingGraph(eventData, fileOffset, recordingEnd, offsets, fileName, entries); + return new RecordingGraph(eventData, fileOffset, recordingEnd, offsets, fileName, entries, phases); } /** @@ -341,8 +359,8 @@ public class GraphGeneratorService { * @return An ArrayList of generated EventDatapoints. */ private static ArrayList<EventDatapoint> getEventGraphDataset(List<Event> events, HashMap<String, Integer> offsets, - boolean includeInconsistentEvents, TooltipDetail detail, - LineMappingMode mapMode) { + boolean includeInconsistentEvents, TooltipDetail detail, + LineMappingMode mapMode, ThreePhaseFiltering filtering) { ArrayList<EventDatapoint> dataPoints = new ArrayList<>(); int lineNumber = 0; int nonInferredLineNumber = 0; @@ -350,7 +368,6 @@ public class GraphGeneratorService { int overrideVisibleRangeBegin = 0; int overrideVisibleRangeEnd = 0; String filePath = ""; - ThreePhaseFiltering filtering = new ThreePhaseFiltering(); for (Event event : events) { //TODO: Instead of filtering the same thing every time, event data sets should be preprocessed once. // Should probably be its own issue. @@ -394,7 +411,7 @@ public class GraphGeneratorService { } } filtering.handleEvent(event); - EventDatapoint eventFacade = new EventDatapoint(event, fileOffset, detail, filtering.getAllProbabilities()); + EventDatapoint eventFacade = new EventDatapoint(event, fileOffset, detail, filtering.getAllProbabilities(), filtering.getMostLikelyPhaseName()); eventFacade.overrideEventLine(lineNumber); eventFacade.overrideVisualRangeBegin(overrideVisibleRangeBegin); eventFacade.overrideVisualRangeEnd(overrideVisibleRangeEnd); @@ -447,6 +464,9 @@ public class GraphGeneratorService { @Getter private final LinkedHashMap<String, Integer> offsets; + @Getter + private final List<PhaseDatapoint> phases; + @Getter private final int maxLine; @@ -458,8 +478,9 @@ public class GraphGeneratorService { //TODO: Use builder pattern private RecordingGraph(List<EventDatapoint> eventDatapoints, int maxLine, long recordingEnd, Map<String, Integer> offsets, - String recordingName, List<EventListEntry> eventEntries) { + String recordingName, List<EventListEntry> eventEntries, List<PhaseDatapoint> phases) { this.maxLine = maxLine; + this.phases = phases; this.recordingEnd = recordingEnd; this.recordingName = recordingName; this.eventEntries = eventEntries; @@ -484,6 +505,23 @@ public class GraphGeneratorService { } } + public static class PhaseDatapoint { + @Getter + private final String phaseName; + + @Getter + private final long begin; + + @Getter + private final long end; + + public PhaseDatapoint(String phaseName, long begin, long end) { + this.phaseName = phaseName; + this.begin = begin; + this.end = end; + } + } + /** * Facade class for events that can be provided with override values for attributes in case they are not set by the @@ -505,11 +543,15 @@ public class GraphGeneratorService { private Map<String, Double> probabilities; - EventDatapoint(Event event, int offset, TooltipDetail detail, Map<String, Double> probabilities) { + @Getter + private String phaseName; + + EventDatapoint(Event event, int offset, TooltipDetail detail, Map<String, Double> probabilities, String phaseName) { this.event = event; this.offset = offset; this.detail = detail; this.probabilities = probabilities; + this.phaseName = phaseName; } /** diff --git a/visual/src/main/webapp/resources/eventGraph.js b/visual/src/main/webapp/resources/eventGraph.js index 05b320dbeaf95e3e0bb871db97994cf9944c5811..2b0f5a35860b2b7536322f12d0208edecb6639e3 100644 --- a/visual/src/main/webapp/resources/eventGraph.js +++ b/visual/src/main/webapp/resources/eventGraph.js @@ -325,6 +325,9 @@ function drawGraph() { // Events are separated into "tracks", which currently are just files chart.options.annotation.annotations = generateAnnotations(data); + chart.options.annotation.fileTrackCount = data.offsets.length; + chart.options.annotation.phaseTrackCount = data.phases.length; + // Now we can generate the legend loadLegend(chart.generateLegend()); @@ -355,6 +358,9 @@ function legendGenerator(theChart, typeSet = theChart.data.datasets[1].types) { text.push('<li><span id="legend-track-item" style="background-color: #cc65fe; color: black; ' + 'user-select: none" onclick="toggleTrackBackgroundVisibility()">Tracks</span></li>'); + text.push('<li><span id="legend-phase-item" style="background-color: #888888; text-decoration: line-through; color: black; ' + + 'user-select: none" onclick="togglePhaseBackgroundVisibility()">Phases</span></li>'); + text.push('<br />'); } @@ -426,6 +432,7 @@ function generateAnnotations(data) { // First we add the box which contains the track annots.push({ + kind: 'track', type: 'box', xScaleID: 'LineXAxis', yScaleID: 'TimeYAxis', @@ -437,6 +444,7 @@ function generateAnnotations(data) { }); // Then we add a vertical line, but it's just to add a label (no other way right now) annots.push({ + kind: 'track', type: 'line', mode: 'vertical', scaleID: 'LineXAxis', @@ -459,6 +467,28 @@ function generateAnnotations(data) { } i += 2; } + for (let phaseIndex in data.phases) { + const phase = data.phases[phaseIndex]; + var phaseColor = "#FFFFFF00"; + if(phase.phaseName === "Investigation") { + phaseColor = "#FF000000"; + } else if (phase.phaseName === "Editing") { + phaseColor = "#00FF0000"; + } else if (phase.phaseName === "Verification") { + phaseColor = "#0000FF00"; + } + annots.push({ + kind: 'phase', + type: '_box', + xScaleID: 'LineXAxis', + yScaleID: 'TimeYAxis', + yMin: phase.begin, + yMax: phase.end, + backgroundColor: phaseColor, + borderWidth: 0, + borderColor: phaseColor + }); + } return annots; } diff --git a/visual/src/main/webapp/resources/eventLegend.js b/visual/src/main/webapp/resources/eventLegend.js index 5ab55a42f17165806ddbe47d8d5cc712992f50ea..508d09f5d9b5a344566cfcb30bdcdb4e1c66e521 100644 --- a/visual/src/main/webapp/resources/eventLegend.js +++ b/visual/src/main/webapp/resources/eventLegend.js @@ -62,8 +62,9 @@ function toggleTrackBackgroundVisibility() { // by setting its type to an empty string. However, we still need to recognize later on which element was a box // and which was a line, so we use invalid but unique names. - // If there are no annotations, we return, since we can't change this. + // If there are no track annotations, we return, since we can't change this. if (annotations.length === 0) return; + if (annotations[0].kind !== "track") return; // First, we check which state S we're in. This implies we want to reach state (S+1)%3. let thisState; @@ -74,24 +75,25 @@ function toggleTrackBackgroundVisibility() { // Then we apply the trick described above for (let i = 0; i < annotations.length; i++) { let thisAnnotation = annotations[i]; - - if (thisState === 0) { - if (thisAnnotation.type === "box") { - // We make the box background invisible using RGBA - thisAnnotation.backgroundColor = thisAnnotation.backgroundColor.substring(0, 7) + "00"; - } - } else if (thisState === 1) { - if (thisAnnotation.type === "line") { - thisAnnotation.type = "_line"; - } else if (thisAnnotation.type === "box") { - thisAnnotation.type = "_box"; - } - } else { - if (thisAnnotation.type === "_box") { - thisAnnotation.type = "box"; - thisAnnotation.backgroundColor = thisAnnotation.backgroundColor.substring(0, 7) + "22"; - } else if (thisAnnotation.type === "_line") { - thisAnnotation.type = "line"; + if(thisAnnotation.kind === "track") { + if (thisState === 0) { + if (thisAnnotation.type === "box") { + // We make the box background invisible using RGBA + thisAnnotation.backgroundColor = thisAnnotation.backgroundColor.substring(0, 7) + "00"; + } + } else if (thisState === 1) { + if (thisAnnotation.type === "line") { + thisAnnotation.type = "_line"; + } else if (thisAnnotation.type === "box") { + thisAnnotation.type = "_box"; + } + } else { + if (thisAnnotation.type === "_box") { + thisAnnotation.type = "box"; + thisAnnotation.backgroundColor = thisAnnotation.backgroundColor.substring(0, 7) + "22"; + } else if (thisAnnotation.type === "_line") { + thisAnnotation.type = "line"; + } } } } @@ -108,6 +110,50 @@ function toggleTrackBackgroundVisibility() { window.chart.update(); } +// For phases we only have two possible states. +// 1: fully visible, 2: fully invisible, which is again recognizable by: +// 1: colored cyan, 2: colored grey and text as strikethrough +function togglePhaseBackgroundVisibility() { + let annotations = window.chart.options.annotation.annotations; + // A bit hacky, but basically the only way to hide an annotation is to make it invalid, for example + // by setting its type to an empty string. However, we still need to recognize later on which element was a box + // and which was a line, so we use invalid but unique names. + + // If there are no phase annotations, we return, since we can't change this. + if (annotations.length === 0) return; + if (annotations[annotations.length-1].kind !== "phase") return; + + // First, we check which state S we're in. This implies we want to reach state (S+1)%3. + let thisState; + if (annotations[annotations.length-1].backgroundColor[8] !== '0') thisState = 0; // If box is not transparent + else thisState = 1; + + // Then we apply the trick described above + for (let i = 0; i < annotations.length; i++) { + let thisAnnotation = annotations[i]; + if(thisAnnotation.kind === "phase") { + if (thisState === 0) { + // We make the box background invisible using RGBA + thisAnnotation.backgroundColor = thisAnnotation.backgroundColor.substring(0, 7) + "00"; + thisAnnotation.type = "_box"; + } else { + thisAnnotation.type = "box"; + thisAnnotation.backgroundColor = thisAnnotation.backgroundColor.substring(0, 7) + "44"; + } + } + } + + if (thisState === 0) { + document.getElementById("legend-phase-item").style.backgroundColor = "#888888"; + document.getElementById("legend-phase-item").style.textDecoration = "line-through"; + } else { + document.getElementById("legend-phase-item").style.textDecoration = ""; + document.getElementById("legend-phase-item").style.backgroundColor = "#65ccfe"; + } + + window.chart.update(); +} + /** * Toggles visibility of all data points that fall within a certain x-coordinate range. * Right now, tracks can only be disabled. This is done destructively and can't be reversed without reloading.