Browse Source
* commit '236567d204c9113b930da3d53b7ec38e884d0abd': REPORT-128652 开启模板认证后,新建fvs模板报错。 【问题原因】主jar修改了sessionID相关的校验,导致fvs插件的部分接口无法通过校验。 【改动思路】更新jar强制FVS插件升级,通过提高fvs独占接口api level实现 REPORT-125641 fix:设计器显示问题-英语显示不全 REPORT-128068 fix:设计器按钮显示不全且无tooltips REPORT-111995 【NewUI】文件-选项和帮助面板翻新 REPORT-125224 fix:补充设计器启动计时器关闭 REPORT-125241 fix:修复模板从内层目录移动到最外层目录时显示异常问题 REPORT-127241 设计器性能埋点字段调整 REPORT-117002 设计器性能埋点监控 调整代码 REPORT-117002 设计器性能埋点监控 调整代码 无jira 打包问题 REPORT-77878 fix: fvs插件-表格默认字体颜色是白色,但是右键清除单元格格式,字体会变为黑色 REPORT-70155 fix: fvs根据主题样式创建默认单元格 REPORT-117002 feat:设计器UI性能监控 REPORT-117002 feat:设计器UI性能监控persist/11.0-arabic
superman
4 months ago
21 changed files with 556 additions and 175 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