next
Version:
The React Framework
215 lines (214 loc) • 8.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
createJsonReporter: null,
default: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
createJsonReporter: function() {
return createJsonReporter;
},
default: function() {
return _default;
}
});
const _shared = require("../shared");
const _fs = /*#__PURE__*/ _interop_require_default(require("fs"));
const _path = /*#__PURE__*/ _interop_require_default(require("path"));
const _constants = require("../../shared/lib/constants");
function _interop_require_default(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
// The webpack/rspack profiling plugins open a span per module, so writing
// events individually would put a syscall in the middle of every module build.
const BUFFER_SIZE = 16 * 1024;
// Each flush is wrapped as a JSON array; the tail of the buffer is reserved
// for this and never used by event data.
const TERMINATOR = Buffer.from(']\n', 'utf8');
const MAX_UTF8_CHAR_BYTES = 4;
/**
* An append-only newline-delimited JSON file.
*
* Closing is not part of the normal lifetime: `writeSync` hands the bytes to
* the OS before it returns, so once flushed they survive `process.exit()` and
* crashes that run no shutdown path. Only the unflushed tail is at risk, which
* is why `flush()` is called wherever traces need to be on disk. `close()`
* exists for callers that must then act on the file itself.
*/ class RotatingWriteStream {
constructor(file, sizeLimit){
this.buffer = Buffer.allocUnsafe(BUFFER_SIZE);
// Bytes of event data in `buffer`, excluding the terminator written at
// flush time. 0 means nothing is pending.
this.buffered = 0;
this.size = 0;
this.file = file;
this.sizeLimit = sizeLimit;
// TODO: opening a file in append mode like we do in dev should seed the
// size from the current file size.
this.size = 0;
}
/**
* Open on first write rather than up front. The dev server records spans
* before it cleans its dist dir, and a descriptor opened that early would be
* unlinked by the clean -- writes to the orphaned inode still succeed, so
* the trace would silently never appear on disk.
*/ open() {
if (this.fd === undefined) {
_fs.default.mkdirSync(_path.default.dirname(this.file), {
recursive: true
});
// In dev, append so traces accumulate across sessions. In production,
// truncate so each build starts with a fresh trace file.
this.fd = _fs.default.openSync(this.file, _shared.traceGlobals.get('phase') === _constants.PHASE_DEVELOPMENT_SERVER ? 'a' : 'w');
}
return this.fd;
}
// Recreate the file
rotate() {
this.closeFd();
_fs.default.mkdirSync(_path.default.dirname(this.file), {
recursive: true
});
// Opening in 'w' mode truncates the file, discarding what rotated out.
this.fd = _fs.default.openSync(this.file, 'w');
this.size = 0;
}
/**
* Append one already-encoded event. Events are written as a single JSON
* array per flush, which is the format readers expect (see
* `parseTraceEvents` and the trace uploader).
*/ write(encodedEvent) {
const capacity = this.buffer.length - TERMINATOR.length;
if (this.tryWriteToBuffer(encodedEvent, capacity)) {
return;
}
// Didn't fit. Start a fresh batch and try again.
this.flush();
if (this.tryWriteToBuffer(encodedEvent, capacity)) {
return;
}
// Too large to ever fit in the buffer, so give it a batch of its own.
this.writeToFile(Buffer.from(`[${encodedEvent}]\n`, 'utf8'));
}
/**
* Append `[event` or `,event` to the buffer, returning false without
* modifying it if the whole thing does not fit.
*/ tryWriteToBuffer(encodedEvent, capacity) {
// `[` opens a new batch, `,` separates events within one.
const prefix = this.buffered === 0 ? '[' : ',';
const written = this.buffer.write(prefix + encodedEvent, this.buffered, capacity - this.buffered, 'utf8');
// `Buffer.write` truncates silently, so the byte count is the only signal
// that the event did not fit -- and because it stops at the last whole
// character, a truncated write can still leave a few bytes unused. Only a
// write that ends clear of that margin is known to be complete.
if (this.buffered + written > capacity - MAX_UTF8_CHAR_BYTES) {
return false;
}
this.buffered += written;
return true;
}
flush() {
if (this.buffered === 0) {
return;
}
this.buffered += TERMINATOR.copy(this.buffer, this.buffered);
const data = this.buffer.subarray(0, this.buffered);
this.buffered = 0;
this.writeToFile(data);
}
writeToFile(data) {
let offset = 0;
try {
if (this.size + data.length > this.sizeLimit) {
this.rotate();
}
const fd = this.open();
// writeSync can write fewer than all the bytes
while(offset < data.length){
offset += _fs.default.writeSync(fd, data, offset);
}
} catch (err) {
// Tracing is diagnostic, so a failed write should not fail the build.
// N.B. This is incredibly rare and a torn write means the data may be corrupted on disk.
console.log(err);
}
this.size += offset;
}
/**
* Release the descriptor without flushing. Callers that want the buffered
* tail on disk flush first; `rotate` deliberately does not.
*/ closeFd() {
if (this.fd === undefined) {
return;
}
try {
_fs.default.closeSync(this.fd);
} catch {
// Nothing useful to do if the descriptor is already gone.
}
this.fd = undefined;
}
/**
* Flush and release the descriptor. Only needed when something else has to
* act on the file (Windows cannot remove a file that is still open), which
* in practice means tests -- the normal lifetime of the process is the
* lifetime of the trace.
*/ close() {
this.flush();
this.closeFd();
}
}
function createJsonReporter(options) {
let writeStream;
function report(event) {
if (options.filter && !options.filter(event)) {
return;
}
const distDir = _shared.traceGlobals.get('distDir');
const phase = _shared.traceGlobals.get('phase');
if (!distDir || !phase) {
return;
}
const file = _path.default.join(distDir, options.filename);
// `distDir` can change within a process (most visibly between tests), so
// a stream opened against a previous one must not keep receiving events.
if (writeStream && writeStream.file !== file) {
writeStream.close();
writeStream = undefined;
}
if (!writeStream) {
// Constructing the stream touches no filesystem state -- the directory
// and file are created lazily on the first write.
const limit = typeof options.sizeLimit === 'function' ? options.sizeLimit(phase) : options.sizeLimit;
writeStream = new RotatingWriteStream(file, limit);
}
writeStream.write(JSON.stringify({
...event,
traceId: _shared.traceId
}));
}
return {
flushAll: ()=>writeStream == null ? void 0 : writeStream.flush(),
close: ()=>{
writeStream == null ? void 0 : writeStream.close();
writeStream = undefined;
},
report
};
}
const _default = createJsonReporter({
filename: 'trace',
sizeLimit: (phase)=>// Development is limited to 50MB, production is unlimited
phase === _constants.PHASE_DEVELOPMENT_SERVER ? 52428800 : Infinity
});
//# sourceMappingURL=to-json.js.map