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