package main import ( "fmt" "math" "math/bits" "strconv" "strings" ) const ( // chatHigh and chatLow are the limits of the chat view in bytes. chatHigh = 128000 chatLow = 64000 // compactionHigh and compactionLow are the limits of the compaction // view in bytes. compactionHigh = 32000 compactionLow = 16000 // pending is the text of a line that is not built yet. pending = "(not summarized yet: zoom it)" ) // nodeReader gives the built nodes of a tree. type nodeReader interface { Built(level, index int) (bool, error) Node(level, index int) (Node, bool, error) } // View is a list of tree nodes that covers messages 0 to end-1 without a gap, // oldest first. Each new message appends its leaf as a line. Once the size of // the view goes above high, one batch merges the most due sibling pairs until // the size is at most low. A batch merges only pairs whose parent is built. If // the batch cannot get to low, it continues at each new message until it does. // // The view keeps the nodes of its built lines in memory, so its size and its // text need no read of the tree. The size of a line is the size of its node // text, or the size of the pending text when the node is not built. // // For the sibling pair (l, i) and (l, i+1), due is (T - last) / 2^l, with T the // number of messages and last the ID of the last message of the pair. That is // how long ago the pair ended, measured in its own line size. With a budget of // the length of the rollback push list of the reference design, this order // makes the same merges as the push. The oldest pair wins a tie. type View struct { high int low int lines []nodeKey nodes map[nodeKey]Node end int merging bool } // viewState is the saved form of a View. The limits are not part of it. type viewState struct { Lines [][2]int `json:"lines"` Merging bool `json:"merging"` } // newView returns an empty view with the limits high and low. func newView(high, low int) *View { return &View{ high: high, low: low, nodes: make(map[nodeKey]Node), } } // restoreView returns the view of state with the limits high and low. Each line // must continue the one before it, and each line above level 0 must be built. func restoreView(state viewState, high, low int, tree nodeReader) (*View, error) { v := newView(high, low) v.merging = state.Merging for _, pair := range state.Lines { line := nodeKey{pair[0], pair[1]} if line.level < 0 || line.level >= bits.UintSize-2 || line.index < 0 || line.index< 0 && !built { return nil, fmt.Errorf("view line (%d, %d) is not built", line.level, line.index) } v.lines = append(v.lines, line) v.end = nodeEnd(line.level, line.index) } return v, nil } // load keeps the node of line when it is built, and tells if it is. func (v *View) load(tree nodeReader, line nodeKey) (bool, error) { node, built, err := tree.Node(line.level, line.index) if err != nil || !built { return false, err } v.nodes[line] = node return true, nil } // State returns the saved form of the view. func (v *View) State() viewState { state := viewState{ Lines: make([][2]int, len(v.lines)), Merging: v.merging, } for n, line := range v.lines { state.Lines[n] = [2]int{line.level, line.index} } return state } // Append adds the leaf of the next message as a line and merges when the view // is too large. It tells if a merge changed the view. func (v *View) Append(tree nodeReader) (bool, error) { line := nodeKey{0, v.end} _, err := v.load(tree, line) if err != nil { return false, err } v.lines = append(v.lines, line) v.end++ return v.fit(tree) } // fit merges the most due pairs while a batch runs, and tells if a merge // changed the view. func (v *View) fit(tree nodeReader) (bool, error) { size := v.Size() if size > v.high { v.merging = true } merged := false for v.merging && size > v.low { best, err := v.mostDue(tree) if err != nil || best < 0 { return merged, err } left, right := v.lines[best], v.lines[best+1] parent := nodeKey{left.level + 1, left.index / 2} _, err = v.load(tree, parent) if err != nil { return merged, err } size += v.lineSize(parent) - v.lineSize(left) - v.lineSize(right) delete(v.nodes, left) delete(v.nodes, right) v.lines[best] = parent v.lines = append(v.lines[:best+1], v.lines[best+2:]...) merged = true } v.merging = false return merged, nil } // Coarsen takes each merge of other, a view of the same messages, and then // merges in one batch until the size is at most low. Each line of the result is // the coarser of the two lines that start at its first message. Dyadic ranges // either nest or do not overlap, so the result covers the messages without a // gap. func (v *View) Coarsen(other *View, tree nodeReader) error { lines := make([]nodeKey, 0, len(v.lines)) nodes := make(map[nodeKey]Node, len(v.nodes)) mine, theirs := 0, 0 for start := 0; start < v.end; { line := v.lines[mine] node, built := v.nodes[line] if other.lines[theirs].level > line.level { line = other.lines[theirs] node, built = other.nodes[line] } lines = append(lines, line) if built { nodes[line] = node } start = nodeEnd(line.level, line.index) for mine < len(v.lines) && v.lines[mine].index<= 0 && due <= bestDue { continue } built, err := tree.Built(left.level+1, left.index/2) if err != nil { return -1, err } if built { best = n bestDue = due } } return best, nil } // Refresh keeps node when it is the node of a pending line. func (v *View) Refresh(node Node) { key := nodeKey{node.Level, node.Index} for n := len(v.lines) - 1; n >= 0; n-- { line := v.lines[n] if line == key { v.nodes[key] = node return } if line.index< tags, with the newlines of the text as spaces. func (v *View) Render() string { return v.RenderWithout(nil) } // RenderWithout returns the view as Render does, without the leaf lines of the // messages in skip. A line that merges such a message with others stays. func (v *View) RenderWithout(skip map[int]bool) string { var text strings.Builder text.WriteString("\n") for _, line := range v.lines { if line.level == 0 && skip[line.index] { continue } v.renderLine(&text, line) } text.WriteString("") return text.String() } // renderLine writes one view line, "id+n|text", and a newline. func (v *View) renderLine(text *strings.Builder, line nodeKey) { body := pending node, built := v.nodes[line] if built { body = strings.ReplaceAll(node.Text, "\n", " ") } text.WriteString(strconv.Itoa(line.index << line.level)) text.WriteByte('+') text.WriteString(strconv.Itoa(1 << line.level)) text.WriteByte('|') text.WriteString(body) text.WriteByte('\n') } // Context returns the lines of the view before message end as the model reads // them. It stops at the first line that is not built, so a compaction never // sees a pending mark or a part of a message. func (v *View) Context(end int) string { var text strings.Builder text.WriteString("\n") for _, line := range v.lines { _, built := v.nodes[line] if !built || nodeEnd(line.level, line.index) > end { break } v.renderLine(&text, line) } text.WriteString("") return text.String() }