Skip to content

Commit

Permalink
perf: create request/response cache for API services being called mul…
Browse files Browse the repository at this point in the history
…tiple times

PiperOrigin-RevId: 532290106
  • Loading branch information
Dustyn Loyda authored and copybara-github committed May 16, 2023
1 parent c8632e7 commit 82973a3
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,17 @@ import {LitElement, ReactiveController, ReactiveControllerHost} from 'lit';

import {APILoader} from '../../api_loader/api_loader.js';
import {RequestErrorEvent} from '../../base/events.js';
import {RequestCache} from '../../utils/request_cache.js';

const CACHE_SIZE = 100;

/**
* Controller that interfaces with the Maps JavaScript API Directions Service.
*/
export class DirectionsController implements ReactiveController {
private static service?: google.maps.DirectionsService;
private static readonly cache = new RequestCache<
google.maps.DirectionsRequest, google.maps.DirectionsResult>(CACHE_SIZE);

constructor(private readonly host: ReactiveControllerHost&LitElement) {
this.host.addController(this);
Expand All @@ -42,9 +47,14 @@ export class DirectionsController implements ReactiveController {
*/
async route(request: google.maps.DirectionsRequest):
Promise<google.maps.DirectionsResult|null> {
const service = await this.getService();
let responsePromise = DirectionsController.cache.get(request);
if (responsePromise === null) {
responsePromise =
this.getService().then((service) => service.route(request));
DirectionsController.cache.set(request, responsePromise);
}
try {
return await service.route(request);
return await responsePromise;
} catch (error: unknown) {
const requestErrorEvent = new RequestErrorEvent(error);
this.host.dispatchEvent(requestErrorEvent);
Expand Down
75 changes: 75 additions & 0 deletions utils/request_cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {LRUMap} from './lru_map.js';

/**
* A limited-capacity cache keyed by serialized request objects.
*/
export class RequestCache<RequestType, ResponseType> {
private readonly requestCacheMap: LRUMap<string, Promise<ResponseType>>;
/**
* @param capacity - The maximum number of objects to keep in the cache.
*/
constructor(capacity: number) {
this.requestCacheMap = new LRUMap(capacity);
}

/**
* Gets the cached result with the given request
*/
get(request: RequestType): Promise<ResponseType>|null {
return this.requestCacheMap.get(this.serialize(request)) ?? null;
}

/**
* Adds the provided request to the cache, replacing the
* existing result if one exists already.
*/
set(key: RequestType, value: Promise<ResponseType>) {
this.requestCacheMap.set(this.serialize(key), value);
}

/**
* Deterministically serializes arbitrary objects to strings.
*/
private serialize(request: RequestType): string {
interface UnknownObject {
[key: string]: unknown;
}

// Non-numeric keys in modern JS are iterated in order of insertion.
// Make a new object and insert the keys in alphabetical order so that
// the object is serialized alphabetically.
const replacer = (key: string, value: unknown) => {
if (value instanceof Object && !(value instanceof Array)) {
const obj = value as UnknownObject;
const sorted: UnknownObject = {};
for (const key of Object.keys(obj).sort()) {
sorted[key] = obj[key];
}
return sorted;
} else {
return value;
}
};
return JSON.stringify(request, replacer);
}
}
101 changes: 101 additions & 0 deletions utils/request_cache_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

// import 'jasmine'; (google3-only)

import {RequestCache} from './request_cache.js';

interface Request {
id: string;
values: number[];
location: string;
deliveryEnabled: boolean;
distance: number;
inventory: {[key: string]: number};
}

interface Response {
id: string;
cost: number;
delivered: boolean;
status: string;
}

const FAKE_REQUEST: Request = {
id: 'xHD87228BCE8',
values: [3, 5, 8, 2, 7, 8],
location: '123 Place St',
deliveryEnabled: true,
distance: 105.5,
inventory: {'D': 35, 'A': 10, 'C': 5, 'B': 20},
};

const SORTED_REQUEST: Request = {
deliveryEnabled: true,
distance: 105.5,
id: 'xHD87228BCE8',
inventory: {'A': 10, 'B': 20, 'C': 5, 'D': 35},
location: '123 Place St',
values: [3, 5, 8, 2, 7, 8],
};

const FAKE_RESPONSE: Response = {
id: 'xHD87228BCE8',
cost: 110.75,
delivered: true,
status: 'OK',
};

const FAKE_RESPONSE_2: Response = {
id: 'xHD87228BCE8',
cost: 110.75,
delivered: false,
status: 'UNKOWN',
};

describe('RequestCache', () => {
it('returns null when no request exists', async () => {
const requestCache = new RequestCache<Request, Response>(10);
const result = requestCache.get(FAKE_REQUEST);
expect(result).toBeNull();
});

it('returns the existing result when one exists', async () => {
const requestCache = new RequestCache<Request, Response>(10);
requestCache.set(FAKE_REQUEST, Promise.resolve(FAKE_RESPONSE));
const result = await requestCache.get(FAKE_REQUEST);
expect(result).toEqual(FAKE_RESPONSE);
});

it('updates result if request already exists', async () => {
const requestCache = new RequestCache<Request, Response>(10);
requestCache.set(FAKE_REQUEST, Promise.resolve(FAKE_RESPONSE));
requestCache.set(FAKE_REQUEST, Promise.resolve(FAKE_RESPONSE_2));
const result = await requestCache.get(FAKE_REQUEST);
expect(result).toEqual(FAKE_RESPONSE_2);
});

it('treats requests the same regardless of property order', async () => {
const requestCache = new RequestCache<Request, Response>(10);
requestCache.set(FAKE_REQUEST, Promise.resolve(FAKE_RESPONSE));
const result = await requestCache.get(SORTED_REQUEST);
expect(result).toEqual(FAKE_RESPONSE);
});
});

0 comments on commit 82973a3

Please sign in to comment.