105 lines
4.2 KiB
TypeScript
105 lines
4.2 KiB
TypeScript
import {performance} from 'node:perf_hooks';
|
|
import type {Bot} from '@wp2508/mineflayer';
|
|
import type {BaseInfo, InfoI18n} from './types.ts';
|
|
|
|
type Meter = {depth: number; total: number};
|
|
const meters = new WeakMap<object, Meter>();
|
|
|
|
/** Only the synchronous invocation is timed, even when it returns a Promise. */
|
|
export function measure_sync<T>(bot: object, invoke: () => T): T {
|
|
const meter = meters.get(bot);
|
|
if (!meter) return invoke();
|
|
const outer = meter.depth++ === 0;
|
|
const start = outer ? performance.now() : 0;
|
|
try { return invoke(); }
|
|
finally {
|
|
meter.depth--;
|
|
if (outer) meter.total += performance.now() - start;
|
|
}
|
|
}
|
|
|
|
export function instrument_bot(bot: Bot): {total: () => number; dispose: () => void} {
|
|
if (meters.has(bot)) throw new Error('Bot resource meter already installed');
|
|
const meter: Meter = {depth: 0, total: 0};
|
|
meters.set(bot, meter);
|
|
const restore: (() => void)[] = [];
|
|
try {
|
|
for (const target of new Set([bot, bot._client])) {
|
|
if (!target || typeof target.emit !== 'function') continue;
|
|
const descriptor = Object.getOwnPropertyDescriptor(target, 'emit');
|
|
const original = target.emit;
|
|
const wrapper = function(this: typeof target, ...args: Parameters<typeof original>) {
|
|
return measure_sync(bot, () => Reflect.apply(original, this, args));
|
|
};
|
|
Object.defineProperty(target, 'emit', {configurable: true, writable: true,
|
|
enumerable: descriptor?.enumerable ?? false, value: wrapper});
|
|
restore.push(() => {
|
|
if (Object.getOwnPropertyDescriptor(target, 'emit')?.value !== wrapper) return;
|
|
if (descriptor) Object.defineProperty(target, 'emit', descriptor);
|
|
else delete (target as unknown as {emit?: unknown}).emit;
|
|
});
|
|
}
|
|
} catch (error) {
|
|
for (const undo of restore.reverse()) undo();
|
|
meters.delete(bot);
|
|
throw error;
|
|
}
|
|
let disposed = false;
|
|
return {total: () => meter.total, dispose: () => {
|
|
if (disposed) return;
|
|
disposed = true;
|
|
meters.delete(bot);
|
|
for (const undo of restore.reverse()) undo();
|
|
}};
|
|
}
|
|
|
|
export function metadata(name: string, description: string, fields: Record<string, readonly [string, string]>): InfoI18n {
|
|
return {lang: 'zh-CN', name, description, fields: Object.fromEntries(Object.entries(fields).map(
|
|
([key, [name, description]]) => [key, {lang: 'zh-CN', name, description}],
|
|
))};
|
|
}
|
|
|
|
/** No module owns bot end listeners; InfoManager owns disposal. */
|
|
export abstract class ResourceInfo<T> implements BaseInfo<T> {
|
|
abstract readonly name: string;
|
|
abstract readonly i18n: InfoI18n;
|
|
protected disposed = false;
|
|
private timer?: ReturnType<typeof setInterval>;
|
|
private running = false;
|
|
private readonly listeners = new Set<(value: T) => void>();
|
|
constructor(protected info: T) {}
|
|
get_info(): T { return this.info; }
|
|
on_update(callback: (value: T) => void): () => void {
|
|
if (this.disposed) throw new Error('Resource info disposed');
|
|
this.listeners.add(callback);
|
|
return () => { this.listeners.delete(callback); };
|
|
}
|
|
protected publish(info: T): void {
|
|
if (this.disposed) return;
|
|
this.info = info;
|
|
for (const listener of Array.from(this.listeners)) {
|
|
if (this.disposed) break;
|
|
try { listener(info); }
|
|
catch (error) { console.error('Resource info listener failed', error); }
|
|
}
|
|
}
|
|
protected start(interval: number, sample: () => void | Promise<void>, immediate = false): void {
|
|
const run = async () => {
|
|
if (this.running || this.disposed) return;
|
|
this.running = true;
|
|
try { await sample(); }
|
|
catch (error) { if (!this.disposed) console.error('Resource sampling failed', error); }
|
|
finally { this.running = false; }
|
|
};
|
|
this.timer = setInterval(() => { void run(); }, interval);
|
|
this.timer.unref?.();
|
|
if (immediate) void run();
|
|
}
|
|
dispose(): void {
|
|
if (this.disposed) return;
|
|
this.disposed = true;
|
|
clearInterval(this.timer);
|
|
this.listeners.clear();
|
|
}
|
|
}
|