Files
magpie/internal/edit/toml_shape_test.go
yetone 2a4042a94d perf: magpie opens and shows the Agents page in a fraction of the time it took (Junjie Zhou on Discord: 8 个 agent 30 个 provider … 每次我打开的时候都会很慢,要好几秒。setting 页面秒进后 我切换到 agent 就有问题) — at start, writing the agents' model lists (agent.SyncCatalog) built every provider's catalog thousands of times with nothing held, each build reading migrations.json, settings and groups again: 6000+ file reads and ~8.5s of CPU in the first 8s with the owner's 30-odd providers, all competing with the first /api/state and the Agents page. Now SyncCatalog, magpieModels and main's start-up moves run under provider.Hold, migrations, groups, settings and a provider's fetched models are held for a second like the rest, and filememo.Hold lets a GET /api/* not stat a file it confirmed unchanged within the last second (any magpie write, Changed() or a recent mtime still reads it again). A big ~/.codex/config.toml parsed in quadratic time (the TOML parser's Shape counts newlines from the start for every key): a 3 MB one took 5.4s, now 0.04s (tomlShaper). theInstalled reads through edit.Read's memo. Measured with a copy of the owner's magpie config and agent configs under a sandbox HOME, the real server in WebKit: CPU in the first 8s 8.59s → 0.39s, page up 475 → 75ms, Agents rows drawn 775 → 325ms after start; the agents' written model files are unchanged (pi byte for byte, opencode the same models). TestTOMLShaperMatchesParser, TestTOMLBigFileParsesInLinearTime (fails on the old code), TestHold (filememo), TestHoldReadsMigrationsOnce. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 03:02:40 +08:00

72 lines
2.0 KiB
Go

package edit
import (
"fmt"
"strings"
"testing"
"time"
"github.com/pelletier/go-toml/v2/unstable"
)
// tomlShaper finds every key and comment where the parser's own Shape does.
func TestTOMLShaperMatchesParser(t *testing.T) {
doc := "# top\na = 1 # one\n\n[t]\n b.c = \"x#y\" # two\n\"q\" = [\n 1,\n]\n[[arr]]\nz = { k = 1 }\n"
lines := splitLines(doc)
shapeOf := tomlShaper(lines)
p := unstable.Parser{KeepComments: true}
p.Reset([]byte(strings.Join(lines, "\n")))
n := 0
check := func(it *unstable.Node) {
if it.Raw.Length == 0 {
return
}
if got, want := shapeOf(it.Raw), p.Shape(it.Raw); got != want {
t.Errorf("%v %q: %+v, the parser says %+v", it.Kind, p.Raw(it.Raw), got, want)
}
n++
}
for p.NextExpression() {
e := p.Expression()
check(e)
if e.Kind != unstable.Comment {
for k := e.Key(); k.Next(); {
check(k.Node())
}
}
if c := e.Next(); c != nil {
check(c)
}
}
if err := p.Error(); err != nil || n < 8 {
t.Fatalf("parsed %d nodes: %v", n, err)
}
}
// A big config.toml is parsed in a time that grows with its size, not its
// square: Codex's own reached 1.5 MB, and each key's line was counted from
// the top of the file, a third of a second at every magpie start.
func TestTOMLBigFileParsesInLinearTime(t *testing.T) {
var b strings.Builder
for i := 0; b.Len() < 3<<20; i++ {
fmt.Fprintf(&b, "[projects.\"/Users/me/p%d\"]\ntrust_level = \"trusted\" # note\n\n", i)
}
b.WriteString("model_provider = \"magpie\"\n")
lines := splitLines("model_provider = \"x\"\n" + b.String())
start := time.Now()
f, err := parseTOMLFile(lines)
if err != nil {
t.Fatal(err)
}
if d := time.Since(start); d > time.Second {
t.Errorf("a 3 MB file took %v to parse", d)
}
if len(f.tables) < 30000 || f.root.keys[0].name != "model_provider" {
t.Fatalf("parsed %d tables, root %+v", len(f.tables), f.root.keys)
}
last := f.tables[len(f.tables)-1]
if k := last.keys[len(last.keys)-1]; k.name != "model_provider" || k.from != len(lines)-2 && k.from != len(lines)-1 {
t.Errorf("last key %+v of %d lines", k, len(lines))
}
}