diff --git a/core/src/main/java/com/taobao/arthas/core/command/model/SqlProfilerModel.java b/core/src/main/java/com/taobao/arthas/core/command/model/SqlProfilerModel.java index 8fe3ebe53..e81b9ad78 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/model/SqlProfilerModel.java +++ b/core/src/main/java/com/taobao/arthas/core/command/model/SqlProfilerModel.java @@ -1,5 +1,6 @@ package com.taobao.arthas.core.command.model; +import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -10,15 +11,10 @@ import java.util.List; */ public class SqlProfilerModel extends ResultModel { private Date ts; - private String className; - private String methodName; - private double cost; - private boolean success; - private String sql; - private List params; - private List batchSql; - private List> batchParams; + private TraceData traceData; + + private MonitorData monitorData; public SqlProfilerModel() { } @@ -36,67 +32,184 @@ public class SqlProfilerModel extends ResultModel { this.ts = ts; } - public String getClassName() { - return className; + public TraceData getTraceData() { + return traceData; } - public void setClassName(String className) { - this.className = className; + public void setTraceData(TraceData traceData) { + this.traceData = traceData; } - public String getMethodName() { - return methodName; + public MonitorData getMonitorData() { + return monitorData; } - public void setMethodName(String methodName) { - this.methodName = methodName; + public void setMonitorData(MonitorData monitorData) { + this.monitorData = monitorData; } - public double getCost() { - return cost; - } + public static class TraceData { + private String className; + private String methodName; + private double cost; + private boolean success; + private String sql; + private List params; - public void setCost(double cost) { - this.cost = cost; - } + private List batchSql; + private List> batchParams; - public String getSql() { - return sql; - } + public String getClassName() { + return className; + } - public void setSql(String sql) { - this.sql = sql; - } + public void setClassName(String className) { + this.className = className; + } - public boolean isSuccess() { - return success; - } + public String getMethodName() { + return methodName; + } - public void setSuccess(boolean success) { - this.success = success; - } + public void setMethodName(String methodName) { + this.methodName = methodName; + } - public List getParams() { - return params; - } + public double getCost() { + return cost; + } - public void setParams(List params) { - this.params = params; - } + public void setCost(double cost) { + this.cost = cost; + } - public List getBatchSql() { - return batchSql; - } + public String getSql() { + return sql; + } + + public void setSql(String sql) { + this.sql = sql; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public List getParams() { + return params; + } + + public void setParams(List params) { + this.params = params; + } + + public List getBatchSql() { + return batchSql; + } + + public void setBatchSql(List batchSql) { + this.batchSql = batchSql; + } + + public List> getBatchParams() { + return batchParams; + } - public void setBatchSql(List batchSql) { - this.batchSql = batchSql; + public void setBatchParams(List> batchParams) { + this.batchParams = batchParams; + } } - public List> getBatchParams() { - return batchParams; + public static class MonitorData { + private List topByTotalCost = new ArrayList(); + private List topByAvgCost = new ArrayList(); + + public List getTopByTotalCost() { + return topByTotalCost; + } + + public void setTopByTotalCost(List topByTotalCost) { + this.topByTotalCost = topByTotalCost; + } + + public List getTopByAvgCost() { + return topByAvgCost; + } + + public void setTopByAvgCost(List topByAvgCost) { + this.topByAvgCost = topByAvgCost; + } } - public void setBatchParams(List> batchParams) { - this.batchParams = batchParams; + public static class SqlStat { + private String sql; + private double avgCost = 0d; + private double totalCost = 0d; + private Integer count = 0; + private Integer successCount = 0; + private Integer failedCount = 0; + + public String getSql() { + return sql; + } + + public void setSql(String sql) { + this.sql = sql; + } + + public double getAvgCost() { + return avgCost; + } + + public void setAvgCost(double avgCost) { + this.avgCost = avgCost; + } + + public double getTotalCost() { + return totalCost; + } + + public void setTotalCost(double totalCost) { + this.totalCost = totalCost; + } + + public Integer getCount() { + return count; + } + + public void setCount(Integer count) { + this.count = count; + } + + public Integer getSuccessCount() { + return successCount; + } + + public void setSuccessCount(Integer successCount) { + this.successCount = successCount; + } + + public Integer getFailedCount() { + return failedCount; + } + + public void setFailedCount(Integer failedCount) { + this.failedCount = failedCount; + } + + public synchronized void addSample(double cost) { + count++; + totalCost += cost; + } + + public synchronized void calc() { + if (count > 0) { + avgCost = totalCost / count; + } + } } } diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/SqlProfilerAdviceListener.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/SqlProfilerAdviceListener.java index 61dafacbb..85f872511 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/monitor200/SqlProfilerAdviceListener.java +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/SqlProfilerAdviceListener.java @@ -19,11 +19,13 @@ import java.lang.reflect.Method; import java.sql.PreparedStatement; import java.sql.Statement; import java.util.*; +import java.util.concurrent.*; /** * @author yangxiaobing 2021/8/4. */ class SqlProfilerAdviceListener extends AdviceListenerAdapter { + private ScheduledExecutorService scheduledExecutorService; private static final Logger logger = LoggerFactory.getLogger(SqlProfilerAdviceListener.class); public static final List STATEMENT_EXECUTE_METHOD_LIST = Arrays.asList("executeQuery", "executeUpdate", "execute"); @@ -49,6 +51,8 @@ class SqlProfilerAdviceListener extends AdviceListenerAdapter { // 用来解决connectionProxy、statementProxy会导致sql语句被记录多次的问题 private ThreadLocal sqlStack = new ThreadLocal(); + private ConcurrentHashMap sqlStatMap; + static { PREPARED_STATEMENT_SET_PARAM_METHOD_MAP = MethodWithSignature.build(PreparedStatement.class, new Function() { @Override @@ -118,6 +122,32 @@ class SqlProfilerAdviceListener extends AdviceListenerAdapter { super.setVerbose(verbose); } + @Override + public synchronized void create() { + if (scheduledExecutorService == null && "monitor".equalsIgnoreCase(command.getAction())) { + sqlStatMap = new ConcurrentHashMap(); + scheduledExecutorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r); + thread.setDaemon(true); + thread.setName("Timer-for-arthas-sqlprofiler-" + process.session().getSessionId()); + return thread; + } + }); + scheduledExecutorService.schedule(new MonitorCollectTask(sqlStatMap, process), + command.getMonitorDuration(), TimeUnit.SECONDS); + } + } + + @Override + public synchronized void destroy() { + if (null != scheduledExecutorService) { + scheduledExecutorService.shutdown(); + scheduledExecutorService = null; + } + } + @Override public void before(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args) throws Throwable { @@ -429,16 +459,44 @@ class SqlProfilerAdviceListener extends AdviceListenerAdapter { } } + if ("trace".equalsIgnoreCase(command.getAction())) { + appendTraceData(advice, cost, isSuccess, sql, paramObjectList, batchSql, batchParamObjectList); + } else { + appendMonitorStat(advice, cost, isSuccess, sql, paramObjectList, batchSql, batchParamObjectList); + } + } + + private void appendMonitorStat(Advice advice, double cost, boolean isSuccess, String sql, Collection paramObjectList, List batchSql, List> batchParamObjectList) { + if (StringUtils.isEmpty(sql)) { + return; + } + + SqlProfilerModel.SqlStat sqlStat = sqlStatMap.get(sql); + if (sqlStat == null) { + sqlStat = new SqlProfilerModel.SqlStat(); + sqlStat.setSql(sql); + if (sqlStatMap.putIfAbsent(sql, sqlStat) != null) { + sqlStat = sqlStatMap.get(sql); + } + } + + sqlStat.addSample(cost); + } + + private void appendTraceData(Advice advice, double cost, boolean isSuccess, String sql, Collection paramObjectList, List batchSql, List> batchParamObjectList) { SqlProfilerModel model = new SqlProfilerModel(); model.setTs(new Date()); - model.setClassName(advice.getClazz().getName()); - model.setMethodName(advice.getMethod().getName()); - model.setCost(cost); - model.setSuccess(isSuccess); - model.setSql(sql); - model.setParams(convertSqlObjectToString(paramObjectList)); - model.setBatchSql(batchSql); - model.setBatchParams(convertSqlObjectToString(batchParamObjectList)); + SqlProfilerModel.TraceData traceData = new SqlProfilerModel.TraceData(); + model.setTraceData(traceData); + + traceData.setClassName(advice.getClazz().getName()); + traceData.setMethodName(advice.getMethod().getName()); + traceData.setCost(cost); + traceData.setSuccess(isSuccess); + traceData.setSql(sql); + traceData.setParams(convertSqlObjectToString(paramObjectList)); + traceData.setBatchSql(batchSql); + traceData.setBatchParams(convertSqlObjectToString(batchParamObjectList)); process.appendResult(model); @@ -719,4 +777,58 @@ class SqlProfilerAdviceListener extends AdviceListenerAdapter { return false; } } + + private class MonitorCollectTask implements Runnable { + private ConcurrentHashMap sqlStatMap; + private CommandProcess process; + + MonitorCollectTask(ConcurrentHashMap sqlStatMap, CommandProcess process) { + this.sqlStatMap = sqlStatMap; + this.process = process; + } + + @Override + public void run() { + SqlProfilerModel profilerModel = new SqlProfilerModel(); + SqlProfilerModel.MonitorData monitorData = new SqlProfilerModel.MonitorData(); + profilerModel.setMonitorData(monitorData); + + ArrayList statArrayList = new ArrayList(sqlStatMap.values()); + for (SqlProfilerModel.SqlStat sqlStat : statArrayList) { + sqlStat.calc(); + } + + monitorData.setTopByAvgCost(getTopK(statArrayList, new Comparator() { + @Override + public int compare(SqlProfilerModel.SqlStat o1, SqlProfilerModel.SqlStat o2) { + return (int) (o2.getAvgCost() - o1.getAvgCost()); + } + }, 10)); + monitorData.setTopByTotalCost(getTopK(statArrayList, new Comparator() { + @Override + public int compare(SqlProfilerModel.SqlStat o1, SqlProfilerModel.SqlStat o2) { + return (int) (o2.getTotalCost() - o1.getTotalCost()); + } + }, 10)); + + process.appendResult(profilerModel); + process.end(); + } + + private List getTopK(List list, Comparator comparator, int k) { + Collections.sort(list, comparator); + + List result = new ArrayList(k); + int i = 0; + for (T t : list) { + result.add(t); + if (++i >= k) { + break; + } + } + + return result; + } + } + } diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/SqlProfilerCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/SqlProfilerCommand.java index 592318818..55168bf76 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/monitor200/SqlProfilerCommand.java +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/SqlProfilerCommand.java @@ -27,6 +27,8 @@ public class SqlProfilerCommand extends EnhancerCommand { private String sqlPattern; private String conditionExpress; private boolean isRegEx = false; + private String action = "trace"; + private Long monitorDuration = 5L; private Matcher sqlMatcher; @@ -59,6 +61,18 @@ public class SqlProfilerCommand extends EnhancerCommand { isRegEx = regEx; } + @Option(shortName = "a", longName = "action", required = false) + @Description("Action to execute, monitor or trace") + public void setAction(String action) { + this.action = action; + } + + @Option(shortName = "d", longName = "duration", required = false) + @Description("run profiling for seconds") + public void setDuration(long duration) { + this.monitorDuration = duration; + } + public String getConditionExpress() { return conditionExpress; } @@ -67,6 +81,14 @@ public class SqlProfilerCommand extends EnhancerCommand { return sqlPattern; } + public String getAction() { + return action; + } + + public Long getMonitorDuration() { + return monitorDuration; + } + public boolean isRegEx() { return isRegEx; } @@ -106,6 +128,17 @@ public class SqlProfilerCommand extends EnhancerCommand { return sqlMatcher; } + @Override + public void process(CommandProcess process) { + if (!"trace".equalsIgnoreCase(this.action) + && !"monitor".equalsIgnoreCase(this.action)) { + process.end(-1, "invalid action argument"); + return; + } + + super.process(process); + } + @Override protected AdviceListener getAdviceListener(CommandProcess process) { final AdviceListener listener = new SqlProfilerAdviceListener(this, process, GlobalOptions.verbose || this.verbose); diff --git a/core/src/main/java/com/taobao/arthas/core/command/view/SqlProfilerView.java b/core/src/main/java/com/taobao/arthas/core/command/view/SqlProfilerView.java index d1e94cb3a..345c19062 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/view/SqlProfilerView.java +++ b/core/src/main/java/com/taobao/arthas/core/command/view/SqlProfilerView.java @@ -1,12 +1,21 @@ package com.taobao.arthas.core.command.view; import com.taobao.arthas.core.command.model.SqlProfilerModel; +import com.taobao.arthas.core.command.monitor200.MonitorData; import com.taobao.arthas.core.shell.command.CommandProcess; import com.taobao.arthas.core.util.DateUtils; import com.taobao.arthas.core.util.StringUtils; +import com.taobao.text.Decoration; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; +import static com.taobao.text.ui.Element.label; + /** * Term view for SqlProfilerModel * @@ -16,10 +25,56 @@ public class SqlProfilerView extends ResultView { @Override public void draw(CommandProcess process, SqlProfilerModel model) { + if (model.getTraceData() != null) { + drawTraceView(process, model.getTs(), model.getTraceData()); + } else { + drawMonitorView(process, model.getTs(), model.getMonitorData()); + } + } + + private void drawMonitorView(CommandProcess process, Date ts, SqlProfilerModel.MonitorData monitorData) { + process.write(String.format("Top %s by total cost: \n", monitorData.getTopByTotalCost().size())); + for (int i = 0; i < monitorData.getTopByTotalCost().size(); i++) { + SqlProfilerModel.SqlStat sqlStat = monitorData.getTopByTotalCost().get(i); + process.write(String.format("[Top %s sql]: \n" + + "[count]: %s\n" + + "[avgCost(ms)]: %s\n" + + "[totalCost(ms)]: %s\n" + + "[success]: %s\n" + + "[fail]: %s\n", + i, + sqlStat.getSql(), + sqlStat.getCount(), + sqlStat.getAvgCost(), + sqlStat.getTotalCost(), + sqlStat.getSuccessCount(), + sqlStat.getFailedCount())); + } + + process.write(String.format("Top %s by avg cost: \n", monitorData.getTopByAvgCost().size())); + for (int i = 0; i < monitorData.getTopByAvgCost().size(); i++) { + SqlProfilerModel.SqlStat sqlStat = monitorData.getTopByAvgCost().get(i); + process.write(String.format("[Top %s sql]: %s\n" + + "[count]: %s\n" + + "[avgCost(ms)]: %s\n" + + "[totalCost(ms)]: %s\n" + + "[success]: %s\n" + + "[fail]: %s\n", + i, + sqlStat.getSql(), + sqlStat.getCount(), + sqlStat.getAvgCost(), + sqlStat.getTotalCost(), + sqlStat.getSuccessCount(), + sqlStat.getFailedCount())); + } + } + + private void drawTraceView(CommandProcess process, Date ts, SqlProfilerModel.TraceData model) { StringBuilder outputBuilder = new StringBuilder(String.format("ts=%s; [method=%s.%s] [cost=%sms]\n" + "sql: %s\n" + "args: ", - DateUtils.formatDate(model.getTs()), + DateUtils.formatDate(ts), model.getClassName(), model.getMethodName(), model.getCost(),