From f5952fe3ceb186bf10d3889ac8bf5ef4e3610e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Levy=2EXie-=E8=A7=A3=E5=AE=89=E6=A3=AE?= Date: Fri, 12 Jul 2024 16:21:20 +0800 Subject: [PATCH 1/2] =?UTF-8?q?REPORT-117002=20feat:=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E5=99=A8UI=E6=80=A7=E8=83=BD=E7=9B=91=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/fr/design/carton/CartonConstants.java | 61 ++++++ .../carton/CartonThreadExecutorPool.java | 10 +- .../com/fr/design/carton/CartonUtils.java | 147 ++++++++++++++ .../EventDispatchThreadHangMonitor.java | 186 ++++-------------- .../design/carton/FeedbackToolboxDialog.java | 4 +- .../design/carton/SwitchForSwingChecker.java | 37 ++-- .../carton/latency/DesignerLatencyMetric.java | 155 +++++++++++++++ .../design/carton/latency/LatencyLevel.java | 67 +++++++ .../fr/design/mainframe/DesignerFrame.java | 2 + .../main/java/com/fr/start/MainDesigner.java | 2 + 10 files changed, 506 insertions(+), 165 deletions(-) create mode 100644 designer-base/src/main/java/com/fr/design/carton/CartonConstants.java create mode 100644 designer-base/src/main/java/com/fr/design/carton/CartonUtils.java create mode 100644 designer-base/src/main/java/com/fr/design/carton/latency/DesignerLatencyMetric.java create mode 100644 designer-base/src/main/java/com/fr/design/carton/latency/LatencyLevel.java diff --git a/designer-base/src/main/java/com/fr/design/carton/CartonConstants.java b/designer-base/src/main/java/com/fr/design/carton/CartonConstants.java new file mode 100644 index 0000000000..c51f1193c5 --- /dev/null +++ b/designer-base/src/main/java/com/fr/design/carton/CartonConstants.java @@ -0,0 +1,61 @@ +package com.fr.design.carton; + +import com.fr.stable.ProductConstantsBase; +import com.fr.stable.StableUtils; + +import java.text.SimpleDateFormat; + +/** + * 卡顿常量类管理 + * + * @author Levy.Xie + * @since 11.0 + * Created on 2024/07/04 + */ +public class CartonConstants { + + public static final String TIME = "time"; + public static final String APPID = "appId"; + public static final String USERID = "userId"; + public static final String LOCAL = "local"; + public static final String REMOTE = "remote"; + public static final String OPERANDS_NUM = "operands"; + public static final String DESIGN_METHOD = "designMethod"; + public static final String DESIGNER_VERSION = "designerVersion"; + public static final String DESIGNER_ID = "designerId"; + + public static final String EASY_CHECKER_FILE_NAME = "easy_check_log.csv"; + public static final String TIMER_CHECKER_FILE_NAME = "timer_check_log.csv"; + + /** + * 开启间隔检测后两次检测的相隔时间ms + */ + public static final long CHECK_INTERVAL_MS = 100; + + /** + * 最大的事件允许执行时间,超过该时间则打印堆栈等相关信息 + */ + public static final long UNREASONABLE_DISPATCH_DURATION_MS = 1500; + + /** + * UI检测采样频率 + */ + public static final int LATENCY_SAMPLING_FREQUENCY = 100; + + /** + * 输出日志所在地址 + */ + public static final String JOURNAL_FILE_PATH = StableUtils.pathJoin(ProductConstantsBase.getEnvHome(), "journal_log"); + + /** + * 日期事件格式 + */ + public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + /** + * Designer4Debug类名 + */ + public static final String DEBUG_MAIN_CLASS_NAME = "com.fr.start.Designer4Debug"; + + +} diff --git a/designer-base/src/main/java/com/fr/design/carton/CartonThreadExecutorPool.java b/designer-base/src/main/java/com/fr/design/carton/CartonThreadExecutorPool.java index 39525110b3..d366791b25 100644 --- a/designer-base/src/main/java/com/fr/design/carton/CartonThreadExecutorPool.java +++ b/designer-base/src/main/java/com/fr/design/carton/CartonThreadExecutorPool.java @@ -57,18 +57,18 @@ public class CartonThreadExecutorPool extends ThreadPoolExecutor { private void examineHang() { StackTraceElement[] currentStack = eventThread.getStackTrace(); - if (lastReportedStack!=null && EventDispatchThreadHangMonitor.stacksEqual(currentStack, lastReportedStack)) { + if (lastReportedStack!=null && CartonUtils.stacksEqual(currentStack, lastReportedStack)) { return; } lastReportedStack = currentStack; - String stackTrace = EventDispatchThreadHangMonitor.stackTraceToString(currentStack); + String stackTrace = CartonUtils.stackTraceToString(currentStack); JSONObject jsonObject = new JSONObject(); jsonObject.put(Toolkit.i18nText("Fine-Design_Basic_Carton_Output_Time"), simpleDateFormatThreadSafe.format(System.currentTimeMillis())); jsonObject.put(Toolkit.i18nText("Fine-Design_Basic_Carton_Event_Number"), "swingWorker_" + hangNumber); jsonObject.put(Toolkit.i18nText("Fine-Design_Basic_Carton_Duration_Task_Execute"), timeSoFar() + "ms"); jsonObject.put(Toolkit.i18nText("Fine-Design_Basic_Carton_Stack_Info"), stackTrace); - EventDispatchThreadHangMonitor.outPutJournalLog(jsonObject.toString(), SwitchForSwingChecker.TIMER_CHECK_FLAG); - EventDispatchThreadHangMonitor.checkForDeadlock(); + CartonUtils.outPutJournalLog(jsonObject.toString(), SwitchForSwingChecker.TIMER_CHECK_FLAG); + CartonUtils.checkForDeadlock(); } } @@ -129,7 +129,7 @@ public class CartonThreadExecutorPool extends ThreadPoolExecutor { jsonObject.put(Toolkit.i18nText("Fine-Design_Basic_Carton_Event_Number"), "swingWorker_" + concurrentHashMap.get(currentThreadId).hangNumber); jsonObject.put(Toolkit.i18nText("Fine-Design_Basic_Carton_Task_Start_Time"), simpleDateFormatThreadSafe.format(concurrentHashMap.get(currentThreadId).startTime)); jsonObject.put(Toolkit.i18nText("Fine-Design_Basic_Carton_Task_Total_Time"), runTime + "ms"); - EventDispatchThreadHangMonitor.outPutJournalLog(jsonObject.toString(), SwitchForSwingChecker.EASY_CHECK_FLAG); + CartonUtils.outPutJournalLog(jsonObject.toString(), SwitchForSwingChecker.EASY_CHECK_FLAG); } concurrentHashMap.remove(currentThreadId); diff --git a/designer-base/src/main/java/com/fr/design/carton/CartonUtils.java b/designer-base/src/main/java/com/fr/design/carton/CartonUtils.java new file mode 100644 index 0000000000..b67fe8c504 --- /dev/null +++ b/designer-base/src/main/java/com/fr/design/carton/CartonUtils.java @@ -0,0 +1,147 @@ +package com.fr.design.carton; + +import com.fr.log.FineLoggerFactory; +import com.fr.stable.ArrayUtils; +import com.fr.stable.StableUtils; +import com.fr.stable.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.text.SimpleDateFormat; + +import static com.fr.design.carton.CartonConstants.EASY_CHECKER_FILE_NAME; +import static com.fr.design.carton.CartonConstants.JOURNAL_FILE_PATH; +import static com.fr.design.carton.CartonConstants.TIMER_CHECKER_FILE_NAME; + +/** + * 设计器卡顿业务工具类 + * + * @author Levy.Xie + * @since 11.0 + * Created on 2024/07/10 + */ +public class CartonUtils { + + /** + * 堆栈元素输出为文本 + * + * @param stackTrace 堆栈元素 + * @return 文本 + */ + public static String stackTraceToString(StackTraceElement[] stackTrace) { + StringBuilder result = new StringBuilder(); + for (StackTraceElement stackTraceElement : stackTrace) { + String indentation = " "; + result.append("~").append(indentation).append(stackTraceElement); + } + return result.toString(); + } + + /** + * 命令行显示堆栈信息 + * + * @param stackTrace 堆栈元素 + * @return 文本 + */ + public static String stackTraceToStringForConsole(StackTraceElement[] stackTrace) { + StringBuilder result = new StringBuilder(); + for (StackTraceElement stackTraceElement : stackTrace) { + String indentation = " "; + result.append("\r\n").append(indentation).append(stackTraceElement); + } + return result.toString(); + } + + /** + * 堆栈是否一致 + * + * @param a 堆栈A + * @param b 堆栈B + * @return 是否一致 + */ + public static boolean stacksEqual(@NotNull StackTraceElement[] a, @NotNull StackTraceElement[] b) { + if (!ArrayUtils.isSameLength(a, b)) { + return false; + } + for (int i = 0; i < a.length; ++i) { + if (!a[i].equals(b[i])) { + return false; + } + } + return true; + } + + /** + * 检查死锁 + */ + public static void checkForDeadlock() { + ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); + long[] threadIds = threadBean.findDeadlockedThreads(); + if (threadIds == null) { + return; + } + FineLoggerFactory.getLogger().warn("deadlock detected involving the following threads:"); + ThreadInfo[] threadInfos = threadBean.getThreadInfo(threadIds, Integer.MAX_VALUE); + for (ThreadInfo info : threadInfos) { + FineLoggerFactory.getLogger().warn("Thread # {} {} ( {} ) waiting on {} held by {} {}", info.getThreadId(), info.getThreadName(), + info.getThreadState(), info.getLockName(), info.getLockOwnerName(), CartonUtils.stackTraceToStringForConsole(info.getStackTrace())); + } + } + + /** + * 输出卡顿日志 + * + * @param message 文本信息 + * @param flag 类型,true时为简单检查、false时为定时检查 + */ + public static void outPutJournalLog(String message, int flag) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + String date = simpleDateFormat.format(System.currentTimeMillis()); + String filename = flag == SwitchForSwingChecker.EASY_CHECK_FLAG ? EASY_CHECKER_FILE_NAME : TIMER_CHECKER_FILE_NAME; + String[] split = date.split("-"); + int month = StringUtils.isEmpty(split[1]) ? -1 : Integer.parseInt(split[1]); + String dirPath = StableUtils.pathJoin(JOURNAL_FILE_PATH, split[0], "month-" + month, date); + File dirFile = new File(dirPath); + File file = new File(StableUtils.pathJoin(dirPath, filename)); + try { + if (!file.exists()) { + if (!dirFile.exists()) { + dirFile.mkdirs(); + } + file.createNewFile(); + } + BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file, true)); + String outputMessage = message.replaceAll("~", "\r\n") + "," + "\r\n"; + bufferedWriter.write(outputMessage); + bufferedWriter.close(); + } catch (IOException e) { + FineLoggerFactory.getLogger().error("output fail", e); + } + } + + /** + * 用于判断是不是特定的堆栈 + */ + public static boolean stackTraceElementIs(StackTraceElement e, String className, String methodName, boolean isNative) { + return e.getClassName().equals(className) && e.getMethodName().equals(methodName) && e.isNativeMethod() == isNative; + } + + /** + * 用于判断某个堆栈是否在等待另一个事件 + * 取当前堆栈前三层判断是是不是匹配等待堆栈的格式 + */ + public static boolean isWaitingForNextEvent(StackTraceElement[] currentStack) { + + return currentStack != null && currentStack.length >= 3 && + stackTraceElementIs(currentStack[0], "java.lang.Object", "wait", true) + && stackTraceElementIs(currentStack[1], "java.lang.Object", "wait", false) + && stackTraceElementIs(currentStack[2], "java.awt.EventQueue", "getNextEvent", false); + } + +} diff --git a/designer-base/src/main/java/com/fr/design/carton/EventDispatchThreadHangMonitor.java b/designer-base/src/main/java/com/fr/design/carton/EventDispatchThreadHangMonitor.java index a7bf03b407..988e7dd81d 100644 --- a/designer-base/src/main/java/com/fr/design/carton/EventDispatchThreadHangMonitor.java +++ b/designer-base/src/main/java/com/fr/design/carton/EventDispatchThreadHangMonitor.java @@ -1,34 +1,26 @@ package com.fr.design.carton; import com.fr.concurrent.FineExecutors; +import com.fr.design.carton.latency.DesignerLatencyMetric; import com.fr.design.ui.util.UIUtil; import com.fr.json.JSONObject; -import com.fr.log.FineLoggerFactory; -import com.fr.stable.ArrayUtils; -import com.fr.stable.ProductConstantsBase; -import com.fr.stable.StableUtils; -import com.fr.stable.StringUtils; -import org.jetbrains.annotations.NotNull; import javax.swing.SwingUtilities; import java.awt.EventQueue; import java.awt.Toolkit; import java.awt.AWTEvent; import java.awt.event.WindowEvent; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.lang.management.ManagementFactory; -import java.lang.management.ThreadInfo; -import java.lang.management.ThreadMXBean; -import java.text.SimpleDateFormat; import java.util.LinkedList; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import static com.fr.design.carton.CartonConstants.CHECK_INTERVAL_MS; +import static com.fr.design.carton.CartonConstants.DATE_FORMAT; +import static com.fr.design.carton.CartonConstants.LATENCY_SAMPLING_FREQUENCY; +import static com.fr.design.carton.CartonConstants.UNREASONABLE_DISPATCH_DURATION_MS; + /** * 参考自git swinghelper * 用于卡顿检测 @@ -38,31 +30,16 @@ import java.util.concurrent.TimeUnit; */ public final class EventDispatchThreadHangMonitor extends EventQueue { - /** - * 日期事件格式 - */ - private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + public static final EventDispatchThreadHangMonitor INSTANCE = new EventDispatchThreadHangMonitor(); /** * 一个timer */ private Timer timer; - /** - * 开启间隔检测后两次检测的相隔时间ms - */ - private static final long CHECK_INTERVAL_MS = 100; - /** - * 最大的事件允许执行时间,超过该时间则打印堆栈等相关信息 - */ - private static final long UNREASONABLE_DISPATCH_DURATION_MS = 1500; /** * 事件唯一编码,用于方便日志的查看 */ - private static long hangCount = 0; - /** - * 输出日志所在地址 - */ - private static final String JOURNAL_FILE_PATH = StableUtils.pathJoin(ProductConstantsBase.getEnvHome(), "journal_log"); + private static long hangCount = 0; /** * 类似于一个开关,当该值为默认的false启动时,定时任务在窗口开启前都不会对执行的事件进行检查 */ @@ -103,43 +80,6 @@ public final class EventDispatchThreadHangMonitor extends EventQueue { return hangCount++; } - /** - * @param a can not be null - * @param b can not be null - * @return - */ - public static boolean stacksEqual(@NotNull StackTraceElement[] a, @NotNull StackTraceElement[] b) { - - if (!ArrayUtils.isSameLength(a, b)) { - return false; - } - for (int i = 0; i < a.length; ++i) { - if (!a[i].equals(b[i])) { - return false; - } - } - return true; - } - - /** - * 用于判断是不是特定的堆栈 - */ - public static boolean stackTraceElementIs(StackTraceElement e, String className, String methodName, boolean isNative) { - return e.getClassName().equals(className) && e.getMethodName().equals(methodName) && e.isNativeMethod() == isNative; - } - - /** - * 用于判断某个堆栈是否在等待另一个事件 - * 取当前堆栈前三层判断是是不是匹配等待堆栈的格式 - */ - public static boolean isWaitingForNextEvent(StackTraceElement[] currentStack) { - - return currentStack != null && currentStack.length >= 3 && - stackTraceElementIs(currentStack[0], "java.lang.Object", "wait", true) - && stackTraceElementIs(currentStack[1], "java.lang.Object", "wait", false) - && stackTraceElementIs(currentStack[2], "java.awt.EventQueue", "getNextEvent", false); - } - /** * event事件的包装类 */ @@ -153,11 +93,13 @@ public final class EventDispatchThreadHangMonitor extends EventQueue { //事件开始的时间 private final long startDispatchTimeMillis = System.currentTimeMillis(); //事件编号 - private long hangNumber; + private final long hangNumber; + //构造函数,给当前对象赋一个递增的唯一编号 - public DispatchInfo() { + public DispatchInfo() { hangNumber = getHangCount(); } + //定时调度任务检测的入口,如果执行时间大于设定的值就进入examineHang()方法 public void checkForHang() { if (timeSoFar() > UNREASONABLE_DISPATCH_DURATION_MS) { @@ -168,36 +110,38 @@ public final class EventDispatchThreadHangMonitor extends EventQueue { private void examineHang() { //获取执行线程的当前堆栈 StackTraceElement[] currentStack = eventDispatchThread.getStackTrace(); - if (isWaitingForNextEvent(currentStack)) { + if (CartonUtils.isWaitingForNextEvent(currentStack)) { return; } //某个事件执行时间很长,定时处理时可能会连续打很多个堆栈,对同一个事件的相同堆栈只打一次 - if (lastReportedStack !=null && stacksEqual(lastReportedStack, currentStack)) { + if (lastReportedStack != null && CartonUtils.stacksEqual(lastReportedStack, currentStack)) { return; } - String stackTrace = stackTraceToString(currentStack); + String stackTrace = CartonUtils.stackTraceToString(currentStack); lastReportedStack = currentStack; JSONObject jsonObject = new JSONObject(); - jsonObject.put(com.fr.design.i18n.Toolkit.i18nText("Fine-Design_Basic_Carton_Output_Time"), simpleDateFormat.format(System.currentTimeMillis())); + jsonObject.put(com.fr.design.i18n.Toolkit.i18nText("Fine-Design_Basic_Carton_Output_Time"), DATE_FORMAT.format(System.currentTimeMillis())); jsonObject.put(com.fr.design.i18n.Toolkit.i18nText("Fine-Design_Basic_Carton_Event_Number"), "eventQueue_" + hangNumber); jsonObject.put(com.fr.design.i18n.Toolkit.i18nText("Fine-Design_Basic_Carton_Duration_Task_Execute"), timeSoFar() + "ms"); jsonObject.put(com.fr.design.i18n.Toolkit.i18nText("Fine-Design_Basic_Carton_Stack_Info"), stackTrace); - outPutJournalLog(jsonObject.toString(), SwitchForSwingChecker.TIMER_CHECK_FLAG); - checkForDeadlock(); + CartonUtils.outPutJournalLog(jsonObject.toString(), SwitchForSwingChecker.TIMER_CHECK_FLAG); + CartonUtils.checkForDeadlock(); } + //记录连续运行了多长时间 - private long timeSoFar() { + public long timeSoFar() { return (System.currentTimeMillis() - lastDispatchTimeMillis); } + //记录一个事件从被分发到结束的总运行时间 - private long totalTime() { + public long totalTime() { return (System.currentTimeMillis() - startDispatchTimeMillis); } //事件处理完后的时间判断 public void dispose() { if (timeSoFar() > UNREASONABLE_DISPATCH_DURATION_MS) { exportCartonLog(true); - } else if (lastReportedStack != null){ + } else if (lastReportedStack != null) { exportCartonLog(false); } } @@ -208,40 +152,15 @@ public final class EventDispatchThreadHangMonitor extends EventQueue { */ private void exportCartonLog(boolean flag) { JSONObject jsonObject = new JSONObject(); - jsonObject.put(com.fr.design.i18n.Toolkit.i18nText("Fine-Design_Basic_Carton_Output_Time"), simpleDateFormat.format(System.currentTimeMillis())); + jsonObject.put(com.fr.design.i18n.Toolkit.i18nText("Fine-Design_Basic_Carton_Output_Time"), DATE_FORMAT.format(System.currentTimeMillis())); jsonObject.put(com.fr.design.i18n.Toolkit.i18nText("Fine-Design_Basic_Carton_Event_Number"), "eventQueue_" + hangNumber); - jsonObject.put(com.fr.design.i18n.Toolkit.i18nText("Fine-Design_Basic_Carton_Task_Start_Time"), simpleDateFormat.format(startDispatchTimeMillis)); + jsonObject.put(com.fr.design.i18n.Toolkit.i18nText("Fine-Design_Basic_Carton_Task_Start_Time"), DATE_FORMAT.format(startDispatchTimeMillis)); if (flag) { jsonObject.put(com.fr.design.i18n.Toolkit.i18nText("Fine-Design_Basic_Carton_Task_Total_Time"), timeSoFar() + "ms"); } else { jsonObject.put(com.fr.design.i18n.Toolkit.i18nText("Fine-Design_Basic_Carton_Task_Total_Time"), totalTime() + "ms"); } - outPutJournalLog(jsonObject.toString(), SwitchForSwingChecker.EASY_CHECK_FLAG); - } - } - - public static void outPutJournalLog(String message, int flag) { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); - String date = simpleDateFormat.format(System.currentTimeMillis()); - String filename = flag == SwitchForSwingChecker.EASY_CHECK_FLAG ? SwitchForSwingChecker.EASY_CHECKER_FILE_NAME: SwitchForSwingChecker.TIMER_CHECKER_FILE_NAME; - String[] split = date.split("-"); - int month = StringUtils.isEmpty(split[1]) ? -1 : Integer.parseInt(split[1]); - String dirPath = StableUtils.pathJoin(JOURNAL_FILE_PATH, split[0], "month-" + month, date); - File dirFile = new File(dirPath); - File file = new File( StableUtils.pathJoin(dirPath, filename)); - try { - if (!file.exists()) { - if (!dirFile.exists()) { - dirFile.mkdirs(); - } - file.createNewFile(); - } - BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file, true)); - String outputMessage = new StringBuilder(message.replaceAll("~", "\r\n")).append(",").append("\r\n").toString(); - bufferedWriter.write(outputMessage); - bufferedWriter.close(); - } catch (IOException e) { - FineLoggerFactory.getLogger().error("output fail", e); + CartonUtils.outPutJournalLog(jsonObject.toString(), SwitchForSwingChecker.EASY_CHECK_FLAG); } } @@ -325,8 +244,7 @@ public final class EventDispatchThreadHangMonitor extends EventQueue { */ @Override protected void dispatchEvent(AWTEvent event) { - //如果两个开关都没开,那就不走重写方法了 - if (!isEasyWitch() && !isTimerWitch()) { + if (!useCustomEventQueue()) { super.dispatchEvent(event); } else { try { @@ -342,6 +260,18 @@ public final class EventDispatchThreadHangMonitor extends EventQueue { } } + private boolean useCustomEventQueue() { + // 开启性能监控或开启卡顿工具箱,则走自定义的EventQueue + return SwitchForSwingChecker.isLatencyMonitoring() || + isEasyWitch() || isTimerWitch(); + } + + private boolean needSampling() { + // UI性能采样逻辑:开启采样并且符合采样频次 + return SwitchForSwingChecker.isLatencyMonitoring() + && (hangCount % LATENCY_SAMPLING_FREQUENCY == 0); + } + /** * Starts tracking a dispatch. */ @@ -357,6 +287,9 @@ public final class EventDispatchThreadHangMonitor extends EventQueue { private synchronized void postDispatchEvent() { synchronized (dispatches) { DispatchInfo justFinishedDispatch = dispatches.removeLast(); + if (needSampling()) { + DesignerLatencyMetric.getInstance().record(justFinishedDispatch.timeSoFar()); + } if (isEasyWitch()) { justFinishedDispatch.dispose(); } @@ -370,39 +303,4 @@ public final class EventDispatchThreadHangMonitor extends EventQueue { } } - /** - * 检查死锁 - */ - public static void checkForDeadlock() { - ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); - long[] threadIds = threadBean.findDeadlockedThreads(); - if (threadIds == null) { - return; - } - FineLoggerFactory.getLogger().warn("deadlock detected involving the following threads:"); - ThreadInfo[] threadInfos = threadBean.getThreadInfo(threadIds, Integer.MAX_VALUE); - for (ThreadInfo info : threadInfos) { - FineLoggerFactory.getLogger().warn("Thread # {} {} ( {} ) waiting on {} held by {} {}", info.getThreadId(), info.getThreadName(), - info.getThreadState(), info.getLockName(), info.getLockOwnerName(), stackTraceToStringForConsole(info.getStackTrace())); - } - } - - public static String stackTraceToString(StackTraceElement[] stackTrace) { - StringBuilder result = new StringBuilder(); - for (StackTraceElement stackTraceElement : stackTrace) { - String indentation = " "; - result.append("~").append(indentation).append(stackTraceElement); - } - return result.toString(); - } - - public static String stackTraceToStringForConsole(StackTraceElement[] stackTrace) { - StringBuilder result = new StringBuilder(); - for (StackTraceElement stackTraceElement : stackTrace) { - String indentation = " "; - result.append("\r\n").append(indentation).append(stackTraceElement); - } - return result.toString(); - } - } \ No newline at end of file diff --git a/designer-base/src/main/java/com/fr/design/carton/FeedbackToolboxDialog.java b/designer-base/src/main/java/com/fr/design/carton/FeedbackToolboxDialog.java index 83d6e24ac8..328e47e8aa 100644 --- a/designer-base/src/main/java/com/fr/design/carton/FeedbackToolboxDialog.java +++ b/designer-base/src/main/java/com/fr/design/carton/FeedbackToolboxDialog.java @@ -49,6 +49,8 @@ import java.text.ParseException; import java.util.List; import java.util.Objects; +import static com.fr.design.carton.CartonConstants.JOURNAL_FILE_PATH; + public class FeedbackToolboxDialog extends JDialog { private UIDatePicker uiDatePicker; @@ -147,7 +149,7 @@ public class FeedbackToolboxDialog extends JDialog { //selectDate 2002-03-09例子 String[] split = selectDate.split("-"); int month = Integer.parseInt(split[1]); - String sourceFilePath = StableUtils.pathJoin(SwitchForSwingChecker.JOURNAL_FILE_PATH, split[0], "month-" + month, selectDate); + String sourceFilePath = StableUtils.pathJoin(JOURNAL_FILE_PATH, split[0], "month-" + month, selectDate); File sourceFile = new File(sourceFilePath); if (sourceFile.exists()) { exportCartonLog(sourceFile, path, sourceFilePath); diff --git a/designer-base/src/main/java/com/fr/design/carton/SwitchForSwingChecker.java b/designer-base/src/main/java/com/fr/design/carton/SwitchForSwingChecker.java index 683179293e..758017465d 100644 --- a/designer-base/src/main/java/com/fr/design/carton/SwitchForSwingChecker.java +++ b/designer-base/src/main/java/com/fr/design/carton/SwitchForSwingChecker.java @@ -7,7 +7,6 @@ import com.fr.json.JSON; import com.fr.json.JSONArray; import com.fr.json.JSONObject; import com.fr.log.FineLoggerFactory; -import com.fr.stable.ProductConstantsBase; import com.fr.stable.StableUtils; import com.fr.stable.StringUtils; import com.fr.stable.xml.XMLPrintWriter; @@ -29,11 +28,13 @@ import java.util.Map; import java.util.Date; import java.util.Calendar; +import static com.fr.design.carton.CartonConstants.DEBUG_MAIN_CLASS_NAME; +import static com.fr.design.carton.CartonConstants.EASY_CHECKER_FILE_NAME; +import static com.fr.design.carton.CartonConstants.JOURNAL_FILE_PATH; +import static com.fr.design.carton.CartonConstants.TIMER_CHECKER_FILE_NAME; + public class SwitchForSwingChecker implements XMLReadable, XMLWriter { - /** - * Designer4Debug类名 - */ - private static final String DEBUG_MAIN_CLASS_NAME = "com.fr.start.Designer4Debug"; + /** * XML标签 */ @@ -46,18 +47,16 @@ public class SwitchForSwingChecker implements XMLReadable, XMLWriter { * 简单记录事件执行时间的开关 */ private static boolean easyChecker = false; + /** + * UI性能监控埋点的开关 + */ + private static boolean latencyMonitor = true; /** * 一个标识位用于区分耗时任务时长检测(简单检测)和timer检测 */ public static final int TIMER_CHECK_FLAG = 0; public static final int EASY_CHECK_FLAG = 1; - /** - * 日志存储地址 - */ - public static final String JOURNAL_FILE_PATH = StableUtils.pathJoin(ProductConstantsBase.getEnvHome(), "journal_log"); - public static final String EASY_CHECKER_FILE_NAME = "easy_check_log.csv"; - public static final String TIMER_CHECKER_FILE_NAME = "timer_check_log.csv"; public static boolean isCheckerTimerSwitch() { return checkerTimerSwitch; } @@ -66,6 +65,13 @@ public class SwitchForSwingChecker implements XMLReadable, XMLWriter { return easyChecker; } + /** + * 是否开启UI性能检测 + */ + public static boolean isLatencyMonitoring() { + return latencyMonitor; + } + public static volatile SwitchForSwingChecker switchForSwingChecker = new SwitchForSwingChecker(); public static SwitchForSwingChecker getInstance() { @@ -137,7 +143,7 @@ public class SwitchForSwingChecker implements XMLReadable, XMLWriter { } /** - *处理文件 + * 处理文件 * 一共四种情况, * 两个文件都不存在 * 文件一存在,文件二不存在 @@ -224,7 +230,7 @@ public class SwitchForSwingChecker implements XMLReadable, XMLWriter { /** * /埋点方法上传卡顿信息入口 - date为 2022-09-08的格式 + * date为 2022-09-08的格式 */ public static List uploadJournalLog(Date dateTime) { List res = new ArrayList<>(); @@ -245,9 +251,8 @@ public class SwitchForSwingChecker implements XMLReadable, XMLWriter { /** * 初始化监控任务,主要是替换EventQueue以及SwingWorker执行任务的线程池 - * */ - public static void initThreadMonitoring () { + public static void initThreadMonitoring() { String mainClass = System.getProperty("sun.java.command"); //判断一下,如果是以Designer4Debug启动,就不注册代码,不然会覆盖掉SwingExplorer,导致其无法使用 if (!StringUtils.equals(mainClass, DEBUG_MAIN_CLASS_NAME)) { @@ -293,6 +298,7 @@ public class SwitchForSwingChecker implements XMLReadable, XMLWriter { if (reader.isAttr()) { checkerTimerSwitch = reader.getAttrAsBoolean("checkerTimerSwitch", false); easyChecker = reader.getAttrAsBoolean("easyChecker", false); + latencyMonitor = reader.getAttrAsBoolean("latencyMonitor", true); } try { initSwitchChecker(); @@ -306,6 +312,7 @@ public class SwitchForSwingChecker implements XMLReadable, XMLWriter { writer.startTAG(XML_TAG); writer.attr("checkerTimerSwitch", checkerTimerSwitch); writer.attr("easyChecker", easyChecker); + writer.attr("latencyMonitor", latencyMonitor); writer.end(); } diff --git a/designer-base/src/main/java/com/fr/design/carton/latency/DesignerLatencyMetric.java b/designer-base/src/main/java/com/fr/design/carton/latency/DesignerLatencyMetric.java new file mode 100644 index 0000000000..d98e26c198 --- /dev/null +++ b/designer-base/src/main/java/com/fr/design/carton/latency/DesignerLatencyMetric.java @@ -0,0 +1,155 @@ +package com.fr.design.carton.latency; + +import com.fanruan.third.v2.org.ehcache.impl.internal.concurrent.ConcurrentHashMap; +import com.fr.concurrent.FineExecutors; +import com.fr.concurrent.NamedThreadFactory; +import com.fr.config.MarketConfig; +import com.fr.design.DesignerEnvManager; +import com.fr.design.carton.SwitchForSwingChecker; +import com.fr.design.mainframe.SiteCenterToken; +import com.fr.event.Event; +import com.fr.event.EventDispatcher; +import com.fr.event.Listener; +import com.fr.general.http.HttpToolbox; +import com.fr.json.JSONObject; +import com.fr.stable.ProductConstants; +import com.fr.workspace.WorkContext; +import com.fr.workspace.Workspace; +import com.fr.workspace.WorkspaceEvent; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.fr.design.carton.CartonConstants.APPID; +import static com.fr.design.carton.CartonConstants.DESIGNER_ID; +import static com.fr.design.carton.CartonConstants.DESIGNER_VERSION; +import static com.fr.design.carton.CartonConstants.DESIGN_METHOD; +import static com.fr.design.carton.CartonConstants.LOCAL; +import static com.fr.design.carton.CartonConstants.OPERANDS_NUM; +import static com.fr.design.carton.CartonConstants.REMOTE; +import static com.fr.design.carton.CartonConstants.TIME; +import static com.fr.design.carton.CartonConstants.USERID; + +/** + * 设计器延迟时间记录Metric + * + * @author Levy.Xie + * @since 11.0 + * Created on 2024/07/01 + */ +public class DesignerLatencyMetric { + + private ExecutorService executorService; + private ScheduledExecutorService scheduler; + private static final Map LATENCY_CONTAINER = new ConcurrentHashMap<>(); + + private static final String LATENCY_INFO_URL = "https://cloud.fanruan.com/api/monitor/record_of_deisgner_latency/single"; + + private final static class InstanceHolder { + static final DesignerLatencyMetric INSTANCE = new DesignerLatencyMetric(); + } + + /** + * 获取单例 + */ + public static DesignerLatencyMetric getInstance() { + return DesignerLatencyMetric.InstanceHolder.INSTANCE; + } + + private DesignerLatencyMetric() { + } + + /** + * 启动 + */ + public void start() { + if (SwitchForSwingChecker.isLatencyMonitoring()) { + // 初始化容器 + initializeContainer(); + // 启动异步性能记录线程池 + executorService = FineExecutors.newCachedThreadPool( + new NamedThreadFactory(DesignerLatencyMetric.class)); + // 启动定时埋点 + this.scheduler = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("LatencyMetricWorker")); + this.scheduler.scheduleWithFixedDelay(this::collectAndSubmit, 60, 60, TimeUnit.SECONDS); + // 注册设计器工作目录切换事件监听 + EventDispatcher.listen(WorkspaceEvent.AfterSwitch, new Listener() { + @Override + public void on(Event event, Workspace param) { + collectAndSubmit(); + } + }); + } + } + + /** + * 关闭 + */ + public void stop() { + if (SwitchForSwingChecker.isLatencyMonitoring()) { + this.executorService.shutdown(); + this.scheduler.shutdown(); + collectAndSubmit(); + } + } + + private void initializeContainer() { + for (LatencyLevel level : LatencyLevel.values()) { + LATENCY_CONTAINER.put(level, new AtomicInteger()); + } + } + + private void resetContainer() { + LATENCY_CONTAINER.values().forEach(count -> count.set(0)); + } + + /** + * 记录性能信息 + */ + public void record(long cost) { + executorService.submit(() -> { + try { + LatencyLevel level = LatencyLevel.measure(cost); + LATENCY_CONTAINER.computeIfAbsent(level, k -> new AtomicInteger()).incrementAndGet(); + } catch (Throwable ignore) { + // 记录失败不影响业务 + } + }); + } + + /** + * 汇总并提交性能监控埋点 + */ + public void collectAndSubmit() { + Map para = new HashMap<>(); + para.put("token", SiteCenterToken.generateToken()); + para.put("content", collect()); + try { + HttpToolbox.post(LATENCY_INFO_URL, para); + } catch (Throwable ignore) { + // doNothing + } + resetContainer(); + } + + private JSONObject collect() { + JSONObject info = new JSONObject(); + info.put(TIME, System.currentTimeMillis()); + info.put(APPID, MarketConfig.getInstance().getCloudOperationMaintenanceId()); + info.put(USERID, MarketConfig.getInstance().getBbsUid()); + info.put(DESIGNER_ID, DesignerEnvManager.getEnvManager().getUUID()); + info.put(DESIGNER_VERSION, ProductConstants.DESIGNER_VERSION); + info.put(DESIGN_METHOD, WorkContext.getCurrent().isLocal() ? LOCAL : REMOTE); + info.put(OPERANDS_NUM, LATENCY_CONTAINER.values().stream().mapToInt(AtomicInteger::get).sum()); + for (Map.Entry entry : LATENCY_CONTAINER.entrySet()) { + info.put(entry.getKey().getMark(), entry.getValue().get()); + } + return info; + } + +} diff --git a/designer-base/src/main/java/com/fr/design/carton/latency/LatencyLevel.java b/designer-base/src/main/java/com/fr/design/carton/latency/LatencyLevel.java new file mode 100644 index 0000000000..0d40b37d89 --- /dev/null +++ b/designer-base/src/main/java/com/fr/design/carton/latency/LatencyLevel.java @@ -0,0 +1,67 @@ +package com.fr.design.carton.latency; + +/** + * 卡顿等级 + * + * @author Levy.Xie + * @since 11.0 + * Created on 2024/07/01 + */ +public enum LatencyLevel { + + // 非常流畅 + FLASH(0, 50, "waitNum1"), + // 流畅 + SMOOTH(50, 100, "waitNum2"), + // 轻微卡顿 + SLIGHT(100, 200, "waitNum3"), + // 中等卡顿 + MILD(200, 500, "waitNum4"), + // 明显卡顿 + NOTICEABLE(500, 1000, "waitNum5"), + // 严重卡顿 + SERVE(1000, 2000, "waitNum6"), + // 非常严重卡顿 + EXTREME(2000, 3000, "waitNum7"), + // 极度卡顿 + CRITICAL(3000, Long.MAX_VALUE, "waitNum8"), + // 未知场景 + UNDEFINE(-1, -1, "unknown"); + + final long start; + final long end; + final String mark; + + LatencyLevel(long start, long end, String mark) { + this.start = start; + this.end = end; + this.mark = mark; + } + + public long getStart() { + return start; + } + + public long getEnd() { + return end; + } + + public String getMark() { + return mark; + } + + /** + * 评估当前卡顿等级 + * + * @param cost UI-EventQueue响应耗时 + * @return 卡顿等级 + */ + public static LatencyLevel measure(long cost) { + for (LatencyLevel level : LatencyLevel.values()) { + if (cost >= level.getStart() && cost < level.getEnd()) { + return level; + } + } + return UNDEFINE; + } +} diff --git a/designer-base/src/main/java/com/fr/design/mainframe/DesignerFrame.java b/designer-base/src/main/java/com/fr/design/mainframe/DesignerFrame.java index afa1a3bcb2..e20debacb7 100644 --- a/designer-base/src/main/java/com/fr/design/mainframe/DesignerFrame.java +++ b/designer-base/src/main/java/com/fr/design/mainframe/DesignerFrame.java @@ -11,6 +11,7 @@ import com.fr.design.ExtraDesignClassManager; import com.fr.design.actions.core.ActionFactory; import com.fr.design.base.mode.DesignModeContext; import com.fr.design.base.mode.DesignerMode; +import com.fr.design.carton.latency.DesignerLatencyMetric; import com.fr.design.constants.UIConstants; import com.fr.design.data.DesignTableDataManager; import com.fr.design.data.datapane.TableDataTreePane; @@ -1161,6 +1162,7 @@ public class DesignerFrame extends JFrame implements JTemplateActionListener, Ta }); DesignerEnvManager.getEnvManager().saveXMLFile(); + DesignerLatencyMetric.getInstance().stop(); } /** diff --git a/designer-realize/src/main/java/com/fr/start/MainDesigner.java b/designer-realize/src/main/java/com/fr/start/MainDesigner.java index ee25802846..dab50eb3ce 100644 --- a/designer-realize/src/main/java/com/fr/start/MainDesigner.java +++ b/designer-realize/src/main/java/com/fr/start/MainDesigner.java @@ -11,6 +11,7 @@ import com.fr.design.actions.server.ServerConfigManagerAction; import com.fr.design.actions.server.TemplateThemeManagerAction; import com.fr.design.actions.server.WidgetManagerAction; import com.fr.design.base.mode.DesignModeContext; +import com.fr.design.carton.latency.DesignerLatencyMetric; import com.fr.design.carton.SwitchForSwingChecker; import com.fr.design.constants.DesignerLaunchStatus; import com.fr.design.constants.UIConstants; @@ -162,6 +163,7 @@ public class MainDesigner extends BaseDesigner { FineLoggerFactory.getLogger().info("Designer started.Time used {} ms", DesignerStartupContext.getRecorder().getTime(TimeUnit.MILLISECONDS)); SwitchForSwingChecker.initThreadMonitoring(); + DesignerLatencyMetric.getInstance().start(); } /** From 727a94e47554c9897720a01a62e129c89aac73fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Levy=2EXie-=E8=A7=A3=E5=AE=89=E6=A3=AE?= Date: Mon, 15 Jul 2024 17:26:24 +0800 Subject: [PATCH 2/2] =?UTF-8?q?REPORT-117002=20feat:=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E5=99=A8UI=E6=80=A7=E8=83=BD=E7=9B=91=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/fr/design/carton/CartonUtils.java | 8 ++++---- .../fr/design/carton/latency/DesignerLatencyMetric.java | 8 ++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/designer-base/src/main/java/com/fr/design/carton/CartonUtils.java b/designer-base/src/main/java/com/fr/design/carton/CartonUtils.java index b67fe8c504..df8361662b 100644 --- a/designer-base/src/main/java/com/fr/design/carton/CartonUtils.java +++ b/designer-base/src/main/java/com/fr/design/carton/CartonUtils.java @@ -116,10 +116,10 @@ public class CartonUtils { } file.createNewFile(); } - BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file, true)); - String outputMessage = message.replaceAll("~", "\r\n") + "," + "\r\n"; - bufferedWriter.write(outputMessage); - bufferedWriter.close(); + try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file, true))) { + String outputMessage = message.replaceAll("~", "\r\n") + "," + "\r\n"; + bufferedWriter.write(outputMessage); + } } catch (IOException e) { FineLoggerFactory.getLogger().error("output fail", e); } diff --git a/designer-base/src/main/java/com/fr/design/carton/latency/DesignerLatencyMetric.java b/designer-base/src/main/java/com/fr/design/carton/latency/DesignerLatencyMetric.java index d98e26c198..766ff17dba 100644 --- a/designer-base/src/main/java/com/fr/design/carton/latency/DesignerLatencyMetric.java +++ b/designer-base/src/main/java/com/fr/design/carton/latency/DesignerLatencyMetric.java @@ -92,8 +92,12 @@ public class DesignerLatencyMetric { */ public void stop() { if (SwitchForSwingChecker.isLatencyMonitoring()) { - this.executorService.shutdown(); - this.scheduler.shutdown(); + if (this.executorService != null) { + this.executorService.shutdown(); + } + if (this.scheduler != null) { + this.scheduler.shutdown(); + } collectAndSubmit(); } }