@xh/hoist 69.0.0-SNAPSHOT.1728943041558 → 69.0.0-SNAPSHOT.1728960616985

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/CHANGELOG.md CHANGED
@@ -4,9 +4,11 @@
4
4
 
5
5
  ### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW - Hoist core update)
6
6
 
7
- * Requires `hoist-core >= 24` to support batch upload of activity tracking logs to server.
7
+ * Requires `hoist-core >= 24` to support batch upload of activity tracking logs to server and
8
+ new memory monitoring persistence.
8
9
  * Replaced `AppState.INITIALIZING` with finer-grained states (not expected to impact most apps).
9
10
 
11
+
10
12
  ### 🎁 New Features
11
13
 
12
14
  * Optimized activity tracking to batch its calls to the server, reducing network overhead.
@@ -16,6 +18,7 @@
16
18
  * Updated the nested search input within Grid column filters to match candidate values on `any` vs
17
19
  `startsWith`. (Note that this does not change how grid filters are applied, only how users can
18
20
  search for values to select/deselect.)
21
+ * Support for persisting of memory monitoring results
19
22
 
20
23
  ### ⚙️ Typescript API Adjustments
21
24
 
@@ -13,12 +13,22 @@ import {LoadSpec, managed, XH} from '@xh/hoist/core';
13
13
  import {lengthIs, required} from '@xh/hoist/data';
14
14
  import {fmtTime, numberRenderer} from '@xh/hoist/format';
15
15
  import {Icon} from '@xh/hoist/icon';
16
- import {forOwn, sortBy} from 'lodash';
16
+ import {bindable, makeObservable} from '@xh/hoist/mobx';
17
+ import {forOwn, orderBy, sortBy} from 'lodash';
18
+ import {observable, runInAction} from 'mobx';
19
+
20
+ export interface PastInstance {
21
+ name: string;
22
+ lastUpdated: number;
23
+ }
17
24
 
18
25
  export class MemoryMonitorModel extends BaseInstanceModel {
19
26
  @managed gridModel: GridModel;
20
27
  @managed chartModel: ChartModel;
21
28
 
29
+ @bindable.ref pastInstance: PastInstance = null;
30
+ @observable.ref pastInstances: PastInstance[] = [];
31
+
22
32
  get enabled(): boolean {
23
33
  return this.conf.enabled;
24
34
  }
@@ -29,8 +39,82 @@ export class MemoryMonitorModel extends BaseInstanceModel {
29
39
 
30
40
  constructor() {
31
41
  super();
42
+ makeObservable(this);
43
+ this.gridModel = this.createGridModel();
44
+ this.chartModel = this.createChartModel();
45
+ this.addReaction({
46
+ track: () => this.pastInstance,
47
+ run: () => this.loadAsync()
48
+ });
49
+ }
50
+
51
+ override async doLoadAsync(loadSpec: LoadSpec) {
52
+ try {
53
+ await this.loadDataAsync(loadSpec);
54
+ await this.loadPastInstancesAsync(loadSpec);
55
+ } catch (e) {
56
+ this.handleLoadException(e, loadSpec);
57
+ }
58
+ }
59
+
60
+ async takeSnapshotAsync() {
61
+ try {
62
+ await XH.fetchJson({
63
+ url: 'memoryMonitorAdmin/takeSnapshot',
64
+ params: {instance: this.instanceName}
65
+ }).linkTo(this.loadModel);
66
+ await this.loadAsync();
67
+ XH.successToast('Updated snapshot loaded');
68
+ } catch (e) {
69
+ XH.handleException(e);
70
+ }
71
+ }
72
+
73
+ async requestGcAsync() {
74
+ try {
75
+ await XH.fetchJson({
76
+ url: 'memoryMonitorAdmin/requestGc',
77
+ params: {instance: this.instanceName}
78
+ }).linkTo(this.loadModel);
79
+ await this.loadAsync();
80
+ XH.successToast('GC run complete');
81
+ } catch (e) {
82
+ XH.handleException(e);
83
+ }
84
+ }
85
+
86
+ async dumpHeapAsync() {
87
+ try {
88
+ const appEnv = XH.getEnv('appEnvironment').toLowerCase(),
89
+ filename = await XH.prompt<string>({
90
+ title: 'Dump Heap',
91
+ icon: Icon.fileArchive(),
92
+ message: `Specify a filename for the heap dump (to be saved in ${this.heapDumpDir})`,
93
+ input: {
94
+ rules: [required, lengthIs({min: 3, max: 250})],
95
+ initialValue: `${XH.appCode}_${appEnv}_${Date.now()}.hprof`
96
+ }
97
+ });
98
+ if (!filename) return;
99
+ await XH.fetchJson({
100
+ url: 'memoryMonitorAdmin/dumpHeap',
101
+ params: {
102
+ instance: this.instanceName,
103
+ filename
104
+ }
105
+ }).linkTo(this.loadModel);
106
+ await this.loadAsync();
107
+ XH.successToast('Heap dumped successfully to ' + filename);
108
+ } catch (e) {
109
+ XH.handleException(e);
110
+ }
111
+ }
32
112
 
33
- this.gridModel = new GridModel({
113
+ //-------------------
114
+ // Implementation
115
+ //-------------------
116
+ private createGridModel(): GridModel {
117
+ return new GridModel({
34
118
  enableExport: true,
35
119
  exportOptions: {filename: exportFilenameWithDate('memory-monitor')},
36
120
  filterModel: true,
@@ -52,8 +136,10 @@ export class MemoryMonitorModel extends BaseInstanceModel {
52
136
  }
53
137
  ]
54
138
  });
139
+ }
55
140
 
56
- this.chartModel = new ChartModel({
141
+ private createChartModel(): ChartModel {
142
+ return new ChartModel({
57
143
  highchartsConfig: {
58
144
  chart: {
59
145
  zoomType: 'x',
@@ -93,125 +179,80 @@ export class MemoryMonitorModel extends BaseInstanceModel {
93
179
  });
94
180
  }
95
181
 
96
- override async doLoadAsync(loadSpec: LoadSpec) {
97
- const {gridModel, chartModel} = this;
98
-
99
- try {
100
- const snapsByTimestamp = await XH.fetchJson({
101
- url: 'memoryMonitorAdmin/snapshots',
102
- params: {instance: this.instanceName},
103
- loadSpec
104
- });
182
+ private async loadDataAsync(loadSpec: LoadSpec) {
183
+ const {gridModel, chartModel, pastInstance} = this;
105
184
 
106
- // Server returns map by timestamp - flatted to array and load into grid records.
107
- let snaps = [];
108
- forOwn(snapsByTimestamp, (snap, ts) => {
109
- snaps.push({timestamp: parseInt(ts), ...snap});
110
- });
111
- snaps = sortBy(snaps, 'timestamp');
112
- gridModel.loadData(snaps);
185
+ const action = pastInstance ? `snapshotsForPastInstance` : 'snapshots',
186
+ instance = pastInstance ? pastInstance.name : this.instanceName;
187
+ const snapsByTimestamp = await XH.fetchJson({
188
+ url: 'memoryMonitorAdmin/' + action,
189
+ params: {instance},
190
+ loadSpec
191
+ });
113
192
 
114
- // Process further for chart series.
115
- const maxSeries = [],
116
- totalSeries = [],
117
- usedSeries = [],
118
- avgGCSeries = [];
193
+ // Server returns map by timestamp - flatted to array and load into grid records.
194
+ let snaps = [];
195
+ forOwn(snapsByTimestamp, (snap, ts) => {
196
+ snaps.push({timestamp: parseInt(ts), ...snap});
197
+ });
198
+ snaps = sortBy(snaps, 'timestamp');
199
+ gridModel.loadData(snaps);
119
200
 
120
- snaps.forEach(snap => {
121
- maxSeries.push([snap.timestamp, snap.maxHeapMb]);
122
- totalSeries.push([snap.timestamp, snap.totalHeapMb]);
123
- usedSeries.push([snap.timestamp, snap.usedHeapMb]);
201
+ // Process further for chart series.
202
+ const maxSeries = [],
203
+ totalSeries = [],
204
+ usedSeries = [],
205
+ avgGCSeries = [];
124
206
 
125
- avgGCSeries.push([snap.timestamp, snap.avgCollectionTime]);
126
- });
207
+ snaps.forEach(snap => {
208
+ maxSeries.push([snap.timestamp, snap.maxHeapMb]);
209
+ totalSeries.push([snap.timestamp, snap.totalHeapMb]);
210
+ usedSeries.push([snap.timestamp, snap.usedHeapMb]);
127
211
 
128
- chartModel.setSeries([
129
- {
130
- name: 'GC Avg',
131
- data: avgGCSeries,
132
- step: true,
133
- yAxis: 0
134
- },
135
- {
136
- name: 'Heap Max',
137
- data: maxSeries,
138
- color: '#ef6c00',
139
- step: true,
140
- yAxis: 1
141
- },
142
- {
143
- name: 'Heap Total',
144
- data: totalSeries,
145
- color: '#1976d2',
146
- step: true,
147
- yAxis: 1
148
- },
149
- {
150
- name: 'Heap Used',
151
- type: 'area',
152
- data: usedSeries,
153
- color: '#bd7c7c',
154
- fillOpacity: 0.3,
155
- lineWidth: 1,
156
- yAxis: 1
157
- }
158
- ]);
159
- } catch (e) {
160
- this.handleLoadException(e, loadSpec);
161
- }
162
- }
163
-
164
- async takeSnapshotAsync() {
165
- try {
166
- await XH.fetchJson({
167
- url: 'memoryMonitorAdmin/takeSnapshot',
168
- params: {instance: this.instanceName}
169
- }).linkTo(this.loadModel);
170
- await this.loadAsync();
171
- XH.successToast('Updated snapshot loaded');
172
- } catch (e) {
173
- XH.handleException(e);
174
- }
175
- }
212
+ avgGCSeries.push([snap.timestamp, snap.avgCollectionTime]);
213
+ });
176
214
 
177
- async requestGcAsync() {
178
- try {
179
- await XH.fetchJson({
180
- url: 'memoryMonitorAdmin/requestGc',
181
- params: {instance: this.instanceName}
182
- }).linkTo(this.loadModel);
183
- await this.loadAsync();
184
- XH.successToast('GC run complete');
185
- } catch (e) {
186
- XH.handleException(e);
187
- }
215
+ chartModel.setSeries([
216
+ {
217
+ name: 'GC Avg',
218
+ data: avgGCSeries,
219
+ step: true,
220
+ yAxis: 0
221
+ },
222
+ {
223
+ name: 'Heap Max',
224
+ data: maxSeries,
225
+ color: '#ef6c00',
226
+ step: true,
227
+ yAxis: 1
228
+ },
229
+ {
230
+ name: 'Heap Total',
231
+ data: totalSeries,
232
+ color: '#1976d2',
233
+ step: true,
234
+ yAxis: 1
235
+ },
236
+ {
237
+ name: 'Heap Used',
238
+ type: 'area',
239
+ data: usedSeries,
240
+ color: '#bd7c7c',
241
+ fillOpacity: 0.3,
242
+ lineWidth: 1,
243
+ yAxis: 1
244
+ }
245
+ ]);
188
246
  }
189
247
 
190
- async dumpHeapAsync() {
191
- try {
192
- const appEnv = XH.getEnv('appEnvironment').toLowerCase(),
193
- filename = await XH.prompt<string>({
194
- title: 'Dump Heap',
195
- icon: Icon.fileArchive(),
196
- message: `Specify a filename for the heap dump (to be saved in ${this.heapDumpDir})`,
197
- input: {
198
- rules: [required, lengthIs({min: 3, max: 250})],
199
- initialValue: `${XH.appCode}_${appEnv}_${Date.now()}.hprof`
200
- }
201
- });
202
- if (!filename) return;
203
- await XH.fetchJson({
204
- url: 'memoryMonitorAdmin/dumpHeap',
205
- params: {
206
- instance: this.instanceName,
207
- filename
208
- }
209
- }).linkTo(this.loadModel);
210
- await this.loadAsync();
211
- XH.successToast('Heap dumped successfully to ' + filename);
212
- } catch (e) {
213
- XH.handleException(e);
214
- }
248
+ private async loadPastInstancesAsync(loadSpec: LoadSpec) {
249
+ const instances = await XH.fetchJson({
250
+ url: 'memoryMonitorAdmin/availablePastInstances',
251
+ loadSpec
252
+ });
253
+ runInAction(() => {
254
+ this.pastInstances = orderBy(instances, ['lastUpdated'], ['desc']);
255
+ });
215
256
  }
216
257
 
217
258
  private get conf() {
@@ -8,14 +8,16 @@ import {AppModel} from '@xh/hoist/admin/AppModel';
8
8
  import {MemoryMonitorModel} from '@xh/hoist/admin/tabs/cluster/memory/MemoryMonitorModel';
9
9
  import {chart} from '@xh/hoist/cmp/chart';
10
10
  import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
11
- import {filler, vframe} from '@xh/hoist/cmp/layout';
11
+ import {code, filler, fragment, hbox, hspacer, span, vframe} from '@xh/hoist/cmp/layout';
12
12
  import {creates, hoistCmp} from '@xh/hoist/core';
13
13
  import {button, exportButton} from '@xh/hoist/desktop/cmp/button';
14
14
  import {errorMessage} from '@xh/hoist/desktop/cmp/error';
15
15
  import {panel} from '@xh/hoist/desktop/cmp/panel';
16
- import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
16
+ import {toolbar, toolbarSeparator} from '@xh/hoist/desktop/cmp/toolbar';
17
+ import {fmtDate} from '@xh/hoist/format';
17
18
  import {Icon} from '@xh/hoist/icon';
18
- import {isNil} from 'lodash';
19
+ import {isEmpty, isNil} from 'lodash';
20
+ import {menu, menuItem, popover} from '@xh/hoist/kit/blueprint';
19
21
 
20
22
  export const memoryMonitorPanel = hoistCmp.factory({
21
23
  model: creates(MemoryMonitorModel),
@@ -47,34 +49,42 @@ export const memoryMonitorPanel = hoistCmp.factory({
47
49
 
48
50
  const bbar = hoistCmp.factory<MemoryMonitorModel>({
49
51
  render({model}) {
50
- const {readonly} = AppModel,
52
+ const isPastInstance = !!model.pastInstance,
53
+ {readonly} = AppModel,
51
54
  dumpDisabled = isNil(model.heapDumpDir);
52
55
 
53
56
  return toolbar(
54
- button({
55
- text: 'Take Snapshot',
56
- icon: Icon.camera(),
57
+ fragment({
57
58
  omit: readonly,
58
- onClick: () => model.takeSnapshotAsync()
59
+ items: [
60
+ button({
61
+ text: 'Take Snapshot',
62
+ icon: Icon.camera(),
63
+ disabled: isPastInstance,
64
+ onClick: () => model.takeSnapshotAsync()
65
+ }),
66
+ toolbarSeparator(),
67
+ button({
68
+ text: 'Request GC',
69
+ icon: Icon.trash(),
70
+ disabled: isPastInstance,
71
+ onClick: () => model.requestGcAsync()
72
+ }),
73
+ toolbarSeparator(),
74
+ button({
75
+ text: 'Dump Heap',
76
+ icon: Icon.fileArchive(),
77
+ disabled: dumpDisabled || isPastInstance,
78
+ tooltip: dumpDisabled
79
+ ? 'Missing required config xhMemoryMonitoringConfig.heapDumpDir'
80
+ : null,
81
+ onClick: () => model.dumpHeapAsync()
82
+ })
83
+ ]
59
84
  }),
60
85
  '-',
61
- button({
62
- text: 'Request GC',
63
- icon: Icon.trash(),
64
- omit: readonly,
65
- onClick: () => model.requestGcAsync()
66
- }),
67
- '-',
68
- button({
69
- text: 'Dump Heap',
70
- icon: Icon.fileArchive(),
71
- omit: readonly,
72
- disabled: dumpDisabled,
73
- tooltip: dumpDisabled
74
- ? 'Missing required config xhMemoryMonitoringConfig.heapDumpDir'
75
- : null,
76
- onClick: () => model.dumpHeapAsync()
77
- }),
86
+ instanceSelect(),
87
+ instanceClear(),
78
88
  filler(),
79
89
  gridCountLabel({unit: 'snapshot'}),
80
90
  '-',
@@ -82,3 +92,60 @@ const bbar = hoistCmp.factory<MemoryMonitorModel>({
82
92
  );
83
93
  }
84
94
  });
95
+
96
+ const instanceSelect = hoistCmp.factory<MemoryMonitorModel>({
97
+ render({model}) {
98
+ const {pastInstance, pastInstances} = model;
99
+ return popover({
100
+ position: 'top-left',
101
+ minimal: true,
102
+ item: fragment(
103
+ button({
104
+ icon: Icon.history(),
105
+ minimal: true,
106
+ text: instanceDisplay({instance: pastInstance}),
107
+ disabled: isEmpty(pastInstances),
108
+ tooltip: 'View past instances'
109
+ })
110
+ ),
111
+ content: panel({
112
+ icon: Icon.history(),
113
+ title: 'Past Instances',
114
+ compactHeader: true,
115
+ item: menu({
116
+ items: pastInstances.map(it =>
117
+ menuItem({
118
+ text: instanceDisplay({instance: it}),
119
+ onClick: () => (model.pastInstance = it)
120
+ })
121
+ )
122
+ })
123
+ })
124
+ });
125
+ }
126
+ });
127
+
128
+ const instanceDisplay = hoistCmp.factory<MemoryMonitorModel>({
129
+ render({instance}) {
130
+ if (!instance) return null;
131
+ return hbox(
132
+ code(instance.name),
133
+ hspacer(10),
134
+ span({
135
+ className: 'xh-text-color-muted xh-font-size-small',
136
+ item: fmtDate(instance.lastUpdated, 'MMM D h:mma')
137
+ })
138
+ );
139
+ }
140
+ });
141
+
142
+ const instanceClear = hoistCmp.factory<MemoryMonitorModel>({
143
+ render({model}) {
144
+ return button({
145
+ omit: !model.pastInstance,
146
+ icon: Icon.x(),
147
+ minimal: true,
148
+ onClick: () => (model.pastInstance = null)
149
+ });
150
+ }
151
+ });
@@ -2,9 +2,15 @@ import { BaseInstanceModel } from '@xh/hoist/admin/tabs/cluster/BaseInstanceMode
2
2
  import { ChartModel } from '@xh/hoist/cmp/chart';
3
3
  import { ColumnSpec, GridModel } from '@xh/hoist/cmp/grid';
4
4
  import { LoadSpec } from '@xh/hoist/core';
5
+ export interface PastInstance {
6
+ name: string;
7
+ lastUpdated: number;
8
+ }
5
9
  export declare class MemoryMonitorModel extends BaseInstanceModel {
6
10
  gridModel: GridModel;
7
11
  chartModel: ChartModel;
12
+ pastInstance: PastInstance;
13
+ pastInstances: PastInstance[];
8
14
  get enabled(): boolean;
9
15
  get heapDumpDir(): string;
10
16
  constructor();
@@ -12,6 +18,10 @@ export declare class MemoryMonitorModel extends BaseInstanceModel {
12
18
  takeSnapshotAsync(): Promise<void>;
13
19
  requestGcAsync(): Promise<void>;
14
20
  dumpHeapAsync(): Promise<void>;
21
+ private createGridModel;
22
+ private createChartModel;
23
+ private loadDataAsync;
24
+ private loadPastInstancesAsync;
15
25
  private get conf();
16
26
  }
17
27
  export declare const totalHeapMb: ColumnSpec;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "69.0.0-SNAPSHOT.1728943041558",
3
+ "version": "69.0.0-SNAPSHOT.1728960616985",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",