Perf: improve reject build

This commit is contained in:
SukkaW
2023-12-10 02:12:53 +08:00
parent 2dc380dbf4
commit 2090d830da
3 changed files with 149 additions and 84 deletions

View File

@@ -82,7 +82,10 @@ function createFetchRetry($fetch: typeof fetch): typeof fetch {
} }
} catch (err: unknown) { } catch (err: unknown) {
if (err instanceof Error) { if (err instanceof Error) {
if (err.name === 'AbortError') { if (
err.name === 'AbortError'
|| ('digest' in err && err.digest === 'AbortError')
) {
console.log('[fetch abort]', url.toString()); console.log('[fetch abort]', url.toString());
return bail(err); return bail(err);
} }

View File

@@ -8,6 +8,7 @@ import { getGorhillPublicSuffixPromise } from './get-gorhill-publicsuffix';
import type { PublicSuffixList } from 'gorhill-publicsuffixlist'; import type { PublicSuffixList } from 'gorhill-publicsuffixlist';
import { isProbablyIpv4 } from './is-fast-ip'; import { isProbablyIpv4 } from './is-fast-ip';
import { traceAsync } from './trace-runner'; import { traceAsync } from './trace-runner';
import picocolors from 'picocolors';
const DEBUG_DOMAIN_TO_FIND: string | null = null; // example.com | null const DEBUG_DOMAIN_TO_FIND: string | null = null; // example.com | null
let foundDebugDomain = false; let foundDebugDomain = false;
@@ -36,11 +37,7 @@ const normalizeDomain = (domain: string) => {
return h[0] === '.' ? h.slice(1) : h; return h[0] === '.' ? h.slice(1) : h;
}; };
export async function processDomainLists(domainListsUrl: string | URL, includeAllSubDomain = false) { export async function processDomainLists(domainListsUrl: string, includeAllSubDomain = false) {
if (typeof domainListsUrl === 'string') {
domainListsUrl = new URL(domainListsUrl);
}
const domainSets = new Set<string>(); const domainSets = new Set<string>();
for await (const line of await fetchRemoteTextAndReadByLine(domainListsUrl)) { for await (const line of await fetchRemoteTextAndReadByLine(domainListsUrl)) {
@@ -50,7 +47,7 @@ export async function processDomainLists(domainListsUrl: string | URL, includeAl
} }
if (DEBUG_DOMAIN_TO_FIND && domainToAdd.includes(DEBUG_DOMAIN_TO_FIND)) { if (DEBUG_DOMAIN_TO_FIND && domainToAdd.includes(DEBUG_DOMAIN_TO_FIND)) {
warnOnce(domainListsUrl.toString(), false, DEBUG_DOMAIN_TO_FIND); warnOnce(domainListsUrl, false, DEBUG_DOMAIN_TO_FIND);
foundDebugDomain = true; foundDebugDomain = true;
} }
@@ -64,12 +61,8 @@ export async function processDomainLists(domainListsUrl: string | URL, includeAl
return domainSets; return domainSets;
} }
export async function processHosts(hostsUrl: string | URL, includeAllSubDomain = false, skipDomainCheck = false) { export async function processHosts(hostsUrl: string, includeAllSubDomain = false, skipDomainCheck = false) {
return traceAsync(`- processHosts: ${hostsUrl.toString()}`, async () => { return traceAsync(`- processHosts: ${hostsUrl}`, async () => {
if (typeof hostsUrl === 'string') {
hostsUrl = new URL(hostsUrl);
}
const domainSets = new Set<string>(); const domainSets = new Set<string>();
for await (const l of await fetchRemoteTextAndReadByLine(hostsUrl)) { for await (const l of await fetchRemoteTextAndReadByLine(hostsUrl)) {
@@ -82,7 +75,7 @@ export async function processHosts(hostsUrl: string | URL, includeAllSubDomain =
const _domain = domains.join(' ').trim(); const _domain = domains.join(' ').trim();
if (DEBUG_DOMAIN_TO_FIND && _domain.includes(DEBUG_DOMAIN_TO_FIND)) { if (DEBUG_DOMAIN_TO_FIND && _domain.includes(DEBUG_DOMAIN_TO_FIND)) {
warnOnce(hostsUrl.href, false, DEBUG_DOMAIN_TO_FIND); warnOnce(hostsUrl, false, DEBUG_DOMAIN_TO_FIND);
foundDebugDomain = true; foundDebugDomain = true;
} }
@@ -101,14 +94,25 @@ export async function processHosts(hostsUrl: string | URL, includeAllSubDomain =
}); });
} }
// eslint-disable-next-line sukka-ts/no-const-enum -- bun bundler is smart, maybe?
const enum ParseType {
WhiteIncludeSubdomain = 0,
WhiteAbsolute = -1,
BlackAbsolute = 1,
BlackIncludeSubdomain = 2,
ErrorMessage = 10
}
export async function processFilterRules( export async function processFilterRules(
filterRulesUrl: string | URL, filterRulesUrl: string,
fallbackUrls?: ReadonlyArray<string | URL> | undefined fallbackUrls?: readonly string[] | undefined
): Promise<{ white: Set<string>, black: Set<string>, foundDebugDomain: boolean }> { ): Promise<{ white: Set<string>, black: Set<string>, foundDebugDomain: boolean }> {
const whitelistDomainSets = new Set<string>(); const whitelistDomainSets = new Set<string>();
const blacklistDomainSets = new Set<string>(); const blacklistDomainSets = new Set<string>();
await traceAsync(`- processFilterRules: ${filterRulesUrl.toString()}`, async () => { const warningMessages: string[] = [];
await traceAsync(`- processFilterRules: ${filterRulesUrl}`, async () => {
const gorhill = await getGorhillPublicSuffixPromise(); const gorhill = await getGorhillPublicSuffixPromise();
/** /**
@@ -125,34 +129,35 @@ export async function processFilterRules(
if (DEBUG_DOMAIN_TO_FIND) { if (DEBUG_DOMAIN_TO_FIND) {
if (hostname.includes(DEBUG_DOMAIN_TO_FIND)) { if (hostname.includes(DEBUG_DOMAIN_TO_FIND)) {
warnOnce(filterRulesUrl.toString(), flag === 0 || flag === -1, DEBUG_DOMAIN_TO_FIND); warnOnce(filterRulesUrl, flag === ParseType.WhiteIncludeSubdomain || flag === ParseType.WhiteAbsolute, DEBUG_DOMAIN_TO_FIND);
foundDebugDomain = true; foundDebugDomain = true;
console.log({ result, flag });
} }
} }
switch (flag) { switch (flag) {
case 0: case ParseType.WhiteIncludeSubdomain:
if (hostname[0] !== '.') { if (hostname[0] !== '.') {
whitelistDomainSets.add(`.${hostname}`); whitelistDomainSets.add(`.${hostname}`);
} else { } else {
whitelistDomainSets.add(hostname); whitelistDomainSets.add(hostname);
} }
break; break;
case -1: case ParseType.WhiteAbsolute:
whitelistDomainSets.add(hostname); whitelistDomainSets.add(hostname);
break; break;
case 1: case ParseType.BlackAbsolute:
blacklistDomainSets.add(hostname); blacklistDomainSets.add(hostname);
break; break;
case 2: case ParseType.BlackIncludeSubdomain:
if (hostname[0] !== '.') { if (hostname[0] !== '.') {
blacklistDomainSets.add(`.${hostname}`); blacklistDomainSets.add(`.${hostname}`);
} else { } else {
blacklistDomainSets.add(hostname); blacklistDomainSets.add(hostname);
} }
break; break;
case ParseType.ErrorMessage:
warningMessages.push(hostname);
break;
default: default:
throw new Error(`Unknown flag: ${flag as any}`); throw new Error(`Unknown flag: ${flag as any}`);
} }
@@ -164,36 +169,24 @@ export async function processFilterRules(
lineCb(line); lineCb(line);
} }
} else { } else {
let filterRules; const filterRules = (await traceAsync(
picocolors.gray(`- download ${filterRulesUrl}`),
try { () => fetchAssets(filterRulesUrl, fallbackUrls),
const controller = new AbortController(); picocolors.gray
)).split('\n');
/** @type string[] */
filterRules = (
await Promise.any(
[filterRulesUrl, ...fallbackUrls].map(async url => {
const r = await fetchWithRetry(url, { signal: controller.signal, ...defaultRequestInit });
const text = await r.text();
console.log('[fetch finish]', url.toString());
controller.abort();
return text;
})
)
).split('\n');
} catch (e) {
console.log(`Download Rule for [${filterRulesUrl.toString()}] failed`);
throw e;
}
for (let i = 0, len = filterRules.length; i < len; i++) { for (let i = 0, len = filterRules.length; i < len; i++) {
lineCb(filterRules[i]); lineCb(filterRules[i]);
} }
} }
}); });
warningMessages.forEach(msg => {
console.warn(
picocolors.yellow(msg),
picocolors.gray(picocolors.underline(filterRulesUrl))
);
});
return { return {
white: whitelistDomainSets, white: whitelistDomainSets,
black: blacklistDomainSets, black: blacklistDomainSets,
@@ -204,10 +197,7 @@ export async function processFilterRules(
const R_KNOWN_NOT_NETWORK_FILTER_PATTERN = /[#%&=~]/; const R_KNOWN_NOT_NETWORK_FILTER_PATTERN = /[#%&=~]/;
const R_KNOWN_NOT_NETWORK_FILTER_PATTERN_2 = /(\$popup|\$removeparam|\$popunder)/; const R_KNOWN_NOT_NETWORK_FILTER_PATTERN_2 = /(\$popup|\$removeparam|\$popunder)/;
/** function parse($line: string, gorhill: PublicSuffixList): null | [hostname: string, flag: ParseType] {
* 0 white include subdomain, 1 black abosulte, 2 black include subdomain, -1 white
*/
function parse($line: string, gorhill: PublicSuffixList): null | [hostname: string, flag: 0 | 1 | 2 | -1] {
if ( if (
// doesn't include // doesn't include
!$line.includes('.') // rule with out dot can not be a domain !$line.includes('.') // rule with out dot can not be a domain
@@ -288,7 +278,7 @@ function parse($line: string, gorhill: PublicSuffixList): null | [hostname: stri
const isIncludeAllSubDomain = filter.isHostnameAnchor(); const isIncludeAllSubDomain = filter.isHostnameAnchor();
if (filter.isException() || filter.isBadFilter()) { if (filter.isException() || filter.isBadFilter()) {
return [hostname, isIncludeAllSubDomain ? 0 : -1]; return [hostname, isIncludeAllSubDomain ? ParseType.WhiteIncludeSubdomain : ParseType.WhiteAbsolute];
} }
const _1p = filter.firstParty(); const _1p = filter.firstParty();
@@ -296,7 +286,7 @@ function parse($line: string, gorhill: PublicSuffixList): null | [hostname: stri
if (_1p) { if (_1p) {
if (_1p === _3p) { if (_1p === _3p) {
return [hostname, isIncludeAllSubDomain ? 2 : 1]; return [hostname, isIncludeAllSubDomain ? ParseType.BlackIncludeSubdomain : ParseType.BlackAbsolute];
} }
return null; return null;
} }
@@ -381,11 +371,13 @@ function parse($line: string, gorhill: PublicSuffixList): null | [hostname: stri
const domain = normalizeDomain(_domain); const domain = normalizeDomain(_domain);
if (domain) { if (domain) {
return [domain, 0]; return [domain, ParseType.WhiteIncludeSubdomain];
} }
console.warn(' * [parse-filter E0001] (white) invalid domain:', _domain); return [
return null; `[parse-filter E0001] (white) invalid domain: ${_domain}`,
ParseType.ErrorMessage
];
} }
} }
@@ -418,11 +410,13 @@ function parse($line: string, gorhill: PublicSuffixList): null | [hostname: stri
const domain = normalizeDomain(_domain); const domain = normalizeDomain(_domain);
if (domain) { if (domain) {
return [domain, includeAllSubDomain ? 2 : 1]; return [domain, includeAllSubDomain ? ParseType.BlackIncludeSubdomain : ParseType.BlackAbsolute];
} }
console.warn(' * [parse-filter E0002] (black) invalid domain:', _domain);
return null; return [
`[parse-filter E0002] (black) invalid domain: ${_domain}`,
ParseType.ErrorMessage
];
} }
} }
@@ -451,11 +445,13 @@ function parse($line: string, gorhill: PublicSuffixList): null | [hostname: stri
const domain = normalizeDomain(_domain); const domain = normalizeDomain(_domain);
if (domain) { if (domain) {
return [domain, 2]; return [domain, ParseType.BlackIncludeSubdomain];
} }
console.warn(' * [parse-filter E0003] (black) invalid domain:', _domain);
return null; return [
`[paparse-filter E0003] (black) invalid domain: ${_domain}`,
ParseType.ErrorMessage
];
} }
/** /**
@@ -485,11 +481,13 @@ function parse($line: string, gorhill: PublicSuffixList): null | [hostname: stri
const domain = normalizeDomain(_domain); const domain = normalizeDomain(_domain);
if (domain) { if (domain) {
return [domain, 1]; return [domain, ParseType.BlackAbsolute];
} }
console.warn(' * [parse-filter E0004] (black) invalid domain:', _domain); return [
return null; `[parse-filter E0004] (black) invalid domain: ${_domain}`,
ParseType.ErrorMessage
];
} }
/** /**
@@ -512,11 +510,13 @@ function parse($line: string, gorhill: PublicSuffixList): null | [hostname: stri
const domain = normalizeDomain(_domain); const domain = normalizeDomain(_domain);
if (domain) { if (domain) {
return [domain, 1]; return [domain, ParseType.BlackAbsolute];
} }
console.warn(' * [parse-filter E0005] (black) invalid domain:', _domain); return [
return null; `[parse-filter E0005] (black) invalid domain: ${_domain}`,
ParseType.ErrorMessage
];
} }
if (lineStartsWithSingleDot) { if (lineStartsWithSingleDot) {
@@ -536,7 +536,7 @@ function parse($line: string, gorhill: PublicSuffixList): null | [hostname: stri
const tryNormalizeDomain = normalizeDomain(_domain); const tryNormalizeDomain = normalizeDomain(_domain);
if (tryNormalizeDomain === _domain) { if (tryNormalizeDomain === _domain) {
// the entire rule is domain // the entire rule is domain
return [line, 2]; return [line, ParseType.BlackIncludeSubdomain];
} }
} else { } else {
/** /**
@@ -554,11 +554,74 @@ function parse($line: string, gorhill: PublicSuffixList): null | [hostname: stri
const tryNormalizeDomain = normalizeDomain(line); const tryNormalizeDomain = normalizeDomain(line);
if (tryNormalizeDomain === line) { if (tryNormalizeDomain === line) {
// the entire rule is domain // the entire rule is domain
return [line, 2]; return [line, ParseType.BlackIncludeSubdomain];
} }
} }
console.warn(' * [parse-filter E0010] can not parse:', line); return [
`[parse-filter E0010] can not parse: ${line}`,
return null; ParseType.ErrorMessage
];
}
class CustomAbortError extends Error {
public readonly name = 'AbortError';
public readonly digest = 'AbortError';
}
function sleepWithAbort(ms: number, signal: AbortSignal) {
return new Promise<void>((resolve, reject) => {
signal.throwIfAborted();
signal.addEventListener('abort', stop);
Bun.sleep(ms).then(done).catch(doReject);
function done() {
signal.removeEventListener('abort', stop);
resolve();
}
function stop(this: AbortSignal) {
reject(this.reason);
}
function doReject(reason: unknown) {
signal.removeEventListener('abort', stop);
reject(reason);
}
});
}
async function fetchAssets(url: string, fallbackUrls: string[] | readonly string[]) {
const controller = new AbortController();
const fetchMainPromise = fetchWithRetry(url, { signal: controller.signal, ...defaultRequestInit })
.then(r => r.text())
.then(text => {
console.log(picocolors.gray('[fetch finish]'), picocolors.gray(url));
controller.abort();
return text;
});
const createFetchFallbackPromise = async (url: string, index: number) => {
// Most assets can be downloaded within 250ms. To avoid wasting bandwidth, we will wait for 350ms before downloading from the fallback URL.
try {
await sleepWithAbort(200 + (index + 1) * 10, controller.signal);
} catch {
console.log(picocolors.gray('[fetch cancelled early]'), picocolors.gray(url));
throw new CustomAbortError();
}
if (controller.signal.aborted) {
console.log(picocolors.gray('[fetch cancelled]'), picocolors.gray(url));
throw new CustomAbortError();
}
const res = await fetchWithRetry(url, { signal: controller.signal, ...defaultRequestInit });
const text = await res.text();
controller.abort();
return text;
};
return Promise.any([
fetchMainPromise,
...fallbackUrls.map(createFetchFallbackPromise)
]).catch(e => {
console.log(`Download Rule for [${url}] failed`);
throw e;
});
} }

View File

@@ -1,24 +1,24 @@
import path from 'path'; import path from 'path';
import picocolors from 'picocolors'; import picocolors from 'picocolors';
function traceSync<T>(prefix: string, fn: () => T): T { type Formatter = (result: string) => string;
export function traceSync<T>(prefix: string, fn: () => T, timeFormatter: Formatter = picocolors.blue): T {
const start = Bun.nanoseconds(); const start = Bun.nanoseconds();
const result = fn(); const result = fn();
const end = Bun.nanoseconds(); const end = Bun.nanoseconds();
console.log(`${picocolors.gray(`[${((end - start) / 1e6).toFixed(3)}ms]`)} ${prefix}`); console.log(`${timeFormatter(`[${((end - start) / 1e6).toFixed(3)}ms]`)} ${prefix}`);
return result; return result;
} }
traceSync.skip = <T>(prefix: string, fn: () => T): T => fn(); traceSync.skip = <T>(_prefix: string, fn: () => T): T => fn();
export { traceSync };
const traceAsync = async <T>(prefix: string, fn: () => Promise<T>): Promise<T> => { export const traceAsync = async <T>(prefix: string, fn: () => Promise<T>, timeFormatter: Formatter = picocolors.blue): Promise<T> => {
const start = Bun.nanoseconds(); const start = Bun.nanoseconds();
const result = await fn(); const result = await fn();
const end = Bun.nanoseconds(); const end = Bun.nanoseconds();
console.log(`${picocolors.gray(`[${((end - start) / 1e6).toFixed(3)}ms]`)} ${prefix}`); console.log(`${timeFormatter(`[${((end - start) / 1e6).toFixed(3)}ms]`)} ${prefix}`);
return result; return result;
}; };
export { traceAsync };
export interface TaskResult { export interface TaskResult {
readonly start: number, readonly start: number,
@@ -26,16 +26,15 @@ export interface TaskResult {
readonly taskName: string readonly taskName: string
} }
const task = <T>(importMetaPath: string, fn: () => Promise<T>, customname: string | null = null) => { export const task = <T>(importMetaPath: string, fn: () => Promise<T>, customname: string | null = null) => {
const taskName = customname ?? path.basename(importMetaPath, path.extname(importMetaPath)); const taskName = customname ?? path.basename(importMetaPath, path.extname(importMetaPath));
return async () => { return async () => {
console.log(`🏃 [${taskName}] Start executing`); console.log(`🏃 [${taskName}] Start executing`);
const start = Bun.nanoseconds(); const start = Bun.nanoseconds();
await fn(); await fn();
const end = Bun.nanoseconds(); const end = Bun.nanoseconds();
console.log(`✅ [${taskName}] [${((end - start) / 1e6).toFixed(3)}ms] Executed successfully`); console.log(`✅ [${taskName}] ${picocolors.blue(`[${((end - start) / 1e6).toFixed(3)}ms]`)} Executed successfully`);
return { start, end, taskName } as TaskResult; return { start, end, taskName } as TaskResult;
}; };
}; };
export { task };