mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-04 10:41:14 +00:00
Similar to video compression which uses key-frames and differences between them: every N publishes we send a full report, but inbetween we only send what has changed. Fairly simple approach in the probe - hold on to the last full report, and for the deltas remove anything that would be merged in from the full report. On the receiving side in the app it already merges a set of reports together to produce the final output for rendering, so provided N is smaller than that set we don't need to do anything different. Deltas don't need to represent nodes that have disappeared - an earlier full node will have that node so it would be merged into the final output anyway.
330 lines
10 KiB
Bash
Executable File
330 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Generate concrete implementations of LatestMap.
|
|
#
|
|
# e.g.
|
|
# $ generate_latest_map ./report/out.go string NodeControlData ...
|
|
#
|
|
# Depends on:
|
|
# - gofmt
|
|
|
|
function generate_header() {
|
|
local out_file="${1}"
|
|
local cmd="${2}"
|
|
|
|
cat <<EOF >"${out_file}"
|
|
// Generated file, do not edit.
|
|
// To regenerate, run ${cmd}
|
|
|
|
package report
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/ugorji/go/codec"
|
|
)
|
|
EOF
|
|
}
|
|
|
|
function generate_latest_map() {
|
|
local out_file="$1"
|
|
local data_type="$2"
|
|
local uppercase_data_type="${data_type^}"
|
|
local lowercase_data_type="${data_type,}"
|
|
local entry_type="${lowercase_data_type}LatestEntry"
|
|
local latest_map_type="${uppercase_data_type}LatestMap"
|
|
local make_function="Make${latest_map_type}"
|
|
|
|
# shellcheck disable=SC2016
|
|
local json_timestamp='`json:"timestamp"`'
|
|
# shellcheck disable=SC2016
|
|
local json_value='`json:"value"`'
|
|
|
|
cat <<EOF >>"${out_file}"
|
|
type ${entry_type} struct {
|
|
key string
|
|
Timestamp time.Time ${json_timestamp}
|
|
Value ${data_type} ${json_value}
|
|
dummySelfer
|
|
}
|
|
|
|
// String returns the StringLatestEntry's string representation.
|
|
func (e *${entry_type}) String() string {
|
|
return fmt.Sprintf("%v (%s)", e.Value, e.Timestamp.Format(time.RFC3339))
|
|
}
|
|
|
|
// Equal returns true if the supplied StringLatestEntry is equal to this one.
|
|
func (e *${entry_type}) Equal(e2 *${entry_type}) bool {
|
|
return e.Timestamp.Equal(e2.Timestamp) && e.Value == e2.Value
|
|
}
|
|
|
|
// ${latest_map_type} holds latest ${data_type} instances, as a slice sorted by key.
|
|
type ${latest_map_type} []${entry_type}
|
|
|
|
// ${make_function} makes an empty ${latest_map_type}.
|
|
func ${make_function}() ${latest_map_type} {
|
|
return ${latest_map_type}{}
|
|
}
|
|
|
|
// Size returns the number of elements.
|
|
func (m ${latest_map_type}) Size() int {
|
|
return len(m)
|
|
}
|
|
|
|
// Merge produces a ${latest_map_type} containing the keys from both inputs.
|
|
// When both inputs contain the same key, the newer value is used.
|
|
// Tries to return one of its inputs, if that already holds the correct result.
|
|
func (m ${latest_map_type}) Merge(n ${latest_map_type}) ${latest_map_type} {
|
|
switch {
|
|
case len(m) == 0:
|
|
return n
|
|
case len(n) == 0:
|
|
return m
|
|
}
|
|
if len(n) > len(m) {
|
|
m, n = n, m //swap so m is always at least as long as n
|
|
} else if len(n) == len(m) && m[0].Timestamp.Before(n[0].Timestamp) {
|
|
// Optimise common case where we merge two nodes with the same contents
|
|
// sampled at different times.
|
|
m, n = n, m // swap equal-length arrays so first element of m is newer
|
|
}
|
|
|
|
i, j := 0, 0
|
|
loop:
|
|
for i < len(m) {
|
|
switch {
|
|
case j >= len(n):
|
|
return m
|
|
case m[i].key == n[j].key:
|
|
if m[i].Timestamp.Before(n[j].Timestamp) {
|
|
break loop
|
|
}
|
|
i++
|
|
j++
|
|
case m[i].key < n[j].key:
|
|
i++
|
|
default:
|
|
break loop
|
|
}
|
|
}
|
|
if i >= len(m) && j >= len(n) {
|
|
return m
|
|
}
|
|
|
|
out := make([]${entry_type}, i, len(m))
|
|
copy(out, m[:i])
|
|
|
|
for i < len(m) {
|
|
switch {
|
|
case j >= len(n):
|
|
out = append(out, m[i:]...)
|
|
return out
|
|
case m[i].key == n[j].key:
|
|
if m[i].Timestamp.Before(n[j].Timestamp) {
|
|
out = append(out, n[j])
|
|
} else {
|
|
out = append(out, m[i])
|
|
}
|
|
i++
|
|
j++
|
|
case m[i].key < n[j].key:
|
|
out = append(out, m[i])
|
|
i++
|
|
default:
|
|
out = append(out, n[j])
|
|
j++
|
|
}
|
|
}
|
|
out = append(out, n[j:]...)
|
|
return out
|
|
}
|
|
|
|
// Lookup the value for the given key.
|
|
func (m ${latest_map_type}) Lookup(key string) (${data_type}, bool) {
|
|
v, _, ok := m.LookupEntry(key)
|
|
if !ok {
|
|
var zero ${data_type}
|
|
return zero, false
|
|
}
|
|
return v, true
|
|
}
|
|
|
|
// LookupEntry returns the raw entry for the given key.
|
|
func (m ${latest_map_type}) LookupEntry(key string) (${data_type}, time.Time, bool) {
|
|
i := sort.Search(len(m), func(i int) bool {
|
|
return m[i].key >= key
|
|
})
|
|
if i < len(m) && m[i].key == key {
|
|
return m[i].Value, m[i].Timestamp, true
|
|
}
|
|
var zero ${data_type}
|
|
return zero, time.Time{}, false
|
|
}
|
|
|
|
// locate the position where key should go, and make room for it if not there already
|
|
func (m *${latest_map_type}) locate(key string) int {
|
|
i := sort.Search(len(*m), func(i int) bool {
|
|
return (*m)[i].key >= key
|
|
})
|
|
// i is now the position where key should go, either at the end or in the middle
|
|
if i == len(*m) || (*m)[i].key != key {
|
|
*m = append(*m, ${entry_type}{})
|
|
copy((*m)[i+1:], (*m)[i:])
|
|
(*m)[i] = ${entry_type}{}
|
|
}
|
|
return i
|
|
}
|
|
|
|
// Set the value for the given key.
|
|
func (m ${latest_map_type}) Set(key string, timestamp time.Time, value ${data_type}) ${latest_map_type} {
|
|
i := sort.Search(len(m), func(i int) bool {
|
|
return m[i].key >= key
|
|
})
|
|
// i is now the position where key should go, either at the end or in the middle
|
|
oldEntries := m
|
|
if i == len(m) {
|
|
m = make([]${entry_type}, len(oldEntries)+1)
|
|
copy(m, oldEntries)
|
|
} else if m[i].key == key {
|
|
m = make([]${entry_type}, len(oldEntries))
|
|
copy(m, oldEntries)
|
|
} else {
|
|
m = make([]${entry_type}, len(oldEntries)+1)
|
|
copy(m, oldEntries[:i])
|
|
copy(m[i+1:], oldEntries[i:])
|
|
}
|
|
m[i] = ${entry_type}{key: key, Timestamp: timestamp, Value: value}
|
|
return m
|
|
}
|
|
|
|
// ForEach executes fn on each key value pair in the map.
|
|
func (m ${latest_map_type}) ForEach(fn func(k string, timestamp time.Time, v ${data_type})) {
|
|
for _, value := range m {
|
|
fn(value.key, value.Timestamp, value.Value)
|
|
}
|
|
}
|
|
|
|
// String returns the ${latest_map_type}'s string representation.
|
|
func (m ${latest_map_type}) String() string {
|
|
buf := bytes.NewBufferString("{")
|
|
for _, val := range m {
|
|
fmt.Fprintf(buf, "%s: %s,\n", val.key, val.String())
|
|
}
|
|
fmt.Fprintf(buf, "}")
|
|
return buf.String()
|
|
}
|
|
|
|
// DeepEqual tests equality with other ${latest_map_type}.
|
|
func (m ${latest_map_type}) DeepEqual(n ${latest_map_type}) bool {
|
|
if m.Size() != n.Size() {
|
|
return false
|
|
}
|
|
for i := range m {
|
|
if m[i].key != n[i].key || !m[i].Equal(&n[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// EqualIgnoringTimestamps returns true if all keys and values are the same.
|
|
func (m ${latest_map_type}) EqualIgnoringTimestamps(n ${latest_map_type}) bool {
|
|
if m.Size() != n.Size() {
|
|
return false
|
|
}
|
|
for i := range m {
|
|
if m[i].key != n[i].key || m[i].Value != n[i].Value {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// CodecEncodeSelf implements codec.Selfer.
|
|
// Duplicates the output for a built-in map without generating an
|
|
// intermediate copy of the data structure, to save time. Note this
|
|
// means we are using undocumented, internal APIs, which could break
|
|
// in the future. See https://github.com/weaveworks/scope/pull/1709
|
|
// for more information.
|
|
func (m ${latest_map_type}) CodecEncodeSelf(encoder *codec.Encoder) {
|
|
z, r := codec.GenHelperEncoder(encoder)
|
|
if m == nil {
|
|
r.EncodeNil()
|
|
return
|
|
}
|
|
r.EncodeMapStart(m.Size())
|
|
for _, val := range m {
|
|
z.EncSendContainerState(containerMapKey)
|
|
r.EncodeString(cUTF8, val.key)
|
|
z.EncSendContainerState(containerMapValue)
|
|
val.CodecEncodeSelf(encoder)
|
|
}
|
|
z.EncSendContainerState(containerMapEnd)
|
|
}
|
|
|
|
// CodecDecodeSelf implements codec.Selfer.
|
|
// Decodes the input as for a built-in map, without creating an
|
|
// intermediate copy of the data structure to save time. Uses
|
|
// undocumented, internal APIs as for CodecEncodeSelf.
|
|
func (m *${latest_map_type}) CodecDecodeSelf(decoder *codec.Decoder) {
|
|
*m = nil
|
|
z, r := codec.GenHelperDecoder(decoder)
|
|
if r.TryDecodeAsNil() {
|
|
return
|
|
}
|
|
|
|
length := r.ReadMapStart()
|
|
if length > 0 {
|
|
*m = make([]${entry_type}, 0, length)
|
|
}
|
|
for i := 0; length < 0 || i < length; i++ {
|
|
if length < 0 && r.CheckBreak() {
|
|
break
|
|
}
|
|
z.DecSendContainerState(containerMapKey)
|
|
var key string
|
|
if !r.TryDecodeAsNil() {
|
|
key = lookupCommonKey(r.DecodeStringAsBytes())
|
|
}
|
|
i := m.locate(key)
|
|
(*m)[i].key = key
|
|
z.DecSendContainerState(containerMapValue)
|
|
if !r.TryDecodeAsNil() {
|
|
(*m)[i].CodecDecodeSelf(decoder)
|
|
}
|
|
}
|
|
z.DecSendContainerState(containerMapEnd)
|
|
}
|
|
|
|
// MarshalJSON shouldn't be used, use CodecEncodeSelf instead.
|
|
func (${latest_map_type}) MarshalJSON() ([]byte, error) {
|
|
panic("MarshalJSON shouldn't be used, use CodecEncodeSelf instead")
|
|
}
|
|
|
|
// UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead.
|
|
func (*${latest_map_type}) UnmarshalJSON(b []byte) error {
|
|
panic("UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead")
|
|
}
|
|
EOF
|
|
}
|
|
|
|
if [ -z "${1}" ]; then
|
|
echo "No output file given"
|
|
exit 1
|
|
fi
|
|
|
|
out="${1}"
|
|
outtmp="${out}.tmp"
|
|
|
|
generate_header "${outtmp}" "${0} ${*}"
|
|
shift
|
|
for t in "${@}"; do
|
|
generate_latest_map "${outtmp}" "${t}"
|
|
done
|
|
|
|
gofmt -s -w "${outtmp}"
|
|
mv "${outtmp}" "${out}"
|