Skip to content

Commit

Permalink
feat(explorer): transaction timings (#3274)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin Ingersoll <[email protected]>
  • Loading branch information
karooolis and holic authored Oct 11, 2024
1 parent 6476dec commit 1b0ffcf
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 78 deletions.
14 changes: 14 additions & 0 deletions .changeset/eighty-humans-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@latticexyz/explorer": patch
---

Transactions in Observe tab are now populated with timing metrics when using the `observer` Viem decorator in local projects.

You can wire up your local project to use transaction timings with:

```
import { observer } from "@latticexyz/explorer/observer";
// Extend the Viem client that is performing writes
walletClient.extend(observer());
```
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function TimeAgo({ timestamp }: { timestamp: bigint }) {
}, [timestamp]);

return (
<span className="text-white/60" title={new Date(Number(timestamp) * 1000).toISOString()}>
<span className="inline-block min-w-[60px] text-white/60" title={new Date(Number(timestamp) * 1000).toISOString()}>
{ago}
</span>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Separator } from "../../../../../../components/ui/Separator";
import { type Write } from "../../../../../../observer/store";
import { cn } from "../../../../../../utils";
import { useTimings } from "./useTimings";

export function TimingRowExpanded(write: Write) {
const timings = useTimings(write);
return (
<>
<Separator className="my-5" />
<div className="flex items-start gap-x-4 pb-2">
<h3 className="inline-block w-[45px] pb-2 text-2xs font-bold uppercase">Timing</h3>
<div className="w-full border border-white/20 p-2 pb-3">
<div className="grid grid-cols-[auto_1fr_auto] items-center gap-x-4 gap-y-1">
{timings.map((timing) => (
<>
<span className="text-xs">{timing.label}:</span>
<span
className={cn(`h-1`, {
"bg-[#5c9af6]": timing.type === "write",
"bg-[#4d7cc0]": timing.type === "waitForTransaction",
"bg-[#3d5c8a]": timing.type === "waitForTransactionReceipt",
})}
style={{
width: `${timing.widthPercentage}%`,
marginLeft: `${timing.startPercentage}%`,
}}
/>
<span className="text-right text-xs">{timing.duration}ms</span>
</>
))}
</div>
</div>
</div>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { type Write } from "../../../../../../observer/store";
import { cn } from "../../../../../../utils";
import { useTimings } from "./useTimings";

export function TimingRowHeader(write: Write) {
const timings = useTimings(write);
return (
<div className="ml-4 inline-block h-full w-14 grayscale lg:-mr-8 lg:ml-8 xl:-mr-16 xl:ml-16">
{timings.map((timing) => (
<div
key={timing.label}
title={timing.label}
className={cn(`h-1`, {
"bg-[#5c9af6]": timing.type === "write",
"mt-0.5 bg-[#4d7cc0]": timing.type === "waitForTransaction",
"mt-0.5 bg-[#3d5c8a]": timing.type === "waitForTransactionReceipt",
})}
style={{
width: `${timing.widthPercentage}%`,
marginLeft: `${timing.startPercentage}%`,
}}
/>
))}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Skeleton } from "../../../../../../components/ui/Skeleton";
import { TableCell, TableRow } from "../../../../../../components/ui/Table";
import { cn } from "../../../../../../utils";
import { Confirmations } from "./Confirmations";
import { TimingRowExpanded } from "./TimingRowExpanded";
import { columns } from "./TransactionsTable";
import { WatchedTransaction } from "./useTransactionWatcher";

Expand Down Expand Up @@ -78,7 +79,6 @@ export function TransactionTableRow({ row }: { row: Row<WatchedTransaction> }) {
</div>

<Separator className="my-5" />

<div className="flex items-start gap-x-4">
<h3 className="w-[45px] flex-shrink-0 text-2xs font-bold uppercase">Inputs</h3>
{Array.isArray(data.functionData?.args) && data.functionData?.args.length > 0 ? (
Expand All @@ -100,27 +100,21 @@ export function TransactionTableRow({ row }: { row: Row<WatchedTransaction> }) {
{data.error ? (
<>
<Separator className="my-5" />

<div className="flex items-start gap-x-4">
<h3 className="w-[45px] flex-shrink-0 text-2xs font-bold uppercase">Error</h3>
{data.error ? (
<div className="flex-grow whitespace-pre-wrap border border-red-500 p-2 font-mono text-xs">
{data.error.message}
</div>
) : (
<p className="text-2xs uppercase text-white/60">No error</p>
)}
<div className="flex-grow whitespace-pre-wrap border border-red-500 p-2 font-mono text-xs">
{data.error.message}
</div>
</div>
</>
) : null}

{!data.error ? (
<>
<Separator className="my-5" />

<div className="flex items-start gap-x-4 pb-2">
<h3 className="inline-block w-[45px] pb-2 text-2xs font-bold uppercase">Logs</h3>
{Array.isArray(logs) && logs.length > 0 ? (
<div className="flex items-start gap-x-4">
<h3 className="inline-block w-[45px] text-2xs font-bold uppercase">Logs</h3>
{Array.isArray(logs) && logs.length > 10 ? (
<div className="flex-grow break-all border border-white/20 p-2 pb-3">
<ul>
{logs.map((log, idx) => {
Expand All @@ -145,12 +139,16 @@ export function TransactionTableRow({ row }: { row: Row<WatchedTransaction> }) {
})}
</ul>
</div>
) : status === "pending" ? (
<Skeleton className="h-4 w-full" />
) : (
<p className="text-2xs uppercase text-white/60">No logs</p>
)}
</div>
</>
) : null}

{data.write && <TimingRowExpanded {...data.write} />}
</>
)}
</TableCell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from ".
import { TruncatedHex } from "../../../../../../components/ui/TruncatedHex";
import { BlockExplorerLink } from "./BlockExplorerLink";
import { TimeAgo } from "./TimeAgo";
import { TimingRowHeader } from "./TimingRowHeader";
import { TransactionTableRow } from "./TransactionTableRow";
import { WatchedTransaction, useTransactionWatcher } from "./useTransactionWatcher";

Expand Down Expand Up @@ -81,8 +82,13 @@ export const columns = [
header: "Time",
cell: (row) => {
const timestamp = row.getValue();
if (!timestamp) return <Skeleton className="h-4 w-full" />;
return <TimeAgo timestamp={timestamp} />;
const write = row.row.original.write;
return (
<>
{timestamp ? <TimeAgo timestamp={timestamp} /> : <Skeleton className="h-4 w-14" />}
{write && <TimingRowHeader {...write} />}
</>
);
},
}),
];
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { isDefined } from "@latticexyz/common/utils";
import { type Write } from "../../../../../../observer/store";

const eventConfig = {
write: { priority: 1, label: "write" },
"write:result": { priority: 2 },
waitForTransaction: { priority: 3, label: "state update" },
"waitForTransaction:result": { priority: 4 },
waitForTransactionReceipt: { priority: 5, label: "transaction receipt" },
"waitForTransactionReceipt:result": { priority: 6 },
} as const;

type EventType = keyof typeof eventConfig;

export function useTimings({ time: start, events }: Write) {
const maxLen = Math.max(...events.map((event) => event.time - start));
const sortedEvents = Object.values(events).sort((a, b) => {
const priorityA = eventConfig[a.type as EventType]?.priority;
const priorityB = eventConfig[b.type as EventType]?.priority;
return priorityA - priorityB;
});

return sortedEvents
.map((event) => {
const type = event.type as EventType;
if (type.endsWith(":result")) return;

const writeResult = events.find((e) => e.type === `${type}:result`);
const endTime = writeResult?.time ?? event.time;
const duration = endTime - event.time;
const startOffset = event.time - start;

const startPercentage = (startOffset / maxLen) * 100;
const widthPercentage = (duration / maxLen) * 100;

const config = eventConfig[type];
return {
type,
label: "label" in config ? config.label : type,
duration,
startPercentage,
widthPercentage,
};
})
.filter(isDefined);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { getTransaction, simulateContract, waitForTransactionReceipt } from "wag
import { useStore } from "zustand";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Message } from "../../../../../../observer/messages";
import { Write, store } from "../../../../../../observer/store";
import { type Write, store } from "../../../../../../observer/store";
import { useChain } from "../../../../hooks/useChain";
import { useWorldAbiQuery } from "../../../../queries/useWorldAbiQuery";

Expand Down Expand Up @@ -135,14 +135,14 @@ export function useTransactionWatcher() {
useEffect(() => {
for (const write of Object.values(observerWrites)) {
const hash = write.hash;
if (write.type === "waitForTransactionReceipt" && hash) {
if (write.type === "waitForTransactionReceipt" && hash && write.address === worldAddress) {
const transaction = transactions.find((transaction) => transaction.hash === hash);
if (!transaction) {
handleTransaction(hash, BigInt(write.time) / 1000n);
}
}
}
}, [handleTransaction, observerWrites, transactions]);
}, [handleTransaction, observerWrites, transactions, worldAddress]);

useWatchBlocks({
onBlock(block) {
Expand All @@ -159,6 +159,8 @@ export function useTransactionWatcher() {
const mergedMap = new Map<string | undefined, WatchedTransaction>();

for (const write of Object.values(observerWrites)) {
if (write.address !== worldAddress) continue;

const parsedAbiItem = parseAbiItem(`function ${write.functionSignature}`) as AbiFunction;
const writeResult = write.events.find((event): event is Message<"write:result"> => event.type === "write:result");

Expand Down Expand Up @@ -188,7 +190,7 @@ export function useTransactionWatcher() {
}

return Array.from(mergedMap.values()).sort((a, b) => Number(b.timestamp ?? 0n) - Number(a.timestamp ?? 0n));
}, [transactions, observerWrites]);
}, [observerWrites, worldAddress, transactions]);

return mergedTransactions;
}
2 changes: 1 addition & 1 deletion packages/explorer/src/app/(explorer)/utils/timeAgo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ export function timeAgo(timestamp: bigint) {
}
}

return "just now";
return "0s ago";
}

0 comments on commit 1b0ffcf

Please sign in to comment.