From 85801b1b9e4885dc44904605d9c6ca49487921da Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 31 Dec 2023 02:32:07 +0800 Subject: [PATCH] Perf: use filesystem cache --- Build/build-speedtest-domainset.ts | 72 +++++++++++++++++------------ Build/download-mock-assets.ts | 8 ++-- Build/download-publicsuffixlist.ts | 17 ++++++- Build/lib/cache-filesystem.ts | 20 +++++++- Build/lib/parse-filter.ts | 37 +++++++-------- Build/lib/random-int.bench.ts | 16 +++++++ Build/lib/reject-data-source.ts | 46 ++++++++++-------- bun.lockb | Bin 91834 -> 92178 bytes package.json | 1 + 9 files changed, 144 insertions(+), 73 deletions(-) create mode 100644 Build/lib/random-int.bench.ts diff --git a/Build/build-speedtest-domainset.ts b/Build/build-speedtest-domainset.ts index 77db7b0a..9f0e1edf 100644 --- a/Build/build-speedtest-domainset.ts +++ b/Build/build-speedtest-domainset.ts @@ -12,17 +12,17 @@ import { getGorhillPublicSuffixPromise } from './lib/get-gorhill-publicsuffix'; import picocolors from 'picocolors'; import { fetchRemoteTextByLine } from './lib/fetch-text-by-line'; import { processLine } from './lib/process-line'; +import { TTL, deserializeArray, fsCache, serializeArray } from './lib/cache-filesystem'; const s = new Sema(2); const latestTopUserAgentsPromise = fetchWithRetry('https://unpkg.com/top-user-agents@latest/index.json') - .then(res => res.json()); + .then(res => res.json()).then(userAgents => userAgents.filter(ua => ua.startsWith('Mozilla/5.0 '))); const querySpeedtestApi = async (keyword: string): Promise> => { - const topUserAgents = (await Promise.all([ - latestTopUserAgentsPromise, - s.acquire() - ]))[0]; + const topUserAgents = await latestTopUserAgentsPromise; + + const url = `https://www.speedtest.net/api/js/servers?engine=js&search=${keyword}&limit=100`; try { const randomUserAgent = topUserAgents[Math.floor(Math.random() * topUserAgents.length)]; @@ -30,39 +30,51 @@ const querySpeedtestApi = async (keyword: string): Promise> console.log(key); console.time(key); - const res = await fetchWithRetry(`https://www.speedtest.net/api/js/servers?engine=js&search=${keyword}&limit=100`, { - headers: { - dnt: '1', - Referer: 'https://www.speedtest.net/', - accept: 'application/json, text/plain, */*', - 'User-Agent': randomUserAgent, - 'Accept-Language': 'en-US,en;q=0.9', - ...(randomUserAgent.includes('Chrome') - ? { - 'Sec-Ch-Ua-Mobile': '?0', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'Sec-Gpc': '1' + const json = await fsCache.apply( + url, + () => s.acquire().then(() => fetchWithRetry(url, { + headers: { + dnt: '1', + Referer: 'https://www.speedtest.net/', + accept: 'application/json, text/plain, */*', + 'User-Agent': randomUserAgent, + 'Accept-Language': 'en-US,en;q=0.9', + ...(randomUserAgent.includes('Chrome') + ? { + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'Sec-Gpc': '1' + } + : {}) + }, + signal: AbortSignal.timeout(1000 * 4), + retry: { + retries: 2 + } + })).then(r => r.json>()).then(data => data.reduce( + (prev, cur) => { + const hn = tldts.getHostname(cur.url, { detectIp: false }); + if (hn) { + prev.push(hn); } - : {}) - }, - signal: AbortSignal.timeout(1000 * 4), - retry: { - retries: 2 + return prev; + }, [] + )).finally(() => s.release()), + { + ttl: TTL.ONE_WEEK(), + serializer: serializeArray, + deserializer: deserializeArray } - }); - - const json = await res.json>(); + ); console.timeEnd(key); - return json.map(({ url }) => tldts.getHostname(url, { detectIp: false })); + return json; } catch (e) { console.log(e); return []; - } finally { - s.release(); } }; diff --git a/Build/download-mock-assets.ts b/Build/download-mock-assets.ts index 9cee2264..19952ee7 100644 --- a/Build/download-mock-assets.ts +++ b/Build/download-mock-assets.ts @@ -4,10 +4,10 @@ import path from 'path'; import { fetchWithRetry } from './lib/fetch-retry'; const ASSETS_LIST = { - 'www-google-analytics-com_ga.js': 'https://unpkg.com/@adguard/scriptlets@1/dist/redirect-files/google-analytics-ga.js', - 'www-googletagservices-com_gpt.js': 'https://unpkg.com/@adguard/scriptlets@1/dist/redirect-files/googletagservices-gpt.js', - 'www-google-analytics-com_analytics.js': 'https://unpkg.com/@adguard/scriptlets@1/dist/redirect-files/google-analytics.js', - 'www-googlesyndication-com_adsbygoogle.js': 'https://unpkg.com/@adguard/scriptlets@1/dist/redirect-files/googlesyndication-adsbygoogle.js' + 'www-google-analytics-com_ga.js': 'https://raw.githubusercontent.com/AdguardTeam/Scriptlets/master/dist/redirect-files/google-analytics-ga.js', + 'www-googletagservices-com_gpt.js': 'https://raw.githubusercontent.com/AdguardTeam/Scriptlets/master/dist/redirect-files/googletagservices-gpt.js', + 'www-google-analytics-com_analytics.js': 'https://raw.githubusercontent.com/AdguardTeam/Scriptlets/master/dist/redirect-files/google-analytics.js', + 'www-googlesyndication-com_adsbygoogle.js': 'https://raw.githubusercontent.com/AdguardTeam/Scriptlets/master/dist/redirect-files/googlesyndication-adsbygoogle.js' } as const; const mockDir = path.resolve(import.meta.dir, '../Mock'); diff --git a/Build/download-publicsuffixlist.ts b/Build/download-publicsuffixlist.ts index 8ce61fd4..150189fb 100644 --- a/Build/download-publicsuffixlist.ts +++ b/Build/download-publicsuffixlist.ts @@ -1,5 +1,20 @@ +import { TTL, fsCache } from './lib/cache-filesystem'; import { defaultRequestInit, fetchWithRetry } from './lib/fetch-retry'; import { createMemoizedPromise } from './lib/memo-promise'; import { traceAsync } from './lib/trace-runner'; -export const getPublicSuffixListTextPromise = createMemoizedPromise(() => traceAsync('obtain public_suffix_list', () => fetchWithRetry('https://publicsuffix.org/list/public_suffix_list.dat', defaultRequestInit).then(r => r.text()))); +export const getPublicSuffixListTextPromise = createMemoizedPromise( + () => traceAsync( + 'obtain public_suffix_list', + () => fsCache.apply( + 'https://publicsuffix.org/list/public_suffix_list.dat', + () => fetchWithRetry('https://publicsuffix.org/list/public_suffix_list.dat', defaultRequestInit).then(r => r.text()), + { + // https://github.com/publicsuffix/list/blob/master/.github/workflows/tld-update.yml + // Though the action runs every 24 hours, the IANA list is updated every 7 days. + // So a 3 day TTL should be enough. + ttl: TTL.THREE_DAYS() + } + ) + ) +); diff --git a/Build/lib/cache-filesystem.ts b/Build/lib/cache-filesystem.ts index 6a1fecfc..935452cb 100644 --- a/Build/lib/cache-filesystem.ts +++ b/Build/lib/cache-filesystem.ts @@ -127,12 +127,28 @@ export class Cache { } } -// export const fsCache = new Cache({ cachePath: path.resolve(import.meta.dir, '../../.cache') }); +export const fsCache = new Cache({ cachePath: path.resolve(import.meta.dir, '../../.cache') }); // process.on('exit', () => { // fsCache.destroy(); // }); -const separator = String.fromCharCode(0); +const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; +// Add some randomness to the cache ttl to avoid thundering herd +export const TTL = { + TWLVE_HOURS: () => randomInt(9, 14) * 60 * 60 * 1000, + THREE_DAYS: () => randomInt(2, 4) * 24 * 60 * 60 * 1000, + ONE_WEEK: () => randomInt(5, 8) * 24 * 60 * 60 * 1000, + TWO_WEEKS: () => randomInt(12, 16) * 24 * 60 * 60 * 1000, + TEN_DAYS: () => randomInt(9, 11) * 24 * 60 * 60 * 1000 +}; + +const separator = String.fromCharCode(0); +// const textEncoder = new TextEncoder(); +// const textDecoder = new TextDecoder(); +// export const serializeString = (str: string) => textEncoder.encode(str); +// export const deserializeString = (str: string) => textDecoder.decode(new Uint8Array(str.split(separator).map(Number))); export const serializeSet = (set: Set) => Array.from(set).join(separator); export const deserializeSet = (str: string) => new Set(str.split(separator)); +export const serializeArray = (arr: string[]) => arr.join(separator); +export const deserializeArray = (str: string) => str.split(separator); diff --git a/Build/lib/parse-filter.ts b/Build/lib/parse-filter.ts index fba447ab..9035f1b2 100644 --- a/Build/lib/parse-filter.ts +++ b/Build/lib/parse-filter.ts @@ -9,14 +9,15 @@ import { traceAsync } from './trace-runner'; import picocolors from 'picocolors'; import { normalizeDomain } from './normalize-domain'; import { fetchAssets } from './fetch-assets'; +import { deserializeSet, fsCache, serializeSet } from './cache-filesystem'; const DEBUG_DOMAIN_TO_FIND: string | null = null; // example.com | null let foundDebugDomain = false; -export function processDomainLists(domainListsUrl: string, includeAllSubDomain = false, _ttl: number | null = null) { - return traceAsync(`- processDomainLists: ${domainListsUrl}`, /* () => fsCache.apply( +export function processDomainLists(domainListsUrl: string, includeAllSubDomain = false, ttl: number | null = null) { + return traceAsync(`- processDomainLists: ${domainListsUrl}`, () => fsCache.apply( domainListsUrl, - */async () => { + async () => { const domainSets = new Set(); for await (const line of await fetchRemoteTextByLine(domainListsUrl)) { @@ -32,19 +33,19 @@ export function processDomainLists(domainListsUrl: string, includeAllSubDomain = } return domainSets; - });/* , + }, { ttl, temporaryBypass: DEBUG_DOMAIN_TO_FIND !== null, serializer: serializeSet, deserializer: deserializeSet } - )); */ + )); } -export function processHosts(hostsUrl: string, includeAllSubDomain = false, skipDomainCheck = false, _ttl: number | null = null) { - return traceAsync(`- processHosts: ${hostsUrl}`, /* () => fsCache.apply( +export function processHosts(hostsUrl: string, includeAllSubDomain = false, skipDomainCheck = false, ttl: number | null = null) { + return traceAsync(`- processHosts: ${hostsUrl}`, () => fsCache.apply( hostsUrl, - */async () => { + async () => { const domainSets = new Set(); for await (const l of await fetchRemoteTextByLine(hostsUrl)) { @@ -73,14 +74,14 @@ export function processHosts(hostsUrl: string, includeAllSubDomain = false, skip console.log(picocolors.gray('[process hosts]'), picocolors.gray(hostsUrl), picocolors.gray(domainSets.size)); return domainSets; - }); - /* { + }, + { ttl, temporaryBypass: DEBUG_DOMAIN_TO_FIND !== null, serializer: serializeSet, deserializer: deserializeSet } - ) */ + )); } // eslint-disable-next-line sukka-ts/no-const-enum -- bun bundler is smart, maybe? @@ -95,15 +96,15 @@ const enum ParseType { export async function processFilterRules( filterRulesUrl: string, fallbackUrls?: readonly string[] | undefined | null, - _ttl: number | null = null + ttl: number | null = null ): Promise<{ white: string[], black: string[], foundDebugDomain: boolean }> { - const [white, black, warningMessages] = await traceAsync(`- processFilterRules: ${filterRulesUrl}`, /* () => fsCache.apply<[ + const [white, black, warningMessages] = await traceAsync(`- processFilterRules: ${filterRulesUrl}`, () => fsCache.apply<[ white: string[], black: string[], warningMessages: string[] ]>( filterRulesUrl, - */async () => { + async () => { const whitelistDomainSets = new Set(); const blacklistDomainSets = new Set(); @@ -168,7 +169,7 @@ export async function processFilterRules( // TODO-SUKKA: add cache here if (!fallbackUrls || fallbackUrls.length === 0) { for await (const line of await fetchRemoteTextByLine(filterRulesUrl)) { - // don't trim here + // don't trim here lineCb(line); } } else { @@ -191,14 +192,14 @@ export async function processFilterRules( Array.from(blacklistDomainSets), warningMessages ]; - }); - /* { + }, + { ttl, temporaryBypass: DEBUG_DOMAIN_TO_FIND !== null, serializer: JSON.stringify, deserializer: JSON.parse } - ) */ + )); warningMessages.forEach(msg => { console.warn( diff --git a/Build/lib/random-int.bench.ts b/Build/lib/random-int.bench.ts new file mode 100644 index 00000000..d6497c60 --- /dev/null +++ b/Build/lib/random-int.bench.ts @@ -0,0 +1,16 @@ +import { bench, group, run } from 'mitata'; +import { randomInt as nativeRandomInt } from 'crypto'; + +const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; + +group('random-int', () => { + bench('crypto.randomInt', () => { + nativeRandomInt(3, 7); + }); + + bench('Math.random', () => { + randomInt(3, 7); + }); +}); + +run(); diff --git a/Build/lib/reject-data-source.ts b/Build/lib/reject-data-source.ts index 50e83fdb..cbd44413 100644 --- a/Build/lib/reject-data-source.ts +++ b/Build/lib/reject-data-source.ts @@ -1,14 +1,20 @@ +import { TTL } from './cache-filesystem'; + export const HOSTS = [ ['https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=0&mimetype=plaintext', true], ['https://someonewhocares.org/hosts/hosts', true], - ['https://raw.githubusercontent.com/hoshsadiq/adblock-nocoin-list/master/hosts.txt', false], - ['https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/spy.txt', true], + // no coin list is not actively maintained, but it updates daily when being maintained, so we set a 3 days cache ttl + ['https://raw.githubusercontent.com/hoshsadiq/adblock-nocoin-list/master/hosts.txt', false, false, TTL.THREE_DAYS()], + // have not been updated for more than a year, so we set a 14 days cache ttl + ['https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/spy.txt', true, false, TTL.TWO_WEEKS()], ['https://raw.githubusercontent.com/jerryn70/GoodbyeAds/master/Extension/GoodbyeAds-Xiaomi-Extension.txt', false], - ['https://raw.githubusercontent.com/jdlingyu/ad-wars/master/hosts', false], + ['https://raw.githubusercontent.com/jerryn70/GoodbyeAds/master/Extension/GoodbyeAds-Huawei-Extension.txt', false], + // ad-wars is not actively maintained, so we set a 7 days cache ttl + ['https://raw.githubusercontent.com/jdlingyu/ad-wars/master/hosts', false, false, TTL.ONE_WEEK()], ['https://raw.githubusercontent.com/durablenapkin/block/master/luminati.txt', true], // CoinBlockerList - // Although the hosts file is still actively maintained, the hosts_browser file is not updated since 2021-07, so we set a 10 days cache ttl - ['https://zerodot1.gitlab.io/CoinBlockerLists/hosts_browser', true, true, 10 * 24 * 60 * 60 * 1000], + // Although the hosts file is still actively maintained, the hosts_browser file is not updated since 2021-07, so we set a 14 days cache ttl + ['https://zerodot1.gitlab.io/CoinBlockerLists/hosts_browser', true, true, TTL.TWO_WEEKS()], // Curben's UrlHaus Malicious URL Blocklist // 'https://curbengh.github.io/urlhaus-filter/urlhaus-filter-agh-online.txt', // 'https://urlhaus-filter.pages.dev/urlhaus-filter-agh-online.txt', @@ -21,23 +27,24 @@ export const HOSTS = [ // Curben's PUP Domains Blocklist // 'https://curbengh.github.io/pup-filter/pup-filter-agh.txt' // 'https://pup-filter.pages.dev/pup-filter-agh.txt' - // The PUP filter has paused the update since 2023-05, so we set a 7 days cache ttl - ['https://curbengh.github.io/pup-filter/pup-filter-hosts.txt', true, true, 7 * 24 * 60 * 60 * 1000] + // The PUP filter has paused the update since 2023-05, so we set a 14 days cache ttl + ['https://curbengh.github.io/pup-filter/pup-filter-hosts.txt', true, true, TTL.TWO_WEEKS()] ] as const; export const DOMAIN_LISTS = [ // BarbBlock - // The barbblock list has never been updated since 2019-05, so we set a 10 days cache ttl - ['https://paulgb.github.io/BarbBlock/blacklists/domain-list.txt', true, 10 * 24 * 60 * 60 * 1000], + // The barbblock list has never been updated since 2019-05, so we set a 14 days cache ttl + ['https://paulgb.github.io/BarbBlock/blacklists/domain-list.txt', true, TTL.TWO_WEEKS()], // DigitalSide Threat-Intel - OSINT Hub - ['https://osint.digitalside.it/Threat-Intel/lists/latestdomains.txt', true], + // Update once per day + ['https://osint.digitalside.it/Threat-Intel/lists/latestdomains.txt', true, 24 * 60 * 60 * 1000], // AdGuard CNAME Filter Combined - // Update on a 7 days basis, so we add a 36 hours cache ttl - ['https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/data/combined_disguised_ads_justdomains.txt', true, 36 * 60 * 60 * 1000], - ['https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/data/combined_disguised_trackers_justdomains.txt', true, 36 * 60 * 60 * 1000], - ['https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/data/combined_disguised_clickthroughs_justdomains.txt', true, 36 * 60 * 60 * 1000], - ['https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/data/combined_disguised_microsites_justdomains.txt', true, 36 * 60 * 60 * 1000], - ['https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/data/combined_disguised_mail_trackers_justdomains.txt', true, 36 * 60 * 60 * 1000] + // Update on a 7 days basis, so we add a 3 hours cache ttl + ['https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/data/combined_disguised_ads_justdomains.txt', true, TTL.THREE_DAYS()], + ['https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/data/combined_disguised_trackers_justdomains.txt', true, TTL.THREE_DAYS()], + ['https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/data/combined_disguised_clickthroughs_justdomains.txt', true, TTL.THREE_DAYS()], + ['https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/data/combined_disguised_microsites_justdomains.txt', true, TTL.THREE_DAYS()], + ['https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/data/combined_disguised_mail_trackers_justdomains.txt', true, TTL.THREE_DAYS()] ] as const; export const ADGUARD_FILTERS = [ @@ -130,14 +137,17 @@ export const ADGUARD_FILTERS = [ // GameConsoleAdblockList 'https://raw.githubusercontent.com/DandelionSprout/adfilt/master/GameConsoleAdblockList.txt', // PiHoleBlocklist + // Update almost once per 3 months, let's set a 10 days cache ttl [ 'https://perflyst.github.io/PiHoleBlocklist/SmartTV-AGH.txt', [ 'https://raw.githubusercontent.com/Perflyst/PiHoleBlocklist/master/SmartTV-AGH.txt' - ] + ], + TTL.TEN_DAYS() ], // Spam404 - 'https://raw.githubusercontent.com/Spam404/lists/master/adblock-list.txt', + // Not actively maintained, let's use a 10 days cache ttl + ['https://raw.githubusercontent.com/Spam404/lists/master/adblock-list.txt', null, TTL.TEN_DAYS()], // Brave First Party & First Party CNAME 'https://raw.githubusercontent.com/brave/adblock-lists/master/brave-lists/brave-firstparty.txt' ] as const; diff --git a/bun.lockb b/bun.lockb index 9bb227706bc34a77d94b388aa12f01a228fa6d32..8dba67e4d25bd8416e3731091edf27d7d333231a 100755 GIT binary patch delta 13816 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 delta 13593 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 LciLl1)WiP`FJSd_ diff --git a/package.json b/package.json index e09d829f..d6a4a65b 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "eslint": "^8.56.0", "eslint-config-sukka": "4.1.10-beta.2", "eslint-formatter-sukka": "4.1.9", + "mitata": "^0.1.6", "typescript": "^5.3.3" }, "resolutions": {