Skip to content

Commit

Permalink
refactor: pairing session WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
niieani committed Nov 22, 2024
1 parent fce6fed commit 7819d6c
Show file tree
Hide file tree
Showing 10 changed files with 31,958 additions and 156 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"source": "src/main.ts",
"types": "src/index.ts",
"scripts": {
"dev": "rm -rf esm && yarn webpack build --mode development --entry ./src/main --watch && echo \"export * from '../src/main'\" > ./esm/main.d.ts",
"dev": "rm -rf esm/* && echo \"export * from '../src/main'\" > ./esm/main.d.ts && yarn webpack build --mode development --entry ./src/main --watch",
"build": "yarn build:cjs && yarn build:esm && yarn copy:css",
"build:cjs": "yarn rrun tsc --outDir cjs --module commonjs --target es2021",
"build:esm": "rm -rf esm && yarn build:esm:ts && yarn build:esm:webpack && cp package.esm.json esm/package.json",
Expand Down
125 changes: 84 additions & 41 deletions src/v3/ActiveTrace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export class TraceStateMachine<ScopeT extends ScopeBase> {
* while debouncing, we need to buffer any spans that come in so they can be re-processed
* once we transition to the 'waiting-for-interactive' state
* otherwise we might miss out on spans that are relevant to calculating the interactive
*
* if we have long tasks before FMP, we want to use them as a potential grouping post FMP.
*/
debouncingSpanBuffer: SpanAndAnnotation<ScopeT>[] = []

Expand Down Expand Up @@ -256,18 +258,30 @@ export class TraceStateMachine<ScopeT extends ScopeBase> {
return { transitionToState: 'waiting-for-interactive' }
}

for (const matcher of this.context.definition.requiredToEnd) {
const { span } = spanAndAnnotation
if (
matcher(spanAndAnnotation, this.context.input.scope) &&
matcher.isIdle &&
'isIdle' in span &&
span.isIdle
) {
// check if we regressed on "isIdle", and if so, transition to interrupted with reason
return {
transitionToState: 'interrupted',
interruptionReason: 'idle-component-no-longer-idle',
const { span } = spanAndAnnotation

// even though we satisfied all the requiredToEnd conditions in the recording state,
// if we see a previously required render span that was requested to be idle, but is no longer idle,
// our trace is deemed invalid and should be interrupted
const isSpanNonIdleRender = 'isIdle' in span && !span.isIdle
// we want to match on all the conditions except for the "isIdle: true"
// for this reason we have to pretend to the matcher about "isIdle" or else our matcher condition would never evaluate to true
const idleRegressionCheckSpan = isSpanNonIdleRender && {
...spanAndAnnotation,
span: { ...span, isIdle: true },
}
if (idleRegressionCheckSpan) {
for (const matcher of this.context.definition.requiredToEnd) {
if (
// TODO: rename matcher in the whole file to 'doesSpanMatch'
matcher(idleRegressionCheckSpan, this.context.input.scope) &&
matcher.isIdle
) {
// check if we regressed on "isIdle", and if so, transition to interrupted with reason
return {
transitionToState: 'interrupted',
interruptionReason: 'idle-component-no-longer-idle',
}
}
}
}
Expand Down Expand Up @@ -325,13 +339,29 @@ export class TraceStateMachine<ScopeT extends ScopeBase> {
}
}

this.interactiveDeadline =
this.lastRequiredSpan.span.startTime.epoch +
this.lastRequiredSpan.span.duration +
((typeof interactiveConfig === 'object' &&
const interruptMillisecondsAfterLastRequiredSpan =
(typeof interactiveConfig === 'object' &&
interactiveConfig.timeout) ||
DEFAULT_INTERACTIVE_TIMEOUT_DURATION)
DEFAULT_INTERACTIVE_TIMEOUT_DURATION

const lastRequiredSpanEndTimeEpoch =
this.lastRequiredSpan.span.startTime.epoch +
this.lastRequiredSpan.span.duration
this.interactiveDeadline =
lastRequiredSpanEndTimeEpoch +
interruptMillisecondsAfterLastRequiredSpan

console.log(
'setting up interactive deadline',
// this.lastRequiredSpan,
{
lastRequiredSpanEndTime: lastRequiredSpanEndTimeEpoch,
interruptMillisecondsAfterLastRequiredSpan,
startTime: this.lastRequiredSpan.span.startTime.epoch,
duration: this.lastRequiredSpan.span.duration,
deadline: this.interactiveDeadline,
},
)
this.cpuIdleLongTaskProcessor = createCPUIdleProcessor<
EntryType<ScopeT>
>(
Expand Down Expand Up @@ -383,24 +413,19 @@ export class TraceStateMachine<ScopeT extends ScopeBase> {
}
}

if (spanEndTimeEpoch > this.interactiveDeadline) {
// we consider this complete, because we have a complete trace
// it's just missing the bonus data from when the browser became "interactive"
return {
transitionToState: 'complete',
interruptionReason: 'waiting-for-interactive-timeout',
lastRequiredSpanAndAnnotation: this.lastRequiredSpan,
}
}

const cpuIdleMatch = this.cpuIdleLongTaskProcessor?.({
entryType: spanAndAnnotation.span.type,
startTime: spanAndAnnotation.span.startTime.now,
duration: spanAndAnnotation.span.duration,
entry: spanAndAnnotation,
})

if (cpuIdleMatch !== undefined) {
if (
cpuIdleMatch !== undefined &&
cpuIdleMatch.entry.span.startTime.epoch +
cpuIdleMatch.entry.span.duration <=
this.interactiveDeadline
) {
// if we match the interactive criteria, transition to complete
// reference https://docs.google.com/document/d/1GGiI9-7KeY3TPqS3YT271upUVimo-XiL5mwWorDUD4c/edit
return {
Expand All @@ -410,6 +435,23 @@ export class TraceStateMachine<ScopeT extends ScopeBase> {
}
}

if (spanEndTimeEpoch > this.interactiveDeadline) {
console.log(
spanEndTimeEpoch,
'deadline',
this.interactiveDeadline,
spanEndTimeEpoch - this.interactiveDeadline,
spanAndAnnotation,
)
// we consider this complete, because we have a complete trace
// it's just missing the bonus data from when the browser became "interactive"
return {
transitionToState: 'complete',
interruptionReason: 'waiting-for-interactive-timeout',
lastRequiredSpanAndAnnotation: this.lastRequiredSpan,
}
}

// if the entry matches any of the interruptOn criteria,
// transition to complete state with the 'matched-on-interrupt' interruptionReason
if (this.context.definition.interruptOn) {
Expand Down Expand Up @@ -661,24 +703,25 @@ export class ActiveTrace<ScopeT extends ScopeBase> {
transition.transitionToState === 'interrupted' ||
transition.transitionToState === 'complete'
) {
const endOfOperationSpan =
(transition.transitionToState === 'complete' &&
(transition.cpuIdleSpanAndAnnotation ??
transition.lastRequiredSpanAndAnnotation)) ||
lastRelevantSpanAndAnnotation
// const endOfOperationSpan =
// (transition.transitionToState === 'complete' &&
// (transition.cpuIdleSpanAndAnnotation ??
// transition.lastRequiredSpanAndAnnotation)) ||
// lastRelevantSpanAndAnnotation

const traceRecording = createTraceRecording(
{
definition: this.definition,
// only keep items captured until the endOfOperationSpan
recordedItems: endOfOperationSpan
? this.recordedItems.filter(
(item) =>
item.span.startTime.now + item.span.duration <=
endOfOperationSpan.span.startTime.now +
endOfOperationSpan.span.duration,
)
: this.recordedItems,
// recordedItems: endOfOperationSpan
// ? this.recordedItems.filter(
// (item) =>
// item.span.startTime.now + item.span.duration <=
// endOfOperationSpan.span.startTime.now +
// endOfOperationSpan.span.duration,
// )
// : this.recordedItems,
recordedItems: this.recordedItems,
input: this.input,
},
transition,
Expand Down
23 changes: 5 additions & 18 deletions src/v3/ensureTimestamp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,6 @@ export const ensureTimestamp = (input?: Partial<Timestamp>): Timestamp => {
const inputNow = input?.now
const hasInputEpoch = typeof inputEpoch === 'number'
const hasInputNow = typeof inputNow === 'number'
const hasData = hasInputEpoch || hasInputNow
if (!hasData) {
// no data provided, use current time
return {
now: performance.now(),
epoch: Date.now(),
}
}
if (hasInputEpoch && hasInputNow) {
return input as Timestamp
}
Expand All @@ -42,22 +34,17 @@ export const ensureTimestamp = (input?: Partial<Timestamp>): Timestamp => {
}
}
if (hasInputNow) {
const differenceFromNow = performance.now() - inputNow
const epoch = Date.now() + differenceFromNow
const elapsedTimeSinceInput = performance.now() - inputNow
const epoch = Date.now() - elapsedTimeSinceInput
return {
epoch,
now: inputNow,
}
}
// no data provided, use current time
return {
epoch:
input?.epoch ??
(input?.now ? performance.timeOrigin + input.now : Date.now()),
now:
input?.now ??
(input?.epoch
? performance.now() - (performance.timeOrigin - input.epoch)
: performance.now()),
now: performance.now(),
epoch: Date.now(),
}
}

Expand Down
21 changes: 16 additions & 5 deletions src/v3/firstCPUIdle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export function createCPUIdleProcessor<T extends number | PerformanceEntryLike>(

const returnType = typeof fmpOrEntry === 'number' ? 'number' : 'object'

// TODO: if a longtask straddles the FMP, then we should push the first CPU idle timestamp to the end of it
// TODO: potentially assume that FMP point is as if inside of a heavy cluster already
return function processPerformanceEntry(
entry: PerformanceEntryLike,
): T | undefined {
Expand Down Expand Up @@ -74,9 +76,11 @@ export function createCPUIdleProcessor<T extends number | PerformanceEntryLike>(
longTaskClusterDurationTotal =
entry.duration - Math.abs(entry.startTime - fmp)

// Move to the end of the cluster:
possibleFirstCPUIdleTimestamp = endTimeStampOfLastLongTask
possibleFirstCPUIdleEntry = entry
if (endTimeStampOfLastLongTask > fmp) {
// Move to the end of the cluster:
possibleFirstCPUIdleTimestamp = endTimeStampOfLastLongTask
possibleFirstCPUIdleEntry = entry
}
} else {
longTaskClusterDurationTotal = entry.duration
}
Expand All @@ -87,7 +91,11 @@ export function createCPUIdleProcessor<T extends number | PerformanceEntryLike>(
// Calculate time since the last long task
const gapSincePreviousTask = entry.startTime - endTimeStampOfLastLongTask

if (isEntryLongTask && gapSincePreviousTask < clusterPadding) {
if (
isEntryLongTask &&
gapSincePreviousTask < clusterPadding &&
gapSincePreviousTask > 0
) {
// Continue to expand the existing cluster
// If less than 1 second since the last long task
// Include the time passed since the last long task in the cluster duration
Expand All @@ -96,7 +104,10 @@ export function createCPUIdleProcessor<T extends number | PerformanceEntryLike>(
lastLongTask = entry

// If the cluster duration exceeds 250ms, update the first CPU idle timestamp
if (longTaskClusterDurationTotal >= heavyClusterThreshold) {
if (
longTaskClusterDurationTotal >= heavyClusterThreshold &&
endTimeStampOfLastLongTask > fmp
) {
// Met criteria for Heavy Cluster
// Move to the end of the cluster
possibleFirstCPUIdleTimestamp = endTimeStampOfLastLongTask
Expand Down
90 changes: 45 additions & 45 deletions src/v3/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,54 +30,54 @@ export const generateUseBeacon =
<ScopeT extends ScopeBase>(
traceManager: TraceManager<ScopeT>,
): UseBeacon<ScopeT> =>
(config: BeaconConfig<GetScopeTFromTraceManager<TraceManager<ScopeT>>>) => {
const renderCountRef = useRef(0)
renderCountRef.current += 1
(config: BeaconConfig<GetScopeTFromTraceManager<TraceManager<ScopeT>>>) => {
const renderCountRef = useRef(0)
renderCountRef.current += 1

const { attributes } = config
const { attributes } = config

const status = config.error ? 'error' : 'ok'
const status = config.error ? 'error' : 'ok'

const renderStartTaskEntry: ComponentRenderSpan<ScopeT> = makeEntry({
...config,
type: 'component-render-start',
duration: 0,
attributes,
status,
renderCount: renderCountRef.current,
})
const renderStartTaskEntry: ComponentRenderSpan<ScopeT> = makeEntry({
...config,
type: 'component-render-start',
duration: 0,
attributes,
status,
renderCount: renderCountRef.current,
})

traceManager.processSpan(renderStartTaskEntry)
traceManager.processSpan(renderStartTaskEntry)

// Beacon effect for tracking 'component-render'. This will fire after every render as it does not have any dependencies:
useEffect(() => {
traceManager.processSpan(
makeEntry({
...config,
type: 'component-render',
duration: performance.now() - renderStartTaskEntry.startTime.now,
status,
attributes,
renderCount: renderCountRef.current,
}),
)
})

// Beacon effect for tracking 'component-unmount' entries
useOnComponentUnmount(
(errorBoundaryMetadata) => {
const unmountEntry = makeEntry({
...config,
type: 'component-unmount',
attributes,
error: errorBoundaryMetadata?.error,
errorInfo: errorBoundaryMetadata?.errorInfo,
duration: 0,
status: errorBoundaryMetadata?.error ? 'error' : 'ok',
renderCount: renderCountRef.current,
})
traceManager.processSpan(unmountEntry)
},
[config.name],
// Beacon effect for tracking 'component-render'. This will fire after every render as it does not have any dependencies:
useEffect(() => {
traceManager.processSpan(
makeEntry({
...config,
type: 'component-render',
duration: performance.now() - renderStartTaskEntry.startTime.now,
status,
attributes,
renderCount: renderCountRef.current,
}),
)
}
})

// Beacon effect for tracking 'component-unmount' entries
useOnComponentUnmount(
(errorBoundaryMetadata) => {
const unmountEntry = makeEntry({
...config,
type: 'component-unmount',
attributes,
error: errorBoundaryMetadata?.error,
errorInfo: errorBoundaryMetadata?.errorInfo,
duration: 0,
status: errorBoundaryMetadata?.error ? 'error' : 'ok',
renderCount: renderCountRef.current,
})
traceManager.processSpan(unmountEntry)
},
[config.name],
)
}
Loading

0 comments on commit 7819d6c

Please sign in to comment.