@logtape/file 1.0.0-dev.237 → 1.0.0-dev.241
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist/filesink.base.cjs +239 -22
- package/dist/filesink.base.d.cts +14 -0
- package/dist/filesink.base.d.cts.map +1 -1
- package/dist/filesink.base.d.ts +14 -0
- package/dist/filesink.base.d.ts.map +1 -1
- package/dist/filesink.base.js +239 -22
- package/dist/filesink.base.js.map +1 -1
- package/dist/filesink.deno.cjs +6 -0
- package/dist/filesink.deno.d.cts.map +1 -1
- package/dist/filesink.deno.d.ts.map +1 -1
- package/dist/filesink.deno.js +6 -0
- package/dist/filesink.deno.js.map +1 -1
- package/dist/filesink.node.cjs +16 -0
- package/dist/filesink.node.d.cts.map +1 -1
- package/dist/filesink.node.d.ts.map +1 -1
- package/dist/filesink.node.js +16 -0
- package/dist/filesink.node.js.map +1 -1
- package/dist/mod.cjs +3 -1
- package/dist/mod.d.cts +2 -1
- package/dist/mod.d.ts +2 -1
- package/dist/mod.js +2 -1
- package/dist/streamfilesink.cjs +84 -0
- package/dist/streamfilesink.d.cts +95 -0
- package/dist/streamfilesink.d.cts.map +1 -0
- package/dist/streamfilesink.d.ts +95 -0
- package/dist/streamfilesink.d.ts.map +1 -0
- package/dist/streamfilesink.js +84 -0
- package/dist/streamfilesink.js.map +1 -0
- package/filesink.base.ts +386 -25
- package/filesink.deno.ts +14 -0
- package/filesink.node.ts +18 -0
- package/mod.ts +2 -0
- package/package.json +2 -2
- package/streamfilesink.test.ts +336 -0
- package/streamfilesink.ts +136 -0
- package/tsdown.config.ts +2 -2
package/filesink.base.ts
CHANGED
|
@@ -5,6 +5,257 @@ import {
|
|
|
5
5
|
type StreamSinkOptions,
|
|
6
6
|
} from "@logtape/logtape";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Adaptive flush strategy that dynamically adjusts buffer thresholds
|
|
10
|
+
* based on recent flush patterns for optimal performance.
|
|
11
|
+
*/
|
|
12
|
+
class AdaptiveFlushStrategy {
|
|
13
|
+
private recentFlushSizes: number[] = [];
|
|
14
|
+
private recentFlushTimes: number[] = [];
|
|
15
|
+
private avgFlushSize: number;
|
|
16
|
+
private avgFlushInterval: number;
|
|
17
|
+
private readonly maxHistorySize = 10;
|
|
18
|
+
private readonly baseThreshold: number;
|
|
19
|
+
|
|
20
|
+
constructor(baseThreshold: number, baseInterval: number) {
|
|
21
|
+
this.baseThreshold = baseThreshold;
|
|
22
|
+
this.avgFlushSize = baseThreshold;
|
|
23
|
+
this.avgFlushInterval = baseInterval;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Record a flush event for pattern analysis.
|
|
28
|
+
* @param size The size of data flushed in bytes.
|
|
29
|
+
* @param timeSinceLastFlush Time since last flush in milliseconds.
|
|
30
|
+
*/
|
|
31
|
+
recordFlush(size: number, timeSinceLastFlush: number): void {
|
|
32
|
+
this.recentFlushSizes.push(size);
|
|
33
|
+
this.recentFlushTimes.push(timeSinceLastFlush);
|
|
34
|
+
|
|
35
|
+
// Keep only recent history
|
|
36
|
+
if (this.recentFlushSizes.length > this.maxHistorySize) {
|
|
37
|
+
this.recentFlushSizes.shift();
|
|
38
|
+
this.recentFlushTimes.shift();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Update averages
|
|
42
|
+
this.updateAverages();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Determine if buffer should be flushed based on adaptive strategy.
|
|
47
|
+
* @param currentSize Current buffer size in bytes.
|
|
48
|
+
* @param timeSinceLastFlush Time since last flush in milliseconds.
|
|
49
|
+
* @returns True if buffer should be flushed.
|
|
50
|
+
*/
|
|
51
|
+
shouldFlush(currentSize: number, timeSinceLastFlush: number): boolean {
|
|
52
|
+
const adaptiveThreshold = this.calculateAdaptiveThreshold();
|
|
53
|
+
const adaptiveInterval = this.calculateAdaptiveInterval();
|
|
54
|
+
|
|
55
|
+
return currentSize >= adaptiveThreshold ||
|
|
56
|
+
(adaptiveInterval > 0 && timeSinceLastFlush >= adaptiveInterval);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private updateAverages(): void {
|
|
60
|
+
if (this.recentFlushSizes.length === 0) return;
|
|
61
|
+
|
|
62
|
+
this.avgFlushSize =
|
|
63
|
+
this.recentFlushSizes.reduce((sum, size) => sum + size, 0) /
|
|
64
|
+
this.recentFlushSizes.length;
|
|
65
|
+
|
|
66
|
+
this.avgFlushInterval =
|
|
67
|
+
this.recentFlushTimes.reduce((sum, time) => sum + time, 0) /
|
|
68
|
+
this.recentFlushTimes.length;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private calculateAdaptiveThreshold(): number {
|
|
72
|
+
// Adjust threshold based on recent patterns
|
|
73
|
+
// Higher average flush sizes suggest larger batches are beneficial
|
|
74
|
+
const adaptiveFactor = Math.min(
|
|
75
|
+
2.0,
|
|
76
|
+
Math.max(0.5, this.avgFlushSize / this.baseThreshold),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return Math.max(
|
|
80
|
+
Math.min(4096, this.baseThreshold / 2),
|
|
81
|
+
Math.min(64 * 1024, this.baseThreshold * adaptiveFactor),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private calculateAdaptiveInterval(): number {
|
|
86
|
+
// If base interval is 0, time-based flushing is disabled
|
|
87
|
+
if (this.avgFlushInterval <= 0) return 0;
|
|
88
|
+
|
|
89
|
+
// Adjust interval based on recent flush frequency
|
|
90
|
+
// More frequent flushes suggest lower latency is preferred
|
|
91
|
+
if (this.recentFlushTimes.length < 3) return this.avgFlushInterval;
|
|
92
|
+
|
|
93
|
+
const variance = this.calculateVariance(this.recentFlushTimes);
|
|
94
|
+
const stabilityFactor = Math.min(2.0, Math.max(0.5, 1000 / variance));
|
|
95
|
+
|
|
96
|
+
return Math.max(
|
|
97
|
+
1000,
|
|
98
|
+
Math.min(10000, this.avgFlushInterval * stabilityFactor),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private calculateVariance(values: number[]): number {
|
|
103
|
+
if (values.length < 2) return 1000; // Default variance
|
|
104
|
+
|
|
105
|
+
const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
|
|
106
|
+
const squaredDiffs = values.map((val) => Math.pow(val - mean, 2));
|
|
107
|
+
return squaredDiffs.reduce((sum, diff) => sum + diff, 0) / values.length;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Memory pool for reusing Uint8Array buffers to minimize GC pressure.
|
|
113
|
+
* Maintains a pool of pre-allocated buffers for efficient reuse.
|
|
114
|
+
*/
|
|
115
|
+
class BufferPool {
|
|
116
|
+
private pool: Uint8Array[] = [];
|
|
117
|
+
private readonly maxPoolSize = 50; // Keep a reasonable pool size
|
|
118
|
+
private readonly maxBufferSize = 64 * 1024; // Don't pool very large buffers
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Acquire a buffer from the pool or create a new one.
|
|
122
|
+
* @param size The minimum size needed for the buffer.
|
|
123
|
+
* @returns A Uint8Array that can be used for encoding.
|
|
124
|
+
*/
|
|
125
|
+
acquire(size: number): Uint8Array {
|
|
126
|
+
// Don't pool very large buffers to avoid memory waste
|
|
127
|
+
if (size > this.maxBufferSize) {
|
|
128
|
+
return new Uint8Array(size);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Try to find a suitable buffer from the pool
|
|
132
|
+
for (let i = this.pool.length - 1; i >= 0; i--) {
|
|
133
|
+
const buffer = this.pool[i];
|
|
134
|
+
if (buffer.length >= size) {
|
|
135
|
+
// Remove from pool and return
|
|
136
|
+
this.pool.splice(i, 1);
|
|
137
|
+
return buffer.subarray(0, size);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// No suitable buffer found, create a new one
|
|
142
|
+
// Create slightly larger buffer to improve reuse chances
|
|
143
|
+
const actualSize = Math.max(size, 1024); // Minimum 1KB
|
|
144
|
+
return new Uint8Array(actualSize);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Return a buffer to the pool for future reuse.
|
|
149
|
+
* @param buffer The buffer to return to the pool.
|
|
150
|
+
*/
|
|
151
|
+
release(buffer: Uint8Array): void {
|
|
152
|
+
// Don't pool if we're at capacity or buffer is too large
|
|
153
|
+
if (
|
|
154
|
+
this.pool.length >= this.maxPoolSize || buffer.length > this.maxBufferSize
|
|
155
|
+
) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Don't pool very small buffers as they're cheap to allocate
|
|
160
|
+
if (buffer.length < 256) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Add to pool for reuse
|
|
165
|
+
this.pool.push(buffer);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Clear the pool to free memory. Useful for cleanup.
|
|
170
|
+
*/
|
|
171
|
+
clear(): void {
|
|
172
|
+
this.pool.length = 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get current pool statistics for monitoring.
|
|
177
|
+
* @returns Object with pool size and buffer count.
|
|
178
|
+
*/
|
|
179
|
+
getStats(): { poolSize: number; totalBuffers: number } {
|
|
180
|
+
return {
|
|
181
|
+
poolSize: this.pool.reduce((sum, buf) => sum + buf.length, 0),
|
|
182
|
+
totalBuffers: this.pool.length,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* High-performance byte buffer for batching log records.
|
|
189
|
+
* Eliminates string concatenation overhead by storing pre-encoded bytes.
|
|
190
|
+
* Uses memory pooling to reduce GC pressure.
|
|
191
|
+
*/
|
|
192
|
+
class ByteRingBuffer {
|
|
193
|
+
private buffers: Uint8Array[] = [];
|
|
194
|
+
private totalSize: number = 0;
|
|
195
|
+
private bufferPool: BufferPool;
|
|
196
|
+
|
|
197
|
+
constructor(bufferPool: BufferPool) {
|
|
198
|
+
this.bufferPool = bufferPool;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Append pre-encoded log record bytes to the buffer.
|
|
203
|
+
* @param data The encoded log record as bytes.
|
|
204
|
+
*/
|
|
205
|
+
append(data: Uint8Array): void {
|
|
206
|
+
this.buffers.push(data);
|
|
207
|
+
this.totalSize += data.length;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get the current total size of buffered data in bytes.
|
|
212
|
+
* @returns The total size in bytes.
|
|
213
|
+
*/
|
|
214
|
+
size(): number {
|
|
215
|
+
return this.totalSize;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get the number of buffered records.
|
|
220
|
+
* @returns The number of records in the buffer.
|
|
221
|
+
*/
|
|
222
|
+
count(): number {
|
|
223
|
+
return this.buffers.length;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Flush all buffered data and return it as an array of byte arrays.
|
|
228
|
+
* This clears the internal buffer and returns used buffers to the pool.
|
|
229
|
+
* @returns Array of buffered byte arrays ready for writev() operations.
|
|
230
|
+
*/
|
|
231
|
+
flush(): Uint8Array[] {
|
|
232
|
+
const result = [...this.buffers];
|
|
233
|
+
this.clear();
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Clear the buffer without returning data.
|
|
239
|
+
* Returns buffers to the pool for reuse.
|
|
240
|
+
*/
|
|
241
|
+
clear(): void {
|
|
242
|
+
// Return buffers to pool for reuse
|
|
243
|
+
for (const buffer of this.buffers) {
|
|
244
|
+
this.bufferPool.release(buffer);
|
|
245
|
+
}
|
|
246
|
+
this.buffers.length = 0;
|
|
247
|
+
this.totalSize = 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Check if the buffer is empty.
|
|
252
|
+
* @returns True if the buffer contains no data.
|
|
253
|
+
*/
|
|
254
|
+
isEmpty(): boolean {
|
|
255
|
+
return this.buffers.length === 0;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
8
259
|
/**
|
|
9
260
|
* Options for the {@link getBaseFileSink} function.
|
|
10
261
|
*/
|
|
@@ -61,6 +312,14 @@ export interface FileSinkDriver<TFile> {
|
|
|
61
312
|
*/
|
|
62
313
|
writeSync(fd: TFile, chunk: Uint8Array): void;
|
|
63
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Write multiple chunks of data to the file in a single operation.
|
|
317
|
+
* This is optional - if not implemented, falls back to multiple writeSync calls.
|
|
318
|
+
* @param fd The file descriptor.
|
|
319
|
+
* @param chunks Array of data chunks to write.
|
|
320
|
+
*/
|
|
321
|
+
writeManySync?(fd: TFile, chunks: Uint8Array[]): void;
|
|
322
|
+
|
|
64
323
|
/**
|
|
65
324
|
* Flush the file to ensure that all data is written to the disk.
|
|
66
325
|
* @param fd The file descriptor.
|
|
@@ -80,6 +339,14 @@ export interface FileSinkDriver<TFile> {
|
|
|
80
339
|
* @since 1.0.0
|
|
81
340
|
*/
|
|
82
341
|
export interface AsyncFileSinkDriver<TFile> extends FileSinkDriver<TFile> {
|
|
342
|
+
/**
|
|
343
|
+
* Asynchronously write multiple chunks of data to the file in a single operation.
|
|
344
|
+
* This is optional - if not implemented, falls back to multiple writeSync calls.
|
|
345
|
+
* @param fd The file descriptor.
|
|
346
|
+
* @param chunks Array of data chunks to write.
|
|
347
|
+
*/
|
|
348
|
+
writeMany?(fd: TFile, chunks: Uint8Array[]): Promise<void>;
|
|
349
|
+
|
|
83
350
|
/**
|
|
84
351
|
* Asynchronously flush the file to ensure that all data is written to the disk.
|
|
85
352
|
* @param fd The file descriptor.
|
|
@@ -119,35 +386,82 @@ export function getBaseFileSink<TFile>(
|
|
|
119
386
|
): Sink & (Disposable | AsyncDisposable) {
|
|
120
387
|
const formatter = options.formatter ?? defaultTextFormatter;
|
|
121
388
|
const encoder = options.encoder ?? new TextEncoder();
|
|
122
|
-
const bufferSize = options.bufferSize ?? 1024 * 8; // Default buffer size of 8192
|
|
389
|
+
const bufferSize = options.bufferSize ?? 1024 * 8; // Default buffer size of 8192 bytes
|
|
123
390
|
const flushInterval = options.flushInterval ?? 5000; // Default flush interval of 5 seconds
|
|
124
391
|
let fd = options.lazy ? null : options.openSync(path);
|
|
125
|
-
|
|
392
|
+
|
|
393
|
+
// Initialize memory pool and buffer systems
|
|
394
|
+
const bufferPool = new BufferPool();
|
|
395
|
+
const byteBuffer = new ByteRingBuffer(bufferPool);
|
|
396
|
+
const adaptiveStrategy = new AdaptiveFlushStrategy(bufferSize, flushInterval);
|
|
126
397
|
let lastFlushTimestamp: number = Date.now();
|
|
127
398
|
|
|
128
399
|
if (!options.nonBlocking) {
|
|
129
400
|
// Blocking mode implementation
|
|
130
401
|
// deno-lint-ignore no-inner-declarations
|
|
131
402
|
function flushBuffer(): void {
|
|
132
|
-
if (fd == null) return;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
403
|
+
if (fd == null || byteBuffer.isEmpty()) return;
|
|
404
|
+
|
|
405
|
+
const flushSize = byteBuffer.size();
|
|
406
|
+
const currentTime = Date.now();
|
|
407
|
+
const timeSinceLastFlush = currentTime - lastFlushTimestamp;
|
|
408
|
+
|
|
409
|
+
const chunks = byteBuffer.flush();
|
|
410
|
+
if (options.writeManySync && chunks.length > 1) {
|
|
411
|
+
// Use batch write if available
|
|
412
|
+
options.writeManySync(fd, chunks);
|
|
413
|
+
} else {
|
|
414
|
+
// Fallback to individual writes
|
|
415
|
+
for (const chunk of chunks) {
|
|
416
|
+
options.writeSync(fd, chunk);
|
|
417
|
+
}
|
|
138
418
|
}
|
|
419
|
+
options.flushSync(fd);
|
|
420
|
+
|
|
421
|
+
// Record flush for adaptive strategy
|
|
422
|
+
adaptiveStrategy.recordFlush(flushSize, timeSinceLastFlush);
|
|
423
|
+
lastFlushTimestamp = currentTime;
|
|
139
424
|
}
|
|
140
425
|
|
|
141
426
|
const sink: Sink & Disposable = (record: LogRecord) => {
|
|
142
427
|
if (fd == null) fd = options.openSync(path);
|
|
143
|
-
buffer += formatter(record);
|
|
144
428
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
429
|
+
// ULTRA FAST PATH: Direct write when buffer is empty and using default buffer settings
|
|
430
|
+
if (byteBuffer.isEmpty() && bufferSize === 8192) {
|
|
431
|
+
// Inline everything for maximum speed - avoid all function calls
|
|
432
|
+
const formattedRecord = formatter(record);
|
|
433
|
+
const encodedRecord = encoder.encode(formattedRecord);
|
|
434
|
+
|
|
435
|
+
// Only use fast path for typical log sizes to avoid breaking edge cases
|
|
436
|
+
if (encodedRecord.length < 200) {
|
|
437
|
+
// Write directly for small logs - no complex buffering logic
|
|
438
|
+
options.writeSync(fd, encodedRecord);
|
|
439
|
+
options.flushSync(fd);
|
|
440
|
+
lastFlushTimestamp = Date.now();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
148
444
|
|
|
149
|
-
|
|
445
|
+
// STANDARD PATH: Complex logic for edge cases
|
|
446
|
+
const formattedRecord = formatter(record);
|
|
447
|
+
const encodedRecord = encoder.encode(formattedRecord);
|
|
448
|
+
byteBuffer.append(encodedRecord);
|
|
449
|
+
|
|
450
|
+
// Check for immediate flush conditions
|
|
451
|
+
if (bufferSize <= 0) {
|
|
452
|
+
// No buffering - flush immediately
|
|
150
453
|
flushBuffer();
|
|
454
|
+
} else {
|
|
455
|
+
// Use adaptive strategy for intelligent flushing
|
|
456
|
+
const timeSinceLastFlush = record.timestamp - lastFlushTimestamp;
|
|
457
|
+
const shouldFlush = adaptiveStrategy.shouldFlush(
|
|
458
|
+
byteBuffer.size(),
|
|
459
|
+
timeSinceLastFlush,
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
if (shouldFlush) {
|
|
463
|
+
flushBuffer();
|
|
464
|
+
}
|
|
151
465
|
}
|
|
152
466
|
};
|
|
153
467
|
sink[Symbol.dispose] = () => {
|
|
@@ -155,6 +469,8 @@ export function getBaseFileSink<TFile>(
|
|
|
155
469
|
flushBuffer();
|
|
156
470
|
options.closeSync(fd);
|
|
157
471
|
}
|
|
472
|
+
// Clean up buffer pool
|
|
473
|
+
bufferPool.clear();
|
|
158
474
|
};
|
|
159
475
|
return sink;
|
|
160
476
|
}
|
|
@@ -166,14 +482,28 @@ export function getBaseFileSink<TFile>(
|
|
|
166
482
|
let flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
167
483
|
|
|
168
484
|
async function flushBuffer(): Promise<void> {
|
|
169
|
-
if (fd == null ||
|
|
485
|
+
if (fd == null || byteBuffer.isEmpty()) return;
|
|
170
486
|
|
|
171
|
-
const
|
|
172
|
-
|
|
487
|
+
const flushSize = byteBuffer.size();
|
|
488
|
+
const currentTime = Date.now();
|
|
489
|
+
const timeSinceLastFlush = currentTime - lastFlushTimestamp;
|
|
490
|
+
|
|
491
|
+
const chunks = byteBuffer.flush();
|
|
173
492
|
try {
|
|
174
|
-
asyncOptions.
|
|
493
|
+
if (asyncOptions.writeMany && chunks.length > 1) {
|
|
494
|
+
// Use async batch write if available
|
|
495
|
+
await asyncOptions.writeMany(fd, chunks);
|
|
496
|
+
} else {
|
|
497
|
+
// Fallback to individual writes
|
|
498
|
+
for (const chunk of chunks) {
|
|
499
|
+
asyncOptions.writeSync(fd, chunk);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
175
502
|
await asyncOptions.flush(fd);
|
|
176
|
-
|
|
503
|
+
|
|
504
|
+
// Record flush for adaptive strategy
|
|
505
|
+
adaptiveStrategy.recordFlush(flushSize, timeSinceLastFlush);
|
|
506
|
+
lastFlushTimestamp = currentTime;
|
|
177
507
|
} catch {
|
|
178
508
|
// Silently ignore errors in non-blocking mode
|
|
179
509
|
}
|
|
@@ -198,16 +528,45 @@ export function getBaseFileSink<TFile>(
|
|
|
198
528
|
const nonBlockingSink: Sink & AsyncDisposable = (record: LogRecord) => {
|
|
199
529
|
if (disposed) return;
|
|
200
530
|
if (fd == null) fd = asyncOptions.openSync(path);
|
|
201
|
-
buffer += formatter(record);
|
|
202
531
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
532
|
+
// ULTRA FAST PATH: Direct write when buffer is empty and using default buffer settings
|
|
533
|
+
if (byteBuffer.isEmpty() && !activeFlush && bufferSize === 8192) {
|
|
534
|
+
// Inline everything for maximum speed - avoid all function calls
|
|
535
|
+
const formattedRecord = formatter(record);
|
|
536
|
+
const encodedRecord = encoder.encode(formattedRecord);
|
|
537
|
+
|
|
538
|
+
// Only use fast path for typical log sizes to avoid breaking edge cases
|
|
539
|
+
if (encodedRecord.length < 200) {
|
|
540
|
+
// Write directly for small logs - no complex buffering logic
|
|
541
|
+
asyncOptions.writeSync(fd, encodedRecord);
|
|
542
|
+
scheduleFlush(); // Async flush
|
|
543
|
+
lastFlushTimestamp = Date.now();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
206
547
|
|
|
207
|
-
|
|
548
|
+
// STANDARD PATH: Complex logic for edge cases
|
|
549
|
+
const formattedRecord = formatter(record);
|
|
550
|
+
const encodedRecord = encoder.encode(formattedRecord);
|
|
551
|
+
byteBuffer.append(encodedRecord);
|
|
552
|
+
|
|
553
|
+
// Check for immediate flush conditions
|
|
554
|
+
if (bufferSize <= 0) {
|
|
555
|
+
// No buffering - flush immediately
|
|
208
556
|
scheduleFlush();
|
|
209
|
-
} else
|
|
210
|
-
|
|
557
|
+
} else {
|
|
558
|
+
// Use adaptive strategy for intelligent flushing
|
|
559
|
+
const timeSinceLastFlush = record.timestamp - lastFlushTimestamp;
|
|
560
|
+
const shouldFlush = adaptiveStrategy.shouldFlush(
|
|
561
|
+
byteBuffer.size(),
|
|
562
|
+
timeSinceLastFlush,
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
if (shouldFlush) {
|
|
566
|
+
scheduleFlush();
|
|
567
|
+
} else if (flushTimer === null && flushInterval > 0) {
|
|
568
|
+
startFlushTimer();
|
|
569
|
+
}
|
|
211
570
|
}
|
|
212
571
|
};
|
|
213
572
|
|
|
@@ -225,6 +584,8 @@ export function getBaseFileSink<TFile>(
|
|
|
225
584
|
// Writer might already be closed or errored
|
|
226
585
|
}
|
|
227
586
|
}
|
|
587
|
+
// Clean up buffer pool
|
|
588
|
+
bufferPool.clear();
|
|
228
589
|
};
|
|
229
590
|
|
|
230
591
|
return nonBlockingSink;
|
package/filesink.deno.ts
CHANGED
|
@@ -18,6 +18,13 @@ export const denoDriver: RotatingFileSinkDriver<Deno.FsFile> = {
|
|
|
18
18
|
writeSync(fd, chunk) {
|
|
19
19
|
fd.writeSync(chunk);
|
|
20
20
|
},
|
|
21
|
+
writeManySync(fd: Deno.FsFile, chunks: Uint8Array[]): void {
|
|
22
|
+
// Deno doesn't have writev, but we can optimize by writing all chunks
|
|
23
|
+
// then doing a single sync operation
|
|
24
|
+
for (const chunk of chunks) {
|
|
25
|
+
fd.writeSync(chunk);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
21
28
|
flushSync(fd) {
|
|
22
29
|
fd.syncSync();
|
|
23
30
|
},
|
|
@@ -34,6 +41,13 @@ export const denoDriver: RotatingFileSinkDriver<Deno.FsFile> = {
|
|
|
34
41
|
*/
|
|
35
42
|
export const denoAsyncDriver: AsyncRotatingFileSinkDriver<Deno.FsFile> = {
|
|
36
43
|
...denoDriver,
|
|
44
|
+
async writeMany(fd: Deno.FsFile, chunks: Uint8Array[]): Promise<void> {
|
|
45
|
+
// Deno doesn't have async writev, but we can write all chunks
|
|
46
|
+
// then do a single async sync
|
|
47
|
+
for (const chunk of chunks) {
|
|
48
|
+
await fd.write(chunk);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
37
51
|
async flush(fd) {
|
|
38
52
|
await fd.sync();
|
|
39
53
|
},
|
package/filesink.node.ts
CHANGED
|
@@ -18,6 +18,15 @@ export const nodeDriver: RotatingFileSinkDriver<number | void> = {
|
|
|
18
18
|
return fs.openSync(path, "a");
|
|
19
19
|
},
|
|
20
20
|
writeSync: fs.writeSync,
|
|
21
|
+
writeManySync(fd: number, chunks: Uint8Array[]): void {
|
|
22
|
+
if (chunks.length === 0) return;
|
|
23
|
+
if (chunks.length === 1) {
|
|
24
|
+
fs.writeSync(fd, chunks[0]);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// Use writev for multiple chunks
|
|
28
|
+
fs.writevSync(fd, chunks);
|
|
29
|
+
},
|
|
21
30
|
flushSync: fs.fsyncSync,
|
|
22
31
|
closeSync: fs.closeSync,
|
|
23
32
|
statSync: fs.statSync,
|
|
@@ -30,6 +39,15 @@ export const nodeDriver: RotatingFileSinkDriver<number | void> = {
|
|
|
30
39
|
*/
|
|
31
40
|
export const nodeAsyncDriver: AsyncRotatingFileSinkDriver<number | void> = {
|
|
32
41
|
...nodeDriver,
|
|
42
|
+
async writeMany(fd: number, chunks: Uint8Array[]): Promise<void> {
|
|
43
|
+
if (chunks.length === 0) return;
|
|
44
|
+
if (chunks.length === 1) {
|
|
45
|
+
await promisify(fs.write)(fd, chunks[0]);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Use async writev for multiple chunks
|
|
49
|
+
await promisify(fs.writev)(fd, chunks);
|
|
50
|
+
},
|
|
33
51
|
flush: promisify(fs.fsync),
|
|
34
52
|
close: promisify(fs.close),
|
|
35
53
|
};
|
package/mod.ts
CHANGED
|
@@ -4,4 +4,6 @@ export type {
|
|
|
4
4
|
RotatingFileSinkDriver,
|
|
5
5
|
RotatingFileSinkOptions,
|
|
6
6
|
} from "./filesink.base.ts";
|
|
7
|
+
export type { StreamFileSinkOptions } from "./streamfilesink.ts";
|
|
7
8
|
export { getFileSink, getRotatingFileSink } from "#filesink";
|
|
9
|
+
export { getStreamFileSink } from "./streamfilesink.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logtape/file",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.241+13810dcf",
|
|
4
4
|
"description": "File sink and rotating file sink for LogTape",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"logging",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@logtape/logtape": "1.0.0-dev.
|
|
53
|
+
"@logtape/logtape": "1.0.0-dev.241+13810dcf"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@alinea/suite": "^0.6.3",
|