diff --git a/wasp/benchspy/basic.go b/wasp/benchspy/basic.go index 8e046fa96..da85dd622 100644 --- a/wasp/benchspy/basic.go +++ b/wasp/benchspy/basic.go @@ -22,6 +22,8 @@ type BasicData struct { GeneratorConfigs map[string]*wasp.Config `json:"generator_configs"` } +// MustNewBasicData creates a new BasicData instance from a commit or tag. +// It panics if the creation fails, ensuring that the caller receives a valid instance. func MustNewBasicData(commitOrTag string, generators ...*wasp.Generator) BasicData { b, err := NewBasicData(commitOrTag, generators...) if err != nil { @@ -31,6 +33,9 @@ func MustNewBasicData(commitOrTag string, generators ...*wasp.Generator) BasicDa return *b } +// NewBasicData creates a new BasicData instance using the provided commit or tag and a list of generators. +// It ensures that at least one generator is provided and that it is associated with a testing.T instance. +// This function is essential for initializing test data configurations in a structured manner. func NewBasicData(commitOrTag string, generators ...*wasp.Generator) (*BasicData, error) { if len(generators) == 0 { return nil, errors.New("at least one generator is required") @@ -58,6 +63,9 @@ func NewBasicData(commitOrTag string, generators ...*wasp.Generator) (*BasicData return b, nil } +// FillStartEndTimes calculates the earliest start time and latest end time from generator schedules. +// It updates the BasicData instance with these times, ensuring all segments have valid start and end times. +// This function is essential for managing and analyzing generator execution timelines. func (b *BasicData) FillStartEndTimes() error { earliestTime := time.Now() var latestTime time.Time @@ -89,6 +97,9 @@ func (b *BasicData) FillStartEndTimes() error { return nil } +// Validate checks the integrity of the BasicData fields, ensuring that the test start and end times are set, +// and that at least one generator configuration is provided. +// It returns an error if any of these conditions are not met, guiding users to correct their input. func (b *BasicData) Validate() error { if b.TestStart.IsZero() { return errors.New("test start time is missing. We cannot query Loki without a time range. Please set it and try again") @@ -104,6 +115,10 @@ func (b *BasicData) Validate() error { return nil } +// IsComparable checks if two BasicData instances have the same configuration settings. +// It validates the count, presence, and equivalence of generator configurations, +// returning an error if any discrepancies are found. This function is useful for ensuring +// consistency between different data reports. func (b *BasicData) IsComparable(otherData BasicData) error { // are all configs present? do they have the same schedule type? do they have the same segments? is call timeout the same? is rate limit timeout the same? if len(b.GeneratorConfigs) != len(otherData.GeneratorConfigs) { diff --git a/wasp/benchspy/generator.go b/wasp/benchspy/generator.go index 8a6b4308b..c57fba15c 100644 --- a/wasp/benchspy/generator.go +++ b/wasp/benchspy/generator.go @@ -20,6 +20,8 @@ type GeneratorQueryExecutor struct { QueryResults map[string]interface{} `json:"query_results"` } +// NewGeneratorQueryExecutor creates a new instance of GeneratorQueryExecutor using the provided generator. +// It initializes standard queries and prepares the executor for handling query results, returning any errors encountered. func NewGeneratorQueryExecutor(generator *wasp.Generator) (*GeneratorQueryExecutor, error) { g := &GeneratorQueryExecutor{ KindName: string(StandardQueryExecutor_Generator), @@ -37,14 +39,20 @@ func NewGeneratorQueryExecutor(generator *wasp.Generator) (*GeneratorQueryExecut return g, nil } +// Results returns the query results as a map of string to interface{}. +// It allows users to access the outcomes of executed queries, facilitating further processing or type assertions. func (g *GeneratorQueryExecutor) Results() map[string]interface{} { return g.QueryResults } +// Kind returns the type of the query executor as a string. +// It is used to identify the specific kind of query executor in various operations. func (l *GeneratorQueryExecutor) Kind() string { return l.KindName } +// IsComparable checks if the current GeneratorQueryExecutor is comparable to another QueryExecutor. +// It validates the type and configuration of the generators, returning an error if they are not compatible. func (g *GeneratorQueryExecutor) IsComparable(otherQueryExecutor QueryExecutor) error { otherType := reflect.TypeOf(otherQueryExecutor) @@ -76,6 +84,8 @@ func (l *GeneratorQueryExecutor) compareQueries(other map[string]GeneratorQueryF return nil } +// Validate checks if the generator is set and if at least one query is provided. +// It returns an error if either condition is not met, ensuring that the query executor is properly configured before execution. func (g *GeneratorQueryExecutor) Validate() error { if g.Generator == nil { return errors.New("generator is not set") @@ -88,6 +98,8 @@ func (g *GeneratorQueryExecutor) Validate() error { return nil } +// Execute runs the defined queries using the generator's data, aggregating results for each query. +// It returns an error if the generator is not set, lacks data, or if any query execution fails. func (g *GeneratorQueryExecutor) Execute(_ context.Context) error { if g.Generator == nil { return errors.New("generator is not set") @@ -123,6 +135,8 @@ func (g *GeneratorQueryExecutor) Execute(_ context.Context) error { return nil } +// TimeRange ensures that the data responses stored in the generator are within the specified time range. +// It is a no-op function, as the generator already maintains the correct time range for its responses. func (g *GeneratorQueryExecutor) TimeRange(_, _ time.Time) { // nothing to do here, since all responses stored in the generator are already in the right time range } @@ -185,6 +199,9 @@ func (g *GeneratorQueryExecutor) standardQuery(standardMetric StandardLoadMetric } } +// MarshalJSON customizes the JSON encoding of the GeneratorQueryExecutor. +// It serializes only the relevant fields, including query names and results, +// making it suitable for efficient data transmission and storage in JSON format. func (g *GeneratorQueryExecutor) MarshalJSON() ([]byte, error) { // we need custom marshalling to only include query names, since the functions are not serializable type QueryExecutor struct { @@ -213,6 +230,9 @@ func (g *GeneratorQueryExecutor) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes JSON data into a GeneratorQueryExecutor instance. +// It populates the Queries and QueryResults fields, converting types as necessary. +// This function is essential for initializing the executor with data from JSON sources. func (g *GeneratorQueryExecutor) UnmarshalJSON(data []byte) error { // helper struct with QueryExecutors as json.RawMessage and QueryResults as map[string]interface{} // and as actual types diff --git a/wasp/benchspy/loki.go b/wasp/benchspy/loki.go index c04d7ddfe..ac3b0bf4b 100644 --- a/wasp/benchspy/loki.go +++ b/wasp/benchspy/loki.go @@ -15,6 +15,9 @@ import ( "golang.org/x/sync/errgroup" ) +// NewLokiQueryExecutor creates a new LokiQueryExecutor instance. +// It initializes the executor with provided queries and Loki configuration, +// enabling efficient querying of logs from a Loki data source. func NewLokiQueryExecutor(queries map[string]string, lokiConfig *wasp.LokiConfig) *LokiQueryExecutor { return &LokiQueryExecutor{ KindName: string(StandardQueryExecutor_Loki), @@ -40,14 +43,20 @@ type LokiQueryExecutor struct { Config *wasp.LokiConfig `json:"-"` } +// Results returns the query results as a map of string keys to interface{} values. +// It allows users to access the outcomes of executed queries, facilitating further processing or type assertions. func (l *LokiQueryExecutor) Results() map[string]interface{} { return l.QueryResults } +// Kind returns the type of the query executor as a string. +// It is used to identify the specific kind of query executor in various contexts. func (l *LokiQueryExecutor) Kind() string { return l.KindName } +// IsComparable checks if the current LokiQueryExecutor is of the same type as another QueryExecutor. +// It validates the queries for equivalence, returning an error if they differ in count or content. func (l *LokiQueryExecutor) IsComparable(otherQueryExecutor QueryExecutor) error { otherType := reflect.TypeOf(otherQueryExecutor) @@ -58,6 +67,9 @@ func (l *LokiQueryExecutor) IsComparable(otherQueryExecutor QueryExecutor) error return l.compareQueries(otherQueryExecutor.(*LokiQueryExecutor).Queries) } +// Validate checks if the LokiQueryExecutor has valid queries and configuration. +// It returns an error if no queries are set or if the configuration is missing, +// ensuring that the executor is properly initialized before execution. func (l *LokiQueryExecutor) Validate() error { if len(l.Queries) == 0 { return errors.New("there are no Loki queries, there's nothing to fetch. Please set them and try again") @@ -69,6 +81,9 @@ func (l *LokiQueryExecutor) Validate() error { return nil } +// Execute runs multiple Loki queries concurrently, collecting their results. +// It requires a valid configuration and handles basic authentication if provided. +// This function is useful for fetching logs from Loki in a structured manner. func (l *LokiQueryExecutor) Execute(ctx context.Context) error { var basicAuth client.LokiBasicAuth @@ -159,11 +174,16 @@ func (l *LokiQueryExecutor) compareQueries(other map[string]string) error { return nil } +// TimeRange sets the start and end time for the Loki query execution. +// This function is essential for defining the time window of the data to be fetched. func (l *LokiQueryExecutor) TimeRange(start, end time.Time) { l.StartTime = start l.EndTime = end } +// UnmarshalJSON parses the JSON-encoded data and populates the LokiQueryExecutor fields. +// It converts the query results from a generic map to a specific type map, ensuring type safety. +// This function is essential for handling JSON data in a structured manner. func (l *LokiQueryExecutor) UnmarshalJSON(data []byte) error { // helper struct with QueryResults map[string]interface{} type Alias LokiQueryExecutor @@ -188,6 +208,8 @@ func (l *LokiQueryExecutor) UnmarshalJSON(data []byte) error { return nil } +// NewStandardMetricsLokiExecutor creates a LokiQueryExecutor configured with standard metrics queries. +// It generates queries based on provided test parameters and time range, returning the executor or an error if query generation fails. func NewStandardMetricsLokiExecutor(lokiConfig *wasp.LokiConfig, testName, generatorName, branch, commit string, startTime, endTime time.Time) (*LokiQueryExecutor, error) { lq := &LokiQueryExecutor{ KindName: string(StandardQueryExecutor_Loki), diff --git a/wasp/benchspy/metrics.go b/wasp/benchspy/metrics.go index 63dd61b16..f0660d332 100644 --- a/wasp/benchspy/metrics.go +++ b/wasp/benchspy/metrics.go @@ -6,6 +6,9 @@ import ( "strconv" ) +// CalculatePercentile computes the specified percentile of a slice of numbers. +// It is useful for statistical analysis, allowing users to understand data distributions +// by retrieving values at specific percentiles, such as median or 95th percentile. func CalculatePercentile(numbers []float64, percentile float64) float64 { // Sort the slice sort.Float64s(numbers) @@ -31,6 +34,9 @@ func CalculatePercentile(numbers []float64, percentile float64) float64 { return numbers[lowerIndex]*(1-weight) + numbers[upperIndex]*weight } +// StringSliceToFloat64Slice converts a slice of strings to a slice of float64 values. +// It returns an error if any string cannot be parsed as a float64, making it useful for +// data transformation in numerical computations. func StringSliceToFloat64Slice(s []string) ([]float64, error) { numbers := make([]float64, len(s)) for i, str := range s { diff --git a/wasp/benchspy/prometheus.go b/wasp/benchspy/prometheus.go index 4259e9972..071115ffd 100644 --- a/wasp/benchspy/prometheus.go +++ b/wasp/benchspy/prometheus.go @@ -47,6 +47,8 @@ func NewPrometheusQueryExecutor(url string, startTime, endTime time.Time, querie }, nil } +// NewStandardPrometheusQueryExecutor creates a PrometheusQueryExecutor with standard queries based on the provided time range and name regex pattern. +// It simplifies the setup of query execution against a Prometheus instance for monitoring and reporting purposes. func NewStandardPrometheusQueryExecutor(url string, startTime, endTime time.Time, nameRegexPattern string) (*PrometheusQueryExecutor, error) { p := &PrometheusQueryExecutor{} @@ -58,6 +60,8 @@ func NewStandardPrometheusQueryExecutor(url string, startTime, endTime time.Time return NewPrometheusQueryExecutor(url, startTime, endTime, standardQueries) } +// Execute runs a series of Prometheus queries concurrently, collecting results and warnings for each query. +// It returns an error if any query fails, allowing for efficient data retrieval in reporting scenarios. func (r *PrometheusQueryExecutor) Execute(ctx context.Context) error { for name, query := range r.Queries { result, warnings, queryErr := r.client.Query(ctx, query, r.endTime) @@ -75,14 +79,22 @@ func (r *PrometheusQueryExecutor) Execute(ctx context.Context) error { return nil } +// Results returns the query results as a map of string to interface{}. +// It allows users to access the outcomes of executed queries, facilitating further processing or analysis of the data. func (r *PrometheusQueryExecutor) Results() map[string]interface{} { return r.QueryResults } +// Kind returns the type of the query executor as a string. +// It is used to identify the specific kind of executor in a collection, +// enabling filtering and type-specific operations. func (l *PrometheusQueryExecutor) Kind() string { return l.KindName } +// Validate checks the PrometheusQueryExecutor's configuration for correctness. +// It ensures that the client is set, queries are provided, and both start and end times are defined. +// This function is essential for preventing execution errors due to misconfiguration. func (r *PrometheusQueryExecutor) Validate() error { if r.client == nil { return errors.New("prometheus client is nil") @@ -103,6 +115,8 @@ func (r *PrometheusQueryExecutor) Validate() error { return nil } +// IsComparable checks if the provided QueryExecutor is of the same type as the receiver. +// It returns an error if the types do not match, ensuring type safety for query comparisons. func (r *PrometheusQueryExecutor) IsComparable(other QueryExecutor) error { otherType := reflect.TypeOf(other) if otherType != reflect.TypeOf(r) { @@ -133,10 +147,16 @@ func (r *PrometheusQueryExecutor) compareQueries(other map[string]string) error return nil } +// Warnings returns a map of warnings encountered during Prometheus queries. +// This function is useful for retrieving any issues that may have occurred, +// allowing users to handle or log them appropriately. func (r *PrometheusQueryExecutor) Warnings() map[string]v1.Warnings { return r.warnings } +// MustResultsAsValue returns a map of query results as model.Values. +// It is used to simplify the retrieval of metric values from the query results, +// enabling easier processing and serialization of metrics in various formats. func (r *PrometheusQueryExecutor) MustResultsAsValue() map[string]model.Value { results := make(map[string]model.Value) for name, result := range r.QueryResults { @@ -145,6 +165,8 @@ func (r *PrometheusQueryExecutor) MustResultsAsValue() map[string]model.Value { return results } +// TimeRange sets the start and end time for the Prometheus query execution. +// This function is essential for defining the time window in which data will be fetched and analyzed. func (r *PrometheusQueryExecutor) TimeRange(startTime, endTime time.Time) { r.startTime = startTime r.endTime = endTime @@ -188,6 +210,9 @@ type TypedMetric struct { MetricType string `json:"metric_type"` } +// MarshalJSON customizes the JSON representation of the PrometheusQueryExecutor. +// It includes only essential fields: Kind, Queries, and simplified QueryResults, +// making it suitable for efficient serialization and data transfer. func (g *PrometheusQueryExecutor) MarshalJSON() ([]byte, error) { // we need custom marshalling to only include some parts of the metrics type QueryExecutor struct { @@ -214,6 +239,9 @@ func (g *PrometheusQueryExecutor) MarshalJSON() ([]byte, error) { return json.Marshal(q) } +// UnmarshalJSON decodes JSON data into a PrometheusQueryExecutor instance. +// It populates the QueryResults field with appropriately typed metrics, +// enabling easy access to the results of Prometheus queries. func (r *PrometheusQueryExecutor) UnmarshalJSON(data []byte) error { // helper struct with QueryResults map[string]interface{} type Alias PrometheusQueryExecutor diff --git a/wasp/benchspy/report.go b/wasp/benchspy/report.go index 88461f01f..975efa5d3 100644 --- a/wasp/benchspy/report.go +++ b/wasp/benchspy/report.go @@ -19,18 +19,27 @@ type StandardReport struct { QueryExecutors []QueryExecutor `json:"query_executors"` } +// Store saves the report to local storage as a JSON file. +// It returns the absolute path of the stored report file or an error if the operation fails. func (b *StandardReport) Store() (string, error) { return b.LocalStorage.Store(b.TestName, b.CommitOrTag, b) } +// Load retrieves a report based on the specified test name and commit or tag. +// It utilizes local storage to find and decode the corresponding report file, +// ensuring that the report is ready for further processing or analysis. func (b *StandardReport) Load(testName, commitOrTag string) error { return b.LocalStorage.Load(testName, commitOrTag, b) } +// LoadLatest retrieves the most recent report for a specified test name from local storage. +// It returns an error if the report cannot be loaded, enabling users to access the latest test results efficiently. func (b *StandardReport) LoadLatest(testName string) error { return b.LocalStorage.Load(testName, "", b) } +// ResultsAs retrieves and casts results from specified query executors to a desired type. +// It returns a map of query names to their corresponding results, or an error if casting fails. func ResultsAs[Type any](newType Type, queryExecutors []QueryExecutor, queryExecutorType StandardQueryExecutorType, queryNames ...string) (map[string]Type, error) { results := make(map[string]Type) @@ -61,6 +70,9 @@ func ResultsAs[Type any](newType Type, queryExecutors []QueryExecutor, queryExec return results, nil } +// MustAllLokiResults retrieves all Loki query results from a StandardReport. +// It panics if an error occurs during the retrieval process, ensuring that +// the caller receives valid results or an immediate failure. func MustAllLokiResults(sr *StandardReport) map[string][]string { results, err := ResultsAs([]string{}, sr.QueryExecutors, StandardQueryExecutor_Loki) if err != nil { @@ -69,6 +81,9 @@ func MustAllLokiResults(sr *StandardReport) map[string][]string { return results } +// MustAllGeneratorResults retrieves all generator results from a StandardReport. +// It panics if an error occurs during the retrieval process, ensuring that the caller +// receives valid results or an immediate failure. func MustAllGeneratorResults(sr *StandardReport) map[string]string { results, err := ResultsAs("string", sr.QueryExecutors, StandardQueryExecutor_Generator) if err != nil { @@ -77,6 +92,9 @@ func MustAllGeneratorResults(sr *StandardReport) map[string]string { return results } +// MustAllPrometheusResults retrieves all Prometheus query results from a StandardReport. +// It returns a map of query names to their corresponding model.Values, ensuring type safety. +// This function is useful for aggregating and accessing Prometheus metrics efficiently. func MustAllPrometheusResults(sr *StandardReport) map[string]model.Value { results := make(map[string]model.Value) @@ -93,6 +111,8 @@ func MustAllPrometheusResults(sr *StandardReport) map[string]model.Value { return results } +// FetchData retrieves data for the report by executing queries within the specified time range. +// It ensures that all necessary parameters are set and handles concurrent execution of query tasks. func (b *StandardReport) FetchData(ctx context.Context) error { // if b.TestStart.IsZero() || b.TestEnd.IsZero() { // fillErr := b.BasicData.FillStartEndTimes() @@ -131,6 +151,8 @@ func (b *StandardReport) FetchData(ctx context.Context) error { return nil } +// IsComparable checks if the current report can be compared to another report. +// It validates the type and compares basic data and query executors, returning an error if they are not comparable. func (b *StandardReport) IsComparable(otherReport Reporter) error { if _, ok := otherReport.(*StandardReport); !ok { return fmt.Errorf("expected type %s, got %T", "*StandardReport", otherReport) @@ -162,24 +184,32 @@ type standardReportConfig struct { type StandardReportOption func(*standardReportConfig) +// WithStandardQueryExecutorType sets the executor type for a standard report configuration. +// This function allows customization of the query execution strategy used in report generation. func WithStandardQueryExecutorType(executorType StandardQueryExecutorType) StandardReportOption { return func(c *standardReportConfig) { c.executorType = executorType } } +// WithGenerators sets the generators for the standard report configuration. +// It allows users to specify multiple generator instances to customize report generation. func WithGenerators(generators ...*wasp.Generator) StandardReportOption { return func(c *standardReportConfig) { c.generators = generators } } +// WithPrometheus sets the Prometheus configuration for the standard report. +// It allows users to integrate Prometheus metrics into their reporting setup. func WithPrometheus(prometheusConfig *PrometheusConfig) StandardReportOption { return func(c *standardReportConfig) { c.prometheusConfig = prometheusConfig } } +// WithReportDirectory sets the directory for storing report files. +// It returns a function that configures the standard report with the specified directory. func WithReportDirectory(reportDirectory string) StandardReportOption { return func(c *standardReportConfig) { c.reportDirectory = reportDirectory @@ -211,6 +241,9 @@ func (c *standardReportConfig) validate() error { return nil } +// NewStandardReport creates a new StandardReport based on the provided commit or tag and options. +// It initializes necessary data and query executors, ensuring all configurations are valid. +// This function is useful for generating reports that require specific data and query setups. func NewStandardReport(commitOrTag string, opts ...StandardReportOption) (*StandardReport, error) { config := standardReportConfig{} for _, opt := range opts { @@ -303,6 +336,9 @@ func generatorHasLabels(g *wasp.Generator) bool { return g.Cfg.Labels["branch"] != "" && g.Cfg.Labels["commit"] != "" } +// UnmarshalJSON parses the JSON-encoded data and populates the StandardReport struct. +// It extracts QueryExecutors and ResourceFetchers as raw JSON messages for further processing. +// This function is essential for deserializing StandardReport instances from JSON format. func (s *StandardReport) UnmarshalJSON(data []byte) error { // helper struct with QueryExecutors as json.RawMessage type Alias StandardReport @@ -439,6 +475,9 @@ func convertQueryResults(results map[string]interface{}) (map[string]interface{} return converted, nil } +// FetchNewReportAndLoadLatestPrevious creates a new report for a specified commit or tag, +// loads the latest previous report, and checks their comparability. +// It returns both reports and any error encountered during the process. func FetchNewReportAndLoadLatestPrevious(ctx context.Context, newCommitOrTag string, newReportOpts ...StandardReportOption) (*StandardReport, *StandardReport, error) { newReport, err := NewStandardReport(newCommitOrTag, newReportOpts...) if err != nil { diff --git a/wasp/benchspy/storage.go b/wasp/benchspy/storage.go index 2621c065e..c6966a072 100644 --- a/wasp/benchspy/storage.go +++ b/wasp/benchspy/storage.go @@ -31,6 +31,9 @@ func (l *LocalStorage) cleanTestName(testName string) string { return strings.ReplaceAll(testName, "/", "_") } +// Store saves a test report as a JSON file in local storage. +// It organizes reports by test name and commit/tag, ensuring easy retrieval and management. +// This function is useful for preserving test results for future reference or analysis. func (l *LocalStorage) Store(testName, commitOrTag string, report interface{}) (string, error) { l.defaultDirectoryIfEmpty() asJson, err := json.MarshalIndent(report, "", " ") @@ -66,6 +69,8 @@ func (l *LocalStorage) Store(testName, commitOrTag string, report interface{}) ( return abs, nil } +// Load retrieves a report from local storage based on the specified test name and optional commit or tag. +// It decodes the report into the provided interface, allowing users to access test results easily. func (l *LocalStorage) Load(testName, commitOrTag string, report interface{}) error { l.defaultDirectoryIfEmpty() if testName == "" {