Commit 7b732eaa authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

stateviz: remove (#9338)

Removes the stateviz code from the repo as it has not
been used in quite some time. It was removed from the
docker compose setup. This is part of cleaning up code
in the monorepo periodically to make sure that we keep
the codebase clean.

stateviz was useful for visualizing the state of the rollup.
parent dcdf044f
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/paginationjs/2.1.5/pagination.css" />
<style>
#snapshot-tables {
font-size: 0.6rem;
}
#snapshot-tables td {
padding: 0.2rem 0.2rem;
}
.tooltip div {
min-width: 40rem;
}
</style>
</head>
<body>
<div class="container-fluid">
<div id="logs" class="row">
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paginationjs/2.1.5/pagination.min.js"
integrity="sha512-1zzZ0ynR2KXnFskJ1C2s+7TIEewmkB2y+5o/+ahF7mwNj9n3PnzARpqalvtjSbUETwx6yuxP5AJXZCpnjEJkQw==" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
crossorigin="anonymous"></script>
</body>
<script type="text/javascript" src="/main.js"></script>
</html>
function prettyHex(hash) {
return `${hash.slice(0, 8)}..${hash.slice(56)}`;
}
// return a light-ish hue
function colorCode(hash) {
const code = parseInt(hash.slice(60), 16);
const h = code % 361;
const s = code % 101;
let l = code % 101;
// yeah this is biased but it's good enough
if (l < 30) {
l += 30;
}
return `hsl(${h}, ${s}%, ${l}%)`;
}
async function fetchLogs() {
const response = await fetch("/logs");
return await response.json();
}
function tooltipFormat(v) {
var out = ""
out += `<div>`
out += `<em>hash</em>: <code>${v["hash"]}</code><br/>`
out += `<em>num</em>: <code>${v["number"]}</code><br/>`
out += `<em>parent</em>: <code>${v["parentHash"]}</code><br/>`
out += `<em>time</em>: <code>${v["timestamp"]}</code><br/>`
if(v.hasOwnProperty("l1origin")) {
out += `<em>L1 hash</em>: <code>${v["l1origin"]["hash"]}</code><br/>`
out += `<em>L1 num</em>: <code>${v["l1origin"]["number"]}</code><br/>`
out += `<em>seq</em>: <code>${v["sequenceNumber"]}</code><br/>`
}
out += `</div>`
return out
}
async function pageTable() {
const logs = await fetchLogs();
if (logs.length === 0) {
return
}
const dataEl = $(`<div id="snapshot-tables" class="row"></div>`);
$("#logs").append(dataEl);
let numCols = 0
if (logs.length !== 0) {
numCols = logs[0].length
}
const paginationEl = $(`<div id="pagination"></div>`)
$("#logs").append(paginationEl)
paginationEl.pagination({
dataSource: logs,
pageSize: 40,
showGoInput: true,
showGoButton: true,
callback: (data, pagination) => {
let tables = []
for (var i = 0; i < numCols; i++) {
// TODO: Fix grid overflow with more than 2 rollup drivers
let html = '<div class="col-6">';
html += `<table class="table">
<thead>
<caption style="caption-side:top">${data[0][i].engine_addr}</caption>
<tr>
<th scope="col">Timestamp</th>
<th scope="col">L1Head</th>
<th scope="col">L1Current</th>
<th scope="col">L2Head</th>
<th scope="col">L2Safe</th>
<th scope="col">L2FinalizedHead</th>
</tr>
</thead>
`;
html += "<tbody>";
// TODO: it'll also be useful to indicate which rollup driver updated its state for the given timestamp
for (const record of data) {
const e = record[i];
if (e === undefined) {
// this column has reached its end
break
}
// outer stringify in title attribute escapes the content and adds the quotes for the html to be valid
// inner stringify in
// TODO: click to copy full hash
html += `<tr>
<td title="${e.event}" data-toggle="tooltip">
${e.t}
</td>
<td title="${tooltipFormat(e.l1Head)}" data-bs-html="true" data-toggle="tooltip" style="background-color:${colorCode(e.l1Head.hash)};">
${prettyHex(e.l1Head.hash)}
</td>
<td title="${tooltipFormat(e.l1Current)}" data-bs-html="true" data-toggle="tooltip" style="background-color:${colorCode(e.l1Current.hash)};">
${prettyHex(e.l1Current.hash)}
</td>
<td title="${tooltipFormat(e.l2Head)}" data-bs-html="true" data-toggle="tooltip" style="background-color:${colorCode(e.l2Head.hash)};">
${prettyHex(e.l2Head.hash)}
</td>
<td title="${tooltipFormat(e.l2Safe)}" data-bs-html="true" data-toggle="tooltip" style="background-color:${colorCode(e.l2Safe.hash)};">
${prettyHex(e.l2Safe.hash)}
</td>
<td title="${tooltipFormat(e.l2FinalizedHead)}" data-bs-html="true" data-toggle="tooltip" style="background-color:${colorCode(e.l2FinalizedHead.hash)};">
${prettyHex(e.l2FinalizedHead.hash)}
</td>
</tr>`;
}
html += "</tbody>";
html += "</table></div>";
tables.push(html);
}
const html = tables.join("\n");
dataEl.html(html);
$('[data-toggle="tooltip"]').tooltip();
}
})
}
(async () => {
pageTable()
})()
package main
import (
"bufio"
"compress/gzip"
"embed"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/fs"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/ethereum-optimism/optimism/op-service/eth"
ophttp "github.com/ethereum-optimism/optimism/op-service/httputil"
"github.com/ethereum/go-ethereum/log"
)
var (
snapshot = flag.String("snapshot", "", "path to snapshot log")
listenAddr = flag.String("addr", "", "listen address of webserver")
refresh = flag.Duration("refresh", 10*time.Second, "snapshot refresh rate")
)
var (
entries map[string][]SnapshotState
entriesMutex sync.Mutex
assetFS fs.FS
)
type SnapshotState struct {
Timestamp string `json:"t"`
EngineAddr string `json:"engine_addr"`
Event string `json:"event"` // event name
L1Head eth.L1BlockRef `json:"l1Head"` // what we see as head on L1
L1Current eth.L1BlockRef `json:"l1Current"` // l1 block that the derivation is currently using
L2Head eth.L2BlockRef `json:"l2Head"` // l2 block that was last optimistically accepted (unsafe head)
L2Safe eth.L2BlockRef `json:"l2Safe"` // l2 block that was last derived
L2FinalizedHead eth.BlockID `json:"l2FinalizedHead"` // l2 block that is irreversible
}
func (e *SnapshotState) UnmarshalJSON(data []byte) error {
t := struct {
Timestamp string `json:"t"`
EngineAddr string `json:"engine_addr"`
Event string `json:"event"`
L1Head json.RawMessage `json:"l1Head"`
L1Current json.RawMessage `json:"l1Current"`
L2Head json.RawMessage `json:"l2Head"`
L2Safe json.RawMessage `json:"l2Safe"`
L2FinalizedHead json.RawMessage `json:"l2FinalizedHead"`
}{}
if err := json.Unmarshal(data, &t); err != nil {
return err
}
e.Timestamp = t.Timestamp
e.EngineAddr = t.EngineAddr
e.Event = t.Event
unquote := func(d json.RawMessage) []byte {
s, _ := strconv.Unquote(string(d))
return []byte(s)
}
if err := json.Unmarshal(unquote(t.L1Head), &e.L1Head); err != nil {
return err
}
if err := json.Unmarshal(unquote(t.L1Current), &e.L1Current); err != nil {
return err
}
if err := json.Unmarshal(unquote(t.L2Head), &e.L2Head); err != nil {
return err
}
if err := json.Unmarshal(unquote(t.L2Safe), &e.L2Safe); err != nil {
return err
}
if err := json.Unmarshal(unquote(t.L2FinalizedHead), &e.L2FinalizedHead); err != nil {
return err
}
return nil
}
//go:embed assets
var embeddedAssets embed.FS
func main() {
flag.Parse()
log.Root().SetHandler(
log.LvlFilterHandler(log.LvlDebug, log.StreamHandler(os.Stdout, log.TerminalFormat(true))),
)
if *snapshot == "" {
log.Crit("missing required -snapshot flag")
}
sub, err := fs.Sub(embeddedAssets, "assets")
if err != nil {
log.Crit("Failed to open asset directory", "message", err)
}
assetFS = sub
go func() {
ticker := time.NewTicker(*refresh)
defer ticker.Stop()
for range ticker.C {
// TODO: incremental load
log.Info("loading snapshot...")
if err := loadSnapshot(); err != nil {
log.Error("failed to load snapshot", "err", err)
}
}
}()
runServer()
}
func loadSnapshot() error {
file, err := os.Open(*snapshot)
if err != nil {
return fmt.Errorf("%w: failed to open snapshot file", err)
}
defer file.Close()
tempEntries := make(map[string][]SnapshotState)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
var entry SnapshotState
if err := json.Unmarshal([]byte(scanner.Text()), &entry); err != nil {
return fmt.Errorf("%w: failed to decode snapshot log", err)
}
tempEntries[entry.EngineAddr] = append(tempEntries[entry.EngineAddr], entry)
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("%w: failed to scan snapshot file", err)
}
entriesMutex.Lock()
entries = tempEntries
entriesMutex.Unlock()
return nil
}
func runServer() {
l, err := net.Listen("tcp", *listenAddr)
if err != nil {
log.Crit("Failed to listen on address", "message", err)
}
mux := http.NewServeMux()
mux.Handle("/", http.FileServer(http.FS(assetFS)))
mux.HandleFunc("/logs", makeGzipHandler(logsHandler))
log.Info("running webserver...")
httpServer := ophttp.NewHttpServer(mux)
if err := httpServer.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Crit("http server failed", "message", err)
}
}
type gzipResponseWriter struct {
io.Writer
http.ResponseWriter
}
func (w gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
func makeGzipHandler(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
fn(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
gzr := gzipResponseWriter{Writer: gz, ResponseWriter: w}
fn(gzr, r)
}
}
func logsHandler(w http.ResponseWriter, r *http.Request) {
var output [][]SnapshotState
entriesMutex.Lock()
// shallow copy so we can update the SnapshotState slice head
entriesCopy := make(map[string][]SnapshotState)
for k, v := range entries {
entriesCopy[k] = v
}
entriesMutex.Unlock()
// sort log entries and zip em up
// Each record/row contains SnapshotStates for each rollup driver
// Note that we assume each SnapshotState slice is sorted by the timestamp
for {
var min *SnapshotState
var minKey string
for k, v := range entriesCopy {
if len(v) == 0 {
continue
}
if min == nil || v[0].Timestamp < min.Timestamp {
min = &v[0]
minKey = k
}
}
if min == nil {
break
}
entriesCopy[minKey] = entriesCopy[minKey][1:]
rec := make([]SnapshotState, 0, len(entriesCopy))
rec = append(rec, *min)
for k, v := range entriesCopy {
if k != minKey && len(v) != 0 {
newEntry := v[0]
newEntry.Timestamp = min.Timestamp
rec = append(rec, newEntry)
}
}
output = append(output, rec)
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "public, max-age=100000")
if err := json.NewEncoder(w).Encode(output); err != nil {
log.Warn("failed to encode logs", "message", err)
}
}
FROM golang:1.20.4-alpine3.16 as builder
RUN apk add --no-cache make gcc musl-dev linux-headers git jq bash
COPY ./go.mod /app/go.mod
COPY ./go.sum /app/go.sum
COPY ./op-bindings /app/op-bindings
COPY ./op-service /app/op-service
WORKDIR /app/op-node
RUN go mod download -x
COPY ./op-node /app/op-node
RUN go build -o ./bin/stateviz ./cmd/stateviz
FROM alpine:3.19
COPY --from=builder /app/op-node/bin/stateviz /usr/local/bin
CMD ["stateviz"]
......@@ -171,17 +171,3 @@ services:
- "${PWD}/../.devnet/:/usr/share/nginx/html/:ro"
security_opt:
- "no-new-privileges:true"
# stateviz:
# build:
# context: ../
# dockerfile: ./ops-bedrock/Dockerfile.stateviz
# command:
# - stateviz
# - -addr=0.0.0.0:8080
# - -snapshot=/op_log/snapshot.log
# - -refresh=10s
# ports:
# - "9090:8080"
# volumes:
# - op_log:/op_log:ro
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment