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
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
55 changes: 5 additions & 50 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 @@ -37,6 +33,7 @@ import {
import { OdspDocumentService } from "./odspDocumentService";
import { INewFileInfo, getOdspResolvedUrl, createOdspLogger } from "./odspUtils";
import { createNewFluidFile } from "./createFile";
import { toInstrumentedOdspTokenFetcher } from "./odspUtils2";

/**
* Factory for creating the sharepoint document service. Use this if you want to
Expand Down Expand Up @@ -88,7 +85,7 @@ export class OdspDocumentServiceFactoryCore implements IDocumentServiceFactory {
},
async (event) => {
odspResolvedUrl = await createNewFluidFile(
this.toInstrumentedOdspTokenFetcher(
toInstrumentedOdspTokenFetcher(
odspLogger,
odspResolvedUrl,
this.getStorageToken,
Expand Down Expand Up @@ -145,7 +142,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 +151,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 +169,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" });
};
}
}
28 changes: 3 additions & 25 deletions packages/drivers/odsp-driver/src/odspDocumentStorageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ import { getWithRetryForTokenRefresh, IOdspResponse } from "./odspUtils";
import { EpochTracker } from "./epochTracker";
import { OdspSummaryUploadManager } from "./odspSummaryUploadManager";
import { RateLimiter } from "./rateLimiter";
import { evalBlobsAndTrees } from "./odspUtils2";

/* eslint-disable max-len */

interface ISnapshotCacheValue {
export interface ISnapshotCacheValue {
snapshot: IOdspSnapshot;
sequenceNumber: number | undefined;
}
Expand Down Expand Up @@ -755,7 +756,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 +817,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
82 changes: 82 additions & 0 deletions packages/drivers/odsp-driver/src/odspUtils2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import { ITelemetryLogger } from "@fluidframework/common-definitions";
import { fetchTokenErrorCode, throwOdspNetworkError } from "@fluidframework/odsp-doclib-utils";
import {
IOdspResolvedUrl,
isTokenFromCache,
OdspResourceTokenFetchOptions,
TokenFetcher,
TokenFetchOptions,
tokenFromResponse,
} from "@fluidframework/odsp-driver-definitions";
import { PerformanceEvent } from "@fluidframework/telemetry-utils";
import { IOdspSnapshot } from "./contracts";

export function evalBlobsAndTrees(snapshot: IOdspSnapshot) {
Copy link
Member

Choose a reason for hiding this comment

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

Rather than an odspUtils2.ts, what about just creating two separate files: evalBlobsAndTrees.ts and toInstrementedOdspTokenFetcher.ts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually I added these to odsputils only because we already import a lot from that file. So no need to add these in a separate file.

let numTrees = 0;
let numBlobs = 0;
let encodedBlobsSize = 0;
let decodedBlobsSize = 0;
for (const tree of snapshot.trees) {
for(const treeEntry of tree.entries) {
jatgarg marked this conversation as resolved.
Show resolved Hide resolved
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