diff --git a/wasp/benchspy/basic.go b/wasp/benchspy/basic.go index 8e046fa96..f466ab33f 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,8 @@ 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. func (b *BasicData) FillStartEndTimes() error { earliestTime := time.Now() var latestTime time.Time @@ -89,6 +96,8 @@ 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. 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 +113,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 data reports before processing or comparison. 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/direct.go b/wasp/benchspy/direct.go index 7385dc574..4f1b85046 100644 --- a/wasp/benchspy/direct.go +++ b/wasp/benchspy/direct.go @@ -20,6 +20,8 @@ type DirectQueryExecutor struct { QueryResults map[string]interface{} `json:"query_results"` } +// NewStandardDirectQueryExecutor creates a new DirectQueryExecutor configured for standard queries. +// It initializes the executor and generates the necessary queries, returning the executor or an error if the process fails. func NewStandardDirectQueryExecutor(generator *wasp.Generator) (*DirectQueryExecutor, error) { g := &DirectQueryExecutor{ KindName: string(StandardQueryExecutor_Direct), @@ -33,6 +35,8 @@ func NewStandardDirectQueryExecutor(generator *wasp.Generator) (*DirectQueryExec return NewDirectQueryExecutor(generator, queries) } +// NewDirectQueryExecutor creates a new DirectQueryExecutor with the specified generator and query functions. +// It initializes the executor with a kind name and prepares a map for query results, enabling efficient query execution. func NewDirectQueryExecutor(generator *wasp.Generator, queries map[string]DirectQueryFn) (*DirectQueryExecutor, error) { g := &DirectQueryExecutor{ KindName: string(StandardQueryExecutor_Direct), @@ -44,14 +48,20 @@ func NewDirectQueryExecutor(generator *wasp.Generator, queries map[string]Direct return g, nil } +// 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 (g *DirectQueryExecutor) Results() map[string]interface{} { return g.QueryResults } +// Kind returns the type of the query executor as a string. +// It is useful for identifying the specific implementation of a query executor in a collection. func (l *DirectQueryExecutor) Kind() string { return l.KindName } +// IsComparable checks if the given QueryExecutor is of the same type and has comparable configurations. +// It returns an error if the types do not match or if the configurations are not comparable. func (g *DirectQueryExecutor) IsComparable(otherQueryExecutor QueryExecutor) error { otherType := reflect.TypeOf(otherQueryExecutor) @@ -83,6 +93,9 @@ func (l *DirectQueryExecutor) compareQueries(other map[string]DirectQueryFn) err return nil } +// Validate checks if the query executor is properly configured. +// It ensures that a generator is set and at least one query is provided. +// Returns an error if validation fails, helping to prevent execution issues. func (g *DirectQueryExecutor) Validate() error { if g.Generator == nil { return errors.New("generator is not set") @@ -95,6 +108,9 @@ func (g *DirectQueryExecutor) Validate() error { return nil } +// Execute runs the defined queries using the data from the generator. +// It validates the generator's data and aggregates responses before executing each query. +// This function is essential for processing and retrieving results from multiple queries concurrently. func (g *DirectQueryExecutor) Execute(_ context.Context) error { if g.Generator == nil { return errors.New("generator is not set") @@ -130,6 +146,8 @@ func (g *DirectQueryExecutor) Execute(_ context.Context) error { return nil } +// TimeRange ensures that the query executor operates within the specified time range. +// It is a no-op for executors that already have responses stored in the correct time range. func (g *DirectQueryExecutor) TimeRange(_, _ time.Time) { // nothing to do here, since all responses stored in the generator are already in the right time range } @@ -198,6 +216,9 @@ func (g *DirectQueryExecutor) standardQuery(standardMetric StandardLoadMetric) ( } } +// MarshalJSON customizes the JSON representation of the DirectQueryExecutor. +// It serializes only the relevant fields, including query names and results, +// making it suitable for efficient data transmission and storage. func (g *DirectQueryExecutor) MarshalJSON() ([]byte, error) { // we need custom marshalling to only include query names, since the functions are not serializable type QueryExecutor struct { @@ -226,6 +247,9 @@ func (g *DirectQueryExecutor) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON decodes JSON data into a DirectQueryExecutor instance. +// It populates the executor's fields, including queries and results, +// enabling seamless integration of JSON configurations into the executor's structure. func (g *DirectQueryExecutor) 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..73fee4cd8 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 Loki in a structured manner. func NewLokiQueryExecutor(queries map[string]string, lokiConfig *wasp.LokiConfig) *LokiQueryExecutor { return &LokiQueryExecutor{ KindName: string(StandardQueryExecutor_Loki), @@ -40,14 +43,21 @@ type LokiQueryExecutor struct { Config *wasp.LokiConfig `json:"-"` } +// 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 (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 operations. func (l *LokiQueryExecutor) Kind() string { return l.KindName } +// IsComparable checks if the given QueryExecutor is of the same type as the current instance. +// It compares the queries of both executors to ensure they are equivalent in structure and content. +// This function is useful for validating compatibility between different query executors. func (l *LokiQueryExecutor) IsComparable(otherQueryExecutor QueryExecutor) error { otherType := reflect.TypeOf(otherQueryExecutor) @@ -58,6 +68,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 ready for 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 +82,9 @@ func (l *LokiQueryExecutor) Validate() error { return nil } +// Execute runs the configured Loki queries concurrently and collects the results. +// It requires a valid configuration and handles basic authentication if provided. +// The function returns an error if any query execution fails or if the configuration is missing. func (l *LokiQueryExecutor) Execute(ctx context.Context) error { var basicAuth client.LokiBasicAuth @@ -159,11 +175,15 @@ 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, enabling type-safe access to the results. 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..ea2b4598c 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,8 @@ 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 conversion tasks. 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 b91a04169..9a025e76b 100644 --- a/wasp/benchspy/prometheus.go +++ b/wasp/benchspy/prometheus.go @@ -22,6 +22,8 @@ type PrometheusConfig struct { const PrometheusUrlEnvVar = "PROMETHEUS_URL" +// NewPrometheusConfig creates a new PrometheusConfig instance with the specified name regex patterns. +// It retrieves the Prometheus URL from the environment and is used to configure query execution for Prometheus data sources. func NewPrometheusConfig(nameRegexPatterns ...string) *PrometheusConfig { return &PrometheusConfig{ Url: os.Getenv(PrometheusUrlEnvVar), @@ -56,6 +58,9 @@ func NewPrometheusQueryExecutor(queries map[string]string, config *PrometheusCon }, nil } +// NewStandardPrometheusQueryExecutor creates a PrometheusQueryExecutor with standard queries +// based on the provided time range and configuration. It simplifies the process of generating +// queries for Prometheus, making it easier to integrate Prometheus data into reports. func NewStandardPrometheusQueryExecutor(startTime, endTime time.Time, config *PrometheusConfig) (*PrometheusQueryExecutor, error) { p := &PrometheusQueryExecutor{} @@ -74,6 +79,8 @@ func NewStandardPrometheusQueryExecutor(startTime, endTime time.Time, config *Pr return NewPrometheusQueryExecutor(standardQueries, config) } +// Execute runs the defined Prometheus queries concurrently, collecting results and warnings. +// It returns an error if any query fails, allowing for efficient data retrieval in reporting tasks. func (r *PrometheusQueryExecutor) Execute(ctx context.Context) error { for name, query := range r.Queries { result, warnings, queryErr := r.client.Query(ctx, query, r.EndTime) @@ -91,14 +98,20 @@ 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 results of executed queries, facilitating data retrieval and manipulation. 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 of query executors. func (l *PrometheusQueryExecutor) Kind() string { return l.KindName } +// Validate checks the PrometheusQueryExecutor for a valid client and ensures that at least one query is provided. +// It returns an error if the client is nil or no queries are specified, helping to ensure proper configuration before execution. func (r *PrometheusQueryExecutor) Validate() error { if r.client == nil { return errors.New("prometheus client is nil") @@ -111,6 +124,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) { @@ -141,10 +156,15 @@ func (r *PrometheusQueryExecutor) compareQueries(other map[string]string) error return nil } +// Warnings returns a map of warnings encountered during query execution. +// This function is useful for retrieving any issues that may have arisen, +// allowing users to handle or log them appropriately. func (r *PrometheusQueryExecutor) Warnings() map[string]v1.Warnings { return r.warnings } +// MustResultsAsValue retrieves the query results as a map of metric names to their corresponding values. +// It ensures that the results are in a consistent format, making it easier to work with metrics in subsequent operations. func (r *PrometheusQueryExecutor) MustResultsAsValue() map[string]model.Value { results := make(map[string]model.Value) for name, result := range r.QueryResults { @@ -180,6 +200,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 for data retrieval, ensuring accurate and relevant results. func (r *PrometheusQueryExecutor) TimeRange(startTime, endTime time.Time) { r.StartTime = startTime r.EndTime = endTime @@ -222,6 +244,9 @@ type TypedMetric struct { MetricType string `json:"metric_type"` } +// MarshalJSON customizes the JSON representation of PrometheusQueryExecutor. +// It includes only essential fields: Kind, Queries, and simplified QueryResults. +// This function is useful for serializing the executor's state in a concise format. func (g *PrometheusQueryExecutor) MarshalJSON() ([]byte, error) { // we need custom marshalling to only include some parts of the metrics type QueryExecutor struct { @@ -248,6 +273,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 c2811f45c..b059cf95d 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 file and any error encountered. 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 available 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 the specified test name from local storage. +// It returns an error if the report cannot be loaded, enabling users to access historical test data 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 } +// MustAllDirectResults retrieves all direct 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 MustAllDirectResults(sr *StandardReport) map[string]float64 { results, err := ResultsAs(0.0, sr.QueryExecutors, StandardQueryExecutor_Direct) if err != nil { @@ -77,6 +92,9 @@ func MustAllDirectResults(sr *StandardReport) map[string]float64 { 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 within the specified time range. +// It validates the time range and executes queries in parallel, returning any errors encountered during execution. func (b *StandardReport) FetchData(ctx context.Context) error { if b.TestStart.IsZero() || b.TestEnd.IsZero() { return errors.New("start and end times are not set") @@ -128,6 +148,9 @@ func (b *StandardReport) FetchData(ctx context.Context) error { return nil } +// IsComparable checks if the current report can be compared with another report. +// It validates the type of the other report and ensures that their basic data and query executors are comparable. +// This function is useful for verifying report consistency before performing further analysis. func (b *StandardReport) IsComparable(otherReport Reporter) error { if _, ok := otherReport.(*StandardReport); !ok { return fmt.Errorf("expected type %s, got %T", "*StandardReport", otherReport) @@ -160,30 +183,42 @@ type standardReportConfig struct { type StandardReportOption func(*standardReportConfig) +// WithStandardQueries sets the executor types for a standard report configuration. +// It allows users to specify which types of query executors to use, enabling customization +// of report generation based on their requirements. func WithStandardQueries(executorTypes ...StandardQueryExecutorType) StandardReportOption { return func(c *standardReportConfig) { c.executorTypes = executorTypes } } +// WithGenerators sets the generators for the standard report configuration. +// It allows users to specify custom generator instances to be included in the report. func WithGenerators(generators ...*wasp.Generator) StandardReportOption { return func(c *standardReportConfig) { c.generators = generators } } +// WithPrometheusConfig sets the Prometheus configuration for the standard report. +// It returns a StandardReportOption that can be used to customize report generation. func WithPrometheusConfig(prometheusConfig *PrometheusConfig) StandardReportOption { return func(c *standardReportConfig) { c.prometheusConfig = prometheusConfig } } +// WithReportDirectory sets the directory for storing report files. +// This function is useful for configuring the output location of reports +// generated by the standard reporting system. func WithReportDirectory(reportDirectory string) StandardReportOption { return func(c *standardReportConfig) { c.reportDirectory = reportDirectory } } +// WithQueryExecutors sets the query executors for a standard report configuration. +// It allows customization of how queries are executed, enhancing report generation flexibility. func WithQueryExecutors(queryExecutors ...QueryExecutor) StandardReportOption { return func(c *standardReportConfig) { c.queryExecutors = queryExecutors @@ -225,6 +260,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 validated. +// This function is essential for generating reports that require specific data sources and execution strategies. func NewStandardReport(commitOrTag string, opts ...StandardReportOption) (*StandardReport, error) { config := standardReportConfig{} for _, opt := range opts { @@ -328,6 +366,9 @@ func generatorHasLabels(g *wasp.Generator) bool { return g.Cfg.Labels["branch"] != "" && g.Cfg.Labels["commit"] != "" } +// UnmarshalJSON decodes JSON data into a StandardReport struct. +// It populates the QueryExecutors and ResourceFetchers fields, +// allowing for dynamic handling of JSON structures in reports. func (s *StandardReport) UnmarshalJSON(data []byte) error { // helper struct with QueryExecutors as json.RawMessage type Alias StandardReport @@ -464,6 +505,9 @@ func convertQueryResults(results map[string]interface{}) (map[string]interface{} return converted, nil } +// FetchNewStandardReportAndLoadLatestPrevious creates a new standard report for a given commit or tag, +// loads the latest previous report, and checks their comparability. +// It returns the new report, the previous report, and any error encountered during the process. func FetchNewStandardReportAndLoadLatestPrevious(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 bafcfa72b..36b1cdbc3 100644 --- a/wasp/benchspy/storage.go +++ b/wasp/benchspy/storage.go @@ -30,6 +30,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. +// Returns the absolute path of the stored report or an error if the operation fails. func (l *LocalStorage) Store(testName, commitOrTag string, report interface{}) (string, error) { l.defaultDirectoryIfEmpty() asJson, err := json.MarshalIndent(report, "", " ") @@ -65,6 +68,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, enabling users to access stored test results. func (l *LocalStorage) Load(testName, commitOrTag string, report interface{}) error { l.defaultDirectoryIfEmpty() if testName == "" {