Skip to content

perf: generator performance optimization work #380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading