Perf: avoid mutate string in ruleset base

This commit is contained in:
SukkaW 2024-12-14 21:14:29 +08:00
parent 5c85a0c504
commit 9c82e5346c
5 changed files with 57 additions and 77 deletions

View File

@ -100,14 +100,14 @@ export abstract class RuleOutput<TPreprocessed = unknown> {
for (let i = 0, len = domains.length; i < len; i++) { for (let i = 0, len = domains.length; i < len; i++) {
d = domains[i]; d = domains[i];
if (d !== null) { if (d !== null) {
this.addDomain(d); this.domainTrie.add(d, false, null, 0);
} }
} }
return this; return this;
} }
addDomainSuffix(domain: string) { addDomainSuffix(domain: string, lineFromDot = domain[0] === '.') {
this.domainTrie.add(domain, true); this.domainTrie.add(domain, true, lineFromDot ? 1 : 0);
return this; return this;
} }
@ -126,9 +126,9 @@ export abstract class RuleOutput<TPreprocessed = unknown> {
private async addFromDomainsetPromise(source: AsyncIterable<string> | Iterable<string> | string[]) { private async addFromDomainsetPromise(source: AsyncIterable<string> | Iterable<string> | string[]) {
for await (const line of source) { for await (const line of source) {
if (line[0] === '.') { if (line[0] === '.') {
this.addDomainSuffix(line); this.addDomainSuffix(line, true);
} else { } else {
this.addDomain(line); this.domainTrie.add(line, false, null, 0);
} }
} }
} }
@ -147,10 +147,10 @@ export abstract class RuleOutput<TPreprocessed = unknown> {
switch (type) { switch (type) {
case 'DOMAIN': case 'DOMAIN':
this.addDomain(value); this.domainTrie.add(value, false, null, 0);
break; break;
case 'DOMAIN-SUFFIX': case 'DOMAIN-SUFFIX':
this.addDomainSuffix(value); this.addDomainSuffix(value, false);
break; break;
case 'DOMAIN-KEYWORD': case 'DOMAIN-KEYWORD':
this.addDomainKeyword(value); this.addDomainKeyword(value);

View File

@ -8,55 +8,47 @@ import { looseTldtsOpt } from '../../constants/loose-tldts-opt';
import { fastStringCompare } from '../misc'; import { fastStringCompare } from '../misc';
import escapeStringRegexp from 'escape-string-regexp-node'; import escapeStringRegexp from 'escape-string-regexp-node';
type Preprocessed = string[]; export class DomainsetOutput extends RuleOutput<string[]> {
export class DomainsetOutput extends RuleOutput<Preprocessed> {
protected type = 'domainset' as const; protected type = 'domainset' as const;
private $surge: string[] = ['this_ruleset_is_made_by_sukkaw.ruleset.skk.moe'];
private $clash: string[] = ['this_ruleset_is_made_by_sukkaw.ruleset.skk.moe'];
private $singbox_domains: string[] = ['this_ruleset_is_made_by_sukkaw.ruleset.skk.moe'];
private $singbox_domains_suffixes: string[] = ['this_ruleset_is_made_by_sukkaw.ruleset.skk.moe'];
preprocess() { preprocess() {
const kwfilter = createKeywordFilter(this.domainKeywords); const kwfilter = createKeywordFilter(this.domainKeywords);
const results: string[] = []; const results: string[] = [];
this.domainTrie.dump((domain) => { this.domainTrie.dumpWithoutDot((domain, subdomain) => {
if (kwfilter(domain)) { if (kwfilter(domain)) {
return; return;
} }
this.$surge.push(subdomain ? '.' + domain : domain);
this.$clash.push(subdomain ? `+.${domain}` : domain);
(subdomain ? this.$singbox_domains : this.$singbox_domains_suffixes).push(domain);
results.push(domain); results.push(domain);
}, true); }, true);
const sorted = results; return results;
sorted.push('this_ruleset_is_made_by_sukkaw.ruleset.skk.moe');
return sorted;
} }
surge(): string[] { surge(): string[] {
return this.$preprocessed; return this.$surge;
} }
clash(): string[] { clash(): string[] {
return this.$preprocessed.map(i => (i[0] === '.' ? `+${i}` : i)); return this.$clash;
} }
singbox(): string[] { singbox(): string[] {
const domains: string[] = [];
const domainSuffixes: string[] = [];
for (let i = 0, len = this.$preprocessed.length; i < len; i++) {
const domain = this.$preprocessed[i];
if (domain[0] === '.') {
domainSuffixes.push(domain.slice(1));
} else {
domains.push(domain);
}
}
return RuleOutput.jsonToLines({ return RuleOutput.jsonToLines({
version: 2, version: 2,
rules: [{ rules: [{
domain: domains, domain: this.$singbox_domains,
domain_suffix: domainSuffixes domain_suffix: this.$singbox_domains_suffixes
}] }]
} satisfies SingboxSourceFormat); } satisfies SingboxSourceFormat);
} }

View File

@ -23,13 +23,13 @@ export class RulesetOutput extends RuleOutput<Preprocessed> {
const domainSuffixes: string[] = []; const domainSuffixes: string[] = [];
const sortedDomainRules: string[] = []; const sortedDomainRules: string[] = [];
this.domainTrie.dump((domain) => { this.domainTrie.dumpWithoutDot((domain, includeAllSubdomain) => {
if (kwfilter(domain)) { if (kwfilter(domain)) {
return; return;
} }
if (domain[0] === '.') { if (includeAllSubdomain) {
domainSuffixes.push(domain.slice(1)); domainSuffixes.push(domain);
sortedDomainRules.push(`DOMAIN-SUFFIX,${domain.slice(1)}`); sortedDomainRules.push(`DOMAIN-SUFFIX,${domain}`);
} else { } else {
domains.push(domain); domains.push(domain);
sortedDomainRules.push(`DOMAIN,${domain}`); sortedDomainRules.push(`DOMAIN,${domain}`);

View File

@ -1,6 +1,15 @@
import { createTrie } from './trie';
import { describe, it } from 'mocha'; import { describe, it } from 'mocha';
import { expect } from 'expect'; import { expect } from 'expect';
import { HostnameSmolTrie, HostnameTrie } from './trie';
function createTrie<Meta = any>(from: string[] | Set<string> | null, smolTree: true): HostnameSmolTrie<Meta>;
function createTrie<Meta = any>(from?: string[] | Set<string> | null, smolTree?: false): HostnameTrie<Meta>;
function createTrie<_Meta = any>(from?: string[] | Set<string> | null, smolTree = true) {
if (smolTree) {
return new HostnameSmolTrie(from);
}
return new HostnameTrie(from);
};
// describe('hostname to tokens', () => { // describe('hostname to tokens', () => {
// it('should split hostname into tokens.', () => { // it('should split hostname into tokens.', () => {

View File

@ -170,10 +170,8 @@ abstract class Triebase<Meta = any> {
}; };
public contains(suffix: string, includeAllSubdomain = suffix[0] === '.'): boolean { public contains(suffix: string, includeAllSubdomain = suffix[0] === '.'): boolean {
let hostnameFromIndex = 0; const hostnameFromIndex = suffix[0] === '.' ? 1 : 0;
if (suffix[0] === '.') {
hostnameFromIndex = 1;
}
const res = this.walkIntoLeafWithSuffix(suffix, hostnameFromIndex); const res = this.walkIntoLeafWithSuffix(suffix, hostnameFromIndex);
if (!res) return false; if (!res) return false;
if (includeAllSubdomain) return res.node[1]; if (includeAllSubdomain) return res.node[1];
@ -333,13 +331,9 @@ abstract class Triebase<Meta = any> {
public find( public find(
inputSuffix: string, inputSuffix: string,
subdomainOnly = inputSuffix[0] === '.', subdomainOnly = inputSuffix[0] === '.',
hostnameFromIndex = 0 hostnameFromIndex = inputSuffix[0] === '.' ? 1 : 0
// /** @default true */ includeEqualWithSuffix = true // /** @default true */ includeEqualWithSuffix = true
): string[] { ): string[] {
if (inputSuffix[0] === '.') {
hostnameFromIndex = 1;
}
const inputTokens = hostnameToTokens(inputSuffix, hostnameFromIndex); const inputTokens = hostnameToTokens(inputSuffix, hostnameFromIndex);
const res = this.walkIntoLeafWithTokens(inputTokens); const res = this.walkIntoLeafWithTokens(inputTokens);
if (res === null) return []; if (res === null) return [];
@ -395,11 +389,7 @@ abstract class Triebase<Meta = any> {
* Method used to assert whether the given prefix exists in the Trie. * Method used to assert whether the given prefix exists in the Trie.
*/ */
public has(suffix: string, includeAllSubdomain = suffix[0] === '.'): boolean { public has(suffix: string, includeAllSubdomain = suffix[0] === '.'): boolean {
let hostnameFromIndex = 0; const hostnameFromIndex = suffix[0] === '.' ? 1 : 0;
if (suffix[0] === '.') {
hostnameFromIndex = 1;
}
const res = this.walkIntoLeafWithSuffix(suffix, hostnameFromIndex); const res = this.walkIntoLeafWithSuffix(suffix, hostnameFromIndex);
@ -409,6 +399,18 @@ abstract class Triebase<Meta = any> {
return true; return true;
}; };
public dumpWithoutDot(onSuffix: (suffix: string, subdomain: boolean) => void, withSort = false) {
const handleSuffix = (suffix: string[], subdomain: boolean) => {
onSuffix(fastStringArrayJoin(suffix, '.'), subdomain);
};
if (withSort) {
this.walkWithSort(handleSuffix);
} else {
this.walk(handleSuffix);
}
}
public dump(onSuffix: (suffix: string) => void, withSort?: boolean): void; public dump(onSuffix: (suffix: string) => void, withSort?: boolean): void;
public dump(onSuffix?: null, withSort?: boolean): string[]; public dump(onSuffix?: null, withSort?: boolean): string[];
public dump(onSuffix?: ((suffix: string) => void) | null, withSort = false): string[] | void { public dump(onSuffix?: ((suffix: string) => void) | null, withSort = false): string[] | void {
@ -490,14 +492,10 @@ abstract class Triebase<Meta = any> {
export class HostnameSmolTrie<Meta = any> extends Triebase<Meta> { export class HostnameSmolTrie<Meta = any> extends Triebase<Meta> {
public smolTree = true; public smolTree = true;
add(suffix: string, includeAllSubdomain = suffix[0] === '.', meta?: Meta, hostnameFromIndex = 0): void { add(suffix: string, includeAllSubdomain = suffix[0] === '.', meta?: Meta, hostnameFromIndex = suffix[0] === '.' ? 1 : 0): void {
let node: TrieNode<Meta> = this.$root; let node: TrieNode<Meta> = this.$root;
let curNodeChildren: Map<string, TrieNode<Meta>> = node[3]; let curNodeChildren: Map<string, TrieNode<Meta>> = node[3];
if (hostnameFromIndex === 0 && suffix[0] === '.') {
hostnameFromIndex = 1;
}
const onToken = (token: string) => { const onToken = (token: string) => {
curNodeChildren = node[3]; curNodeChildren = node[3];
if (curNodeChildren.has(token)) { if (curNodeChildren.has(token)) {
@ -544,11 +542,7 @@ export class HostnameSmolTrie<Meta = any> extends Triebase<Meta> {
node[4] = meta!; node[4] = meta!;
} }
public whitelist(suffix: string, includeAllSubdomain = suffix[0] === '.', hostnameFromIndex = 0) { public whitelist(suffix: string, includeAllSubdomain = suffix[0] === '.', hostnameFromIndex = suffix[0] === '.' ? 1 : 0) {
if (suffix[0] === '.') {
hostnameFromIndex = 1;
}
const tokens = hostnameToTokens(suffix, hostnameFromIndex); const tokens = hostnameToTokens(suffix, hostnameFromIndex);
const res = this.getSingleChildLeaf(tokens); const res = this.getSingleChildLeaf(tokens);
@ -584,7 +578,7 @@ export class HostnameTrie<Meta = any> extends Triebase<Meta> {
return this.$size; return this.$size;
} }
add(suffix: string, includeAllSubdomain = suffix[0] === '.', meta?: Meta, hostnameFromIndex = 0): void { add(suffix: string, includeAllSubdomain = suffix[0] === '.', meta?: Meta, hostnameFromIndex = suffix[0] === '.' ? 1 : 0): void {
let node: TrieNode<Meta> = this.$root; let node: TrieNode<Meta> = this.$root;
const onToken = (token: string) => { const onToken = (token: string) => {
@ -599,10 +593,6 @@ export class HostnameTrie<Meta = any> extends Triebase<Meta> {
return false; return false;
}; };
if (hostnameFromIndex === 0 && suffix[0] === '.') {
hostnameFromIndex = 1;
}
// When walkHostnameTokens returns true, we should skip the rest // When walkHostnameTokens returns true, we should skip the rest
if (walkHostnameTokens(suffix, onToken, hostnameFromIndex)) { if (walkHostnameTokens(suffix, onToken, hostnameFromIndex)) {
return; return;
@ -620,17 +610,6 @@ export class HostnameTrie<Meta = any> extends Triebase<Meta> {
} }
} }
export function createTrie<Meta = any>(from: string[] | Set<string> | null, smolTree: true): HostnameSmolTrie<Meta>;
export function createTrie<Meta = any>(from?: string[] | Set<string> | null, smolTree?: false): HostnameTrie<Meta>;
export function createTrie<_Meta = any>(from?: string[] | Set<string> | null, smolTree = true) {
if (smolTree) {
return new HostnameSmolTrie(from);
}
return new HostnameTrie(from);
};
export type Trie = ReturnType<typeof createTrie>;
// function deepEqualArray(a: string[], b: string[]) { // function deepEqualArray(a: string[], b: string[]) {
// let len = a.length; // let len = a.length;
// if (len !== b.length) return false; // if (len !== b.length) return false;