Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prefetch snapshot so as to avoid network call from critical path. #5968

Merged
merged 18 commits into from
May 3, 2021
10 changes: 10 additions & 0 deletions examples/utils/bundle-size-tests/src/odspPrefetchSnapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import { prefetchLatestSnapshot } from "@fluidframework/odsp-driver";

export function apisToBundle() {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
prefetchLatestSnapshot(undefined as any, undefined as any, undefined as any, undefined as any, undefined);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Emails suggested that this produced 30kb bundle. Is this really the case? There is not much code in here.
It's worth taking a look on why it's so large. I assume numbers are for non-minimized code, right?

Copy link
Member

@christiango christiango Apr 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be minified code. I would recommend having someone take a look of the output of a plugin like this when we run just this api through webpack in production mode: https://github.com/webpack-contrib/webpack-bundle-analyzer

Copy link
Contributor Author

@jatgarg jatgarg Apr 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prefetchSnapshot api file seems to be about ~10Kb and then almost all utilities are used from odspUtils which also seems to account for ~10KB and then there are some other imports from driver-defn, assert, uuid etc which could have accounted for 10Kb more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@heliocliu These are non-minimized bundle size, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The metric is for minified code. The 30kb includes all its dependencies (and their dependencies, etc) as well. If a host were to take a dependency on this method without already having any dependencies on this method's dependencies, the host would incur a 30kb size increase. Like mentioned before, the tool doesn't seem to handle new metrics well, but the way it produces the numbers in the table is otherwise simple and the difference in the Total Size Size Diff value and the sum of individual Size Diff values gives the size of the newly added metrics

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replied in the email with division of sizes. Just pasting here but we can continue discussion in email thread.

Source Size(KB)
OdspPrefetchSnapshot(Main code file) 2.72
Common-utils 3.86
uuid 1.14
node-fetch 0.325
ms/index.js 1.38
node_modules/debug/src 4.26
Telemetry utils 5.05
odsp-doclib-utils 3.5
driver-utils 4.16
Driver-defn 2.07
  28.465

}
1 change: 1 addition & 0 deletions examples/utils/bundle-size-tests/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = {
'map': './src/map',
'matrix': './src/matrix',
'odspDriver': './src/odspDriver',
'odspPrefetchSnapshot': './src/odspPrefetchSnapshot',
'sharedString': './src/sharedString'
},
mode: 'production',
Expand Down
1 change: 1 addition & 0 deletions packages/drivers/odsp-driver/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from "./odspPublicUtils";
export * from "./odspUrlHelper";
export * from "./createOdspUrl";
export * from "./checkUrl";
export * from "./prefetchLatestSnapshot";

// Factory
export * from "./odspDocumentServiceFactoryCore";
Expand Down
56 changes: 5 additions & 51 deletions packages/drivers/odsp-driver/src/odspDocumentServiceFactoryCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License.
*/

import { ITelemetryBaseLogger, ITelemetryLogger } from "@fluidframework/common-definitions";
import { ITelemetryBaseLogger } from "@fluidframework/common-definitions";
import {
IDocumentService,
IDocumentServiceFactory,
Expand All @@ -15,13 +15,9 @@ import {
PerformanceEvent,
} from "@fluidframework/telemetry-utils";
import { ensureFluidResolvedUrl } from "@fluidframework/driver-utils";
import { fetchTokenErrorCode, throwOdspNetworkError } from "@fluidframework/odsp-doclib-utils";
import {
IOdspResolvedUrl,
TokenFetchOptions,
OdspResourceTokenFetchOptions,
isTokenFromCache,
tokenFromResponse,
TokenFetcher,
IPersistedCache,
HostStoragePolicy,
Expand All @@ -35,7 +31,7 @@ import {
ICacheAndTracker,
} from "./epochTracker";
import { OdspDocumentService } from "./odspDocumentService";
import { INewFileInfo, getOdspResolvedUrl, createOdspLogger } from "./odspUtils";
import { INewFileInfo, getOdspResolvedUrl, createOdspLogger, toInstrumentedOdspTokenFetcher } from "./odspUtils";
import { createNewFluidFile } from "./createFile";

/**
Expand Down Expand Up @@ -88,7 +84,7 @@ export class OdspDocumentServiceFactoryCore implements IDocumentServiceFactory {
},
async (event) => {
odspResolvedUrl = await createNewFluidFile(
this.toInstrumentedOdspTokenFetcher(
toInstrumentedOdspTokenFetcher(
odspLogger,
odspResolvedUrl,
this.getStorageToken,
Expand Down Expand Up @@ -145,7 +141,7 @@ export class OdspDocumentServiceFactoryCore implements IDocumentServiceFactory {
{ resolvedUrl: odspResolvedUrl, docId: odspResolvedUrl.hashedDocumentId },
odspLogger);

const storageTokenFetcher = this.toInstrumentedOdspTokenFetcher(
const storageTokenFetcher = toInstrumentedOdspTokenFetcher(
odspLogger,
odspResolvedUrl,
this.getStorageToken,
Expand All @@ -154,7 +150,7 @@ export class OdspDocumentServiceFactoryCore implements IDocumentServiceFactory {

const webSocketTokenFetcher = this.getWebsocketToken === undefined
? undefined
: async (options: TokenFetchOptions) => this.toInstrumentedOdspTokenFetcher(
: async (options: TokenFetchOptions) => toInstrumentedOdspTokenFetcher(
odspLogger,
odspResolvedUrl,
this.getWebsocketToken!,
Expand All @@ -172,46 +168,4 @@ export class OdspDocumentServiceFactoryCore implements IDocumentServiceFactory {
cacheAndTracker.epochTracker,
);
}

private toInstrumentedOdspTokenFetcher(
logger: ITelemetryLogger,
resolvedUrl: IOdspResolvedUrl,
tokenFetcher: TokenFetcher<OdspResourceTokenFetchOptions>,
throwOnNullToken: boolean,
): (options: TokenFetchOptions, name: string) => Promise<string | null> {
return async (options: TokenFetchOptions, name: string) => {
// Telemetry note: if options.refresh is true, there is a potential perf issue:
// Host should optimize and provide non-expired tokens on all critical paths.
// Exceptions: race conditions around expiration, revoked tokens, host that does not care
// (fluid-fetcher)
return PerformanceEvent.timedExecAsync(
logger,
{
eventName: `${name}_GetToken`,
attempts: options.refresh ? 2 : 1,
hasClaims: !!options.claims,
hasTenantId: !!options.tenantId,
},
async (event) => tokenFetcher({
...options,
siteUrl: resolvedUrl.siteUrl,
driveId: resolvedUrl.driveId,
itemId: resolvedUrl.itemId,
}).then((tokenResponse) => {
const token = tokenFromResponse(tokenResponse);
// This event alone generates so many events that is materially impacts cost of telemetry
// Thus do not report end event when it comes back quickly.
// Note that most of the hosts do not report if result is comming from cache or not,
// so we can't rely on that here
if (event.duration >= 32) {
event.end({ fromCache: isTokenFromCache(tokenResponse), isNull: token === null });
}
if (token === null && throwOnNullToken) {
throwOdspNetworkError(`${name} Token is null`, fetchTokenErrorCode);
}
return token;
}),
{ cancel: "generic" });
};
}
}
51 changes: 11 additions & 40 deletions packages/drivers/odsp-driver/src/odspDocumentStorageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,13 @@ import {
import { fetchSnapshot } from "./fetchSnapshot";
import { getUrlAndHeadersWithAuth } from "./getUrlAndHeadersWithAuth";
import { IOdspCache } from "./odspCache";
import { getWithRetryForTokenRefresh, IOdspResponse } from "./odspUtils";
import { evalBlobsAndTrees, getWithRetryForTokenRefresh, IOdspResponse, ISnapshotCacheValue } from "./odspUtils";
import { EpochTracker } from "./epochTracker";
import { OdspSummaryUploadManager } from "./odspSummaryUploadManager";
import { RateLimiter } from "./rateLimiter";

/* eslint-disable max-len */

interface ISnapshotCacheValue {
snapshot: IOdspSnapshot;
sequenceNumber: number | undefined;
}

/**
* Build a tree hierarchy base on a flat tree
*
Expand Down Expand Up @@ -629,9 +624,6 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
tokenFetchOptions: TokenFetchOptions,
) {
const snapshotOptions: ISnapshotOptions = driverSnapshotOptions ?? {
deltas: 1,
channels: 1,
blobs: 2,
mds: this.maxSnapshotSizeLimit,
...hostSnapshotOptions,
timeout: hostSnapshotOptions?.timeout ? Math.min(hostSnapshotOptions.timeout, this.maxSnapshotFetchTimeout) : this.maxSnapshotFetchTimeout,
Expand Down Expand Up @@ -671,21 +663,23 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
const storageToken = await this.getStorageToken(tokenFetchOptions, "TreesLatest");
const url = `${this.snapshotUrl}/trees/latest?ump=1`;
const formBoundary = uuid();
let postBody = `--${formBoundary}\r\n`;
postBody += `Authorization: Bearer ${storageToken}\r\n`;
postBody += `X-HTTP-Method-Override: GET\r\n`;
const formParams: string[] = [];
formParams.push(`--${formBoundary}`);
formParams.push(`Authorization: Bearer ${storageToken}`);
formParams.push(`X-HTTP-Method-Override: GET`);
const logOptions = {};
Object.entries(snapshotOptions).forEach(([key, value]) => {
if (value !== undefined) {
postBody += `${key}: ${value}\r\n`;
formParams.push(`${key}: ${value}`);
logOptions[`snapshotOption_${key}`] = value;
}
});
if (this.redeemSharingLink) {
postBody += `sl: ${this.redeemSharingLink}\r\n`;
formParams.push(`sl: ${this.redeemSharingLink}`);
}
postBody += `_post: 1\r\n`;
postBody += `\r\n--${formBoundary}--`;
formParams.push(`_post: 1`);
formParams.push(`\r\n--${formBoundary}--`);
const postBody = formParams.join("\r\n");
const headers: {[index: string]: any} = {
"Content-Type": `multipart/form-data;boundary=${formBoundary}`,
};
Expand Down Expand Up @@ -755,7 +749,7 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
}
}

const { numTrees, numBlobs, encodedBlobsSize, decodedBlobsSize } = this.evalBlobsAndTrees(snapshot);
const { numTrees, numBlobs, encodedBlobsSize, decodedBlobsSize } = evalBlobsAndTrees(snapshot);
const clientTime = networkTime ? overallTime - networkTime : undefined;

// There are some scenarios in ODSP where we cannot cache, trees/latest will explicitly tell us when we cannot cache using an HTTP response header.
Expand Down Expand Up @@ -816,29 +810,6 @@ export class OdspDocumentStorageService implements IDocumentStorageService {
});
}

private evalBlobsAndTrees(snapshot: IOdspSnapshot) {
let numTrees = 0;
let numBlobs = 0;
let encodedBlobsSize = 0;
let decodedBlobsSize = 0;
for (const tree of snapshot.trees) {
for(const treeEntry of tree.entries) {
if (treeEntry.type === "blob") {
numBlobs++;
} else if (treeEntry.type === "tree") {
numTrees++;
}
}
}
if (snapshot.blobs !== undefined) {
for (const blob of snapshot.blobs) {
decodedBlobsSize += blob.size;
encodedBlobsSize += blob.content.length;
}
}
return { numTrees, numBlobs, encodedBlobsSize, decodedBlobsSize };
}

public async write(tree: api.ITree, parents: string[], message: string): Promise<api.IVersion> {
this.checkSnapshotUrl();

Expand Down
80 changes: 78 additions & 2 deletions packages/drivers/odsp-driver/src/odspUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,43 @@
* Licensed under the MIT License.
*/

import { ITelemetryProperties, ITelemetryBaseLogger } from "@fluidframework/common-definitions";
import { ITelemetryProperties, ITelemetryBaseLogger, ITelemetryLogger } from "@fluidframework/common-definitions";
import { IResolvedUrl, DriverErrorType } from "@fluidframework/driver-definitions";
import { isOnline, OnlineStatus } from "@fluidframework/driver-utils";
import { assert, performance } from "@fluidframework/common-utils";
import { ChildLogger } from "@fluidframework/telemetry-utils";
import { ChildLogger, PerformanceEvent } from "@fluidframework/telemetry-utils";
import {
fetchIncorrectResponse,
offlineFetchFailureStatusCode,
fetchFailureStatusCode,
fetchTimeoutStatusCode,
throwOdspNetworkError,
getSPOAndGraphRequestIdsFromResponse,
fetchTokenErrorCode,
} from "@fluidframework/odsp-doclib-utils";
import {
IOdspResolvedUrl,
TokenFetchOptions,
OdspErrorType,
tokenFromResponse,
isTokenFromCache,
OdspResourceTokenFetchOptions,
TokenFetcher,
} from "@fluidframework/odsp-driver-definitions";
import { debug } from "./debug";
import { fetch } from "./fetch";
import { RateLimiter } from "./rateLimiter";
import { pkgVersion } from "./packageVersion";
import { IOdspSnapshot } from "./contracts";

/** Parse the given url and return the origin (host name) */
export const getOrigin = (url: string) => new URL(url).origin;

export interface ISnapshotCacheValue {
snapshot: IOdspSnapshot;
sequenceNumber: number | undefined;
}

export interface IOdspResponse<T> {
content: T;
headers: Map<string, string>;
Expand Down Expand Up @@ -217,3 +228,68 @@ export const createOdspLogger = (logger?: ITelemetryBaseLogger) =>
driverVersion: pkgVersion,
},
});

export function evalBlobsAndTrees(snapshot: IOdspSnapshot) {
jatgarg marked this conversation as resolved.
Show resolved Hide resolved
let numTrees = 0;
let numBlobs = 0;
let encodedBlobsSize = 0;
let decodedBlobsSize = 0;
for (const tree of snapshot.trees) {
for (const treeEntry of tree.entries) {
if (treeEntry.type === "blob") {
numBlobs++;
} else if (treeEntry.type === "tree") {
numTrees++;
}
}
}
if (snapshot.blobs !== undefined) {
for (const blob of snapshot.blobs) {
decodedBlobsSize += blob.size;
encodedBlobsSize += blob.content.length;
}
}
return { numTrees, numBlobs, encodedBlobsSize, decodedBlobsSize };
}

export function toInstrumentedOdspTokenFetcher(
logger: ITelemetryLogger,
resolvedUrl: IOdspResolvedUrl,
tokenFetcher: TokenFetcher<OdspResourceTokenFetchOptions>,
throwOnNullToken: boolean,
): (options: TokenFetchOptions, name: string) => Promise<string | null> {
return async (options: TokenFetchOptions, name: string) => {
// Telemetry note: if options.refresh is true, there is a potential perf issue:
// Host should optimize and provide non-expired tokens on all critical paths.
// Exceptions: race conditions around expiration, revoked tokens, host that does not care
// (fluid-fetcher)
return PerformanceEvent.timedExecAsync(
logger,
{
eventName: `${name}_GetToken`,
attempts: options.refresh ? 2 : 1,
hasClaims: !!options.claims,
hasTenantId: !!options.tenantId,
},
async (event) => tokenFetcher({
...options,
siteUrl: resolvedUrl.siteUrl,
driveId: resolvedUrl.driveId,
itemId: resolvedUrl.itemId,
}).then((tokenResponse) => {
const token = tokenFromResponse(tokenResponse);
// This event alone generates so many events that is materially impacts cost of telemetry
// Thus do not report end event when it comes back quickly.
// Note that most of the hosts do not report if result is comming from cache or not,
// so we can't rely on that here
if (event.duration >= 32) {
event.end({ fromCache: isTokenFromCache(tokenResponse), isNull: token === null });
}
if (token === null && throwOnNullToken) {
throwOdspNetworkError(`${name} Token is null`, fetchTokenErrorCode);
}
return token;
}),
{ cancel: "generic" });
};
}
Loading