cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Build JEV cache keys by hand instead of with encoding/json

Dami committed 4643078
JEV cells are volatile, so every recalculation looks each one up in the
cache, and every answer that arrives recalculates them all: with
thousands of JEV cells, encoding each key as JSON took most of the
time. Key now writes the shapes a RemoteCall holds directly, tagged and
length-prefixed so calls never collide (tested), with JSON as the
fallback for anything else. Keys aren't persisted, so the format can
change.

  BenchmarkJEV/1000 (drain 1000 answers)  575 ms -> 277 ms (274 us/answer)
  BenchmarkJEV/4000                       9.33 s -> 4.87 s
  Key                                     174 ns, 7 allocs
commit
464307812aa177bb9df1a6ad77e0e45f6c94ff57
parent
6176a98ff1acc0d45fdd526229a0cac1e7d44127
author
Dami <dami.etoile@gmail.com>
3 files changed, +153 -10
Minternal/jev/jev.go10---
Ainternal/jev/key.go96++++++++++++++++++++++++++++++
Ainternal/jev/key_test.go57++++++++++++++++++
M internal/jev/jev.go +0 -10
A internal/jev/key.go +96 -0
@@ -0,0 +1,96 @@
1+package jev2+3+import (4+ "encoding/json"5+ "fmt"6+ "maps"7+ "slices"8+ "strconv"9+ "strings"10+11+ "012/internal/sheet"12+)13+14+// Key identifies a question by its content. JEV cells are volatile, so15+// every recalculation looks each one up: with thousands of them,16+// encoding the key with encoding/json took most of the time an answer17+// spent in recalc. The shapes a RemoteCall holds are encoded by hand,18+// each value tagged and each string length-prefixed so different calls19+// never share a key; anything else falls back to JSON.20+func Key(c sheet.RemoteCall) string {21+ var b strings.Builder22+ b.Grow(64 + len(c.Instructions))23+ str(&b, c.Kind)24+ str(&b, c.Instructions)25+ if appendValue(&b, c.State) && appendValue(&b, c.Criteria) {26+ return b.String()27+ }28+ j, err := json.Marshal(c)29+ if err != nil {30+ return fmt.Sprintf("%#v", c)31+ }32+ return "j" + string(j)33+}34+35+func str(b *strings.Builder, s string) {36+ b.WriteString(strconv.Itoa(len(s)))37+ b.WriteByte(':')38+ b.WriteString(s)39+}40+41+// appendValue encodes v and reports whether it is a shape it knows.42+func appendValue(b *strings.Builder, v any) bool {43+ switch v := v.(type) {44+ case nil:45+ b.WriteByte('n')46+ case string:47+ b.WriteByte('s')48+ str(b, v)49+ case float64:50+ b.WriteByte('f')51+ b.WriteString(strconv.FormatFloat(v, 'g', -1, 64))52+ b.WriteByte(';')53+ case bool:54+ if v {55+ b.WriteByte('T')56+ } else {57+ b.WriteByte('F')58+ }59+ case [2]string:60+ b.WriteByte('p')61+ str(b, v[0])62+ str(b, v[1])63+ case []string:64+ b.WriteByte('l')65+ b.WriteString(strconv.Itoa(len(v)))66+ for _, s := range v {67+ str(b, s)68+ }69+ case map[string]string:70+ b.WriteByte('m')71+ b.WriteString(strconv.Itoa(len(v)))72+ for _, k := range slices.Sorted(maps.Keys(v)) {73+ str(b, k)74+ str(b, v[k])75+ }76+ case []any:77+ b.WriteByte('a')78+ b.WriteString(strconv.Itoa(len(v)))79+ for _, e := range v {80+ if !appendValue(b, e) {81+ return false82+ }83+ }84+ case [][]any:85+ b.WriteByte('r')86+ b.WriteString(strconv.Itoa(len(v)))87+ for _, row := range v {88+ if !appendValue(b, row) {89+ return false90+ }91+ }92+ default:93+ return false94+ }95+ return true96+}
A internal/jev/key_test.go +57 -0
@@ -0,0 +1,57 @@
1+package jev2+3+import (4+ "testing"5+6+ "012/internal/sheet"7+)8+9+// Every call must get its own key, and equal calls the same one, map10+// order notwithstanding.11+func TestKeysAreDistinct(t *testing.T) {12+ calls := []sheet.RemoteCall{13+ {Kind: "noul", State: "The box was crushed", Instructions: "Is this a complaint?"},14+ {Kind: "noul", State: "The box was crushed", Instructions: "Is this a complaint?", Criteria: [2]string{"yes", ""}},15+ {Kind: "noul", State: "The box was crushed", Instructions: "Is this a complaint?", Criteria: [2]string{"", "yes"}},16+ {Kind: "noul", State: "The box was crushed ", Instructions: "Is this a complaint?"},17+ {Kind: "noul", State: "The box", Instructions: " was crushedIs this a complaint?"},18+ {Kind: "noul", State: 12.0, Instructions: "q"},19+ {Kind: "noul", State: "12", Instructions: "q"},20+ {Kind: "noul", State: true, Instructions: "q"},21+ {Kind: "noul", State: "", Instructions: "q"},22+ {Kind: "noul", State: nil, Instructions: "q"},23+ {Kind: "noul", State: [][]any{{"a", 1.0}, {"b", 2.0}}, Instructions: "q"},24+ {Kind: "noul", State: [][]any{{"a", 1.0, "b", 2.0}}, Instructions: "q"},25+ {Kind: "choice", State: "x", Instructions: "q", Criteria: map[string]string{"neg": "bad", "pos": "good"}},26+ {Kind: "choice", State: "x", Instructions: "q", Criteria: map[string]string{"neg": "good", "pos": "bad"}},27+ {Kind: "score", State: "x", Instructions: "q", Criteria: []string{"low", "mid", "high"}},28+ {Kind: "score", State: "x", Instructions: "q", Criteria: []string{"low", "midhigh"}},29+ {Kind: "score", State: "x", Instructions: "q", Criteria: []int{1, 2}}, // JSON fallback30+ }31+ seen := map[string]int{}32+ for i, c := range calls {33+ k := Key(c)34+ if j, dup := seen[k]; dup {35+ t.Errorf("calls %d and %d share key %q", j, i, k)36+ }37+ seen[k] = i38+ }39+ m := map[string]string{}40+ for _, l := range []string{"a", "b", "c", "d", "e", "f"} {41+ m[l] = l + "!"42+ }43+ c := sheet.RemoteCall{Kind: "choice", State: "x", Instructions: "q", Criteria: m}44+ for range 20 {45+ if Key(c) != Key(c) {46+ t.Fatal("map order changed the key")47+ }48+ }49+}50+51+func BenchmarkKey(b *testing.B) {52+ c := sheet.RemoteCall{Kind: "choice", State: "The box was crushed", Instructions: "Sentiment",53+ Criteria: map[string]string{"negative": "negative", "positive": "positive"}}54+ for b.Loop() {55+ Key(c)56+ }57+}