Skip to content

Commit

Permalink
perf: generator performance optimization work (#380)
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford authored Sep 9, 2024
1 parent e9016ae commit db9571c
Show file tree
Hide file tree
Showing 26 changed files with 948 additions and 495 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ jobs:
env:
CI_BROWSER: /opt/hostedtoolcache/firefox/${{ matrix.firefox }}/x64/firefox
CI_BROWSER_FLAGS: -headless
SKIP_PERF: 1

test-servers:
name: Node.js & Deno Tests
Expand Down
2 changes: 1 addition & 1 deletion chompfile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ run = 'node -C source --enable-source-maps $DEP'
name = 'test:integration'
dep = 'integration:'


[[task]]
name = 'integration:#'
deps = ['test/##.test.js', 'lib']
# env = { JSPM_GENERATOR_LOG = '1' }
run = 'node -C source --enable-source-maps $DEP'

[[task]]
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"open": "^8.4.1",
"prettier": "^2.8.4",
"rollup": "^2.79.1",
"typescript": "^4.9.5"
"typescript": "^5.5.4"
},
"files": [
"dist",
Expand Down
102 changes: 89 additions & 13 deletions src/common/fetch-common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface Response {
export interface WrappedResponse {
status: number;
statusText?: string;
text?(): Promise<string>;
Expand All @@ -7,29 +7,105 @@ export interface Response {
}

export type FetchFn = (
url: URL,
url: URL | string,
...args: any[]
) => Promise<Response | globalThis.Response>;
) => Promise<WrappedResponse | globalThis.Response>

let retryCount = 3;
export type WrappedFetch = ((
url: URL | string,
...args: any[]
) => Promise<WrappedResponse | globalThis.Response>) & {
arrayBuffer: (url: URL | string, ...args: any[]) => Promise<ArrayBuffer | null>,
text: (url: URL | string, ...args: any[]) => Promise<string | null>
};

let retryCount = 5, poolSize = 100;

export function setRetryCount(count: number) {
retryCount = count;
}

export function setFetchPoolSize(size: number) {
poolSize = size;
}

/**
* Wraps a fetch request with retry logic on exceptions, which is useful for
* spotty connections that may fail intermittently.
* Wraps a fetch request with pooling, and retry logic on exceptions (emfile / network errors).
*/
export function wrapWithRetry(fetch: FetchFn): FetchFn {
return async function (url: URL, ...args: any[]) {
export function wrappedFetch(fetch: FetchFn): WrappedFetch {
const wrappedFetch = async function (url: URL | string, ...args: any[]) {
url = url.toString();
let retries = 0;
while (true) {
try {
return await fetch(url, ...args);
} catch (e) {
if (retries++ >= retryCount) throw e;
try {
await pushFetchPool();
while (true) {
try {
return await fetch(url, ...args);
} catch (e) {
if (retries++ >= retryCount) throw e;
}
}
} finally {
popFetchPool();
}
};
wrappedFetch.arrayBuffer = async function (url, ...args) {
url = url.toString();
let retries = 0;
try {
await pushFetchPool();
while (true) {
try {
var res = await fetch(url, ...args);
} catch (e) {
if (retries++ >= retryCount)
throw e;
continue;
}
switch (res.status) {
case 200:
case 304:
break;
// not found = null
case 404:
return null;
default:
throw new Error(`Invalid status code ${res.status}`);
}
try {
return await res.arrayBuffer();
} catch (e) {
if (retries++ >= retryCount &&
e.code === "ERR_SOCKET_TIMEOUT" ||
e.code === "ETIMEOUT" ||
e.code === "ECONNRESET" ||
e.code === 'FETCH_ERROR') {

}
}
}
} finally {
popFetchPool();
}
};
wrappedFetch.text = async function (url, ...args) {
const arrayBuffer = await this.arrayBuffer(url, ...args);
if (!arrayBuffer)
return null;
return new TextDecoder().decode(arrayBuffer);
};
return wrappedFetch;
}

// restrict in-flight fetches to a pool of 100
let p = [];
let c = 0;
function pushFetchPool () {
if (++c > poolSize)
return new Promise(r => p.push(r));
}
function popFetchPool () {
c--;
if (p.length)
p.shift()();
}
4 changes: 2 additions & 2 deletions src/common/fetch-deno.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { fileURLToPath } from "url";
import { wrapWithRetry, FetchFn } from "./fetch-common.js";
import { wrappedFetch, WrappedFetch } from "./fetch-common.js";

export function clearCache() {}

export const fetch: FetchFn = wrapWithRetry(async function (
export const fetch: WrappedFetch = wrappedFetch(async function (
url: URL,
...args: any[]
) {
Expand Down
41 changes: 2 additions & 39 deletions src/common/fetch-native.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,5 @@
import { FetchFn, wrapWithRetry } from "./fetch-common.js";
import { WrappedFetch, wrappedFetch } from "./fetch-common.js";

// Browser native fetch doesn't deal well with high contention
// restrict in-flight fetches to a pool of 100
let p = [];
let c = 0;
function pushFetchPool() {
if (++c > 100) return new Promise((r) => p.push(r));
}
function popFetchPool() {
c--;
if (p.length) p.shift()();
}

export const fetch: FetchFn = wrapWithRetry(async function fetch(url, opts) {
const poolQueue = pushFetchPool();
if (poolQueue) await poolQueue;
try {
return await globalThis.fetch(url as any, opts);
} catch (e) {
// CORS errors throw a fetch type error
// Instead, treat this as an actual unauthorized response
if (e instanceof TypeError) {
return {
status: 401,
async text() {
return "";
},
async json() {
throw new Error("Not JSON");
},
arrayBuffer() {
return new ArrayBuffer(0);
},
};
}
} finally {
popFetchPool();
}
});
export const fetch: WrappedFetch = wrappedFetch(globalThis.fetch);

export const clearCache = () => {};
5 changes: 2 additions & 3 deletions src/common/fetch-node.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-ignore
import version from "../version.js";
import { wrapWithRetry, FetchFn } from "./fetch-common.js";
import { wrappedFetch, WrappedFetch } from "./fetch-common.js";
import path from "path";
import { homedir } from "os";
import process from "process";
Expand Down Expand Up @@ -65,11 +65,10 @@ const dirResponse = {
},
};

export const fetch: FetchFn = wrapWithRetry(async function (
export const fetch: WrappedFetch = wrappedFetch(async function (
url: URL,
opts?: Record<string, any>
) {
if (!opts) throw new Error("Always expect fetch options to be passed");
const urlString = url.toString();
const protocol = urlString.slice(0, urlString.indexOf(":") + 1);
let source: string | Buffer;
Expand Down
5 changes: 2 additions & 3 deletions src/common/fetch-vscode.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FetchFn, wrapWithRetry } from "./fetch-common.js";
import { WrappedFetch, wrappedFetch } from "./fetch-common.js";
import { fetch as _fetch } from "./fetch-native.js";
export { clearCache } from "./fetch-native.js";

Expand Down Expand Up @@ -33,11 +33,10 @@ const dirResponse = {
// @ts-ignore
const vscode = require("vscode");

export const fetch: FetchFn = wrapWithRetry(async function (
export const fetch: WrappedFetch = wrappedFetch(async function (
url: URL,
opts?: Record<string, any>
) {
if (!opts) throw new Error("Always expect fetch options to be passed");
const urlString = url.toString();
const protocol = urlString.slice(0, urlString.indexOf(":") + 1);
switch (protocol) {
Expand Down
Loading

0 comments on commit db9571c

Please sign in to comment.