Levy.Xie-解安森
5 months ago
47 changed files with 725 additions and 239 deletions
@ -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"; |
||||
|
||||
|
||||
} |
@ -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(); |
||||
} |
||||
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); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 用于判断是不是特定的堆栈 |
||||
*/ |
||||
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); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,174 @@
|
||||
package com.fr.design.carton.latency; |
||||
|
||||
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.CloudCenter; |
||||
import com.fr.general.GeneralUtils; |
||||
import com.fr.general.http.HttpToolbox; |
||||
import com.fr.json.JSONObject; |
||||
import com.fr.log.FineLoggerFactory; |
||||
import com.fr.stable.StringUtils; |
||||
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.ConcurrentHashMap; |
||||
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 String latencyUrl; |
||||
private ExecutorService executorService; |
||||
private ScheduledExecutorService scheduler; |
||||
private static final Map<LatencyLevel, AtomicInteger> LATENCY_CONTAINER = new ConcurrentHashMap<>(); |
||||
|
||||
private static final String DEFAULT_MONITOR_URL = "https://cloud.fanruan.com/api/monitor/"; |
||||
private static final String LATENCY_TABLE_SUFFIX = "record_of_designer_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 = Executors.newFixedThreadPool(8); |
||||
// 启动定时埋点
|
||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("LatencyMetricWorker")); |
||||
this.scheduler.scheduleWithFixedDelay(this::collectAndSubmit, 60, 60, TimeUnit.MINUTES); |
||||
// 注册设计器工作目录切换事件监听
|
||||
EventDispatcher.listen(WorkspaceEvent.BeforeSwitch, new Listener<Workspace>() { |
||||
@Override |
||||
public void on(Event event, Workspace param) { |
||||
collectAndSubmit(); |
||||
} |
||||
}); |
||||
FineLoggerFactory.getLogger().info("[Latency] designer latency metric started."); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 关闭 |
||||
*/ |
||||
public void stop() { |
||||
if (SwitchForSwingChecker.isLatencyMonitoring()) { |
||||
if (this.executorService != null) { |
||||
this.executorService.shutdown(); |
||||
} |
||||
if (this.scheduler != null) { |
||||
this.scheduler.shutdown(); |
||||
} |
||||
collectAndSubmit(); |
||||
FineLoggerFactory.getLogger().info("[Latency] designer latency metric stopped."); |
||||
} |
||||
} |
||||
|
||||
private String getLatencyUrl() { |
||||
if (StringUtils.isEmpty(latencyUrl)) { |
||||
String monitorEntry = CloudCenter.getInstance().acquireUrlByKind("cloud.monitor.api.entrypoint"); |
||||
latencyUrl = (StringUtils.isNotEmpty(monitorEntry) ? monitorEntry : DEFAULT_MONITOR_URL) |
||||
+ LATENCY_TABLE_SUFFIX; |
||||
} |
||||
return latencyUrl; |
||||
} |
||||
|
||||
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<String, Object> para = new HashMap<>(); |
||||
para.put("token", SiteCenterToken.generateToken()); |
||||
para.put("content", collect()); |
||||
try { |
||||
HttpToolbox.post(getLatencyUrl(), para); |
||||
FineLoggerFactory.getLogger().debug("[Latency] submit latency log to cloud."); |
||||
} catch (Throwable t) { |
||||
FineLoggerFactory.getLogger().debug(t,"[Latency] failed to submit latency log to cloud."); |
||||
} |
||||
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, GeneralUtils.getVersion()); |
||||
info.put(DESIGN_METHOD, WorkContext.getCurrent().isLocal() ? LOCAL : REMOTE); |
||||
info.put(OPERANDS_NUM, LATENCY_CONTAINER.values().stream().mapToInt(AtomicInteger::get).sum()); |
||||
for (Map.Entry<LatencyLevel, AtomicInteger> entry : LATENCY_CONTAINER.entrySet()) { |
||||
info.put(entry.getKey().getMark(), entry.getValue().get()); |
||||
} |
||||
return info; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,65 @@
|
||||
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"); |
||||
|
||||
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 CRITICAL; |
||||
} |
||||
} |
Loading…
Reference in new issue