Viewing files at commit c10cefe7abe5Commit detailsView latest

oakmark/main.go 269 lines · 9.7 KB · go
1
// Command oakmark measures per-repo push (and clone) throughput of an oak
2
// server — the oak analogue of ForgeMark. It forks ForgeMark's measurement
3
// harness (concurrency sweep, warm-up exclusion, nearest-rank latency
4
// percentiles, ops/sec → ops/hour, JSON schema in stats.go — kept verbatim for
5
// comparability) and swaps the git-over-smart-HTTP transport for oak's own
6
// protocol via one of two backends:
7
//
8
//	-transport cli       shell out to the real `oak` binary (honest real-agent
9
//	                     floor; includes client chunking + process spin-up)
10
//	-transport protocol  drive the in-process oak-engine (Rust, on oakvcs-core);
11
//	                     the ForgeMark-comparable ceiling
12
//
13
// Strategies mirror ForgeMark: branch (headline single-repo push throughput),
14
// repo (horizontal across N repos), session (clone+checkpoint read/write mix;
15
// cli-only). Each agent owns its own branch (oak's flat model), so branch/repo
16
// pushes never lose a CAS race — we measure the server's per-repo throughput.
17
package main
18
19
import (
20
	"context"
21
	"flag"
22
	"fmt"
23
	"io"
24
	"os"
25
	"strconv"
26
	"strings"
27
	"sync"
28
	"time"
29
)
30
31
// engineStderr is where every protocol-transport oak-engine child sends its
32
// diagnostics. One shared sink keeps engine noise out of stdout (which carries
33
// the JSON protocol).
34
var engineStderr io.Writer = os.Stderr
35
36
type repoRef struct{ owner, name string }
37
38
func (r repoRef) String() string { return r.owner + "/" + r.name }
39
40
func main() {
41
	remote := flag.String("remote", "http://127.0.0.1:8080", "oak server base URL (e.g. https://oak.space or a local `oak serve`)")
42
	transportKind := flag.String("transport", "cli", "push backend: cli | protocol")
43
	oakBin := flag.String("oak-bin", "oak", "path to the oak binary (cli transport + repo seeding)")
44
	engineBin := flag.String("engine-bin", "./oak-engine/target/release/oak-engine", "path to oak-engine (protocol transport)")
45
	tokenFile := flag.String("token-file", "", "file holding the oak.space bearer token; '-' for stdin. Falls back to $OAK_API_KEY")
46
	strategy := flag.String("strategy", "branch", "workload strategy: branch | repo | session")
47
	owner := flag.String("owner", "bench", "org/owner slug that owns the target repos (must exist on oak.space)")
48
	repoBase := flag.String("repo", "oakmark-tp", "target repo name (branch/session); base name for the repo strategy (repo-<n>)")
49
	repoCount := flag.Int("repo-count", 4, "number of repos for the repo strategy")
50
	concurrencyCSV := flag.String("concurrency", "1,8,32", "comma-separated agent counts to sweep")
51
	duration := flag.Duration("duration", 30*time.Second, "measurement window per concurrency level")
52
	warmup := flag.Duration("warmup", 5*time.Second, "discarded warm-up window per level")
53
	filesMin := flag.Int("files-min", 1, "min files per changeset")
54
	filesMax := flag.Int("files-max", 10, "max files per changeset")
55
	fileSize := flag.Int("file-size", 2048, "bytes per file")
56
	sessionCommits := flag.Int("session-commits", 5, "checkpoint pushes per session (session strategy)")
57
	cloneDepth := flag.Int("clone-depth", 1, "clone depth for the session read (0 = full history, else shallow)")
58
	branchPrefix := flag.String("branch-prefix", "", "branch name prefix (default: oakmark-<runid>)")
59
	out := flag.String("out", "", "write JSON results to this path (default: stdout only)")
60
	reportOut := flag.String("report", "", "write a markdown throughput report to this path")
61
	doSeed := flag.Bool("seed", true, "create/seed the target repos before the sweep")
62
	flag.Parse()
63
64
	runID := fmt.Sprintf("run%d", time.Now().Unix())
65
	prefix := *branchPrefix
66
	if prefix == "" {
67
		prefix = "oakmark-" + runID
68
	}
69
	token := loadToken(*tokenFile)
70
71
	if *strategy == "session" && *transportKind == "protocol" {
72
		fatalf("%v", errNoSession)
73
	}
74
	if *strategy != "branch" && *strategy != "repo" && *strategy != "session" {
75
		fatalf("unknown -strategy %q (want branch|repo|session)", *strategy)
76
	}
77
	if *transportKind != "cli" && *transportKind != "protocol" {
78
		fatalf("unknown -transport %q (want cli|protocol)", *transportKind)
79
	}
80
81
	repos := repoList(*owner, *repoBase, *strategy, *repoCount)
82
	levels, err := parseLevels(*concurrencyCSV)
83
	if err != nil {
84
		fatalf("bad -concurrency: %v", err)
85
	}
86
	cfg := &commitConfig{filesMin: *filesMin, filesMax: *filesMax, fileSize: *fileSize}
87
	commitFiles := fmt.Sprintf("%d-%d x %dB", cfg.filesMin, cfg.filesMax, cfg.fileSize)
88
	var sess *sessionConfig
89
	if *strategy == "session" {
90
		sess = &sessionConfig{commits: *sessionCommits, cloneDepth: *cloneDepth}
91
	}
92
93
	fmt.Fprintf(os.Stderr, "oakmark: remote=%s transport=%s strategy=%s repos=%d changeset=%q window=%s warmup=%s\n",
94
		*remote, *transportKind, *strategy, len(repos), commitFiles, *duration, *warmup)
95
96
	if *doSeed {
97
		for _, r := range repos {
98
			if err := seedRepo(*oakBin, *remote, r); err != nil {
99
				fatalf("seed %s: %v", r, err)
100
			}
101
		}
102
		fmt.Fprintf(os.Stderr, "oakmark: seeded %d repo(s)\n", len(repos))
103
	}
104
105
	runCfg := &runConfig{
106
		transport: *transportKind, oakBin: *oakBin, engineBin: *engineBin,
107
		remote: *remote, token: token, prefix: prefix, cfg: cfg, sess: sess,
108
		cloneDepth: *cloneDepth, repos: repos,
109
	}
110
111
	results := make([]levelResult, 0, len(levels))
112
	for _, level := range levels {
113
		fmt.Fprintf(os.Stderr, "oakmark: level c=%d …\n", level)
114
		samples := runLevel(runCfg, level, *warmup, *duration)
115
		r := summarize(samples, level, *strategy, len(repos), 1, *warmup, *duration, commitFiles)
116
		results = append(results, r)
117
		printLevel(r)
118
	}
119
120
	emitJSON(results, *out, runID, *remote, *transportKind)
121
	report := renderReport(results, runID, *remote, *transportKind, *strategy)
122
	if *reportOut != "" {
123
		if err := os.WriteFile(*reportOut, []byte(report), 0o644); err != nil {
124
			fmt.Fprintf(os.Stderr, "oakmark: write report: %v\n", err)
125
		} else {
126
			fmt.Fprintf(os.Stderr, "oakmark: report → %s\n", *reportOut)
127
		}
128
	}
129
	fmt.Print(report)
130
}
131
132
// runConfig is the immutable per-run setup a level needs to build agents.
133
type runConfig struct {
134
	transport  string
135
	oakBin     string
136
	engineBin  string
137
	remote     string
138
	token      string
139
	prefix     string
140
	cfg        *commitConfig
141
	sess       *sessionConfig
142
	cloneDepth int
143
	repos      []repoRef
144
}
145
146
// runLevel runs `level` concurrent agents for warmup+window and returns every
147
// sample. Agents are assigned round-robin across repos (the repo strategy
148
// spreads load; branch/session use a single repo). Each owns branch
149
// <prefix>-a<i>.
150
func runLevel(rc *runConfig, level int, warmup, window time.Duration) []sample {
151
	ctx, cancel := context.WithTimeout(context.Background(), warmup+window)
152
	defer cancel()
153
154
	agents := make([]*agent, level)
155
	for i := range level {
156
		repo := rc.repos[i%len(rc.repos)]
157
		branch := fmt.Sprintf("%s-a%d", rc.prefix, i)
158
		tr := buildTransport(rc, repo, i, branch)
159
		agents[i] = newAgent(i, rc.cfg, rc.sess, tr)
160
	}
161
162
	start := time.Now()
163
	var wg sync.WaitGroup
164
	for _, a := range agents {
165
		wg.Add(1)
166
		go func(a *agent) { defer wg.Done(); a.run(ctx, start) }(a)
167
	}
168
	wg.Wait()
169
170
	var all []sample
171
	for _, a := range agents {
172
		all = append(all, a.samples...)
173
	}
174
	return all
175
}
176
177
func buildTransport(rc *runConfig, repo repoRef, id int, branch string) Transport {
178
	if rc.transport == "protocol" {
179
		author := fmt.Sprintf("agent-%d", id)
180
		return newProtocolTransport(rc.engineBin, rc.remote, repo.owner, repo.name, rc.token, author, branch)
181
	}
182
	return newCLITransport(rc.oakBin, rc.remote, repo.owner, repo.name, id, branch, rc.cloneDepth)
183
}
184
185
// repoList builds the target repo set. branch/session use one repo; repo
186
// spreads across repo-<0..n-1>.
187
func repoList(owner, base, strategy string, count int) []repoRef {
188
	if strategy == "repo" {
189
		if count < 1 {
190
			count = 1
191
		}
192
		repos := make([]repoRef, count)
193
		for i := range count {
194
			repos[i] = repoRef{owner: owner, name: fmt.Sprintf("%s-%d", base, i)}
195
		}
196
		return repos
197
	}
198
	return []repoRef{{owner: owner, name: base}}
199
}
200
201
func parseLevels(csv string) ([]int, error) {
202
	parts := strings.Split(csv, ",")
203
	levels := make([]int, 0, len(parts))
204
	for _, p := range parts {
205
		p = strings.TrimSpace(p)
206
		if p == "" {
207
			continue
208
		}
209
		n, err := strconv.Atoi(p)
210
		if err != nil || n < 1 {
211
			return nil, fmt.Errorf("%q is not a positive integer", p)
212
		}
213
		levels = append(levels, n)
214
	}
215
	if len(levels) == 0 {
216
		return nil, fmt.Errorf("no levels")
217
	}
218
	return levels, nil
219
}
220
221
// loadToken resolves the bearer token: -token-file (or '-' for stdin), else
222
// $OAK_API_KEY, else "" (anonymous — fine for an open loopback `oak serve`).
223
func loadToken(tokenFile string) string {
224
	if tokenFile == "-" {
225
		b, _ := io.ReadAll(os.Stdin)
226
		return strings.TrimSpace(string(b))
227
	}
228
	if tokenFile != "" {
229
		b, err := os.ReadFile(tokenFile)
230
		if err != nil {
231
			fatalf("read token file %s: %v", tokenFile, err)
232
		}
233
		return strings.TrimSpace(string(b))
234
	}
235
	return strings.TrimSpace(os.Getenv("OAK_API_KEY"))
236
}
237
238
// seedRepo creates the target repo on the server so agents can clone it: a
239
// fresh `oak init` + `oak commit` + `oak push --repo owner/name`. Idempotent —
240
// a second seed of an existing repo just re-links and pushes a new branch.
241
func seedRepo(oakBin, remote string, repo repoRef) error {
242
	dir, err := os.MkdirTemp("", "oakmark-seed-")
243
	if err != nil {
244
		return err
245
	}
246
	defer os.RemoveAll(dir)
247
	env := append(os.Environ(), "OAK_REMOTE="+remote)
248
	runOak := func(d string, args ...string) error {
249
		return runCmd(oakBin, d, env, args...)
250
	}
251
	if err := runOak(dir, "init", "."); err != nil {
252
		return fmt.Errorf("init: %w", err)
253
	}
254
	if err := os.WriteFile(dir+"/README.md", []byte("oakmark seed for "+repo.String()+"\n"), 0o644); err != nil {
255
		return err
256
	}
257
	if err := runOak(dir, "commit", "--quiet"); err != nil {
258
		return fmt.Errorf("commit: %w", err)
259
	}
260
	if err := runOak(dir, "push", "--repo", repo.String(), "-r", remote); err != nil {
261
		return fmt.Errorf("push: %w", err)
262
	}
263
	return nil
264
}
265
266
func fatalf(format string, args ...any) {
267
	fmt.Fprintf(os.Stderr, "oakmark: "+format+"\n", args...)
268
	os.Exit(1)
269
}