package querycontext
import (
"fmt"
"io"
"os"
"strings"
)
// Rendering time!
// We want to be able to render an rbfTxStore, or a QueryContext, or the whole system.
//
// Here's the things we have:
// rbfDBQueryContexts
// rbfTxStore
// rbfTxWrappers
// rbfQueryContext
// rbfQueryRead
// rbfQueryWrite
// rbf.DB
// rbf.Tx
// QueryScope
//
// Relationships that exist:
// rbfTxStore -> map[dbKey]rbfDBQueryContexts -> {one rbf.DB, map[rbfTxWrappers]rbfQueryContext}
// rbfQueryContext -> many rbfTxWrappers -> {one rbf.Tx, one rbfDBQueryContext, map[txKey]QueryRead}
// rbfQueryRead -> rbfTxWrappers
//
// For a TxStore, it's useful to graph the set of rbfDBQueryContexts it
// has (which represent live databases), and the QueryContexts associated
// with them. For a QueryContext, it's useful to graph the set of
// rbfTxWrappers it has, and also its scope.
//
// It is also interesting to graph the whole thing. There should always
// be a round trip in these relations; an rbfTxWrapper always maps to
// an rbfDBQueryContext, which should always have a reference back to
// the rbfQueryContext that owns the rbfTxWrapper. Similarly, if
// you have an rbfDBQueryContext, every rbfTxWrapper it references
// has a Tx against the associated DB.
// colors: ['#d7191c','#fdae61','#ffffbf','#abd9e9','#2c7bb6'] is a
// colorblind-friendly palette obtained from ColorBrewer. We assign
// meanings:
const (
colorBlack = "#000000"
colorWhite = "#ffffff"
colorWrite = "#fdae61"
colorRead = "#abd9e9"
colorError = "#d7191c"
colorUnused1 = "#ffffbf"
colorUnused2 = "##2c7bb6"
)
var colorWritable = map[bool]string{
false: colorRead,
true: colorWrite,
}
type dotNode interface {
writeNode(io.Writer)
addTo(*dotGraph)
dotId() string
dotClass() string
}
type dotEdge interface {
writeEdge(io.Writer, *dotGraph)
}
// dotGraph stores a list of nodes and edges it's found so far.
// we also have a queue. the reason is that if you don't, descent
// is depth-first, and this can lead to finding a node with very
// little depth left before you would have found it with a higher
// depth, and potentially not recording its edges.
type dotGraph struct {
depth int
queue []dotNode
edges []dotEdge
nodes map[dotNode]string
orderedNodes []dotNode
clusters map[string][]dotNode
}
func (dg *dotGraph) enqueue(n dotNode) {
if _, ok := dg.nodes[n]; ok {
return
}
dg.queue = append(dg.queue, n)
}
// build starts with the enqueued roots and processes
// them, looking for new things, depth times.
func (dg *dotGraph) build(depth int) {
if dg.nodes == nil {
dg.nodes = make(map[dotNode]string)
}
dg.depth = depth
for len(dg.queue) > 0 && dg.depth > 0 {
working := dg.queue
dg.queue = []dotNode{}
for _, node := range working {
node.addTo(dg)
}
dg.depth--
}
}
// consider checks whether we're sufficiently done with this item, which
// means either we've already seen it, or we haven't but we're out of depth.
func (dg *dotGraph) consider(n dotNode) bool {
if _, ok := dg.nodes[n]; ok {
return true
}
dg.nodes[n] = n.dotId()
// maintain a list in the order we saw them, so items at the top of
// a given list will correspond to items at the top of the next list too
dg.orderedNodes = append(dg.orderedNodes, n)
return dg.depth < 1
}
var nodeQualities = map[string][2]map[string]string{
"rdqc": {{"rank": "same"}, {"shape": "cylinder"}},
"txw": {{"rank": "same"}, {"shape": "folder"}},
"qrw": {{"rank": "same"}, {"shape": "note"}},
"txstore": {{"rank": "same"}, {"shape": "hexagon"}},
"rqc": {{"rank": "same"}, {"shape": "box3d"}},
}
var nodeOrder = []string{"txstore", "rdqc", "txw", "qrw", "rqc"}
func (dg *dotGraph) WriteClass(w io.Writer, class string) error {
nodes := dg.clusters[class]
if len(nodes) == 0 {
return nil
}
fmt.Fprintf(w, "\tsubgraph %s {\n", class)
if qualities, ok := nodeQualities[class]; ok {
for k, v := range qualities[0] {
fmt.Fprintf(w, "\t\t%s=%q\n", k, v)
}
if len(qualities[1]) > 0 {
fmt.Fprintf(w, "\t\tnode [")
for k, v := range qualities[1] {
fmt.Fprintf(w, "%s=%q", k, v)
}
fmt.Fprintln(w, "]")
}
}
for _, node := range nodes {
fmt.Fprintf(w, "\t\t\"%s\" ", dg.nodes[node])
node.writeNode(w)
fmt.Fprintln(w, "")
}
fmt.Fprintf(w, "\t}\n")
return nil
}
func (dg *dotGraph) Write(w io.Writer) error {
w.Write([]byte(`
digraph g {
rankdir = "LR"
newrank = true
concentrate = true
`))
dg.clusters = make(map[string][]dotNode)
classes := make(map[string]struct{})
for _, node := range dg.orderedNodes {
class := node.dotClass()
dg.clusters[class] = append(dg.clusters[class], node)
classes[class] = struct{}{}
}
for _, class := range nodeOrder {
err := dg.WriteClass(w, class)
if err != nil {
return err
}
delete(classes, class)
}
if len(classes) > 0 {
return fmt.Errorf("unordered classes: %v", classes)
}
for _, edge := range dg.edges {
edge.writeEdge(w, dg)
}
fmt.Fprintln(w, "}")
return nil
}
// pointerEdge is a naive edge that just represents two pointer-shaped things
type pointerEdge struct {
from, to dotNode
label string
style string
}
func (d *pointerEdge) writeEdge(w io.Writer, dg *dotGraph) {
var style string
if d.style != "" {
style = fmt.Sprintf(" style=%q", d.style)
}
fmt.Fprintf(w, " %q -> %q [label=%q%s]\n", dg.nodes[d.from], dg.nodes[d.to], d.label, style)
}
func (dg *dotGraph) addEdge(from, to dotNode, extra ...string) {
var label, style string
switch len(extra) {
case 2:
style = extra[1]
fallthrough
case 1:
label = extra[0]
}
edge := &pointerEdge{from, to, label, style}
dg.edges = append(dg.edges, edge)
}
// RBF backend to Graphviz glue
func (r *rbfTxStore) addTo(dg *dotGraph) {
if dg.consider(r) {
return
}
for _, dbqc := range r.dbs {
dg.enqueue(dbqc)
dg.addEdge(r, dbqc)
}
}
func (r *rbfDBQueryContexts) addTo(dg *dotGraph) {
if dg.consider(r) {
return
}
for tx, qc := range r.queryContexts {
dg.enqueue(qc)
dg.enqueue(tx)
dg.addEdge(r, qc)
dg.addEdge(r, tx, qc.name)
}
}
func (r *rbfQueryContext) addTo(dg *dotGraph) {
if dg.consider(r) {
return
}
dg.addEdge(r, r.txStore, "", "dashed")
for _, wrappers := range r.queries {
dg.enqueue(wrappers)
dg.addEdge(r, wrappers)
}
}
func (r *rbfTxWrappers) addTo(dg *dotGraph) {
if dg.consider(r) {
return
}
dg.enqueue(r.db)
dg.addEdge(r, r.db, "", "dashed")
for _, reader := range r.queries {
switch v := reader.(type) {
case *rbfQueryRead:
dg.enqueue(v)
dg.addEdge(r, v)
case *rbfQueryWrite:
dg.enqueue(v)
dg.addEdge(r, v)
default:
unk := &unknownEntry{v}
dg.enqueue(unk)
dg.addEdge(r, unk)
}
}
}
func (r *rbfTxStore) dotClass() string {
return "txstore"
}
func (r *rbfTxStore) dotId() string {
return fmt.Sprintf("txstore-%p", r)
}
func (r *rbfTxStore) writeNode(w io.Writer) {
// long root path is annoying to read, so
temp := os.TempDir()
path := r.rootPath
if strings.HasPrefix(r.rootPath, temp+"/") {
path = ".../" + strings.TrimPrefix(r.rootPath, temp+"/")
}
fmt.Fprintf(w, `[label=%s>]`, path)
}
func (r *rbfDBQueryContexts) dotClass() string {
return "rdqc"
}
func (r *rbfDBQueryContexts) dotId() string {
return fmt.Sprintf("rdqc-%p", r)
}
func (r *rbfDBQueryContexts) writeNode(w io.Writer) {
var locked string
var maybeError = colorBlack
if r.lockCheck.TryLock() {
r.lockCheck.Unlock()
} else {
locked = fmt.Sprintf("
[LOCKED]", colorWrite)
}
if r.db == nil {
maybeError = colorError
}
fmt.Fprintf(w, `[label=%s
%s%s> color=%q]`, r.key, r.dbPath, locked, maybeError)
}
func (r *rbfTxWrappers) dotClass() string {
return "txw"
}
func (r *rbfTxWrappers) dotId() string {
return fmt.Sprintf("txw-%p", r)
}
func (r *rbfTxWrappers) writeNode(w io.Writer) {
var write string
if r.writeTx {
write = "write "
}
fmt.Fprintf(w, `[label=%s
[%sTx %p]> style="filled" fillcolor=%q]`, r.key, write, r.tx, colorWritable[r.writeTx])
}
func (r *rbfQueryContext) dotClass() string {
return "rqc"
}
func (r *rbfQueryContext) dotId() string {
return fmt.Sprintf("rqc-%p", r)
}
func (r *rbfQueryContext) writeNode(w io.Writer) {
writeScope := "[read only]"
if r.scope != nil {
writeScope = r.scope.String()
if len(writeScope) > 20 {
writeScope = writeScope[:20] + "..."
}
writeScope = fmt.Sprintf("scope: %s", writeScope)
}
fmt.Fprintf(w, `[label=%s
%s> style="filled" fillcolor=%q]`, r.name, writeScope, colorWritable[r.scope != nil])
}
func (r *rbfQueryRead) dotClass() string {
return "qrw"
}
func (r *rbfQueryRead) dotId() string {
return fmt.Sprintf("qrw-%p", r)
}
func (r *rbfQueryRead) addTo(dg *dotGraph) {
dg.consider(r)
}
func (r *rbfQueryRead) writeNode(w io.Writer) {
fmt.Fprintf(w, `[label=[%s]> style="filled" fillcolor=%q]`, r.fk, colorRead)
}
func (r *rbfQueryWrite) dotClass() string {
return "qrw"
}
func (r *rbfQueryWrite) dotId() string {
return fmt.Sprintf("qrw-%p", r)
}
func (r *rbfQueryWrite) addTo(dg *dotGraph) {
dg.consider(r)
}
func (r *rbfQueryWrite) writeNode(w io.Writer) {
fmt.Fprintf(w, `[label=[%s]> style="filled" fillcolor=%q]`, r.fk, colorWrite)
}
type unknownEntry struct {
data interface{}
}
func (u *unknownEntry) dotClass() string {
return "unk"
}
func (u *unknownEntry) dotId() string {
return fmt.Sprintf("unk-%p", u)
}
func (u *unknownEntry) writeNode(w io.Writer) {
fmt.Fprintf(w, "[label=\"unknown %T\"]", u.data)
}
func (u *unknownEntry) addTo(dg *dotGraph) {
dg.consider(u)
}