Skip to content

Commit

Permalink
fix: fix and improve timeline updates and fiber tracking (aidenybai#228)
Browse files Browse the repository at this point in the history
  - Optimize fiber tree traversal by caching root fiber
  - Fix timeline batch updates
  - Improve props/state change tracking logic
  - Fix wrapper types count display
  - fix: trackChangeCount
  - fix: overlay stats
  • Loading branch information
pivanov committed Feb 2, 2025
1 parent c5b87cc commit aac979b
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 85 deletions.
2 changes: 1 addition & 1 deletion packages/scan/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-scan",
"version": "0.1.1",
"version": "0.1.3",
"description": "Scan your React app for renders",
"keywords": [
"react",
Expand Down
16 changes: 1 addition & 15 deletions packages/scan/src/web/components/inspector/overlay/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
import { signalIsSettingsOpen } from '~web/state';
import { cn, throttle } from '~web/utils/helpers';
import { lerp } from '~web/utils/lerp';
import { timelineState } from '../states';

type DrawKind = 'locked' | 'inspecting';

Expand Down Expand Up @@ -101,24 +100,11 @@ export const ScanOverlay = () => {
) => {
if (!fiber) return;

const currentUpdate = timelineState.value.updates[timelineState.value.currentIndex];

const stats = {
count: timelineState.value.updates.length - 1,
time: currentUpdate?.fiberInfo?.selfTime,
};

const pillHeight = 24;
const pillPadding = 8;
const componentName =
(fiber?.type && getDisplayName(fiber.type)) ?? 'Unknown';
let text = componentName;
if (stats.count > 0) {
text += ` • ×${stats.count}`;
if (stats.time) {
text += ` (${stats.time.toFixed(1)}ms)`;
}
}
const text = componentName;

ctx.save();
ctx.font = '12px system-ui, -apple-system, sans-serif';
Expand Down
115 changes: 60 additions & 55 deletions packages/scan/src/web/components/inspector/states.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,62 @@ export const timelineState = signal<TimelineState>(timelineStateDefault);

export const inspectorUpdateSignal = signal<number>(0);

// Add batching storage
let pendingUpdates: Array<{ update: TimelineUpdate; fiber: Fiber | null }> = [];
let batchTimeout: ReturnType<typeof setTimeout> | null = null;

const batchUpdates = () => {
if (pendingUpdates.length === 0) return;

const batchedUpdates = [...pendingUpdates];

const { updates, totalUpdates, currentIndex, isViewingHistory } =
timelineState.value;
const newUpdates = [...updates];
let newTotalUpdates = totalUpdates;

for (const { update } of batchedUpdates) {
if (newUpdates.length >= TIMELINE_MAX_UPDATES) {
newUpdates.shift();
}
newUpdates.push(update);
newTotalUpdates++;
}

const newWindowOffset = Math.max(0, newTotalUpdates - TIMELINE_MAX_UPDATES);

let newCurrentIndex: number;
if (isViewingHistory) {
if (currentIndex === totalUpdates - 1) {
newCurrentIndex = newUpdates.length - 1;
} else if (currentIndex === 0) {
newCurrentIndex = 0;
} else {
if (newWindowOffset === 0) {
newCurrentIndex = currentIndex;
} else {
newCurrentIndex = currentIndex - 1;
}
}
} else {
newCurrentIndex = newUpdates.length - 1;
}

const lastUpdate = batchedUpdates[batchedUpdates.length - 1];

timelineState.value = {
...timelineState.value,
latestFiber: lastUpdate.fiber,
updates: newUpdates,
totalUpdates: newTotalUpdates,
windowOffset: newWindowOffset,
currentIndex: newCurrentIndex,
isViewingHistory,
};

// Only after signal is updated, remove the processed updates
pendingUpdates = pendingUpdates.slice(batchedUpdates.length);
};

export const timelineActions = {
showTimeline: () => {
timelineState.value = {
Expand Down Expand Up @@ -88,67 +140,20 @@ export const timelineActions = {
},

addUpdate: (update: TimelineUpdate, latestFiber: Fiber | null) => {
// Collect the update
pendingUpdates.push({ update, fiber: latestFiber });

if (!batchTimeout) {
// If no batch is scheduled, schedule one
batchTimeout = setTimeout(() => {
// Process all collected updates
const batchedUpdates = pendingUpdates;
pendingUpdates = [];
batchTimeout = null;
const processBatch = () => {
batchUpdates();

// Apply all updates in the batch
const { updates, totalUpdates, currentIndex, isViewingHistory } =
timelineState.value;
const newUpdates = [...updates];
let newTotalUpdates = totalUpdates;

// Process each update in the batch
for (const { update } of batchedUpdates) {
if (newUpdates.length >= TIMELINE_MAX_UPDATES) {
newUpdates.shift();
}
newUpdates.push(update);
newTotalUpdates++;
}
batchTimeout = null;

const newWindowOffset = Math.max(
0,
newTotalUpdates - TIMELINE_MAX_UPDATES,
);

let newCurrentIndex: number;
if (isViewingHistory) {
if (currentIndex === totalUpdates - 1) {
newCurrentIndex = newUpdates.length - 1;
} else if (currentIndex === 0) {
newCurrentIndex = 0;
} else {
if (newWindowOffset === 0) {
newCurrentIndex = currentIndex;
} else {
newCurrentIndex = currentIndex - 1;
}
}
} else {
newCurrentIndex = newUpdates.length - 1;
if (pendingUpdates.length > 0) {
batchTimeout = setTimeout(processBatch, 96);
}
};

// Get the latest fiber from the last update in batch
const lastUpdate = batchedUpdates[batchedUpdates.length - 1];

timelineState.value = {
...timelineState.value,
latestFiber: lastUpdate.fiber,
updates: newUpdates,
totalUpdates: newTotalUpdates,
windowOffset: newWindowOffset,
currentIndex: newCurrentIndex,
isViewingHistory,
};
}, 100);
batchTimeout = setTimeout(processBatch, 96);
}
},

Expand Down
23 changes: 20 additions & 3 deletions packages/scan/src/web/components/inspector/timeline/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,30 @@ export const trackChange = (
previousValue: unknown,
): { hasChanged: boolean; count: number } => {
const existing = tracker.get(key);
const isInitialValue = tracker === propsTracker || tracker === contextTracker;
const hasChanged = !isEqual(currentValue, previousValue);

if (!existing || !isEqual(existing.currentValue, currentValue)) {
const newCount = (existing?.count ?? 0) + 1;
if (!existing) {
// For props and context, start with count 1 if there's a change
tracker.set(key, {
count: hasChanged && isInitialValue ? 1 : 0,
currentValue,
previousValue,
lastUpdated: Date.now(),
});

return {
hasChanged,
count: hasChanged && isInitialValue ? 1 : isInitialValue ? 0 : 1,
};
}

if (!isEqual(existing.currentValue, currentValue)) {
const newCount = existing.count + 1;
tracker.set(key, {
count: newCount,
currentValue,
previousValue: existing?.currentValue ?? previousValue,
previousValue: existing.currentValue,
lastUpdated: Date.now(),
});
return { hasChanged: true, count: newCount };
Expand Down
32 changes: 28 additions & 4 deletions packages/scan/src/web/components/inspector/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,17 +231,41 @@ export const getCompositeFiberFromElement = (
let fiber = knownFiber ?? getNearestFiberFromElement(element);
if (!fiber) return {};

// Get the current associated fiber
fiber = isCurrentTree(fiber) ? fiber : (fiber.alternate ?? fiber);
// Find root once and cache it
let curr: Fiber | null = fiber;
let rootFiber: Fiber | null = null;
let currentRootFiber: Fiber | null = null;

while (curr) {
if (!curr.stateNode) {
curr = curr.return;
continue;
}
if (ReactScanInternals.instrumentation?.fiberRoots.has(curr.stateNode)) {
rootFiber = curr;
currentRootFiber = curr.stateNode.current;
break;
}
curr = curr.return;
}

if (!rootFiber || !currentRootFiber) return {};

// Get the current associated fiber using cached root
fiber = isFiberInTree(fiber, currentRootFiber)
? fiber
: (fiber.alternate ?? fiber);
if (!fiber) return {};

if (!getFirstStateNode(fiber)) return {};

// Fetch the parent composite fiber efficiently
// Get parent composite fiber
const parentCompositeFiber = getParentCompositeFiber(fiber)?.[0];
if (!parentCompositeFiber) return {};

// Use cached root to check parent fiber
return {
parentCompositeFiber: isCurrentTree(parentCompositeFiber)
parentCompositeFiber: isFiberInTree(parentCompositeFiber, currentRootFiber)
? parentCompositeFiber
: (parentCompositeFiber.alternate ?? parentCompositeFiber),
};
Expand Down
16 changes: 10 additions & 6 deletions packages/scan/src/web/components/inspector/what-changed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ const safeGetValue = (value: unknown): { value: unknown; error?: string } => {
}

try {
// proxies or getter errors
const proto = Object.getPrototypeOf(value);
if (proto === Promise.prototype || proto?.constructor?.name === 'Promise') {
return { value: 'Promise' };
Expand Down Expand Up @@ -88,6 +87,7 @@ export const WhatChangedSection = memo(() => {
cancelAnimationFrame(rafId);
};
}, []);

return (
<>
{
Expand Down Expand Up @@ -390,25 +390,29 @@ const Section = memo(({ title, isExpanded }: SectionProps) => {
const currentData = currentUpdate?.[title.toLowerCase() as SectionType];
const prevData = prevUpdate?.[title.toLowerCase() as SectionType];

if (!currentData || !prevData) {
if (!currentData) {
return;
}

refFiberInfo.current = currentUpdate?.fiberInfo;

refLastUpdated.current.clear();

const changesMap = new Map<string | number, Change>(
refLatestChanges.current.map((c) => [c.name, c]),
);

for (const { name, value } of currentData.current) {
const count = currentData.changesCounts?.get(name) ?? 0;
const prevValue = prevData.current.find(
const currentCount = currentData.changesCounts?.get(name) ?? 0;
const prevCount = prevData?.changesCounts?.get(name) ?? 0;
const count = Math.max(currentCount, prevCount);

const prevValue = prevData?.current.find(
(p) => p.name === name,
)?.value;

if (!isEqual(value, prevValue) || count > 0) {
const hasValueChange = !isEqual(value, prevValue);

if (count > 0 || hasValueChange) {
const { value: safePrevValue, error: prevError } =
safeGetValue(prevValue);
const { value: safeCurrValue, error: currError } =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ const TreeNodeItem = ({
)}
</>
)}
{wrapperTypes.length > 1 && ${wrapperTypes.length - 1}`}
{wrapperTypes.length > 1 && ${wrapperTypes.length}`}
</span>
);
}, [node.fiber, typeHighlight]);
Expand Down

0 comments on commit aac979b

Please sign in to comment.