-
Notifications
You must be signed in to change notification settings - Fork 54
/
Copy pathvisualization.go
207 lines (193 loc) Β· 6.17 KB
/
visualization.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package porcupine
import (
"embed"
"encoding/json"
"fmt"
"io"
"os"
"sort"
)
type historyElement struct {
ClientId int
Start int64
End int64
Description string
}
type annotation struct {
ClientId int
Tag string
Start int64
End int64
Description string
Details string
Annotation bool // always true
TextColor string
BackgroundColor string
}
type linearizationStep struct {
Index int
StateDescription string
}
type partialLinearization = []linearizationStep
type partitionVisualizationData struct {
History []historyElement
PartialLinearizations []partialLinearization
Largest map[int]int
}
type visualizationData struct {
Partitions []partitionVisualizationData
Annotations []annotation
}
// Annotations to add to histories.
//
// Either a ClientId or Tag must be supplied. The End is optional, for "point
// in time" annotations. If the end is left unspecified, the framework
// interprets it as Start. The text given in Description is shown in the main
// visualization, and the text given in Details (optional) is shown in the
// tooltip for the annotation. TextColor and BackgroundColor are both optional;
// if specified, they should be valid CSS colors, e.g., "#efaefc".
//
// To attach annotations to a visualization, use
// [LinearizationInfo.AddAnnotations].
type Annotation struct {
ClientId int
Tag string
Start int64
End int64
Description string
Details string
TextColor string
BackgroundColor string
}
// AddAnnotations adds extra annotations to a visualization.
//
// This can be used to add extra client operations or it can be used to add
// standalone annotations with arbitrary tags, e.g., associated with "servers"
// rather than clients, or even a "test framework".
//
// See documentation on [Annotation] for what kind of annotations you can add.
func (li *LinearizationInfo) AddAnnotations(annotations []Annotation) {
for _, elem := range annotations {
end := elem.End
if end < elem.Start {
end = elem.Start
}
li.annotations = append(li.annotations, annotation{
ClientId: elem.ClientId,
Tag: elem.Tag,
Start: elem.Start,
End: end,
Description: elem.Description,
Details: elem.Details,
Annotation: true,
TextColor: elem.TextColor,
BackgroundColor: elem.BackgroundColor,
})
}
}
func computeVisualizationData(model Model, info LinearizationInfo) visualizationData {
model = fillDefault(model)
partitions := make([]partitionVisualizationData, len(info.history))
for partition := 0; partition < len(info.history); partition++ {
// history
n := len(info.history[partition]) / 2
history := make([]historyElement, n)
callValue := make(map[int]interface{})
returnValue := make(map[int]interface{})
for _, elem := range info.history[partition] {
switch elem.kind {
case callEntry:
history[elem.id].ClientId = elem.clientId
history[elem.id].Start = elem.time
callValue[elem.id] = elem.value
case returnEntry:
history[elem.id].End = elem.time
history[elem.id].Description = model.DescribeOperation(callValue[elem.id], elem.value)
returnValue[elem.id] = elem.value
}
// historyElement.Annotation defaults to false, so we
// don't need to explicitly set it here; all of these
// are non-annotation elements
}
// partial linearizations
largestIndex := make(map[int]int)
largestSize := make(map[int]int)
linearizations := make([]partialLinearization, len(info.partialLinearizations[partition]))
partials := info.partialLinearizations[partition]
sort.Slice(partials, func(i, j int) bool {
return len(partials[i]) > len(partials[j])
})
for i, partial := range partials {
linearization := make(partialLinearization, len(partial))
state := model.Init()
for j, histId := range partial {
var ok bool
ok, state = model.Step(state, callValue[histId], returnValue[histId])
if !ok {
panic("valid partial linearization returned non-ok result from model step")
}
stateDesc := model.DescribeState(state)
linearization[j] = linearizationStep{histId, stateDesc}
if largestSize[histId] < len(partial) {
largestSize[histId] = len(partial)
largestIndex[histId] = i
}
}
linearizations[i] = linearization
}
partitions[partition] = partitionVisualizationData{
History: history,
PartialLinearizations: linearizations,
Largest: largestIndex,
}
}
annotations := info.annotations
if annotations == nil {
annotations = make([]annotation, 0)
}
data := visualizationData{
Partitions: partitions,
Annotations: annotations,
}
return data
}
// Visualize produces a visualization of a history and (partial) linearization
// as an HTML file that can be viewed in a web browser.
//
// If the history is linearizable, the visualization shows the linearization of
// the history. If the history is not linearizable, the visualization shows
// partial linearizations and illegal linearization points.
//
// To get the LinearizationInfo that this function requires, you can use
// [CheckOperationsVerbose] / [CheckEventsVerbose].
//
// This function writes the visualization, an HTML file with embedded
// JavaScript and data, to the given output.
func Visualize(model Model, info LinearizationInfo, output io.Writer) error {
data := computeVisualizationData(model, info)
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
templateB, _ := visualizationFS.ReadFile("visualization/index.html")
template := string(templateB)
css, _ := visualizationFS.ReadFile("visualization/index.css")
js, _ := visualizationFS.ReadFile("visualization/index.js")
_, err = fmt.Fprintf(output, template, css, js, jsonData)
if err != nil {
return err
}
return nil
}
// VisualizePath is a wrapper around [Visualize] to write the visualization to
// a file path.
func VisualizePath(model Model, info LinearizationInfo, path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return Visualize(model, info, f)
}
//go:embed visualization
var visualizationFS embed.FS