@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.
@@ -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
+ }