@363045841yyt/klinechart 0.7.4 → 0.7.5-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -153
- package/dist/index.cjs +2 -2
- package/dist/index.js +15 -9
- package/dist/klinechart.css +1 -1
- package/package.json +82 -76
- package/src/__tests__/_mockController.ts +192 -192
- package/src/__tests__/contract.test.ts +132 -132
- package/src/components/DrawingStyleToolbar.vue +199 -199
- package/src/components/IndicatorParams.vue +570 -570
- package/src/components/IndicatorSelector.vue +1169 -1169
- package/src/components/KLineChart.vue +1570 -1570
- package/src/components/KLineTooltip.vue +200 -200
- package/src/components/LeftToolbar.vue +844 -844
- package/src/components/MarkerTooltip.vue +155 -155
- package/src/components/index.ts +7 -7
- package/src/composables/useFullscreenTeleportTarget.ts +18 -18
- package/src/debug/canvasProfiler.ts +296 -296
- package/src/index.ts +402 -402
- package/src/version.ts +3 -3
|
@@ -1,296 +1,296 @@
|
|
|
1
|
-
type MetricEntry = {
|
|
2
|
-
count: number
|
|
3
|
-
totalTime: number
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
type MetricBucket = Record<string, MetricEntry>
|
|
7
|
-
|
|
8
|
-
type CanvasProfilerMetrics = {
|
|
9
|
-
ctxMethods: MetricBucket
|
|
10
|
-
ctxProps: MetricBucket
|
|
11
|
-
canvasProps: MetricBucket
|
|
12
|
-
ctxMethodSources: Record<string, MetricBucket>
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type CanvasProfilerReportRow = {
|
|
16
|
-
name: string
|
|
17
|
-
count: number
|
|
18
|
-
totalTime: string
|
|
19
|
-
averageTime: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
declare global {
|
|
23
|
-
interface Window {
|
|
24
|
-
__KMAP_CANVAS_PROFILER_INSTALLED__?: boolean
|
|
25
|
-
__KMAP_CANVAS_PROFILER_METRICS__?: CanvasProfilerMetrics
|
|
26
|
-
showCanvasReport?: () => void
|
|
27
|
-
resetCanvasReport?: () => void
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function createBucket(): MetricBucket {
|
|
32
|
-
return Object.create(null) as MetricBucket
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function createMetrics(): CanvasProfilerMetrics {
|
|
36
|
-
return {
|
|
37
|
-
ctxMethods: createBucket(),
|
|
38
|
-
ctxProps: createBucket(),
|
|
39
|
-
canvasProps: createBucket(),
|
|
40
|
-
ctxMethodSources: Object.create(null) as Record<string, MetricBucket>,
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function record(bucket: MetricBucket, name: string, duration: number): void {
|
|
45
|
-
const entry = bucket[name] ??= { count: 0, totalTime: 0 }
|
|
46
|
-
entry.count += 1
|
|
47
|
-
entry.totalTime += duration
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function recordMethodSource(metrics: CanvasProfilerMetrics, methodName: string, source: string, duration: number): void {
|
|
51
|
-
const bucket = metrics.ctxMethodSources[methodName] ??= createBucket()
|
|
52
|
-
record(bucket, source, duration)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function toRows(bucket: MetricBucket): CanvasProfilerReportRow[] {
|
|
56
|
-
return Object.entries(bucket)
|
|
57
|
-
.filter(([, entry]) => entry.count > 0)
|
|
58
|
-
.map(([name, entry]) => ({
|
|
59
|
-
name,
|
|
60
|
-
count: entry.count,
|
|
61
|
-
totalTime: entry.totalTime.toFixed(2),
|
|
62
|
-
averageTime: (entry.totalTime / entry.count).toFixed(4),
|
|
63
|
-
}))
|
|
64
|
-
.sort((a, b) => Number(b.totalTime) - Number(a.totalTime))
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** 全局开关:是否启用 Canvas Profiler 插桩 */
|
|
68
|
-
let isProfilerEnabled = false
|
|
69
|
-
|
|
70
|
-
/** 保存原始方法用于卸载 */
|
|
71
|
-
const originalMethods = new Map<string, (...args: unknown[]) => unknown>()
|
|
72
|
-
const originalSetters = new Map<string, PropertyDescriptor>()
|
|
73
|
-
|
|
74
|
-
/** 启用/禁用 Canvas Profiler */
|
|
75
|
-
export function setCanvasProfilerEnabled(enabled: boolean): void {
|
|
76
|
-
isProfilerEnabled = enabled
|
|
77
|
-
if (enabled) {
|
|
78
|
-
if (typeof window !== 'undefined' && !window.__KMAP_CANVAS_PROFILER_INSTALLED__) {
|
|
79
|
-
installCanvasProfiler()
|
|
80
|
-
}
|
|
81
|
-
} else {
|
|
82
|
-
uninstallCanvasProfiler()
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** 获取 Canvas Profiler 启用状态 */
|
|
87
|
-
export function isCanvasProfilerEnabled(): boolean {
|
|
88
|
-
return isProfilerEnabled
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function getRelevantStackFrame(): string {
|
|
92
|
-
// 如果未启用,直接返回空字符串避免开销
|
|
93
|
-
if (!isProfilerEnabled) return 'disabled'
|
|
94
|
-
|
|
95
|
-
const stack = new Error().stack
|
|
96
|
-
if (!stack) return 'unknown'
|
|
97
|
-
|
|
98
|
-
const frames = stack
|
|
99
|
-
.split('\n')
|
|
100
|
-
.map((line) => line.trim())
|
|
101
|
-
.filter(Boolean)
|
|
102
|
-
|
|
103
|
-
for (const frame of frames) {
|
|
104
|
-
if (
|
|
105
|
-
frame.includes('canvasProfiler')
|
|
106
|
-
|| frame.includes('CanvasRenderingContext2D')
|
|
107
|
-
|| frame === 'Error'
|
|
108
|
-
) {
|
|
109
|
-
continue
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const normalized = frame.replace(/^at\s+/, '')
|
|
113
|
-
const srcMatch = normalized.match(/((?:src|node_modules)[^\s)]+):(\d+):(\d+)/)
|
|
114
|
-
if (srcMatch) {
|
|
115
|
-
return `${srcMatch[1]}:${srcMatch[2]}`
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const parenMatch = normalized.match(/\(([^)]+):(\d+):(\d+)\)$/)
|
|
119
|
-
if (parenMatch) {
|
|
120
|
-
return `${parenMatch[1]}:${parenMatch[2]}`
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return normalized
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return 'unknown'
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function wrapMethod(
|
|
130
|
-
proto: object,
|
|
131
|
-
name: string,
|
|
132
|
-
metrics: CanvasProfilerMetrics,
|
|
133
|
-
options?: { captureSource?: boolean }
|
|
134
|
-
): void {
|
|
135
|
-
const key = `${proto.constructor?.name ?? 'proto'}:${name}`
|
|
136
|
-
if (originalMethods.has(key)) return
|
|
137
|
-
|
|
138
|
-
const original = Reflect.get(proto, name)
|
|
139
|
-
if (typeof original !== 'function') return
|
|
140
|
-
|
|
141
|
-
originalMethods.set(key, original as (...args: unknown[]) => unknown)
|
|
142
|
-
|
|
143
|
-
Reflect.set(proto, name, function (this: object, ...args: unknown[]) {
|
|
144
|
-
// 快速路径:如果 profiler 未启用,直接调用原方法
|
|
145
|
-
if (!isProfilerEnabled) {
|
|
146
|
-
return original.apply(this, args)
|
|
147
|
-
}
|
|
148
|
-
const source = options?.captureSource ? getRelevantStackFrame() : null
|
|
149
|
-
const start = performance.now()
|
|
150
|
-
const result = original.apply(this, args)
|
|
151
|
-
const duration = performance.now() - start
|
|
152
|
-
record(metrics.ctxMethods, name, duration)
|
|
153
|
-
if (source) {
|
|
154
|
-
recordMethodSource(metrics, name, source, duration)
|
|
155
|
-
}
|
|
156
|
-
return result
|
|
157
|
-
})
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function wrapSetter(proto: object, prop: string, bucket: MetricBucket): void {
|
|
161
|
-
const descriptor = Object.getOwnPropertyDescriptor(proto, prop)
|
|
162
|
-
if (!descriptor?.set || !descriptor.configurable) return
|
|
163
|
-
|
|
164
|
-
const key = `${proto.constructor?.name ?? 'proto'}:${prop}`
|
|
165
|
-
if (originalSetters.has(key)) return
|
|
166
|
-
|
|
167
|
-
originalSetters.set(key, descriptor)
|
|
168
|
-
|
|
169
|
-
Object.defineProperty(proto, prop, {
|
|
170
|
-
configurable: true,
|
|
171
|
-
enumerable: descriptor.enumerable ?? false,
|
|
172
|
-
get: descriptor.get,
|
|
173
|
-
set(this: object, value: unknown) {
|
|
174
|
-
// 快速路径:如果 profiler 未启用,直接调用原 setter
|
|
175
|
-
if (!isProfilerEnabled) {
|
|
176
|
-
descriptor.set!.call(this, value)
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
const start = performance.now()
|
|
180
|
-
descriptor.set!.call(this, value)
|
|
181
|
-
record(bucket, prop, performance.now() - start)
|
|
182
|
-
},
|
|
183
|
-
})
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function installCanvasProfiler(): void {
|
|
187
|
-
if (typeof window === 'undefined') return
|
|
188
|
-
if (window.__KMAP_CANVAS_PROFILER_INSTALLED__) return
|
|
189
|
-
|
|
190
|
-
const ctxProto = CanvasRenderingContext2D?.prototype
|
|
191
|
-
const canvasProto = HTMLCanvasElement?.prototype
|
|
192
|
-
if (!ctxProto || !canvasProto) return
|
|
193
|
-
|
|
194
|
-
const metrics = createMetrics()
|
|
195
|
-
|
|
196
|
-
wrapMethod(ctxProto, 'fillText', metrics, { captureSource: true })
|
|
197
|
-
wrapMethod(ctxProto, 'measureText', metrics, { captureSource: true })
|
|
198
|
-
wrapMethod(ctxProto, 'drawImage', metrics)
|
|
199
|
-
wrapMethod(ctxProto, 'save', metrics)
|
|
200
|
-
wrapMethod(ctxProto, 'restore', metrics)
|
|
201
|
-
wrapMethod(ctxProto, 'clip', metrics)
|
|
202
|
-
wrapMethod(ctxProto, 'setTransform', metrics)
|
|
203
|
-
wrapMethod(ctxProto, 'scale', metrics)
|
|
204
|
-
|
|
205
|
-
wrapSetter(ctxProto, 'font', metrics.ctxProps)
|
|
206
|
-
wrapSetter(ctxProto, 'filter', metrics.ctxProps)
|
|
207
|
-
wrapSetter(ctxProto, 'shadowBlur', metrics.ctxProps)
|
|
208
|
-
wrapSetter(ctxProto, 'lineWidth', metrics.ctxProps)
|
|
209
|
-
|
|
210
|
-
wrapSetter(canvasProto, 'width', metrics.canvasProps)
|
|
211
|
-
wrapSetter(canvasProto, 'height', metrics.canvasProps)
|
|
212
|
-
|
|
213
|
-
window.__KMAP_CANVAS_PROFILER_METRICS__ = metrics
|
|
214
|
-
window.__KMAP_CANVAS_PROFILER_INSTALLED__ = true
|
|
215
|
-
|
|
216
|
-
window.showCanvasReport = () => {
|
|
217
|
-
const currentMetrics = window.__KMAP_CANVAS_PROFILER_METRICS__
|
|
218
|
-
if (!currentMetrics) return
|
|
219
|
-
|
|
220
|
-
console.group('[kmap] Canvas profiler report')
|
|
221
|
-
console.log('ctx methods')
|
|
222
|
-
console.table(toRows(currentMetrics.ctxMethods))
|
|
223
|
-
console.log('ctx props')
|
|
224
|
-
console.table(toRows(currentMetrics.ctxProps))
|
|
225
|
-
console.log('canvas props')
|
|
226
|
-
console.table(toRows(currentMetrics.canvasProps))
|
|
227
|
-
|
|
228
|
-
for (const methodName of ['fillText', 'measureText']) {
|
|
229
|
-
const bucket = currentMetrics.ctxMethodSources[methodName]
|
|
230
|
-
if (!bucket) continue
|
|
231
|
-
console.log(`${methodName} sources`)
|
|
232
|
-
console.table(toRows(bucket).slice(0, 20))
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
console.groupEnd()
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
window.resetCanvasReport = () => {
|
|
239
|
-
window.__KMAP_CANVAS_PROFILER_METRICS__ = createMetrics()
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
console.info('[kmap] Canvas profiler enabled. Use window.showCanvasReport() and window.resetCanvasReport().')
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/** 卸载 Canvas Profiler,恢复原始方法 */
|
|
246
|
-
export function uninstallCanvasProfiler(): void {
|
|
247
|
-
if (typeof window === 'undefined') return
|
|
248
|
-
if (!window.__KMAP_CANVAS_PROFILER_INSTALLED__) return
|
|
249
|
-
|
|
250
|
-
const ctxProto = CanvasRenderingContext2D?.prototype
|
|
251
|
-
const canvasProto = HTMLCanvasElement?.prototype
|
|
252
|
-
|
|
253
|
-
// 恢复原始方法
|
|
254
|
-
originalMethods.forEach((original, key) => {
|
|
255
|
-
const match = key.match(/^(.+):(.+)$/)
|
|
256
|
-
if (!match) return
|
|
257
|
-
const [, protoName, methodName] = match
|
|
258
|
-
|
|
259
|
-
let proto: object | null = null
|
|
260
|
-
if (protoName === 'CanvasRenderingContext2D') {
|
|
261
|
-
proto = ctxProto
|
|
262
|
-
} else if (protoName === 'HTMLCanvasElement') {
|
|
263
|
-
proto = canvasProto
|
|
264
|
-
}
|
|
265
|
-
if (proto) {
|
|
266
|
-
Reflect.set(proto, methodName, original)
|
|
267
|
-
}
|
|
268
|
-
})
|
|
269
|
-
originalMethods.clear()
|
|
270
|
-
|
|
271
|
-
// 恢复原始 setter
|
|
272
|
-
originalSetters.forEach((descriptor, key) => {
|
|
273
|
-
const match = key.match(/^(.+):(.+)$/)
|
|
274
|
-
if (!match) return
|
|
275
|
-
const [, protoName, propName] = match
|
|
276
|
-
|
|
277
|
-
let proto: object | null = null
|
|
278
|
-
if (protoName === 'CanvasRenderingContext2D') {
|
|
279
|
-
proto = ctxProto
|
|
280
|
-
} else if (protoName === 'HTMLCanvasElement') {
|
|
281
|
-
proto = canvasProto
|
|
282
|
-
}
|
|
283
|
-
if (proto && descriptor.configurable) {
|
|
284
|
-
Object.defineProperty(proto, propName, descriptor)
|
|
285
|
-
}
|
|
286
|
-
})
|
|
287
|
-
originalSetters.clear()
|
|
288
|
-
|
|
289
|
-
// 清理全局状态
|
|
290
|
-
window.__KMAP_CANVAS_PROFILER_INSTALLED__ = false
|
|
291
|
-
window.__KMAP_CANVAS_PROFILER_METRICS__ = undefined
|
|
292
|
-
window.showCanvasReport = undefined
|
|
293
|
-
window.resetCanvasReport = undefined
|
|
294
|
-
|
|
295
|
-
console.info('[kmap] Canvas profiler disabled.')
|
|
296
|
-
}
|
|
1
|
+
type MetricEntry = {
|
|
2
|
+
count: number
|
|
3
|
+
totalTime: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type MetricBucket = Record<string, MetricEntry>
|
|
7
|
+
|
|
8
|
+
type CanvasProfilerMetrics = {
|
|
9
|
+
ctxMethods: MetricBucket
|
|
10
|
+
ctxProps: MetricBucket
|
|
11
|
+
canvasProps: MetricBucket
|
|
12
|
+
ctxMethodSources: Record<string, MetricBucket>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type CanvasProfilerReportRow = {
|
|
16
|
+
name: string
|
|
17
|
+
count: number
|
|
18
|
+
totalTime: string
|
|
19
|
+
averageTime: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare global {
|
|
23
|
+
interface Window {
|
|
24
|
+
__KMAP_CANVAS_PROFILER_INSTALLED__?: boolean
|
|
25
|
+
__KMAP_CANVAS_PROFILER_METRICS__?: CanvasProfilerMetrics
|
|
26
|
+
showCanvasReport?: () => void
|
|
27
|
+
resetCanvasReport?: () => void
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createBucket(): MetricBucket {
|
|
32
|
+
return Object.create(null) as MetricBucket
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createMetrics(): CanvasProfilerMetrics {
|
|
36
|
+
return {
|
|
37
|
+
ctxMethods: createBucket(),
|
|
38
|
+
ctxProps: createBucket(),
|
|
39
|
+
canvasProps: createBucket(),
|
|
40
|
+
ctxMethodSources: Object.create(null) as Record<string, MetricBucket>,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function record(bucket: MetricBucket, name: string, duration: number): void {
|
|
45
|
+
const entry = bucket[name] ??= { count: 0, totalTime: 0 }
|
|
46
|
+
entry.count += 1
|
|
47
|
+
entry.totalTime += duration
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function recordMethodSource(metrics: CanvasProfilerMetrics, methodName: string, source: string, duration: number): void {
|
|
51
|
+
const bucket = metrics.ctxMethodSources[methodName] ??= createBucket()
|
|
52
|
+
record(bucket, source, duration)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toRows(bucket: MetricBucket): CanvasProfilerReportRow[] {
|
|
56
|
+
return Object.entries(bucket)
|
|
57
|
+
.filter(([, entry]) => entry.count > 0)
|
|
58
|
+
.map(([name, entry]) => ({
|
|
59
|
+
name,
|
|
60
|
+
count: entry.count,
|
|
61
|
+
totalTime: entry.totalTime.toFixed(2),
|
|
62
|
+
averageTime: (entry.totalTime / entry.count).toFixed(4),
|
|
63
|
+
}))
|
|
64
|
+
.sort((a, b) => Number(b.totalTime) - Number(a.totalTime))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 全局开关:是否启用 Canvas Profiler 插桩 */
|
|
68
|
+
let isProfilerEnabled = false
|
|
69
|
+
|
|
70
|
+
/** 保存原始方法用于卸载 */
|
|
71
|
+
const originalMethods = new Map<string, (...args: unknown[]) => unknown>()
|
|
72
|
+
const originalSetters = new Map<string, PropertyDescriptor>()
|
|
73
|
+
|
|
74
|
+
/** 启用/禁用 Canvas Profiler */
|
|
75
|
+
export function setCanvasProfilerEnabled(enabled: boolean): void {
|
|
76
|
+
isProfilerEnabled = enabled
|
|
77
|
+
if (enabled) {
|
|
78
|
+
if (typeof window !== 'undefined' && !window.__KMAP_CANVAS_PROFILER_INSTALLED__) {
|
|
79
|
+
installCanvasProfiler()
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
uninstallCanvasProfiler()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 获取 Canvas Profiler 启用状态 */
|
|
87
|
+
export function isCanvasProfilerEnabled(): boolean {
|
|
88
|
+
return isProfilerEnabled
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getRelevantStackFrame(): string {
|
|
92
|
+
// 如果未启用,直接返回空字符串避免开销
|
|
93
|
+
if (!isProfilerEnabled) return 'disabled'
|
|
94
|
+
|
|
95
|
+
const stack = new Error().stack
|
|
96
|
+
if (!stack) return 'unknown'
|
|
97
|
+
|
|
98
|
+
const frames = stack
|
|
99
|
+
.split('\n')
|
|
100
|
+
.map((line) => line.trim())
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
|
|
103
|
+
for (const frame of frames) {
|
|
104
|
+
if (
|
|
105
|
+
frame.includes('canvasProfiler')
|
|
106
|
+
|| frame.includes('CanvasRenderingContext2D')
|
|
107
|
+
|| frame === 'Error'
|
|
108
|
+
) {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const normalized = frame.replace(/^at\s+/, '')
|
|
113
|
+
const srcMatch = normalized.match(/((?:src|node_modules)[^\s)]+):(\d+):(\d+)/)
|
|
114
|
+
if (srcMatch) {
|
|
115
|
+
return `${srcMatch[1]}:${srcMatch[2]}`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const parenMatch = normalized.match(/\(([^)]+):(\d+):(\d+)\)$/)
|
|
119
|
+
if (parenMatch) {
|
|
120
|
+
return `${parenMatch[1]}:${parenMatch[2]}`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return normalized
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return 'unknown'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function wrapMethod(
|
|
130
|
+
proto: object,
|
|
131
|
+
name: string,
|
|
132
|
+
metrics: CanvasProfilerMetrics,
|
|
133
|
+
options?: { captureSource?: boolean }
|
|
134
|
+
): void {
|
|
135
|
+
const key = `${proto.constructor?.name ?? 'proto'}:${name}`
|
|
136
|
+
if (originalMethods.has(key)) return
|
|
137
|
+
|
|
138
|
+
const original = Reflect.get(proto, name)
|
|
139
|
+
if (typeof original !== 'function') return
|
|
140
|
+
|
|
141
|
+
originalMethods.set(key, original as (...args: unknown[]) => unknown)
|
|
142
|
+
|
|
143
|
+
Reflect.set(proto, name, function (this: object, ...args: unknown[]) {
|
|
144
|
+
// 快速路径:如果 profiler 未启用,直接调用原方法
|
|
145
|
+
if (!isProfilerEnabled) {
|
|
146
|
+
return original.apply(this, args)
|
|
147
|
+
}
|
|
148
|
+
const source = options?.captureSource ? getRelevantStackFrame() : null
|
|
149
|
+
const start = performance.now()
|
|
150
|
+
const result = original.apply(this, args)
|
|
151
|
+
const duration = performance.now() - start
|
|
152
|
+
record(metrics.ctxMethods, name, duration)
|
|
153
|
+
if (source) {
|
|
154
|
+
recordMethodSource(metrics, name, source, duration)
|
|
155
|
+
}
|
|
156
|
+
return result
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function wrapSetter(proto: object, prop: string, bucket: MetricBucket): void {
|
|
161
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, prop)
|
|
162
|
+
if (!descriptor?.set || !descriptor.configurable) return
|
|
163
|
+
|
|
164
|
+
const key = `${proto.constructor?.name ?? 'proto'}:${prop}`
|
|
165
|
+
if (originalSetters.has(key)) return
|
|
166
|
+
|
|
167
|
+
originalSetters.set(key, descriptor)
|
|
168
|
+
|
|
169
|
+
Object.defineProperty(proto, prop, {
|
|
170
|
+
configurable: true,
|
|
171
|
+
enumerable: descriptor.enumerable ?? false,
|
|
172
|
+
get: descriptor.get,
|
|
173
|
+
set(this: object, value: unknown) {
|
|
174
|
+
// 快速路径:如果 profiler 未启用,直接调用原 setter
|
|
175
|
+
if (!isProfilerEnabled) {
|
|
176
|
+
descriptor.set!.call(this, value)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
const start = performance.now()
|
|
180
|
+
descriptor.set!.call(this, value)
|
|
181
|
+
record(bucket, prop, performance.now() - start)
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function installCanvasProfiler(): void {
|
|
187
|
+
if (typeof window === 'undefined') return
|
|
188
|
+
if (window.__KMAP_CANVAS_PROFILER_INSTALLED__) return
|
|
189
|
+
|
|
190
|
+
const ctxProto = CanvasRenderingContext2D?.prototype
|
|
191
|
+
const canvasProto = HTMLCanvasElement?.prototype
|
|
192
|
+
if (!ctxProto || !canvasProto) return
|
|
193
|
+
|
|
194
|
+
const metrics = createMetrics()
|
|
195
|
+
|
|
196
|
+
wrapMethod(ctxProto, 'fillText', metrics, { captureSource: true })
|
|
197
|
+
wrapMethod(ctxProto, 'measureText', metrics, { captureSource: true })
|
|
198
|
+
wrapMethod(ctxProto, 'drawImage', metrics)
|
|
199
|
+
wrapMethod(ctxProto, 'save', metrics)
|
|
200
|
+
wrapMethod(ctxProto, 'restore', metrics)
|
|
201
|
+
wrapMethod(ctxProto, 'clip', metrics)
|
|
202
|
+
wrapMethod(ctxProto, 'setTransform', metrics)
|
|
203
|
+
wrapMethod(ctxProto, 'scale', metrics)
|
|
204
|
+
|
|
205
|
+
wrapSetter(ctxProto, 'font', metrics.ctxProps)
|
|
206
|
+
wrapSetter(ctxProto, 'filter', metrics.ctxProps)
|
|
207
|
+
wrapSetter(ctxProto, 'shadowBlur', metrics.ctxProps)
|
|
208
|
+
wrapSetter(ctxProto, 'lineWidth', metrics.ctxProps)
|
|
209
|
+
|
|
210
|
+
wrapSetter(canvasProto, 'width', metrics.canvasProps)
|
|
211
|
+
wrapSetter(canvasProto, 'height', metrics.canvasProps)
|
|
212
|
+
|
|
213
|
+
window.__KMAP_CANVAS_PROFILER_METRICS__ = metrics
|
|
214
|
+
window.__KMAP_CANVAS_PROFILER_INSTALLED__ = true
|
|
215
|
+
|
|
216
|
+
window.showCanvasReport = () => {
|
|
217
|
+
const currentMetrics = window.__KMAP_CANVAS_PROFILER_METRICS__
|
|
218
|
+
if (!currentMetrics) return
|
|
219
|
+
|
|
220
|
+
console.group('[kmap] Canvas profiler report')
|
|
221
|
+
console.log('ctx methods')
|
|
222
|
+
console.table(toRows(currentMetrics.ctxMethods))
|
|
223
|
+
console.log('ctx props')
|
|
224
|
+
console.table(toRows(currentMetrics.ctxProps))
|
|
225
|
+
console.log('canvas props')
|
|
226
|
+
console.table(toRows(currentMetrics.canvasProps))
|
|
227
|
+
|
|
228
|
+
for (const methodName of ['fillText', 'measureText']) {
|
|
229
|
+
const bucket = currentMetrics.ctxMethodSources[methodName]
|
|
230
|
+
if (!bucket) continue
|
|
231
|
+
console.log(`${methodName} sources`)
|
|
232
|
+
console.table(toRows(bucket).slice(0, 20))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.groupEnd()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
window.resetCanvasReport = () => {
|
|
239
|
+
window.__KMAP_CANVAS_PROFILER_METRICS__ = createMetrics()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.info('[kmap] Canvas profiler enabled. Use window.showCanvasReport() and window.resetCanvasReport().')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** 卸载 Canvas Profiler,恢复原始方法 */
|
|
246
|
+
export function uninstallCanvasProfiler(): void {
|
|
247
|
+
if (typeof window === 'undefined') return
|
|
248
|
+
if (!window.__KMAP_CANVAS_PROFILER_INSTALLED__) return
|
|
249
|
+
|
|
250
|
+
const ctxProto = CanvasRenderingContext2D?.prototype
|
|
251
|
+
const canvasProto = HTMLCanvasElement?.prototype
|
|
252
|
+
|
|
253
|
+
// 恢复原始方法
|
|
254
|
+
originalMethods.forEach((original, key) => {
|
|
255
|
+
const match = key.match(/^(.+):(.+)$/)
|
|
256
|
+
if (!match) return
|
|
257
|
+
const [, protoName, methodName] = match
|
|
258
|
+
|
|
259
|
+
let proto: object | null = null
|
|
260
|
+
if (protoName === 'CanvasRenderingContext2D') {
|
|
261
|
+
proto = ctxProto
|
|
262
|
+
} else if (protoName === 'HTMLCanvasElement') {
|
|
263
|
+
proto = canvasProto
|
|
264
|
+
}
|
|
265
|
+
if (proto) {
|
|
266
|
+
Reflect.set(proto, methodName, original)
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
originalMethods.clear()
|
|
270
|
+
|
|
271
|
+
// 恢复原始 setter
|
|
272
|
+
originalSetters.forEach((descriptor, key) => {
|
|
273
|
+
const match = key.match(/^(.+):(.+)$/)
|
|
274
|
+
if (!match) return
|
|
275
|
+
const [, protoName, propName] = match
|
|
276
|
+
|
|
277
|
+
let proto: object | null = null
|
|
278
|
+
if (protoName === 'CanvasRenderingContext2D') {
|
|
279
|
+
proto = ctxProto
|
|
280
|
+
} else if (protoName === 'HTMLCanvasElement') {
|
|
281
|
+
proto = canvasProto
|
|
282
|
+
}
|
|
283
|
+
if (proto && descriptor.configurable) {
|
|
284
|
+
Object.defineProperty(proto, propName, descriptor)
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
originalSetters.clear()
|
|
288
|
+
|
|
289
|
+
// 清理全局状态
|
|
290
|
+
window.__KMAP_CANVAS_PROFILER_INSTALLED__ = false
|
|
291
|
+
window.__KMAP_CANVAS_PROFILER_METRICS__ = undefined
|
|
292
|
+
window.showCanvasReport = undefined
|
|
293
|
+
window.resetCanvasReport = undefined
|
|
294
|
+
|
|
295
|
+
console.info('[kmap] Canvas profiler disabled.')
|
|
296
|
+
}
|