mirror of
https://github.com/SukkaW/Surge.git
synced 2026-09-12 10:34:35 +08:00
Perf: fast sing-box JSON print
This commit is contained in:
@@ -21,6 +21,16 @@ export function getOutputWorkerFarm(): OutputWorkerFarm {
|
||||
return farm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot the farm ahead of time. Spawning the threads costs ~200-300ms (each loads
|
||||
* @swc-node/register and compiles the module graph), and since every big output
|
||||
* dispatches at the very end of a task, that cost otherwise lands entirely on the
|
||||
* critical path. Call this as early as the task starts so it overlaps the downloads.
|
||||
*/
|
||||
export function warmOutputWorkerFarm(): void {
|
||||
getOutputWorkerFarm();
|
||||
}
|
||||
|
||||
export async function endOutputWorkerFarm(): Promise<void> {
|
||||
if (farm) {
|
||||
const f = farm;
|
||||
|
||||
@@ -2,9 +2,9 @@ import { BaseWriteStrategy } from './base';
|
||||
import { appendArrayInPlace } from 'foxts/append-array-in-place';
|
||||
import { noop } from 'foxts/noop';
|
||||
import { withIdentityContent } from '../misc';
|
||||
import stringify from 'json-stringify-pretty-compact';
|
||||
import { OUTPUT_SINGBOX_DIR } from '../../constants/dir';
|
||||
import { MARKER_DOMAIN } from '../../constants/description';
|
||||
import { fastStringArrayJoin } from 'foxts/fast-string-array-join';
|
||||
|
||||
interface SingboxHeadlessRule {
|
||||
domain: string[],
|
||||
@@ -27,13 +27,91 @@ export interface SingboxSourceFormat {
|
||||
rules: SingboxHeadlessRule[]
|
||||
}
|
||||
|
||||
/**
|
||||
* json-stringify-pretty-compact spends most of its time re-serializing the whole
|
||||
* subtree at every nesting level just to decide whether it fits on one line -- for
|
||||
* a 145k-domain ruleset that is several full passes over a multi-megabyte string,
|
||||
* plus a final split('\n').
|
||||
*
|
||||
* Our document shape is fixed (`{version, rules: [rule]}`, rule values are flat
|
||||
* arrays of string/number), so we emit the lines directly and only probe as many
|
||||
* items as it takes to know a line cannot fit.
|
||||
*/
|
||||
const SINGBOX_MAX_LENGTH = 120;
|
||||
/** rule values live at indent 6 */
|
||||
const RULE_VALUE_INDENT = 6;
|
||||
|
||||
type SingboxRuleValue = string[] | number[];
|
||||
|
||||
/** prettified JSON of `arr` when it fits `budget`, else null -- bails out early */
|
||||
function inlineIfFits(arr: SingboxRuleValue, budget: number): string | null {
|
||||
if (arr.length === 0) {
|
||||
return '[]';
|
||||
}
|
||||
|
||||
let len = 2; // [ and ]
|
||||
if (len > budget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const first = JSON.stringify(arr[0]);
|
||||
len += first.length;
|
||||
if (len > budget) {
|
||||
return null;
|
||||
}
|
||||
const parts: string[] = [
|
||||
first
|
||||
];
|
||||
for (let i = 1, l = arr.length; i < l; i++) {
|
||||
const item = JSON.stringify(arr[i]);
|
||||
len += item.length + 2; // ', ' separator
|
||||
if (len > budget) {
|
||||
return null;
|
||||
}
|
||||
parts.push(item);
|
||||
}
|
||||
return '[' + fastStringArrayJoin(parts, ', ') + ']';
|
||||
}
|
||||
|
||||
export function singboxSourceToLines(rule: SingboxHeadlessRule): string[] {
|
||||
const keys = Object.keys(rule) as Array<keyof SingboxHeadlessRule>;
|
||||
const lines: string[] = ['{', ' "version": 2,', ' "rules": [', ' {'];
|
||||
|
||||
for (let i = 0, l = keys.length; i < l; i++) {
|
||||
const key = keys[i];
|
||||
const arr = rule[key];
|
||||
if (!Array.isArray(arr)) {
|
||||
throw new TypeError('singbox rule source value is not an array');
|
||||
}
|
||||
|
||||
const keyPart = JSON.stringify(key) + ': ';
|
||||
const isLast = i === l - 1;
|
||||
const trailing = isLast ? '' : ',';
|
||||
// the library reserves the key prefix plus, unless last, the trailing comma
|
||||
const budget = SINGBOX_MAX_LENGTH - RULE_VALUE_INDENT - (keyPart.length + (isLast ? 0 : 1));
|
||||
|
||||
const inlined = inlineIfFits(arr, budget);
|
||||
if (inlined !== null) {
|
||||
lines.push(' ' + keyPart + inlined + trailing);
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.push(' ' + keyPart + '[');
|
||||
for (let j = 0, l2 = arr.length; j < l2; j++) {
|
||||
lines.push(' ' + JSON.stringify(arr[j]) + (j === l2 - 1 ? '' : ','));
|
||||
}
|
||||
lines.push(' ]' + trailing);
|
||||
}
|
||||
|
||||
lines.push(' }', ' ]', '}');
|
||||
return lines;
|
||||
}
|
||||
|
||||
export class SingboxSource extends BaseWriteStrategy {
|
||||
public readonly name = 'singbox';
|
||||
|
||||
readonly fileExtension = 'json';
|
||||
|
||||
static readonly jsonToLines = (json: unknown): string[] => stringify(json).split('\n');
|
||||
|
||||
// JSON output has no metadata comment at all, nothing to preserve
|
||||
protected override readonly skipCompareOnCI = true;
|
||||
|
||||
@@ -43,10 +121,7 @@ export class SingboxSource extends BaseWriteStrategy {
|
||||
};
|
||||
|
||||
protected get result() {
|
||||
return SingboxSource.jsonToLines({
|
||||
version: 2,
|
||||
rules: [this.singbox]
|
||||
});
|
||||
return singboxSourceToLines(this.singbox);
|
||||
}
|
||||
|
||||
constructor(
|
||||
|
||||
Reference in New Issue
Block a user