package org.spotter.ext.detection.stifle;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.aim.api.exceptions.InstrumentationException;
import org.aim.api.exceptions.MeasurementException;
import org.aim.api.measurement.dataset.Dataset;
import org.aim.api.measurement.dataset.DatasetCollection;
import org.aim.api.measurement.utils.MeasurementDataUtils;
import org.aim.artifacts.probes.ResponsetimeProbe;
import org.aim.artifacts.probes.SQLQueryProbe;
import org.aim.artifacts.probes.ThreadTracingProbe;
import org.aim.artifacts.records.ResponseTimeRecord;
import org.aim.artifacts.records.SQLQueryRecord;
import org.aim.artifacts.records.ThreadTracingRecord;
import org.aim.artifacts.scopes.EntryPointScope;
import org.aim.artifacts.scopes.JDBCScope;
import org.aim.description.InstrumentationDescription;
import org.aim.description.builder.InstrumentationDescriptionBuilder;
import org.lpe.common.extension.IExtension;
import org.lpe.common.util.LpeNumericUtils;
import org.lpe.common.util.LpeStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spotter.core.ProgressManager;
import org.spotter.core.detection.AbstractDetectionController;
import org.spotter.core.detection.IDetectionController;
import org.spotter.exceptions.WorkloadException;
import org.spotter.shared.result.model.SpotterResult;
public class StifleDetectionController extends AbstractDetectionController {
private static final Logger LOGGER = LoggerFactory.getLogger(StifleDetectionController.class);
public StifleDetectionController(IExtension<IDetectionController> provider) {
super(provider);
// TODO Auto-generated constructor stub
}
@Override
public void loadProperties() {
// TODO Auto-generated method stub
}
@Override
public long getExperimentSeriesDuration() {
return ProgressManager.getInstance().calculateDefaultExperimentSeriesDuration(1);
}
@Override
public void executeExperiments() throws InstrumentationException, MeasurementException, WorkloadException {
instrumentApplication(getInstrumentationDescription());
getMeasurementController().prepareMonitoring(getInstrumentationDescription());
runExperiment(this, 1);
getMeasurementController().resetMonitoring();
uninstrumentApplication();
}
@Override
protected SpotterResult analyze(DatasetCollection data) {
LOGGER.info("Fetching datasets.");
Dataset sqlDataset = data.getDataSet(SQLQueryRecord.class);
Dataset rtDataset = data.getDataSet(ResponseTimeRecord.class);
Dataset tracingDataset = data.getDataSet(ThreadTracingRecord.class);
LOGGER.info("Converting SQL dataset.");
List<SQLQueryRecord> sqlRecords = sqlDataset.getRecords(SQLQueryRecord.class);
List<ThreadTracingRecord> ttRecords = tracingDataset.getRecords(ThreadTracingRecord.class);
sqlRecords = filterQueryRecords(sqlRecords, ttRecords);
LOGGER.info("Converting RT dataset.");
List<ResponseTimeRecord> rtRecords = rtDataset.getRecords(ResponseTimeRecord.class);
rtRecords = filterResponsetimeRecords(rtRecords);
LOGGER.info("Analyzing datasets.");
Map<String, List<StifleQuery>> stifleQueries = analyzeDatasets(rtRecords, sqlRecords);
LOGGER.info("Creating results.");
SpotterResult result = new SpotterResult();
result.setDetected(false);
if (!stifleQueries.isEmpty()) {
result.setDetected(true);
StringBuilder strBuilder = new StringBuilder();
for (String operation : stifleQueries.keySet()) {
strBuilder.append("Stifles in operation " + operation + " found:");
strBuilder.append("\n");
List<StifleQuery> stifles = stifleQueries.get(operation);
for (StifleQuery stifle : stifles) {
strBuilder.append("Query: " + stifle.getQuery());
strBuilder.append("\n");
strBuilder.append("occured: ( " + LpeNumericUtils.min(stifle.getOccurrences()) + " , "
+ LpeNumericUtils.average(stifle.getOccurrences()) + " , "
+ LpeNumericUtils.max(stifle.getOccurrences()) + " ) times");
strBuilder.append("\n");
}
strBuilder.append("\n");
strBuilder.append("\n");
}
result.addMessage(strBuilder.toString());
}
return result;
}
private List<SQLQueryRecord> filterQueryRecords(List<SQLQueryRecord> sqlRecords, List<ThreadTracingRecord> ttRecords) {
MeasurementDataUtils.sortRecordsAscending(sqlRecords, SQLQueryRecord.PAR_CALL_ID);
MeasurementDataUtils.sortRecordsAscending(ttRecords, ThreadTracingRecord.PAR_CALL_ID);
List<SQLQueryRecord> sqlRecordsTmp = new ArrayList<>();
List<ThreadTracingRecord> ttRecordsTmp = new ArrayList<>();
int firstSQLIndex = 0;
int firstTTIndex = 0;
int lastSQLIndex = sqlRecords.size() - 1;
int lastTTIndex = ttRecords.size() - 1;
while (sqlRecords.get(firstSQLIndex).getCallId() < ttRecords.get(firstTTIndex).getCallId()) {
firstSQLIndex++;
}
while (sqlRecords.get(firstSQLIndex).getCallId() > ttRecords.get(firstTTIndex).getCallId()) {
firstTTIndex++;
}
while (sqlRecords.get(lastSQLIndex).getCallId() < ttRecords.get(lastTTIndex).getCallId()) {
lastTTIndex--;
}
while (sqlRecords.get(lastSQLIndex).getCallId() > ttRecords.get(lastTTIndex).getCallId()) {
lastSQLIndex--;
}
int ttIndex = firstTTIndex;
int i = firstSQLIndex;
while (i <= lastSQLIndex && ttIndex <= lastTTIndex) {
try {
while (ttRecords.get(ttIndex).getCallId() < sqlRecords.get(i).getCallId()) {
ttIndex++;
}
while (ttRecords.get(ttIndex).getCallId() > sqlRecords.get(i).getCallId()) {
i++;
}
sqlRecordsTmp.add(sqlRecords.get(i));
ttRecordsTmp.add(ttRecords.get(ttIndex));
} catch (Exception e) {
}
i++;
}
// for (int i = firstTTIndex; i <= lastTTIndex; i++) {
// ttRecordsTmp.add(ttRecords.get(i));
// }
if (sqlRecordsTmp.size() != ttRecordsTmp.size()) {
throw new RuntimeException("Unequal amount of records!");
}
List<SQLQueryRecord> uniqueSQLRecords = new ArrayList<>();
int index = 0;
long currentStart = -1;
long refEnd = -1;
while (index < sqlRecordsTmp.size()) {
if (sqlRecordsTmp.get(index).getCallId() != ttRecordsTmp.get(index).getCallId()) {
throw new RuntimeException("CallIDs do not match!");
}
currentStart = ttRecordsTmp.get(index).getEnterNanoTime();
if (refEnd < 0 || currentStart > refEnd) {
refEnd = ttRecordsTmp.get(index).getExitNanoTime();
uniqueSQLRecords.add(sqlRecordsTmp.get(index));
}
index++;
}
return uniqueSQLRecords;
}
private List<ResponseTimeRecord> filterResponsetimeRecords(List<ResponseTimeRecord> rtRecords) {
MeasurementDataUtils.sortRecordsAscending(rtRecords, ResponseTimeRecord.PAR_CALL_ID);
List<ResponseTimeRecord> uniqueServletRecords = new ArrayList<>();
int index = 0;
long currentStart = -1;
long refEnd = -1;
while (index < rtRecords.size()) {
currentStart = rtRecords.get(index).getTimeStamp();
if (refEnd < 0 || currentStart > refEnd) {
refEnd = rtRecords.get(index).getTimeStamp() + rtRecords.get(index).getResponseTime();
uniqueServletRecords.add(rtRecords.get(index));
}
index++;
}
return uniqueServletRecords;
}
/**
* A stifle antipattern can be detected with instrumenting the following:
* <ul>
* <li>servlet response time probe</li>
* <li>JDBC API queries</li>
* </ul>
*
* @return the build {@link InstrumentationDescription}
* @throws InstrumentationException
*/
private InstrumentationDescription getInstrumentationDescription() throws InstrumentationException {
InstrumentationDescriptionBuilder idBuilder = new InstrumentationDescriptionBuilder();
idBuilder.newAPIScopeEntity(EntryPointScope.class.getName()).addProbe(ResponsetimeProbe.MODEL_PROBE)
.entityDone();
idBuilder.newAPIScopeEntity(JDBCScope.class.getName()).addProbe(SQLQueryProbe.MODEL_PROBE)
.addProbe(ThreadTracingProbe.MODEL_PROBE).entityDone();
return idBuilder.build();
}
private Map<String, List<StifleQuery>> analyzeDatasets(List<ResponseTimeRecord> rtRecords,
List<SQLQueryRecord> sqlRecords) {
if (rtRecords.size() < 2) {
LOGGER.info("Less than two response time samples. We have too few data to do an analysis: Skipping stifle analyzing.");
return new HashMap<String, List<StifleQuery>>();
}
Map<String, List<StifleQuery>> stifleQueries = new HashMap<>();
// in this loop we will always be one index ahead of the element we
// currently analyze
ResponseTimeRecord currentRtRecord = null;
int sqlIndex = 0;
for (ResponseTimeRecord nextRtRecord : rtRecords) {
if (currentRtRecord == null) {
currentRtRecord = nextRtRecord;
continue;
}
// the timespace is too inaccurate, hence we use the callId
long currentRTCallId = currentRtRecord.getCallId();
long nextRTCallId = nextRtRecord.getCallId();
// we skip the first SQL queries, which are not related to the first
// RT record
while (sqlIndex < sqlRecords.size() && sqlRecords.get(sqlIndex).getCallId() < currentRTCallId) {
sqlIndex++;
}
if (sqlIndex >= sqlRecords.size()) {
return stifleQueries;
}
Map<String, Integer> potentialStifles = new HashMap<>();
while (sqlIndex < sqlRecords.size() && sqlRecords.get(sqlIndex).getCallId() <= nextRTCallId) {
String query = sqlRecords.get(sqlIndex).getQueryString();
String sql = query;
String generalizedSql = LpeStringUtils.getGeneralizedQuery(sql);
if (generalizedSql == null) {
if (sql.contains("$")) {
int idx_1 = sql.indexOf(",", sql.indexOf("$"));
int idx_2 = sql.indexOf(" ", sql.indexOf("$"));
if (idx_1 < 0 && idx_2 < 0) {
idx_1 = sql.length();
}
idx_1 = idx_1 < 0 ? Integer.MAX_VALUE : idx_1;
idx_2 = idx_2 < 0 ? Integer.MAX_VALUE : idx_2;
int endIndex = Math.min(idx_1, idx_2);
String name = sql.substring(sql.indexOf("$"), endIndex);
generalizedSql = sql.replace(name, "tmp");
} else {
generalizedSql = sql;
}
}
query = generalizedSql;
sqlIndex++;
boolean found = false;
for (String alreadyObservedQuery : potentialStifles.keySet()) {
if (alreadyObservedQuery.equals(query)) {
int newCount = potentialStifles.get(alreadyObservedQuery) + 1;
potentialStifles.put(alreadyObservedQuery, newCount);
found = true;
break;
}
}
if (!found) {
potentialStifles.put(query, 1);
}
}
String operation = currentRtRecord.getOperation();
for (Entry<String, Integer> potStifle : potentialStifles.entrySet()) {
if (potStifle.getValue() > 1) {
if (!stifleQueries.containsKey(operation)) {
stifleQueries.put(operation, new ArrayList<StifleQuery>());
}
List<StifleQuery> tmpStifleQueries = stifleQueries.get(operation);
StifleQuery sQuery = null;
for (StifleQuery tmpQuery : tmpStifleQueries) {
if (potStifle.getKey().equals(tmpQuery.getQuery())) {
sQuery = tmpQuery;
break;
}
}
if (sQuery == null) {
sQuery = new StifleQuery(potStifle.getKey());
tmpStifleQueries.add(sQuery);
}
sQuery.addOccurrence(potStifle.getValue());
}
}
currentRtRecord = nextRtRecord;
}
return stifleQueries;
}
}