Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
145 lines
5.5 KiB
Plaintext
145 lines
5.5 KiB
Plaintext
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
|
const core = require('@sentry/core');
|
|
const debugBuild = require('../debug-build.js');
|
|
const helpers = require('../helpers.js');
|
|
const utils = require('./utils.js');
|
|
|
|
/**
|
|
* Wraps startTransaction and stopTransaction with profiling related logic.
|
|
* startProfileForTransaction is called after the call to startTransaction in order to avoid our own code from
|
|
* being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
|
|
*/
|
|
function startProfileForSpan(span) {
|
|
// Start the profiler and get the profiler instance.
|
|
let startTimestamp;
|
|
if (utils.isAutomatedPageLoadSpan(span)) {
|
|
startTimestamp = core.timestampInSeconds() * 1000;
|
|
}
|
|
|
|
const profiler = utils.startJSSelfProfile();
|
|
|
|
// We failed to construct the profiler, so we skip.
|
|
// No need to log anything as this has already been logged in startProfile.
|
|
if (!profiler) {
|
|
return;
|
|
}
|
|
|
|
if (debugBuild.DEBUG_BUILD) {
|
|
core.debug.log(`[Profiling] started profiling span: ${core.spanToJSON(span).description}`);
|
|
}
|
|
|
|
// We create "unique" span names to avoid concurrent spans with same names
|
|
// from being ignored by the profiler. From here on, only this span name should be used when
|
|
// calling the profiler methods. Note: we log the original name to the user to avoid confusion.
|
|
const profileId = core.uuid4();
|
|
|
|
// A couple of important things to note here:
|
|
// `CpuProfilerBindings.stopProfiling` will be scheduled to run in 30seconds in order to exceed max profile duration.
|
|
// Whichever of the two (span.finish/timeout) is first to run, the profiling will be stopped and the gathered profile
|
|
// will be processed when the original span is finished. Since onProfileHandler can be invoked multiple times in the
|
|
// event of an error or user mistake (calling span.finish multiple times), it is important that the behavior of onProfileHandler
|
|
// is idempotent as we do not want any timings or profiles to be overridden by the last call to onProfileHandler.
|
|
// After the original finish method is called, the event will be reported through the integration and delegated to transport.
|
|
let processedProfile = null;
|
|
|
|
core.getCurrentScope().setContext('profile', {
|
|
profile_id: profileId,
|
|
start_timestamp: startTimestamp,
|
|
});
|
|
|
|
/**
|
|
* Idempotent handler for profile stop
|
|
*/
|
|
async function onProfileHandler() {
|
|
// Check if the profile exists and return it the behavior has to be idempotent as users may call span.finish multiple times.
|
|
if (!span) {
|
|
return;
|
|
}
|
|
// Satisfy the type checker, but profiler will always be defined here.
|
|
if (!profiler) {
|
|
return;
|
|
}
|
|
if (processedProfile) {
|
|
if (debugBuild.DEBUG_BUILD) {
|
|
core.debug.log('[Profiling] profile for:', core.spanToJSON(span).description, 'already exists, returning early');
|
|
}
|
|
return;
|
|
}
|
|
|
|
return profiler
|
|
.stop()
|
|
.then((profile) => {
|
|
if (maxDurationTimeoutID) {
|
|
helpers.WINDOW.clearTimeout(maxDurationTimeoutID);
|
|
maxDurationTimeoutID = undefined;
|
|
}
|
|
|
|
if (debugBuild.DEBUG_BUILD) {
|
|
core.debug.log(`[Profiling] stopped profiling of span: ${core.spanToJSON(span).description}`);
|
|
}
|
|
|
|
// In case of an overlapping span, stopProfiling may return null and silently ignore the overlapping profile.
|
|
if (!profile) {
|
|
if (debugBuild.DEBUG_BUILD) {
|
|
core.debug.log(
|
|
`[Profiling] profiler returned null profile for: ${core.spanToJSON(span).description}`,
|
|
'this may indicate an overlapping span or a call to stopProfiling with a profile title that was never started',
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
processedProfile = profile;
|
|
utils.addProfileToGlobalCache(profileId, profile);
|
|
})
|
|
.catch(error => {
|
|
if (debugBuild.DEBUG_BUILD) {
|
|
core.debug.log('[Profiling] error while stopping profiler:', error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Enqueue a timeout to prevent profiles from running over max duration.
|
|
let maxDurationTimeoutID = helpers.WINDOW.setTimeout(() => {
|
|
if (debugBuild.DEBUG_BUILD) {
|
|
core.debug.log('[Profiling] max profile duration elapsed, stopping profiling for:', core.spanToJSON(span).description);
|
|
}
|
|
// If the timeout exceeds, we want to stop profiling, but not finish the span
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
onProfileHandler();
|
|
}, utils.MAX_PROFILE_DURATION_MS);
|
|
|
|
// We need to reference the original end call to avoid creating an infinite loop
|
|
const originalEnd = span.end.bind(span);
|
|
|
|
/**
|
|
* Wraps span `end()` with profiling related logic.
|
|
* startProfiling is called after the call to spanStart in order to avoid our own code from
|
|
* being profiled. Because of that same reason, stopProfiling is called before the call to spanEnd.
|
|
*/
|
|
function profilingWrappedSpanEnd() {
|
|
if (!span) {
|
|
return originalEnd();
|
|
}
|
|
// onProfileHandler should always return the same profile even if this is called multiple times.
|
|
// Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
|
|
void onProfileHandler().then(
|
|
() => {
|
|
originalEnd();
|
|
},
|
|
() => {
|
|
// If onProfileHandler fails, we still want to call the original finish method.
|
|
originalEnd();
|
|
},
|
|
);
|
|
|
|
return span;
|
|
}
|
|
|
|
span.end = profilingWrappedSpanEnd;
|
|
}
|
|
|
|
exports.startProfileForSpan = startProfileForSpan;
|
|
//# sourceMappingURL=startProfileForSpan.js.map
|