Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,25 @@ make

### 2. Start backends

The included `docker-compose.yml` uses profiles, which provide an easy way to only spin up a subset of containers.

Please note the 'sample' dataset which is included does not provide a meaningful benchmark, it's designed to show how to use the system.
Each dataset under `datasets/` ships with its own `docker-compose.yml` that pins
the exact images and tuning used for that benchmark. Run compose from the
dataset directory so the captured `Container` tab in the dashboard reflects the
real configuration:

```bash
docker compose --profile paradedb --profile postgres up -d
docker compose -f datasets/sample/docker-compose.yml up -d
```

The repo-root `docker-compose.yml` is a kitchen-sink template containing every
supported backend with profiles; it's intended as a starting point when
authoring a new dataset, not for running an existing one.

For backends running off-host (e.g. AWS RDS, managed Elasticsearch), pass
`{ type: "paradedb", container: "" }` in the backend config to skip docker
metrics for that backend.

Please note the 'sample' dataset which is included does not provide a meaningful benchmark, it's designed to show how to use the system.

See [Docker Setup](docs/docker.md) for all available profiles and services.

### 3. Load data
Expand Down
66 changes: 62 additions & 4 deletions backends.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package search
import (
"context"
"fmt"
"os"
"path/filepath"
"time"

"github.com/grafana/sobek"
Expand Down Expand Up @@ -65,6 +67,20 @@ func (m *ModuleInstance) newBackends(config map[string]interface{}) *Backends {
defaults := backends.DefaultConnections()
defaultContainers := backends.DefaultContainers()

// Capture dataset.yaml (written by `loader pull`) if present.
if datasetPath != "" {
if data, err := os.ReadFile(filepath.Join(datasetPath, "dataset.yaml")); err == nil {
metrics.RegisterRunCapture("dataset_yaml", string(data))
}
}

// Capture the running k6 script's source. k6 is invoked as
// `./k6 run [flags] <script.js>` — the script path is in os.Args.
if src, path := readRunningScript(); src != "" {
metrics.RegisterRunCapture("script", src)
metrics.RegisterRunCapture("script_path", path)
}

// Parse backends array
backendsArray, ok := config["backends"].([]interface{})
if !ok {
Expand All @@ -74,6 +90,10 @@ func (m *ModuleInstance) newBackends(config map[string]interface{}) *Backends {

for _, item := range backendsArray {
var backendType, alias, container, color, conn string
// containerExplicit tracks whether the user set the container field at all
// (including to ""). An explicit empty string opts the backend out of
// docker metrics — used for off-host services like AWS RDS.
var containerExplicit bool

switch v := item.(type) {
case string:
Expand All @@ -92,8 +112,11 @@ func (m *ModuleInstance) newBackends(config map[string]interface{}) *Backends {
if a, ok := v["alias"].(string); ok {
alias = a
}
if c, ok := v["container"].(string); ok {
container = c
if raw, ok := v["container"]; ok {
containerExplicit = true
if c, ok := raw.(string); ok {
container = c
}
}
if c, ok := v["color"].(string); ok {
color = c
Expand All @@ -118,7 +141,9 @@ func (m *ModuleInstance) newBackends(config map[string]interface{}) *Backends {
if conn == "" {
conn = defaults[backendType]
}
if container == "" {
// Only default the container name if the user didn't explicitly set it.
// An explicit "" opts out of docker capture (off-host backends).
if !containerExplicit {
if alias != backendType {
container = alias // default container to alias if alias is set
} else {
Expand Down Expand Up @@ -147,7 +172,9 @@ func (m *ModuleInstance) newBackends(config map[string]interface{}) *Backends {

client := backends.NewK6Client(m.vu, driver, alias)
b.clients[alias] = client
enabledContainers = append(enabledContainers, container)
if container != "" {
enabledContainers = append(enabledContainers, container)
}

driver.CaptureConfig(ctx, alias)
metrics.CapturePrePostScripts(alias, backendType, datasetPath, backendCfg.FileType)
Expand Down Expand Up @@ -239,6 +266,37 @@ func (b *Backends) SetTimeout(seconds int) {
}
}

// readRunningScript locates the currently-running k6 script via os.Args and
// returns its source text plus absolute path. The xk6 extension runs inside
// the k6 process so the script path is reachable from argv directly; k6's
// public extension API doesn't expose it as cleanly. Returns ("", "") if no
// candidate is found.
func readRunningScript() (source, path string) {
for _, arg := range os.Args[1:] {
if !looksLikeScript(arg) {
continue
}
abs, err := filepath.Abs(arg)
if err != nil {
continue
}
data, err := os.ReadFile(abs)
if err != nil {
continue
}
return string(data), abs
}
return "", ""
}

func looksLikeScript(arg string) bool {
switch filepath.Ext(arg) {
case ".js", ".ts", ".mjs", ".cjs":
return true
}
return false
}

// parseDatasetPath extracts dataset path from config.
// Defaults to "../" (parent of k6 script directory) and resolves relative to script location.
func parseDatasetPath(config map[string]interface{}, vu modules.VU) string {
Expand Down
18 changes: 18 additions & 0 deletions cmd/loader/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
writeDatasetManifest(destDir, sourceURL)
return
}

Expand Down Expand Up @@ -452,6 +453,23 @@
if failed > 0 {
os.Exit(1)
}
writeDatasetManifest(destDir, sourceURL)
}

// writeDatasetManifest records where the dataset was pulled from. The k6
// extension reads this file at run time and embeds it in the dashboard JSON
// so the run record carries the canonical S3 location.
func writeDatasetManifest(destDir, sourceURL string) {
manifest := fmt.Sprintf("s3: %s\npulled_at: %s\n",
sourceURL,
time.Now().UTC().Format(time.RFC3339),
)
path := filepath.Join(destDir, "dataset.yaml")
if err := os.WriteFile(path, []byte(manifest), 0644); err != nil {
fmt.Printf("Warning: failed to write %s: %v\n", path, err)
return
}
fmt.Printf("Wrote %s\n", path)
}

// prepareDestDir ensures destDir is a real, empty directory we can safely
Expand Down Expand Up @@ -516,7 +534,7 @@
}

switch hdr.Typeflag {
case tar.TypeReg, tar.TypeRegA, tar.TypeDir:

Check failure on line 537 in cmd/loader/main.go

View workflow job for this annotation

GitHub Actions / Lint

SA1019: tar.TypeRegA has been deprecated since Go 1.11 and an alternative has been available since Go 1.1: Use TypeReg instead. (staticcheck)
default:
fmt.Printf(" Skipping %s (unsupported tar entry type %c)\n", hdr.Name, hdr.Typeflag)
skipped++
Expand Down
139 changes: 95 additions & 44 deletions dashboard/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -709,23 +709,18 @@ func (o *Output) getSummary() map[string]interface{} {
}
}

// Add database config based on backend tag
config, containerLimits := getBackendConfig(rm.Backend, rm.Container)

runs[name] = map[string]interface{}{
"name": rm.Name,
"backend": rm.Backend,
"container": rm.Container,
"alias": rm.Alias,
"color": rm.Color,
"chart": rm.Chart,
"ingestRate": rm.IngestRate,
"totalIngested": rm.TotalIngested,
"avgIngestRate": ingestRate,
"queries": queries,
"startTime": rm.StartTime,
"config": config,
"containerLimits": containerLimits,
"name": rm.Name,
"backend": rm.Backend,
"container": rm.Container,
"alias": rm.Alias,
"color": rm.Color,
"chart": rm.Chart,
"ingestRate": rm.IngestRate,
"totalIngested": rm.TotalIngested,
"avgIngestRate": ingestRate,
"queries": queries,
"startTime": rm.StartTime,
}
}

Expand All @@ -742,15 +737,21 @@ func (o *Output) getSummary() map[string]interface{} {
}
}

return map[string]interface{}{
out := map[string]interface{}{
"elapsed": elapsed,
"chartDuration": chartDuration,
"runs": runs,
"backends": buildBackendsBlock(),
"containers": containers,
"startTime": o.data.StartTime.UnixMilli(),
"broadcastInterval": o.broadcastInterval.Milliseconds(),
"timelineWindow": o.timelineWindow.Milliseconds(),
}
if meta := readMetaEnv(); meta != nil {
out["meta"] = meta
}
addRunCaptures(out)
return out
}

// getExportData returns raw data for JSON export — no pre-aggregated timeline,
Expand All @@ -775,8 +776,6 @@ func (o *Output) getExportData() map[string]interface{} {
}
}

config, containerLimits := getBackendConfig(rm.Backend, rm.Container)

var endTime int64
if rm.EndTime > 0 {
endTime = rm.EndTime
Expand All @@ -787,19 +786,17 @@ func (o *Output) getExportData() map[string]interface{} {
}

runs[name] = map[string]interface{}{
"name": rm.Name,
"backend": rm.Backend,
"container": rm.Container,
"alias": rm.Alias,
"color": rm.Color,
"chart": rm.Chart,
"ingestRate": rm.IngestRate,
"totalIngested": rm.TotalIngested,
"startTime": rm.StartTime,
"endTime": endTime,
"config": config,
"containerLimits": containerLimits,
"queries": queries,
"name": rm.Name,
"backend": rm.Backend,
"container": rm.Container,
"alias": rm.Alias,
"color": rm.Color,
"chart": rm.Chart,
"ingestRate": rm.IngestRate,
"totalIngested": rm.TotalIngested,
"startTime": rm.StartTime,
"endTime": endTime,
"queries": queries,
}
}

Expand All @@ -815,11 +812,17 @@ func (o *Output) getExportData() map[string]interface{} {
}
}

return map[string]interface{}{
out := map[string]interface{}{
"startTime": o.data.StartTime.UnixMilli(),
"runs": runs,
"backends": buildBackendsBlock(),
"containers": containers,
}
if meta := readMetaEnv(); meta != nil {
out["meta"] = meta
}
addRunCaptures(out)
return out
}

// aggregateExportData takes raw export JSON (with latencies/timestamps per query)
Expand Down Expand Up @@ -1142,19 +1145,67 @@ func getQueryPattern(backend, chart, qName string) string {
return metrics.GetQueryPattern(backend, chart, qName)
}

// getBackendConfig returns the database config and container limits for a backend type.
// Container limits are looked up by container name, not backend name.
func getBackendConfig(backend, container string) (map[string]interface{}, map[string]interface{}) {
if backend == "" {
return nil, nil
// buildBackendsBlock returns the deduplicated per-backend snapshot for the
// dashboard JSON. Each entry combines the backend's database config (postgres
// GUCs, version, pre/post scripts), the docker-inspect data for its container,
// and display options (alias, color). The frontend looks up by backend alias.
func buildBackendsBlock() map[string]interface{} {
options := metrics.GetAllBackendOptions()
backends := make(map[string]interface{}, len(options))
for alias, opt := range options {
entry := map[string]interface{}{
"alias": opt.Alias,
"container": opt.Container,
"color": opt.Color,
}
if cfg := metrics.GetBackendConfig(alias); cfg != nil {
entry["config"] = cfg
}
if opt.Container != "" {
if info := metrics.GetContainerInfo(opt.Container); info != nil {
entry["container_info"] = info
}
}
backends[alias] = entry
}
return backends
}

// addRunCaptures stamps any registered run-level captures (dataset.yaml text,
// k6 script source) onto the given dashboard output map under top-level keys.
// Absent captures are not stamped — the frontend hides their tabs.
func addRunCaptures(out map[string]interface{}) {
if s := metrics.GetRunCapture("dataset_yaml"); s != "" {
out["dataset_yaml"] = s
}
if s := metrics.GetRunCapture("script"); s != "" {
out["script"] = s
}
// Look up limits by container name (which may be alias or custom container name)
limits := metrics.GetContainerLimits(container)
if limits == nil && container != backend {
// Fall back to backend name for backwards compatibility
limits = metrics.GetContainerLimits(backend)
if s := metrics.GetRunCapture("script_path"); s != "" {
out["script_path"] = s
}
}

// readMetaEnv returns the parsed BENCHMARKER_META env var, or nil if unset.
// Supports either inline JSON ('{"commit":"abc"}') or '@path/to/file.json'.
func readMetaEnv() map[string]interface{} {
raw := os.Getenv("BENCHMARKER_META")
if raw == "" {
return nil
}
data := []byte(raw)
if strings.HasPrefix(raw, "@") {
b, err := os.ReadFile(raw[1:])
if err != nil {
return nil
}
data = b
}
var m map[string]interface{}
if err := json.Unmarshal(data, &m); err != nil {
return nil
}
return metrics.GetBackendConfig(backend), limits
return m
}

// ServeFile starts a server to view a saved dashboard JSON file.
Expand Down
Loading
Loading