The gmeasure package provides support for benchmarking and measuring code performance. It is designed as a robust replacement for Ginkgo V1's Measure nodes and integrates seamlessly with Ginkgo V2's reporting infrastructure.
import "github.com/onsi/gomega/gmeasure"gmeasure is organized around the metaphor of an Experiment that records multiple Measurements. A Measurement is a named collection of data points supporting:
time.Duration)float64)Experiments allow recording measurements directly or measuring functions automatically. When measuring functions, experiments handle timing (for Duration measurements) and/or recording return values (for Value measurements). Experiments also support sampling - running functions repeatedly to gather statistical data.
The main type for conducting performance measurements.
type Experiment struct {
Name string
Measurements Measurements
// contains filtered or unexported fields
}func NewExperiment(name string) *Experiment{ .api }
Creates a new experiment with the specified name.
Example:
experiment := gmeasure.NewExperiment("My Benchmark")
AddReportEntry(experiment.Name, experiment) // Register with Ginkgofunc (e *Experiment) RecordNote(note string, args ...any){ .api }
Records a textual note to annotate the experiment. Notes appear in experiment reports.
Supported decorations: Style()
Example:
experiment.RecordNote("Beginning performance tests", gmeasure.Style("{{blue}}"))func (e *Experiment) RecordDuration(name string, duration time.Duration, args ...any){ .api }
Records a duration on a Duration Measurement with the given name. Creates the measurement if it doesn't exist.
Supported decorations: Style(), Precision(), Annotation()
Example:
experiment.RecordDuration("response time", 150*time.Millisecond,
gmeasure.Annotation("first request"))func (e *Experiment) RecordValue(name string, value float64, args ...any){ .api }
Records a float64 value on a Value Measurement with the given name. Creates the measurement if it doesn't exist.
Supported decorations: Style(), Units(), Precision(), Annotation()
Example:
experiment.RecordValue("throughput", 1250.5,
gmeasure.Units("MB/s"),
gmeasure.Precision(2))func (e *Experiment) MeasureDuration(name string, callback func(), args ...any) time.Duration{ .api }
Executes the callback and times its duration. Records the result and returns the measured duration.
Supported decorations: Style(), Precision(), Annotation()
Example:
duration := experiment.MeasureDuration("database query", func() {
db.Query("SELECT * FROM users")
})func (e *Experiment) MeasureValue(name string, callback func() float64, args ...any) float64{ .api }
Executes the callback and records its return value. Returns the measured value.
Supported decorations: Style(), Units(), Precision(), Annotation()
Example:
value := experiment.MeasureValue("items processed", func() float64 {
return float64(processItems())
}, gmeasure.Units("items"))func (e *Experiment) SampleDuration(name string, callback func(idx int),
samplingConfig SamplingConfig, args ...any){ .api }
Samples the callback multiple times, timing each execution. The callback receives a zero-based index.
Supported decorations: Style(), Precision(), Annotation() (applied to all samples)
Example:
experiment.SampleDuration("api call", func(idx int) {
makeAPICall()
}, gmeasure.SamplingConfig{N: 100})func (e *Experiment) SampleAnnotatedDuration(name string,
callback func(idx int) Annotation, samplingConfig SamplingConfig, args ...any){ .api }
Samples the callback multiple times, timing each execution. The callback must return an Annotation for each sample.
Supported decorations: Style(), Precision()
Example:
experiment.SampleAnnotatedDuration("request", func(idx int) gmeasure.Annotation {
status := makeRequest()
return gmeasure.Annotation(fmt.Sprintf("status: %d", status))
}, gmeasure.SamplingConfig{N: 50})func (e *Experiment) SampleValue(name string, callback func(idx int) float64,
samplingConfig SamplingConfig, args ...any){ .api }
Samples the callback multiple times, recording each return value. The callback must return a float64.
Supported decorations: Style(), Units(), Precision(), Annotation() (applied to all samples)
Example:
experiment.SampleValue("memory usage", func(idx int) float64 {
return float64(getCurrentMemoryMB())
}, gmeasure.SamplingConfig{N: 100}, gmeasure.Units("MB"))func (e *Experiment) SampleAnnotatedValue(name string,
callback func(idx int) (float64, Annotation), samplingConfig SamplingConfig, args ...any){ .api }
Samples the callback multiple times, recording each return value and annotation.
Supported decorations: Style(), Units(), Precision()
Example:
experiment.SampleAnnotatedValue("cpu usage", func(idx int) (float64, gmeasure.Annotation) {
usage := getCPUUsage()
annotation := fmt.Sprintf("cores: %d", runtime.NumCPU())
return usage, gmeasure.Annotation(annotation)
}, gmeasure.SamplingConfig{Duration: 10*time.Second})func (e *Experiment) Sample(callback func(idx int), samplingConfig SamplingConfig){ .api }
Generic sampling function that executes the callback repeatedly according to the sampling configuration. The callback receives a zero-based index.
Example:
experiment.Sample(func(idx int) {
// Custom measurement logic
duration := measureSomething()
experiment.RecordDuration("custom", duration)
}, gmeasure.SamplingConfig{N: 100})func (e *Experiment) Get(name string) Measurement{ .api }
Returns the Measurement with the specified name. Returns a zero-value Measurement{} if not found.
Example:
measurement := experiment.Get("response time")
if measurement.Type != gmeasure.MeasurementTypeInvalid {
fmt.Println("Durations:", measurement.Durations)
}func (e *Experiment) GetStats(name string) Stats{ .api }
Returns statistics for the Measurement with the specified name. Equivalent to experiment.Get(name).Stats().
Example:
stats := experiment.GetStats("response time")
fmt.Printf("Mean: %v\n", stats.DurationFor(gmeasure.StatMean))func (e *Experiment) NewStopwatch() *Stopwatch{ .api }
Creates a stopwatch for manual timing operations tied to this experiment.
Example:
stopwatch := experiment.NewStopwatch()
// ... do work ...
stopwatch.Record("phase 1").Reset()
// ... do more work ...
stopwatch.Record("phase 2")func (e *Experiment) String() string{ .api }
Returns an unformatted summary of the experiment and all measurements.
func (e *Experiment) ColorableString() string{ .api }
Returns a Ginkgo-formatted summary with styling. Called automatically by Ginkgo's reporting when registered via AddReportEntry.
Represents a single named measurement with all captured data.
type Measurement struct {
Type MeasurementType
ExperimentName string
Note string
Name string
Style string
Units string
PrecisionBundle PrecisionBundle
Durations []time.Duration
Values []float64
Annotations []string
}MeasurementTypeNote, MeasurementTypeDuration, or MeasurementTypeValueMeasurementTypeNote)"duration" for Duration measurements)func (m Measurement) Stats() Stats{ .api }
Calculates and returns statistics for this measurement.
func (m Measurement) String() string{ .api }
Returns an unstyled report including all data points.
func (m Measurement) ColorableString() string{ .api }
Returns a styled report. Called by Ginkgo when registered via AddReportEntry.
Statistical summary of a measurement.
type Stats struct {
Type StatsType
ExperimentName string
MeasurementName string
Units string
Style string
PrecisionBundle PrecisionBundle
N int
ValueBundle map[Stat]float64
DurationBundle map[Stat]time.Duration
AnnotationBundle map[Stat]string
}StatsTypeValue or StatsTypeDurationfunc (s Stats) ValueFor(stat Stat) float64{ .api }
Returns the float64 value for a specific statistic. Use only with StatsTypeValue.
Example:
median := stats.ValueFor(gmeasure.StatMedian)func (s Stats) DurationFor(stat Stat) time.Duration{ .api }
Returns the duration for a specific statistic. Use only with StatsTypeDuration.
Example:
mean := stats.DurationFor(gmeasure.StatMean)func (s Stats) FloatFor(stat Stat) float64{ .api }
Returns a float64 representation regardless of type. For durations, returns float64(duration).
func (s Stats) StringFor(stat Stat) string{ .api }
Returns a formatted string representation honoring precision settings.
func (s Stats) String() string{ .api }
Returns a summary in the format: "MIN < [MEDIAN] | <MEAN> ±STDDEV < MAX"
Collection type for managing multiple measurements.
type Measurements []MeasurementConfigures sampling behavior for the Sample* family of methods.
type SamplingConfig struct {
N int
Duration time.Duration
MinSamplingInterval time.Duration
NumParallel int
}{ .api }
NumParallel)MinSamplingInterval)Notes:
N or Duration must be specifiedMinSamplingInterval and NumParallel are mutually exclusiveExamples:
// Sample exactly 100 times
config := gmeasure.SamplingConfig{N: 100}
// Sample for 10 seconds
config := gmeasure.SamplingConfig{Duration: 10*time.Second}
// Sample 100 times with at least 100ms between samples
config := gmeasure.SamplingConfig{N: 100, MinSamplingInterval: 100*time.Millisecond}
// Sample for 30 seconds using 4 parallel workers
config := gmeasure.SamplingConfig{Duration: 30*time.Second, NumParallel: 4}Decorators customize measurement recording by providing additional metadata.
type Units stringSpecifies units for value measurements (ignored for durations).
func Units(unit string) Units{ .api }
Example:
experiment.RecordValue("bandwidth", 125.5, gmeasure.Units("MB/s"))Note: Units are set only on the first recording of a measurement name.
type Annotation stringAdds contextual information to individual data points.
func Annotation(text string) Annotation{ .api }
Example:
experiment.RecordDuration("query", duration, gmeasure.Annotation("user lookup"))type Style stringAdds styling/coloring hints for console reports using Ginkgo's format strings.
func Style(style string) Style{ .api }
Example:
experiment.RecordValue("errors", count, gmeasure.Style("{{red}}{{bold}}"))Note: Style is set only on the first recording of a measurement name.
func Precision(p any) PrecisionBundle{ .api }
Controls display precision for measurements.
Parameters:
int: Number of decimal places for value measurements (equivalent to "%.Nf")time.Duration: Rounding precision for duration measurementsExamples:
// Show 2 decimal places for values
experiment.RecordValue("score", 3.14159, gmeasure.Precision(2)) // displays as "3.14"
// Round durations to nearest 100ms
experiment.RecordDuration("time", 3214*time.Millisecond,
gmeasure.Precision(100*time.Millisecond)) // displays as "3.2s"Note: Precision is set only on the first recording of a measurement name.
type PrecisionBundle struct {
Duration time.Duration
ValueFormat string
}{ .api }
Stores precision settings for both duration and value measurements.
Default:
var DefaultPrecisionBundle = PrecisionBundle{
Duration: 100 * time.Microsecond,
ValueFormat: "%.3f",
}type MeasurementType uint
const (
MeasurementTypeInvalid MeasurementType = iota
MeasurementTypeNote
MeasurementTypeDuration
MeasurementTypeValue
){ .api }
Identifies the type of measurement.
type Stat uint
const (
StatInvalid Stat = iota
StatMin
StatMax
StatMean
StatMedian
StatStdDev
){ .api }
Statistical measures available from Stats.
type StatsType uint
const (
StatsTypeInvalid StatsType = iota
StatsTypeValue
StatsTypeDuration
){ .api }
Identifies the type of statistics.
type RankingCriteria uint
const (
LowerMeanIsBetter RankingCriteria = iota
HigherMeanIsBetter
LowerMedianIsBetter
HigherMedianIsBetter
LowerMinIsBetter
HigherMinIsBetter
LowerMaxIsBetter
HigherMaxIsBetter
){ .api }
Criteria for ranking statistics.
Manual timing utility for measuring durations across code sections.
type Stopwatch struct {
Experiment *Experiment
// contains filtered or unexported fields
}Stopwatches are created from experiments:
stopwatch := experiment.NewStopwatch()A stopwatch can create additional stopwatches pointing to the same experiment (useful for goroutines):
stopwatch2 := stopwatch.NewStopwatch()Note: Individual stopwatches are not thread-safe. Create separate stopwatches for different goroutines.
func (s *Stopwatch) Record(name string, args ...any) *Stopwatch{ .api }
Records elapsed time since creation or last Reset(). Does not reset the stopwatch.
Supported decorations: Same as RecordDuration
Example:
stopwatch := experiment.NewStopwatch()
setupDatabase()
stopwatch.Record("setup").Reset()
runQueries()
stopwatch.Record("queries").Reset()func (s *Stopwatch) Reset() *Stopwatch{ .api }
Resets the stopwatch timer. Unpauses if paused. Returns *Stopwatch for chaining.
func (s *Stopwatch) Pause() *Stopwatch{ .api }
Pauses time accumulation. Must be running when called.
func (s *Stopwatch) Resume() *Stopwatch{ .api }
Resumes time accumulation after a pause. Must be paused when called.
Example:
stopwatch := experiment.NewStopwatch()
// Phase 1
stopwatch.Pause()
// Expensive setup we don't want to measure
setupTest()
stopwatch.Resume()
// Phase 2
stopwatch.Record("actual work") // Only measures Phase 1 + Phase 2func (s *Stopwatch) NewStopwatch() *Stopwatch{ .api }
Creates a new stopwatch pointing to the same experiment.
Provides directory-based caching of experiments to avoid re-running expensive benchmarks.
type ExperimentCache struct {
Path string
}func NewExperimentCache(path string) (ExperimentCache, error){ .api }
Creates a cache at the specified directory path. Creates the directory if it doesn't exist.
func (cache ExperimentCache) Load(name string, version int) *Experiment{ .api }
Loads an experiment from cache by name. Returns the experiment only if the cached version is >= the requested version. Returns nil if not found or version is too old.
func (cache ExperimentCache) Save(name string, version int, experiment *Experiment) error{ .api }
Saves an experiment to the cache with the specified name and version.
func (cache ExperimentCache) List() ([]CachedExperimentHeader, error){ .api }
Returns a list of all cached experiments.
func (cache ExperimentCache) Delete(name string) error{ .api }
Removes an experiment from the cache by name.
func (cache ExperimentCache) Clear() error{ .api }
Removes all cached experiments. Use with caution.
type CachedExperimentHeader struct {
Name string
Version int
}{ .api }
Metadata about a cached experiment.
const EXPERIMENT_VERSION = 1 // Bump to bust cache
var _ = Describe("benchmarks", func() {
var cache gmeasure.ExperimentCache
var experiment *gmeasure.Experiment
BeforeEach(func() {
cache, _ = gmeasure.NewExperimentCache("./cache")
name := CurrentSpecReport().LeafNodeText
experiment = cache.Load(name, EXPERIMENT_VERSION)
if experiment != nil {
AddReportEntry(experiment.Name, experiment)
Skip("cached")
}
experiment = gmeasure.NewExperiment(name)
AddReportEntry(experiment.Name, experiment)
})
It("measures API performance", func() {
experiment.SampleDuration("api call", func(idx int) {
makeAPICall()
}, gmeasure.SamplingConfig{N: 100})
})
AfterEach(func() {
if !CurrentSpecReport().State.Is(types.SpecStateSkipped) {
cache.Save(experiment.Name, EXPERIMENT_VERSION, experiment)
}
})
})Ranks multiple statistics by specified criteria for comparison.
type Ranking struct {
Criteria RankingCriteria
Stats []Stats
}func RankStats(criteria RankingCriteria, stats ...Stats) Ranking{ .api }
Creates a ranking of the provided stats according to the criteria.
Example:
stats1 := experiment1.GetStats("response time")
stats2 := experiment2.GetStats("response time")
stats3 := experiment3.GetStats("response time")
ranking := gmeasure.RankStats(gmeasure.LowerMeanIsBetter, stats1, stats2, stats3)
AddReportEntry("Performance Ranking", ranking)func (r Ranking) Winner() Stats{ .api }
Returns the top-ranked Stats according to the ranking criteria.
func (r Ranking) String() string{ .api }
Returns an unstyled report with rank-ordered statistics.
func (r Ranking) ColorableString() string{ .api }
Returns a styled report. Called by Ginkgo when registered via AddReportEntry.
The table sub-package (github.com/onsi/gomega/gmeasure/table) provides utilities for building formatted tables to display measurement results. These are used internally by Experiment, Measurement, and Ranking types for their String() and ColorableString() methods.
func NewTable() *TableCreates a new empty table for displaying formatted output.
Usage Example:
import "github.com/onsi/gomega/gmeasure/table"
t := table.NewTable()
t.AppendRow(table.R(table.C("Name"), table.C("Value")))
t.AppendRow(table.R(table.C("Test1"), table.C("42")))
fmt.Println(t.Render())func R(args ...any) *RowCreates a new table row with the specified cells and options. Arguments can be:
Cell objects created with C()Divider for row dividersAlignType for row-level alignmentUsage Example:
import "github.com/onsi/gomega/gmeasure/table"
// Simple row with cells
row := table.R(table.C("col1"), table.C("col2"))
// Row with styling
row := table.R(table.C("Name"), table.C("Value"), "{{bold}}")
// Row with divider
row := table.R(table.C("Header"), table.Divider("="))func C(contents string, args ...any) CellCreates a table cell with the specified content and optional formatting. Arguments can be:
AlignType for cell alignment (AlignTypeLeft, AlignTypeRight, AlignTypeCenter)Usage Example:
import "github.com/onsi/gomega/gmeasure/table"
// Simple cell
cell := table.C("Hello")
// Styled cell
cell := table.C("Important", "{{bold}}")
// Right-aligned cell
cell := table.C("123.45", table.AlignTypeRight)
// Styled and aligned cell
cell := table.C("Error", "{{red}}", table.AlignTypeCenter)Complete Table Example:
import (
"fmt"
"github.com/onsi/gomega/gmeasure/table"
)
t := table.NewTable()
// Header row with divider
t.AppendRow(table.R(
table.C("Test Name"),
table.C("Duration"),
table.C("Status"),
table.Divider("="),
))
// Data rows
t.AppendRow(table.R(
table.C("TestA"),
table.C("1.23s", table.AlignTypeRight),
table.C("PASS", "{{green}}"),
))
t.AppendRow(table.R(
table.C("TestB"),
table.C("0.45s", table.AlignTypeRight),
table.C("FAIL", "{{red}}"),
))
fmt.Println(t.Render())package mypackage_test
import (
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gmeasure"
)
var _ = Describe("Performance Tests", func() {
var experiment *gmeasure.Experiment
BeforeEach(func() {
experiment = gmeasure.NewExperiment("API Performance")
AddReportEntry(experiment.Name, experiment)
})
It("measures response times", func() {
// Record single duration
experiment.RecordDuration("warmup", 50*time.Millisecond)
// Measure function duration
experiment.MeasureDuration("single request", func() {
makeAPIRequest()
}, gmeasure.Annotation("GET /users"))
// Sample multiple times
experiment.SampleDuration("load test", func(idx int) {
makeAPIRequest()
}, gmeasure.SamplingConfig{
N: 100,
Duration: 30 * time.Second,
}, gmeasure.Precision(time.Millisecond))
// Record custom values
experiment.RecordValue("requests/sec", 1250.5,
gmeasure.Units("req/s"),
gmeasure.Precision(1),
gmeasure.Style("{{green}}"))
// Use stopwatch for detailed timing
stopwatch := experiment.NewStopwatch()
setupDatabase()
stopwatch.Record("setup").Reset()
runQueries()
stopwatch.Record("queries").Reset()
cleanup()
stopwatch.Record("cleanup")
// Get statistics
stats := experiment.GetStats("load test")
Expect(stats.DurationFor(gmeasure.StatMean)).To(BeNumerically("<", 100*time.Millisecond))
})
It("compares multiple implementations", func() {
// Test implementation A
experimentA := gmeasure.NewExperiment("Implementation A")
experimentA.SampleDuration("execution", func(idx int) {
implementationA()
}, gmeasure.SamplingConfig{N: 50})
// Test implementation B
experimentB := gmeasure.NewExperiment("Implementation B")
experimentB.SampleDuration("execution", func(idx int) {
implementationB()
}, gmeasure.SamplingConfig{N: 50})
// Rank them
statsA := experimentA.GetStats("execution")
statsB := experimentB.GetStats("execution")
ranking := gmeasure.RankStats(gmeasure.LowerMeanIsBetter, statsA, statsB)
AddReportEntry("Performance Ranking", ranking)
Expect(ranking.Winner().ExperimentName).To(Equal("Implementation A"))
})
})gmeasure integrates seamlessly with Ginkgo V2:
Register experiments, measurements, or rankings as report entries:
AddReportEntry(experiment.Name, experiment) // Full experiment
AddReportEntry("Response Times", measurement) // Single measurement
AddReportEntry("Performance Comparison", ranking) // RankingAll gmeasure types support JSON marshaling for test report export:
data, _ := json.Marshal(experiment)Use Ginkgo's format strings in Style() decorators:
{{red}}, {{green}}, {{blue}}, {{yellow}}, {{cyan}}, {{gray}}{{bold}}, {{underline}}{{/}} to close stylingstopwatch.NewStopwatch()