From dbea3e6b52035d546fcd3b3b281c7488d7047295 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Mon, 29 Apr 2024 13:23:12 +0300 Subject: [PATCH] fix: delayed thread group registration --- src/decorator/Bunyamin.ts | 4 +- src/decorator/types/BunyaminConfig.ts | 2 +- src/realm.ts | 16 +++++-- .../BunyanTraceEventStream.ts | 6 +-- .../options/normalizeOptions.ts | 20 ++++---- .../threads/ThreadGroupDispatcher.test.ts | 37 +++++++++----- .../threads/ThreadGroupDispatcher.ts | 48 +++++++++++++------ src/thread-groups/ThreadGroups.test.ts | 7 +-- src/thread-groups/ThreadGroups.ts | 10 ++-- 9 files changed, 95 insertions(+), 55 deletions(-) diff --git a/src/decorator/Bunyamin.ts b/src/decorator/Bunyamin.ts index a08c92c..1c7ed98 100644 --- a/src/decorator/Bunyamin.ts +++ b/src/decorator/Bunyamin.ts @@ -4,8 +4,8 @@ import type { ThreadGroupConfig } from '../streams'; import type { ThreadID } from '../types'; import { flow, isActionable, isError, isObject, isPromiseLike } from '../utils'; import type { - BunyaminLogMethod, BunyaminConfig, + BunyaminLogMethod, BunyaminLogRecordFields as UserFields, BunyanLikeLogger, BunyanLogLevel, @@ -48,7 +48,7 @@ export class Bunyamin { /** @deprecated */ get threadGroups(): ThreadGroupConfig[] { - return []; + return [...(this.#shared.threadGroups ?? [])]; } get logger(): Logger { diff --git a/src/decorator/types/BunyaminConfig.ts b/src/decorator/types/BunyaminConfig.ts index 4dd23d9..14e77fe 100644 --- a/src/decorator/types/BunyaminConfig.ts +++ b/src/decorator/types/BunyaminConfig.ts @@ -14,7 +14,7 @@ export type BunyaminConfig = { /** * Thread groups to be used for grouping log records. */ - threadGroups?: ThreadGroupConfig[]; + threadGroups?: Iterable; /** * Fallback message to be used when there was no previous message * passed with {@link BunyaminLogMethod#begin}. diff --git a/src/realm.ts b/src/realm.ts index c6e7c51..f017850 100644 --- a/src/realm.ts +++ b/src/realm.ts @@ -1,3 +1,4 @@ +/* eslint-disable prefer-const */ import { Bunyamin } from './decorator'; import { noopLogger } from './noopLogger'; import { isSelfDebug } from './is-debug'; @@ -10,13 +11,22 @@ type Realm = { }; function create() { + let bunyamin: Bunyamin; + let nobunyamin: Bunyamin; + const selfDebug = isSelfDebug(); - const bunyamin = new Bunyamin({ logger: noopLogger() }); - const nobunyamin = new Bunyamin({ + const threadGroups = new ThreadGroups(() => bunyamin); + + bunyamin = new Bunyamin({ logger: noopLogger(), + threadGroups, + }); + + nobunyamin = new Bunyamin({ immutable: true, + logger: noopLogger(), + threadGroups, }); - const threadGroups = new ThreadGroups(bunyamin); if (selfDebug) { bunyamin.trace({ cat: 'bunyamin' }, 'bunyamin global instance created'); diff --git a/src/streams/bunyan-trace-event/BunyanTraceEventStream.ts b/src/streams/bunyan-trace-event/BunyanTraceEventStream.ts index 683a37a..ba35983 100644 --- a/src/streams/bunyan-trace-event/BunyanTraceEventStream.ts +++ b/src/streams/bunyan-trace-event/BunyanTraceEventStream.ts @@ -27,11 +27,9 @@ export class BunyanTraceEventStream extends Transform { strict: options.strict ?? false, defaultThreadName: options.defaultThreadName ?? 'Main Thread', maxConcurrency: options.maxConcurrency ?? 100, + // Lazy to add a `NormalizedOptions...` type, so we just cast it here. + threadGroups: options.threadGroups as Iterable, }); - - for (const threadGroup of options.threadGroups) { - this.#threadGroupDispatcher.registerThreadGroup(threadGroup as ThreadGroupConfig); - } } _transform( diff --git a/src/streams/bunyan-trace-event/options/normalizeOptions.ts b/src/streams/bunyan-trace-event/options/normalizeOptions.ts index 6355424..3c3f8ff 100644 --- a/src/streams/bunyan-trace-event/options/normalizeOptions.ts +++ b/src/streams/bunyan-trace-event/options/normalizeOptions.ts @@ -1,5 +1,5 @@ -import type { TraceEventStreamOptions } from './TraceEventStreamOptions'; import type { ThreadGroupConfig } from '../threads'; +import type { TraceEventStreamOptions } from './TraceEventStreamOptions'; export function normalizeOptions( options: TraceEventStreamOptions, @@ -8,14 +8,16 @@ export function normalizeOptions( options.defaultThreadName = options.defaultThreadName ?? 'Main Thread'; options.maxConcurrency = options.maxConcurrency ?? 100; options.strict = options.strict ?? false; - options.threadGroups = [...(options.threadGroups ?? [])].map((threadGroup, index) => - typeof threadGroup === 'string' - ? { - id: threadGroup, - displayName: threadGroup, - } - : validateThreadGroup(threadGroup, index), - ); + options.threadGroups = Array.isArray(options.threadGroups) + ? options.threadGroups.map((threadGroup, index) => + typeof threadGroup === 'string' + ? { + id: threadGroup, + displayName: threadGroup, + } + : validateThreadGroup(threadGroup, index), + ) + : options.threadGroups ?? []; if (options.maxConcurrency < 1) { throw new Error(`maxConcurrency must be at least 1, got ${options.maxConcurrency}`); diff --git a/src/streams/bunyan-trace-event/threads/ThreadGroupDispatcher.test.ts b/src/streams/bunyan-trace-event/threads/ThreadGroupDispatcher.test.ts index 25381b2..0f4a643 100644 --- a/src/streams/bunyan-trace-event/threads/ThreadGroupDispatcher.test.ts +++ b/src/streams/bunyan-trace-event/threads/ThreadGroupDispatcher.test.ts @@ -11,10 +11,12 @@ describe('ThreadGroupDispatcher', () => { defaultThreadName: 'Main Thread', maxConcurrency: 100, strict: false, - }) - .registerThreadGroup({ id: 'foo', displayName: 'A' }) - .registerThreadGroup({ id: 'bar', displayName: 'B', maxConcurrency: 2 }) - .registerThreadGroup({ id: 'baz', displayName: 'C', maxConcurrency: 3 }); + threadGroups: [ + { id: 'foo', displayName: 'A' }, + { id: 'bar', displayName: 'B', maxConcurrency: 2 }, + { id: 'baz', displayName: 'C', maxConcurrency: 3 }, + ], + }); }); it.each(PHASES)('should fallback to 0 for null tid (ph = %j)', (ph) => { @@ -74,9 +76,12 @@ describe('ThreadGroupDispatcher', () => { defaultThreadName: 'Main Thread', maxConcurrency: 2, strict: true, - }).registerThreadGroup({ - id: 'foo', - displayName: 'A', + threadGroups: [ + { + id: 'foo', + displayName: 'A', + }, + ], }); }); @@ -106,9 +111,12 @@ describe('ThreadGroupDispatcher', () => { defaultThreadName: 'Main Thread', maxConcurrency: 1, strict: true, - }).registerThreadGroup({ - id: 'foo', - displayName: 'Single Thread', + threadGroups: [ + { + id: 'foo', + displayName: 'Single Thread', + }, + ], }); expect(dispatcher.resolve('B', 'foo')).toBe(1); @@ -127,9 +135,12 @@ describe('ThreadGroupDispatcher', () => { defaultThreadName: 'Main Thread', maxConcurrency: 2, strict: false, - }).registerThreadGroup({ - id: 'foo', - displayName: 'A', + threadGroups: [ + { + id: 'foo', + displayName: 'A', + }, + ], }); }); diff --git a/src/streams/bunyan-trace-event/threads/ThreadGroupDispatcher.ts b/src/streams/bunyan-trace-event/threads/ThreadGroupDispatcher.ts index 5484fdd..98f7dd9 100644 --- a/src/streams/bunyan-trace-event/threads/ThreadGroupDispatcher.ts +++ b/src/streams/bunyan-trace-event/threads/ThreadGroupDispatcher.ts @@ -8,6 +8,7 @@ export type ThreadGroupDispatcherConfig = { defaultThreadName: string; maxConcurrency: number; strict: boolean; + threadGroups: Iterable; }; export class ThreadGroupDispatcher { @@ -15,29 +16,22 @@ export class ThreadGroupDispatcher { readonly #dispatchers: Record = {}; readonly #maxConcurrency: number; readonly #defaultThreadName: string; + readonly #threadGroups: Iterable; readonly #names: IntervalTree = new IntervalTree(); #freeThreadId = 1; + #initialized = false; constructor(options: ThreadGroupDispatcherConfig) { this.#defaultThreadName = options.defaultThreadName; this.#maxConcurrency = options.maxConcurrency; this.#strict = options.strict; - } - - registerThreadGroup(config: ThreadGroupConfig): this { - const maxConcurrency = config.maxConcurrency ?? this.#maxConcurrency; - const min = this.#freeThreadId; - const max = min + maxConcurrency - 1; - - this.#dispatchers[config.id] = new ThreadDispatcher(config.displayName, this.#strict, min, max); - this.#names.insert([min, max], config.displayName); - this.#freeThreadId = max + 1; - - return this; + this.#threadGroups = options.threadGroups; } name(tid: number): string | undefined { + this.#ensureInitialized(); + if (tid === 0) { return this.#defaultThreadName; } @@ -46,6 +40,8 @@ export class ThreadGroupDispatcher { } resolve(ph: string | undefined, tid: ThreadID | undefined): number | Error { + this.#ensureInitialized(); + if (tid == null) { return 0; } @@ -74,7 +70,29 @@ export class ThreadGroupDispatcher { } } - #resolveDispatcher(threadAlias: ThreadAlias): ThreadDispatcher { + #ensureInitialized() { + if (!this.#initialized) { + this.#initialized = true; + + for (const group of this.#threadGroups) { + this.#registerThreadGroup(group); + } + } + } + + #registerThreadGroup(config: ThreadGroupConfig): this { + const maxConcurrency = config.maxConcurrency ?? this.#maxConcurrency; + const min = this.#freeThreadId; + const max = min + maxConcurrency - 1; + + this.#dispatchers[config.id] = new ThreadDispatcher(config.displayName, this.#strict, min, max); + this.#names.insert([min, max], config.displayName); + this.#freeThreadId = max + 1; + + return this; + } + + #resolveDispatcher(threadAlias: ThreadAlias): ThreadDispatcher | undefined { const groupName = typeof threadAlias === 'string' ? threadAlias : threadAlias[0]; return this.#ensureGroupDispatcher(groupName); } @@ -89,9 +107,9 @@ export class ThreadGroupDispatcher { : threadAlias[1]; } - #ensureGroupDispatcher(threadGroup: string): ThreadDispatcher { + #ensureGroupDispatcher(threadGroup: string): ThreadDispatcher | undefined { if (!this.#dispatchers[threadGroup] && !this.#strict) { - this.registerThreadGroup({ id: threadGroup, displayName: threadGroup }); + this.#registerThreadGroup({ id: threadGroup, displayName: threadGroup }); } return this.#dispatchers[threadGroup]; diff --git a/src/thread-groups/ThreadGroups.test.ts b/src/thread-groups/ThreadGroups.test.ts index 79b7a84..e9a4b98 100644 --- a/src/thread-groups/ThreadGroups.test.ts +++ b/src/thread-groups/ThreadGroups.test.ts @@ -1,10 +1,11 @@ +/* eslint-disable @typescript-eslint/consistent-type-imports */ import { beforeEach, describe, expect, jest, it } from '@jest/globals'; import type { ThreadGroups } from './ThreadGroups'; import { wrapLogger } from '../wrapLogger'; import type { Bunyamin } from '../decorator'; describe('ThreadGroups', () => { - let ThreadGroups: new (logger: Bunyamin) => ThreadGroups; + let ThreadGroups: typeof import('./ThreadGroups').ThreadGroups; let threadGroups: ThreadGroups; let isDebug: jest.Mocked; let logger: Bunyamin; @@ -26,7 +27,7 @@ describe('ThreadGroups', () => { describe('in regular mode', () => { beforeEach(() => { isDebug.isSelfDebug.mockReturnValue(false); - threadGroups = new ThreadGroups(logger); + threadGroups = new ThreadGroups(() => logger); }); it('should be empty by default', () => { @@ -47,7 +48,7 @@ describe('ThreadGroups', () => { describe('in debug mode', () => { beforeEach(() => { isDebug.isSelfDebug.mockReturnValue(true); - threadGroups = new ThreadGroups(logger); + threadGroups = new ThreadGroups(() => logger); }); it('should call logger.trace upon addition', () => { diff --git a/src/thread-groups/ThreadGroups.ts b/src/thread-groups/ThreadGroups.ts index 595525c..8a00c34 100644 --- a/src/thread-groups/ThreadGroups.ts +++ b/src/thread-groups/ThreadGroups.ts @@ -3,13 +3,13 @@ import type { ThreadGroupConfig } from '../streams'; import { isSelfDebug } from '../is-debug'; import { StackTraceError } from '../decorator/StackTraceError'; -export class ThreadGroups { - readonly #bunyamin: Bunyamin; +export class ThreadGroups implements Iterable { readonly #debugMode = isSelfDebug(); + readonly #getBunyamin: () => Bunyamin; readonly #groups = new Map(); - constructor(bunyamin: Bunyamin) { - this.#bunyamin = bunyamin; + constructor(getBunyamin: () => Bunyamin) { + this.#getBunyamin = getBunyamin; this.#groups = new Map(); } @@ -32,7 +32,7 @@ export class ThreadGroups { #logAddition(group: ThreadGroupConfig, action: string) { const { stack } = new StackTraceError(); - this.#bunyamin.trace( + this.#getBunyamin().trace( { cat: 'bunyamin' }, `thread group ${action}: ${group.id} (${group.displayName})\n\n${stack}`, );