@fluid-tools/fetch-tool 1.4.0-115997 → 2.0.0-dev-rc.1.0.0.224419

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 (37) hide show
  1. package/.eslintrc.js +6 -8
  2. package/CHANGELOG.md +117 -0
  3. package/README.md +38 -7
  4. package/bin/fluid-fetch +0 -0
  5. package/dist/fluidAnalyzeMessages.d.ts.map +1 -1
  6. package/dist/fluidAnalyzeMessages.js +106 -116
  7. package/dist/fluidAnalyzeMessages.js.map +1 -1
  8. package/dist/fluidFetch.js +5 -3
  9. package/dist/fluidFetch.js.map +1 -1
  10. package/dist/fluidFetchArgs.d.ts +0 -3
  11. package/dist/fluidFetchArgs.d.ts.map +1 -1
  12. package/dist/fluidFetchArgs.js +10 -14
  13. package/dist/fluidFetchArgs.js.map +1 -1
  14. package/dist/fluidFetchInit.d.ts +0 -1
  15. package/dist/fluidFetchInit.d.ts.map +1 -1
  16. package/dist/fluidFetchInit.js +41 -34
  17. package/dist/fluidFetchInit.js.map +1 -1
  18. package/dist/fluidFetchMessages.d.ts.map +1 -1
  19. package/dist/fluidFetchMessages.js +168 -200
  20. package/dist/fluidFetchMessages.js.map +1 -1
  21. package/dist/fluidFetchSharePoint.d.ts +0 -1
  22. package/dist/fluidFetchSharePoint.d.ts.map +1 -1
  23. package/dist/fluidFetchSharePoint.js +20 -6
  24. package/dist/fluidFetchSharePoint.js.map +1 -1
  25. package/dist/fluidFetchSnapshot.d.ts.map +1 -1
  26. package/dist/fluidFetchSnapshot.js +18 -20
  27. package/dist/fluidFetchSnapshot.js.map +1 -1
  28. package/package.json +47 -42
  29. package/prettier.config.cjs +8 -0
  30. package/src/fluidAnalyzeMessages.ts +701 -630
  31. package/src/fluidFetch.ts +93 -88
  32. package/src/fluidFetchArgs.ts +167 -168
  33. package/src/fluidFetchInit.ts +133 -104
  34. package/src/fluidFetchMessages.ts +253 -232
  35. package/src/fluidFetchSharePoint.ts +130 -112
  36. package/src/fluidFetchSnapshot.ts +313 -295
  37. package/tsconfig.json +8 -15
@@ -3,19 +3,20 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { assert, unreachableCase } from "@fluidframework/common-utils";
6
+ import { assert, unreachableCase } from "@fluidframework/core-utils";
7
7
  import {
8
- ISequencedDocumentMessage,
9
- ISummaryProposal,
10
- MessageType,
11
- TreeEntry,
8
+ ISequencedDocumentMessage,
9
+ ISummaryAck,
10
+ ISummaryNack,
11
+ ISummaryProposal,
12
+ MessageType,
13
+ TreeEntry,
12
14
  } from "@fluidframework/protocol-definitions";
13
15
  import { IAttachMessage, IEnvelope } from "@fluidframework/runtime-definitions";
14
16
  import {
15
- IChunkedOp,
16
- isRuntimeMessage,
17
- RuntimeMessage,
18
- unpackRuntimeMessage,
17
+ IChunkedOp,
18
+ ContainerMessageType,
19
+ unpackRuntimeMessage,
19
20
  } from "@fluidframework/container-runtime";
20
21
  import { DataStoreMessageType } from "@fluidframework/datastore";
21
22
 
@@ -23,139 +24,155 @@ const noClientName = "No Client";
23
24
  const objectTypePrefix = "https://graph.microsoft.com/types/";
24
25
 
25
26
  function incr(map: Map<string, [number, number]>, key: string, size: number, count = 1) {
26
- const value = map.get(key);
27
- if (value === undefined) {
28
- map.set(key, [count, size]);
29
- } else {
30
- value[0] += count;
31
- value[1] += size;
32
- map.set(key, value);
33
- }
27
+ const value = map.get(key);
28
+ if (value === undefined) {
29
+ map.set(key, [count, size]);
30
+ } else {
31
+ value[0] += count;
32
+ value[1] += size;
33
+ map.set(key, value);
34
+ }
34
35
  }
35
36
 
36
37
  interface ISessionInfo {
37
- startSeq: number;
38
- opCount: number;
39
- email: string;
40
- duration: number;
38
+ startSeq: number;
39
+ opCount: number;
40
+ email: string;
41
+ duration: number;
41
42
  }
42
43
 
43
44
  interface IMessageAnalyzer {
44
- processOp(op: ISequencedDocumentMessage, msgSize: number, filteredOutOp: boolean): void;
45
- reportAnalyzes(lastOp: ISequencedDocumentMessage): void;
45
+ processOp(op: ISequencedDocumentMessage, msgSize: number, filteredOutOp: boolean): void;
46
+ reportAnalyzes(lastOp: ISequencedDocumentMessage): void;
46
47
  }
47
48
 
48
49
  /**
49
50
  * Helper class to track session statistics
50
51
  */
51
52
  class ActiveSession {
52
- public static create(email: string, message: ISequencedDocumentMessage) {
53
- return new ActiveSession(email, message);
54
- }
55
-
56
- private opCount = 0;
57
-
58
- constructor(private readonly email: string, private readonly startMessage: ISequencedDocumentMessage) {
59
- }
60
-
61
- public reportOp(timestamp: number) {
62
- this.opCount++;
63
- }
64
-
65
- public leave(timestamp: number): ISessionInfo {
66
- return {
67
- opCount: this.opCount,
68
- email: this.email,
69
- startSeq: this.startMessage.sequenceNumber,
70
- duration: timestamp - this.startMessage.timestamp,
71
- };
72
- }
53
+ public static create(email: string, message: ISequencedDocumentMessage) {
54
+ return new ActiveSession(email, message);
55
+ }
56
+
57
+ private opCount = 0;
58
+
59
+ constructor(
60
+ private readonly email: string,
61
+ private readonly startMessage: ISequencedDocumentMessage,
62
+ ) {}
63
+
64
+ public reportOp(timestamp: number) {
65
+ this.opCount++;
66
+ }
67
+
68
+ public leave(timestamp: number): ISessionInfo {
69
+ return {
70
+ opCount: this.opCount,
71
+ email: this.email,
72
+ startSeq: this.startMessage.sequenceNumber,
73
+ duration: timestamp - this.startMessage.timestamp,
74
+ };
75
+ }
73
76
  }
74
77
 
75
78
  // Format a number separating 3 digits by comma
76
- // eslint-disable-next-line unicorn/no-unsafe-regex
77
- export const formatNumber = (num: number): string => num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
79
+ export const formatNumber = (num: number): string =>
80
+ // eslint-disable-next-line unicorn/no-unsafe-regex
81
+ num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
78
82
 
79
83
  function dumpStats(
80
- map: Map<string, [number, number]>,
81
- props: {
82
- title: string;
83
- headers: [string, string];
84
- lines?: number;
85
- orderByFirstColumn?: boolean;
86
- reverseColumnsInUI?: boolean;
87
- removeTotals?: boolean;
88
- reverseSort?: boolean;
89
- }) {
90
- const fieldSizes = [10, 14];
91
- const nameLength = 72;
92
- const fieldsLength = fieldSizes[0] + fieldSizes[1] + 1;
93
- let headers = props.headers;
94
-
95
- let recordsToShow = props.lines ?? 10;
96
- if (map.size !== recordsToShow && props.removeTotals === undefined && recordsToShow > 1) {
97
- recordsToShow--;
98
- }
99
-
100
- let sorted: [string, [number, number]][];
101
- const sortIndex = props.orderByFirstColumn === true ? 0 : 1;
102
- let add: string;
103
- if (props.reverseSort !== undefined) {
104
- sorted = [...map.entries()].sort((a, b) => a[1][sortIndex] - b[1][sortIndex]);
105
- add = "↑";
106
- } else {
107
- sorted = [...map.entries()].sort((a, b) => b[1][sortIndex] - a[1][sortIndex]);
108
- add = "↓";
109
- }
110
- headers[sortIndex] = `${headers[sortIndex]} ${add}`;
111
-
112
- if (props.reverseColumnsInUI !== undefined) {
113
- headers = [headers[1], headers[0]];
114
- const sorted2: [string, [number, number]][] = [];
115
- for (const [name, [count, size]] of sorted) {
116
- sorted2.push([name, [size, count]]);
117
- }
118
- sorted = sorted2;
119
- }
120
-
121
- let totalCount = 0;
122
- let sizeTotal = 0;
123
-
124
- props.title = `${props.title} (${sorted.length})`;
125
- const header0 = headers[0].padStart(fieldSizes[0]);
126
- let overflow = header0.length - fieldSizes[0];
127
- console.log(`\n\n${props.title.padEnd(nameLength)} ${header0} ${headers[1].padStart(fieldSizes[1] - overflow)}`);
128
-
129
- console.log(`${"─".repeat(nameLength + 1)}┼${"─".repeat(fieldsLength + 1)}`);
130
- let index = 0;
131
- let allOtherCount = 0;
132
- let allOtherSize = 0;
133
- for (const [name, [count, size]] of sorted) {
134
- index++;
135
- totalCount += count;
136
- sizeTotal += size;
137
- if (index <= recordsToShow) {
138
- const item = name.padEnd(nameLength);
139
- overflow = item.length - nameLength;
140
- const col1 = formatNumber(count).padStart(fieldSizes[0] - overflow);
141
- overflow += col1.length - fieldSizes[0];
142
- const col2 = formatNumber(size).padStart(fieldSizes[1] - overflow);
143
- console.log(`${item} ${col1} ${col2}`);
144
- } else {
145
- allOtherCount += count;
146
- allOtherSize += size;
147
- }
148
- }
149
-
150
- if (props.removeTotals === undefined) {
151
- if (allOtherCount || allOtherSize) {
152
- // eslint-disable-next-line max-len
153
- console.log(`${`All Others (${sorted.length - recordsToShow})`.padEnd(nameLength)} │ ${formatNumber(allOtherCount).padStart(fieldSizes[0])} ${formatNumber(allOtherSize).padStart(fieldSizes[1])}`);
154
- }
155
- console.log(`${"─".repeat(nameLength + 1)}┼${"─".repeat(fieldsLength + 1)}`);
156
- // eslint-disable-next-line max-len
157
- console.log(`${"Total".padEnd(nameLength)} │ ${formatNumber(totalCount).padStart(fieldSizes[0])} ${formatNumber(sizeTotal).padStart(fieldSizes[1])}`);
158
- }
84
+ map: Map<string, [number, number]>,
85
+ props: {
86
+ title: string;
87
+ headers: [string, string];
88
+ lines?: number;
89
+ orderByFirstColumn?: boolean;
90
+ reverseColumnsInUI?: boolean;
91
+ removeTotals?: boolean;
92
+ reverseSort?: boolean;
93
+ },
94
+ ) {
95
+ const fieldSizes = [10, 14];
96
+ const nameLength = 72;
97
+ const fieldsLength = fieldSizes[0] + fieldSizes[1] + 1;
98
+ let headers = props.headers;
99
+
100
+ let recordsToShow = props.lines ?? 10;
101
+ if (map.size !== recordsToShow && props.removeTotals === undefined && recordsToShow > 1) {
102
+ recordsToShow--;
103
+ }
104
+
105
+ let sorted: [string, [number, number]][];
106
+ const sortIndex = props.orderByFirstColumn === true ? 0 : 1;
107
+ let add: string;
108
+ if (props.reverseSort !== undefined) {
109
+ sorted = [...map.entries()].sort((a, b) => a[1][sortIndex] - b[1][sortIndex]);
110
+ add = "↑";
111
+ } else {
112
+ sorted = [...map.entries()].sort((a, b) => b[1][sortIndex] - a[1][sortIndex]);
113
+ add = "↓";
114
+ }
115
+ headers[sortIndex] = `${headers[sortIndex]} ${add}`;
116
+
117
+ if (props.reverseColumnsInUI !== undefined) {
118
+ headers = [headers[1], headers[0]];
119
+ const sorted2: [string, [number, number]][] = [];
120
+ for (const [name, [count, size]] of sorted) {
121
+ sorted2.push([name, [size, count]]);
122
+ }
123
+ sorted = sorted2;
124
+ }
125
+
126
+ let totalCount = 0;
127
+ let sizeTotal = 0;
128
+
129
+ props.title = `${props.title} (${sorted.length})`;
130
+ const header0 = headers[0].padStart(fieldSizes[0]);
131
+ let overflow = header0.length - fieldSizes[0];
132
+ console.log(
133
+ `\n\n${props.title.padEnd(nameLength)} ${header0} ${headers[1].padStart(
134
+ fieldSizes[1] - overflow,
135
+ )}`,
136
+ );
137
+
138
+ console.log(`${"─".repeat(nameLength + 1)}┼${"─".repeat(fieldsLength + 1)}`);
139
+ let index = 0;
140
+ let allOtherCount = 0;
141
+ let allOtherSize = 0;
142
+ for (const [name, [count, size]] of sorted) {
143
+ index++;
144
+ totalCount += count;
145
+ sizeTotal += size;
146
+ if (index <= recordsToShow) {
147
+ const item = name.padEnd(nameLength);
148
+ overflow = item.length - nameLength;
149
+ const col1 = formatNumber(count).padStart(fieldSizes[0] - overflow);
150
+ overflow += col1.length - fieldSizes[0];
151
+ const col2 = formatNumber(size).padStart(fieldSizes[1] - overflow);
152
+ console.log(`${item} │ ${col1} ${col2}`);
153
+ } else {
154
+ allOtherCount += count;
155
+ allOtherSize += size;
156
+ }
157
+ }
158
+
159
+ if (props.removeTotals === undefined) {
160
+ if (allOtherCount || allOtherSize) {
161
+ console.log(
162
+ `${`All Others (${sorted.length - recordsToShow})`.padEnd(
163
+ nameLength,
164
+ )} │ ${formatNumber(allOtherCount).padStart(fieldSizes[0])} ${formatNumber(
165
+ allOtherSize,
166
+ ).padStart(fieldSizes[1])}`,
167
+ );
168
+ }
169
+ console.log(`${"─".repeat(nameLength + 1)}┼${"─".repeat(fieldsLength + 1)}`);
170
+ console.log(
171
+ `${"Total".padEnd(nameLength)} │ ${formatNumber(totalCount).padStart(
172
+ fieldSizes[0],
173
+ )} ${formatNumber(sizeTotal).padStart(fieldSizes[1])}`,
174
+ );
175
+ }
159
176
  }
160
177
 
161
178
  const getObjectId = (dataStoreId: string, id: string) => `[${dataStoreId}]/${id}`;
@@ -164,569 +181,623 @@ const getObjectId = (dataStoreId: string, id: string) => `[${dataStoreId}]/${id}
164
181
  * Analyzer for sessions
165
182
  */
166
183
  class SessionAnalyzer implements IMessageAnalyzer {
167
- private readonly sessionsInProgress = new Map<string, ActiveSession>();
168
- private readonly sessions = new Map<string, [number, number]>();
169
- private readonly users = new Map<string, [number, number]>();
170
-
171
- private first = true;
172
-
173
- public processOp(message: ISequencedDocumentMessage, msgSize: number, skipMessage: boolean): void {
174
- if (this.first) {
175
- this.first = false;
176
- // Start of the road.
177
- const noNameSession = ActiveSession.create(noClientName, message);
178
- this.sessionsInProgress.set(noClientName, noNameSession);
179
- }
180
- const session = processQuorumMessages(
181
- message,
182
- skipMessage,
183
- this.sessionsInProgress,
184
- this.sessions,
185
- this.users);
186
- if (!skipMessage && session) {
187
- session.reportOp(message.timestamp);
188
- }
189
- }
190
-
191
- public reportAnalyzes(lastOp: ISequencedDocumentMessage): void {
192
- // Close any open sessions
193
- reportOpenSessions(
194
- lastOp.timestamp,
195
- this.sessionsInProgress,
196
- this.sessions,
197
- this.users);
198
- dumpStats(this.users, {
199
- title: "Users",
200
- headers: ["Sessions", "Op count"],
201
- reverseColumnsInUI: true,
202
- lines: 6,
203
- });
204
- dumpStats(this.sessions, {
205
- title: "Sessions",
206
- headers: ["Duration(s)", "Op count"],
207
- reverseColumnsInUI: true,
208
- lines: 6,
209
- });
210
- dumpStats(this.sessions, {
211
- title: "Sessions",
212
- headers: ["Duration(s)", "Op count"],
213
- orderByFirstColumn: true,
214
- reverseColumnsInUI: true,
215
- removeTotals: true,
216
- lines: 5,
217
- });
218
- }
184
+ private readonly sessionsInProgress = new Map<string, ActiveSession>();
185
+ private readonly sessions = new Map<string, [number, number]>();
186
+ private readonly users = new Map<string, [number, number]>();
187
+
188
+ private first = true;
189
+
190
+ public processOp(
191
+ message: ISequencedDocumentMessage,
192
+ msgSize: number,
193
+ skipMessage: boolean,
194
+ ): void {
195
+ if (this.first) {
196
+ this.first = false;
197
+ // Start of the road.
198
+ const noNameSession = ActiveSession.create(noClientName, message);
199
+ this.sessionsInProgress.set(noClientName, noNameSession);
200
+ }
201
+ const session = processQuorumMessages(
202
+ message,
203
+ skipMessage,
204
+ this.sessionsInProgress,
205
+ this.sessions,
206
+ this.users,
207
+ );
208
+ if (!skipMessage && session) {
209
+ session.reportOp(message.timestamp);
210
+ }
211
+ }
212
+
213
+ public reportAnalyzes(lastOp: ISequencedDocumentMessage): void {
214
+ // Close any open sessions
215
+ reportOpenSessions(lastOp.timestamp, this.sessionsInProgress, this.sessions, this.users);
216
+ dumpStats(this.users, {
217
+ title: "Users",
218
+ headers: ["Sessions", "Op count"],
219
+ reverseColumnsInUI: true,
220
+ lines: 6,
221
+ });
222
+ dumpStats(this.sessions, {
223
+ title: "Sessions",
224
+ headers: ["Duration(s)", "Op count"],
225
+ reverseColumnsInUI: true,
226
+ lines: 6,
227
+ });
228
+ dumpStats(this.sessions, {
229
+ title: "Sessions",
230
+ headers: ["Duration(s)", "Op count"],
231
+ orderByFirstColumn: true,
232
+ reverseColumnsInUI: true,
233
+ removeTotals: true,
234
+ lines: 5,
235
+ });
236
+ }
219
237
  }
220
238
 
221
239
  /**
222
240
  * Analyzer for data structures
223
241
  */
224
242
  class DataStructureAnalyzer implements IMessageAnalyzer {
225
- private readonly messageTypeStats = new Map<string, [number, number]>();
226
- private readonly dataType = new Map<string, string>();
227
- private readonly dataTypeStats = new Map<string, [number, number]>();
228
- private readonly objectStats = new Map<string, [number, number]>();
229
- private readonly chunkMap = new Map<string, { chunks: string[]; totalSize: number; }>();
230
-
231
- public processOp(message: ISequencedDocumentMessage, msgSize: number, skipMessage: boolean): void {
232
- if (!skipMessage) {
233
- processOp(
234
- message,
235
- this.dataType,
236
- this.objectStats,
237
- msgSize,
238
- this.dataTypeStats,
239
- this.messageTypeStats,
240
- this.chunkMap);
241
- }
242
- }
243
-
244
- public reportAnalyzes(lastOp: ISequencedDocumentMessage): void {
245
- dumpStats(this.messageTypeStats, {
246
- title: "Message Type",
247
- headers: ["Op count", "Bytes"],
248
- lines: 20,
249
- });
250
- dumpStats(calcChannelStats(this.dataType, this.objectStats), {
251
- title: "Channel name",
252
- headers: ["Op count", "Bytes"],
253
- lines: 7,
254
- });
255
- /*
243
+ private readonly messageTypeStats = new Map<string, [number, number]>();
244
+ private readonly dataType = new Map<string, string>();
245
+ private readonly dataTypeStats = new Map<string, [number, number]>();
246
+ private readonly objectStats = new Map<string, [number, number]>();
247
+ // eslint-disable-next-line @typescript-eslint/member-delimiter-style
248
+ private readonly chunkMap = new Map<string, { chunks: string[]; totalSize: number }>();
249
+
250
+ public processOp(
251
+ message: ISequencedDocumentMessage,
252
+ msgSize: number,
253
+ skipMessage: boolean,
254
+ ): void {
255
+ if (!skipMessage) {
256
+ processOp(
257
+ message,
258
+ this.dataType,
259
+ this.objectStats,
260
+ msgSize,
261
+ this.dataTypeStats,
262
+ this.messageTypeStats,
263
+ this.chunkMap,
264
+ );
265
+ }
266
+ }
267
+
268
+ public reportAnalyzes(lastOp: ISequencedDocumentMessage): void {
269
+ dumpStats(this.messageTypeStats, {
270
+ title: "Message Type",
271
+ headers: ["Op count", "Bytes"],
272
+ lines: 20,
273
+ });
274
+ dumpStats(calcChannelStats(this.dataType, this.objectStats), {
275
+ title: "Channel name",
276
+ headers: ["Op count", "Bytes"],
277
+ lines: 7,
278
+ });
279
+ /*
256
280
  dumpStats(this.dataTypeStats, {
257
281
  title: "Channel type",
258
282
  headers: ["Op count", "Bytes"],
259
283
  });
260
284
  */
261
- }
285
+ }
262
286
  }
263
287
 
264
288
  /**
265
289
  * Helper class to report if we filtered out any messages.
266
290
  */
267
291
  class FilteredMessageAnalyzer implements IMessageAnalyzer {
268
- private sizeTotal = 0;
269
- private opsTotal = 0;
270
- private sizeFiltered = 0;
271
- private opsFiltered = 0;
272
- private filtered = false;
273
-
274
- public processOp(message: ISequencedDocumentMessage, msgSize: number, skipMessage: boolean): void {
275
- this.sizeTotal += msgSize;
276
- this.opsTotal++;
277
- if (!skipMessage) {
278
- this.sizeFiltered += msgSize;
279
- this.opsFiltered++;
280
- } else {
281
- this.filtered = true;
282
- }
283
- }
284
-
285
- public reportAnalyzes(lastOp: ISequencedDocumentMessage): void {
286
- if (this.filtered) {
287
- // eslint-disable-next-line max-len
288
- console.log(`\nData is filtered according to --filter:messageType argument(s):\nOp size: ${this.sizeFiltered} / ${this.sizeTotal}\nOp count ${this.opsFiltered} / ${this.opsTotal}`);
289
- }
290
- if (this.opsTotal === 0) {
291
- console.error("No ops were found");
292
- }
293
- }
292
+ private sizeTotal = 0;
293
+ private opsTotal = 0;
294
+ private sizeFiltered = 0;
295
+ private opsFiltered = 0;
296
+ private filtered = false;
297
+
298
+ public processOp(
299
+ message: ISequencedDocumentMessage,
300
+ msgSize: number,
301
+ skipMessage: boolean,
302
+ ): void {
303
+ this.sizeTotal += msgSize;
304
+ this.opsTotal++;
305
+ if (!skipMessage) {
306
+ this.sizeFiltered += msgSize;
307
+ this.opsFiltered++;
308
+ } else {
309
+ this.filtered = true;
310
+ }
311
+ }
312
+
313
+ public reportAnalyzes(lastOp: ISequencedDocumentMessage): void {
314
+ if (this.filtered) {
315
+ console.log(
316
+ `\nData is filtered according to --filter:messageType argument(s):\nOp size: ${this.sizeFiltered} / ${this.sizeTotal}\nOp count ${this.opsFiltered} / ${this.opsTotal}`,
317
+ );
318
+ }
319
+ if (this.opsTotal === 0) {
320
+ console.error("No ops were found");
321
+ }
322
+ }
294
323
  }
295
324
 
296
325
  /**
297
326
  * Helper class to find places where we generated too many ops
298
327
  */
299
328
  class MessageDensityAnalyzer implements IMessageAnalyzer {
300
- private readonly opChunk = 1000;
301
- private opLimit = 1;
302
- private size = 0;
303
- private timeStart = 0;
304
- private doctimerStart = 0;
305
- private readonly ranges = new Map<string, [number, number]>();
306
-
307
- public processOp(message: ISequencedDocumentMessage, msgSize: number, skipMessage: boolean): void {
308
- if (message.sequenceNumber >= this.opLimit) {
309
- if (message.sequenceNumber !== 1) {
310
- const timeDiff = durationFromTime(message.timestamp - this.timeStart);
311
- const opsString = `ops = [${this.opLimit - this.opChunk}, ${this.opLimit - 1}]`.padEnd(26);
312
- // eslint-disable-next-line max-len
313
- const timeString = `time = [${durationFromTime(this.timeStart - this.doctimerStart)}, ${durationFromTime(message.timestamp - this.doctimerStart)}]`;
314
- this.ranges.set(
315
- `${opsString} ${timeString}`,
316
- [timeDiff, this.size]);
317
- } else {
318
- this.doctimerStart = message.timestamp;
319
- }
320
- this.opLimit += this.opChunk;
321
- this.size = 0;
322
- this.timeStart = message.timestamp;
323
- }
324
- if (!skipMessage) {
325
- this.size += msgSize;
326
- }
327
- }
328
-
329
- public reportAnalyzes(lastOp: ISequencedDocumentMessage): void {
330
- dumpStats(this.ranges, {
331
- title: "Fastest 1000 op ranges",
332
- headers: ["Duration(s)", "Bytes"],
333
- orderByFirstColumn: true,
334
- reverseSort: true,
335
- removeTotals: true,
336
- lines: 3,
337
- });
338
- }
329
+ private readonly opChunk = 1000;
330
+ private opLimit = 1;
331
+ private size = 0;
332
+ private timeStart = 0;
333
+ private doctimerStart = 0;
334
+ private readonly ranges = new Map<string, [number, number]>();
335
+
336
+ public processOp(
337
+ message: ISequencedDocumentMessage,
338
+ msgSize: number,
339
+ skipMessage: boolean,
340
+ ): void {
341
+ if (message.sequenceNumber >= this.opLimit) {
342
+ if (message.sequenceNumber !== 1) {
343
+ const timeDiff = durationFromTime(message.timestamp - this.timeStart);
344
+ const opsString = `ops = [${this.opLimit - this.opChunk}, ${
345
+ this.opLimit - 1
346
+ }]`.padEnd(26);
347
+ const timeString = `time = [${durationFromTime(
348
+ this.timeStart - this.doctimerStart,
349
+ )}, ${durationFromTime(message.timestamp - this.doctimerStart)}]`;
350
+ this.ranges.set(`${opsString} ${timeString}`, [timeDiff, this.size]);
351
+ } else {
352
+ this.doctimerStart = message.timestamp;
353
+ }
354
+ this.opLimit += this.opChunk;
355
+ this.size = 0;
356
+ this.timeStart = message.timestamp;
357
+ }
358
+ if (!skipMessage) {
359
+ this.size += msgSize;
360
+ }
361
+ }
362
+
363
+ public reportAnalyzes(lastOp: ISequencedDocumentMessage): void {
364
+ dumpStats(this.ranges, {
365
+ title: "Fastest 1000 op ranges",
366
+ headers: ["Duration(s)", "Bytes"],
367
+ orderByFirstColumn: true,
368
+ reverseSort: true,
369
+ removeTotals: true,
370
+ lines: 3,
371
+ });
372
+ }
339
373
  }
340
374
 
341
375
  /**
342
376
  * Helper class to analyze collab window size
343
377
  */
344
378
  class CollabWindowSizeAnalyzer implements IMessageAnalyzer {
345
- private maxCollabWindow = 0;
346
- private opSeq = 0;
347
-
348
- public processOp(message: ISequencedDocumentMessage, msgSize: number, skipMessage: boolean): void {
349
- const value = message.sequenceNumber - message.minimumSequenceNumber;
350
- if (value > this.maxCollabWindow) {
351
- this.maxCollabWindow = value;
352
- this.opSeq = message.sequenceNumber;
353
- }
354
- }
355
-
356
- public reportAnalyzes(lastOp: ISequencedDocumentMessage): void {
357
- console.log(`\nMaximum collab window size: ${this.maxCollabWindow}, seq# ${this.opSeq}`);
358
- }
379
+ private maxCollabWindow = 0;
380
+ private opSeq = 0;
381
+
382
+ public processOp(
383
+ message: ISequencedDocumentMessage,
384
+ msgSize: number,
385
+ skipMessage: boolean,
386
+ ): void {
387
+ const value = message.sequenceNumber - message.minimumSequenceNumber;
388
+ if (value > this.maxCollabWindow) {
389
+ this.maxCollabWindow = value;
390
+ this.opSeq = message.sequenceNumber;
391
+ }
392
+ }
393
+
394
+ public reportAnalyzes(lastOp: ISequencedDocumentMessage): void {
395
+ console.log(`\nMaximum collab window size: ${this.maxCollabWindow}, seq# ${this.opSeq}`);
396
+ }
359
397
  }
360
398
 
361
399
  /**
362
400
  * Helper class to analyze frequency of summaries
363
401
  */
364
402
  class SummaryAnalyzer implements IMessageAnalyzer {
365
- private lastSummaryOp = 0;
366
- private maxDistance = 0;
367
- private maxSeq = 0;
368
- private minDistance = Number.MAX_SAFE_INTEGER;
369
- private minSeq = 0;
370
- private maxResponse = 0;
371
- private maxResponseSeq = 0;
372
-
373
- public processOp(message: ISequencedDocumentMessage, msgSize: number, skipMessage: boolean): void {
374
- if (message.type === MessageType.SummaryAck) {
375
- const distance = message.sequenceNumber - this.lastSummaryOp - 1;
376
- if (this.maxDistance < distance) {
377
- this.maxDistance = distance;
378
- this.maxSeq = message.sequenceNumber;
379
- }
380
- if (this.minDistance > distance) {
381
- this.minDistance = distance;
382
- this.minSeq = message.sequenceNumber;
383
- }
384
-
385
- this.lastSummaryOp = message.sequenceNumber;
386
- }
387
- if (message.type === MessageType.SummaryAck || message.type === MessageType.SummaryNack) {
388
- const contents: ISummaryProposal = message.contents.summaryProposal;
389
- const distance = message.sequenceNumber - contents.summarySequenceNumber;
390
- if (distance > this.maxResponse) {
391
- this.maxResponse = distance;
392
- this.maxResponseSeq = message.sequenceNumber;
393
- }
394
- }
395
- }
396
-
397
- public reportAnalyzes(lastOp: ISequencedDocumentMessage): void {
398
- const distance = lastOp.sequenceNumber - this.lastSummaryOp;
399
- if (this.maxDistance < distance) {
400
- this.maxDistance = distance;
401
- this.maxSeq = lastOp.sequenceNumber + 1;
402
- }
403
-
404
- console.log("");
405
- if (this.minDistance === Number.MAX_SAFE_INTEGER) {
406
- console.log("No summaries found in this document");
407
- } else {
408
- console.log(`Maximum distance between summaries: ${this.maxDistance}, seq# ${this.maxSeq}`);
409
- console.log(`Maximum server response for summary: ${this.maxResponse}, seq# ${this.maxResponseSeq}`);
410
- console.log(`Minimum distance between summaries: ${this.minDistance}, seq# ${this.minSeq}`);
411
- }
412
- }
403
+ private lastSummaryOp = 0;
404
+ private maxDistance = 0;
405
+ private maxSeq = 0;
406
+ private minDistance = Number.MAX_SAFE_INTEGER;
407
+ private minSeq = 0;
408
+ private maxResponse = 0;
409
+ private maxResponseSeq = 0;
410
+
411
+ public processOp(
412
+ message: ISequencedDocumentMessage,
413
+ msgSize: number,
414
+ skipMessage: boolean,
415
+ ): void {
416
+ if (message.type === MessageType.SummaryAck) {
417
+ const distance = message.sequenceNumber - this.lastSummaryOp - 1;
418
+ if (this.maxDistance < distance) {
419
+ this.maxDistance = distance;
420
+ this.maxSeq = message.sequenceNumber;
421
+ }
422
+ if (this.minDistance > distance) {
423
+ this.minDistance = distance;
424
+ this.minSeq = message.sequenceNumber;
425
+ }
426
+
427
+ this.lastSummaryOp = message.sequenceNumber;
428
+ }
429
+ if (message.type === MessageType.SummaryAck || message.type === MessageType.SummaryNack) {
430
+ const contents: ISummaryProposal = (message.contents as ISummaryAck | ISummaryNack)
431
+ .summaryProposal;
432
+ const distance = message.sequenceNumber - contents.summarySequenceNumber;
433
+ if (distance > this.maxResponse) {
434
+ this.maxResponse = distance;
435
+ this.maxResponseSeq = message.sequenceNumber;
436
+ }
437
+ }
438
+ }
439
+
440
+ public reportAnalyzes(lastOp: ISequencedDocumentMessage): void {
441
+ const distance = lastOp.sequenceNumber - this.lastSummaryOp;
442
+ if (this.maxDistance < distance) {
443
+ this.maxDistance = distance;
444
+ this.maxSeq = lastOp.sequenceNumber + 1;
445
+ }
446
+
447
+ console.log("");
448
+ if (this.minDistance === Number.MAX_SAFE_INTEGER) {
449
+ console.log("No summaries found in this document");
450
+ } else {
451
+ console.log(
452
+ `Maximum distance between summaries: ${this.maxDistance}, seq# ${this.maxSeq}`,
453
+ );
454
+ console.log(
455
+ `Maximum server response for summary: ${this.maxResponse}, seq# ${this.maxResponseSeq}`,
456
+ );
457
+ console.log(
458
+ `Minimum distance between summaries: ${this.minDistance}, seq# ${this.minSeq}`,
459
+ );
460
+ }
461
+ }
413
462
  }
414
463
 
415
464
  /**
416
465
  * Helper class to dump messages to console
417
466
  */
418
467
  class MessageDumper implements IMessageAnalyzer {
419
- public processOp(message: ISequencedDocumentMessage, msgSize: number, skipMessage: boolean): void {
420
- if (!skipMessage) {
421
- console.log(JSON.stringify(message, undefined, 2));
422
- }
423
- }
424
-
425
- public reportAnalyzes(lastOp: ISequencedDocumentMessage): void {
426
- }
468
+ public processOp(
469
+ message: ISequencedDocumentMessage,
470
+ msgSize: number,
471
+ skipMessage: boolean,
472
+ ): void {
473
+ if (!skipMessage) {
474
+ console.log(JSON.stringify(message, undefined, 2));
475
+ }
476
+ }
477
+
478
+ public reportAnalyzes(lastOp: ISequencedDocumentMessage): void {}
427
479
  }
428
480
 
429
481
  export async function printMessageStats(
430
- generator, // AsyncGenerator<ISequencedDocumentMessage[]>,
431
- dumpMessageStats: boolean,
432
- dumpMessages: boolean,
433
- messageTypeFilter: Set<string> = new Set<string>()) {
434
- let lastMessage: ISequencedDocumentMessage | undefined;
435
-
436
- const analyzers: IMessageAnalyzer[] = [
437
- new FilteredMessageAnalyzer(), // Should come first
438
- new SessionAnalyzer(),
439
- new DataStructureAnalyzer(),
440
- new MessageDensityAnalyzer(),
441
- new CollabWindowSizeAnalyzer(),
442
- new SummaryAnalyzer(),
443
- ];
444
-
445
- if (dumpMessages) {
446
- analyzers.push(new MessageDumper());
447
- }
448
-
449
- for await (const messages of generator) {
450
- for (const message of (messages as ISequencedDocumentMessage[])) {
451
- const msgSize = JSON.stringify(message).length;
452
- lastMessage = message;
453
-
454
- const skipMessage = messageTypeFilter.size !== 0 && !messageTypeFilter.has(message.type);
455
-
456
- for (const analyzer of analyzers) {
457
- analyzer.processOp(message, msgSize, skipMessage);
458
- }
459
- }
460
- }
461
-
462
- if (lastMessage !== undefined) {
463
- if (dumpMessageStats) {
464
- for (const analyzer of analyzers) {
465
- analyzer.reportAnalyzes(lastMessage);
466
- }
467
- } else {
468
- // Warn about filtered messages
469
- analyzers[0].reportAnalyzes(lastMessage);
470
- }
471
- }
472
- console.log("");
482
+ generator, // AsyncGenerator<ISequencedDocumentMessage[]>,
483
+ dumpMessageStats: boolean,
484
+ dumpMessages: boolean,
485
+ messageTypeFilter: Set<string> = new Set<string>(),
486
+ ) {
487
+ let lastMessage: ISequencedDocumentMessage | undefined;
488
+
489
+ const analyzers: IMessageAnalyzer[] = [
490
+ new FilteredMessageAnalyzer(), // Should come first
491
+ new SessionAnalyzer(),
492
+ new DataStructureAnalyzer(),
493
+ new MessageDensityAnalyzer(),
494
+ new CollabWindowSizeAnalyzer(),
495
+ new SummaryAnalyzer(),
496
+ ];
497
+
498
+ if (dumpMessages) {
499
+ analyzers.push(new MessageDumper());
500
+ }
501
+
502
+ for await (const messages of generator) {
503
+ for (const message of messages as ISequencedDocumentMessage[]) {
504
+ const msgSize = JSON.stringify(message).length;
505
+ lastMessage = message;
506
+
507
+ const skipMessage =
508
+ messageTypeFilter.size !== 0 && !messageTypeFilter.has(message.type);
509
+
510
+ for (const analyzer of analyzers) {
511
+ analyzer.processOp(message, msgSize, skipMessage);
512
+ }
513
+ }
514
+ }
515
+
516
+ if (lastMessage !== undefined) {
517
+ if (dumpMessageStats) {
518
+ for (const analyzer of analyzers) {
519
+ analyzer.reportAnalyzes(lastMessage);
520
+ }
521
+ } else {
522
+ // Warn about filtered messages
523
+ analyzers[0].reportAnalyzes(lastMessage);
524
+ }
525
+ }
526
+ console.log("");
473
527
  }
474
528
 
475
529
  function processOp(
476
- message: ISequencedDocumentMessage,
477
- dataType: Map<string, string>,
478
- objectStats: Map<string, [number, number]>,
479
- msgSize: number,
480
- dataTypeStats: Map<string, [number, number]>,
481
- messageTypeStats: Map<string, [number, number]>,
482
- chunkMap: Map<string, { chunks: string[]; totalSize: number; }>) {
483
- let type = message.type;
484
- let recorded = false;
485
- let totalMsgSize = msgSize;
486
- let opCount = 1;
487
- if (isRuntimeMessage(message)) {
488
- let runtimeMessage = unpackRuntimeMessage(message);
489
- const messageType = runtimeMessage.type as RuntimeMessage;
490
- switch (messageType) {
491
- case RuntimeMessage.Attach: {
492
- const attachMessage = runtimeMessage.contents as IAttachMessage;
493
- processDataStoreAttachOp(attachMessage, dataType);
494
- break;
495
- }
496
- // skip for now because these ops do not have contents
497
- case RuntimeMessage.BlobAttach: {
498
- break;
499
- }
500
- case RuntimeMessage.ChunkedOp: {
501
- const chunk = runtimeMessage.contents as IChunkedOp;
502
- if (!chunkMap.has(runtimeMessage.clientId)) {
503
- chunkMap.set(
504
- runtimeMessage.clientId, {
505
- chunks: new Array<string>(chunk.totalChunks),
506
- totalSize: 0,
507
- });
508
- }
509
- const value = chunkMap.get(runtimeMessage.clientId);
510
- assert(value !== undefined, 0x2b8 /* "Chunk should be set in map" */);
511
- const chunks = value.chunks;
512
- const chunkIndex = chunk.chunkId - 1;
513
- if (chunks[chunkIndex] !== undefined) {
514
- throw new Error("Chunk already assigned");
515
- }
516
- chunks[chunkIndex] = chunk.contents;
517
- value.totalSize += msgSize;
518
- if (chunk.chunkId === chunk.totalChunks) {
519
- opCount = chunk.totalChunks; // 1 op for each chunk.
520
- runtimeMessage = Object.create(runtimeMessage);
521
- runtimeMessage.contents = chunks.join("");
522
- runtimeMessage.type = chunk.originalType;
523
- type = chunk.originalType;
524
- totalMsgSize = value.totalSize;
525
- chunkMap.delete(runtimeMessage.clientId);
526
- } else {
527
- return;
528
- }
529
- // eslint-disable-next-line no-fallthrough
530
- }
531
- case RuntimeMessage.FluidDataStoreOp:
532
- case RuntimeMessage.Alias:
533
- case RuntimeMessage.Rejoin:
534
- case RuntimeMessage.Operation:
535
- {
536
- let envelope = runtimeMessage.contents as IEnvelope;
537
- // TODO: Legacy?
538
- if (envelope !== undefined && typeof envelope === "string") {
539
- envelope = JSON.parse(envelope);
540
- }
541
- const innerContent = envelope.contents as {
542
- content: any;
543
- type: string;
544
- };
545
- const address = envelope.address;
546
- type = `${type}/${innerContent.type}`;
547
- switch (innerContent.type) {
548
- case DataStoreMessageType.Attach: {
549
- const attachMessage = innerContent.content as IAttachMessage;
550
- let objectType = attachMessage.type;
551
- if (objectType.startsWith(objectTypePrefix)) {
552
- objectType = objectType.substring(objectTypePrefix.length);
553
- }
554
- dataType.set(getObjectId(address, attachMessage.id), objectType);
555
- break;
556
- }
557
- case DataStoreMessageType.ChannelOp:
558
- default: {
559
- const innerEnvelope = innerContent.content as IEnvelope;
560
- const innerContent2 = innerEnvelope.contents as {
561
- type?: string;
562
- value?: any;
563
- };
564
-
565
- const objectId = getObjectId(address, innerEnvelope.address);
566
- incr(objectStats, objectId, totalMsgSize, opCount);
567
- let objectType = dataType.get(objectId);
568
- if (objectType === undefined) {
569
- // Somehow we do not have data...
570
- dataType.set(objectId, objectId);
571
- objectType = objectId;
572
- }
573
- incr(dataTypeStats, objectType, totalMsgSize, opCount);
574
- recorded = true;
575
-
576
- let subType = innerContent2.type;
577
- if (innerContent2.type === "set" &&
578
- typeof innerContent2.value === "object" &&
579
- innerContent2.value !== null) {
580
- type = `${type}/${subType}`;
581
- subType = innerContent2.value.type;
582
- } else if (objectType === "mergeTree" && subType !== undefined) {
583
- const types = ["insert", "remove", "annotate", "group"];
584
- if (types[subType] !== undefined) {
585
- subType = types[subType];
586
- }
587
- }
588
- if (subType !== undefined) {
589
- type = `${type}/${subType}`;
590
- }
591
-
592
- type = `${type} (${objectType})`;
593
- }
594
- }
595
- break;
596
- }
597
- default:
598
- unreachableCase(messageType, "Message type not recognized!");
599
- }
600
- }
601
-
602
- incr(messageTypeStats, type, totalMsgSize, opCount);
603
- if (!recorded) {
604
- // const objectId = `${type} (system)`;
605
- const objectId = `(system messages)`;
606
- const objectType = objectId;
607
- if (dataType.get(objectId) === undefined) {
608
- dataType.set(objectId, objectId);
609
- }
610
- incr(objectStats, objectId, totalMsgSize, opCount);
611
- incr(dataTypeStats, objectType, totalMsgSize, opCount);
612
- }
530
+ runtimeMessage: ISequencedDocumentMessage,
531
+ dataType: Map<string, string>,
532
+ objectStats: Map<string, [number, number]>,
533
+ msgSize: number,
534
+ dataTypeStats: Map<string, [number, number]>,
535
+ messageTypeStats: Map<string, [number, number]>,
536
+ chunkMap: Map<string, { chunks: string[]; totalSize: number }>,
537
+ ) {
538
+ let type = runtimeMessage.type;
539
+ let recorded = false;
540
+ let totalMsgSize = msgSize;
541
+ let opCount = 1;
542
+ if (unpackRuntimeMessage(runtimeMessage)) {
543
+ const messageType = runtimeMessage.type as ContainerMessageType;
544
+ switch (messageType) {
545
+ case ContainerMessageType.Attach: {
546
+ const attachMessage = runtimeMessage.contents as IAttachMessage;
547
+ processDataStoreAttachOp(attachMessage, dataType);
548
+ break;
549
+ }
550
+ // skip for now because these ops do not have contents
551
+ case ContainerMessageType.BlobAttach: {
552
+ break;
553
+ }
554
+ // The default method to count stats should be used for GC messages.
555
+ case ContainerMessageType.GC: {
556
+ break;
557
+ }
558
+ case ContainerMessageType.ChunkedOp: {
559
+ const chunk = runtimeMessage.contents as IChunkedOp;
560
+ // TODO: Verify whether this should be able to handle server-generated ops (with null clientId)
561
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
562
+ if (!chunkMap.has(runtimeMessage.clientId as string)) {
563
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
564
+ chunkMap.set(runtimeMessage.clientId as string, {
565
+ chunks: new Array<string>(chunk.totalChunks),
566
+ totalSize: 0,
567
+ });
568
+ }
569
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
570
+ const value = chunkMap.get(runtimeMessage.clientId as string);
571
+ assert(value !== undefined, 0x2b8 /* "Chunk should be set in map" */);
572
+ const chunks = value.chunks;
573
+ const chunkIndex = chunk.chunkId - 1;
574
+ if (chunks[chunkIndex] !== undefined) {
575
+ throw new Error("Chunk already assigned");
576
+ }
577
+ chunks[chunkIndex] = chunk.contents;
578
+ value.totalSize += msgSize;
579
+ if (chunk.chunkId === chunk.totalChunks) {
580
+ opCount = chunk.totalChunks; // 1 op for each chunk.
581
+ const patchedMessage = Object.create(runtimeMessage);
582
+ patchedMessage.contents = chunks.join("");
583
+ patchedMessage.type = chunk.originalType;
584
+ type = chunk.originalType;
585
+ totalMsgSize = value.totalSize;
586
+ chunkMap.delete(patchedMessage.clientId);
587
+ } else {
588
+ return;
589
+ }
590
+ // eslint-disable-next-line no-fallthrough
591
+ }
592
+ case ContainerMessageType.IdAllocation:
593
+ case ContainerMessageType.FluidDataStoreOp:
594
+ case ContainerMessageType.Alias:
595
+ case ContainerMessageType.Rejoin: {
596
+ let envelope = runtimeMessage.contents as IEnvelope;
597
+ // TODO: Legacy?
598
+ if (envelope !== undefined && typeof envelope === "string") {
599
+ envelope = JSON.parse(envelope);
600
+ }
601
+ const innerContent = envelope.contents as {
602
+ content: any;
603
+ type: string;
604
+ };
605
+ const address = envelope.address;
606
+ type = `${type}/${innerContent.type}`;
607
+ switch (innerContent.type) {
608
+ case DataStoreMessageType.Attach: {
609
+ const attachMessage = innerContent.content as IAttachMessage;
610
+ let objectType = attachMessage.type;
611
+ if (objectType.startsWith(objectTypePrefix)) {
612
+ objectType = objectType.substring(objectTypePrefix.length);
613
+ }
614
+ dataType.set(getObjectId(address, attachMessage.id), objectType);
615
+ break;
616
+ }
617
+ case DataStoreMessageType.ChannelOp:
618
+ default: {
619
+ const innerEnvelope = innerContent.content as IEnvelope;
620
+ const innerContent2 = innerEnvelope.contents as {
621
+ type?: string;
622
+ value?: any;
623
+ };
624
+
625
+ const objectId = getObjectId(address, innerEnvelope.address);
626
+ incr(objectStats, objectId, totalMsgSize, opCount);
627
+ let objectType = dataType.get(objectId);
628
+ if (objectType === undefined) {
629
+ // Somehow we do not have data...
630
+ dataType.set(objectId, objectId);
631
+ objectType = objectId;
632
+ }
633
+ incr(dataTypeStats, objectType, totalMsgSize, opCount);
634
+ recorded = true;
635
+
636
+ let subType = innerContent2.type;
637
+ if (
638
+ innerContent2.type === "set" &&
639
+ typeof innerContent2.value === "object" &&
640
+ innerContent2.value !== null
641
+ ) {
642
+ type = `${type}/${subType}`;
643
+ subType = innerContent2.value.type;
644
+ } else if (objectType === "mergeTree" && subType !== undefined) {
645
+ const types = ["insert", "remove", "annotate", "group"];
646
+ if (types[subType] !== undefined) {
647
+ subType = types[subType];
648
+ }
649
+ }
650
+ if (subType !== undefined) {
651
+ type = `${type}/${subType}`;
652
+ }
653
+
654
+ type = `${type} (${objectType})`;
655
+ }
656
+ }
657
+ break;
658
+ }
659
+ default:
660
+ unreachableCase(messageType, "Message type not recognized!");
661
+ }
662
+ }
663
+
664
+ incr(messageTypeStats, type, totalMsgSize, opCount);
665
+ if (!recorded) {
666
+ // const objectId = `${type} (system)`;
667
+ const objectId = `(system messages)`;
668
+ const objectType = objectId;
669
+ if (dataType.get(objectId) === undefined) {
670
+ dataType.set(objectId, objectId);
671
+ }
672
+ incr(objectStats, objectId, totalMsgSize, opCount);
673
+ incr(dataTypeStats, objectType, totalMsgSize, opCount);
674
+ }
613
675
  }
614
676
 
615
677
  function processDataStoreAttachOp(
616
- attachMessage: IAttachMessage | string,
617
- dataType: Map<string, string>) {
618
- // dataType.set(getObjectId(attachMessage.id), attachMessage.type);
619
-
620
- // That's data store, and it brings a bunch of data structures.
621
- // Let's try to crack it.
622
- let parsedAttachMessage: IAttachMessage;
623
- if (typeof attachMessage === "string") {
624
- parsedAttachMessage = JSON.parse(attachMessage);
625
- } else {
626
- parsedAttachMessage = attachMessage;
627
- }
628
- for (const entry of parsedAttachMessage.snapshot.entries) {
629
- if (entry.type === TreeEntry.Tree) {
630
- for (const entry2 of entry.value.entries) {
631
- if (entry2.path === ".attributes" && entry2.type === TreeEntry.Blob) {
632
- const attrib = JSON.parse(entry2.value.contents);
633
- let objectType: string = attrib.type;
634
- if (objectType.startsWith(objectTypePrefix)) {
635
- objectType = objectType.substring(objectTypePrefix.length);
636
- }
637
- dataType.set(getObjectId(parsedAttachMessage.id, entry.path), objectType);
638
- }
639
- }
640
- }
641
- }
678
+ attachMessage: IAttachMessage | string,
679
+ dataType: Map<string, string>,
680
+ ) {
681
+ // dataType.set(getObjectId(attachMessage.id), attachMessage.type);
682
+
683
+ // That's data store, and it brings a bunch of data structures.
684
+ // Let's try to crack it.
685
+ const parsedAttachMessage =
686
+ typeof attachMessage === "string" ? JSON.parse(attachMessage) : attachMessage;
687
+ for (const entry of parsedAttachMessage.snapshot.entries) {
688
+ if (entry.type === TreeEntry.Tree) {
689
+ for (const entry2 of entry.value.entries) {
690
+ if (entry2.path === ".attributes" && entry2.type === TreeEntry.Blob) {
691
+ const attrib = JSON.parse(entry2.value.contents);
692
+ let objectType: string = attrib.type;
693
+ if (objectType.startsWith(objectTypePrefix)) {
694
+ objectType = objectType.substring(objectTypePrefix.length);
695
+ }
696
+ dataType.set(getObjectId(parsedAttachMessage.id, entry.path), objectType);
697
+ }
698
+ }
699
+ }
700
+ }
642
701
  }
643
702
 
644
703
  function reportOpenSessions(
645
- lastOpTimestamp: number,
646
- sessionsInProgress: Map<string, ActiveSession>,
647
- sessions: Map<string, [number, number]>,
648
- users: Map<string, [number, number]>) {
649
- const activeSessions = new Map<string, [number, number]>();
650
-
651
- for (const [clientId, ses] of sessionsInProgress) {
652
- const sessionInfo = ses.leave(lastOpTimestamp);
653
- if (clientId !== noClientName) {
654
- const sessionName = `${clientId} (${sessionInfo.email})`;
655
- const sessionPayload: [number, number] = [durationFromTime(sessionInfo.duration), sessionInfo.opCount];
656
- sessions.set(sessionName, sessionPayload);
657
- activeSessions.set(sessionName, sessionPayload);
658
- } else {
659
- sessions.set(
660
- `Full file lifespan (noClient messages)`,
661
- [durationFromTime(sessionInfo.duration), sessionInfo.opCount]);
662
- }
663
- incr(users, sessionInfo.email, sessionInfo.opCount);
664
- }
665
-
666
- if (activeSessions.size > 0) {
667
- dumpStats(activeSessions, {
668
- title: "Active sessions",
669
- headers: ["Duration", "Op count"],
670
- lines: 6,
671
- orderByFirstColumn: true,
672
- removeTotals: true,
673
- });
674
- }
704
+ lastOpTimestamp: number,
705
+ sessionsInProgress: Map<string, ActiveSession>,
706
+ sessions: Map<string, [number, number]>,
707
+ users: Map<string, [number, number]>,
708
+ ) {
709
+ const activeSessions = new Map<string, [number, number]>();
710
+
711
+ for (const [clientId, ses] of sessionsInProgress) {
712
+ const sessionInfo = ses.leave(lastOpTimestamp);
713
+ if (clientId !== noClientName) {
714
+ const sessionName = `${clientId} (${sessionInfo.email})`;
715
+ const sessionPayload: [number, number] = [
716
+ durationFromTime(sessionInfo.duration),
717
+ sessionInfo.opCount,
718
+ ];
719
+ sessions.set(sessionName, sessionPayload);
720
+ activeSessions.set(sessionName, sessionPayload);
721
+ } else {
722
+ sessions.set(`Full file lifespan (noClient messages)`, [
723
+ durationFromTime(sessionInfo.duration),
724
+ sessionInfo.opCount,
725
+ ]);
726
+ }
727
+ incr(users, sessionInfo.email, sessionInfo.opCount);
728
+ }
729
+
730
+ if (activeSessions.size > 0) {
731
+ dumpStats(activeSessions, {
732
+ title: "Active sessions",
733
+ headers: ["Duration", "Op count"],
734
+ lines: 6,
735
+ orderByFirstColumn: true,
736
+ removeTotals: true,
737
+ });
738
+ }
675
739
  }
676
740
 
677
- function calcChannelStats(dataType: Map<string, string>, objectStats: Map<string, [number, number]>) {
678
- const channelStats = new Map<string, [number, number]>();
679
- for (const [objectId, type] of dataType) {
680
- let value = objectStats.get(objectId);
681
- if (value === undefined) {
682
- value = [0, 0];
683
- }
684
- if (type === objectId) {
685
- channelStats.set(`${objectId}`, value);
686
- } else {
687
- channelStats.set(`${objectId} (${type})`, value);
688
- }
689
- }
690
- return channelStats;
741
+ function calcChannelStats(
742
+ dataType: Map<string, string>,
743
+ objectStats: Map<string, [number, number]>,
744
+ ) {
745
+ const channelStats = new Map<string, [number, number]>();
746
+ for (const [objectId, type] of dataType) {
747
+ let value = objectStats.get(objectId);
748
+ if (value === undefined) {
749
+ value = [0, 0];
750
+ }
751
+ if (type === objectId) {
752
+ channelStats.set(`${objectId}`, value);
753
+ } else {
754
+ channelStats.set(`${objectId} (${type})`, value);
755
+ }
756
+ }
757
+ return channelStats;
691
758
  }
692
759
 
693
760
  function processQuorumMessages(
694
- message: ISequencedDocumentMessage,
695
- skipMessage: boolean,
696
- sessionsInProgress: Map<string, ActiveSession>,
697
- sessions: Map<string, [number, number]>,
698
- users: Map<string, [number, number]>) {
699
- let session: ActiveSession | undefined;
700
- const dataString = (message as any).data;
701
- if (message.type === "join") {
702
- const data = JSON.parse(dataString);
703
- session = ActiveSession.create(data.detail.user.id, message);
704
- sessionsInProgress.set(data.clientId, session);
705
- } else if (message.type === "leave") {
706
- const clientId = JSON.parse(dataString);
707
- session = sessionsInProgress.get(clientId);
708
- sessionsInProgress.delete(clientId);
709
- assert(!!session, 0x1b7 /* "Bad session state for processing quorum messages" */);
710
- if (session !== undefined) {
711
- if (!skipMessage) {
712
- session.reportOp(message.timestamp);
713
- }
714
- const sessionInfo: ISessionInfo = session.leave(message.timestamp);
715
- sessions.set(
716
- `${clientId} (${sessionInfo.email})`,
717
- [durationFromTime(sessionInfo.duration), sessionInfo.opCount]);
718
- incr(users, sessionInfo.email, sessionInfo.opCount);
719
- session = undefined; // Do not record it second time
720
- }
721
- } else {
722
- // message.clientId can be null
723
- session = sessionsInProgress.get(message.clientId);
724
- if (session === undefined) {
725
- session = sessionsInProgress.get(noClientName);
726
- assert(!!session, 0x1b8 /* "Bad session state for processing quorum messages" */);
727
- }
728
- }
729
- return session;
761
+ message: ISequencedDocumentMessage,
762
+ skipMessage: boolean,
763
+ sessionsInProgress: Map<string, ActiveSession>,
764
+ sessions: Map<string, [number, number]>,
765
+ users: Map<string, [number, number]>,
766
+ ) {
767
+ let session: ActiveSession | undefined;
768
+ const dataString = (message as any).data;
769
+ if (message.type === "join") {
770
+ const data = JSON.parse(dataString);
771
+ session = ActiveSession.create(data.detail.user.id, message);
772
+ sessionsInProgress.set(data.clientId, session);
773
+ } else if (message.type === "leave") {
774
+ const clientId = JSON.parse(dataString);
775
+ session = sessionsInProgress.get(clientId);
776
+ sessionsInProgress.delete(clientId);
777
+ assert(!!session, 0x1b7 /* "Bad session state for processing quorum messages" */);
778
+ if (session !== undefined) {
779
+ if (!skipMessage) {
780
+ session.reportOp(message.timestamp);
781
+ }
782
+ const sessionInfo: ISessionInfo = session.leave(message.timestamp);
783
+ sessions.set(`${clientId} (${sessionInfo.email})`, [
784
+ durationFromTime(sessionInfo.duration),
785
+ sessionInfo.opCount,
786
+ ]);
787
+ incr(users, sessionInfo.email, sessionInfo.opCount);
788
+ session = undefined; // Do not record it second time
789
+ }
790
+ } else {
791
+ // message.clientId can be null
792
+ // TODO: Verify whether this should be able to handle server-generated ops (with null clientId)
793
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
794
+ session = sessionsInProgress.get(message.clientId as string);
795
+ if (session === undefined) {
796
+ session = sessionsInProgress.get(noClientName);
797
+ assert(!!session, 0x1b8 /* "Bad session state for processing quorum messages" */);
798
+ }
799
+ }
800
+ return session;
730
801
  }
731
802
 
732
803
  const durationFromTime = (time: number): number => Math.floor(time / 1000);