/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.tools.usagestats;
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import java.awt.event.MouseEvent;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.swing.AbstractButton;
import javax.swing.Action;
import javax.swing.JCheckBox;
import javax.swing.JToggleButton;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import com.rapidminer.Process;
import com.rapidminer.ProcessListener;
import com.rapidminer.RapidMiner;
import com.rapidminer.example.ExampleSet;
import com.rapidminer.gui.RapidMinerGUI;
import com.rapidminer.gui.tools.ResourceAction;
import com.rapidminer.gui.tools.components.AbstractLinkButton;
import com.rapidminer.io.process.XMLTools;
import com.rapidminer.operator.IOObject;
import com.rapidminer.operator.Operator;
import com.rapidminer.operator.UserError;
import com.rapidminer.operator.ports.InputPort;
import com.rapidminer.operator.ports.OutputPort;
import com.rapidminer.operator.ports.Port;
import com.rapidminer.tools.XMLException;
import com.vlsolutions.swing.docking.event.DockableStateChangeEvent;
import com.vlsolutions.swing.docking.event.DockableStateChangeListener;
/**
* Supersedes the old functionality of {@link UsageStatistics} to collect usage records of the form
* type, value, arg. Where type can be operator, error, template, and any other category. Value is
* the object of that type which is to be counted (e.g. read_csv if type=operator). arg is most
* often null, but can be used to allow a more fine-grained logging, e.g., in the case of operators,
* arg can be "execute", "stop", or "fail". Note that type, value, and arg will be used as grouping
* attributes for aggregated counts. Therefore, arg cannot be too detailed, e.g. error messages
* "File not found: /path/to/file".
*
* Records can be logged using {@link #log(String, String, String)} which will add 1 to the counter.
* Perspective switches use a timer and can call {@link #log(String, String, String, long)} to use
* other increments than 1.
*
* @author Simon Fischer
*/
public enum ActionStatisticsCollector {
INSTANCE;
public static final String TYPE_CONSTANT = "rapidminer";
private static final String TYPE_DOCKABLE = "dockable";
private static final String TYPE_ACTION = "action";
public static final String TYPE_OPERATOR = "operator";
public static final String TYPE_PERSPECTIVE = "perspective";
public static final String TYPE_ERROR = "error";
public static final String TYPE_IMPORT = "import";
public static final String TYPE_DIALOG = "dialog";
public static final String TYPE_CONSTRAINT = "constraint";
public static final String TYPE_LICENSE_LEVEL = "license-level";
public static final String TYPE_PROGRESS_THREAD = "progress-thread";
public static final String TYPE_TEMPLATE = "template";
public static final String TYPE_RENDERER = "renderer";
public static final String TYPE_CHART = "chart";
/** new data access dialog (since 7.0.0) */
public static final String TYPE_NEW_IMPORT = "new_import";
/** start-up dialog (since 7.0.0) */
public static final String TYPE_GETTING_STARTED = "getting_started";
/** operator search field (since 7.1.1) */
public static final String TYPE_OPERATOR_SEARCH = "operator_search";
/** onboarding dialog (since 7.1.1) */
public static final String TYPE_ONBOARDING = "onboarding";
public static final String OPERATOR_EVENT_EXECUTION = "EXECUTE";
public static final String OPERATOR_EVENT_STOPPED = "STOPPED";
public static final String OPERATOR_EVENT_FAILURE = "FAILURE";
public static final String OPERATOR_EVENT_USER_ERROR = "USER_ERROR";
public static final String OPERATOR_EVENT_OPERATOR_EXCEPTION = "OPERATOR_EXCEPTION";
public static final String OPERATOR_EVENT_RUNTIME_EXCEPTION = "RUNTIME_EXCEPTION";
/** runtime of an operator (since 7.1.1) */
private static final String OPERATOR_RUNTIME = "RUNTIME";
/** input and output volume of an operator port (since 7.1.1) */
private static final String TYPE_INPUT_VOLUME = "input_volume";
private static final String TYPE_OUTPUT_VOLUME = "output_volume";
/** jvm total memory logging (since 7.1.1) */
private static final String TYPE_MEMORY = "memory";
private static final String MEMORY_USED = "used";
private static final String MEMORY_ARG = "MEMORY";
/** arguments to log operator port volume, cells = columns*rows, (since 7.1.1) */
private static final String VOLUMNE_CELLS = "CELLS";
private static final String VOLUME_COLUMNS = "COLUMNS";
private static final String VOLUME_ROWS = "ROWS";
/** row limit check (since 7.2) */
public static final String TYPE_ROW_LIMIT = "row-limit";
public static final String VALUE_ROW_LIMIT_EXCEEDED = "exceeded";
public static final String ARG_ROW_LIMIT_CHECK = "check";
public static final String ARG_ROW_LIMIT_DOWNSAMPLED = "downsampled";
public static final String ARG_ROW_LIMIT_ABORTED = "aborted";
public static final String VALUE_ROW_LIMIT_UPGRADE_FIX = "upgrade_fix";
public static final String VALUE_ROW_LIMIT_UPGRADE_NOT_ENOUGH = "upgrade_not_enough";
public static final String VALUE_ROW_LIMIT_UPGRADE_SELECTED = "upgrade_selected";
public static final String ARG_ROW_LIMIT_NO_UPGRADE = "no_upgrade";
/** commercial and educational sign up (since 7.3) */
public static final String TYPE_SIGN_UP = "sign_up";
public static final String VALUE_ACCOUNT_TYPE = "account_type";
public static final String ARG_COMMERCIAL = "commercial";
public static final String ARG_EDUCATIONAL = "educational";
public static final String VALUE_ACCOUNT_CREATION = "account_creation";
public static final String ARG_ACCOUNT_CREATION_ABORTED = "aborted";
public static final String ARG_ACCOUNT_CREATION_SUCCESS = "success";
public static final String ARG_ACCOUNT_ALREADY_EXISTS = "already_exists";
public static final String ARG_COMMUNICATION_ERROR = "communication_error";
public static final String VALUE_EMAIL_VERIFICATION = "email_verification";
public static final String ARG_EMAIL_VERIFICATION_SUCCESS = "success";
public static final String ARG_EMAIL_VERIFICATION_PENDING = "pending";
/** row limit check additions (since 7.3) */
public static final String VALUE_ROW_LIMIT_DIALOG = "dialog";
/** beta features (since 7.3) */
public static final String TYPE_BETA_FEATURES = "beta-features";
public static final String VALUE_BETA_FEATURES_ACTIVATION = "activated";
/** marketplace search (since 7.3) */
public static final String TYPE_MARKETPLACE = "marketplace";
public static final String VALUE_OPERATOR_SEARCH = "operator_search";
public static final String VALUE_SEARCH = "search";
public static final String VALUE_EXTENSION_INSTALLATION = "extension_installation";
/** extension initialization (since 7.3) */
public static final String VALUE_EXTENSION_INITIALIZATION = "extension_initialization";
/** type cta (since 7.5) */
public static final String TYPE_CTA = "cta";
public static final String VALUE_CTA_FAILURE = "failure";
public static final String VALUE_RULE_TRIGGERED = "cta_triggered";
/**
* added to a key arg to indicated that this stores the maximum amount of all the amounts stored
* for arg
*/
private static final String MAXIMUM_INDICATOR = "_MAX";
/**
* added to a key arg to indicated that this stores the minimum amount of all the amounts stored
* for arg
*/
private static final String MINIMUM_INDICATOR = "_MIN";
/**
* added to a key arg to indicated that this stores the number of times an amount was stored for
* arg
*/
private static final String COUNT_INDICATOR = "_COUNT";
/** conversion constant for bytes to megabytes */
private static final int BYTE_TO_MB = 1024 * 1024;
public static final String XML_TAG = "action-statistics";
public static final class Key {
private String type;
private String value;
private String arg;
public Key(String type, String value, String arg) {
super();
this.type = type;
this.value = value;
this.arg = arg;
}
public String getType() {
return type;
}
public String getValue() {
return value;
}
public String getArg() {
return arg;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (arg == null ? 0 : arg.hashCode());
result = prime * result + (type == null ? 0 : type.hashCode());
result = prime * result + (value == null ? 0 : value.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Key other = (Key) obj;
if (arg == null) {
if (other.arg != null) {
return false;
}
} else if (!arg.equals(other.arg)) {
return false;
}
if (type == null) {
if (other.type != null) {
return false;
}
} else if (!type.equals(other.type)) {
return false;
}
if (value == null) {
if (other.value != null) {
return false;
}
} else if (!value.equals(other.value)) {
return false;
}
return true;
}
@Override
public String toString() {
return type + ",\t" + value + ",\t" + arg;
}
}
/** Listener that logs input and output volume at operator ports. */
private final ProcessListener operatorVolumeListener = new ProcessListener() {
@Override
public void processStarts(Process process) {
// not needed
}
@Override
public void processStartedOperator(Process process, Operator op) {
// log the input volumes of the operator
for (InputPort inputPort : op.getInputPorts().getAllPorts()) {
try {
IOObject ioObject = inputPort.getDataOrNull(IOObject.class);
if (ioObject instanceof ExampleSet) {
ExampleSet exampleSet = (ExampleSet) ioObject;
logInputVolume(op, inputPort, exampleSet.size(), exampleSet.getAttributes().allSize());
}
} catch (UserError e) {
// cannot log volume
}
}
}
@Override
public void processFinishedOperator(Process process, Operator op) {
// log the output volumes of the operator
for (OutputPort outputPort : op.getOutputPorts().getAllPorts()) {
try {
IOObject ioObject = outputPort.getDataOrNull(IOObject.class);
if (ioObject instanceof ExampleSet) {
ExampleSet exampleSet = (ExampleSet) ioObject;
logOutputVolume(op, outputPort, exampleSet.size(), exampleSet.getAttributes().allSize());
}
} catch (UserError e) {
// cannot log volume
}
}
// log the memory volume used
logMemory();
}
@Override
public void processEnded(Process process) {
// not needed
}
};
private final Map<Key, Long> counts = new HashMap<>();
/** flag whether the rowLimit was already exceeded during this session */
private boolean rowLimitExceeded;
public static ActionStatisticsCollector getInstance() {
return INSTANCE;
}
protected void start() {
if (RapidMiner.getExecutionMode().isHeadless()) {
return;
}
long eventMask = AWTEvent.MOUSE_EVENT_MASK;
Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {
@Override
public void eventDispatched(AWTEvent e) {
if (e.getID() == MouseEvent.MOUSE_RELEASED) {
final MouseEvent me = (MouseEvent) e;
Component component = me.getComponent();
logAction(component);
}
}
}, eventMask);
RapidMinerGUI.getMainFrame().getDockingDesktop().addDockableStateChangeListener(new DockableStateChangeListener() {
@Override
public void dockableStateChanged(DockableStateChangeEvent e) {
log(TYPE_DOCKABLE, e.getNewState().getDockable().getDockKey().getKey(),
e.getNewState().getLocation().toString());
}
});
log(TYPE_CONSTANT, "start", null);
}
private void logAction(Object component) {
if (component == null) {
return;
}
if (component instanceof AbstractButton) {
AbstractButton button = (AbstractButton) component;
Action action = button.getAction();
// Only log ResourceActions. Otherwise, we would also log recent files, including file
// names, etc.
if (action instanceof ResourceAction) {
String actionCommand = button.getActionCommand();
if (actionCommand != null) {
if (button instanceof JToggleButton || button instanceof JCheckBox) {
log(TYPE_ACTION, actionCommand, button.isSelected() ? "deselected" : "selected");
} else {
log(TYPE_ACTION, actionCommand, "clicked");
}
}
}
} else if (component instanceof AbstractLinkButton) {
AbstractLinkButton button = (AbstractLinkButton) component;
Action action = button.getAction();
// Only log ResourceActions
if (action instanceof ResourceAction) {
log(TYPE_ACTION, ((ResourceAction) action).getKey(), "clicked");
}
}
}
/**
* Logs the operator execution event and adds the {@link ProcessListener} logging the operator
* volumes.
*
* @param process
* the started process
*/
public void logExecution(Process process) {
if (process == null) {
return;
}
// add listener for operator port volume logging
process.getRootOperator().addProcessListener(operatorVolumeListener);
List<Operator> allInnerOperators = process.getRootOperator().getAllInnerOperators();
for (Operator op : allInnerOperators) {
log(TYPE_OPERATOR, op.getOperatorDescription().getKey(), OPERATOR_EVENT_EXECUTION);
}
}
/**
* Logs the execution time for all operators in the process and removes the
* {@link ProcessListener} logging the operator volumes.
*
* @param process
* the finished process
*/
public void logExecutionFinished(Process process) {
if (process == null) {
return;
}
// remove listener for operator port volume logging
process.getRootOperator().removeProcessListener(operatorVolumeListener);
Collection<Operator> allInnerOperators = process.getAllOperators();
for (Operator op : allInnerOperators) {
// only log if the operator finished
if (!op.isDirty()) {
// retrieve execution time stored with the operator
double executionTime = (double) op.getValue("execution-time").getValue();
logOperatorExecutionTime(op, (long) executionTime);
}
}
}
/**
* Logs that the user exceeded the row limit and schedules a transmission soon.
*/
public void logRowLimitExceeded() {
log(ActionStatisticsCollector.TYPE_ROW_LIMIT, ActionStatisticsCollector.VALUE_ROW_LIMIT_EXCEEDED,
ActionStatisticsCollector.ARG_ROW_LIMIT_CHECK);
if (!rowLimitExceeded) {
rowLimitExceeded = true;
UsageStatistics.getInstance().scheduleTransmissionSoon();
}
}
public void logCtaRuleTriggered(String ruleID, String result) {
log(ActionStatisticsCollector.VALUE_RULE_TRIGGERED, ruleID, result);
UsageStatistics.getInstance().scheduleTransmissionSoon();
}
/**
* Logs the volume for the operator input port. Logs the columns, rows and cells (rows *
* columns) and for each their sum, min, max and count.
*
* @param operator
* the operator the input port belongs to
* @param port
* the input port for which to log the volume
* @param rows
* the rows of the example set at the port
* @param columns
* the columns of the example set at the port
*/
private void logInputVolume(Operator operator, InputPort port, int rows, int columns) {
logVolume(TYPE_INPUT_VOLUME, operator, port, rows, columns);
}
/**
* Logs the volume for the operator output port. Logs the columns, rows and cells (rows *
* columns) and for each their sum, min, max and count.
*
* @param operator
* the operator the output port belongs to
* @param port
* the output port for which to log the volume
* @param rows
* the rows of the example set at the port
* @param columns
* the columns of the example set at the port
*/
private void logOutputVolume(Operator operator, OutputPort port, int rows, int columns) {
logVolume(TYPE_OUTPUT_VOLUME, operator, port, rows, columns);
}
public void log(Operator op, String event) {
if (op == null) {
return;
}
log(TYPE_OPERATOR, op.getOperatorDescription().getKey(), event);
}
public void log(String type, String value, String arg) {
log(type, value, arg, 1);
}
/**
* Logs the executionTime for the operator. Adjusts the sum, min, max and count of the execution
* times logged before.
*
* @param operator
* the operator to log
* @param executionTime
* the execution time (in milliseconds) to log
*/
private void logOperatorExecutionTime(Operator operator, long executionTime) {
logCountSumMinMax(TYPE_OPERATOR, operator.getOperatorDescription().getKey(), OPERATOR_RUNTIME, executionTime);
}
/**
* Logs sum, max and count of the total memory currently used.
*/
private void logMemory() {
long totalSize = Runtime.getRuntime().totalMemory() / BYTE_TO_MB;
log(TYPE_MEMORY, MEMORY_USED, MEMORY_ARG + COUNT_INDICATOR);
log(TYPE_MEMORY, MEMORY_USED, MEMORY_ARG, totalSize);
logMax(TYPE_MEMORY, MEMORY_USED, MEMORY_ARG, totalSize);
}
/**
* Logs the volume for an operator port. Logs the columns, rows and cells and for each their
* sum, min, max and count.
*/
private void logVolume(String type, Operator operator, Port port, int rows, int columns) {
String value = operator.getOperatorDescription().getKey() + "." + port.getName();
logCountSumMinMax(type, value, VOLUME_ROWS, rows);
logCountSumMinMax(type, value, VOLUME_COLUMNS, columns);
logCountSumMinMax(type, value, VOLUMNE_CELLS, (long) columns * rows);
}
/**
* For the key given by type, value and arg logs the amount, its minimum and maximum and how
* often a amount was logged.
*/
private void logCountSumMinMax(String type, String value, String arg, long amount) {
log(type, value, arg + COUNT_INDICATOR);
log(type, value, arg, amount);
logMin(type, value, arg, amount);
logMax(type, value, arg, amount);
}
private void log(String type, String value, String arg, long count) {
Key key = new Key(type, value, arg);
CtaEventAggregator.INSTANCE.log(key, count);
synchronized (counts) {
Long oldAggregate = counts.get(key);
if (oldAggregate == null) {
oldAggregate = 0l;
}
counts.put(key, oldAggregate + count);
}
}
/**
* Logs the minimum amount that was logged for (type, value, arg) under (type, value, arg_MIN).
*/
private void logMin(String type, String value, String arg, long amount) {
Key key = new Key(type, value, arg + MINIMUM_INDICATOR);
synchronized (counts) {
Long oldMin = counts.get(key);
if (oldMin == null) {
oldMin = amount;
}
counts.put(key, Math.min(oldMin, amount));
}
}
/**
* Logs the maximum amount that was logged for (type, value, arg) under (type, value, arg_MAX).
*/
private void logMax(String type, String value, String arg, long amount) {
Key key = new Key(type, value, arg + MAXIMUM_INDICATOR);
synchronized (counts) {
Long oldMax = counts.get(key);
if (oldMax == null) {
oldMax = amount;
}
counts.put(key, Math.max(oldMax, amount));
}
}
private Map<Key, Long> runningTimers = new HashMap<>();
public void startTimer(String type, String value, String arg) {
runningTimers.put(new Key(type, value, arg), System.currentTimeMillis());
}
public void stopTimer(String type, String value, String arg) {
Long startTime = runningTimers.remove(new Key(type, value, arg));
if (startTime != null) {
log(type, value, arg, System.currentTimeMillis() - startTime);
}
}
protected Element getXML(Document doc) {
synchronized (counts) {
Element root = doc.createElement(XML_TAG);
doc.getDocumentElement().appendChild(root);
for (Entry<Key, Long> entry : counts.entrySet()) {
Element actionElement = doc.createElement(TYPE_ACTION);
Key key = entry.getKey();
Long count = entry.getValue();
XMLTools.addTag(actionElement, "type", key.type);
XMLTools.addTag(actionElement, "value", key.value);
if (key.arg != null) {
XMLTools.addTag(actionElement, "arg", key.arg);
}
XMLTools.addTag(actionElement, "count", String.valueOf(count));
root.appendChild(actionElement);
}
root.setAttribute("os-name", System.getProperty("os.name"));
root.setAttribute("os-version", System.getProperty("os.version"));
return root;
}
}
protected void load(Element element) throws XMLException {
synchronized (counts) {
counts.clear();
NodeList actionElements = element.getElementsByTagName(TYPE_ACTION);
for (int i = 0; i < actionElements.getLength(); i++) {
Element actionElement = (Element) actionElements.item(i);
Key key = new Key(XMLTools.getTagContents(actionElement, "type"),
XMLTools.getTagContents(actionElement, "value"), XMLTools.getTagContents(actionElement, "arg"));
counts.put(key, XMLTools.getTagContentsAsLong(actionElement, "count"));
}
}
}
public void clear() {
synchronized (counts) {
counts.clear();
}
}
/** Returns a copy of the current stats. */
public Map<Key, Long> getCounts() {
return new HashMap<>(counts);
}
public long getCount(String type, String value, String arg) {
Long count = counts.get(new Key(type, value, arg));
return count != null ? count : 0;
}
}