Skip to content

Commit e4f66d5

Browse files
committed
refactor: wip
1 parent cadc7ee commit e4f66d5

File tree

4 files changed

+100
-28
lines changed

4 files changed

+100
-28
lines changed

packages/utils/src/lib/profiler/wal-json-trace.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { performance } from 'node:perf_hooks';
1+
import { defaultClock } from '../clock-epoch.js';
22
import type { InvalidEntry, WalFormat } from '../wal.js';
33
import {
44
decodeTraceEvent,
@@ -13,10 +13,10 @@ import type { TraceEvent, UserTimingTraceEvent } from './trace-file.type.js';
1313
const TRACE_START_MARGIN_NAME = '[trace padding start]';
1414
/** Name for the trace end margin event */
1515
const TRACE_END_MARGIN_NAME = '[trace padding end]';
16-
/** Milliseconds of padding to add before/after trace events */
17-
const TRACE_MARGIN_MS = 1000;
18-
/** Duration in milliseconds for margin events */
19-
const TRACE_MARGIN_DURATION_MS = 20;
16+
/** Microseconds of padding to add before/after trace events (1000ms = 1,000,000μs) */
17+
const TRACE_MARGIN_US = 1_000_000;
18+
/** Duration in microseconds for margin events (20ms = 20,000μs) */
19+
const TRACE_MARGIN_DURATION_US = 20_000;
2020

2121
/**
2222
* Generates a complete Chrome DevTools trace file content as JSON string.
@@ -38,16 +38,16 @@ export function generateTraceContent(
3838
},
3939
});
4040

41-
const marginMs = TRACE_MARGIN_MS;
42-
const marginDurMs = TRACE_MARGIN_DURATION_MS;
41+
const marginUs = TRACE_MARGIN_US;
42+
const marginDurUs = TRACE_MARGIN_DURATION_US;
4343

4444
const sortedEvents = [...events].sort((a, b) => a.ts - b.ts);
45-
const fallbackTs = performance.now();
45+
const fallbackTs = defaultClock.epochNowUs();
4646
const firstTs: number = sortedEvents.at(0)?.ts ?? fallbackTs;
4747
const lastTs: number = sortedEvents.at(-1)?.ts ?? fallbackTs;
4848

49-
const startTs = firstTs - marginMs;
50-
const endTs = lastTs + marginMs;
49+
const startTs = firstTs - marginUs;
50+
const endTs = lastTs + marginUs;
5151

5252
const traceEvents: TraceEvent[] = [
5353
getInstantEventTracingStartedInBrowser({
@@ -57,13 +57,13 @@ export function generateTraceContent(
5757
getCompleteEvent({
5858
name: TRACE_START_MARGIN_NAME,
5959
ts: startTs,
60-
dur: marginDurMs,
60+
dur: marginDurUs,
6161
}),
6262
...sortedEvents,
6363
getCompleteEvent({
6464
name: TRACE_END_MARGIN_NAME,
6565
ts: endTs,
66-
dur: marginDurMs,
66+
dur: marginDurUs,
6767
}),
6868
];
6969

packages/utils/src/lib/profiler/wal-json-trace.unit.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ describe('generateTraceContent', () => {
2828
expect.objectContaining({
2929
name: '[trace padding start]',
3030
ph: 'X',
31-
dur: 20,
31+
dur: 20_000,
3232
cat: 'devtools.timeline',
3333
}),
3434
expect.objectContaining({
3535
name: '[trace padding end]',
3636
ph: 'X',
37-
dur: 20,
37+
dur: 20_000,
3838
cat: 'devtools.timeline',
3939
}),
4040
],
@@ -102,14 +102,14 @@ describe('generateTraceContent', () => {
102102
expect.objectContaining({
103103
name: '[trace padding start]',
104104
ph: 'X',
105-
dur: 20,
105+
dur: 20_000,
106106
cat: 'devtools.timeline',
107107
}),
108108
...events,
109109
expect.objectContaining({
110110
name: '[trace padding end]',
111111
ph: 'X',
112-
dur: 20,
112+
dur: 20_000,
113113
cat: 'devtools.timeline',
114114
}),
115115
],
@@ -191,24 +191,24 @@ describe('generateTraceContent', () => {
191191
}),
192192
);
193193

194-
// Second should be start margin at ts - 1000
194+
// Second should be start margin at ts - 1,000,000μs (1000ms)
195195
expect(traceEvents[1]).toStrictEqual(
196196
expect.objectContaining({
197197
name: '[trace padding start]',
198198
ph: 'X',
199-
dur: 20,
199+
dur: 20_000,
200200
}),
201201
);
202202

203203
// Third should be the actual event
204204
expect(traceEvents[2]).toStrictEqual(events[0]);
205205

206-
// Fourth should be end margin at lastTs + 1000
206+
// Fourth should be end margin at lastTs + 1,000,000μs (1000ms)
207207
expect(traceEvents[3]).toStrictEqual(
208208
expect.objectContaining({
209209
name: '[trace padding end]',
210210
ph: 'X',
211-
dur: 20,
211+
dur: 20_000,
212212
}),
213213
);
214214
});

packages/utils/src/lib/wal.ts

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ export type RecoverResult<T> = {
3838
partialTail: string | null;
3939
};
4040

41+
/**
42+
* Statistics about the WAL file state and last recovery operation.
43+
*/
44+
export type WalStats<T> = {
45+
/** File path for this WAL */
46+
filePath: string;
47+
/** Whether the WAL file is currently closed */
48+
isClosed: boolean;
49+
/** Whether the WAL file exists on disk */
50+
fileExists: boolean;
51+
/** File size in bytes (0 if file doesn't exist) */
52+
fileSize: number;
53+
/** Last recovery state from the most recent {@link recover} or {@link repack} operation */
54+
lastRecovery: RecoverResult<T | InvalidEntry<string>> | null;
55+
};
56+
4157
export const createTolerantCodec = <I, O = string>(codec: {
4258
encode: (v: I) => O;
4359
decode: (d: O) => I;
@@ -121,6 +137,7 @@ export class WriteAheadLogFile<T> implements AppendableSink<T> {
121137
readonly #file: string;
122138
readonly #decode: Codec<T | InvalidEntry<string>>['decode'];
123139
readonly #encode: Codec<T>['encode'];
140+
#lastRecoveryState: RecoverResult<T | InvalidEntry<string>> | null = null;
124141

125142
/**
126143
* Create a new WAL file instance.
@@ -170,20 +187,27 @@ export class WriteAheadLogFile<T> implements AppendableSink<T> {
170187
/**
171188
* Recover all records from the WAL file.
172189
* Handles partial writes and decode errors gracefully.
190+
* Updates the recovery state (accessible via {@link getStats}).
173191
* @returns Recovery result with records, errors, and partial tail
174192
*/
175193
recover(): RecoverResult<T | InvalidEntry<string>> {
176194
if (!fs.existsSync(this.#file)) {
177-
return { records: [], errors: [], partialTail: null };
195+
this.#lastRecoveryState = { records: [], errors: [], partialTail: null };
196+
return this.#lastRecoveryState;
178197
}
179-
180198
const txt = fs.readFileSync(this.#file, 'utf8');
181-
return recoverFromContent<T | InvalidEntry<string>>(txt, this.#decode);
199+
this.#lastRecoveryState = recoverFromContent<T | InvalidEntry<string>>(
200+
txt,
201+
this.#decode,
202+
);
203+
204+
return this.#lastRecoveryState;
182205
}
183206

184207
/**
185208
* Repack the WAL by recovering all valid records and rewriting cleanly.
186209
* Removes corrupted entries and ensures clean formatting.
210+
* Updates the recovery state (accessible via {@link getStats}).
187211
* @param out - Output path (defaults to current file)
188212
*/
189213
repack(out = this.#file) {
@@ -208,6 +232,22 @@ export class WriteAheadLogFile<T> implements AppendableSink<T> {
208232
fs.mkdirSync(path.dirname(out), { recursive: true });
209233
fs.writeFileSync(out, `${recordsToWrite.map(this.#encode).join('\n')}\n`);
210234
}
235+
236+
/**
237+
* Get comprehensive statistics about the WAL file state.
238+
* Includes file information, open/close status, and last recovery state.
239+
* @returns Statistics object with file info and last recovery state
240+
*/
241+
getStats(): WalStats<T> {
242+
const fileExists = fs.existsSync(this.#file);
243+
return {
244+
filePath: this.#file,
245+
isClosed: this.#fd == null,
246+
fileExists,
247+
fileSize: fileExists ? fs.statSync(this.#file).size : 0,
248+
lastRecovery: this.#lastRecoveryState,
249+
};
250+
}
211251
}
212252

213253
/**
@@ -246,7 +286,7 @@ export const stringCodec = <
246286
/**
247287
* Parses a partial WalFormat configuration and returns a complete WalFormat object.
248288
* All fallback values are targeting string types.
249-
* - baseName defaults to Date.now().toString()
289+
* - baseName defaults to 'wal'
250290
* - walExtension defaults to '.log'
251291
* - finalExtension defaults to '.log'
252292
* - codec defaults to stringCodec<T>()
@@ -258,14 +298,23 @@ export function parseWalFormat<T extends object | string = object>(
258298
format: Partial<WalFormat<T>>,
259299
): WalFormat<T> {
260300
const {
261-
baseName = 'trace',
301+
baseName = 'wal',
262302
walExtension = '.log',
263303
finalExtension = walExtension,
264304
codec = stringCodec<T>(),
265-
finalizer = (encodedRecords: (T | InvalidEntry<string>)[]) =>
266-
`${encodedRecords.join('\n')}\n`,
267305
} = format;
268306

307+
const finalizer =
308+
format.finalizer ??
309+
((encodedRecords: (T | InvalidEntry<string>)[]) => {
310+
const encoded = encodedRecords.map(record =>
311+
typeof record === 'object' && record != null && '__invalid' in record
312+
? (record as InvalidEntry<string>).raw
313+
: codec.encode(record as T),
314+
);
315+
return `${encoded.join('\n')}\n`;
316+
});
317+
269318
return {
270319
baseName,
271320
walExtension,
@@ -301,6 +350,7 @@ export function setLeaderWal(envVarName: string, profilerID: string): void {
301350

302351
// eslint-disable-next-line functional/no-let
303352
let shardCount = 0;
353+
304354
/**
305355
* Generates a human-readable shard ID.
306356
* This ID is unique per process/thread/shard combination and used in the file name.
@@ -354,6 +404,7 @@ export function sortableReadableDateString(timestampMs: string): string {
354404

355405
return `${yyyy}${mm}${dd}-${hh}${min}${ss}-${ms}`;
356406
}
407+
357408
/**
358409
* Generates a path to a shard file using human-readable IDs.
359410
* Both groupId and shardId are already in readable date format.

packages/utils/src/lib/wal.unit.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ describe('parseWalFormat', () => {
572572
it('should apply all defaults when given empty config', () => {
573573
const result = parseWalFormat({});
574574

575-
expect(result.baseName).toBe('trace');
575+
expect(result.baseName).toBe('wal');
576576
expect(result.walExtension).toBe('.log');
577577
expect(result.finalExtension).toBe('.log');
578578
expect(result.codec).toBeDefined();
@@ -641,6 +641,27 @@ describe('parseWalFormat', () => {
641641
expect(result.finalizer(['line1', 'line2'])).toBe('line1\nline2\n');
642642
expect(result.finalizer([])).toBe('\n');
643643
});
644+
645+
it('should encode objects to JSON strings in default finalizer', () => {
646+
const result = parseWalFormat<object>({ baseName: 'test' });
647+
const records = [
648+
{ id: 1, name: 'test' },
649+
{ id: 2, name: 'test2' },
650+
];
651+
const output = result.finalizer(records);
652+
expect(output).toBe('{"id":1,"name":"test"}\n{"id":2,"name":"test2"}\n');
653+
});
654+
655+
it('should handle InvalidEntry in default finalizer', () => {
656+
const result = parseWalFormat<string>({ baseName: 'test' });
657+
const records: (string | InvalidEntry<string>)[] = [
658+
'valid',
659+
{ __invalid: true, raw: 'invalid-raw' },
660+
'also-valid',
661+
];
662+
const output = result.finalizer(records);
663+
expect(output).toBe('valid\ninvalid-raw\nalso-valid\n');
664+
});
644665
});
645666

646667
describe('isLeaderWal', () => {

0 commit comments

Comments
 (0)