fix(tracing): clear recording state at the end of tracing.stop (#8120)

This ensures that any tracing operations can access the recording state.
When stopping, we await for all operations to finish and then clear the state.
To avoid reentrancy, a new flag `isStopping` is introduced.
This commit is contained in:
Dmitry Gozman 2021-08-10 17:36:06 -07:00 committed by GitHub
parent 4975f4179e
commit 708fa43f03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -55,6 +55,7 @@ export class Tracing implements InstrumentationListener {
private _context: BrowserContext;
private _resourcesDir: string;
private _recording: RecordingState | undefined;
private _isStopping = false;
private _tracesDir: string;
constructor(context: BrowserContext) {
@ -65,6 +66,8 @@ export class Tracing implements InstrumentationListener {
}
async start(options: TracerOptions): Promise<void> {
if (this._isStopping)
throw new Error('Cannot start tracing while stopping');
// context + page must be the first events added, this method can't have awaits before them.
const state = this._recording;
@ -130,14 +133,16 @@ export class Tracing implements InstrumentationListener {
}
async stop(): Promise<void> {
if (!this._recording)
if (!this._recording || this._isStopping)
return;
this._recording = undefined;
this._isStopping = true;
this._context.instrumentation.removeListener(this);
this._stopScreencast();
await this._snapshotter.stop();
// Ensure all writes are finished.
await this._appendEventChain;
this._recording = undefined;
this._isStopping = false;
}
async dispose() {
@ -145,9 +150,6 @@ export class Tracing implements InstrumentationListener {
}
async export(): Promise<Artifact> {
if (!this._recording)
throw new Error('Must start tracing before exporting');
for (const { sdkObject, metadata, beforeSnapshot, actionSnapshot, afterSnapshot } of this._pendingCalls.values()) {
await Promise.all([beforeSnapshot, actionSnapshot, afterSnapshot]);
let callMetadata = metadata;
@ -161,19 +163,19 @@ export class Tracing implements InstrumentationListener {
await this.onAfterCall(sdkObject, callMetadata);
}
if (!this._recording)
throw new Error('Must start tracing before exporting');
// Chain the export operation against write operations,
// so that neither trace file nor sha1s change during the export.
return await this._appendTraceOperation(async () => {
if (!this._recording)
throw new Error('Must start tracing before exporting');
await this._snapshotter.checkpoint();
const resetIndex = this._recording.lastReset;
let state = this._recording;
const recording = this._recording!;
let state = recording;
// Make a filtered trace if needed.
if (resetIndex)
state = await this._filterTrace(this._recording, resetIndex);
if (recording.lastReset)
state = await this._filterTrace(recording, recording.lastReset);
const zipFile = new yazl.ZipFile();
const failedPromise = new Promise<Artifact>((_, reject) => (zipFile as any as EventEmitter).on('error', reject));
@ -192,7 +194,7 @@ export class Tracing implements InstrumentationListener {
});
return Promise.race([failedPromise, succeededPromise]).finally(async () => {
// Remove the filtered trace.
if (resetIndex)
if (recording.lastReset)
await fs.promises.unlink(state.traceFile).catch(() => {});
});
});
@ -313,10 +315,8 @@ export class Tracing implements InstrumentationListener {
private _appendTraceEvent(event: any) {
// Serialize all writes to the trace file.
this._appendTraceOperation(async () => {
if (!this._recording)
return;
visitSha1s(event, this._recording.sha1s);
await fs.promises.appendFile(this._recording.traceFile, JSON.stringify(event) + '\n');
visitSha1s(event, this._recording!.sha1s);
await fs.promises.appendFile(this._recording!.traceFile, JSON.stringify(event) + '\n');
});
}