Skip to content

Commit db9571c

Browse files
authored
perf: generator performance optimization work (#380)
1 parent e9016ae commit db9571c

26 files changed

+948
-495
lines changed

.github/workflows/ci.yml

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ jobs:
5252
env:
5353
CI_BROWSER: /opt/hostedtoolcache/firefox/${{ matrix.firefox }}/x64/firefox
5454
CI_BROWSER_FLAGS: -headless
55+
SKIP_PERF: 1
5556

5657
test-servers:
5758
name: Node.js & Deno Tests

chompfile.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,10 @@ run = 'node -C source --enable-source-maps $DEP'
8484
name = 'test:integration'
8585
dep = 'integration:'
8686

87-
8887
[[task]]
8988
name = 'integration:#'
9089
deps = ['test/##.test.js', 'lib']
90+
# env = { JSPM_GENERATOR_LOG = '1' }
9191
run = 'node -C source --enable-source-maps $DEP'
9292

9393
[[task]]

package-lock.json

+5-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"open": "^8.4.1",
6565
"prettier": "^2.8.4",
6666
"rollup": "^2.79.1",
67-
"typescript": "^4.9.5"
67+
"typescript": "^5.5.4"
6868
},
6969
"files": [
7070
"dist",

src/common/fetch-common.ts

+89-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export interface Response {
1+
export interface WrappedResponse {
22
status: number;
33
statusText?: string;
44
text?(): Promise<string>;
@@ -7,29 +7,105 @@ export interface Response {
77
}
88

99
export type FetchFn = (
10-
url: URL,
10+
url: URL | string,
1111
...args: any[]
12-
) => Promise<Response | globalThis.Response>;
12+
) => Promise<WrappedResponse | globalThis.Response>
1313

14-
let retryCount = 3;
14+
export type WrappedFetch = ((
15+
url: URL | string,
16+
...args: any[]
17+
) => Promise<WrappedResponse | globalThis.Response>) & {
18+
arrayBuffer: (url: URL | string, ...args: any[]) => Promise<ArrayBuffer | null>,
19+
text: (url: URL | string, ...args: any[]) => Promise<string | null>
20+
};
21+
22+
let retryCount = 5, poolSize = 100;
1523

1624
export function setRetryCount(count: number) {
1725
retryCount = count;
1826
}
1927

28+
export function setFetchPoolSize(size: number) {
29+
poolSize = size;
30+
}
31+
2032
/**
21-
* Wraps a fetch request with retry logic on exceptions, which is useful for
22-
* spotty connections that may fail intermittently.
33+
* Wraps a fetch request with pooling, and retry logic on exceptions (emfile / network errors).
2334
*/
24-
export function wrapWithRetry(fetch: FetchFn): FetchFn {
25-
return async function (url: URL, ...args: any[]) {
35+
export function wrappedFetch(fetch: FetchFn): WrappedFetch {
36+
const wrappedFetch = async function (url: URL | string, ...args: any[]) {
37+
url = url.toString();
2638
let retries = 0;
27-
while (true) {
28-
try {
29-
return await fetch(url, ...args);
30-
} catch (e) {
31-
if (retries++ >= retryCount) throw e;
39+
try {
40+
await pushFetchPool();
41+
while (true) {
42+
try {
43+
return await fetch(url, ...args);
44+
} catch (e) {
45+
if (retries++ >= retryCount) throw e;
46+
}
3247
}
48+
} finally {
49+
popFetchPool();
3350
}
3451
};
52+
wrappedFetch.arrayBuffer = async function (url, ...args) {
53+
url = url.toString();
54+
let retries = 0;
55+
try {
56+
await pushFetchPool();
57+
while (true) {
58+
try {
59+
var res = await fetch(url, ...args);
60+
} catch (e) {
61+
if (retries++ >= retryCount)
62+
throw e;
63+
continue;
64+
}
65+
switch (res.status) {
66+
case 200:
67+
case 304:
68+
break;
69+
// not found = null
70+
case 404:
71+
return null;
72+
default:
73+
throw new Error(`Invalid status code ${res.status}`);
74+
}
75+
try {
76+
return await res.arrayBuffer();
77+
} catch (e) {
78+
if (retries++ >= retryCount &&
79+
e.code === "ERR_SOCKET_TIMEOUT" ||
80+
e.code === "ETIMEOUT" ||
81+
e.code === "ECONNRESET" ||
82+
e.code === 'FETCH_ERROR') {
83+
84+
}
85+
}
86+
}
87+
} finally {
88+
popFetchPool();
89+
}
90+
};
91+
wrappedFetch.text = async function (url, ...args) {
92+
const arrayBuffer = await this.arrayBuffer(url, ...args);
93+
if (!arrayBuffer)
94+
return null;
95+
return new TextDecoder().decode(arrayBuffer);
96+
};
97+
return wrappedFetch;
98+
}
99+
100+
// restrict in-flight fetches to a pool of 100
101+
let p = [];
102+
let c = 0;
103+
function pushFetchPool () {
104+
if (++c > poolSize)
105+
return new Promise(r => p.push(r));
106+
}
107+
function popFetchPool () {
108+
c--;
109+
if (p.length)
110+
p.shift()();
35111
}

src/common/fetch-deno.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { fileURLToPath } from "url";
2-
import { wrapWithRetry, FetchFn } from "./fetch-common.js";
2+
import { wrappedFetch, WrappedFetch } from "./fetch-common.js";
33

44
export function clearCache() {}
55

6-
export const fetch: FetchFn = wrapWithRetry(async function (
6+
export const fetch: WrappedFetch = wrappedFetch(async function (
77
url: URL,
88
...args: any[]
99
) {

src/common/fetch-native.ts

+2-39
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,5 @@
1-
import { FetchFn, wrapWithRetry } from "./fetch-common.js";
1+
import { WrappedFetch, wrappedFetch } from "./fetch-common.js";
22

3-
// Browser native fetch doesn't deal well with high contention
4-
// restrict in-flight fetches to a pool of 100
5-
let p = [];
6-
let c = 0;
7-
function pushFetchPool() {
8-
if (++c > 100) return new Promise((r) => p.push(r));
9-
}
10-
function popFetchPool() {
11-
c--;
12-
if (p.length) p.shift()();
13-
}
14-
15-
export const fetch: FetchFn = wrapWithRetry(async function fetch(url, opts) {
16-
const poolQueue = pushFetchPool();
17-
if (poolQueue) await poolQueue;
18-
try {
19-
return await globalThis.fetch(url as any, opts);
20-
} catch (e) {
21-
// CORS errors throw a fetch type error
22-
// Instead, treat this as an actual unauthorized response
23-
if (e instanceof TypeError) {
24-
return {
25-
status: 401,
26-
async text() {
27-
return "";
28-
},
29-
async json() {
30-
throw new Error("Not JSON");
31-
},
32-
arrayBuffer() {
33-
return new ArrayBuffer(0);
34-
},
35-
};
36-
}
37-
} finally {
38-
popFetchPool();
39-
}
40-
});
3+
export const fetch: WrappedFetch = wrappedFetch(globalThis.fetch);
414

425
export const clearCache = () => {};

src/common/fetch-node.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @ts-ignore
22
import version from "../version.js";
3-
import { wrapWithRetry, FetchFn } from "./fetch-common.js";
3+
import { wrappedFetch, WrappedFetch } from "./fetch-common.js";
44
import path from "path";
55
import { homedir } from "os";
66
import process from "process";
@@ -65,11 +65,10 @@ const dirResponse = {
6565
},
6666
};
6767

68-
export const fetch: FetchFn = wrapWithRetry(async function (
68+
export const fetch: WrappedFetch = wrappedFetch(async function (
6969
url: URL,
7070
opts?: Record<string, any>
7171
) {
72-
if (!opts) throw new Error("Always expect fetch options to be passed");
7372
const urlString = url.toString();
7473
const protocol = urlString.slice(0, urlString.indexOf(":") + 1);
7574
let source: string | Buffer;

src/common/fetch-vscode.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FetchFn, wrapWithRetry } from "./fetch-common.js";
1+
import { WrappedFetch, wrappedFetch } from "./fetch-common.js";
22
import { fetch as _fetch } from "./fetch-native.js";
33
export { clearCache } from "./fetch-native.js";
44

@@ -33,11 +33,10 @@ const dirResponse = {
3333
// @ts-ignore
3434
const vscode = require("vscode");
3535

36-
export const fetch: FetchFn = wrapWithRetry(async function (
36+
export const fetch: WrappedFetch = wrappedFetch(async function (
3737
url: URL,
3838
opts?: Record<string, any>
3939
) {
40-
if (!opts) throw new Error("Always expect fetch options to be passed");
4140
const urlString = url.toString();
4241
const protocol = urlString.slice(0, urlString.indexOf(":") + 1);
4342
switch (protocol) {

0 commit comments

Comments
 (0)