@fluid-tools/fetch-tool 0.53.0-46105

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