next
Version:
The React Framework
520 lines (519 loc) • 23.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
const _promises = require("fs/promises");
const _fs = /*#__PURE__*/ _interop_require_wildcard(require("fs"));
const _path = require("path");
const _os = require("os");
const _constants = require("../shared/lib/constants");
const _tojson = require("./report/to-json");
const _shared = require("./shared");
const _trace = require("./trace");
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function(nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interop_require_wildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
return {
default: obj
};
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {
__proto__: null
};
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
for(var key in obj){
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
function traceEvent(name) {
return {
name,
duration: 1,
timestamp: 1,
id: 1,
startTime: 1,
tags: {}
};
}
// Newline-delimited JSON: each line is an array of spans, as read by
// `test/lib/parse-trace-file.ts` and `trace-uploader.ts`.
function readSpans(file) {
return (0, _fs.readFileSync)(file, 'utf-8').split('\n').filter(Boolean).flatMap((line)=>JSON.parse(line));
}
function readSpanNames(file) {
return readSpans(file).map((event)=>event.name);
}
describe('Trace', ()=>{
const tmpDirs = [];
async function makeTmpDir() {
const dir = await (0, _promises.mkdtemp)((0, _path.join)((0, _os.tmpdir)(), 'json-reporter'));
tmpDirs.push(dir);
return dir;
}
// Reporters created by a test hold a descriptor on their trace file until
// closed. `closeAllTraces` only reaches the global reporter, so these have
// to be tracked separately or cleanup cannot remove their directory.
const reporters = [];
function makeJsonReporter(...args) {
const reporter = (0, _tojson.createJsonReporter)(...args);
reporters.push(reporter);
return reporter;
}
beforeEach(()=>{
(0, _trace.initializeTraceState)({
lastId: 0,
shouldSaveTraceEvents: true
});
(0, _trace.clearTraceEvents)();
// `traceGlobals` outlives each test, so reset what these tests set.
(0, _shared.setGlobal)('distDir', undefined);
(0, _shared.setGlobal)('phase', undefined);
});
afterAll(async ()=>{
// Windows refuses to remove a directory containing an open file, so every
// trace file handle has to go first -- the global reporter's and the ones
// the tests created themselves.
(0, _trace.closeAllTraces)();
for (const reporter of reporters.splice(0)){
reporter.close == null ? void 0 : reporter.close.call(reporter);
}
await Promise.all(tmpDirs.splice(0).map((dir)=>(0, _promises.rm)(dir, {
recursive: true,
force: true
})));
});
describe('Tracer', ()=>{
it('traces a block of code', async ()=>{
const tmpDir = await makeTmpDir();
(0, _shared.setGlobal)('distDir', tmpDir);
(0, _shared.setGlobal)('phase', 'anything');
const root = (0, _trace.trace)('root-span', undefined, {
'some-tag': 'some-value'
});
root.traceChild('child-span').traceFn(()=>null);
await root.traceChild('async-child-span').traceAsyncFn(async ()=>{
const delayedPromise = new Promise((resolve)=>{
setTimeout(resolve, 100);
});
await delayedPromise;
});
root.stop();
const traceEvents = (0, _trace.getTraceEvents)();
expect(traceEvents.length).toEqual(3);
expect(traceEvents[0].name).toEqual('child-span');
expect(traceEvents[1].name).toEqual('async-child-span');
expect(traceEvents[2].name).toEqual('root-span');
// Check that the serialized .next/trace file looks correct.
(0, _trace.flushAllTraces)();
const serializedTraces = readSpans((0, _path.join)(tmpDir, 'trace'));
expect(serializedTraces).toMatchObject([
{
id: 2,
name: 'child-span',
parentId: 1,
startTime: expect.any(Number),
timestamp: expect.any(Number),
duration: expect.any(Number),
tags: {}
},
{
id: 3,
name: 'async-child-span',
parentId: 1,
startTime: expect.any(Number),
timestamp: expect.any(Number),
duration: expect.any(Number),
tags: {}
},
{
id: 1,
name: 'root-span',
startTime: expect.any(Number),
timestamp: expect.any(Number),
duration: expect.any(Number),
tags: {
'some-tag': 'some-value'
}
}
]);
});
it('records manualTraceChild spans with their own start time', ()=>{
const root = (0, _trace.trace)('root-span');
// A span that started one minute ago and ran for 1.5s, as happens when
// the event is only recorded after the work completes (e.g. Turbopack
// compilation events forwarded from Rust).
const startMs = Date.now() - 60000;
const stopMs = startMs + 1500;
const msToNs = (ms)=>BigInt(ms) * BigInt(1000000);
root.manualTraceChild('manual-child', msToNs(startMs), msToNs(stopMs));
root.stop();
const events = (0, _trace.getTraceEvents)();
const child = events.find((e)=>e.name === 'manual-child');
expect(child).toBeDefined();
// startTime must be the span's actual start, not the time the event was
// recorded (which is when manualTraceChild is called).
expect(child.startTime).toEqual(startMs);
expect(child.duration).toEqual(1500000);
});
// Builds flush repeatedly -- after each compilation, and again at the
// end -- so every flush must leave the file usable.
it('writes buffered spans on each flush', async ()=>{
const tmpDir = await makeTmpDir();
(0, _shared.setGlobal)('distDir', tmpDir);
(0, _shared.setGlobal)('phase', _constants.PHASE_PRODUCTION_BUILD);
const reporter = makeJsonReporter({
filename: 'trace',
sizeLimit: Infinity
});
const file = (0, _path.join)(tmpDir, 'trace');
reporter.report(traceEvent('first'));
reporter.flushAll();
expect(readSpanNames(file)).toEqual([
'first'
]);
// A flush with nothing buffered is harmless.
reporter.report(traceEvent('second'));
reporter.flushAll();
reporter.flushAll();
expect(readSpanNames(file)).toEqual([
'first',
'second'
]);
});
// The dev server records spans before it cleans its dist dir. Holding a
// descriptor across that clean would leave it pointing at an unlinked
// inode -- writes still succeed, so the trace silently never appears on
// disk. Staying closed until there is something to write avoids it.
it('does not touch the filesystem until the first flush', async ()=>{
const tmpDir = await makeTmpDir();
const distDir = (0, _path.join)(tmpDir, 'dist');
(0, _shared.setGlobal)('distDir', distDir);
(0, _shared.setGlobal)('phase', _constants.PHASE_DEVELOPMENT_SERVER);
const reporter = makeJsonReporter({
filename: 'trace',
sizeLimit: Infinity
});
reporter.report(traceEvent('buffered'));
// Buffered only: neither the dist dir nor the file exists yet, so a
// clean at this point has nothing to unlink.
expect(_fs.default.existsSync(distDir)).toBe(false);
reporter.flushAll();
expect(readSpanNames((0, _path.join)(distDir, 'trace'))).toEqual([
'buffered'
]);
});
it('flushes on its own once the buffer fills', async ()=>{
const tmpDir = await makeTmpDir();
(0, _shared.setGlobal)('distDir', tmpDir);
(0, _shared.setGlobal)('phase', _constants.PHASE_PRODUCTION_BUILD);
const reporter = makeJsonReporter({
filename: 'trace',
sizeLimit: Infinity
});
const file = (0, _path.join)(tmpDir, 'trace');
// Each event encodes to ~100 bytes, so this far exceeds the buffer.
for(let i = 0; i < 2000; i++){
reporter.report(traceEvent(`span-${i}`));
}
expect(readSpanNames(file).length).toBeGreaterThan(0);
reporter.flushAll();
expect(readSpanNames(file).length).toBe(2000);
});
it('truncates in production but appends in development', async ()=>{
const tmpDir = await makeTmpDir();
(0, _shared.setGlobal)('distDir', tmpDir);
const file = (0, _path.join)(tmpDir, 'trace');
(0, _shared.setGlobal)('phase', _constants.PHASE_PRODUCTION_BUILD);
const build = makeJsonReporter({
filename: 'trace',
sizeLimit: Infinity
});
build.report(traceEvent('from-build'));
build.flushAll();
expect(readSpanNames(file)).toEqual([
'from-build'
]);
// A second production reporter starts the file over.
const rebuild = makeJsonReporter({
filename: 'trace',
sizeLimit: Infinity
});
rebuild.report(traceEvent('from-rebuild'));
rebuild.flushAll();
expect(readSpanNames(file)).toEqual([
'from-rebuild'
]);
// Dev keeps accumulating across sessions instead.
(0, _shared.setGlobal)('phase', _constants.PHASE_DEVELOPMENT_SERVER);
const dev = makeJsonReporter({
filename: 'trace',
sizeLimit: Infinity
});
dev.report(traceEvent('from-dev'));
dev.flushAll();
expect(readSpanNames(file)).toEqual([
'from-rebuild',
'from-dev'
]);
});
// The limit is counted in UTF-8 bytes: counting UTF-16 code units instead
// lets a non-ASCII trace reach ~3x the configured cap.
it('starts the file over once it exceeds the size limit', async ()=>{
const tmpDir = await makeTmpDir();
(0, _shared.setGlobal)('distDir', tmpDir);
(0, _shared.setGlobal)('phase', _constants.PHASE_PRODUCTION_BUILD);
const file = (0, _path.join)(tmpDir, 'trace');
// 200 CJK characters: 200 code units, but 600 UTF-8 bytes, so a limit
// of 1024 fits one of these lines and not two.
const first = '第'.repeat(200);
const second = '弐'.repeat(200);
const limit = 1024;
const reporter = makeJsonReporter({
filename: 'trace',
sizeLimit: limit
});
reporter.report(traceEvent(first));
reporter.flushAll();
expect(readSpanNames(file)).toEqual([
first
]);
reporter.report(traceEvent(second));
reporter.flushAll();
expect(readSpanNames(file)).toEqual([
second
]);
expect(Buffer.byteLength((0, _fs.readFileSync)(file, 'utf-8'), 'utf8')).toBeLessThanOrEqual(limit);
});
// A truncated batch fails quietly: the last line has no newline, so
// readers skip it and those spans simply vanish.
it('writes complete batches at every buffer boundary', async ()=>{
const tmpDir = await makeTmpDir();
(0, _shared.setGlobal)('distDir', tmpDir);
(0, _shared.setGlobal)('phase', _constants.PHASE_PRODUCTION_BUILD);
// Equal-sized events tile the buffer, so each length puts a batch
// boundary at a different offset. Sweeping rather than picking one
// length because the encoded size shifts with `traceId` ($TRACE_ID).
// 200 events of any of these sizes overflows the buffer several times.
for(let length = 100; length < 260; length++){
const reporter = makeJsonReporter({
filename: `trace-${length}`,
sizeLimit: Infinity
});
const names = [];
for(let i = 0; i < 200; i++){
const name = 'n'.repeat(length);
names.push(name);
reporter.report(traceEvent(name));
}
reporter.flushAll();
const written = (0, _path.join)(tmpDir, `trace-${length}`);
expect((0, _fs.readFileSync)(written, 'utf-8').endsWith(']\n')).toBe(true);
expect(readSpanNames(written)).toEqual(names);
}
});
it('writes an event too large to buffer', async ()=>{
const tmpDir = await makeTmpDir();
(0, _shared.setGlobal)('distDir', tmpDir);
(0, _shared.setGlobal)('phase', _constants.PHASE_PRODUCTION_BUILD);
const reporter = makeJsonReporter({
filename: 'trace',
sizeLimit: Infinity
});
const file = (0, _path.join)(tmpDir, 'trace');
// Far larger than the buffer.
const huge = 'x'.repeat(100 * 1024);
reporter.report(traceEvent('before-huge'));
reporter.report(traceEvent(huge));
reporter.report(traceEvent('after-huge'));
reporter.flushAll();
expect(readSpanNames(file)).toEqual([
'before-huge',
huge,
'after-huge'
]);
});
// `Buffer.write` stops at the last whole character, so an event that
// overruns the buffer reports a count short of the capacity. Treating
// that as complete stores a JSON string with no closing quote, which
// breaks the whole line.
it('never stores a partially encoded multi-byte event', async ()=>{
const tmpDir = await makeTmpDir();
(0, _shared.setGlobal)('distDir', tmpDir);
(0, _shared.setGlobal)('phase', _constants.PHASE_PRODUCTION_BUILD);
// Sweep name lengths so events land at every offset relative to the
// buffer boundary, mixing 1-, 3- and 4-byte characters.
for(let length = 1; length < 120; length++){
const reporter = makeJsonReporter({
filename: `trace-${length}`,
sizeLimit: Infinity
});
const names = [];
for(let i = 0; i < 400; i++){
const name = `${'第'.repeat(length)}😀${'a'.repeat(i % 7)}`;
names.push(name);
reporter.report(traceEvent(name));
}
reporter.flushAll();
expect(readSpanNames((0, _path.join)(tmpDir, `trace-${length}`))).toEqual(names);
}
});
// Tracing is diagnostic: a full disk should cost spans, not the build.
it('keeps tracing when a write fails', async ()=>{
const tmpDir = await makeTmpDir();
(0, _shared.setGlobal)('distDir', tmpDir);
(0, _shared.setGlobal)('phase', _constants.PHASE_PRODUCTION_BUILD);
const reporter = makeJsonReporter({
filename: 'trace',
sizeLimit: Infinity
});
const file = (0, _path.join)(tmpDir, 'trace');
reporter.report(traceEvent('before-failure'));
reporter.flushAll();
const writeSync = jest.spyOn(_fs.default, 'writeSync').mockImplementation(()=>{
throw Object.assign(new Error('ENOSPC'), {
code: 'ENOSPC'
});
});
const consoleLog = jest.spyOn(console, 'log').mockImplementation(()=>{});
try {
reporter.report(traceEvent('during-failure'));
expect(()=>reporter.flushAll()).not.toThrow();
expect(consoleLog).toHaveBeenCalled();
} finally{
consoleLog.mockRestore();
writeSync.mockRestore();
}
reporter.report(traceEvent('after-failure'));
reporter.flushAll();
expect(readSpanNames(file)).toEqual([
'before-failure',
'after-failure'
]);
});
});
describe('Worker', ()=>{
it('exports and initializes trace state', ()=>{
const root = (0, _trace.trace)('root-span');
expect(root.getId()).toEqual(1);
const traceState = (0, _trace.exportTraceState)();
expect(traceState.lastId).toEqual(1);
(0, _trace.initializeTraceState)({
lastId: 101
});
const span = (0, _trace.trace)('another-span');
expect(span.getId()).toEqual(102);
});
it('trace data is serializable to a worker', async ()=>{
const root = (0, _trace.trace)('root-span');
root.traceChild('child-span').traceFn(()=>null);
root.stop();
const traceEvents = (0, _trace.getTraceEvents)();
expect(traceEvents.length).toEqual(2);
// This is a proxy check to make sure the object would be serializable
// to a worker. It will fail if the data contains some unserializable
// objects like BigInt.
const clone = JSON.parse(JSON.stringify(traceEvents));
expect(clone).toEqual(traceEvents);
});
it('correctly reports trace data from multiple workers', ()=>{
// This test simulates workers creating traces and propagating them
// back to the main process for recording. It doesn't use
// actual workers since they are more difficult to set up in tests.
(0, _trace.initializeTraceState)({
lastId: 5,
defaultParentSpanId: 1,
shouldSaveTraceEvents: true
});
const worker1Span = (0, _trace.trace)('worker1');
worker1Span.traceChild('webpack-compilation1').traceFn(()=>null);
worker1Span.stop();
const worker1Traces = (0, _trace.getTraceEvents)();
expect(worker1Traces.length).toEqual(2);
// Repeat for a second worker.
(0, _trace.clearTraceEvents)();
(0, _trace.initializeTraceState)({
lastId: 10,
defaultParentSpanId: 1,
shouldSaveTraceEvents: true
});
const worker2Span = (0, _trace.trace)('worker2');
worker2Span.traceChild('webpack-compilation2').traceFn(()=>null);
worker2Span.stop();
const worker2Traces = (0, _trace.getTraceEvents)();
expect(worker2Traces.length).toEqual(2);
// Now simulate the traces in the main process and record the traces
// from each worker.
(0, _trace.clearTraceEvents)();
(0, _trace.initializeTraceState)({
lastId: 0,
shouldSaveTraceEvents: true
});
const root = (0, _trace.trace)('next-build');
root.traceChild('some-child-span').traceFn(()=>null);
(0, _trace.recordTraceEvents)(worker1Traces);
expect((0, _trace.exportTraceState)().lastId).toEqual(8);
(0, _trace.recordTraceEvents)(worker2Traces);
expect((0, _trace.exportTraceState)().lastId).toEqual(13);
root.traceChild('another-child-span').traceFn(()=>null);
root.stop();
// Check that the final output looks correct.
const allTraces = (0, _trace.getTraceEvents)();
expect(allTraces.length).toEqual(7);
const firstSpan = allTraces[0];
expect(firstSpan.name).toEqual('some-child-span');
expect(firstSpan.id).toEqual(2);
expect(firstSpan.parentId).toEqual(1);
const worker1Child = allTraces[1];
expect(worker1Child.name).toEqual('webpack-compilation1');
expect(worker1Child.id).toEqual(7);
expect(worker1Child.parentId).toEqual(6);
const worker1Root = allTraces[2];
expect(worker1Root.name).toEqual('worker1');
expect(worker1Root.id).toEqual(6);
expect(worker1Root.parentId).toEqual(1);
const worker2Child = allTraces[3];
expect(worker2Child.name).toEqual('webpack-compilation2');
expect(worker2Child.id).toEqual(12);
expect(worker2Child.parentId).toEqual(11);
const worker2Root = allTraces[4];
expect(worker2Root.name).toEqual('worker2');
expect(worker2Root.id).toEqual(11);
expect(worker2Root.parentId).toEqual(1);
const lastChildSpan = allTraces[5];
expect(lastChildSpan.name).toEqual('another-child-span');
expect(lastChildSpan.id).toEqual(14);
expect(lastChildSpan.parentId).toEqual(1);
const rootSpan = allTraces[6];
expect(rootSpan.name).toEqual('next-build');
expect(rootSpan.id).toEqual(1);
expect(rootSpan.parentId).toBeUndefined();
});
});
});
//# sourceMappingURL=trace.test.js.map