Skip to content

Commit a7887d0

Browse files
fix(performance): pre-compute and lazily initialize endpoint methods (#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 <[email protected]>
1 parent 7b8950d commit a7887d0

File tree

2 files changed

+72
-45
lines changed

2 files changed

+72
-45
lines changed

src/endpoints-to-methods.ts

Lines changed: 70 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,83 @@
11
import type { Octokit } from "@octokit/core";
2-
import type {
3-
EndpointOptions,
4-
RequestParameters,
5-
RequestMethod,
6-
Route,
7-
Url,
8-
} from "@octokit/types";
9-
import type {
10-
EndpointsDefaultsAndDecorations,
11-
EndpointDecorations,
12-
} from "./types";
2+
import type { EndpointOptions, RequestParameters, Route } from "@octokit/types";
3+
import ENDPOINTS from "./generated/endpoints";
134
import type { RestEndpointMethods } from "./generated/method-types";
5+
import type { EndpointDecorations } from "./types";
146

15-
type EndpointMethods = {
16-
[methodName: string]: typeof Octokit.prototype.request;
7+
// The following code was refactored in: https://github.com/octokit/plugin-rest-endpoint-methods.js/pull/622
8+
// to optimise the runtime performance of Octokit initialization.
9+
//
10+
// This optimization involves two key changes:
11+
// 1. Pre-Computation: The endpoint methods are pre-computed once at module load
12+
// time instead of each invocation of `endpointsToMethods()`.
13+
// 2. Lazy initialization and caching: We use a Proxy for each scope to only
14+
// initialize methods that are actually called. This reduces runtime overhead
15+
// as the initialization involves deep merging of objects. The initialized
16+
// methods are then cached for future use.
17+
18+
const endpointMethodsMap = new Map();
19+
for (const [scope, endpoints] of Object.entries(ENDPOINTS)) {
20+
for (const [methodName, endpoint] of Object.entries(endpoints)) {
21+
const [route, defaults, decorations] = endpoint;
22+
const [method, url] = route.split(/ /);
23+
const endpointDefaults = Object.assign(
24+
{
25+
method,
26+
url,
27+
},
28+
defaults
29+
);
30+
31+
if (!endpointMethodsMap.has(scope)) {
32+
endpointMethodsMap.set(scope, new Map());
33+
}
34+
35+
endpointMethodsMap.get(scope).set(methodName, {
36+
scope,
37+
methodName,
38+
endpointDefaults,
39+
decorations,
40+
});
41+
}
42+
}
43+
44+
type ProxyTarget = {
45+
octokit: Octokit;
46+
scope: string;
47+
cache: Record<string, (...args: any[]) => any>;
1748
};
1849

19-
export function endpointsToMethods(
20-
octokit: Octokit,
21-
endpointsMap: EndpointsDefaultsAndDecorations
22-
) {
23-
const newMethods = {} as { [key: string]: object };
50+
const handler = {
51+
get({ octokit, scope, cache }: ProxyTarget, methodName: string) {
52+
if (cache[methodName]) {
53+
return cache[methodName];
54+
}
55+
56+
const { decorations, endpointDefaults } = endpointMethodsMap
57+
.get(scope)
58+
.get(methodName);
2459

25-
for (const [scope, endpoints] of Object.entries(endpointsMap)) {
26-
for (const [methodName, endpoint] of Object.entries(endpoints)) {
27-
const [route, defaults, decorations] = endpoint;
28-
const [method, url] = route.split(/ /) as [RequestMethod, Url];
29-
const endpointDefaults: EndpointOptions = Object.assign(
30-
{ method, url },
31-
defaults
60+
if (decorations) {
61+
cache[methodName] = decorate(
62+
octokit,
63+
scope,
64+
methodName,
65+
endpointDefaults,
66+
decorations
3267
);
68+
} else {
69+
cache[methodName] = octokit.request.defaults(endpointDefaults);
70+
}
3371

34-
if (!newMethods[scope]) {
35-
newMethods[scope] = {};
36-
}
72+
return cache[methodName];
73+
},
74+
};
3775

38-
const scopeMethods = newMethods[scope] as EndpointMethods;
39-
40-
if (decorations) {
41-
scopeMethods[methodName] = decorate(
42-
octokit,
43-
scope,
44-
methodName,
45-
endpointDefaults,
46-
decorations
47-
);
48-
continue;
49-
}
76+
export function endpointsToMethods(octokit: Octokit): RestEndpointMethods {
77+
const newMethods = {} as { [key: string]: object };
5078

51-
scopeMethods[methodName] = octokit.request.defaults(endpointDefaults);
52-
}
79+
for (const scope of endpointMethodsMap.keys()) {
80+
newMethods[scope] = new Proxy({ octokit, scope, cache: {} }, handler);
5381
}
5482

5583
return newMethods as RestEndpointMethods;

src/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
import type { Octokit } from "@octokit/core";
22

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

98
export function restEndpointMethods(octokit: Octokit): Api {
10-
const api = endpointsToMethods(octokit, ENDPOINTS);
9+
const api = endpointsToMethods(octokit);
1110
return {
1211
rest: api,
1312
};
1413
}
1514
restEndpointMethods.VERSION = VERSION;
1615

1716
export function legacyRestEndpointMethods(octokit: Octokit): Api["rest"] & Api {
18-
const api = endpointsToMethods(octokit, ENDPOINTS);
17+
const api = endpointsToMethods(octokit);
1918
return {
2019
...api,
2120
rest: api,

0 commit comments

Comments
 (0)