1. **==Major Optimization==**: Resolved the potential stuck issue when Obsidian switches back to the foreground, improved the timer performance.
This commit is contained in:
Carrianna 2025-09-12 19:54:47 +08:00
parent f4f8867663
commit b50ba91ec8
4 changed files with 332 additions and 18 deletions

View file

@ -4,6 +4,9 @@
A text block timing tool designed for Obsidian, allowing you to add a **count-up stopwatch** function to any text line in your notes.
## Latest Change
**==Major Optimization==**: Resolved the potential stuck issue when Obsidian switches back to the foreground, improved the timer performance.
## Features
![Demo](https://github.com/wth461694678/text-block-timer/blob/main/demo/demo.gif)
@ -83,6 +86,8 @@ In the default settings, path control is disabled. When you only want to use the
![pathControl_black](https://github.com/wth461694678/text-block-timer/blob/main/demo/pathControl_black.gif)
## Version Log:
- V1.0.7
1. **==Major Optimization==**: Resolved the potential stuck issue when Obsidian switches back to the foreground, improved the timer performance.
- V1.0.6
1. BUG Fix: Resolved the issue of startup failure caused by incorrect language configuration code
- V1.0.5
@ -109,4 +114,4 @@ In the default settings, path control is disabled. When you only want to use the
## Development Information
- Developer: frankthwang
- Version: 1.0.6
- Version: 1.0.7

View file

@ -4,6 +4,9 @@
一个为Obsidian设计的文本块计时工具可以在笔记中为任意文本行添加**正计时**功能。
## 最新变更
**==重大优化==**解决obsidian切回前台时可能出现的卡顿问题,显著提升了该场景的性能。
## 功能特性
![Demo](https://github.com/wth461694678/text-block-timer/blob/main/demo/demo.gif)
@ -91,6 +94,8 @@
## 版本日志:
- V1.0.7
1. **==重大优化==**解决obsidian切回前台时可能出现的卡顿问题,显著提升了该场景的性能。
- V1.0.6
1. BUG修复解决了语言配置代码错误导致启动失败问题
- V1.0.5
@ -117,4 +122,4 @@
## 开发信息
- 开发者: frankthwang
- 版本: 1.0.6
- 版本: 1.0.7

329
main.js
View file

@ -16,7 +16,237 @@ const CHECKBOX_REGEX = /^(?:(\s*)(?:[-+*]|\d+\.)\s+)\[(.{1})\]\s+/
// —— Base62 工具函数 —— //
const BASE62_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
const DEBUG = false;
const DEBUG = true;
// Performance monitor: lightweight sync/async timing collector
class PerfMonitor {
constructor() {
this.enabled = false;
this.data = new Map(); // name -> { count, total }
this.sessionName = null;
this.startTime = 0;
this.endTime = 0;
// 调用间隔监控
this.intervalTracking = false;
this.intervals = new Map(); // methodName -> { lastCallTime, gaps: [] }
}
startSession() {
this.enabled = true;
this.startTime = Date.now();
// 输出简短的使用方法说明
console.log('[PerfMonitor] 使用方法:');
console.log('• PerfMonitorStart() - 开始监控');
console.log('• PerfMonitorStop() - 停止监控并查看报告');
// 自动包装核心类
PerfMonitor.wrapCoreClasses();
}
stopSession() {
this.enabled = false;
this.endTime = Date.now();
console.log(`[PerfMonitor] session stopped, duration ${this.endTime - this.startTime} ms`);
this.print();
// 自动重置数据(静默重置,不输出日志)
this.reset(true);
}
reset(silent = false) {
this.data.clear();
this.intervals.clear();
if (!silent) {
console.log('[PerfMonitor] reset');
}
}
// 启用调用间隔监控
enableIntervalTracking() {
this.intervalTracking = true;
console.log('[PerfMonitor] interval tracking enabled');
}
// 禁用调用间隔监控
disableIntervalTracking() {
this.intervalTracking = false;
console.log('[PerfMonitor] interval tracking disabled');
}
// 记录调用间隔
recordInterval(name) {
if (!this.intervalTracking) return;
const now = Date.now();
if (!this.intervals.has(name)) {
this.intervals.set(name, { lastCallTime: now, gaps: [] });
return;
}
const interval = this.intervals.get(name);
const gap = now - interval.lastCallTime;
interval.gaps.push(gap);
interval.lastCallTime = now;
// 检测异常间隔超过5秒
if (gap > 5000) {
console.warn(`[PerfMonitor] ${name} 异常间隔: ${gap}ms可能是后台节流恢复`);
}
}
// 打印间隔统计
printIntervals() {
if (!this.intervalTracking) {
console.log('[PerfMonitor] interval tracking not enabled');
return;
}
console.group('[PerfMonitor] Interval Analysis');
this.intervals.forEach((data, name) => {
if (data.gaps.length === 0) {
console.log(`${name}: no intervals recorded`);
return;
}
const gaps = data.gaps;
const min = Math.min(...gaps);
const max = Math.max(...gaps);
const avg = Math.round(gaps.reduce((a, b) => a + b, 0) / gaps.length);
const abnormal = gaps.filter(g => g > 5000).length;
console.log(`${name}: ${gaps.length} intervals, min ${min}ms, max ${max}ms, avg ${avg}ms, abnormal(>5s) ${abnormal}`);
});
console.groupEnd();
}
record(name, ms) {
if (!this.data.has(name)) this.data.set(name, { count: 0, total: 0, max: 0 });
const v = this.data.get(name);
v.count += 1;
v.total += ms;
if (ms > v.max) v.max = ms;
}
// helper: time a synchronous function
timeSync(name, fn, ...args) {
const t0 = Date.now();
const res = fn(...args);
const t1 = Date.now();
this.record(name, t1 - t0);
return res;
}
// helper: time an async function (returns promise)
timeAsync(name, fn, ...args) {
const t0 = Date.now();
return Promise.resolve(fn(...args)).then((r) => {
this.record(name, Date.now() - t0);
return r;
});
}
print() {
const rows = Array.from(this.data.entries()).map(([k, v]) => ({ k, count: v.count, total: v.total, avg: Math.round(v.total / v.count), max: v.max }));
rows.sort((a, b) => b.total - a.total);
console.group('[PerfMonitor] Summary');
rows.forEach(r => console.log(`${r.k}: ${r.total}ms over ${r.count} calls (avg ${r.avg}ms, max ${r.max}ms)`));
console.groupEnd();
}
// wrap a method (object can be prototype or class/static object)
wrapMethod(object, methodName, displayName = null) {
if (!object) return;
const orig = object[methodName];
if (!orig || orig._perfmonitor_wrapped) return;
const name = displayName || `${object.constructor && object.constructor.name ? object.constructor.name : 'obj'}.${methodName}`;
const self = this;
object[methodName] = function(...args) {
if (!window.__PerfMonitor || !window.__PerfMonitor.enabled) return orig.apply(this, args);
// 记录调用间隔
self.recordInterval(name);
const t0 = Date.now();
try {
const res = orig.apply(this, args);
if (res && typeof res.then === 'function') {
return res.then(r => {
self.record(name, Date.now() - t0);
return r;
}).catch(e => {
self.record(name, Date.now() - t0);
throw e;
});
} else {
self.record(name, Date.now() - t0);
return res;
}
} catch (err) {
self.record(name, Date.now() - t0);
throw err;
}
};
object[methodName]._perfmonitor_wrapped = true;
}
// convenience: wrap multiple methods on an object
wrapMethods(object, methodNames, prefix = '') {
for (const m of methodNames) this.wrapMethod(object, m, prefix ? `${prefix}.${m}` : m);
}
// 静态方法初始化全局性能监控仅在DEBUG模式下
static initGlobalMonitor() {
if (!window.__PerfMonitor) {
window.__PerfMonitor = new PerfMonitor();
// 暴露简化的控制函数到全局
window.PerfMonitorStart = () => window.__PerfMonitor.startSession();
window.PerfMonitorStop = () => window.__PerfMonitor.stopSession();
window.PerfMonitorPrint = () => window.__PerfMonitor.print();
// 调用间隔监控控制
window.PerfMonitorIntervalStart = () => window.__PerfMonitor.enableIntervalTracking();
window.PerfMonitorIntervalStop = () => window.__PerfMonitor.disableIntervalTracking();
window.PerfMonitorIntervalPrint = () => window.__PerfMonitor.printIntervals();
console.log('[PerfMonitor] Debug mode enabled. Use PerfMonitorStart() to begin monitoring.');
}
}
// 静态方法:包装核心类
static wrapCoreClasses() {
if (!DEBUG || !window.__PerfMonitor) return;
try {
// TimerManager
if (typeof TimerManager !== 'undefined') {
window.__PerfMonitor.wrapMethods(TimerManager.prototype, ['startTimer', 'stopTimer', 'updateTimerData', 'getTimerData', 'hasTimer', 'clearAll'], 'TimerManager');
}
if (typeof TimerFileManager !== 'undefined') {
window.__PerfMonitor.wrapMethods(TimerFileManager.prototype, ['writeTimer', 'updateTimerByIdWithSearch', 'findTimerGlobally', 'calculateInsertPosition', 'upgradeOldTimers'], 'TimerFileManager');
}
if (typeof TimerParser !== 'undefined') {
window.__PerfMonitor.wrapMethods(TimerParser, ['parse', 'parseOldFormat', 'parseNewFormat'], 'TimerParser');
}
if (typeof TimerRenderer !== 'undefined') {
window.__PerfMonitor.wrapMethods(TimerRenderer, ['render'], 'TimerRenderer');
}
if (typeof TimerDataUpdater !== 'undefined') {
window.__PerfMonitor.wrapMethods(TimerDataUpdater, ['calculate'], 'TimerDataUpdater');
}
// TimerPlugin methods - wrap prototype methods after class is defined
if (typeof TimerPlugin !== 'undefined') {
window.__PerfMonitor.wrapMethods(TimerPlugin.prototype, ['onload', 'onunload', 'onEditorMenu', 'onFileOpen', 'restoreTimers', 'handleStart', 'handlePause', 'handleContinue', 'handleDelete', 'onTick'], 'TimerPlugin');
}
// console.log('[PerfMonitor] Core classes wrapped. Start session with PerfMonitorStart()');
} catch (e) {
console.error('[PerfMonitor] Wrap failed:', e);
}
}
}
function compressId(timestamp) {
if (!timestamp) timestamp = Date.now();
@ -114,7 +344,7 @@ const TRANSLATIONS = {
desc: "配置计时器行为",
tutorial: "更多图片和视频教程,请访问:",
askforvote: "如果你喜欢这款插件请为我的Github项目点个Star🌟",
issue: "如果有任何问题或建议可以在Github项目中提出Issue并注明使用的插件版本和复现步骤",
issue: "如果有任何问题或建议可以在Github项目中提出Issue并注明使用的插件版本和复现步骤中国用户遇到阻断BUG可以直接联系微信Franklin_wth",
sections: {
basic: { name: "通用设置" },
bycommand: {
@ -458,6 +688,52 @@ class TimerManager {
constructor() {
this.timers = new Map(); // timerId -> { intervalId, data }
this.startedIds = new Set(); // 记录本次onload以来所有启动过的timerId
this.runningTicks = new Set(); // 记录正在执行onTick的timerId防止重叠执行
// 页面可见性监控
this.isVisible = !document.hidden;
this.lastVisibleTime = Date.now();
this.backgroundThreshold = 1000; // 1分钟 = 60000ms
this.setupVisibilityMonitor();
}
setupVisibilityMonitor() {
const handleVisibilityChange = () => {
this.isVisible = !document.hidden;
if (this.isVisible) {
this.lastVisibleTime = Date.now();
if (DEBUG) {
console.log('[TimerManager] 页面回到前台');
}
} else {
if (DEBUG) {
console.log('[TimerManager] 页面进入后台');
}
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
// 记录初始状态
if (this.isVisible) {
this.lastVisibleTime = Date.now();
}
}
// 检查是否应该跳过onTick后台超过1分钟
shouldSkipTick() {
if (this.isVisible) {
return false; // 前台时不跳过
}
const backgroundDuration = Date.now() - this.lastVisibleTime;
const shouldSkip = backgroundDuration > this.backgroundThreshold;
if (shouldSkip && DEBUG) {
console.log(`[TimerManager] 后台时间 ${Math.round(backgroundDuration/1000)}s跳过onTick`);
}
return shouldSkip;
}
startTimer(timerId, initialData, tickCallback) {
@ -528,6 +804,7 @@ class TimerManager {
});
this.timers.clear();
this.startedIds.clear();
this.runningTicks.clear();
}
}
@ -889,6 +1166,8 @@ class TimerParser {
// —— Main plugin class —— //
class TimerPlugin extends obsidian.Plugin {
async onload() {
PerfMonitor.initGlobalMonitor();
this.manager = new TimerManager();
this.fileFirstOpen = true;
@ -1222,20 +1501,43 @@ class TimerPlugin extends obsidian.Plugin {
}
async onTick(timerId) {
// 可见性检查后台超过1分钟时跳过
if (this.manager.shouldSkipTick()) {
if (DEBUG) {
console.log(`页面后台超过1分钟跳过 onTick: ${timerId}`);
}
return;
}
// 1. Get and update data
const oldData = this.manager.getTimerData(timerId);
if (!oldData || oldData.class !== 'timer-r') return;
// 防重叠执行如果该timer的onTick正在执行跳过本次调用
if (this.manager.runningTicks.has(timerId)) {
if (DEBUG) {
console.log(`跳过重叠的 onTick: ${timerId}`);
}
return;
}
const newData = TimerDataUpdater.calculate('update', oldData);
this.manager.updateTimerData(timerId, newData);
// 标记开始执行
this.manager.runningTicks.add(timerId);
// 2. Update file (includes search logic)
const updated = await this.fileManager.updateTimerByIdWithSearch(timerId, newData);
try {
// 1. Get and update data
const oldData = this.manager.getTimerData(timerId);
if (!oldData || oldData.class !== 'timer-r') return;
if (!updated) {
// Timer no longer exists, stop it
this.manager.stopTimer(timerId);
const newData = TimerDataUpdater.calculate('update', oldData);
this.manager.updateTimerData(timerId, newData);
// 2. Update file (includes search logic)
const updated = await this.fileManager.updateTimerByIdWithSearch(timerId, newData);
if (!updated) {
// Timer no longer exists, stop it
this.manager.stopTimer(timerId);
}
} finally {
// 确保无论成功还是异常都会清理执行标记
this.manager.runningTicks.delete(timerId);
}
}
@ -1574,4 +1876,5 @@ class TimerSettingTab extends obsidian.PluginSettingTab {
}
}
module.exports = TimerPlugin;
module.exports = TimerPlugin;
/* nosourcemap */

View file

@ -1,9 +1,10 @@
{
"id": "text-block-timer",
"version": "1.0.6",
"version": "1.0.7",
"minAppVersion": "1.8.0",
"name": "Text Block Timer",
"author": "frankthwang",
"description": "Add a timer to text block to enable count-up timing.",
"isDesktopOnly": false
"description": "Add a timer to text block to enable count-up timing (Performance Improved Now!).",
"isDesktopOnly": false,
"main": "main.js"
}