From 13093ee2cb7641f055dde1aef1a9f8cc4f788090 Mon Sep 17 00:00:00 2001 From: Calvin Koepke Date: Fri, 22 Nov 2024 17:38:44 -0700 Subject: [PATCH] chore: working event stream and animator --- ui/bun.lockb | Bin 183019 -> 183642 bytes ui/package.json | 4 +- ui/src/app/Test.tsx | 4 +- ui/src/app/api/messages/batch/route.ts | 194 +++++----- ui/src/app/api/topography/route.ts | 22 -- ui/src/app/api/transactions/last/route.ts | 75 ---- ui/src/app/page.tsx | 13 +- ui/src/app/queries.ts | 99 +++++ ui/src/components/Graph/Graph.tsx | 207 ----------- ui/src/components/Graph/GraphWapper.tsx | 39 ++ ui/src/components/Graph/hooks/queries.tsx | 347 +++++------------- ui/src/components/Graph/hooks/useHandlers.ts | 132 +++---- ui/src/components/Graph/modules/Canvas.tsx | 4 +- .../Graph/modules/Chart.TransactionsSent.tsx | 4 +- ui/src/components/Graph/modules/Controls.tsx | 6 +- ui/src/components/Graph/modules/Slider.tsx | 2 +- ui/src/components/Graph/types.ts | 5 + .../GraphContext/GraphContextProvider.tsx | 130 +++---- ui/src/contexts/GraphContext/context.ts | 22 +- ui/src/contexts/GraphContext/reducer.ts | 113 ++++++ ui/src/contexts/GraphContext/types.ts | 55 ++- 21 files changed, 636 insertions(+), 841 deletions(-) delete mode 100644 ui/src/app/api/topography/route.ts delete mode 100644 ui/src/app/api/transactions/last/route.ts create mode 100644 ui/src/app/queries.ts delete mode 100644 ui/src/components/Graph/Graph.tsx create mode 100644 ui/src/components/Graph/GraphWapper.tsx create mode 100644 ui/src/contexts/GraphContext/reducer.ts diff --git a/ui/bun.lockb b/ui/bun.lockb index fa5fc39b01bf0ead4081bcff8343f7952f7046d7..98551310b0bd315aa9926ad7300f7f219fa9be86 100755 GIT binary patch delta 30078 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 delta 29671 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 A?*IS* diff --git a/ui/package.json b/ui/package.json index 25d2426..4cf779c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,8 +14,8 @@ "@sundaeswap/prettier-config": "^2.0.13", "@types/bun": "latest", "@types/d3": "^7.4.3", - "@types/msgpack-lite": "^0.1.11", "@types/oboe": "^2.1.4", + "@types/pako": "^2.0.3", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.3", @@ -31,10 +31,12 @@ "@visx/responsive": "^3.12.0", "concat-stream": "^2.0.0", "d3": "^7.9.0", + "debounce": "^2.2.0", "linebyline": "^1.3.0", "msgpack-lite": "^0.1.26", "next": "^15.0.2", "oboe": "^2.1.7", + "pako": "^2.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-force-graph": "^1.44.6", diff --git a/ui/src/app/Test.tsx b/ui/src/app/Test.tsx index 383bfda..12e9762 100644 --- a/ui/src/app/Test.tsx +++ b/ui/src/app/Test.tsx @@ -3,13 +3,13 @@ import { useGraphContext } from "@/contexts/GraphContext/context"; export const Test = () => { - const { topography, transactions, messages, currentTime, maxTime } = useGraphContext(); + const { state: { topography, transactions, messages, currentTime, maxTime } } = useGraphContext(); return (

Nodes: {topography.nodes.size}
Links: {topography.links.size}
- Total Events: {messages.length}
+ Total Events: {messages.size}
Transaction List: {transactions.size}
Current Time: {currentTime}
Max Time: {maxTime} diff --git a/ui/src/app/api/messages/batch/route.ts b/ui/src/app/api/messages/batch/route.ts index 8639fb4..6af8700 100644 --- a/ui/src/app/api/messages/batch/route.ts +++ b/ui/src/app/api/messages/batch/route.ts @@ -1,13 +1,10 @@ import { createReadStream, statSync } from "fs"; -import msgpack from "msgpack-lite"; import { NextResponse } from "next/server"; -import readline from 'readline'; +import readline from "readline"; import { IServerMessage } from "@/components/Graph/types"; import { messagesPath } from "../../utils"; -const MESSAGE_BUFFER_IN_MS = 200; - async function findStartPosition(filePath: string, targetTimestamp: number) { const fileSize = statSync(filePath).size; let left = 0; @@ -29,17 +26,20 @@ async function findStartPosition(filePath: string, targetTimestamp: number) { return bestPosition; } -function getTimestampAtPosition(filePath: string, position: number): Promise { +function getTimestampAtPosition( + filePath: string, + position: number, +): Promise { return new Promise((resolve, reject) => { const stream = createReadStream(filePath, { start: position }); let foundNewLine = false; let adjustedPosition = position; // Read a few bytes to find the newline character - stream.on('data', (chunk) => { + stream.on("data", (chunk) => { const decoded = chunk.toString("utf8"); for (let i = 0; i < decoded.length; i++) { - if (decoded[i] === '\n') { + if (decoded[i] === "\n") { foundNewLine = true; adjustedPosition += i + 1; // Move to the start of the next line break; @@ -49,151 +49,141 @@ function getTimestampAtPosition(filePath: string, position: number): Promise { + stream.on("close", () => { if (foundNewLine) { // Now use readline to get the timestamp from the new line - const lineStream = createReadStream(filePath, { start: adjustedPosition }); + const lineStream = createReadStream(filePath, { + start: adjustedPosition, + }); const rl = readline.createInterface({ input: lineStream, crlfDelay: Infinity, }); - rl.on('line', (line) => { + rl.on("line", (line) => { const message: IServerMessage = JSON.parse(line); const timestamp = message.time / 1_000_000; rl.close(); resolve(timestamp); }); - rl.on('error', (err) => { + rl.on("error", (err) => { reject(err); }); } else { - reject(new Error("Could not find a newline character in the provided range")); + reject( + new Error("Could not find a newline character in the provided range"), + ); } }); - stream.on('error', (err) => { + stream.on("error", (err) => { reject(err); }); }); } -export async function GET(req: Request) { +export async function GET(req: Request, res: Response) { try { const url = new URL(req.url); - const currentTime = parseInt(url.searchParams.get("time") || "", 10); + const startTime = parseInt(url.searchParams.get("startTime") || ""); + const speed = parseInt(url.searchParams.get("speed") || ""); - if (isNaN(currentTime)) { - return new NextResponse("Invalid currentTime parameter", { status: 400 }); + if (isNaN(startTime)) { + return new NextResponse(null, { status: 400, statusText: "Invalid currentTime parameter" }); } - const startPosition = await findStartPosition(messagesPath, currentTime); - const fileStream = createReadStream(messagesPath, { encoding: "utf8", start: startPosition }); + if (isNaN(speed)) { + return new NextResponse(null, { status: 400, statusText: "Invalid speed parameter" }); + } + + const startPosition = await findStartPosition(messagesPath, startTime); + const fileStream = createReadStream(messagesPath, { + encoding: "utf8", + start: startPosition, + }); const rl = readline.createInterface({ input: fileStream, - crlfDelay: Infinity - }) - + crlfDelay: Infinity, + }); + + let initialEventTime: number | null = null; + let interval: Timer | undefined; + const startTimeOnServer = Date.now(); + const eventBuffer: { line: string; timeToSend: number }[] = []; + let rlClosed = false; + const stream = new ReadableStream({ + cancel() { + clearInterval(interval); + rl.close(); + }, + start(controller) { - rl.on("line", (line) => { + const processEventBuffer = () => { + const now = Date.now(); + + while (eventBuffer.length > 0) { + const { line, timeToSend } = eventBuffer[0]; + if (timeToSend <= now) { + // Send the event to the client + controller.enqueue(`data: ${line}\n\n`); + eventBuffer.shift(); + } else { + // Next event isn't ready yet + break; + } + } + + // Close the stream if all events have been sent and the file has been fully read + if (eventBuffer.length === 0 && rlClosed) { + clearInterval(interval); + controller.close(); + } + }; + + interval = setInterval(processEventBuffer, 50); + + const processLine = (line: string) => { try { const message: IServerMessage = JSON.parse(line); - const aboveLowerLimit = message.time / 1_000_000 >= currentTime; - const underUpperLimit = message.time / 1_000_000 < currentTime + MESSAGE_BUFFER_IN_MS; - - // Check if the message falls within the time range - if (aboveLowerLimit && underUpperLimit) { - controller.enqueue(msgpack.encode(message)) - } + const eventTime = message.time; // Timestamp in nanoseconds - // Free up resources if we're already pass the buffer window. - if (message.time / 1_000_000 >= currentTime + MESSAGE_BUFFER_IN_MS) { - rl.close(); - fileStream.destroy(); + if (initialEventTime === null) { + initialEventTime = eventTime; } + + const deltaTime = eventTime - initialEventTime; // Difference in nanoseconds + const adjustedDelay = (deltaTime / 1_000_000) * speed; // Convert to ms and apply multiplier + + const timeToSend = startTimeOnServer + adjustedDelay; + + eventBuffer.push({ line, timeToSend }); } catch (error) { - // Handle JSON parse errors or other issues controller.error(error); } + }; + + rl.on("line", (line) => { + processLine(line); }); - + rl.on("close", () => { - controller.close(); + console.log('rl closed') }); - + rl.on("error", (error) => { controller.error(error); + console.log('rl error') }); - } + }, }); - // const stream = new ReadableStream( - // { - // start(controller) { - // start = performance.now(); - // fileStream.on("data", (chunk) => { - // buffer += chunk; - // let lines = buffer.split("\n"); - // buffer = lines.pop() || ""; // Keep the last incomplete line - - // for (const line of lines) { - // if (!line.trim()) continue; - - // try { - // const message: IServerMessage = JSON.parse(line); - // const aboveLowerLimit = message.time / 1_000_000 >= currentTime - halfRange; - // const underUpperLimit = message.time / 1_000_000 < currentTime + halfRange; - - // // Check if the message falls within the time range - // if (aboveLowerLimit && underUpperLimit && !sent) { - // // Stream the message if it matches the time range - // controller.enqueue(new TextEncoder().encode(JSON.stringify(message) + "\n")); - // sent = true; - // } - // } catch (error) { - // console.error("Error parsing JSON line:", error); - // controller.error(new Error("Error parsing JSON line")); - // fileStream.destroy(); - // break; - // } - // } - // }); - - // fileStream.on("end", () => { - // end = performance.now(); - // console.log(end - start); - // if (buffer.trim()) { - // try { - // const message = JSON.parse(buffer); // Parse the last incomplete line - // controller.enqueue( - // new TextEncoder().encode(JSON.stringify(message) + "\n"), - // ); - // } catch (error) { - // console.error("Error parsing final JSON line:", error); - // controller.error(new Error("Error parsing final JSON line")); - // } - // } - // controller.close(); - // }); - - // fileStream.on("error", (error) => { - // console.error("File stream error:", error); - // controller.error(new Error("File stream error")); - // }); - // }, - // }, - // { - // highWaterMark: 100_000, - // }, - // ); - - // Return a streaming response return new NextResponse(stream, { headers: { - "Content-Type": "application/jsonl", - "Transfer-Encoding": "chunked", + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", }, }); } catch (e) { diff --git a/ui/src/app/api/topography/route.ts b/ui/src/app/api/topography/route.ts deleted file mode 100644 index 312fa51..0000000 --- a/ui/src/app/api/topography/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { parse } from "@iarna/toml"; -import { createReadStream } from 'fs'; -import { NextResponse } from 'next/server'; -import path from 'path'; - -import { ILink, INode } from "@/components/Graph/types"; - -export async function GET(req: Request) { - try { - const filePath = path.resolve(__dirname, "../../../../../../../sim-rs/test_data/realistic.toml"); - const fileStream = createReadStream(filePath, { encoding: "utf8", highWaterMark: 100_000 }); - const result = await parse.stream(fileStream) as unknown as { links: ILink[]; nodes: INode[] }; - - // Return a streaming response - return NextResponse.json(result); - } catch (e) { - return new NextResponse(null, { - status: 500, - statusText: (e as Error)?.message - }) - } -} diff --git a/ui/src/app/api/transactions/last/route.ts b/ui/src/app/api/transactions/last/route.ts deleted file mode 100644 index 03a1a4d..0000000 --- a/ui/src/app/api/transactions/last/route.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { EMessageType, IServerMessage } from "@/components/Graph/types"; -import { closeSync, openSync, readSync, statSync } from "fs"; -import { NextResponse } from "next/server"; -import { messagesPath } from "../../utils"; - -async function getLastTransactionReceived(filePath: string, bufferSize = 1024): Promise { - return new Promise((resolve, reject) => { - const fileSize = statSync(filePath).size; - if (fileSize === 0) { - return reject(new Error("File is empty")); - } - - const fileDescriptor = openSync(filePath, 'r'); - let buffer = Buffer.alloc(bufferSize); - let position = fileSize; - let lastLine = ""; - let foundLastTransactionReceived = false; - - while (position > 0 && !foundLastTransactionReceived) { - // Calculate how many bytes to read - const bytesToRead = Math.min(bufferSize, position); - position -= bytesToRead; - - // Read from the file - readSync(fileDescriptor, new Uint8Array(buffer.buffer), 0, bytesToRead, position); - const chunk = buffer.toString('utf8', 0, bytesToRead); - - // Search for the last newline character - const lines = chunk.split(/\n/).reverse(); - for (const line of lines) { - if (!line) { - continue; - } - - try { - const message: IServerMessage = JSON.parse(line); - if (message.message.type === EMessageType.TransactionReceived) { - lastLine = line; - foundLastTransactionReceived = true; - break; - } - } catch (e) { - console.log(`Could not parse: ${line}`) - } - } - - position -= bytesToRead; - } - - closeSync(fileDescriptor); - - if (!foundLastTransactionReceived && lastLine.length === 0) { - return reject(new Error("Could not find any complete line in the file")); - } - - if (!lastLine) { - reject("Could not find the last transaction.") - } else { - resolve(lastLine.trim()); - } - }); -} - -export async function GET() { - try { - const line = await getLastTransactionReceived(messagesPath); - console.log(line) - const data: IServerMessage = JSON.parse(line); - return NextResponse.json(data); - } catch(e) { - return new NextResponse(null, { - status: 500 - }) - } -} diff --git a/ui/src/app/page.tsx b/ui/src/app/page.tsx index cc19031..5df2625 100644 --- a/ui/src/app/page.tsx +++ b/ui/src/app/page.tsx @@ -1,16 +1,17 @@ -import { Graph } from "@/components/Graph/Graph"; +import { GraphWrapper } from "@/components/Graph/GraphWapper"; import Image from "next/image"; +import { getSetSimulationMaxTime, getSimulationTopography } from "./queries"; export default async function Home() { - // const [messages, topography] = await Promise.all([ - // getMessages(), - // getTopography() - // ]) + const [maxTime, topography] = await Promise.all([ + getSetSimulationMaxTime(), + getSimulationTopography(), + ]); return (

- +