node

package
v0.15.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 16, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

README

Benchmarking Custom Views Tutorial

This guide shows how to benchmark a custom view.View implementation using FSC's exported benchmark infrastructure from an external repository.

What You Implement

Your view must satisfy two interfaces from FSC:

// your_view.go

type YourView struct{ params YourParams }

func (v *YourView) Call(viewCtx view.Context) (any, error) {
    // your workload logic (e.g. ZKP proving, signing, validation)
    return result, nil
}

type YourViewFactory struct{}

func (f *YourViewFactory) NewView(in []byte) (view.View, error) {
    v := &YourView{}
    if in != nil {
        json.Unmarshal(in, &v.params)
    }
    return v, nil
}

File Structure

your-repo/
├── your_view.go                        # YourView + YourViewFactory
├── bench/
│   ├── standalone_bench_test.go        # pure view benchmark (no FSC node)
│   ├── node_bench_test.go              # local node benchmarks (API + gRPC)
│   └── remote/
│       ├── server/main.go              # remote server
│       └── client/main.go              # remote client
  • your_view.go : your View and ViewFactory implementations.
  • standalone_bench_test.go : benchmarks that call your view directly, no FSC infrastructure.
  • node_bench_test.go : benchmarks that run your view through a real FSC node (API and gRPC).
  • remote/server/main.go and remote/client/main.go : binaries for running benchmarks across two machines.

Define Your Workload

Wrap your view in a node.Workload to plug into FSC's benchmark runners:

var yourWorkload = node.Workload{
    Name:    "your-view",
    Factory: &YourViewFactory{},
    Params:  &YourParams{Size: 1024},
}

Benchmark Levels

1. Standalone - Pure View Performance

No FSC node, no gRPC. Directly benchmarks Call().

// standalone_bench_test.go

func BenchmarkYourView(b *testing.B) {
    f := &YourViewFactory{}
    input, _ := json.Marshal(&YourParams{Size: 1024})

    b.RunParallel(func(pb *testing.PB) {
        v, _ := f.NewView(input)
        for pb.Next() {
            _, _ = v.Call(nil)
        }
    })
    benchmark.ReportTPS(b)
}
go test -bench=BenchmarkYourView -benchmem -count=5 -cpu=1,2,4,8 ./bench/
2. Local Node - View API and gRPC

Spins up an FSC node in-process using node.GenerateConfig and node.SetupNode.

RunAPIBenchmark produces two sub-benchmarks:

  • f=0 : creates one view instance per goroutine and reuses it. Measures pure view execution cost.
  • f=1 : creates a new view via the factory on every call. Measures view execution + factory overhead.

RunAPIGRPCBenchmark adds the full gRPC stack on top and tests across multiple connection counts.

// node_bench_test.go

func BenchmarkYourViewAPI(b *testing.B) {
    testdataPath := b.TempDir()
    nodeConfPath := path.Join(testdataPath, "fsc", "nodes", "test-node.0")

    require.NoError(b, node.GenerateConfig(testdataPath))

    n, err := node.SetupNode(nodeConfPath, node.NamedFactory{
        Name:    yourWorkload.Name,
        Factory: yourWorkload.Factory,
    })
    require.NoError(b, err)
    defer n.Stop()

    vm, err := viewregistry.GetManager(n)
    require.NoError(b, err)

    node.RunAPIBenchmark(b, vm, yourWorkload)
}

func BenchmarkYourViewAPIGRPC(b *testing.B) {
    testdataPath := b.TempDir()
    nodeConfPath := path.Join(testdataPath, "fsc", "nodes", "test-node.0")
    clientConfPath := path.Join(nodeConfPath, "client-config.yaml")

    require.NoError(b, node.GenerateConfig(testdataPath))

    n, err := node.SetupNode(nodeConfPath, node.NamedFactory{
        Name:    yourWorkload.Name,
        Factory: yourWorkload.Factory,
    })
    require.NoError(b, err)
    defer n.Stop()

    node.RunAPIGRPCBenchmark(b, yourWorkload, clientConfPath, *numConn)
}
go test -bench=BenchmarkYourViewAPI -benchmem -count=5 -cpu=1,2,4,8 ./bench/
go test -bench=BenchmarkYourViewAPIGRPC -benchmem -count=5 -cpu=1,8,16 -numConn=1,2,4 ./bench/
3. Remote - Two Machines

Server - register your factory and start the node:

// remote/server/main.go

func main() {
    testdataPath := "./out/testdata"
    nodeConfPath := path.Join(testdataPath, "fsc", "nodes", "test-node.0")

    if err := node.GenerateConfig(testdataPath); err != nil {
        panic(err)
    }

    n, err := node.SetupNode(nodeConfPath,
        node.NamedFactory{Name: "your-view", Factory: &YourViewFactory{}},
    )
    if err != nil {
        panic(err)
    }

    // wait for signal...
}

Client - populate config and call RunRemoteBenchmarkSuite:

// remote/client/main.go

func main() {
    flag.Parse()
    node.RunRemoteBenchmarkSuite(node.RemoteBenchmarkConfig{
        Workloads:      []node.Workload{yourWorkload},
        ClientConfPath: clientConfPath,
        ConnCounts:     *numConn,
        WorkerCounts:   *numWorker,
        WarmupDur:      *warmupDur,
        BenchTime:      *duration,
        Count:          *count,
        BenchName:      "BenchmarkRemote",
    })
}
Reference

FSC's own remote benchmarks use the same infrastructure:

Deployment

When the server starts, node.GenerateConfig generates all required crypto material and configuration into ./out/testdata, including TLS certificates used by both server and client. This folder must be copied to the client machine before running benchmarks. The client currently expects it at ./out/testdata this path is not yet configurable.

# server machine
go run ./bench/remote/server/

# copy crypto material to client
rsync -av server:/path/to/out/testdata ./out/testdata
vim ./out/testdata/fsc/nodes/test-node.0/client-config.yaml  # update server address

# client machine
go run ./bench/remote/client/ \
  -benchtime=10s -count=5 -cpu=1,8,16,32 -numConn=1,2,4 2>&1 | tee results.txt

Summary

Level What it measures How to run
Standalone Pure Call() go test -bench
API f=0 ViewManager + reused view node.RunAPIBenchmark
API f=1 ViewManager + factory per call node.RunAPIBenchmark
gRPC (local) Full FSC gRPC stack node.RunAPIGRPCBenchmark
gRPC (remote) Full stack + real network node.RunRemoteBenchmarkSuite

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DefaultWorkloads = []Workload{
	{
		Name:    "noop",
		Factory: &views.NoopViewFactory{},
	},
	{
		Name:    "cpu",
		Factory: &views.CPUViewFactory{},
		Params:  &views.CPUParams{N: 200000},
	},
	{
		Name:    "sign",
		Factory: &views.ECDSASignViewFactory{},
		Params:  &views.ECDSASignParams{},
	},
}

DefaultWorkloads defines the standard set of benchmark workloads.

View Source
var HistogramOptions = stats.HistogramOptions{
	NumBuckets:   2495,
	GrowthFactor: .01,
}

HistogramOptions are the default bucket settings for latency histograms used by deadline-driven (remote) benchmarks.

Functions

func CreateClients

func CreateClients(tb testing.TB, numConn int, clientConfPath string) ([]*benchmark.ViewClient, func())

CreateClients creates numConn ViewClients using the given client config path. It returns the clients and a cleanup function that closes all connections.

func CreateRemoteClients

func CreateRemoteClients(numConn int, clientConfPath string) ([]*benchmark.ViewClient, func())

CreateRemoteClients creates numConn ViewClients for remote benchmarks. Unlike CreateClients (which takes testing.TB), this panics on errors since it runs outside of `go test`.

func GenerateConfig

func GenerateConfig(testdataDir string) error

func MakeGRPCCaller

func MakeGRPCCaller(tb testing.TB, cli *benchmark.ViewClient, fid string, input []byte) func(ctx context.Context) error

MakeGRPCCaller creates a caller function that sends a signed gRPC command for the given workload name and input. The signed command is created once and reused across all calls for efficiency.

func MakeRemoteGRPCCaller

func MakeRemoteGRPCCaller(cli *benchmark.ViewClient, fid string, input []byte) func(ctx context.Context) error

MakeRemoteGRPCCaller creates a caller function for deadline-driven (remote) benchmarks. Unlike MakeGRPCCaller (which takes testing.TB), this version panics on setup errors since it runs outside of `go test`.

func MarshalWorkloadParams

func MarshalWorkloadParams(params any) []byte

MarshalWorkloadParams marshals workload params to JSON. Returns nil if params is nil.

func Percentile

func Percentile(p float64, h *stats.Histogram) int64

Percentile computes the given percentile (0.0–1.0) from a histogram.

func PrintHistogram

func PrintHistogram(hist *stats.Histogram, duration time.Duration)

PrintHistogram formats and prints benchmark results from a histogram, including TPS and latency percentiles (p5, p95, p99).

Output format matches Go's testing.BenchmarkResult so it can be parsed by standard benchmark tooling.

func RunAPIBenchmark

func RunAPIBenchmark(b *testing.B, vm ViewManager, wl Workload)

RunAPIBenchmark runs a direct View API benchmark for a single workload against the given view manager. It creates two sub-benchmarks:

  • f=0: reuses a single view instance per goroutine (measures pure view performance)
  • f=1: creates a new view per invocation (measures view + factory overhead)

func RunAPIGRPCBenchmark

func RunAPIGRPCBenchmark(b *testing.B, wl Workload, clientConfPath string, connCounts []int)

RunAPIGRPCBenchmark runs a gRPC View API benchmark for a single workload across multiple connection counts. It handles client creation, warmup, and cleanup.

func RunGRPCBenchmark

func RunGRPCBenchmark(b *testing.B, ccs []*benchmark.ViewClient, makeCaller func(cli *benchmark.ViewClient) func(ctx context.Context) error)

RunGRPCBenchmark runs a parallel benchmark over multiple ViewClients using round-robin caller selection. This is the core gRPC benchmark loop used by both local and remote node benchmarks.

func RunRemoteBenchmark

func RunRemoteBenchmark(
	ccs []*benchmark.ViewClient,
	makeCaller func(cli *benchmark.ViewClient) func(ctx context.Context) error,
	numWorker int,
	warmDeadline, endDeadline time.Time,
) *stats.Histogram

RunRemoteBenchmark runs a deadline-driven benchmark across multiple ViewClients. Unlike RunGRPCBenchmark (which uses testing.B), this uses explicit warmup and measurement phases with histogram-based latency collection.

It returns the merged histogram of all workers. Callers typically pass the result to PrintHistogram().

func RunRemoteBenchmarkSuite

func RunRemoteBenchmarkSuite(cfg RemoteBenchmarkConfig)

RunRemoteBenchmarkSuite runs the full remote benchmark loop: workloads × connection counts × worker counts × count.

func SetupClient

func SetupClient(confPath string) (*benchmark.ViewClient, func(), error)

func SetupNode

func SetupNode(confPath string, factories ...NamedFactory) (*node.Node, error)

func WarmupClients

func WarmupClients(ccs []*benchmark.ViewClient, makeCaller func(*benchmark.ViewClient) func(ctx context.Context) error) error

Types

type NamedFactory

type NamedFactory struct {
	Name    string
	Factory viewregistry.Factory
}

type RemoteBenchmarkConfig

type RemoteBenchmarkConfig struct {
	Workloads      []Workload
	ClientConfPath string
	ConnCounts     []int // number of gRPC connections to test
	WorkerCounts   []int // number of concurrent workers to test
	WarmupDur      time.Duration
	BenchTime      time.Duration
	Count          int    // number of executions per configuration
	BenchName      string // prefix for output lines, e.g. "BenchmarkAPIGRPCRemote"
}

RemoteBenchmarkConfig holds all parameters for a remote benchmark suite.

type ViewManager

type ViewManager interface {
	NewView(id string, in []byte) (view.View, error)
	InitiateView(ctx context.Context, view view.View) (any, error)
}

ViewManager is the minimal interface needed for API benchmarks. It is satisfied by the value returned from viewregistry.GetManager().

type Workload

type Workload struct {
	Name    string
	Factory viewregistry.Factory
	Params  any
}

Workload defines a benchmark workload: a named view factory with optional parameters.

Directories

Path Synopsis
remote
client command
server command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL