From f78b2b05d10768f2cbd34a84e38a0d76caea80a4 Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Wed, 11 Sep 2024 14:54:52 -0700 Subject: [PATCH 01/18] Support loading additional supplemental recordings --- packages/protocol/socket.ts | 1 - packages/shared/client/ReplayClient.ts | 83 ++++++++++++++++++-------- packages/shared/client/types.ts | 10 +++- src/ui/actions/session.ts | 23 ++++++- 4 files changed, 87 insertions(+), 30 deletions(-) diff --git a/packages/protocol/socket.ts b/packages/protocol/socket.ts index 540701f387d..6c9e83950e0 100644 --- a/packages/protocol/socket.ts +++ b/packages/protocol/socket.ts @@ -128,7 +128,6 @@ let gSessionCallbacks: SessionCallbacks | undefined; export function setSessionCallbacks(sessionCallbacks: SessionCallbacks) { if (gSessionCallbacks !== undefined) { - console.error("Session callbacks can only be set once"); return; } diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index cf6b41aa185..b78d9fb42be 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -78,7 +78,7 @@ import uniqueId from "lodash/uniqueId"; // eslint-disable-next-line no-restricted-imports import { addEventListener, client, initSocket, removeEventListener } from "protocol/socket"; -import { assert, compareNumericStrings, defer, waitForTime } from "protocol/utils"; +import { assert, compareNumericStrings, defer, Deferred, waitForTime } from "protocol/utils"; import { initProtocolMessagesStore } from "replay-next/components/protocol/ProtocolMessagesStore"; import { insert } from "replay-next/src/utils/array"; import { TOO_MANY_POINTS_TO_FIND } from "shared/constants"; @@ -90,6 +90,7 @@ import { ReplayClientEvents, ReplayClientInterface, SourceLocationRange, + SupplementalSession, TimeStampedPointWithPaintHash, } from "./types"; @@ -111,6 +112,8 @@ export class ReplayClient implements ReplayClientInterface { private sessionWaiter = defer(); + private supplemental: SupplementalSession[] = []; + private focusWindow: TimeStampedPointRange | null = null; private nextFindPointsId = 1; @@ -131,11 +134,11 @@ export class ReplayClient implements ReplayClientInterface { // Configures the client to use an already initialized session iD. // This method should be used for apps that use the protocol package directly. // Apps that only communicate with the Replay protocol through this client should use the initialize method instead. - async configure(sessionId: string): Promise { + async configure(sessionId: string, supplemental: SupplementalSession[]): Promise { this._sessionId = sessionId; this._dispatchEvent("sessionCreated"); this.sessionWaiter.resolve(sessionId); - + this.supplemental.push(...supplemental); await this.syncFocusWindow(); } @@ -143,6 +146,30 @@ export class ReplayClient implements ReplayClientInterface { return this.sessionWaiter.promise; } + private async forEachSession(callback: (sessionId: string, supplementalIndex: number) => Promise) { + const sessionId = await this.waitForSession(); + await callback(sessionId, 0); + for (let i = 0; i < this.supplemental.length; i++) { + await callback(this.supplemental[i].sessionId, i + 1); + } + } + + private transformSupplementalId(id: string, supplementalIndex: number) { + return `s${supplementalIndex}-${id}`; + } + + private async breakdownSupplementalId(id: string): Promise<{ id: string, sessionId: string }> { + const match = /^s(\d+)-(.*)/.exec(id); + if (!match) { + const sessionId = await this.waitForSession(); + return { id, sessionId }; + } + const supplementalIndex = +match[1]; + const supplementalInfo = this.supplemental[supplementalIndex - 1]; + assert(supplementalInfo); + return { id: match[2], sessionId: supplementalInfo.sessionId }; + } + get loadedRegions(): LoadedRegions | null { return this._loadedRegions; } @@ -508,24 +535,26 @@ export class ReplayClient implements ReplayClientInterface { async findSources(): Promise { const sources: Source[] = []; - await this.waitForSession(); - - const sessionId = await this.waitForSession(); - - const newSourceListener = (source: Source) => { - sources.push(source); - }; - const newSourcesListener = ({ sources: sourcesList }: newSources) => { - for (const source of sourcesList) { + await this.forEachSession(async (sessionId, supplementalIndex) => { + const newSourceListener = (source: Source) => { sources.push(source); - } - }; + }; + const newSourcesListener = ({ sources: sourcesList }: newSources) => { + for (const source of sourcesList) { + if (supplementalIndex) { + source.sourceId = this.transformSupplementalId(source.sourceId, supplementalIndex); + source.generatedSourceIds = source.generatedSourceIds?.map(id => this.transformSupplementalId(id, supplementalIndex)); + } + sources.push(source); + } + }; - client.Debugger.addNewSourceListener(newSourceListener); - client.Debugger.addNewSourcesListener(newSourcesListener); - await client.Debugger.findSources({}, sessionId); - client.Debugger.removeNewSourceListener(newSourceListener); - client.Debugger.removeNewSourcesListener(newSourcesListener); + client.Debugger.addNewSourceListener(newSourceListener); + client.Debugger.addNewSourcesListener(newSourcesListener); + await client.Debugger.findSources({}, sessionId); + client.Debugger.removeNewSourceListener(newSourceListener); + client.Debugger.removeNewSourcesListener(newSourcesListener); + }); return sources; } @@ -796,14 +825,15 @@ export class ReplayClient implements ReplayClientInterface { getSessionId = (): SessionId | null => this._sessionId; async getSourceHitCounts( - sourceId: SourceId, + transformedSourceId: SourceId, locations: SameLineSourceLocations[], focusRange: PointRange | null ) { - const sessionId = await this.waitForSession(); + const { id: sourceId, sessionId } = await this.breakdownSupplementalId(transformedSourceId); + await this.waitForRangeToBeInFocusRange(focusRange); const { hits } = await client.Debugger.getHitCounts( - { sourceId, locations, maxHits: TOO_MANY_POINTS_TO_FIND, range: focusRange || undefined }, + { sourceId, locations, maxHits: TOO_MANY_POINTS_TO_FIND, range: /*focusRange ||*/ undefined }, sessionId ); return hits; @@ -815,10 +845,11 @@ export class ReplayClient implements ReplayClientInterface { } async getBreakpointPositions( - sourceId: SourceId, + transformedSourceId: SourceId, locationRange: SourceLocationRange | null ): Promise { - const sessionId = await this.waitForSession(); + const { id: sourceId, sessionId } = await this.breakdownSupplementalId(transformedSourceId); + const begin = locationRange ? locationRange.start : undefined; const end = locationRange ? locationRange.end : undefined; @@ -1063,11 +1094,11 @@ export class ReplayClient implements ReplayClientInterface { } async streamSourceContents( - sourceId: SourceId, + transformedSourceId: SourceId, onSourceContentsInfo: (params: sourceContentsInfo) => void, onSourceContentsChunk: (params: sourceContentsChunk) => void ): Promise { - const sessionId = await this.waitForSession(); + const { id: sourceId, sessionId } = await this.breakdownSupplementalId(transformedSourceId); let pendingChunk = ""; let pendingThrottlePromise: Promise | null = null; diff --git a/packages/shared/client/types.ts b/packages/shared/client/types.ts index c610a26a487..ed730a739e8 100644 --- a/packages/shared/client/types.ts +++ b/packages/shared/client/types.ts @@ -162,10 +162,18 @@ export interface TimeStampedPointWithPaintHash extends TimeStampedPoint { export type AnnotationListener = (annotation: Annotation) => void; +export interface SupplementalRecording { + recordingId: string; +} + +export interface SupplementalSession extends SupplementalRecording { + sessionId: string; +} + export interface ReplayClientInterface { get loadedRegions(): LoadedRegions | null; addEventListener(type: ReplayClientEvents, handler: Function): void; - configure(sessionId: string): Promise; + configure(sessionId: string, supplemental: SupplementalSession[]): Promise; createPause(executionPoint: ExecutionPoint): Promise; evaluateExpression( pauseId: PauseId, diff --git a/src/ui/actions/session.ts b/src/ui/actions/session.ts index a6bfda80317..1bcbd5b7d3a 100644 --- a/src/ui/actions/session.ts +++ b/src/ui/actions/session.ts @@ -41,6 +41,7 @@ import { subscriptionExpired } from "ui/utils/workspace"; import { setExpectedError, setUnexpectedError } from "./errors"; import { setToolboxLayout, setViewMode } from "./layout"; import { jumpToInitialPausePoint } from "./timeline"; +import { SupplementalRecording } from "shared/client/types"; export { setExpectedError, setUnexpectedError }; @@ -90,6 +91,14 @@ export function getAccessibleRecording( }; } +function getSupplementalRecordings(recordingId: string): SupplementalRecording[] { + switch (recordingId) { + case "d5513383-5986-4de5-ab9d-2a7e1f367e90": + return [{ recordingId: "c54962d6-9ac6-428a-a6af-2bb2bf6633ca" }]; + } + return []; +} + function clearRecordingNotAccessibleError(): UIThunkAction { return async (dispatch, getState) => { const state = getState(); @@ -274,7 +283,7 @@ export function createSocket(recordingId: string): UIThunkAction { }) ); - const sessionId = await createSession( + const doCreateSession = (recordingId: string) => createSession( recordingId, experimentalSettings, focusWindowFromURL !== null ? focusWindowFromURL : undefined, @@ -353,12 +362,22 @@ export function createSocket(recordingId: string): UIThunkAction { } ); + const sessionId = await doCreateSession(recordingId); + console.log("MainSessionId", JSON.stringify({ recordingId, sessionId })); + + const supplementalRecordings = getSupplementalRecordings(recordingId); + const supplemental = await Promise.all(supplementalRecordings.map(async ({ recordingId }) => { + const sessionId = await doCreateSession(recordingId); + console.log("SupplementalSessionId", JSON.stringify({ recordingId, sessionId })); + return { recordingId, sessionId }; + })); + Sentry.configureScope(scope => { scope.setExtra("sessionId", sessionId); }); window.sessionId = sessionId; - await replayClient.configure(sessionId); + await replayClient.configure(sessionId, supplemental); const recordingTarget = await recordingTargetCache.readAsync(replayClient); dispatch(actions.setRecordingTarget(recordingTarget)); From c4bd544b3cabf109aec098da1a532e27b52535eb Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Thu, 12 Sep 2024 13:26:06 -0700 Subject: [PATCH 02/18] wip --- packages/protocol/RecordedEventsCache.ts | 4 +- packages/protocol/execution-point-utils.ts | 6 +- packages/protocol/utils.ts | 19 ++++- .../src/suspense/HitPointsCache.ts | 4 +- packages/replay-next/src/utils/string.ts | 4 - packages/shared/client/ReplayClient.ts | 79 +++++++++++++------ src/ui/components/NetworkMonitor/utils.ts | 4 +- .../TestSuite/suspense/AnnotationsCache.ts | 6 +- src/ui/suspense/annotationsCaches.ts | 8 +- 9 files changed, 89 insertions(+), 45 deletions(-) diff --git a/packages/protocol/RecordedEventsCache.ts b/packages/protocol/RecordedEventsCache.ts index 6ea22b83ad6..f54f813e386 100644 --- a/packages/protocol/RecordedEventsCache.ts +++ b/packages/protocol/RecordedEventsCache.ts @@ -5,7 +5,7 @@ import { } from "@replayio/protocol"; import { createSingleEntryCache } from "suspense"; -import { compareNumericStrings } from "protocol/utils"; +import { compareExecutionPoints } from "protocol/utils"; import { findIndexLTE } from "replay-next/src/utils/array"; import { replayClient } from "shared/client/ReplayClientContext"; @@ -39,7 +39,7 @@ export const RecordedEventsCache = createSingleEntryCache<[], RecordedEvent[]>({ const events = [...keyboardEvents, ...mouseEvents, ...navigationEvents]; - return events.sort((a, b) => compareNumericStrings(a.point, b.point)); + return events.sort((a, b) => compareExecutionPoints(a.point, b.point)); }, }); diff --git a/packages/protocol/execution-point-utils.ts b/packages/protocol/execution-point-utils.ts index bfb15f34b13..9db443ac2db 100644 --- a/packages/protocol/execution-point-utils.ts +++ b/packages/protocol/execution-point-utils.ts @@ -1,15 +1,15 @@ import { ExecutionPoint } from "@replayio/protocol"; -import { compareNumericStrings } from "protocol/utils"; +import { compareExecutionPoints } from "protocol/utils"; export function pointEquals(p1: ExecutionPoint, p2: ExecutionPoint) { p1 == p2; } export function pointPrecedes(p1: ExecutionPoint, p2: ExecutionPoint) { - return compareNumericStrings(p1, p2) < 0; + return compareExecutionPoints(p1, p2) < 0; } export function comparePoints(p1: ExecutionPoint, p2: ExecutionPoint) { - return compareNumericStrings(p1, p2); + return compareExecutionPoints(p1, p2); } diff --git a/packages/protocol/utils.ts b/packages/protocol/utils.ts index 5b86d5ab4ab..46fa0531495 100644 --- a/packages/protocol/utils.ts +++ b/packages/protocol/utils.ts @@ -140,13 +140,30 @@ export class ArrayMap { } } +export function transformSupplementalId(id: string, supplementalIndex: number) { + return `s${supplementalIndex}-${id}`; +} + +export function breakdownSupplementalId(id: string): { id: string, supplementalIndex: number } { + const match = /^s(\d+)-(.*)/.exec(id); + if (!match) { + return { id, supplementalIndex: 0 }; + } + const supplementalIndex = +match[1]; + assert(supplementalIndex > 0); + return { id: match[2], supplementalIndex }; +} + /** * Compare 2 integers encoded as numeric strings, because we want to avoid using BigInt (for now). * This will only work correctly if both strings encode positive integers (without decimal places), * using the same base (usually 10) and don't use "fancy stuff" like leading "+", "0" or scientific * notation. */ -export function compareNumericStrings(a: string, b: string) { +export function compareExecutionPoints(transformedA: string, transformedB: string) { + const { id: a, supplementalIndex: indexA } = breakdownSupplementalId(transformedA); + const { id: b, supplementalIndex: indexB } = breakdownSupplementalId(transformedB); + assert(indexA == indexB); return a.length < b.length ? -1 : a.length > b.length ? 1 : a < b ? -1 : a > b ? 1 : 0; } diff --git a/packages/replay-next/src/suspense/HitPointsCache.ts b/packages/replay-next/src/suspense/HitPointsCache.ts index 2a588fc0f43..c8ca92ca677 100644 --- a/packages/replay-next/src/suspense/HitPointsCache.ts +++ b/packages/replay-next/src/suspense/HitPointsCache.ts @@ -3,7 +3,7 @@ import { createCache } from "suspense"; import { breakpointPositionsIntervalCache } from "replay-next/src/suspense/BreakpointPositionsCache"; import { bucketBreakpointLines } from "replay-next/src/utils/source"; -import { compareNumericStrings } from "replay-next/src/utils/string"; +import { compareExecutionPoints } from "protocol/utils"; import { MAX_POINTS_TO_RUN_EVALUATION } from "shared/client/ReplayClient"; import { HitPointStatus, @@ -69,7 +69,7 @@ export const hitPointsCache = createFocusIntervalCacheForExecutionPoints< ); } ); - hitPoints.sort((a, b) => compareNumericStrings(a.point, b.point)); + hitPoints.sort((a, b) => compareExecutionPoints(a.point, b.point)); } else { const pointDescriptions = await replayClient.findPoints( { kind: "locations", locations }, diff --git a/packages/replay-next/src/utils/string.ts b/packages/replay-next/src/utils/string.ts index 97b3e4453c5..24046a56d95 100644 --- a/packages/replay-next/src/utils/string.ts +++ b/packages/replay-next/src/utils/string.ts @@ -1,9 +1,5 @@ export const NEW_LINE_REGEX = /\r\n?|\n|\u2028|\u2029/; -export function compareNumericStrings(a: string, b: string): number { - return a.length < b.length ? -1 : a.length > b.length ? 1 : a < b ? -1 : a > b ? 1 : 0; -} - // Convenience function to JSON-stringify values that may contain circular references. export function stringify(value: any, space?: string | number): string { const cache: any[] = []; diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index b78d9fb42be..671c956c87e 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -78,7 +78,7 @@ import uniqueId from "lodash/uniqueId"; // eslint-disable-next-line no-restricted-imports import { addEventListener, client, initSocket, removeEventListener } from "protocol/socket"; -import { assert, compareNumericStrings, defer, Deferred, waitForTime } from "protocol/utils"; +import { assert, compareExecutionPoints, defer, waitForTime, transformSupplementalId, breakdownSupplementalId } from "protocol/utils"; import { initProtocolMessagesStore } from "replay-next/components/protocol/ProtocolMessagesStore"; import { insert } from "replay-next/src/utils/array"; import { TOO_MANY_POINTS_TO_FIND } from "shared/constants"; @@ -146,6 +146,11 @@ export class ReplayClient implements ReplayClientInterface { return this.sessionWaiter.promise; } + isMainSession(sessionId: string) { + assert(this._sessionId); + return sessionId == this._sessionId; + } + private async forEachSession(callback: (sessionId: string, supplementalIndex: number) => Promise) { const sessionId = await this.waitForSession(); await callback(sessionId, 0); @@ -154,20 +159,15 @@ export class ReplayClient implements ReplayClientInterface { } } - private transformSupplementalId(id: string, supplementalIndex: number) { - return `s${supplementalIndex}-${id}`; - } - - private async breakdownSupplementalId(id: string): Promise<{ id: string, sessionId: string }> { - const match = /^s(\d+)-(.*)/.exec(id); - if (!match) { + private async breakdownSupplementalIdAndSession(transformedId: string): Promise<{ id: string, sessionId: string }> { + const { id, supplementalIndex } = breakdownSupplementalId(transformedId); + if (!supplementalIndex) { const sessionId = await this.waitForSession(); return { id, sessionId }; } - const supplementalIndex = +match[1]; const supplementalInfo = this.supplemental[supplementalIndex - 1]; assert(supplementalInfo); - return { id: match[2], sessionId: supplementalInfo.sessionId }; + return { id, sessionId: supplementalInfo.sessionId }; } get loadedRegions(): LoadedRegions | null { @@ -314,7 +314,7 @@ export class ReplayClient implements ReplayClientInterface { let middleIndex = (lowIndex + highIndex) >>> 1; const message = sortedMessages[middleIndex]; - if (compareNumericStrings(message.point.point, newMessagePoint)) { + if (compareExecutionPoints(message.point.point, newMessagePoint)) { lowIndex = middleIndex + 1; } else { highIndex = middleIndex; @@ -357,7 +357,7 @@ export class ReplayClient implements ReplayClientInterface { const sortedMessages = response.messages.sort((messageA: Message, messageB: Message) => { const pointA = messageA.point.point; const pointB = messageB.point.point; - return compareNumericStrings(pointA, pointB); + return compareExecutionPoints(pointA, pointB); }); return { @@ -464,14 +464,45 @@ export class ReplayClient implements ReplayClientInterface { return sortedPaints; } + async breakdownSupplementalLocation(location: Location) { + const { id: sourceId, sessionId } = await this.breakdownSupplementalIdAndSession(location.sourceId); + return { location: { ...location, sourceId }, sessionId }; + } + + async breakdownSupplementalPointSelector(pointSelector: PointSelector) { + switch (pointSelector.kind) { + case "location": { + const { location, sessionId } = await this.breakdownSupplementalLocation(pointSelector.location); + return { pointSelector: { ...pointSelector, location }, sessionId }; + } + case "locations": { + let commonSessionId: string | undefined; + const locations = await Promise.all(pointSelector.locations.map(async transformedLocation => { + const { location, sessionId } = await this.breakdownSupplementalLocation(transformedLocation); + if (commonSessionId) { + assert(commonSessionId == sessionId); + } else { + commonSessionId = sessionId; + } + return location; + })); + assert(commonSessionId); + return { pointSelector: { ...pointSelector, locations }, sessionId: commonSessionId }; + } + default: + return { pointSelector, sessionId: await this.waitForSession() }; + } + } + async findPoints( - pointSelector: PointSelector, + transformedPointSelector: PointSelector, pointLimits?: PointPageLimits ): Promise { + const { pointSelector, sessionId } = await this.breakdownSupplementalPointSelector(transformedPointSelector); + const points: PointDescription[] = []; - const sessionId = await this.waitForSession(); const findPointsId = String(this.nextFindPointsId++); - pointLimits = pointLimits ? { ...pointLimits } : {}; + pointLimits = (pointLimits && this.isMainSession(sessionId)) ? { ...pointLimits } : {}; if (!pointLimits.maxCount) { pointLimits.maxCount = MAX_POINTS_TO_FIND; } @@ -504,7 +535,7 @@ export class ReplayClient implements ReplayClientInterface { throw commandError("Too many points", ProtocolError.TooManyPoints); } - points.sort((a, b) => compareNumericStrings(a.point, b.point)); + points.sort((a, b) => compareExecutionPoints(a.point, b.point)); return points; } @@ -542,8 +573,8 @@ export class ReplayClient implements ReplayClientInterface { const newSourcesListener = ({ sources: sourcesList }: newSources) => { for (const source of sourcesList) { if (supplementalIndex) { - source.sourceId = this.transformSupplementalId(source.sourceId, supplementalIndex); - source.generatedSourceIds = source.generatedSourceIds?.map(id => this.transformSupplementalId(id, supplementalIndex)); + source.sourceId = transformSupplementalId(source.sourceId, supplementalIndex); + source.generatedSourceIds = source.generatedSourceIds?.map(id => transformSupplementalId(id, supplementalIndex)); } sources.push(source); } @@ -829,18 +860,18 @@ export class ReplayClient implements ReplayClientInterface { locations: SameLineSourceLocations[], focusRange: PointRange | null ) { - const { id: sourceId, sessionId } = await this.breakdownSupplementalId(transformedSourceId); + const { id: sourceId, sessionId } = await this.breakdownSupplementalIdAndSession(transformedSourceId); await this.waitForRangeToBeInFocusRange(focusRange); const { hits } = await client.Debugger.getHitCounts( - { sourceId, locations, maxHits: TOO_MANY_POINTS_TO_FIND, range: /*focusRange ||*/ undefined }, + { sourceId, locations, maxHits: TOO_MANY_POINTS_TO_FIND, range: (this.isMainSession(sessionId) && focusRange) || undefined }, sessionId ); return hits; } - async getSourceOutline(sourceId: SourceId) { - const sessionId = await this.waitForSession(); + async getSourceOutline(transformedSourceId: SourceId) { + const { id: sourceId, sessionId } = await this.breakdownSupplementalIdAndSession(transformedSourceId); return client.Debugger.getSourceOutline({ sourceId }, sessionId); } @@ -848,7 +879,7 @@ export class ReplayClient implements ReplayClientInterface { transformedSourceId: SourceId, locationRange: SourceLocationRange | null ): Promise { - const { id: sourceId, sessionId } = await this.breakdownSupplementalId(transformedSourceId); + const { id: sourceId, sessionId } = await this.breakdownSupplementalIdAndSession(transformedSourceId); const begin = locationRange ? locationRange.start : undefined; const end = locationRange ? locationRange.end : undefined; @@ -1098,7 +1129,7 @@ export class ReplayClient implements ReplayClientInterface { onSourceContentsInfo: (params: sourceContentsInfo) => void, onSourceContentsChunk: (params: sourceContentsChunk) => void ): Promise { - const { id: sourceId, sessionId } = await this.breakdownSupplementalId(transformedSourceId); + const { id: sourceId, sessionId } = await this.breakdownSupplementalIdAndSession(transformedSourceId); let pendingChunk = ""; let pendingThrottlePromise: Promise | null = null; diff --git a/src/ui/components/NetworkMonitor/utils.ts b/src/ui/components/NetworkMonitor/utils.ts index f4d956a616e..bab0967a26b 100644 --- a/src/ui/components/NetworkMonitor/utils.ts +++ b/src/ui/components/NetworkMonitor/utils.ts @@ -11,7 +11,7 @@ import { } from "@replayio/protocol"; import keyBy from "lodash/keyBy"; -import { assert, compareNumericStrings } from "protocol/utils"; +import { assert, compareExecutionPoints } from "protocol/utils"; import { NetworkRequestsCacheData } from "replay-next/src/suspense/NetworkRequestsCache"; export enum CanonicalRequestType { @@ -174,7 +174,7 @@ export const partialRequestsToCompleteSummaries = ( }) .filter(row => types.size === 0 || types.has(row.type)); - summaries.sort((a, b) => compareNumericStrings(a.point.point, b.point.point)); + summaries.sort((a, b) => compareExecutionPoints(a.point.point, b.point.point)); return summaries; }; diff --git a/src/ui/components/TestSuite/suspense/AnnotationsCache.ts b/src/ui/components/TestSuite/suspense/AnnotationsCache.ts index 1855ae3a512..f806cd28d0b 100644 --- a/src/ui/components/TestSuite/suspense/AnnotationsCache.ts +++ b/src/ui/components/TestSuite/suspense/AnnotationsCache.ts @@ -1,4 +1,4 @@ -import { compareNumericStrings } from "protocol/utils"; +import { compareExecutionPoints } from "protocol/utils"; import { insert } from "replay-next/src/utils/array"; import { createSingleEntryCacheWithTelemetry } from "replay-next/src/utils/suspense"; import { ReplayClientInterface } from "shared/client/types"; @@ -20,7 +20,7 @@ export const AnnotationsCache = createSingleEntryCacheWithTelemetry< }; insert(sortedAnnotations, processedAnnotation, (a, b) => - compareNumericStrings(a.point, b.point) + compareExecutionPoints(a.point, b.point) ); }); @@ -47,7 +47,7 @@ export const PlaywrightAnnotationsCache = createSingleEntryCacheWithTelemetry< }; insert(sortedAnnotations, processedAnnotation, (a, b) => - compareNumericStrings(a.point, b.point) + compareExecutionPoints(a.point, b.point) ); } }); diff --git a/src/ui/suspense/annotationsCaches.ts b/src/ui/suspense/annotationsCaches.ts index d6aaa7f19b5..622cbdb5ae0 100644 --- a/src/ui/suspense/annotationsCaches.ts +++ b/src/ui/suspense/annotationsCaches.ts @@ -1,7 +1,7 @@ import { Annotation, TimeStampedPoint } from "@replayio/protocol"; import { Cache, createCache, createSingleEntryCache } from "suspense"; -import { compareNumericStrings } from "protocol/utils"; +import { compareExecutionPoints } from "protocol/utils"; import { ReplayClientInterface } from "shared/client/types"; import { InteractionEventKind } from "ui/actions/eventListeners/constants"; import { EventListenerWithFunctionInfo } from "ui/actions/eventListeners/eventListenerUtils"; @@ -58,7 +58,7 @@ export const reactDevToolsAnnotationsCache = createSingleEntryCache< receivedAnnotations.push(annotation); }); - receivedAnnotations.sort((a1, a2) => compareNumericStrings(a1.point, a2.point)); + receivedAnnotations.sort((a1, a2) => compareExecutionPoints(a1.point, a2.point)); const parsedAnnotations: ParsedReactDevToolsAnnotation[] = receivedAnnotations.map( ({ point, time, contents }) => ({ @@ -84,7 +84,7 @@ export const reduxDevToolsAnnotationsCache = createSingleEntryCache< receivedAnnotations.push(annotation); }); - receivedAnnotations.sort((a1, a2) => compareNumericStrings(a1.point, a2.point)); + receivedAnnotations.sort((a1, a2) => compareExecutionPoints(a1.point, a2.point)); const parsedAnnotations = processReduxAnnotations(receivedAnnotations); @@ -104,7 +104,7 @@ export const eventListenersJumpLocationsCache = createSingleEntryCache< receivedAnnotations.push(annotation); }); - receivedAnnotations.sort((a1, a2) => compareNumericStrings(a1.point, a2.point)); + receivedAnnotations.sort((a1, a2) => compareExecutionPoints(a1.point, a2.point)); const parsedAnnotations: ParsedJumpToCodeAnnotation[] = receivedAnnotations.map( ({ point, time, contents }) => ({ From 5a2a1244c67ee39dedfeb67eccbc5fa63a5dfd5c Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Thu, 12 Sep 2024 13:38:10 -0700 Subject: [PATCH 03/18] wip --- packages/shared/client/ReplayClient.ts | 29 +++++++++++++++----------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index 671c956c87e..f95dad9880f 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -159,15 +159,15 @@ export class ReplayClient implements ReplayClientInterface { } } - private async breakdownSupplementalIdAndSession(transformedId: string): Promise<{ id: string, sessionId: string }> { + private async breakdownSupplementalIdAndSession(transformedId: string): Promise<{ id: string, sessionId: string, supplementalIndex: number }> { const { id, supplementalIndex } = breakdownSupplementalId(transformedId); if (!supplementalIndex) { const sessionId = await this.waitForSession(); - return { id, sessionId }; + return { id, sessionId, supplementalIndex }; } const supplementalInfo = this.supplemental[supplementalIndex - 1]; assert(supplementalInfo); - return { id, sessionId: supplementalInfo.sessionId }; + return { id, sessionId: supplementalInfo.sessionId, supplementalIndex }; } get loadedRegions(): LoadedRegions | null { @@ -465,32 +465,34 @@ export class ReplayClient implements ReplayClientInterface { } async breakdownSupplementalLocation(location: Location) { - const { id: sourceId, sessionId } = await this.breakdownSupplementalIdAndSession(location.sourceId); - return { location: { ...location, sourceId }, sessionId }; + const { id: sourceId, sessionId, supplementalIndex } = await this.breakdownSupplementalIdAndSession(location.sourceId); + return { location: { ...location, sourceId }, sessionId, supplementalIndex }; } async breakdownSupplementalPointSelector(pointSelector: PointSelector) { switch (pointSelector.kind) { case "location": { - const { location, sessionId } = await this.breakdownSupplementalLocation(pointSelector.location); - return { pointSelector: { ...pointSelector, location }, sessionId }; + const { location, sessionId, supplementalIndex } = await this.breakdownSupplementalLocation(pointSelector.location); + return { pointSelector: { ...pointSelector, location }, sessionId, supplementalIndex }; } case "locations": { let commonSessionId: string | undefined; + let commonSupplementalIndex = 0; const locations = await Promise.all(pointSelector.locations.map(async transformedLocation => { - const { location, sessionId } = await this.breakdownSupplementalLocation(transformedLocation); + const { location, sessionId, supplementalIndex } = await this.breakdownSupplementalLocation(transformedLocation); if (commonSessionId) { assert(commonSessionId == sessionId); } else { commonSessionId = sessionId; + commonSupplementalIndex = supplementalIndex; } return location; })); assert(commonSessionId); - return { pointSelector: { ...pointSelector, locations }, sessionId: commonSessionId }; + return { pointSelector: { ...pointSelector, locations }, sessionId: commonSessionId, supplementalIndex: commonSupplementalIndex }; } default: - return { pointSelector, sessionId: await this.waitForSession() }; + return { pointSelector, sessionId: await this.waitForSession(), supplementalIndex: 0 }; } } @@ -498,7 +500,7 @@ export class ReplayClient implements ReplayClientInterface { transformedPointSelector: PointSelector, pointLimits?: PointPageLimits ): Promise { - const { pointSelector, sessionId } = await this.breakdownSupplementalPointSelector(transformedPointSelector); + const { pointSelector, sessionId, supplementalIndex } = await this.breakdownSupplementalPointSelector(transformedPointSelector); const points: PointDescription[] = []; const findPointsId = String(this.nextFindPointsId++); @@ -536,7 +538,10 @@ export class ReplayClient implements ReplayClientInterface { } points.sort((a, b) => compareExecutionPoints(a.point, b.point)); - return points; + return points.map(desc => { + const point = transformSupplementalId(desc.point, supplementalIndex); + return { ...desc, point }; + }); } async findStepInTarget(point: ExecutionPoint): Promise { From df8b9eae7a4f3d083a3b53456b5db4fca9113fa8 Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Thu, 12 Sep 2024 13:46:17 -0700 Subject: [PATCH 04/18] wip --- packages/protocol/RecordedEventsCache.ts | 4 ++-- packages/protocol/execution-point-utils.ts | 6 +++--- packages/protocol/utils.ts | 15 +++++++++++---- .../replay-next/src/suspense/HitPointsCache.ts | 4 ++-- packages/replay-next/src/utils/string.ts | 4 ++++ packages/shared/client/ReplayClient.ts | 10 +++++----- src/ui/components/NetworkMonitor/utils.ts | 4 ++-- .../TestSuite/suspense/AnnotationsCache.ts | 6 +++--- src/ui/suspense/annotationsCaches.ts | 8 ++++---- 9 files changed, 36 insertions(+), 25 deletions(-) diff --git a/packages/protocol/RecordedEventsCache.ts b/packages/protocol/RecordedEventsCache.ts index f54f813e386..6ea22b83ad6 100644 --- a/packages/protocol/RecordedEventsCache.ts +++ b/packages/protocol/RecordedEventsCache.ts @@ -5,7 +5,7 @@ import { } from "@replayio/protocol"; import { createSingleEntryCache } from "suspense"; -import { compareExecutionPoints } from "protocol/utils"; +import { compareNumericStrings } from "protocol/utils"; import { findIndexLTE } from "replay-next/src/utils/array"; import { replayClient } from "shared/client/ReplayClientContext"; @@ -39,7 +39,7 @@ export const RecordedEventsCache = createSingleEntryCache<[], RecordedEvent[]>({ const events = [...keyboardEvents, ...mouseEvents, ...navigationEvents]; - return events.sort((a, b) => compareExecutionPoints(a.point, b.point)); + return events.sort((a, b) => compareNumericStrings(a.point, b.point)); }, }); diff --git a/packages/protocol/execution-point-utils.ts b/packages/protocol/execution-point-utils.ts index 9db443ac2db..bfb15f34b13 100644 --- a/packages/protocol/execution-point-utils.ts +++ b/packages/protocol/execution-point-utils.ts @@ -1,15 +1,15 @@ import { ExecutionPoint } from "@replayio/protocol"; -import { compareExecutionPoints } from "protocol/utils"; +import { compareNumericStrings } from "protocol/utils"; export function pointEquals(p1: ExecutionPoint, p2: ExecutionPoint) { p1 == p2; } export function pointPrecedes(p1: ExecutionPoint, p2: ExecutionPoint) { - return compareExecutionPoints(p1, p2) < 0; + return compareNumericStrings(p1, p2) < 0; } export function comparePoints(p1: ExecutionPoint, p2: ExecutionPoint) { - return compareExecutionPoints(p1, p2); + return compareNumericStrings(p1, p2); } diff --git a/packages/protocol/utils.ts b/packages/protocol/utils.ts index 46fa0531495..665b761417e 100644 --- a/packages/protocol/utils.ts +++ b/packages/protocol/utils.ts @@ -154,16 +154,23 @@ export function breakdownSupplementalId(id: string): { id: string, supplementalI return { id: match[2], supplementalIndex }; } +const SupplementalNumericStringShift = 200; + +export function transformSupplementalNumericString(v: string, supplementalIndex: number): string { + assert(BigInt(v) < BigInt(1) << BigInt(SupplementalNumericStringShift)); + if (!supplementalIndex) { + return v; + } + return (BigInt(v) | (BigInt(1) << BigInt(SupplementalNumericStringShift))).toString(); +} + /** * Compare 2 integers encoded as numeric strings, because we want to avoid using BigInt (for now). * This will only work correctly if both strings encode positive integers (without decimal places), * using the same base (usually 10) and don't use "fancy stuff" like leading "+", "0" or scientific * notation. */ -export function compareExecutionPoints(transformedA: string, transformedB: string) { - const { id: a, supplementalIndex: indexA } = breakdownSupplementalId(transformedA); - const { id: b, supplementalIndex: indexB } = breakdownSupplementalId(transformedB); - assert(indexA == indexB); +export function compareNumericStrings(a: string, b: string) { return a.length < b.length ? -1 : a.length > b.length ? 1 : a < b ? -1 : a > b ? 1 : 0; } diff --git a/packages/replay-next/src/suspense/HitPointsCache.ts b/packages/replay-next/src/suspense/HitPointsCache.ts index c8ca92ca677..2a588fc0f43 100644 --- a/packages/replay-next/src/suspense/HitPointsCache.ts +++ b/packages/replay-next/src/suspense/HitPointsCache.ts @@ -3,7 +3,7 @@ import { createCache } from "suspense"; import { breakpointPositionsIntervalCache } from "replay-next/src/suspense/BreakpointPositionsCache"; import { bucketBreakpointLines } from "replay-next/src/utils/source"; -import { compareExecutionPoints } from "protocol/utils"; +import { compareNumericStrings } from "replay-next/src/utils/string"; import { MAX_POINTS_TO_RUN_EVALUATION } from "shared/client/ReplayClient"; import { HitPointStatus, @@ -69,7 +69,7 @@ export const hitPointsCache = createFocusIntervalCacheForExecutionPoints< ); } ); - hitPoints.sort((a, b) => compareExecutionPoints(a.point, b.point)); + hitPoints.sort((a, b) => compareNumericStrings(a.point, b.point)); } else { const pointDescriptions = await replayClient.findPoints( { kind: "locations", locations }, diff --git a/packages/replay-next/src/utils/string.ts b/packages/replay-next/src/utils/string.ts index 24046a56d95..97b3e4453c5 100644 --- a/packages/replay-next/src/utils/string.ts +++ b/packages/replay-next/src/utils/string.ts @@ -1,5 +1,9 @@ export const NEW_LINE_REGEX = /\r\n?|\n|\u2028|\u2029/; +export function compareNumericStrings(a: string, b: string): number { + return a.length < b.length ? -1 : a.length > b.length ? 1 : a < b ? -1 : a > b ? 1 : 0; +} + // Convenience function to JSON-stringify values that may contain circular references. export function stringify(value: any, space?: string | number): string { const cache: any[] = []; diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index f95dad9880f..6a551488df4 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -78,7 +78,7 @@ import uniqueId from "lodash/uniqueId"; // eslint-disable-next-line no-restricted-imports import { addEventListener, client, initSocket, removeEventListener } from "protocol/socket"; -import { assert, compareExecutionPoints, defer, waitForTime, transformSupplementalId, breakdownSupplementalId } from "protocol/utils"; +import { assert, compareNumericStrings, defer, waitForTime, transformSupplementalId, breakdownSupplementalId, transformSupplementalNumericString } from "protocol/utils"; import { initProtocolMessagesStore } from "replay-next/components/protocol/ProtocolMessagesStore"; import { insert } from "replay-next/src/utils/array"; import { TOO_MANY_POINTS_TO_FIND } from "shared/constants"; @@ -314,7 +314,7 @@ export class ReplayClient implements ReplayClientInterface { let middleIndex = (lowIndex + highIndex) >>> 1; const message = sortedMessages[middleIndex]; - if (compareExecutionPoints(message.point.point, newMessagePoint)) { + if (compareNumericStrings(message.point.point, newMessagePoint)) { lowIndex = middleIndex + 1; } else { highIndex = middleIndex; @@ -357,7 +357,7 @@ export class ReplayClient implements ReplayClientInterface { const sortedMessages = response.messages.sort((messageA: Message, messageB: Message) => { const pointA = messageA.point.point; const pointB = messageB.point.point; - return compareExecutionPoints(pointA, pointB); + return compareNumericStrings(pointA, pointB); }); return { @@ -537,9 +537,9 @@ export class ReplayClient implements ReplayClientInterface { throw commandError("Too many points", ProtocolError.TooManyPoints); } - points.sort((a, b) => compareExecutionPoints(a.point, b.point)); + points.sort((a, b) => compareNumericStrings(a.point, b.point)); return points.map(desc => { - const point = transformSupplementalId(desc.point, supplementalIndex); + const point = transformSupplementalNumericString(desc.point, supplementalIndex); return { ...desc, point }; }); } diff --git a/src/ui/components/NetworkMonitor/utils.ts b/src/ui/components/NetworkMonitor/utils.ts index bab0967a26b..f4d956a616e 100644 --- a/src/ui/components/NetworkMonitor/utils.ts +++ b/src/ui/components/NetworkMonitor/utils.ts @@ -11,7 +11,7 @@ import { } from "@replayio/protocol"; import keyBy from "lodash/keyBy"; -import { assert, compareExecutionPoints } from "protocol/utils"; +import { assert, compareNumericStrings } from "protocol/utils"; import { NetworkRequestsCacheData } from "replay-next/src/suspense/NetworkRequestsCache"; export enum CanonicalRequestType { @@ -174,7 +174,7 @@ export const partialRequestsToCompleteSummaries = ( }) .filter(row => types.size === 0 || types.has(row.type)); - summaries.sort((a, b) => compareExecutionPoints(a.point.point, b.point.point)); + summaries.sort((a, b) => compareNumericStrings(a.point.point, b.point.point)); return summaries; }; diff --git a/src/ui/components/TestSuite/suspense/AnnotationsCache.ts b/src/ui/components/TestSuite/suspense/AnnotationsCache.ts index f806cd28d0b..1855ae3a512 100644 --- a/src/ui/components/TestSuite/suspense/AnnotationsCache.ts +++ b/src/ui/components/TestSuite/suspense/AnnotationsCache.ts @@ -1,4 +1,4 @@ -import { compareExecutionPoints } from "protocol/utils"; +import { compareNumericStrings } from "protocol/utils"; import { insert } from "replay-next/src/utils/array"; import { createSingleEntryCacheWithTelemetry } from "replay-next/src/utils/suspense"; import { ReplayClientInterface } from "shared/client/types"; @@ -20,7 +20,7 @@ export const AnnotationsCache = createSingleEntryCacheWithTelemetry< }; insert(sortedAnnotations, processedAnnotation, (a, b) => - compareExecutionPoints(a.point, b.point) + compareNumericStrings(a.point, b.point) ); }); @@ -47,7 +47,7 @@ export const PlaywrightAnnotationsCache = createSingleEntryCacheWithTelemetry< }; insert(sortedAnnotations, processedAnnotation, (a, b) => - compareExecutionPoints(a.point, b.point) + compareNumericStrings(a.point, b.point) ); } }); diff --git a/src/ui/suspense/annotationsCaches.ts b/src/ui/suspense/annotationsCaches.ts index 622cbdb5ae0..d6aaa7f19b5 100644 --- a/src/ui/suspense/annotationsCaches.ts +++ b/src/ui/suspense/annotationsCaches.ts @@ -1,7 +1,7 @@ import { Annotation, TimeStampedPoint } from "@replayio/protocol"; import { Cache, createCache, createSingleEntryCache } from "suspense"; -import { compareExecutionPoints } from "protocol/utils"; +import { compareNumericStrings } from "protocol/utils"; import { ReplayClientInterface } from "shared/client/types"; import { InteractionEventKind } from "ui/actions/eventListeners/constants"; import { EventListenerWithFunctionInfo } from "ui/actions/eventListeners/eventListenerUtils"; @@ -58,7 +58,7 @@ export const reactDevToolsAnnotationsCache = createSingleEntryCache< receivedAnnotations.push(annotation); }); - receivedAnnotations.sort((a1, a2) => compareExecutionPoints(a1.point, a2.point)); + receivedAnnotations.sort((a1, a2) => compareNumericStrings(a1.point, a2.point)); const parsedAnnotations: ParsedReactDevToolsAnnotation[] = receivedAnnotations.map( ({ point, time, contents }) => ({ @@ -84,7 +84,7 @@ export const reduxDevToolsAnnotationsCache = createSingleEntryCache< receivedAnnotations.push(annotation); }); - receivedAnnotations.sort((a1, a2) => compareExecutionPoints(a1.point, a2.point)); + receivedAnnotations.sort((a1, a2) => compareNumericStrings(a1.point, a2.point)); const parsedAnnotations = processReduxAnnotations(receivedAnnotations); @@ -104,7 +104,7 @@ export const eventListenersJumpLocationsCache = createSingleEntryCache< receivedAnnotations.push(annotation); }); - receivedAnnotations.sort((a1, a2) => compareExecutionPoints(a1.point, a2.point)); + receivedAnnotations.sort((a1, a2) => compareNumericStrings(a1.point, a2.point)); const parsedAnnotations: ParsedJumpToCodeAnnotation[] = receivedAnnotations.map( ({ point, time, contents }) => ({ From 7eed684b7b5c352766559567b1da09d0194aa40c Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Thu, 12 Sep 2024 13:46:30 -0700 Subject: [PATCH 05/18] reapply --- packages/protocol/RecordedEventsCache.ts | 4 ++-- packages/protocol/execution-point-utils.ts | 6 +++--- packages/protocol/utils.ts | 15 ++++----------- .../replay-next/src/suspense/HitPointsCache.ts | 4 ++-- packages/replay-next/src/utils/string.ts | 4 ---- packages/shared/client/ReplayClient.ts | 10 +++++----- src/ui/components/NetworkMonitor/utils.ts | 4 ++-- .../TestSuite/suspense/AnnotationsCache.ts | 6 +++--- src/ui/suspense/annotationsCaches.ts | 8 ++++---- 9 files changed, 25 insertions(+), 36 deletions(-) diff --git a/packages/protocol/RecordedEventsCache.ts b/packages/protocol/RecordedEventsCache.ts index 6ea22b83ad6..f54f813e386 100644 --- a/packages/protocol/RecordedEventsCache.ts +++ b/packages/protocol/RecordedEventsCache.ts @@ -5,7 +5,7 @@ import { } from "@replayio/protocol"; import { createSingleEntryCache } from "suspense"; -import { compareNumericStrings } from "protocol/utils"; +import { compareExecutionPoints } from "protocol/utils"; import { findIndexLTE } from "replay-next/src/utils/array"; import { replayClient } from "shared/client/ReplayClientContext"; @@ -39,7 +39,7 @@ export const RecordedEventsCache = createSingleEntryCache<[], RecordedEvent[]>({ const events = [...keyboardEvents, ...mouseEvents, ...navigationEvents]; - return events.sort((a, b) => compareNumericStrings(a.point, b.point)); + return events.sort((a, b) => compareExecutionPoints(a.point, b.point)); }, }); diff --git a/packages/protocol/execution-point-utils.ts b/packages/protocol/execution-point-utils.ts index bfb15f34b13..9db443ac2db 100644 --- a/packages/protocol/execution-point-utils.ts +++ b/packages/protocol/execution-point-utils.ts @@ -1,15 +1,15 @@ import { ExecutionPoint } from "@replayio/protocol"; -import { compareNumericStrings } from "protocol/utils"; +import { compareExecutionPoints } from "protocol/utils"; export function pointEquals(p1: ExecutionPoint, p2: ExecutionPoint) { p1 == p2; } export function pointPrecedes(p1: ExecutionPoint, p2: ExecutionPoint) { - return compareNumericStrings(p1, p2) < 0; + return compareExecutionPoints(p1, p2) < 0; } export function comparePoints(p1: ExecutionPoint, p2: ExecutionPoint) { - return compareNumericStrings(p1, p2); + return compareExecutionPoints(p1, p2); } diff --git a/packages/protocol/utils.ts b/packages/protocol/utils.ts index 665b761417e..46fa0531495 100644 --- a/packages/protocol/utils.ts +++ b/packages/protocol/utils.ts @@ -154,23 +154,16 @@ export function breakdownSupplementalId(id: string): { id: string, supplementalI return { id: match[2], supplementalIndex }; } -const SupplementalNumericStringShift = 200; - -export function transformSupplementalNumericString(v: string, supplementalIndex: number): string { - assert(BigInt(v) < BigInt(1) << BigInt(SupplementalNumericStringShift)); - if (!supplementalIndex) { - return v; - } - return (BigInt(v) | (BigInt(1) << BigInt(SupplementalNumericStringShift))).toString(); -} - /** * Compare 2 integers encoded as numeric strings, because we want to avoid using BigInt (for now). * This will only work correctly if both strings encode positive integers (without decimal places), * using the same base (usually 10) and don't use "fancy stuff" like leading "+", "0" or scientific * notation. */ -export function compareNumericStrings(a: string, b: string) { +export function compareExecutionPoints(transformedA: string, transformedB: string) { + const { id: a, supplementalIndex: indexA } = breakdownSupplementalId(transformedA); + const { id: b, supplementalIndex: indexB } = breakdownSupplementalId(transformedB); + assert(indexA == indexB); return a.length < b.length ? -1 : a.length > b.length ? 1 : a < b ? -1 : a > b ? 1 : 0; } diff --git a/packages/replay-next/src/suspense/HitPointsCache.ts b/packages/replay-next/src/suspense/HitPointsCache.ts index 2a588fc0f43..c8ca92ca677 100644 --- a/packages/replay-next/src/suspense/HitPointsCache.ts +++ b/packages/replay-next/src/suspense/HitPointsCache.ts @@ -3,7 +3,7 @@ import { createCache } from "suspense"; import { breakpointPositionsIntervalCache } from "replay-next/src/suspense/BreakpointPositionsCache"; import { bucketBreakpointLines } from "replay-next/src/utils/source"; -import { compareNumericStrings } from "replay-next/src/utils/string"; +import { compareExecutionPoints } from "protocol/utils"; import { MAX_POINTS_TO_RUN_EVALUATION } from "shared/client/ReplayClient"; import { HitPointStatus, @@ -69,7 +69,7 @@ export const hitPointsCache = createFocusIntervalCacheForExecutionPoints< ); } ); - hitPoints.sort((a, b) => compareNumericStrings(a.point, b.point)); + hitPoints.sort((a, b) => compareExecutionPoints(a.point, b.point)); } else { const pointDescriptions = await replayClient.findPoints( { kind: "locations", locations }, diff --git a/packages/replay-next/src/utils/string.ts b/packages/replay-next/src/utils/string.ts index 97b3e4453c5..24046a56d95 100644 --- a/packages/replay-next/src/utils/string.ts +++ b/packages/replay-next/src/utils/string.ts @@ -1,9 +1,5 @@ export const NEW_LINE_REGEX = /\r\n?|\n|\u2028|\u2029/; -export function compareNumericStrings(a: string, b: string): number { - return a.length < b.length ? -1 : a.length > b.length ? 1 : a < b ? -1 : a > b ? 1 : 0; -} - // Convenience function to JSON-stringify values that may contain circular references. export function stringify(value: any, space?: string | number): string { const cache: any[] = []; diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index 6a551488df4..f95dad9880f 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -78,7 +78,7 @@ import uniqueId from "lodash/uniqueId"; // eslint-disable-next-line no-restricted-imports import { addEventListener, client, initSocket, removeEventListener } from "protocol/socket"; -import { assert, compareNumericStrings, defer, waitForTime, transformSupplementalId, breakdownSupplementalId, transformSupplementalNumericString } from "protocol/utils"; +import { assert, compareExecutionPoints, defer, waitForTime, transformSupplementalId, breakdownSupplementalId } from "protocol/utils"; import { initProtocolMessagesStore } from "replay-next/components/protocol/ProtocolMessagesStore"; import { insert } from "replay-next/src/utils/array"; import { TOO_MANY_POINTS_TO_FIND } from "shared/constants"; @@ -314,7 +314,7 @@ export class ReplayClient implements ReplayClientInterface { let middleIndex = (lowIndex + highIndex) >>> 1; const message = sortedMessages[middleIndex]; - if (compareNumericStrings(message.point.point, newMessagePoint)) { + if (compareExecutionPoints(message.point.point, newMessagePoint)) { lowIndex = middleIndex + 1; } else { highIndex = middleIndex; @@ -357,7 +357,7 @@ export class ReplayClient implements ReplayClientInterface { const sortedMessages = response.messages.sort((messageA: Message, messageB: Message) => { const pointA = messageA.point.point; const pointB = messageB.point.point; - return compareNumericStrings(pointA, pointB); + return compareExecutionPoints(pointA, pointB); }); return { @@ -537,9 +537,9 @@ export class ReplayClient implements ReplayClientInterface { throw commandError("Too many points", ProtocolError.TooManyPoints); } - points.sort((a, b) => compareNumericStrings(a.point, b.point)); + points.sort((a, b) => compareExecutionPoints(a.point, b.point)); return points.map(desc => { - const point = transformSupplementalNumericString(desc.point, supplementalIndex); + const point = transformSupplementalId(desc.point, supplementalIndex); return { ...desc, point }; }); } diff --git a/src/ui/components/NetworkMonitor/utils.ts b/src/ui/components/NetworkMonitor/utils.ts index f4d956a616e..bab0967a26b 100644 --- a/src/ui/components/NetworkMonitor/utils.ts +++ b/src/ui/components/NetworkMonitor/utils.ts @@ -11,7 +11,7 @@ import { } from "@replayio/protocol"; import keyBy from "lodash/keyBy"; -import { assert, compareNumericStrings } from "protocol/utils"; +import { assert, compareExecutionPoints } from "protocol/utils"; import { NetworkRequestsCacheData } from "replay-next/src/suspense/NetworkRequestsCache"; export enum CanonicalRequestType { @@ -174,7 +174,7 @@ export const partialRequestsToCompleteSummaries = ( }) .filter(row => types.size === 0 || types.has(row.type)); - summaries.sort((a, b) => compareNumericStrings(a.point.point, b.point.point)); + summaries.sort((a, b) => compareExecutionPoints(a.point.point, b.point.point)); return summaries; }; diff --git a/src/ui/components/TestSuite/suspense/AnnotationsCache.ts b/src/ui/components/TestSuite/suspense/AnnotationsCache.ts index 1855ae3a512..f806cd28d0b 100644 --- a/src/ui/components/TestSuite/suspense/AnnotationsCache.ts +++ b/src/ui/components/TestSuite/suspense/AnnotationsCache.ts @@ -1,4 +1,4 @@ -import { compareNumericStrings } from "protocol/utils"; +import { compareExecutionPoints } from "protocol/utils"; import { insert } from "replay-next/src/utils/array"; import { createSingleEntryCacheWithTelemetry } from "replay-next/src/utils/suspense"; import { ReplayClientInterface } from "shared/client/types"; @@ -20,7 +20,7 @@ export const AnnotationsCache = createSingleEntryCacheWithTelemetry< }; insert(sortedAnnotations, processedAnnotation, (a, b) => - compareNumericStrings(a.point, b.point) + compareExecutionPoints(a.point, b.point) ); }); @@ -47,7 +47,7 @@ export const PlaywrightAnnotationsCache = createSingleEntryCacheWithTelemetry< }; insert(sortedAnnotations, processedAnnotation, (a, b) => - compareNumericStrings(a.point, b.point) + compareExecutionPoints(a.point, b.point) ); } }); diff --git a/src/ui/suspense/annotationsCaches.ts b/src/ui/suspense/annotationsCaches.ts index d6aaa7f19b5..622cbdb5ae0 100644 --- a/src/ui/suspense/annotationsCaches.ts +++ b/src/ui/suspense/annotationsCaches.ts @@ -1,7 +1,7 @@ import { Annotation, TimeStampedPoint } from "@replayio/protocol"; import { Cache, createCache, createSingleEntryCache } from "suspense"; -import { compareNumericStrings } from "protocol/utils"; +import { compareExecutionPoints } from "protocol/utils"; import { ReplayClientInterface } from "shared/client/types"; import { InteractionEventKind } from "ui/actions/eventListeners/constants"; import { EventListenerWithFunctionInfo } from "ui/actions/eventListeners/eventListenerUtils"; @@ -58,7 +58,7 @@ export const reactDevToolsAnnotationsCache = createSingleEntryCache< receivedAnnotations.push(annotation); }); - receivedAnnotations.sort((a1, a2) => compareNumericStrings(a1.point, a2.point)); + receivedAnnotations.sort((a1, a2) => compareExecutionPoints(a1.point, a2.point)); const parsedAnnotations: ParsedReactDevToolsAnnotation[] = receivedAnnotations.map( ({ point, time, contents }) => ({ @@ -84,7 +84,7 @@ export const reduxDevToolsAnnotationsCache = createSingleEntryCache< receivedAnnotations.push(annotation); }); - receivedAnnotations.sort((a1, a2) => compareNumericStrings(a1.point, a2.point)); + receivedAnnotations.sort((a1, a2) => compareExecutionPoints(a1.point, a2.point)); const parsedAnnotations = processReduxAnnotations(receivedAnnotations); @@ -104,7 +104,7 @@ export const eventListenersJumpLocationsCache = createSingleEntryCache< receivedAnnotations.push(annotation); }); - receivedAnnotations.sort((a1, a2) => compareNumericStrings(a1.point, a2.point)); + receivedAnnotations.sort((a1, a2) => compareExecutionPoints(a1.point, a2.point)); const parsedAnnotations: ParsedJumpToCodeAnnotation[] = receivedAnnotations.map( ({ point, time, contents }) => ({ From 8231e7c6243568a420c9003405aa527562ba6a34 Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Fri, 13 Sep 2024 06:51:04 -0700 Subject: [PATCH 06/18] wip --- packages/protocol/utils.ts | 38 +++++++++++++++++-- .../components/console/MessagesList.tsx | 13 ++++--- .../sources/log-point-panel/LogPointPanel.tsx | 5 ++- .../src/suspense/FocusIntervalCache.ts | 6 ++- .../src/suspense/HitPointsCache.ts | 28 ++++++++++---- packages/replay-next/src/utils/loggables.ts | 7 +++- packages/replay-next/src/utils/time.ts | 5 +-- 7 files changed, 78 insertions(+), 24 deletions(-) diff --git a/packages/protocol/utils.ts b/packages/protocol/utils.ts index 46fa0531495..6249c2ff2c7 100644 --- a/packages/protocol/utils.ts +++ b/packages/protocol/utils.ts @@ -1,4 +1,4 @@ -import { SourceLocation } from "@replayio/protocol"; +import { SourceLocation, TimeStampedPoint } from "@replayio/protocol"; type ErrorHandler = (error: Error) => void; @@ -154,17 +154,47 @@ export function breakdownSupplementalId(id: string): { id: string, supplementalI return { id: match[2], supplementalIndex }; } -/** +export function sameSupplementalIndex(idA: string, idB: string) { + return breakdownSupplementalId(idA).supplementalIndex == breakdownSupplementalId(idB).supplementalIndex; +} + +const SupplementalNumericStringShift = 200; + +export function transformSupplementalNumericString(v: string, supplementalIndex: number): string { + assert(BigInt(v) < BigInt(1) << BigInt(SupplementalNumericStringShift)); + if (!supplementalIndex) { + return v; + } + return (BigInt(v) | (BigInt(1) << BigInt(SupplementalNumericStringShift))).toString(); +} + +/* * Compare 2 integers encoded as numeric strings, because we want to avoid using BigInt (for now). * This will only work correctly if both strings encode positive integers (without decimal places), * using the same base (usually 10) and don't use "fancy stuff" like leading "+", "0" or scientific * notation. */ +function compareNumericIntegers(a: string, b: string) { + return a.length < b.length ? -1 : a.length > b.length ? 1 : a < b ? -1 : a > b ? 1 : 0; +} + +// Compare execution points, which must be from the same recording. export function compareExecutionPoints(transformedA: string, transformedB: string) { const { id: a, supplementalIndex: indexA } = breakdownSupplementalId(transformedA); const { id: b, supplementalIndex: indexB } = breakdownSupplementalId(transformedB); - assert(indexA == indexB); - return a.length < b.length ? -1 : a.length > b.length ? 1 : a < b ? -1 : a > b ? 1 : 0; + assert(indexA == indexB, `Points ${transformedA} and ${transformedB} are not comparable`); + return compareNumericIntegers(a, b); +} + +// Compare execution points along with their times. Falls back onto time +// comparison for points from different recordings. +export function compareTimeStampedPoints(transformedA: TimeStampedPoint, transformedB: TimeStampedPoint) { + const { id: a, supplementalIndex: indexA } = breakdownSupplementalId(transformedA.point); + const { id: b, supplementalIndex: indexB } = breakdownSupplementalId(transformedB.point); + if (indexA == indexB) { + return compareNumericIntegers(a, b); + } + return transformedA.time - transformedB.time; } export function locationsInclude(haystack: SourceLocation[], needle: SourceLocation) { diff --git a/packages/replay-next/components/console/MessagesList.tsx b/packages/replay-next/components/console/MessagesList.tsx index a73ad1e4559..d6affa5ce0e 100644 --- a/packages/replay-next/components/console/MessagesList.tsx +++ b/packages/replay-next/components/console/MessagesList.tsx @@ -16,13 +16,13 @@ import { useMostRecentLoadedPause } from "replay-next/src/hooks/useMostRecentLoa import { useStreamingMessages } from "replay-next/src/hooks/useStreamingMessages"; import { getLoggableExecutionPoint, + getLoggableTime, isEventLog, isPointInstance, isProtocolMessage, isTerminalExpression, isUncaughtException, } from "replay-next/src/utils/loggables"; -import { isExecutionPointsLessThan } from "replay-next/src/utils/time"; import { ConsoleSearchContext } from "./ConsoleSearchContext"; import CurrentTimeIndicator from "./CurrentTimeIndicator"; @@ -34,6 +34,7 @@ import TerminalExpressionRenderer from "./renderers/TerminalExpressionRenderer"; import UncaughtExceptionRenderer from "./renderers/UncaughtExceptionRenderer"; import styles from "./MessagesList.module.css"; import rendererStyles from "./renderers/shared.module.css"; +import { compareTimeStampedPoints } from "protocol/utils"; type CurrentTimeIndicatorPlacement = Loggable | "begin" | "end"; @@ -66,21 +67,23 @@ function MessagesListSuspends({ forwardedRef }: { forwardedRef: ForwardedRef(() => { - if (!currentExecutionPoint) { + if (!currentExecutionPoint || !currentTime) { return null; } if (currentExecutionPoint === "0") { return "begin"; } const nearestLoggable = loggables.find(loggable => { - const executionPoint = getLoggableExecutionPoint(loggable); - if (!isExecutionPointsLessThan(executionPoint, currentExecutionPoint)) { + const point = getLoggableExecutionPoint(loggable); + const time = getLoggableTime(loggable); + const v = compareTimeStampedPoints({ point, time }, { point: currentExecutionPoint, time: currentTime }); + if (v >= 0) { return true; } }); diff --git a/packages/replay-next/components/sources/log-point-panel/LogPointPanel.tsx b/packages/replay-next/components/sources/log-point-panel/LogPointPanel.tsx index 4c3f4bd7e1d..f8a4cb0af41 100644 --- a/packages/replay-next/components/sources/log-point-panel/LogPointPanel.tsx +++ b/packages/replay-next/components/sources/log-point-panel/LogPointPanel.tsx @@ -9,7 +9,7 @@ import { useTransition, } from "react"; -import { assert } from "protocol/utils"; +import { assert, sameSupplementalIndex } from "protocol/utils"; import AvatarImage from "replay-next/components/AvatarImage"; import { InlineErrorBoundary } from "replay-next/components/errors/InlineErrorBoundary"; import Icon from "replay-next/components/Icon"; @@ -214,6 +214,9 @@ export function PointPanelWithHitPoints({ if (!currentExecutionPoint) { return null; } + if (!sameSupplementalIndex(currentExecutionPoint, location.sourceId)) { + return null; + } const executionPoints = hitPoints.map(hitPoint => hitPoint.point); const index = findIndexBigInt(executionPoints, currentExecutionPoint, false); return hitPoints[index] || null; diff --git a/packages/replay-next/src/suspense/FocusIntervalCache.ts b/packages/replay-next/src/suspense/FocusIntervalCache.ts index e527ffdc1b8..892724c7494 100644 --- a/packages/replay-next/src/suspense/FocusIntervalCache.ts +++ b/packages/replay-next/src/suspense/FocusIntervalCache.ts @@ -7,6 +7,7 @@ import { } from "suspense"; import { ProtocolError, isCommandError } from "shared/utils/error"; +import { breakdownSupplementalId, transformSupplementalNumericString } from "protocol/utils"; type Options, Value> = { debugLabel?: string; @@ -35,7 +36,10 @@ export function createFocusIntervalCacheForExecutionPoints({ ...rest, getPointForValue: (value: Value): bigint => { - return BigInt(getPointForValue(value)); + const transformedPoint = getPointForValue(value); + const { id: point, supplementalIndex } = breakdownSupplementalId(transformedPoint); + const newPoint = transformSupplementalNumericString(point, supplementalIndex); + return BigInt(newPoint); }, load: (start: bigint, end: bigint, ...params: [...Params, IntervalCacheLoadOptions]) => load(start.toString(), end.toString(), ...params), diff --git a/packages/replay-next/src/suspense/HitPointsCache.ts b/packages/replay-next/src/suspense/HitPointsCache.ts index c8ca92ca677..d5f12eeea60 100644 --- a/packages/replay-next/src/suspense/HitPointsCache.ts +++ b/packages/replay-next/src/suspense/HitPointsCache.ts @@ -3,7 +3,7 @@ import { createCache } from "suspense"; import { breakpointPositionsIntervalCache } from "replay-next/src/suspense/BreakpointPositionsCache"; import { bucketBreakpointLines } from "replay-next/src/utils/source"; -import { compareExecutionPoints } from "protocol/utils"; +import { compareExecutionPoints, breakdownSupplementalId } from "protocol/utils"; import { MAX_POINTS_TO_RUN_EVALUATION } from "shared/client/ReplayClient"; import { HitPointStatus, @@ -107,13 +107,25 @@ export const hitPointsForLocationCache = createCache< let hitPoints: TimeStampedPoint[] = []; let status: HitPointStatus = "complete"; try { - hitPoints = await hitPointsCache.readAsync( - BigInt(range.begin), - BigInt(range.end), - replayClient, - location, - condition - ); + const { id: sourceId, supplementalIndex } = breakdownSupplementalId(location.sourceId); + if (supplementalIndex) { + if (condition) { + throw new Error("NYI"); + } + const sources = await sourcesByIdCache.readAsync(replayClient); + const locations = getCorrespondingLocations(sources, location); + hitPoints = await replayClient.findPoints( + { kind: "locations", locations }, undefined + ); + } else { + hitPoints = await hitPointsCache.readAsync( + BigInt(range.begin), + BigInt(range.end), + replayClient, + location, + condition + ); + } if (hitPoints.length > MAX_POINTS_TO_RUN_EVALUATION) { status = "too-many-points-to-run-analysis"; } diff --git a/packages/replay-next/src/utils/loggables.ts b/packages/replay-next/src/utils/loggables.ts index 41c6b7b9216..0c3cde54b5a 100644 --- a/packages/replay-next/src/utils/loggables.ts +++ b/packages/replay-next/src/utils/loggables.ts @@ -7,7 +7,7 @@ import { TerminalExpression } from "replay-next/src/contexts/TerminalContext"; import { EventLog } from "replay-next/src/suspense/EventsCache"; import { UncaughtException } from "replay-next/src/suspense/ExceptionsCache"; -import { compareExecutionPoints } from "./time"; +import { compareTimeStampedPoints } from "protocol/utils"; export function isEventLog(loggable: Loggable): loggable is EventLog { return loggable.type === "EventLog"; @@ -80,5 +80,8 @@ export function loggableSort(a: Loggable, b: Loggable): number { } } - return compareExecutionPoints(aPoint, bPoint); + const aTime = getLoggableTime(a); + const bTime = getLoggableTime(b); + + return compareTimeStampedPoints({ point: aPoint, time: aTime }, { point: bPoint, time: bTime }); } diff --git a/packages/replay-next/src/utils/time.ts b/packages/replay-next/src/utils/time.ts index 878ede2c9e2..d95da91e4b3 100644 --- a/packages/replay-next/src/utils/time.ts +++ b/packages/replay-next/src/utils/time.ts @@ -6,10 +6,9 @@ import differenceInWeeks from "date-fns/differenceInWeeks"; import differenceInYears from "date-fns/differenceInYears"; import padStart from "lodash/padStart"; import prettyMilliseconds from "pretty-ms"; +import { compareExecutionPoints as baseCompareExecutionPoints } from "protocol/utils"; -export function compareExecutionPoints(a: ExecutionPoint, b: ExecutionPoint): number { - return Number(BigInt(a) - BigInt(b)); -} +export const compareExecutionPoints = baseCompareExecutionPoints; export function isExecutionPointsGreaterThan(a: ExecutionPoint, b: ExecutionPoint): boolean { return compareExecutionPoints(a, b) > 0; From f48b3890253847c083b7534d6734bd83d689b5e0 Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Fri, 13 Sep 2024 07:28:35 -0700 Subject: [PATCH 07/18] wip --- .../components/console/MessageHoverButton.tsx | 10 +++--- .../components/sources/SeekHoverButtons.tsx | 6 ++-- .../log-point-panel/HitPointTimeline.tsx | 17 +++++----- .../sources/log-point-panel/LogPointPanel.tsx | 12 +++---- .../sources/useSourceContextMenu.tsx | 8 ++--- .../sources/utils/findLastHitPoint.ts | 10 +++--- .../sources/utils/findNextHitPoints.ts | 10 +++--- .../components/sources/utils/points.ts | 32 +++++++++++-------- packages/shared/utils/time.ts | 4 +++ 9 files changed, 58 insertions(+), 51 deletions(-) diff --git a/packages/replay-next/components/console/MessageHoverButton.tsx b/packages/replay-next/components/console/MessageHoverButton.tsx index 3d095eba3a9..8f3da15a6cd 100644 --- a/packages/replay-next/components/console/MessageHoverButton.tsx +++ b/packages/replay-next/components/console/MessageHoverButton.tsx @@ -22,10 +22,10 @@ import { TimelineContext } from "replay-next/src/contexts/TimelineContext"; import { useNag } from "replay-next/src/hooks/useNag"; import { sourcesByIdCache } from "replay-next/src/suspense/SourcesCache"; import { getPreferredLocationWorkaround } from "replay-next/src/utils/sources"; -import { isExecutionPointsGreaterThan } from "replay-next/src/utils/time"; import { ReplayClientContext } from "shared/client/ReplayClientContext"; import { addComment as addCommentGraphQL } from "shared/graphql/Comments"; import { Nag } from "shared/graphql/types"; +import { compareTimeStampedPoints } from "protocol/utils"; import styles from "./MessageHoverButton.module.css"; @@ -48,7 +48,7 @@ export default function MessageHoverButton({ const { inspectFunctionDefinition, showCommentsPanel } = useContext(InspectorContext); const { accessToken, recordingId, trackEvent } = useContext(SessionContext); const graphQLClient = useContext(GraphQLClientContext); - const { executionPoint: currentExecutionPoint, update } = useContext(TimelineContext); + const { executionPoint: currentExecutionPoint, time: currentTime, update } = useContext(TimelineContext); const { canShowConsoleAndSources } = useContext(LayoutContext); const invalidateCache = useCacheRefresh(); @@ -134,9 +134,11 @@ export default function MessageHoverButton({ dismissFirstConsoleNavigateNag(); }; + const pointTS = { point: executionPoint, time }; + const label = currentExecutionPoint === null || - isExecutionPointsGreaterThan(executionPoint, currentExecutionPoint) + compareTimeStampedPoints(pointTS, { point: currentExecutionPoint, time: currentTime }) > 0 ? "Fast-forward" : "Rewind"; @@ -153,7 +155,7 @@ export default function MessageHoverButton({ className={styles.ConsoleMessageHoverButtonIcon} type={ currentExecutionPoint === null || - isExecutionPointsGreaterThan(executionPoint, currentExecutionPoint) + compareTimeStampedPoints(pointTS, { point: currentExecutionPoint, time: currentTime }) > 0 ? "fast-forward" : "rewind" } diff --git a/packages/replay-next/components/sources/SeekHoverButtons.tsx b/packages/replay-next/components/sources/SeekHoverButtons.tsx index 5f27e050bed..d43067d4be4 100644 --- a/packages/replay-next/components/sources/SeekHoverButtons.tsx +++ b/packages/replay-next/components/sources/SeekHoverButtons.tsx @@ -33,7 +33,7 @@ export function SeekHoverButtons(props: Props) { function SuspendingComponent({ lineHitCounts, lineNumber, source }: Props) { const { rangeForSuspense: focusRange } = useContext(FocusContext); const replayClient = useContext(ReplayClientContext); - const { executionPoint, update } = useContext(TimelineContext); + const { executionPoint, time, update } = useContext(TimelineContext); let hitPoints: TimeStampedPoint[] | null = null; let hitPointStatus: HitPointStatus | null = null; @@ -56,7 +56,7 @@ function SuspendingComponent({ lineHitCounts, lineNumber, source }: Props) { let goToPrevPoint: undefined | EventHandler = undefined; let goToNextPoint: undefined | EventHandler = undefined; if (executionPoint && hitPoints !== null && hitPointStatus !== "too-many-points-to-find") { - const prevTargetPoint = findLastHitPoint(hitPoints, executionPoint); + const prevTargetPoint = findLastHitPoint(hitPoints, { point: executionPoint, time }); if (prevTargetPoint) { goToPrevPoint = () => { const location = { @@ -68,7 +68,7 @@ function SuspendingComponent({ lineHitCounts, lineNumber, source }: Props) { }; } - const nextTargetPoint = findNextHitPoint(hitPoints, executionPoint); + const nextTargetPoint = findNextHitPoint(hitPoints, { point: executionPoint, time }); if (nextTargetPoint) { goToNextPoint = () => { const location = { diff --git a/packages/replay-next/components/sources/log-point-panel/HitPointTimeline.tsx b/packages/replay-next/components/sources/log-point-panel/HitPointTimeline.tsx index 61e216f5b02..034c03c10ca 100644 --- a/packages/replay-next/components/sources/log-point-panel/HitPointTimeline.tsx +++ b/packages/replay-next/components/sources/log-point-panel/HitPointTimeline.tsx @@ -14,16 +14,13 @@ import Icon from "replay-next/components/Icon"; import { SessionContext } from "replay-next/src/contexts/SessionContext"; import { TimelineContext } from "replay-next/src/contexts/TimelineContext"; import { imperativelyGetClosestPointForTime } from "replay-next/src/suspense/ExecutionPointsCache"; -import { - isExecutionPointsGreaterThan, - isExecutionPointsLessThan, -} from "replay-next/src/utils/time"; import { formatTimestamp } from "replay-next/src/utils/time"; import { ReplayClientContext } from "shared/client/ReplayClientContext"; import { HitPointStatus, Point } from "shared/client/types"; import { findHitPointAfter, findHitPointBefore, noMatchTuple } from "../utils/points"; import { findHitPoint } from "../utils/points"; +import { compareTimeStampedPoints } from "protocol/utils"; import Capsule from "./Capsule"; import styles from "./HitPointTimeline.module.css"; @@ -53,6 +50,8 @@ export default function HitPointTimeline({ update, } = useContext(TimelineContext); + const currentTS: TimeStampedPoint = { point: currentExecutionPoint!, time: currentTime }; + const pointEditable = point.user?.id === currentUserInfo?.id; const [hoverCoordinates, setHoverCoordinates] = useState<{ @@ -73,7 +72,7 @@ export default function HitPointTimeline({ const [closestHitPoint, closestHitPointIndex] = useMemo( () => - currentExecutionPoint ? findHitPoint(hitPoints, currentExecutionPoint, false) : noMatchTuple, + currentExecutionPoint ? findHitPoint(hitPoints, currentTS, false) : noMatchTuple, [currentExecutionPoint, hitPoints] ); @@ -116,11 +115,11 @@ export default function HitPointTimeline({ const previousButtonEnabled = currentExecutionPoint && firstHitPoint != null && - isExecutionPointsLessThan(firstHitPoint.point, currentExecutionPoint); + compareTimeStampedPoints(firstHitPoint, currentTS) < 0; const nextButtonEnabled = currentExecutionPoint && lastHitPoint != null && - isExecutionPointsGreaterThan(lastHitPoint.point, currentExecutionPoint); + compareTimeStampedPoints(lastHitPoint, currentTS) > 0; const goToIndex = (index: number) => { const hitPoint = hitPoints[index]; @@ -134,7 +133,7 @@ export default function HitPointTimeline({ if (!currentExecutionPoint) { return; } - const [prevHitPoint] = findHitPointBefore(hitPoints, currentExecutionPoint); + const [prevHitPoint] = findHitPointBefore(hitPoints, currentTS); if (prevHitPoint !== null) { setOptimisticTime(prevHitPoint.time); update(prevHitPoint.time, prevHitPoint.point, false, point.location); @@ -144,7 +143,7 @@ export default function HitPointTimeline({ if (!currentExecutionPoint) { return; } - const [nextHitPoint] = findHitPointAfter(hitPoints, currentExecutionPoint); + const [nextHitPoint] = findHitPointAfter(hitPoints, currentTS); if (nextHitPoint !== null) { setOptimisticTime(nextHitPoint.time); update(nextHitPoint.time, nextHitPoint.point, false, point.location); diff --git a/packages/replay-next/components/sources/log-point-panel/LogPointPanel.tsx b/packages/replay-next/components/sources/log-point-panel/LogPointPanel.tsx index f8a4cb0af41..4409c5c8d1f 100644 --- a/packages/replay-next/components/sources/log-point-panel/LogPointPanel.tsx +++ b/packages/replay-next/components/sources/log-point-panel/LogPointPanel.tsx @@ -9,7 +9,7 @@ import { useTransition, } from "react"; -import { assert, sameSupplementalIndex } from "protocol/utils"; +import { assert } from "protocol/utils"; import AvatarImage from "replay-next/components/AvatarImage"; import { InlineErrorBoundary } from "replay-next/components/errors/InlineErrorBoundary"; import Icon from "replay-next/components/Icon"; @@ -28,8 +28,8 @@ import { TimelineContext } from "replay-next/src/contexts/TimelineContext"; import { useNag } from "replay-next/src/hooks/useNag"; import { hitPointsForLocationCache } from "replay-next/src/suspense/HitPointsCache"; import { getSourceSuspends } from "replay-next/src/suspense/SourcesCache"; -import { findIndexBigInt } from "replay-next/src/utils/array"; import { validateCode } from "replay-next/src/utils/code"; +import { findHitPointBefore } from "../utils/points"; import { MAX_POINTS_TO_RUN_EVALUATION } from "shared/client/ReplayClient"; import { ReplayClientContext } from "shared/client/ReplayClientContext"; import { @@ -214,12 +214,8 @@ export function PointPanelWithHitPoints({ if (!currentExecutionPoint) { return null; } - if (!sameSupplementalIndex(currentExecutionPoint, location.sourceId)) { - return null; - } - const executionPoints = hitPoints.map(hitPoint => hitPoint.point); - const index = findIndexBigInt(executionPoints, currentExecutionPoint, false); - return hitPoints[index] || null; + const [hitPoint] = findHitPointBefore(hitPoints, { point: currentExecutionPoint, time: currentTime }); + return hitPoint || null; }, [hitPoints, currentExecutionPoint]); // If we've found a hit point match, use data from its scope. diff --git a/packages/replay-next/components/sources/useSourceContextMenu.tsx b/packages/replay-next/components/sources/useSourceContextMenu.tsx index 5966405746b..13071edfad7 100644 --- a/packages/replay-next/components/sources/useSourceContextMenu.tsx +++ b/packages/replay-next/components/sources/useSourceContextMenu.tsx @@ -133,7 +133,7 @@ function FastForwardButton({ lineNumber: number; sourceId: SourceId; }) { - const { executionPoint: currentExecutionPoint, update } = useContext(TimelineContext); + const { executionPoint: currentExecutionPoint, time: currentTime, update } = useContext(TimelineContext); const [hitPoints, hitPointStatus] = useDeferredHitCounts({ lineHitCounts, @@ -143,7 +143,7 @@ function FastForwardButton({ let fastForwardToExecutionPoint: TimeStampedPoint | null = null; if (currentExecutionPoint && hitPoints !== null && hitPointStatus !== "too-many-points-to-find") { - fastForwardToExecutionPoint = findNextHitPoint(hitPoints, currentExecutionPoint); + fastForwardToExecutionPoint = findNextHitPoint(hitPoints, { point: currentExecutionPoint, time: currentTime }); } const fastForward = () => { @@ -172,7 +172,7 @@ function RewindButton({ lineNumber: number; sourceId: SourceId; }) { - const { executionPoint: currentExecutionPoint, update } = useContext(TimelineContext); + const { executionPoint: currentExecutionPoint, time: currentTime, update } = useContext(TimelineContext); const [hitPoints, hitPointStatus] = useDeferredHitCounts({ lineHitCounts, @@ -182,7 +182,7 @@ function RewindButton({ let rewindToExecutionPoint: TimeStampedPoint | null = null; if (currentExecutionPoint && hitPoints !== null && hitPointStatus !== "too-many-points-to-find") { - rewindToExecutionPoint = findLastHitPoint(hitPoints, currentExecutionPoint); + rewindToExecutionPoint = findLastHitPoint(hitPoints, { point: currentExecutionPoint, time: currentTime }); } const rewind = () => { diff --git a/packages/replay-next/components/sources/utils/findLastHitPoint.ts b/packages/replay-next/components/sources/utils/findLastHitPoint.ts index e9db55e94ce..f4fed10f755 100644 --- a/packages/replay-next/components/sources/utils/findLastHitPoint.ts +++ b/packages/replay-next/components/sources/utils/findLastHitPoint.ts @@ -1,15 +1,15 @@ -import { ExecutionPoint, TimeStampedPoint } from "@replayio/protocol"; +import { TimeStampedPoint } from "@replayio/protocol"; import findLast from "lodash/findLast"; -import { compareExecutionPoints, isExecutionPointsLessThan } from "replay-next/src/utils/time"; +import { compareTimeStampedPoints } from "protocol/utils"; -export function findLastHitPoint(hitPoints: TimeStampedPoint[], executionPoint: ExecutionPoint) { +export function findLastHitPoint(hitPoints: TimeStampedPoint[], executionPoint: TimeStampedPoint) { const hitPoint = findLast( hitPoints, - point => compareExecutionPoints(point.point, executionPoint) < 0 + point => compareTimeStampedPoints(point, executionPoint) < 0 ); if (hitPoint != null) { - if (isExecutionPointsLessThan(hitPoint.point, executionPoint)) { + if (compareTimeStampedPoints(hitPoint, executionPoint) < 0) { return hitPoint; } } diff --git a/packages/replay-next/components/sources/utils/findNextHitPoints.ts b/packages/replay-next/components/sources/utils/findNextHitPoints.ts index 6e457ade9bb..6fe17ed7916 100644 --- a/packages/replay-next/components/sources/utils/findNextHitPoints.ts +++ b/packages/replay-next/components/sources/utils/findNextHitPoints.ts @@ -1,11 +1,11 @@ -import { ExecutionPoint, TimeStampedPoint } from "@replayio/protocol"; +import { TimeStampedPoint } from "@replayio/protocol"; -import { compareExecutionPoints, isExecutionPointsGreaterThan } from "replay-next/src/utils/time"; +import { compareTimeStampedPoints } from "protocol/utils"; -export function findNextHitPoint(hitPoints: TimeStampedPoint[], executionPoint: ExecutionPoint) { - const hitPoint = hitPoints.find(point => compareExecutionPoints(point.point, executionPoint) > 0); +export function findNextHitPoint(hitPoints: TimeStampedPoint[], executionPoint: TimeStampedPoint) { + const hitPoint = hitPoints.find(point => compareTimeStampedPoints(point, executionPoint) > 0); if (hitPoint != null) { - if (isExecutionPointsGreaterThan(hitPoint.point, executionPoint)) { + if (compareTimeStampedPoints(hitPoint, executionPoint) > 0) { return hitPoint; } } diff --git a/packages/replay-next/components/sources/utils/points.ts b/packages/replay-next/components/sources/utils/points.ts index 4a2c585c044..6ad219ca612 100644 --- a/packages/replay-next/components/sources/utils/points.ts +++ b/packages/replay-next/components/sources/utils/points.ts @@ -1,8 +1,7 @@ import { ExecutionPoint, SourceId, TimeStampedPoint } from "@replayio/protocol"; -import { binarySearch } from "protocol/utils"; +import { binarySearch, compareTimeStampedPoints, sameSupplementalIndex } from "protocol/utils"; import { - compareExecutionPoints, isExecutionPointsGreaterThan, isExecutionPointsLessThan, } from "replay-next/src/utils/time"; @@ -15,23 +14,30 @@ export const noMatchTuple: NoMatchTuple = [null, -1]; export function findClosestHitPoint( hitPoints: TimeStampedPoint[], - executionPoint: ExecutionPoint + executionPoint: TimeStampedPoint ): HitPointAndIndexTuple | NoMatchTuple { const index = binarySearch(0, hitPoints.length, (index: number) => - compareExecutionPoints(executionPoint, hitPoints[index].point) + compareTimeStampedPoints(executionPoint, hitPoints[index]) ); if (index >= 0 && index < hitPoints.length) { const hitPoint = hitPoints[index]; - if (hitPoint.point === executionPoint) { + if (hitPoint.point === executionPoint.point) { // Exact match return [hitPoint, index]; } - const executionBigInt = BigInt(executionPoint); + if (!sameSupplementalIndex(executionPoint.point, hitPoint.point)) { + return [hitPoint, index]; + } + + // Note (bhackett): The code below should be removed, it isn't valid + // to do any operations on the BigInts in execution points other than + // comparing them. + const executionBigInt = BigInt(executionPoint.point); const currentBigInt = BigInt(hitPoint.point); - if (executionBigInt < currentBigInt) { + if (compareTimeStampedPoints(executionPoint, hitPoint) < 0) { const currentDelta = currentBigInt - executionBigInt; const prevHitPoint = hitPoints[index - 1] ?? null; if (prevHitPoint) { @@ -62,12 +68,12 @@ export function findClosestHitPoint( export function findHitPoint( hitPoints: TimeStampedPoint[], - executionPoint: ExecutionPoint, + executionPoint: TimeStampedPoint, exactMatch: boolean = true ): HitPointAndIndexTuple | NoMatchTuple { const [hitPoint, hitPointIndex] = findClosestHitPoint(hitPoints, executionPoint); if (hitPoint !== null) { - if (hitPoint.point === executionPoint) { + if (hitPoint.point === executionPoint.point) { return [hitPoint, hitPointIndex]; } else if (!exactMatch) { return [hitPoint, hitPointIndex]; @@ -78,11 +84,11 @@ export function findHitPoint( export function findHitPointAfter( hitPoints: TimeStampedPoint[], - executionPoint: ExecutionPoint + executionPoint: TimeStampedPoint ): HitPointAndIndexTuple | NoMatchTuple { const [hitPoint, index] = findClosestHitPoint(hitPoints, executionPoint); if (hitPoint !== null) { - if (isExecutionPointsGreaterThan(hitPoint.point, executionPoint)) { + if (compareTimeStampedPoints(hitPoint, executionPoint) > 0) { return [hitPoint, index]; } else { const nextIndex = index + 1; @@ -96,11 +102,11 @@ export function findHitPointAfter( export function findHitPointBefore( hitPoints: TimeStampedPoint[], - executionPoint: ExecutionPoint + executionPoint: TimeStampedPoint ): HitPointAndIndexTuple | NoMatchTuple { const [hitPoint, index] = findClosestHitPoint(hitPoints, executionPoint); if (hitPoint !== null) { - if (isExecutionPointsLessThan(hitPoint.point, executionPoint)) { + if (compareTimeStampedPoints(hitPoint, executionPoint) < 0) { return [hitPoint, index]; } else { const prevIndex = index - 1; diff --git a/packages/shared/utils/time.ts b/packages/shared/utils/time.ts index ee453c9ea82..e615835e302 100644 --- a/packages/shared/utils/time.ts +++ b/packages/shared/utils/time.ts @@ -4,6 +4,7 @@ import { TimeStampedPoint, TimeStampedPointRange, } from "@replayio/protocol"; +import { sameSupplementalIndex } from "protocol/utils"; const supportsPerformanceNow = typeof performance !== "undefined" && typeof performance.now === "function"; @@ -74,6 +75,9 @@ export function isRangeInRegions( } export function isPointInRegion(point: ExecutionPoint, range: TimeStampedPointRange): boolean { + if (!sameSupplementalIndex(point, range.begin.point)) { + return true; + } const pointNumber = BigInt(point); return pointNumber >= BigInt(range.begin.point) && pointNumber <= BigInt(range.end.point); } From c56161e39d4d6f5008acc8c056958bda6496df2e Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Fri, 13 Sep 2024 07:51:57 -0700 Subject: [PATCH 08/18] wip --- packages/shared/client/ReplayClient.ts | 89 +++++++++++++++----------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index f95dad9880f..7d09fbbd5ff 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -114,6 +114,8 @@ export class ReplayClient implements ReplayClientInterface { private supplemental: SupplementalSession[] = []; + private pauseIdToSessionId = new Map(); + private focusWindow: TimeStampedPointRange | null = null; private nextFindPointsId = 1; @@ -170,6 +172,11 @@ export class ReplayClient implements ReplayClientInterface { return { id, sessionId: supplementalInfo.sessionId, supplementalIndex }; } + private async getPauseSessionId(pauseId: string): Promise { + const sessionId = this.pauseIdToSessionId.get(pauseId); + return sessionId || await this.waitForSession(); + } + get loadedRegions(): LoadedRegions | null { return this._loadedRegions; } @@ -189,13 +196,17 @@ export class ReplayClient implements ReplayClientInterface { return buildId; } - async createPause(executionPoint: ExecutionPoint): Promise { - const sessionId = await this.waitForSession(); + async createPause(transformedExecutionPoint: ExecutionPoint): Promise { + const { id: executionPoint, sessionId } = await this.breakdownSupplementalIdAndSession(transformedExecutionPoint); await this.waitForPointToBeInFocusRange(executionPoint); const response = await client.Session.createPause({ point: executionPoint }, sessionId); + if (response.pauseId) { + this.pauseIdToSessionId.set(response.pauseId, sessionId); + } + return response; } @@ -205,7 +216,7 @@ export class ReplayClient implements ReplayClientInterface { frameId: FrameId | null, pure?: boolean ): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); // Edge case handling: // User is logging a plan object (e.g. "{...}") @@ -544,26 +555,26 @@ export class ReplayClient implements ReplayClientInterface { }); } - async findStepInTarget(point: ExecutionPoint): Promise { - const sessionId = await this.waitForSession(); + async findStepInTarget(transformedPoint: ExecutionPoint): Promise { + const { id: point, sessionId } = await this.breakdownSupplementalIdAndSession(transformedPoint); const { target } = await client.Debugger.findStepInTarget({ point }, sessionId); return target; } - async findStepOutTarget(point: ExecutionPoint): Promise { - const sessionId = await this.waitForSession(); + async findStepOutTarget(transformedPoint: ExecutionPoint): Promise { + const { id: point, sessionId } = await this.breakdownSupplementalIdAndSession(transformedPoint); const { target } = await client.Debugger.findStepOutTarget({ point }, sessionId); return target; } - async findStepOverTarget(point: ExecutionPoint): Promise { - const sessionId = await this.waitForSession(); + async findStepOverTarget(transformedPoint: ExecutionPoint): Promise { + const { id: point, sessionId } = await this.breakdownSupplementalIdAndSession(transformedPoint); const { target } = await client.Debugger.findStepOverTarget({ point }, sessionId); return target; } - async findReverseStepOverTarget(point: ExecutionPoint): Promise { - const sessionId = await this.waitForSession(); + async findReverseStepOverTarget(transformedPoint: ExecutionPoint): Promise { + const { id: point, sessionId } = await this.breakdownSupplementalIdAndSession(transformedPoint); const { target } = await client.Debugger.findReverseStepOverTarget({ point }, sessionId); return target; } @@ -596,13 +607,13 @@ export class ReplayClient implements ReplayClientInterface { } async getAllFrames(pauseId: PauseId): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); const result = await client.Pause.getAllFrames({}, sessionId, pauseId); return result; } - async getPointStack(point: ExecutionPoint, maxCount: number): Promise { - const sessionId = await this.waitForSession(); + async getPointStack(transformedPoint: ExecutionPoint, maxCount: number): Promise { + const { id: point, sessionId } = await this.breakdownSupplementalIdAndSession(transformedPoint); const result = await client.Session.getPointStack({ point, maxCount }, sessionId); return result.frames; } @@ -658,7 +669,7 @@ export class ReplayClient implements ReplayClientInterface { } async getTopFrame(pauseId: PauseId): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); const result = await client.Pause.getTopFrame({}, sessionId, pauseId); return result; } @@ -676,12 +687,12 @@ export class ReplayClient implements ReplayClientInterface { } async getAllBoundingClientRects(pauseId: string): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); return client.DOM.getAllBoundingClientRects({}, sessionId, pauseId); } async getAppliedRules(pauseId: string, nodeId: string): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); return client.CSS.getAppliedRules({ node: nodeId }, sessionId, pauseId); } @@ -689,37 +700,37 @@ export class ReplayClient implements ReplayClientInterface { pauseId: string, nodeId: string ): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); return client.DOM.getBoundingClientRect({ node: nodeId }, sessionId, pauseId); } async getBoxModel(pauseId: string, nodeId: string): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); return client.DOM.getBoxModel({ node: nodeId }, sessionId, pauseId); } async getComputedStyle(pauseId: PauseId, nodeId: string): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); return client.CSS.getComputedStyle({ node: nodeId }, sessionId, pauseId); } async getDocument(pauseId: string): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); return client.DOM.getDocument({}, sessionId, pauseId); } async getEventListeners(pauseId: string, nodeId: string): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); return client.DOM.getEventListeners({ node: nodeId }, sessionId, pauseId); } async getParentNodes(pauseId: string, nodeId: string): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); return client.DOM.getParentNodes({ node: nodeId }, sessionId, pauseId); } async performSearch(pauseId: string, query: string): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); return client.DOM.performSearch({ query }, sessionId, pauseId); } @@ -728,7 +739,7 @@ export class ReplayClient implements ReplayClientInterface { nodeId: string, selector: string ): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); return client.DOM.querySelector({ node: nodeId, selector }, sessionId, pauseId); } @@ -755,7 +766,7 @@ export class ReplayClient implements ReplayClientInterface { } async getExceptionValue(pauseId: PauseId): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); return client.Pause.getExceptionValue({}, sessionId, pauseId); } @@ -768,7 +779,7 @@ export class ReplayClient implements ReplayClientInterface { } async getFrameSteps(pauseId: PauseId, frameId: FrameId): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); const { steps } = await client.Pause.getFrameSteps({ frameId }, sessionId, pauseId); return steps; } @@ -778,7 +789,7 @@ export class ReplayClient implements ReplayClientInterface { pauseId: PauseId, propertyName: string ): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); const { result } = await client.Pause.getObjectProperty( { object: objectId, @@ -795,7 +806,7 @@ export class ReplayClient implements ReplayClientInterface { pauseId: PauseId, level?: ObjectPreviewLevel ): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); const result = await client.Pause.getObjectPreview( { level, object: objectId }, sessionId, @@ -823,7 +834,7 @@ export class ReplayClient implements ReplayClientInterface { } async getScope(pauseId: PauseId, scopeId: ScopeId): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); const result = await client.Pause.getScope({ scope: scopeId }, sessionId, pauseId); return result; } @@ -834,8 +845,8 @@ export class ReplayClient implements ReplayClientInterface { return map; } - async getScreenshot(point: ExecutionPoint): Promise { - const sessionId = await this.waitForSession(); + async getScreenshot(transformedPoint: ExecutionPoint): Promise { + const { id: point, sessionId } = await this.breakdownSupplementalIdAndSession(transformedPoint); const { screen } = await client.Graphics.getPaintContents( { point, mimeType: "image/jpeg" }, sessionId @@ -843,8 +854,8 @@ export class ReplayClient implements ReplayClientInterface { return screen; } - async mapExpressionToGeneratedScope(expression: string, location: Location): Promise { - const sessionId = await this.waitForSession(); + async mapExpressionToGeneratedScope(expression: string, transformedLocation: Location): Promise { + const { location, sessionId } = await this.breakdownSupplementalLocation(transformedLocation); const result = await client.Debugger.mapExpressionToGeneratedScope( { expression, location }, sessionId @@ -900,8 +911,8 @@ export class ReplayClient implements ReplayClientInterface { return lineLocations!; } - async getMappedLocation(location: Location): Promise { - const sessionId = await this.waitForSession(); + async getMappedLocation(transformedLocation: Location): Promise { + const { location, sessionId } = await this.breakdownSupplementalLocation(transformedLocation); const { mappedLocation } = await client.Debugger.getMappedLocation({ location }, sessionId); return mappedLocation; } @@ -933,7 +944,7 @@ export class ReplayClient implements ReplayClientInterface { } async repaintGraphics(pauseId: PauseId): Promise { - const sessionId = await this.waitForSession(); + const sessionId = await this.getPauseSessionId(pauseId); return client.DOM.repaintGraphics({}, sessionId, pauseId); } @@ -1084,7 +1095,7 @@ export class ReplayClient implements ReplayClientInterface { }, onResults: (results: RunEvaluationResult[]) => void ): Promise { - const sessionId = await this.waitForSession(); + const { pointSelector, sessionId } = await this.breakdownSupplementalPointSelector(opts.selector); const runEvaluationId = String(this.nextRunEvaluationId++); const pointLimits: PointPageLimits = opts.limits ? { ...opts.limits } : {}; if (!pointLimits.maxCount) { @@ -1114,7 +1125,7 @@ export class ReplayClient implements ReplayClientInterface { frameIndex: opts.frameIndex, fullReturnedPropertyPreview: opts.fullPropertyPreview, pointLimits, - pointSelector: opts.selector, + pointSelector, runEvaluationId, shareProcesses: opts.shareProcesses, }, From e79961c693da50a3d87e99e4683a0649fccfa86d Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Fri, 13 Sep 2024 08:22:03 -0700 Subject: [PATCH 09/18] wip --- .../components/sources/utils/points.ts | 5 +- packages/replay-next/src/utils/time.ts | 5 +- packages/shared/client/ReplayClient.ts | 59 ++++++++++++++++++- .../SecondaryPanes/FrameTimeline.tsx | 9 ++- 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/packages/replay-next/components/sources/utils/points.ts b/packages/replay-next/components/sources/utils/points.ts index 6ad219ca612..e18b1a70325 100644 --- a/packages/replay-next/components/sources/utils/points.ts +++ b/packages/replay-next/components/sources/utils/points.ts @@ -1,6 +1,6 @@ import { ExecutionPoint, SourceId, TimeStampedPoint } from "@replayio/protocol"; -import { binarySearch, compareTimeStampedPoints, sameSupplementalIndex } from "protocol/utils"; +import { binarySearch, compareTimeStampedPoints, breakdownSupplementalId } from "protocol/utils"; import { isExecutionPointsGreaterThan, isExecutionPointsLessThan, @@ -27,7 +27,8 @@ export function findClosestHitPoint( return [hitPoint, index]; } - if (!sameSupplementalIndex(executionPoint.point, hitPoint.point)) { + if (breakdownSupplementalId(executionPoint.point).supplementalIndex || + breakdownSupplementalId(hitPoint.point).supplementalIndex) { return [hitPoint, index]; } diff --git a/packages/replay-next/src/utils/time.ts b/packages/replay-next/src/utils/time.ts index d95da91e4b3..96af4bc69e4 100644 --- a/packages/replay-next/src/utils/time.ts +++ b/packages/replay-next/src/utils/time.ts @@ -6,7 +6,7 @@ import differenceInWeeks from "date-fns/differenceInWeeks"; import differenceInYears from "date-fns/differenceInYears"; import padStart from "lodash/padStart"; import prettyMilliseconds from "pretty-ms"; -import { compareExecutionPoints as baseCompareExecutionPoints } from "protocol/utils"; +import { compareExecutionPoints as baseCompareExecutionPoints, sameSupplementalIndex } from "protocol/utils"; export const compareExecutionPoints = baseCompareExecutionPoints; @@ -23,6 +23,9 @@ export function isExecutionPointsWithinRange( beginPoint: ExecutionPoint, endPoint: ExecutionPoint ): boolean { + if (!sameSupplementalIndex(point, beginPoint)) { + return true; + } return !( isExecutionPointsLessThan(point, beginPoint) || isExecutionPointsGreaterThan(point, endPoint) ); diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index 7d09fbbd5ff..a4465ad25a5 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -177,6 +177,15 @@ export class ReplayClient implements ReplayClientInterface { return sessionId || await this.waitForSession(); } + private getSessionIdSupplementalIndex(sessionId: string) { + for (let i = 0; i < this.supplemental.length; i++) { + if (sessionId == this.supplemental[i].sessionId) { + return i + 1; + } + } + return 0; + } + get loadedRegions(): LoadedRegions | null { return this._loadedRegions; } @@ -234,6 +243,7 @@ export class ReplayClient implements ReplayClientInterface { sessionId, pauseId ); + this.transformSupplementalPauseData(response.result.data, sessionId); return response.result; } else { const response = await client.Pause.evaluateInFrame( @@ -246,6 +256,7 @@ export class ReplayClient implements ReplayClientInterface { sessionId, pauseId ); + this.transformSupplementalPauseData(response.result.data, sessionId); return response.result; } } @@ -558,24 +569,28 @@ export class ReplayClient implements ReplayClientInterface { async findStepInTarget(transformedPoint: ExecutionPoint): Promise { const { id: point, sessionId } = await this.breakdownSupplementalIdAndSession(transformedPoint); const { target } = await client.Debugger.findStepInTarget({ point }, sessionId); + this.transformSupplementalPointDescription(target, sessionId); return target; } async findStepOutTarget(transformedPoint: ExecutionPoint): Promise { const { id: point, sessionId } = await this.breakdownSupplementalIdAndSession(transformedPoint); const { target } = await client.Debugger.findStepOutTarget({ point }, sessionId); + this.transformSupplementalPointDescription(target, sessionId); return target; } async findStepOverTarget(transformedPoint: ExecutionPoint): Promise { const { id: point, sessionId } = await this.breakdownSupplementalIdAndSession(transformedPoint); const { target } = await client.Debugger.findStepOverTarget({ point }, sessionId); + this.transformSupplementalPointDescription(target, sessionId); return target; } async findReverseStepOverTarget(transformedPoint: ExecutionPoint): Promise { const { id: point, sessionId } = await this.breakdownSupplementalIdAndSession(transformedPoint); const { target } = await client.Debugger.findReverseStepOverTarget({ point }, sessionId); + this.transformSupplementalPointDescription(target, sessionId); return target; } @@ -606,9 +621,43 @@ export class ReplayClient implements ReplayClientInterface { return sources; } + private transformSupplementalLocation(location: Location, supplementalIndex: number) { + location.sourceId = transformSupplementalId(location.sourceId, supplementalIndex); + } + + private transformSupplementalMappedLocation(mappedLocation: MappedLocation | undefined, supplementalIndex: number) { + for (const location of mappedLocation || []) { + this.transformSupplementalLocation(location, supplementalIndex); + } + } + + private transformSupplementalPauseData(data: PauseData, sessionId: string) { + const supplementalIndex = this.getSessionIdSupplementalIndex(sessionId); + if (!supplementalIndex) { + return; + } + for (const frame of data.frames || []) { + this.transformSupplementalMappedLocation(frame.location, supplementalIndex); + this.transformSupplementalMappedLocation(frame.functionLocation, supplementalIndex); + } + for (const object of data.objects || []) { + this.transformSupplementalMappedLocation(object.preview?.functionLocation, supplementalIndex); + } + } + + private transformSupplementalPointDescription(point: PointDescription, sessionId: string) { + const supplementalIndex = this.getSessionIdSupplementalIndex(sessionId); + if (!supplementalIndex) { + return; + } + point.point = transformSupplementalId(point.point, supplementalIndex); + this.transformSupplementalMappedLocation(point.frame, supplementalIndex); + } + async getAllFrames(pauseId: PauseId): Promise { const sessionId = await this.getPauseSessionId(pauseId); const result = await client.Pause.getAllFrames({}, sessionId, pauseId); + this.transformSupplementalPauseData(result.data, sessionId); return result; } @@ -671,6 +720,7 @@ export class ReplayClient implements ReplayClientInterface { async getTopFrame(pauseId: PauseId): Promise { const sessionId = await this.getPauseSessionId(pauseId); const result = await client.Pause.getTopFrame({}, sessionId, pauseId); + this.transformSupplementalPauseData(result.data, sessionId); return result; } @@ -767,7 +817,9 @@ export class ReplayClient implements ReplayClientInterface { async getExceptionValue(pauseId: PauseId): Promise { const sessionId = await this.getPauseSessionId(pauseId); - return client.Pause.getExceptionValue({}, sessionId, pauseId); + const result = await client.Pause.getExceptionValue({}, sessionId, pauseId); + this.transformSupplementalPauseData(result.data, sessionId); + return result; } private async syncFocusWindow(): Promise { @@ -781,6 +833,9 @@ export class ReplayClient implements ReplayClientInterface { async getFrameSteps(pauseId: PauseId, frameId: FrameId): Promise { const sessionId = await this.getPauseSessionId(pauseId); const { steps } = await client.Pause.getFrameSteps({ frameId }, sessionId, pauseId); + for (const step of steps) { + this.transformSupplementalPointDescription(step, sessionId); + } return steps; } @@ -798,6 +853,7 @@ export class ReplayClient implements ReplayClientInterface { sessionId, pauseId ); + this.transformSupplementalPauseData(result.data, sessionId); return result; } @@ -812,6 +868,7 @@ export class ReplayClient implements ReplayClientInterface { sessionId, pauseId || undefined ); + this.transformSupplementalPauseData(result.data, sessionId); return result.data; } diff --git a/src/devtools/client/debugger/src/components/SecondaryPanes/FrameTimeline.tsx b/src/devtools/client/debugger/src/components/SecondaryPanes/FrameTimeline.tsx index dd4d526898c..60f9fb96a4d 100644 --- a/src/devtools/client/debugger/src/components/SecondaryPanes/FrameTimeline.tsx +++ b/src/devtools/client/debugger/src/components/SecondaryPanes/FrameTimeline.tsx @@ -32,9 +32,11 @@ import { PauseAndFrameId, PauseFrame, getExecutionPoint, + getTime, getSelectedFrameId, } from "../../reducers/pause"; import { getSelectedFrameSuspense } from "../../selectors/pause"; +import { compareTimeStampedPoints } from "protocol/utils"; function getBoundingClientRect(element?: HTMLElement) { if (!element) { @@ -51,6 +53,7 @@ interface FrameTimelineState { interface FrameTimelineProps { executionPoint: string | null; + time: number; selectedLocation: PartialLocation | null; selectedFrame: PauseFrame | null; frameSteps: PointDescription[] | undefined; @@ -176,7 +179,7 @@ class FrameTimelineRenderer extends Component BigInt(position.point) <= BigInt(executionPoint) + position => compareTimeStampedPoints(position, { point: executionPoint, time }) <= 0 ); // Check if the current executionPoint's corresponding index is similar to the @@ -236,6 +239,7 @@ function FrameTimeline({ selectedFrameId }: { selectedFrameId: PauseAndFrameId | const replayClient = useContext(ReplayClientContext); const sourcesState = useAppSelector(state => state.sources); const executionPoint = useAppSelector(getExecutionPoint); + const time = useAppSelector(getTime); const selectedLocation = useAppSelector(getSelectedLocation); const selectedFrame = useAppSelector(state => getSelectedFrameSuspense(replayClient, state, selectedFrameId) @@ -256,6 +260,7 @@ function FrameTimeline({ selectedFrameId }: { selectedFrameId: PauseAndFrameId | return ( Date: Fri, 13 Sep 2024 14:23:52 -0700 Subject: [PATCH 10/18] wip --- packages/protocol/utils.ts | 3 ++ packages/shared/client/ReplayClient.ts | 48 +++++++++++++++++++++++--- packages/shared/client/types.ts | 12 +++++-- src/ui/actions/session.ts | 43 +++++++++++++++++++---- 4 files changed, 93 insertions(+), 13 deletions(-) diff --git a/packages/protocol/utils.ts b/packages/protocol/utils.ts index 6249c2ff2c7..487efbc5190 100644 --- a/packages/protocol/utils.ts +++ b/packages/protocol/utils.ts @@ -141,6 +141,9 @@ export class ArrayMap { } export function transformSupplementalId(id: string, supplementalIndex: number) { + if (!supplementalIndex) { + return id; + } return `s${supplementalIndex}-${id}`; } diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index a4465ad25a5..48b6037ff88 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -113,6 +113,7 @@ export class ReplayClient implements ReplayClientInterface { private sessionWaiter = defer(); private supplemental: SupplementalSession[] = []; + private supplementalTimeDeltas: number[] = []; private pauseIdToSessionId = new Map(); @@ -136,11 +137,16 @@ export class ReplayClient implements ReplayClientInterface { // Configures the client to use an already initialized session iD. // This method should be used for apps that use the protocol package directly. // Apps that only communicate with the Replay protocol through this client should use the initialize method instead. - async configure(sessionId: string, supplemental: SupplementalSession[]): Promise { + async configure(recordingId: string, sessionId: string, supplemental: SupplementalSession[]): Promise { + this._recordingId = recordingId; this._sessionId = sessionId; this._dispatchEvent("sessionCreated"); this.sessionWaiter.resolve(sessionId); this.supplemental.push(...supplemental); + for (const supplementalSession of supplemental) { + const timeDelta = computeSupplementalTimeDelta(recordingId, supplementalSession); + this.supplementalTimeDeltas.push(timeDelta); + } await this.syncFocusWindow(); } @@ -560,10 +566,8 @@ export class ReplayClient implements ReplayClientInterface { } points.sort((a, b) => compareExecutionPoints(a.point, b.point)); - return points.map(desc => { - const point = transformSupplementalId(desc.point, supplementalIndex); - return { ...desc, point }; - }); + points.forEach(desc => this.transformSupplementalPointDescription(desc, sessionId)); + return points; } async findStepInTarget(transformedPoint: ExecutionPoint): Promise { @@ -645,12 +649,24 @@ export class ReplayClient implements ReplayClientInterface { } } + // Convert a time from either the main or a supplemental recording into a time + // for the main recording. + private normalizeSupplementalTime(time: number, supplementalIndex: number) { + if (!supplementalIndex) { + return 0; + } + const delta = this.supplementalTimeDeltas[supplementalIndex - 1]; + assert(typeof delta == "number"); + return time - delta; + } + private transformSupplementalPointDescription(point: PointDescription, sessionId: string) { const supplementalIndex = this.getSessionIdSupplementalIndex(sessionId); if (!supplementalIndex) { return; } point.point = transformSupplementalId(point.point, supplementalIndex); + point.time = this.normalizeSupplementalTime(point.time, supplementalIndex); this.transformSupplementalMappedLocation(point.frame, supplementalIndex); } @@ -1352,3 +1368,25 @@ function waitForOpenConnection( }, intervalMs); }); } + +function computeSupplementalTimeDelta(recordingId: string, supplemental: SupplementalSession) { + let minDelta: number | undefined; + let maxDelta: number | undefined; + for (const { clientFirst, clientRecordingId, clientPoint, serverPoint } of supplemental.connections) { + assert(recordingId == clientRecordingId); + const delta = serverPoint.time - clientPoint.time; + if (clientFirst) { + if (typeof maxDelta == "undefined" || delta > maxDelta) { + maxDelta = delta; + } + } else { + if (typeof minDelta == "undefined" || delta < minDelta) { + minDelta = delta; + } + } + } + assert(typeof minDelta != "undefined"); + assert(typeof maxDelta != "undefined"); + assert(minDelta <= maxDelta); + return (minDelta + maxDelta) / 2; +} diff --git a/packages/shared/client/types.ts b/packages/shared/client/types.ts index ed730a739e8..bdad963d993 100644 --- a/packages/shared/client/types.ts +++ b/packages/shared/client/types.ts @@ -162,8 +162,16 @@ export interface TimeStampedPointWithPaintHash extends TimeStampedPoint { export type AnnotationListener = (annotation: Annotation) => void; +export interface SupplementalRecordingConnection { + clientFirst: boolean; + clientRecordingId: string; + clientPoint: TimeStampedPoint; + serverPoint: TimeStampedPoint; +} + export interface SupplementalRecording { - recordingId: string; + serverRecordingId: string; + connections: SupplementalRecordingConnection[]; } export interface SupplementalSession extends SupplementalRecording { @@ -173,7 +181,7 @@ export interface SupplementalSession extends SupplementalRecording { export interface ReplayClientInterface { get loadedRegions(): LoadedRegions | null; addEventListener(type: ReplayClientEvents, handler: Function): void; - configure(sessionId: string, supplemental: SupplementalSession[]): Promise; + configure(recordingId: string, sessionId: string, supplemental: SupplementalSession[]): Promise; createPause(executionPoint: ExecutionPoint): Promise; evaluateExpression( pauseId: PauseId, diff --git a/src/ui/actions/session.ts b/src/ui/actions/session.ts index 1bcbd5b7d3a..6f829a3d98f 100644 --- a/src/ui/actions/session.ts +++ b/src/ui/actions/session.ts @@ -94,7 +94,37 @@ export function getAccessibleRecording( function getSupplementalRecordings(recordingId: string): SupplementalRecording[] { switch (recordingId) { case "d5513383-5986-4de5-ab9d-2a7e1f367e90": - return [{ recordingId: "c54962d6-9ac6-428a-a6af-2bb2bf6633ca" }]; + return [{ + serverRecordingId: "c54962d6-9ac6-428a-a6af-2bb2bf6633ca", + connections: [ + // First /public network call is made. + { + clientFirst: true, + clientRecordingId: "d5513383-5986-4de5-ab9d-2a7e1f367e90", + clientPoint: { + point: "16225927684179678680302888878605214", + time: 10800, + }, + serverPoint: { + point: "17848520611783618390044493509296165", + time: 53985.21055489196, + }, + }, + // First /public network call returns. + { + clientFirst: false, + clientRecordingId: "d5513383-5986-4de5-ab9d-2a7e1f367e90", + clientPoint: { + point: "18173039006159032102657777614192642", + time: 11394.013505789708, + }, + serverPoint: { + point: "17848520612057445317839151914025709", + time: 54294.69953306548, + }, + }, + ], + }]; } return []; } @@ -366,10 +396,11 @@ export function createSocket(recordingId: string): UIThunkAction { console.log("MainSessionId", JSON.stringify({ recordingId, sessionId })); const supplementalRecordings = getSupplementalRecordings(recordingId); - const supplemental = await Promise.all(supplementalRecordings.map(async ({ recordingId }) => { - const sessionId = await doCreateSession(recordingId); - console.log("SupplementalSessionId", JSON.stringify({ recordingId, sessionId })); - return { recordingId, sessionId }; + const supplemental = await Promise.all(supplementalRecordings.map(async supplemental => { + const { serverRecordingId } = supplemental; + const sessionId = await doCreateSession(serverRecordingId); + console.log("SupplementalSessionId", JSON.stringify({ serverRecordingId, sessionId })); + return { ...supplemental, sessionId }; })); Sentry.configureScope(scope => { @@ -377,7 +408,7 @@ export function createSocket(recordingId: string): UIThunkAction { }); window.sessionId = sessionId; - await replayClient.configure(sessionId, supplemental); + await replayClient.configure(recordingId, sessionId, supplemental); const recordingTarget = await recordingTargetCache.readAsync(replayClient); dispatch(actions.setRecordingTarget(recordingTarget)); From 89a71644727f56f6318634b5d79edaec9a866eef Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Fri, 13 Sep 2024 15:27:20 -0700 Subject: [PATCH 11/18] wip --- packages/protocol/utils.ts | 10 ------- .../src/suspense/FocusIntervalCache.ts | 26 +++++++++++++--- packages/shared/client/ReplayClient.ts | 30 +++++++++++++++---- 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/packages/protocol/utils.ts b/packages/protocol/utils.ts index 487efbc5190..25a33a31153 100644 --- a/packages/protocol/utils.ts +++ b/packages/protocol/utils.ts @@ -161,16 +161,6 @@ export function sameSupplementalIndex(idA: string, idB: string) { return breakdownSupplementalId(idA).supplementalIndex == breakdownSupplementalId(idB).supplementalIndex; } -const SupplementalNumericStringShift = 200; - -export function transformSupplementalNumericString(v: string, supplementalIndex: number): string { - assert(BigInt(v) < BigInt(1) << BigInt(SupplementalNumericStringShift)); - if (!supplementalIndex) { - return v; - } - return (BigInt(v) | (BigInt(1) << BigInt(SupplementalNumericStringShift))).toString(); -} - /* * Compare 2 integers encoded as numeric strings, because we want to avoid using BigInt (for now). * This will only work correctly if both strings encode positive integers (without decimal places), diff --git a/packages/replay-next/src/suspense/FocusIntervalCache.ts b/packages/replay-next/src/suspense/FocusIntervalCache.ts index 892724c7494..39e4278c4b2 100644 --- a/packages/replay-next/src/suspense/FocusIntervalCache.ts +++ b/packages/replay-next/src/suspense/FocusIntervalCache.ts @@ -7,7 +7,7 @@ import { } from "suspense"; import { ProtocolError, isCommandError } from "shared/utils/error"; -import { breakdownSupplementalId, transformSupplementalNumericString } from "protocol/utils"; +import { assert, breakdownSupplementalId } from "protocol/utils"; type Options, Value> = { debugLabel?: string; @@ -21,6 +21,26 @@ type Options, Value> = ) => PromiseLike> | Array; }; +const gSupplementalFocusPointMap = new Map(); + +// This is an especially stupid hack to get the focus interval cache to include +// points from other recordings. As long as the focus interval contains the start +// of the recording we treat points in supplemental recordings as having a low +// yet unique (to avoid deduping) execution point that will be part of the +// focus window in the main recording. +function getSupplementalFocusIntervalPoint(transformedPoint: ExecutionPoint): bigint { + const { supplementalIndex } = breakdownSupplementalId(transformedPoint); + if (!supplementalIndex) { + return BigInt(transformedPoint); + } + if (!gSupplementalFocusPointMap.has(transformedPoint)) { + gSupplementalFocusPointMap.set(transformedPoint, gSupplementalFocusPointMap.size); + } + let existing = gSupplementalFocusPointMap.get(transformedPoint); + assert(typeof existing == "number"); + return BigInt(existing); +} + // Convenience wrapper around createFocusIntervalCache that converts BigInts to ExecutionPoints (strings) export function createFocusIntervalCacheForExecutionPoints, Value>( options: Omit, "getPointForValue" | "load"> & { @@ -37,9 +57,7 @@ export function createFocusIntervalCacheForExecutionPoints { const transformedPoint = getPointForValue(value); - const { id: point, supplementalIndex } = breakdownSupplementalId(transformedPoint); - const newPoint = transformSupplementalNumericString(point, supplementalIndex); - return BigInt(newPoint); + return getSupplementalFocusIntervalPoint(transformedPoint); }, load: (start: bigint, end: bigint, ...params: [...Params, IntervalCacheLoadOptions]) => load(start.toString(), end.toString(), ...params), diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index 48b6037ff88..b2aa94c19c6 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -519,6 +519,22 @@ export class ReplayClient implements ReplayClientInterface { assert(commonSessionId); return { pointSelector: { ...pointSelector, locations }, sessionId: commonSessionId, supplementalIndex: commonSupplementalIndex }; } + case "points": { + let commonSessionId: string | undefined; + let commonSupplementalIndex = 0; + const points = await Promise.all(pointSelector.points.map(async transformedPoint => { + const { id: point, sessionId, supplementalIndex } = await this.breakdownSupplementalIdAndSession(transformedPoint); + if (commonSessionId) { + assert(commonSessionId == sessionId); + } else { + commonSessionId = sessionId; + commonSupplementalIndex = supplementalIndex; + } + return point; + })); + assert(commonSessionId); + return { pointSelector: { ...pointSelector, points }, sessionId: commonSessionId, supplementalIndex: commonSupplementalIndex }; + } default: return { pointSelector, sessionId: await this.waitForSession(), supplementalIndex: 0 }; } @@ -528,7 +544,7 @@ export class ReplayClient implements ReplayClientInterface { transformedPointSelector: PointSelector, pointLimits?: PointPageLimits ): Promise { - const { pointSelector, sessionId, supplementalIndex } = await this.breakdownSupplementalPointSelector(transformedPointSelector); + const { pointSelector, sessionId } = await this.breakdownSupplementalPointSelector(transformedPointSelector); const points: PointDescription[] = []; const findPointsId = String(this.nextFindPointsId++); @@ -1168,9 +1184,9 @@ export class ReplayClient implements ReplayClientInterface { }, onResults: (results: RunEvaluationResult[]) => void ): Promise { - const { pointSelector, sessionId } = await this.breakdownSupplementalPointSelector(opts.selector); + const { pointSelector, sessionId, supplementalIndex } = await this.breakdownSupplementalPointSelector(opts.selector); const runEvaluationId = String(this.nextRunEvaluationId++); - const pointLimits: PointPageLimits = opts.limits ? { ...opts.limits } : {}; + const pointLimits: PointPageLimits = (opts.limits && !supplementalIndex) ? { ...opts.limits } : {}; if (!pointLimits.maxCount) { pointLimits.maxCount = MAX_POINTS_TO_RUN_EVALUATION; } @@ -1181,11 +1197,15 @@ export class ReplayClient implements ReplayClientInterface { : null ); - function onResultsWrapper(results: runEvaluationResults) { + const onResultsWrapper = (results: runEvaluationResults) => { if (results.runEvaluationId === runEvaluationId) { + for (const result of results.results) { + this.transformSupplementalPointDescription(result.point, sessionId); + this.transformSupplementalPauseData(result.data, sessionId); + } onResults(results.results); } - } + }; addEventListener("Session.runEvaluationResults", onResultsWrapper); From 0d7b0f95a5c250abea932ab56847c5f53345f734 Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Fri, 13 Sep 2024 16:09:51 -0700 Subject: [PATCH 12/18] wip --- packages/shared/client/ReplayClient.ts | 36 +++++++++++++++++++------- src/ui/actions/session.ts | 26 +++++++++++++++++++ 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index b2aa94c19c6..c5d9e23f9a6 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -1389,24 +1389,40 @@ function waitForOpenConnection( }); } +// Compute the delta to adjust the timestamps for a supplemental server recording. +// A delta is the time difference between the base recording and the supplemental +// recording: a smaller delta moves the supplemental recording later, and a larger +// delta moves the supplemental recording closer. +// +// We want to find a delta that is small enough that all client-first connections +// happen on the client before they happen on the server, and large enough that +// all client-last connections happen on the server before they happen on the client. function computeSupplementalTimeDelta(recordingId: string, supplemental: SupplementalSession) { - let minDelta: number | undefined; - let maxDelta: number | undefined; + // Delta which ensures that all clientFirst connections happen + // on the client before they happen on the server. + let clientFirstDelta: number | undefined; + + // Delta which ensures that all clientLast connections happen + // on the server before they happen on the client. + let clientLastDelta: number | undefined; + for (const { clientFirst, clientRecordingId, clientPoint, serverPoint } of supplemental.connections) { assert(recordingId == clientRecordingId); const delta = serverPoint.time - clientPoint.time; + console.log("FoundDelta", delta, clientFirst); if (clientFirst) { - if (typeof maxDelta == "undefined" || delta > maxDelta) { - maxDelta = delta; + if (typeof clientFirstDelta == "undefined" || delta < clientFirstDelta) { + clientFirstDelta = delta; } } else { - if (typeof minDelta == "undefined" || delta < minDelta) { - minDelta = delta; + if (typeof clientLastDelta == "undefined" || delta > clientLastDelta) { + clientLastDelta = delta; } } } - assert(typeof minDelta != "undefined"); - assert(typeof maxDelta != "undefined"); - assert(minDelta <= maxDelta); - return (minDelta + maxDelta) / 2; + console.log("Deltas", clientFirstDelta, clientLastDelta); + assert(typeof clientFirstDelta != "undefined"); + assert(typeof clientLastDelta != "undefined"); + assert(clientFirstDelta >= clientLastDelta); + return (clientFirstDelta + clientLastDelta) / 2; } diff --git a/src/ui/actions/session.ts b/src/ui/actions/session.ts index 6f829a3d98f..f74f0ad26a4 100644 --- a/src/ui/actions/session.ts +++ b/src/ui/actions/session.ts @@ -123,6 +123,32 @@ function getSupplementalRecordings(recordingId: string): SupplementalRecording[] time: 54294.69953306548, }, }, + // Second /public network call is made. + { + clientFirst: true, + clientRecordingId: "d5513383-5986-4de5-ab9d-2a7e1f367e90", + clientPoint: { + point: "56141709792928477884264450686451782", + time: 25301.991150442478, + }, + serverPoint: { + point: "20444669041365036140696939482054693", + time: 66999.47944537815, + }, + }, + // Second /public network call returns. + { + clientFirst: false, + clientRecordingId: "d5513383-5986-4de5-ab9d-2a7e1f367e90", + clientPoint: { + point: "56790746901441151898702525872734210", + time: 25524.865255979654, + }, + serverPoint: { + point: "20444669041638352324265057053573869", + time: 68611.1162184874, + }, + }, ], }]; } From 748334148ee0876781379e88a0b8389c5a19bcda Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Sat, 14 Sep 2024 07:09:18 -0700 Subject: [PATCH 13/18] wip --- packages/shared/client/ReplayClient.ts | 64 ++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index c5d9e23f9a6..0c4068e95e5 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -113,7 +113,7 @@ export class ReplayClient implements ReplayClientInterface { private sessionWaiter = defer(); private supplemental: SupplementalSession[] = []; - private supplementalTimeDeltas: number[] = []; + private supplementalTimeDeltas: (number | undefined)[] = []; private pauseIdToSessionId = new Map(); @@ -672,8 +672,13 @@ export class ReplayClient implements ReplayClientInterface { return 0; } const delta = this.supplementalTimeDeltas[supplementalIndex - 1]; - assert(typeof delta == "number"); - return time - delta; + if (typeof delta == "number") { + return time - delta; + } + const supplementalSession = this.supplemental[supplementalIndex - 1]; + assert(this._recordingId); + assert(supplementalSession); + return interpolateSupplementalTime(this._recordingId, supplementalSession, time); } private transformSupplementalPointDescription(point: PointDescription, sessionId: string) { @@ -1397,7 +1402,10 @@ function waitForOpenConnection( // We want to find a delta that is small enough that all client-first connections // happen on the client before they happen on the server, and large enough that // all client-last connections happen on the server before they happen on the client. -function computeSupplementalTimeDelta(recordingId: string, supplemental: SupplementalSession) { +// +// If there is no such delta then we're seeing inconsistent timing information with +// the connections and will fall back onto interpolateSupplementalTime. +function computeSupplementalTimeDelta(recordingId: string, supplemental: SupplementalSession): number | undefined { // Delta which ensures that all clientFirst connections happen // on the client before they happen on the server. let clientFirstDelta: number | undefined; @@ -1423,6 +1431,52 @@ function computeSupplementalTimeDelta(recordingId: string, supplemental: Supplem console.log("Deltas", clientFirstDelta, clientLastDelta); assert(typeof clientFirstDelta != "undefined"); assert(typeof clientLastDelta != "undefined"); - assert(clientFirstDelta >= clientLastDelta); + + if (clientFirstDelta < clientLastDelta) { + // There is no single delta we'll be able to use. + return undefined; + } + return (clientFirstDelta + clientLastDelta) / 2; } + +// Use an interpolation strategy to normalize a time from a supplemental recording +// to a time in the base recording. +// +// This works even if there is no single consistent delta to use throughout the +// recording, and requires that events on either side of the connections happen +// in the same order in the two recordings. +function interpolateSupplementalTime(recordingId: string, supplemental: SupplementalSession, supplementalTime: number): number { + assert(supplemental.connections.length); + for (const connection of supplemental.connections) { + assert(connection.clientRecordingId == recordingId); + } + + // Check if the time happens between two connections. + for (let i = 1; i < supplemental.connections.length; i++) { + const previous = supplemental.connections[i - 1]; + const next = supplemental.connections[i]; + assert(previous.clientPoint.time <= next.clientPoint.time); + assert(previous.serverPoint.time <= next.serverPoint.time); + if (supplementalTime >= previous.serverPoint.time && + supplementalTime <= next.serverPoint.time) { + const clientElapsed = next.clientPoint.time - previous.clientPoint.time; + const serverElapsed = next.serverPoint.time - previous.serverPoint.time; + const fraction = (supplementalTime - previous.serverPoint.time) / serverElapsed; + return previous.clientPoint.time + fraction * clientElapsed; + } + } + + // Check if the time happened before the first connection. + const firstConnection = supplemental.connections[0]; + if (supplementalTime <= firstConnection.serverPoint.time) { + const delta = firstConnection.serverPoint.time - firstConnection.clientPoint.time; + return supplementalTime - delta; + } + + // The time must have happened after the last connection. + const lastConnection = supplemental.connections[supplemental.connections.length - 1]; + assert(supplementalTime >= lastConnection.serverPoint.time); + const delta = lastConnection.serverPoint.time - lastConnection.clientPoint.time; + return supplementalTime - delta; +} From b5a5ff29aec4079221e7909c93eddb506c3d2e5a Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Sat, 14 Sep 2024 07:37:34 -0700 Subject: [PATCH 14/18] wip --- packages/shared/client/ReplayClient.ts | 34 ++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index 0c4068e95e5..4b1e0d6e815 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -91,6 +91,7 @@ import { ReplayClientInterface, SourceLocationRange, SupplementalSession, + SupplementalRecordingConnection, TimeStampedPointWithPaintHash, } from "./types"; @@ -1417,7 +1418,6 @@ function computeSupplementalTimeDelta(recordingId: string, supplemental: Supplem for (const { clientFirst, clientRecordingId, clientPoint, serverPoint } of supplemental.connections) { assert(recordingId == clientRecordingId); const delta = serverPoint.time - clientPoint.time; - console.log("FoundDelta", delta, clientFirst); if (clientFirst) { if (typeof clientFirstDelta == "undefined" || delta < clientFirstDelta) { clientFirstDelta = delta; @@ -1428,7 +1428,6 @@ function computeSupplementalTimeDelta(recordingId: string, supplemental: Supplem } } } - console.log("Deltas", clientFirstDelta, clientLastDelta); assert(typeof clientFirstDelta != "undefined"); assert(typeof clientLastDelta != "undefined"); @@ -1440,6 +1439,26 @@ function computeSupplementalTimeDelta(recordingId: string, supplemental: Supplem return (clientFirstDelta + clientLastDelta) / 2; } +// If necessary add an artificial small time difference between client +// server points in a connection when interpolating. This ensures that +// client events in the connection actually happen before the server +// event on the timeline instead of at the exact same time, which can +// cause console messages to be rendered in the wrong order. +function adjustInterpolateSupplementalTime(connection: SupplementalRecordingConnection, clientTime: number) { + const { clientFirst, clientPoint } = connection; + const Epsilon = 0.1; + if (clientFirst) { + if (clientTime >= clientPoint.time && clientTime - clientPoint.time <= Epsilon) { + return clientPoint.time + Epsilon; + } + } else { + if (clientTime <= clientPoint.time && clientPoint.time - clientTime <= Epsilon) { + return clientPoint.time - Epsilon; + } + } + return clientTime; +} + // Use an interpolation strategy to normalize a time from a supplemental recording // to a time in the base recording. // @@ -1463,7 +1482,12 @@ function interpolateSupplementalTime(recordingId: string, supplemental: Suppleme const clientElapsed = next.clientPoint.time - previous.clientPoint.time; const serverElapsed = next.serverPoint.time - previous.serverPoint.time; const fraction = (supplementalTime - previous.serverPoint.time) / serverElapsed; - return previous.clientPoint.time + fraction * clientElapsed; + const clientTime = previous.clientPoint.time + fraction * clientElapsed; + const adjustPrevious = adjustInterpolateSupplementalTime(previous, clientTime); + if (adjustPrevious != clientTime) { + return adjustPrevious; + } + return adjustInterpolateSupplementalTime(next, clientTime); } } @@ -1471,12 +1495,12 @@ function interpolateSupplementalTime(recordingId: string, supplemental: Suppleme const firstConnection = supplemental.connections[0]; if (supplementalTime <= firstConnection.serverPoint.time) { const delta = firstConnection.serverPoint.time - firstConnection.clientPoint.time; - return supplementalTime - delta; + return adjustInterpolateSupplementalTime(firstConnection, supplementalTime - delta); } // The time must have happened after the last connection. const lastConnection = supplemental.connections[supplemental.connections.length - 1]; assert(supplementalTime >= lastConnection.serverPoint.time); const delta = lastConnection.serverPoint.time - lastConnection.clientPoint.time; - return supplementalTime - delta; + return adjustInterpolateSupplementalTime(lastConnection, supplementalTime - delta); } From c3c4a1f5e182a07e73a9fb7e66ccd51374a8a0aa Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Sat, 14 Sep 2024 07:58:45 -0700 Subject: [PATCH 15/18] wip --- .../client/debugger/src/components/shared/tree.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/devtools/client/debugger/src/components/shared/tree.tsx b/src/devtools/client/debugger/src/components/shared/tree.tsx index b555227af05..a1280c4760e 100644 --- a/src/devtools/client/debugger/src/components/shared/tree.tsx +++ b/src/devtools/client/debugger/src/components/shared/tree.tsx @@ -936,10 +936,17 @@ export class Tree extends React.Component, TreeState> { const traversal = this._dfsFromRoots(); const { active, focused } = this.props; - const nodes = traversal.map((v, i) => { + const seenKeys = new Set(); + + const nodes: JSX.Element[] = []; + traversal.forEach((v, i) => { const { item, depth } = v; const key = this.props.getKey(item); - return ( + if (seenKeys.has(key)) { + return; + } + seenKeys.add(key); + nodes.push( // We make a key unique depending on whether the tree node is in active // or inactive state to make sure that it is actually replaced and the From 4bbde6d4968c9dd86d3ed652d7c690d98b4b186a Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Sat, 14 Sep 2024 08:14:09 -0700 Subject: [PATCH 16/18] ignore browser-external sources --- .../debugger/src/utils/sources-tree/updateTree.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/devtools/client/debugger/src/utils/sources-tree/updateTree.ts b/src/devtools/client/debugger/src/utils/sources-tree/updateTree.ts index d01597d4e32..09b517cc818 100644 --- a/src/devtools/client/debugger/src/utils/sources-tree/updateTree.ts +++ b/src/devtools/client/debugger/src/utils/sources-tree/updateTree.ts @@ -62,7 +62,9 @@ export function updateTree({ newSources, prevSources, uncollapsedTree }: UpdateT // produce a fully immutable update. const newUncollapsedTree = createNextState(uncollapsedTree, draft => { for (const source of sourcesToAdd) { - addToTree(draft, source, debuggeeHost!); + if (!ignoreSource(source)) { + addToTree(draft, source, debuggeeHost!); + } } }); @@ -74,3 +76,10 @@ export function updateTree({ newSources, prevSources, uncollapsedTree }: UpdateT parentMap: createParentMap(newSourceTree), }; } + +function ignoreSource(source: SourceDetails) { + if (source.url?.startsWith("browser-external:")) { + return true; + } + return false; +} From b421e57f49b57d1263124bd54e06aa848c671e3c Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Sat, 14 Sep 2024 08:59:45 -0700 Subject: [PATCH 17/18] wip --- packages/shared/client/ReplayClient.ts | 78 +++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index 4b1e0d6e815..a4bcffba7ac 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -77,7 +77,7 @@ import throttle from "lodash/throttle"; import uniqueId from "lodash/uniqueId"; // eslint-disable-next-line no-restricted-imports -import { addEventListener, client, initSocket, removeEventListener } from "protocol/socket"; +import { addEventListener, client, initSocket, sendMessage, removeEventListener } from "protocol/socket"; import { assert, compareExecutionPoints, defer, waitForTime, transformSupplementalId, breakdownSupplementalId } from "protocol/utils"; import { initProtocolMessagesStore } from "replay-next/components/protocol/ProtocolMessagesStore"; import { insert } from "replay-next/src/utils/array"; @@ -168,15 +168,19 @@ export class ReplayClient implements ReplayClientInterface { } } - private async breakdownSupplementalIdAndSession(transformedId: string): Promise<{ id: string, sessionId: string, supplementalIndex: number }> { - const { id, supplementalIndex } = breakdownSupplementalId(transformedId); + private async getSupplementalIndexSession(supplementalIndex: number) { if (!supplementalIndex) { - const sessionId = await this.waitForSession(); - return { id, sessionId, supplementalIndex }; + return this.waitForSession(); } const supplementalInfo = this.supplemental[supplementalIndex - 1]; assert(supplementalInfo); - return { id, sessionId: supplementalInfo.sessionId, supplementalIndex }; + return supplementalInfo.sessionId; + } + + private async breakdownSupplementalIdAndSession(transformedId: string): Promise<{ id: string, sessionId: string, supplementalIndex: number }> { + const { id, supplementalIndex } = breakdownSupplementalId(transformedId); + const sessionId = await this.getSupplementalIndexSession(supplementalIndex); + return { id, sessionId, supplementalIndex }; } private async getPauseSessionId(pauseId: string): Promise { @@ -587,8 +591,68 @@ export class ReplayClient implements ReplayClientInterface { return points; } + private getSupplementalIndexRecordingId(supplementalIndex: number) { + if (!supplementalIndex) { + assert(this._recordingId); + return this._recordingId; + } + return this.supplemental[supplementalIndex - 1].serverRecordingId; + } + + private forAllConnections(callback: (serverRecordingId: string, connection: SupplementalRecordingConnection, supplementalIndex: number) => void) { + this.supplemental.forEach(({ serverRecordingId, connections }, i) => { + for (const connection of connections) { + callback(serverRecordingId, connection, i + 1); + } + }); + } + + private async maybeGetConnectionStepTarget(point: ExecutionPoint, pointSupplementalIndex: number): Promise { + const recordingId = this.getSupplementalIndexRecordingId(pointSupplementalIndex); + + console.log("STEP_IN", point, recordingId); + + let targetPoint: ExecutionPoint | undefined; + let targetSupplementalIndex = 0; + this.forAllConnections((serverRecordingId, connection, supplementalIndex) => { + const { clientFirst, clientRecordingId, clientPoint, serverPoint } = connection; + if (clientFirst) { + if (clientRecordingId == recordingId && clientPoint.point == point) { + targetPoint = serverPoint.point; + targetSupplementalIndex = supplementalIndex; + } + } else { + if (serverRecordingId == recordingId && serverPoint.point == point) { + assert(clientRecordingId == this._recordingId, "NYI"); + targetPoint = clientPoint.point; + targetSupplementalIndex = 0; + } + } + }); + + console.log("TARGET_POINT", targetPoint, targetSupplementalIndex); + + if (!targetPoint) { + return null; + } + + const sessionId = await this.getSupplementalIndexSession(targetSupplementalIndex); + + const response = await sendMessage("Session.getPointFrameSteps", { point: targetPoint }, sessionId); + const { steps } = response; + const desc = steps.find(step => step.point == targetPoint); + assert(desc); + + this.transformSupplementalPointDescription(desc, sessionId); + return { ...desc, reason: "step" }; + } + async findStepInTarget(transformedPoint: ExecutionPoint): Promise { - const { id: point, sessionId } = await this.breakdownSupplementalIdAndSession(transformedPoint); + const { id: point, sessionId, supplementalIndex } = await this.breakdownSupplementalIdAndSession(transformedPoint); + const connectionStepTarget = await this.maybeGetConnectionStepTarget(point, supplementalIndex); + if (connectionStepTarget) { + return connectionStepTarget; + } const { target } = await client.Debugger.findStepInTarget({ point }, sessionId); this.transformSupplementalPointDescription(target, sessionId); return target; From 6350756e0dd96ddbb0a2730a2ce9a0220600aca4 Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Thu, 19 Sep 2024 15:09:15 -0700 Subject: [PATCH 18/18] Fix typecheck errors --- .../components/sources/utils/points.test.ts | 8 +-- packages/shared/client/ReplayClient.ts | 12 ++--- packages/shared/client/types.ts | 1 + .../debugger/src/components/Editor/Footer.tsx | 2 +- .../components/PrimaryPanes/SourcesTree.tsx | 30 +++++++++--- .../src/components/PrimaryPanes/index.tsx | 49 ++++++++++++++----- .../src/components/shared/ManagedTree.tsx | 2 +- 7 files changed, 73 insertions(+), 31 deletions(-) diff --git a/packages/replay-next/components/sources/utils/points.test.ts b/packages/replay-next/components/sources/utils/points.test.ts index 20f6d7583a4..f5fc360db2f 100644 --- a/packages/replay-next/components/sources/utils/points.test.ts +++ b/packages/replay-next/components/sources/utils/points.test.ts @@ -17,7 +17,7 @@ describe("utils/points", () => { describe("findClosestHitPoint", () => { function verifyIndex(executionPoint: ExecutionPoint, expectedIndex: number) { - const match = findClosestHitPoint(hitPoints, executionPoint); + const match = findClosestHitPoint(hitPoints, { point: executionPoint, time: 0 }); expect(match).toHaveLength(2); expect(match[1]).toBe(expectedIndex); } @@ -49,7 +49,7 @@ describe("utils/points", () => { exactMatch: boolean, expectedIndex: number ) { - const match = findHitPoint(hitPoints, executionPoint, exactMatch); + const match = findHitPoint(hitPoints, { point: executionPoint, time: 0 }, exactMatch); expect(match).toHaveLength(2); expect(match[1]).toBe(expectedIndex); } @@ -92,7 +92,7 @@ describe("utils/points", () => { describe("findHitPointAfter", () => { function verifyIndex(executionPoint: ExecutionPoint, expectedIndex: number) { - const match = findHitPointAfter(hitPoints, executionPoint); + const match = findHitPointAfter(hitPoints, { point: executionPoint, time: 0 }); expect(match).toHaveLength(2); expect(match[1]).toBe(expectedIndex); } @@ -119,7 +119,7 @@ describe("utils/points", () => { // 0:1000, 1:4000, 2:6000 describe("findHitPointBefore", () => { function verifyIndex(executionPoint: ExecutionPoint, expectedIndex: number) { - const match = findHitPointBefore(hitPoints, executionPoint); + const match = findHitPointBefore(hitPoints, { point: executionPoint, time: 0 }); expect(match).toHaveLength(2); expect(match[1]).toBe(expectedIndex); } diff --git a/packages/shared/client/ReplayClient.ts b/packages/shared/client/ReplayClient.ts index a4bcffba7ac..3dc23555669 100644 --- a/packages/shared/client/ReplayClient.ts +++ b/packages/shared/client/ReplayClient.ts @@ -160,6 +160,10 @@ export class ReplayClient implements ReplayClientInterface { return sessionId == this._sessionId; } + numSupplementalRecordings() { + return this.supplemental.length; + } + private async forEachSession(callback: (sessionId: string, supplementalIndex: number) => Promise) { const sessionId = await this.waitForSession(); await callback(sessionId, 0); @@ -610,8 +614,6 @@ export class ReplayClient implements ReplayClientInterface { private async maybeGetConnectionStepTarget(point: ExecutionPoint, pointSupplementalIndex: number): Promise { const recordingId = this.getSupplementalIndexRecordingId(pointSupplementalIndex); - console.log("STEP_IN", point, recordingId); - let targetPoint: ExecutionPoint | undefined; let targetSupplementalIndex = 0; this.forAllConnections((serverRecordingId, connection, supplementalIndex) => { @@ -630,17 +632,15 @@ export class ReplayClient implements ReplayClientInterface { } }); - console.log("TARGET_POINT", targetPoint, targetSupplementalIndex); - if (!targetPoint) { return null; } const sessionId = await this.getSupplementalIndexSession(targetSupplementalIndex); - const response = await sendMessage("Session.getPointFrameSteps", { point: targetPoint }, sessionId); + const response = await sendMessage("Session.getPointFrameSteps" as any, { point: targetPoint }, sessionId); const { steps } = response; - const desc = steps.find(step => step.point == targetPoint); + const desc = steps.find((step: PointDescription) => step.point == targetPoint); assert(desc); this.transformSupplementalPointDescription(desc, sessionId); diff --git a/packages/shared/client/types.ts b/packages/shared/client/types.ts index bdad963d993..c09594a8181 100644 --- a/packages/shared/client/types.ts +++ b/packages/shared/client/types.ts @@ -323,4 +323,5 @@ export interface ReplayClientInterface { onSourceContentsChunk: ({ chunk, sourceId }: { chunk: string; sourceId: SourceId }) => void ): Promise; waitForSession(): Promise; + numSupplementalRecordings(): number; } diff --git a/src/devtools/client/debugger/src/components/Editor/Footer.tsx b/src/devtools/client/debugger/src/components/Editor/Footer.tsx index acc2a58e5fe..2fcfa445e60 100644 --- a/src/devtools/client/debugger/src/components/Editor/Footer.tsx +++ b/src/devtools/client/debugger/src/components/Editor/Footer.tsx @@ -20,7 +20,7 @@ function SourceFooter() {
An error occurred while replaying
} + fallback={
} > diff --git a/src/devtools/client/debugger/src/components/PrimaryPanes/SourcesTree.tsx b/src/devtools/client/debugger/src/components/PrimaryPanes/SourcesTree.tsx index 17d9722b074..22151b00372 100644 --- a/src/devtools/client/debugger/src/components/PrimaryPanes/SourcesTree.tsx +++ b/src/devtools/client/debugger/src/components/PrimaryPanes/SourcesTree.tsx @@ -28,6 +28,7 @@ import { } from "ui/reducers/sources"; import type { UIState } from "ui/state"; import { trackEvent } from "ui/utils/telemetry"; +import { breakdownSupplementalId } from "protocol/utils"; // Utils import { @@ -86,6 +87,8 @@ const connector = connect(mapStateToProps, { type PropsFromRedux = ConnectedProps; +type AllProps = PropsFromRedux & { supplementalIndex: number }; + interface STState { uncollapsedTree: TreeDirectory; sourceTree: TreeNode; @@ -94,13 +97,23 @@ interface STState { highlightItems?: TreeNode[]; } -class SourcesTree extends Component { - constructor(props: PropsFromRedux) { +function filterSources(sources: SourcesMap, supplementalIndex: number): SourcesMap { + const rv: SourcesMap = {}; + for (const [path, source] of Object.entries(sources)) { + if (breakdownSupplementalId(source.id).supplementalIndex == supplementalIndex) { + rv[path] = source; + } + } + return rv; +} + +class SourcesTree extends Component { + constructor(props: AllProps) { super(props); - const { sources } = this.props; + const { sources, supplementalIndex } = this.props; const state = createTree({ - sources: sources as SourcesMap, + sources: filterSources(sources as SourcesMap, supplementalIndex) }) as STState; if (props.shownSource) { @@ -119,8 +132,8 @@ class SourcesTree extends Component { this.state = state; } - UNSAFE_componentWillReceiveProps(nextProps: PropsFromRedux) { - const { sources, shownSource, selectedSource } = this.props; + UNSAFE_componentWillReceiveProps(nextProps: AllProps) { + const { sources, shownSource, selectedSource, supplementalIndex } = this.props; const { uncollapsedTree, sourceTree } = this.state; if (nextProps.shownSource && nextProps.shownSource != shownSource) { @@ -130,6 +143,7 @@ class SourcesTree extends Component { if (nextProps.selectedSource && nextProps.selectedSource != selectedSource) { const highlightItems = getDirectories(nextProps.selectedSource, sourceTree as TreeDirectory); + console.log("HighlightItems", supplementalIndex, highlightItems); this.setState({ highlightItems }); } @@ -139,7 +153,7 @@ class SourcesTree extends Component { this.setState( updateTree({ debuggeeUrl: "", - newSources: nextProps.sources, + newSources: filterSources(nextProps.sources, supplementalIndex), prevSources: sources, uncollapsedTree, sourceTree, @@ -305,7 +319,7 @@ class SourcesTree extends Component { } } -const WrappedSourcesTree = (props: PropsFromRedux) => { +const WrappedSourcesTree = (props: AllProps) => { const [, dismissExploreSourcesNag] = useNag(Nag.EXPLORE_SOURCES); useEffect(() => { diff --git a/src/devtools/client/debugger/src/components/PrimaryPanes/index.tsx b/src/devtools/client/debugger/src/components/PrimaryPanes/index.tsx index 9a1c1790153..80deef8a2bc 100644 --- a/src/devtools/client/debugger/src/components/PrimaryPanes/index.tsx +++ b/src/devtools/client/debugger/src/components/PrimaryPanes/index.tsx @@ -1,10 +1,12 @@ import classnames from "classnames"; import { useGraphQLUserData } from "shared/user-data/GraphQL/useGraphQLUserData"; +import { useContext, useState } from "react"; import Outline from "../SourceOutline/SourceOutline"; import QuickOpenButton from "./QuickOpenButton"; import SourcesTree from "./SourcesTree"; +import { ReplayClientContext } from "shared/client/ReplayClientContext"; import { Accordion, AccordionPane } from "@recordreplay/accordion"; @@ -15,19 +17,44 @@ export default function PrimaryPanes() { const [sourcesCollapsed, setSourcesCollapsed] = useGraphQLUserData("layout_sourcesCollapsed"); const [enableLargeText] = useGraphQLUserData("global_enableLargeText"); + const [supplementalSourcesCollapsed, setSupplementalSourcesCollapsed] = useState(sourcesCollapsed); + const replayClient = useContext(ReplayClientContext); + + const sourcePanes = []; + + sourcePanes.push( + setSourcesCollapsed(!sourcesCollapsed)} + initialHeight={400} + button={} + > + + + ); + + for (let i = 0; i < replayClient.numSupplementalRecordings(); i++) { + sourcePanes.push( + setSupplementalSourcesCollapsed(!supplementalSourcesCollapsed)} + initialHeight={200} + button={} + > + + + ); + } + return ( - setSourcesCollapsed(!sourcesCollapsed)} - initialHeight={400} - button={} - > - - + {...sourcePanes as any} extends Component< expandListItems(listItems: T[]) { const { expanded } = this.state; listItems.forEach(item => expanded.add(this.props.getPath(item))); - this.props.onFocus(listItems[0]); + //this.props.onFocus(listItems[0]); this.setState({ expanded }); }