Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support loading additional supplemental recordings #10616

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
4 changes: 2 additions & 2 deletions packages/protocol/RecordedEventsCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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));
},
});

Expand Down
6 changes: 3 additions & 3 deletions packages/protocol/execution-point-utils.ts
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 0 additions & 1 deletion packages/protocol/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
46 changes: 43 additions & 3 deletions packages/protocol/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SourceLocation } from "@replayio/protocol";
import { SourceLocation, TimeStampedPoint } from "@replayio/protocol";

Check failure on line 1 in packages/protocol/utils.ts

View workflow job for this annotation

GitHub Actions / Trunk Check

prettier

Incorrect formatting, autoformat by running 'trunk fmt'

type ErrorHandler = (error: Error) => void;

Expand Down Expand Up @@ -140,16 +140,56 @@
}
}

/**
export function transformSupplementalId(id: string, supplementalIndex: number) {
if (!supplementalIndex) {
return id;
}
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 };
}

export function sameSupplementalIndex(idA: string, idB: string) {
return breakdownSupplementalId(idA).supplementalIndex == breakdownSupplementalId(idB).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) {
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, `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) {
return haystack.some(
location => location.line === needle.line && location.column === needle.column
Expand Down
10 changes: 6 additions & 4 deletions packages/replay-next/components/console/MessageHoverButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExecutionPoint, Location } from "@replayio/protocol";

Check failure on line 1 in packages/replay-next/components/console/MessageHoverButton.tsx

View workflow job for this annotation

GitHub Actions / Trunk Check

prettier

Incorrect formatting, autoformat by running 'trunk fmt'
import {
MouseEvent,
unstable_useCacheRefresh as useCacheRefresh,
Expand All @@ -22,10 +22,10 @@
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";

Expand All @@ -48,7 +48,7 @@
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();
Expand Down Expand Up @@ -134,9 +134,11 @@
dismissFirstConsoleNavigateNag();
};

const pointTS = { point: executionPoint, time };

const label =
currentExecutionPoint === null ||
isExecutionPointsGreaterThan(executionPoint, currentExecutionPoint)
compareTimeStampedPoints(pointTS, { point: currentExecutionPoint, time: currentTime }) > 0
? "Fast-forward"
: "Rewind";

Expand All @@ -153,7 +155,7 @@
className={styles.ConsoleMessageHoverButtonIcon}
type={
currentExecutionPoint === null ||
isExecutionPointsGreaterThan(executionPoint, currentExecutionPoint)
compareTimeStampedPoints(pointTS, { point: currentExecutionPoint, time: currentTime }) > 0
? "fast-forward"
: "rewind"
}
Expand Down
13 changes: 8 additions & 5 deletions packages/replay-next/components/console/MessagesList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {

Check failure on line 1 in packages/replay-next/components/console/MessagesList.tsx

View workflow job for this annotation

GitHub Actions / Trunk Check

prettier

Incorrect formatting, autoformat by running 'trunk fmt'
ForwardedRef,
MutableRefObject,
ReactNode,
Expand All @@ -16,13 +16,13 @@
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";
Expand All @@ -34,6 +34,7 @@
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";

Expand Down Expand Up @@ -66,26 +67,28 @@
} = useContext(ConsoleFiltersContext);
const { loggables, streamingStatus } = useContext(LoggablesContext);
const [searchState] = useContext(ConsoleSearchContext);
const { point: currentExecutionPoint } = useMostRecentLoadedPause() ?? {};
const { point: currentExecutionPoint, time: currentTime } = useMostRecentLoadedPause() ?? {};

// The Console should render a line indicating the current execution point.
// This point might match multiple logs– or it might be between logs, or after the last log, etc.
// This looking finds the best place to render the indicator.
const currentTimeIndicatorPlacement = useMemo<CurrentTimeIndicatorPlacement | null>(() => {
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;
}
});
return nearestLoggable || "end";
}, [currentExecutionPoint, loggables]);

Check failure on line 91 in packages/replay-next/components/console/MessagesList.tsx

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(react-hooks/exhaustive-deps)

[new] React Hook useMemo has a missing dependency: 'currentTime'. Either include it or remove the dependency array.

const {
messageMetadata: { countAfter, countBefore, didOverflow },
Expand Down
6 changes: 3 additions & 3 deletions packages/replay-next/components/sources/SeekHoverButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = {
Expand All @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TimeStampedPoint } from "@replayio/protocol";

Check failure on line 1 in packages/replay-next/components/sources/log-point-panel/HitPointTimeline.tsx

View workflow job for this annotation

GitHub Actions / Trunk Check

prettier

Incorrect formatting, autoformat by running 'trunk fmt'
import {
MouseEvent,
useContext,
Expand All @@ -14,16 +14,13 @@
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";

Expand Down Expand Up @@ -53,6 +50,8 @@
update,
} = useContext(TimelineContext);

const currentTS: TimeStampedPoint = { point: currentExecutionPoint!, time: currentTime };

const pointEditable = point.user?.id === currentUserInfo?.id;

const [hoverCoordinates, setHoverCoordinates] = useState<{
Expand All @@ -73,8 +72,8 @@

const [closestHitPoint, closestHitPointIndex] = useMemo(
() =>
currentExecutionPoint ? findHitPoint(hitPoints, currentExecutionPoint, false) : noMatchTuple,
currentExecutionPoint ? findHitPoint(hitPoints, currentTS, false) : noMatchTuple,
[currentExecutionPoint, hitPoints]

Check failure on line 76 in packages/replay-next/components/sources/log-point-panel/HitPointTimeline.tsx

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(react-hooks/exhaustive-deps)

[new] React Hook useMemo has a missing dependency: 'currentTS'. Either include it or remove the dependency array.
);

const currentHitPoint = closestHitPoint?.point === currentExecutionPoint ? closestHitPoint : null;
Expand Down Expand Up @@ -116,11 +115,11 @@
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];
Expand All @@ -134,7 +133,7 @@
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);
Expand All @@ -144,7 +143,7 @@
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TimeStampedPoint, TimeStampedPointRange } from "@replayio/protocol";

Check failure on line 1 in packages/replay-next/components/sources/log-point-panel/LogPointPanel.tsx

View workflow job for this annotation

GitHub Actions / Trunk Check

prettier

Incorrect formatting, autoformat by running 'trunk fmt'
import {
MouseEvent,
Suspense,
Expand Down Expand Up @@ -28,8 +28,8 @@
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 {
Expand Down Expand Up @@ -214,10 +214,9 @@
if (!currentExecutionPoint) {
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]);

Check failure on line 219 in packages/replay-next/components/sources/log-point-panel/LogPointPanel.tsx

View workflow job for this annotation

GitHub Actions / Trunk Check

eslint(react-hooks/exhaustive-deps)

[new] React Hook useMemo has a missing dependency: 'currentTime'. Either include it or remove the dependency array.

// If we've found a hit point match, use data from its scope.
// Otherwise fall back to using the global execution point.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import assert from "assert";

Check failure on line 1 in packages/replay-next/components/sources/useSourceContextMenu.tsx

View workflow job for this annotation

GitHub Actions / Trunk Check

prettier

Incorrect formatting, autoformat by running 'trunk fmt'
import { SourceId, TimeStampedPoint } from "@replayio/protocol";
import {
Suspense,
Expand Down Expand Up @@ -133,7 +133,7 @@
lineNumber: number;
sourceId: SourceId;
}) {
const { executionPoint: currentExecutionPoint, update } = useContext(TimelineContext);
const { executionPoint: currentExecutionPoint, time: currentTime, update } = useContext(TimelineContext);

const [hitPoints, hitPointStatus] = useDeferredHitCounts({
lineHitCounts,
Expand All @@ -143,7 +143,7 @@

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 = () => {
Expand Down Expand Up @@ -172,7 +172,7 @@
lineNumber: number;
sourceId: SourceId;
}) {
const { executionPoint: currentExecutionPoint, update } = useContext(TimelineContext);
const { executionPoint: currentExecutionPoint, time: currentTime, update } = useContext(TimelineContext);

const [hitPoints, hitPointStatus] = useDeferredHitCounts({
lineHitCounts,
Expand All @@ -182,7 +182,7 @@

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 = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Expand Down
Loading
Loading