Skip to content

Commit d7680b6

Browse files
author
John Doe
committed
refactor: setup better testing infra
1 parent 2fe519a commit d7680b6

File tree

8 files changed

+352
-424
lines changed

8 files changed

+352
-424
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { type PerformanceEntry, performance } from 'node:perf_hooks';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
import {
4+
type PerformanceObserverOptions,
5+
PerformanceObserverSink,
6+
} from './performance-observer.js';
7+
import type { Sink } from './sink-source.types';
8+
9+
class MockSink implements Sink<string, string> {
10+
private writtenItems: string[] = [];
11+
private closed = false;
12+
13+
open(): void {
14+
this.closed = false;
15+
}
16+
17+
write(input: string): void {
18+
this.writtenItems.push(input);
19+
}
20+
21+
close(): void {
22+
this.closed = true;
23+
}
24+
25+
isClosed(): boolean {
26+
return this.closed;
27+
}
28+
29+
encode(input: string): string {
30+
return `${input}-${this.constructor.name}-encoded`;
31+
}
32+
33+
recover(): string[] {
34+
return [...this.writtenItems];
35+
}
36+
}
37+
38+
describe('PerformanceObserverSink', () => {
39+
let sink: MockSink;
40+
let options: PerformanceObserverOptions<string>;
41+
42+
beforeEach(() => {
43+
vi.clearAllMocks();
44+
performance.clearMeasures();
45+
performance.clearMarks();
46+
sink = new MockSink();
47+
48+
options = {
49+
sink,
50+
encode: vi.fn((entry: PerformanceEntry) => [
51+
`${entry.name}:${entry.entryType}`,
52+
]),
53+
};
54+
});
55+
56+
afterEach(() => {
57+
vi.restoreAllMocks();
58+
});
59+
60+
it('creates instance with default options', () => {
61+
expect(() => new PerformanceObserverSink(options)).not.toThrow();
62+
});
63+
64+
it('creates instance with custom options', () => {
65+
expect(
66+
() =>
67+
new PerformanceObserverSink({
68+
...options,
69+
buffered: true,
70+
flushThreshold: 10,
71+
}),
72+
).not.toThrow();
73+
});
74+
75+
it('should observe performance entries and write them to the sink on flush', () => {
76+
const observer = new PerformanceObserverSink(options);
77+
78+
observer.subscribe();
79+
performance.mark('test-mark');
80+
observer.flush();
81+
expect(sink.recover()).toHaveLength(1);
82+
});
83+
84+
it('should observe buffered performance entries when buffered is enabled', async () => {
85+
const observer = new PerformanceObserverSink({
86+
...options,
87+
buffered: true,
88+
});
89+
90+
performance.mark('test-mark-1');
91+
performance.mark('test-mark-2');
92+
await new Promise(resolve => setTimeout(resolve, 10));
93+
observer.subscribe();
94+
await new Promise(resolve => setTimeout(resolve, 10));
95+
expect(performance.getEntries()).toHaveLength(2);
96+
observer.flush();
97+
expect(sink.recover()).toHaveLength(2);
98+
});
99+
100+
it('handles multiple encoded items per performance entry', () => {
101+
const multiEncodeFn = vi.fn(e => [
102+
`${e.entryType}-item1`,
103+
`${e.entryType}item2`,
104+
]);
105+
const observer = new PerformanceObserverSink({
106+
...options,
107+
encode: multiEncodeFn,
108+
});
109+
110+
observer.subscribe();
111+
112+
performance.mark('test-mark');
113+
observer.flush();
114+
115+
expect(sink.recover()).toHaveLength(2);
116+
});
117+
});
Lines changed: 33 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,88 @@
11
import {
2+
type EntryType,
23
type PerformanceEntry,
34
PerformanceObserver,
5+
type PerformanceObserverEntryList,
46
performance,
57
} from 'node:perf_hooks';
6-
import type { Buffered, Encoder, Sink } from './sink-source.types.js';
8+
import type { Buffered, Encoder, Observer, Sink } from './sink-source.types.js';
79

810
export const DEFAULT_FLUSH_THRESHOLD = 20;
911

1012
export type PerformanceObserverOptions<T> = {
1113
sink: Sink<T, unknown>;
1214
encode: (entry: PerformanceEntry) => T[];
13-
captureBuffered?: boolean;
15+
buffered?: boolean;
1416
flushThreshold?: number;
1517
};
1618

17-
export class PerformanceObserverHandle<T>
18-
implements Buffered, Encoder<PerformanceEntry, T[]>
19+
export class PerformanceObserverSink<T>
20+
implements Observer, Buffered, Encoder<PerformanceEntry, T[]>
1921
{
2022
#encode: (entry: PerformanceEntry) => T[];
21-
#captureBuffered: boolean;
22-
#observedEntryCount: number;
23+
#buffered: boolean;
2324
#flushThreshold: number;
2425
#sink: Sink<T, unknown>;
2526
#observer: PerformanceObserver | undefined;
26-
#closed = false;
27+
#observedTypes: EntryType[] = ['mark', 'measure'];
28+
#getEntries = (list: PerformanceObserverEntryList) =>
29+
this.#observedTypes.flatMap(t => list.getEntriesByType(t));
30+
#observedCount: number = 0;
2731

2832
constructor(options: PerformanceObserverOptions<T>) {
2933
this.#encode = options.encode;
3034
this.#sink = options.sink;
31-
this.#captureBuffered = options.captureBuffered ?? false;
35+
this.#buffered = options.buffered ?? false;
3236
this.#flushThreshold = options.flushThreshold ?? DEFAULT_FLUSH_THRESHOLD;
33-
this.#observedEntryCount = 0;
3437
}
3538

3639
encode(entry: PerformanceEntry): T[] {
3740
return this.#encode(entry);
3841
}
3942

40-
connect(): void {
41-
if (this.#observer || this.#closed) {
43+
subscribe(): void {
44+
if (this.#observer) {
4245
return;
4346
}
44-
this.#observer = new PerformanceObserver(() => {
45-
this.#observedEntryCount++;
46-
if (this.#observedEntryCount >= this.#flushThreshold) {
47+
48+
this.#observer = new PerformanceObserver(list => {
49+
const entries = this.#getEntries(list);
50+
this.#observedCount += entries.length;
51+
if (this.#observedCount >= this.#flushThreshold) {
4752
this.flush();
48-
this.#observedEntryCount = 0;
4953
}
5054
});
5155

5256
this.#observer.observe({
53-
entryTypes: ['mark', 'measure'],
54-
buffered: this.#captureBuffered,
57+
entryTypes: this.#observedTypes,
58+
buffered: this.#buffered,
5559
});
5660
}
5761

58-
flush(clear = false): void {
59-
if (this.#closed || !this.#sink) {
62+
flush(): void {
63+
if (!this.#observer) {
6064
return;
6165
}
62-
const entries = [
63-
...performance.getEntriesByType('mark'),
64-
...performance.getEntriesByType('measure'),
65-
];
6666

67-
// Process all entries
68-
entries
69-
.filter(e => e.entryType === 'mark' || e.entryType === 'measure')
70-
.forEach(e => {
71-
const encoded = this.encode(e);
72-
encoded.forEach(item => {
73-
this.#sink.write(item);
74-
});
75-
76-
if (clear) {
77-
if (e.entryType === 'mark') {
78-
performance.clearMarks(e.name);
79-
}
80-
if (e.entryType === 'measure') {
81-
performance.clearMeasures(e.name);
82-
}
83-
}
67+
const entries = this.#getEntries(performance);
68+
entries.forEach(entry => {
69+
const encoded = this.encode(entry);
70+
encoded.forEach(item => {
71+
this.#sink.write(item);
8472
});
73+
});
74+
this.#observedCount = 0;
8575
}
8676

87-
disconnect(): void {
77+
unsubscribe(): void {
8878
if (!this.#observer) {
8979
return;
9080
}
9181
this.#observer?.disconnect();
9282
this.#observer = undefined;
9383
}
9484

95-
close(): void {
96-
if (this.#closed) {
97-
return;
98-
}
99-
this.flush();
100-
this.#closed = true;
101-
this.disconnect();
102-
}
103-
104-
isConnected(): boolean {
105-
return this.#observer !== undefined && !this.#closed;
85+
isSubscribed(): boolean {
86+
return this.#observer !== undefined;
10687
}
10788
}

0 commit comments

Comments
 (0)