@difizen/libro-core 1.0.0 → 1.0.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.
Files changed (42) hide show
  1. package/es/cell/libro-cell-service.d.ts +2 -0
  2. package/es/cell/libro-cell-service.d.ts.map +1 -1
  3. package/es/cell/libro-cell-service.js +9 -2
  4. package/es/cell/libro-cell-view.d.ts +2 -0
  5. package/es/cell/libro-cell-view.d.ts.map +1 -1
  6. package/es/cell/libro-cell-view.js +26 -0
  7. package/es/cell/libro-edit-cell-view.d.ts.map +1 -1
  8. package/es/cell/libro-edit-cell-view.js +22 -0
  9. package/es/command/libro-command-contribution.d.ts +4 -0
  10. package/es/command/libro-command-contribution.d.ts.map +1 -1
  11. package/es/command/libro-command-contribution.js +40 -26
  12. package/es/index.less +1 -1
  13. package/es/libro-protocol.d.ts +5 -2
  14. package/es/libro-protocol.d.ts.map +1 -1
  15. package/es/libro-protocol.js +0 -1
  16. package/es/libro-service.d.ts +2 -0
  17. package/es/libro-service.d.ts.map +1 -1
  18. package/es/libro-service.js +37 -21
  19. package/es/libro-setting.d.ts +1 -0
  20. package/es/libro-setting.d.ts.map +1 -1
  21. package/es/libro-setting.js +10 -0
  22. package/es/libro-view-tracker.d.ts +44 -1
  23. package/es/libro-view-tracker.d.ts.map +1 -1
  24. package/es/libro-view-tracker.js +117 -8
  25. package/es/libro-view.d.ts +3 -0
  26. package/es/libro-view.d.ts.map +1 -1
  27. package/es/libro-view.js +80 -23
  28. package/es/utils/index.d.ts +13 -0
  29. package/es/utils/index.d.ts.map +1 -1
  30. package/es/utils/index.js +125 -1
  31. package/package.json +5 -5
  32. package/src/cell/libro-cell-service.ts +2 -0
  33. package/src/cell/libro-cell-view.tsx +36 -0
  34. package/src/cell/libro-edit-cell-view.tsx +25 -0
  35. package/src/command/libro-command-contribution.ts +37 -23
  36. package/src/index.less +1 -1
  37. package/src/libro-protocol.ts +6 -3
  38. package/src/libro-service.ts +10 -3
  39. package/src/libro-setting.ts +11 -0
  40. package/src/libro-view-tracker.ts +105 -2
  41. package/src/libro-view.tsx +63 -3
  42. package/src/utils/index.ts +142 -0
@@ -1,7 +1,7 @@
1
1
  import type { Disposable } from '@difizen/libro-common/app';
2
2
  import { DisposableCollection, Emitter } from '@difizen/libro-common/app';
3
3
  import { ThemeService, ViewManager } from '@difizen/libro-common/app';
4
- import { inject, singleton } from '@difizen/libro-common/app';
4
+ import { inject, singleton, ConfigurationService } from '@difizen/libro-common/app';
5
5
  import { prop } from '@difizen/libro-common/app';
6
6
 
7
7
  import type {
@@ -15,6 +15,7 @@ import {
15
15
  ModelFactory,
16
16
  NotebookService,
17
17
  } from './libro-protocol.js';
18
+ import { SpmReporter } from './libro-setting.js';
18
19
  import { LibroViewTracker } from './libro-view-tracker.js';
19
20
 
20
21
  export interface NotebookViewChange {
@@ -37,6 +38,7 @@ export class LibroService implements NotebookService, Disposable {
37
38
  protected toDispose = new DisposableCollection();
38
39
  @inject(ModelFactory) protected libroModelFactory: ModelFactory;
39
40
  @inject(ViewManager) protected viewManager: ViewManager;
41
+ @inject(ConfigurationService) configurationService: ConfigurationService;
40
42
  @inject(LibroViewTracker) protected libroViewTracker: LibroViewTracker;
41
43
  protected themeService: ThemeService;
42
44
  @prop()
@@ -142,12 +144,17 @@ export class LibroService implements NotebookService, Disposable {
142
144
  return model;
143
145
  }
144
146
  async getOrCreateView(options: NotebookOption): Promise<NotebookView> {
145
- const model = this.getOrCreateModel(options);
147
+ if (options.id) {
148
+ const exist = this.libroViewTracker.viewCache.get(options.id);
149
+ if (exist) {
150
+ return exist;
151
+ }
152
+ }
146
153
  const notebookViewPromise = this.viewManager.getOrCreateView<NotebookView>(
147
154
  notebookViewFactoryId,
148
155
  {
149
156
  ...(options || {}),
150
- modelId: model.id,
157
+ modelId: options.modelId || options.id,
151
158
  },
152
159
  );
153
160
  const notebookView = await notebookViewPromise;
@@ -108,3 +108,14 @@ export const SupportCodeFormat: ConfigurationNode<boolean> = {
108
108
  type: 'boolean',
109
109
  },
110
110
  };
111
+
112
+ export const SpmReporter: ConfigurationNode<boolean> = {
113
+ id: 'libro.spm.reporter',
114
+ description: l10n.t('是否支持数据日志'),
115
+ title: l10n.t('是否支持数据日志'),
116
+ type: 'checkbox',
117
+ defaultValue: false,
118
+ schema: {
119
+ type: 'boolean',
120
+ },
121
+ };
@@ -1,9 +1,112 @@
1
- import { singleton } from '@difizen/libro-common/app';
1
+ import { singleton, Emitter } from '@difizen/libro-common/app';
2
+ import { v4 } from 'uuid';
2
3
 
3
- import type { NotebookModel, NotebookView } from './libro-protocol.js';
4
+ import type {
5
+ ITracker,
6
+ NotebookModel,
7
+ NotebookView,
8
+ Options,
9
+ } from './libro-protocol.js';
10
+
11
+ export class Tracker implements ITracker {
12
+ [key: string]: any;
13
+ startTime?: number;
14
+ endTime?: number;
15
+ extra?: any;
16
+ id: string;
17
+
18
+ constructor(id?: string, extra?: any) {
19
+ this.id = id || v4();
20
+ this.startTime = Date.now();
21
+ this.extra = extra || {};
22
+ }
23
+
24
+ clearAll() {
25
+ this.startTime = undefined;
26
+ this.endTime = undefined;
27
+ }
28
+
29
+ getDuration() {
30
+ if (this.endTime && this.startTime) {
31
+ return this.endTime - this.startTime;
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ log() {
37
+ const result = {
38
+ id: this.id,
39
+ startTime: this.startTime,
40
+ endTime: this.endTime,
41
+ duration: this.getDuration(),
42
+ extra: this.extra,
43
+ };
44
+ this.clearAll();
45
+ return result;
46
+ }
47
+ }
48
+
49
+ export class FpsTracker implements ITracker {
50
+ [key: string]: any;
51
+ extra?: any;
52
+ id: string;
53
+ avgFPS: number;
54
+ maxFrameTime: number;
55
+ totalDropped: number;
56
+ cellsCount: number;
57
+
58
+ constructor(id?: string, extra?: any) {
59
+ this.id = id || v4();
60
+ this.extra = extra || {};
61
+ }
62
+
63
+ log() {
64
+ const result = {
65
+ id: this.id,
66
+ avgFPS: this.avgFPS,
67
+ maxFrameTime: this.maxFrameTime,
68
+ totalDropped: this.totalDropped,
69
+ extra: this.extra,
70
+ cellsCount: this.cellsCount,
71
+ };
72
+ return result;
73
+ }
74
+ }
4
75
 
5
76
  @singleton()
6
77
  export class LibroViewTracker {
7
78
  viewCache: Map<string, NotebookView> = new Map();
8
79
  modelCache: Map<string, NotebookModel> = new Map();
80
+ trackers: Map<string, ITracker> = new Map();
81
+ isEnabledSpmReporter: boolean;
82
+
83
+ protected onTrackerEmitter: Emitter<Record<string, any>> = new Emitter();
84
+ get onTracker() {
85
+ return this.onTrackerEmitter.event;
86
+ }
87
+
88
+ getOrCreateTrackers(options: Options) {
89
+ const id = options.id || v4();
90
+ const exist = this.trackers.get(id);
91
+ if (exist) {
92
+ if (options['type'] !== 'fps' && !exist['startTime']) {
93
+ exist['startTime'] = Date.now();
94
+ }
95
+ return exist;
96
+ }
97
+
98
+ const tracker = options['type'] === 'fps' ? new FpsTracker(id) : new Tracker(id);
99
+ this.trackers.set(id, tracker);
100
+ return tracker;
101
+ }
102
+
103
+ tracker(tracker: ITracker) {
104
+ const trackerLog = tracker.log();
105
+ this.trackers.delete(tracker.id);
106
+ this.onTrackerEmitter.fire(trackerLog);
107
+ }
108
+
109
+ hasTracker(id: string) {
110
+ return this.trackers.has(id);
111
+ }
9
112
  }
@@ -26,7 +26,8 @@ import {
26
26
  ConfigurationService,
27
27
  useConfigurationValue,
28
28
  } from '@difizen/libro-common/app';
29
- import { FloatButton, Button, Spin } from 'antd';
29
+ import { l10n } from '@difizen/libro-common/l10n';
30
+ import { FloatButton, Button, Spin, message } from 'antd';
30
31
  import type { FC, ForwardRefExoticComponent, RefAttributes } from 'react';
31
32
  import { forwardRef, memo, useCallback, useEffect, useRef } from 'react';
32
33
  import { v4 } from 'uuid';
@@ -62,8 +63,9 @@ import {
62
63
  HeaderToolbarVisible,
63
64
  RightContentFixed,
64
65
  } from './libro-setting.js';
66
+ import { LibroViewTracker } from './libro-view-tracker.js';
65
67
  import { LibroSlotManager, LibroSlotView } from './slot/index.js';
66
- import { useSize } from './utils/index.js';
68
+ import { useFrameMonitor, useSize } from './utils/index.js';
67
69
  import { VirtualizedManagerHelper } from './virtualized-manager-helper.js';
68
70
  import type { VirtualizedManager } from './virtualized-manager.js';
69
71
  import './index.less';
@@ -83,6 +85,19 @@ export const LibroContentComponent = memo(function LibroContentComponent() {
83
85
  const HeaderRender = getOrigin(instance.headerRender);
84
86
  const [headerVisible] = useConfigurationValue(HeaderToolbarVisible);
85
87
  const [rightContentFixed] = useConfigurationValue(RightContentFixed);
88
+ useFrameMonitor(
89
+ libroViewContentRef,
90
+ instance.libroViewTracker.isEnabledSpmReporter,
91
+ (payload) => {
92
+ const fpsTracker = instance.libroViewTracker.getOrCreateTrackers({ type: 'fps' });
93
+ fpsTracker['avgFPS'] = payload.summary.avgFPS;
94
+ fpsTracker['maxFrameTime'] = payload.summary.maxFrameTime;
95
+ fpsTracker['totalDropped'] = payload.summary.totalDropped;
96
+ fpsTracker['extra'] = payload.frames;
97
+ fpsTracker['cells'] = instance.model.cells.length;
98
+ instance.libroViewTracker.tracker(fpsTracker);
99
+ },
100
+ );
86
101
 
87
102
  const handleScroll = useCallback(() => {
88
103
  instance.cellScrollEmitter.fire();
@@ -325,7 +340,7 @@ export class LibroView extends BaseView implements NotebookView {
325
340
  @inject(LibroService) libroService: LibroService;
326
341
  @inject(LibroSlotManager) libroSlotManager: LibroSlotManager;
327
342
  @inject(LibroContextKey) contextKey: LibroContextKey;
328
-
343
+ @inject(LibroViewTracker) libroViewTracker: LibroViewTracker;
329
344
  @inject(ViewManager) protected viewManager: ViewManager;
330
345
  @inject(ConfigurationService) protected configurationService: ConfigurationService;
331
346
 
@@ -350,6 +365,8 @@ export class LibroView extends BaseView implements NotebookView {
350
365
  @prop()
351
366
  saving?: boolean;
352
367
 
368
+ options: NotebookOption;
369
+
353
370
  onSaveEmitter: Emitter<boolean> = new Emitter();
354
371
  get onSave() {
355
372
  return this.onSaveEmitter.event;
@@ -386,6 +403,7 @@ export class LibroView extends BaseView implements NotebookView {
386
403
  if (options.id) {
387
404
  this.id = options.id;
388
405
  }
406
+ this.options = options;
389
407
  this.notebookService = notebookService;
390
408
  this.model = this.notebookService.getOrCreateModel(options);
391
409
  this.collapseService = collapseServiceFactory({ view: this });
@@ -463,6 +481,24 @@ export class LibroView extends BaseView implements NotebookView {
463
481
  this.libroService.active = this;
464
482
  this.libroSlotManager.setup(this);
465
483
 
484
+ if (this.libroViewTracker.isEnabledSpmReporter) {
485
+ this.libroViewTracker.getOrCreateTrackers({
486
+ id: this.options.modelId || this.options.id,
487
+ });
488
+ }
489
+ if (
490
+ this.model.cells.length > 30 ||
491
+ (this.options['fileSize'] || 0) / (1024 * 1024) >= 5
492
+ ) {
493
+ message.warning(
494
+ <div>
495
+ {l10n.t(
496
+ '即将打开的文件(内容过大 / cell 过多),可能会导致操作卡顿,请耐心等待~',
497
+ )}
498
+ <Button type="link">{l10n.t('我知道了')}</Button>
499
+ </div>,
500
+ );
501
+ }
466
502
  // this.libroService.libroPerformanceStatistics.setRenderEnd(new Date());
467
503
 
468
504
  // console.log(
@@ -519,11 +555,27 @@ export class LibroView extends BaseView implements NotebookView {
519
555
  };
520
556
 
521
557
  addCell = async (option: CellOptions, position?: number) => {
558
+ if (this.libroViewTracker.isEnabledSpmReporter && option.id) {
559
+ const id = option.id + this.id;
560
+ const libroTracker = this.libroViewTracker.getOrCreateTrackers({
561
+ id: id + 'add',
562
+ });
563
+ libroTracker['extra'].cellsCount = this.model.cells.length;
564
+ libroTracker['extra'].cellOperation = 'add';
565
+ }
522
566
  const cellView = await this.getCellViewByOption(option);
523
567
  this.model.addCell(cellView, position);
524
568
  };
525
569
 
526
570
  addCellAbove = async (option: CellOptions, position?: number) => {
571
+ if (this.libroViewTracker.isEnabledSpmReporter && option.id) {
572
+ const id = option.id + this.id;
573
+ const libroTracker = this.libroViewTracker.getOrCreateTrackers({
574
+ id: id + 'add',
575
+ });
576
+ libroTracker['extra'].cellsCount = this.model.cells.length;
577
+ libroTracker['extra'].cellOperation = 'add';
578
+ }
527
579
  const cellView = await this.getCellViewByOption(option);
528
580
  this.model.addCell(cellView, position, 'above');
529
581
  };
@@ -548,6 +600,14 @@ export class LibroView extends BaseView implements NotebookView {
548
600
  };
549
601
 
550
602
  deleteCell = (cell: CellView) => {
603
+ if (this.libroViewTracker.isEnabledSpmReporter && cell.model.id) {
604
+ const id = cell.model.id + this.id;
605
+ const libroTracker = this.libroViewTracker.getOrCreateTrackers({
606
+ id: id + 'delete',
607
+ });
608
+ libroTracker['extra'].cellsCount = this.model.cells.length;
609
+ libroTracker['extra'].cellOperation = 'delete';
610
+ }
551
611
  const deleteIndex = this.model.getCells().findIndex((item) => {
552
612
  return equals(item, cell);
553
613
  });
@@ -1,5 +1,6 @@
1
1
  import { useUnmount } from '@difizen/libro-common/app';
2
2
  import type { RefObject } from 'react';
3
+ import { useEffect } from 'react';
3
4
  import { useCallback, useLayoutEffect, useRef, useState } from 'react';
4
5
 
5
6
  function useRafState<S>(initialState?: S) {
@@ -46,3 +47,144 @@ export function useSize(ref: RefObject<HTMLDivElement>): Size | undefined {
46
47
  }, [ref, setSize]);
47
48
  return size;
48
49
  }
50
+
51
+ interface FrameMetrics {
52
+ timestamp: number;
53
+ fps: number;
54
+ frameTime: number;
55
+ droppedFrames: number;
56
+ }
57
+
58
+ export const useFrameMonitor = (
59
+ scrollContainerRef: React.RefObject<HTMLElement>,
60
+ isEnabledSpmReporter: boolean,
61
+ onReport?: (payload: { frames: FrameMetrics[]; summary: any }) => void,
62
+ ) => {
63
+ const frameData = useRef<FrameMetrics[]>([]);
64
+ const lastFrameTime = useRef(performance.now());
65
+ const frameCount = useRef(0);
66
+ const lastScrollPos = useRef(0);
67
+ const rafId = useRef<number>();
68
+ const isMonitoring = useRef(false);
69
+ // const reportTimeout = useRef<NodeJS.Timeout>();
70
+ const intervalId = useRef<NodeJS.Timeout>();
71
+ const scrollDebounceTimer = useRef<NodeJS.Timeout>();
72
+
73
+ const stopFrameCapture = useCallback(() => {
74
+ if (rafId.current) {
75
+ cancelAnimationFrame(rafId.current);
76
+ }
77
+ if (intervalId.current) {
78
+ clearInterval(intervalId.current);
79
+ }
80
+ isMonitoring.current = false;
81
+ frameCount.current = 0;
82
+ lastFrameTime.current = performance.now();
83
+ }, []);
84
+
85
+ const startFrameCapture = useCallback(() => {
86
+ if (isMonitoring.current) {
87
+ return;
88
+ }
89
+
90
+ const calculateFPS = () => {
91
+ if (!isMonitoring.current) {
92
+ return;
93
+ }
94
+ const now = performance.now();
95
+ const delta = now - lastFrameTime.current;
96
+ frameData.current.push({
97
+ timestamp: now,
98
+ fps: Math.round((frameCount.current * 1000) / delta),
99
+ frameTime: delta / frameCount.current,
100
+ droppedFrames: Math.max(0, Math.floor(delta / 16.67) - frameCount.current),
101
+ });
102
+ lastFrameTime.current = now;
103
+ frameCount.current = 0;
104
+ };
105
+
106
+ const loop = () => {
107
+ if (isMonitoring.current) {
108
+ frameCount.current++;
109
+ rafId.current = requestAnimationFrame(loop);
110
+ }
111
+ };
112
+
113
+ isMonitoring.current = true;
114
+ frameCount.current = 0;
115
+ lastFrameTime.current = performance.now();
116
+
117
+ if (intervalId.current) {
118
+ clearInterval(intervalId.current);
119
+ }
120
+ intervalId.current = setInterval(calculateFPS, 1500); // 每秒生成一个数据点
121
+ rafId.current = requestAnimationFrame(loop);
122
+ }, []);
123
+
124
+ const reportFrames = useCallback(() => {
125
+ if (frameData.current.length === 0) {
126
+ return;
127
+ }
128
+
129
+ const send = () => {
130
+ const payload = {
131
+ frames: frameData.current,
132
+ summary: {
133
+ avgFPS:
134
+ frameData.current.reduce((a, b) => a + b.fps, 0) / frameData.current.length,
135
+ maxFrameTime: Math.max(...frameData.current.map((f) => f.frameTime)),
136
+ totalDropped: frameData.current.reduce((a, b) => a + b.droppedFrames, 0),
137
+ },
138
+ };
139
+ if (onReport) {
140
+ onReport(payload); // 触发外部回调
141
+ }
142
+ frameData.current = [];
143
+ };
144
+
145
+ requestIdleCallback(send, { timeout: 1000 }) || setTimeout(send, 0);
146
+ }, [onReport]); // 添加 onReport 依赖
147
+
148
+ useEffect(() => {
149
+ if (!isEnabledSpmReporter) {
150
+ return;
151
+ }
152
+ const container = scrollContainerRef.current || (window as unknown as HTMLElement);
153
+
154
+ const handleScroll = () => {
155
+ // const currentScroll =
156
+ // (container as Window).scrollY || (container as HTMLElement).scrollTop;
157
+ const currentScroll =
158
+ container instanceof Window ? container.scrollY : container.scrollTop;
159
+ const scrollDelta = Math.abs(currentScroll - lastScrollPos.current);
160
+
161
+ if (scrollDelta > 10) {
162
+ if (!isMonitoring.current) {
163
+ startFrameCapture();
164
+ }
165
+
166
+ clearTimeout(scrollDebounceTimer.current);
167
+ scrollDebounceTimer.current = setTimeout(() => {
168
+ stopFrameCapture();
169
+ reportFrames();
170
+ }, 2000);
171
+ }
172
+
173
+ lastScrollPos.current = currentScroll;
174
+ };
175
+
176
+ container.addEventListener('scroll', handleScroll, { passive: true });
177
+ return () => {
178
+ container.removeEventListener('scroll', handleScroll);
179
+ stopFrameCapture();
180
+ };
181
+ }, [
182
+ scrollContainerRef,
183
+ startFrameCapture,
184
+ stopFrameCapture,
185
+ reportFrames,
186
+ isEnabledSpmReporter,
187
+ ]);
188
+
189
+ return { frameData, reportFrames };
190
+ };