Skip to content

Commit

Permalink
fix(performance): pre-compute and lazily initialize endpoint methods (#…
Browse files Browse the repository at this point in the history
…622)

We have observed that the octokit initialisation is quite slow,
especially in combination with [probots](https://github.com/probot/probot)
which creates a new octokit instance for each incoming request.
This causes the main event loop to block for a considerable time.

With our change we moved the preparation of the endpoints api object
into the module scope and use a Proxy object to defer the initialisation
of octokit defaults and decorations to the first API call per method.

Although we have not measured it, we believe the overhead that comes
from the proxied method call is insignificant in comparison to the
network latency of an API call.

Co-authored-by: wolfy1339 <4595477+wolfy1339@users.noreply.github.com>
  • Loading branch information
ZauberNerd and wolfy1339 committed Jun 14, 2023
1 parent 7b8950d commit a7887d0
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 45 deletions.
112 changes: 70 additions & 42 deletions src/endpoints-to-methods.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,83 @@
import type { Octokit } from "@octokit/core";
import type {
EndpointOptions,
RequestParameters,
RequestMethod,
Route,
Url,
} from "@octokit/types";
import type {
EndpointsDefaultsAndDecorations,
EndpointDecorations,
} from "./types";
import type { EndpointOptions, RequestParameters, Route } from "@octokit/types";
import ENDPOINTS from "./generated/endpoints";
import type { RestEndpointMethods } from "./generated/method-types";
import type { EndpointDecorations } from "./types";

type EndpointMethods = {
[methodName: string]: typeof Octokit.prototype.request;
// The following code was refactored in: https://github.com/octokit/plugin-rest-endpoint-methods.js/pull/622
// to optimise the runtime performance of Octokit initialization.
//
// This optimization involves two key changes:
// 1. Pre-Computation: The endpoint methods are pre-computed once at module load
// time instead of each invocation of `endpointsToMethods()`.
// 2. Lazy initialization and caching: We use a Proxy for each scope to only
// initialize methods that are actually called. This reduces runtime overhead
// as the initialization involves deep merging of objects. The initialized
// methods are then cached for future use.

const endpointMethodsMap = new Map();
for (const [scope, endpoints] of Object.entries(ENDPOINTS)) {
for (const [methodName, endpoint] of Object.entries(endpoints)) {
const [route, defaults, decorations] = endpoint;
const [method, url] = route.split(/ /);
const endpointDefaults = Object.assign(
{
method,
url,
},
defaults
);

if (!endpointMethodsMap.has(scope)) {
endpointMethodsMap.set(scope, new Map());
}

endpointMethodsMap.get(scope).set(methodName, {
scope,
methodName,
endpointDefaults,
decorations,
});
}
}

type ProxyTarget = {
octokit: Octokit;
scope: string;
cache: Record<string, (...args: any[]) => any>;
};

export function endpointsToMethods(
octokit: Octokit,
endpointsMap: EndpointsDefaultsAndDecorations
) {
const newMethods = {} as { [key: string]: object };
const handler = {
get({ octokit, scope, cache }: ProxyTarget, methodName: string) {
if (cache[methodName]) {
return cache[methodName];
}

const { decorations, endpointDefaults } = endpointMethodsMap
.get(scope)
.get(methodName);

for (const [scope, endpoints] of Object.entries(endpointsMap)) {
for (const [methodName, endpoint] of Object.entries(endpoints)) {
const [route, defaults, decorations] = endpoint;
const [method, url] = route.split(/ /) as [RequestMethod, Url];
const endpointDefaults: EndpointOptions = Object.assign(
{ method, url },
defaults
if (decorations) {
cache[methodName] = decorate(
octokit,
scope,
methodName,
endpointDefaults,
decorations
);
} else {
cache[methodName] = octokit.request.defaults(endpointDefaults);
}

if (!newMethods[scope]) {
newMethods[scope] = {};
}
return cache[methodName];
},
};

const scopeMethods = newMethods[scope] as EndpointMethods;

if (decorations) {
scopeMethods[methodName] = decorate(
octokit,
scope,
methodName,
endpointDefaults,
decorations
);
continue;
}
export function endpointsToMethods(octokit: Octokit): RestEndpointMethods {
const newMethods = {} as { [key: string]: object };

scopeMethods[methodName] = octokit.request.defaults(endpointDefaults);
}
for (const scope of endpointMethodsMap.keys()) {
newMethods[scope] = new Proxy({ octokit, scope, cache: {} }, handler);
}

return newMethods as RestEndpointMethods;
Expand Down
5 changes: 2 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import type { Octokit } from "@octokit/core";

import ENDPOINTS from "./generated/endpoints";
export type { RestEndpointMethodTypes } from "./generated/parameters-and-response-types";
import { VERSION } from "./version";
import type { Api } from "./types";
import { endpointsToMethods } from "./endpoints-to-methods";

export function restEndpointMethods(octokit: Octokit): Api {
const api = endpointsToMethods(octokit, ENDPOINTS);
const api = endpointsToMethods(octokit);
return {
rest: api,
};
}
restEndpointMethods.VERSION = VERSION;

export function legacyRestEndpointMethods(octokit: Octokit): Api["rest"] & Api {
const api = endpointsToMethods(octokit, ENDPOINTS);
const api = endpointsToMethods(octokit);
return {
...api,
rest: api,
Expand Down

0 comments on commit a7887d0

Please sign in to comment.