Skip to content
Open
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
4 changes: 3 additions & 1 deletion .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ jobs:
exit 1
fi

if [[ "$PR_TITLE" =~ ^[a-z]+(\([^)]*\))?!: ]]; then
breaking_pattern='^[a-z]+(\([^)]*\))?!:'

if [[ "$PR_TITLE" =~ $breaking_pattern ]]; then
echo "::warning::This pull request will trigger a major release after merge."
{
echo "## Major release"
Expand Down
89 changes: 54 additions & 35 deletions pkg/rules/aio.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,58 @@ func computeAIO(in *input.Input, cfg *category.ExportCfg) (*category.ExportCfg,
// Set default values already from NewStorageCfg
// Adjust io_workers based on profile and CPU cores

// Factor per profile
var factor float64
workerCalculation := calculateIOWorkers(in.TotalCPU, in.Profile, in.DiskType)
cfg.Storage.IOWorkers = workerCalculation.Value

// Tune io_max_combine_limit and io_max_concurrency based on profile
// Values assume 8KB pages for limits (16 = 128KB, 128 = 1MB)
switch in.Profile {
case profile.DW:
// Data Warehouse benefits from larger I/O chunks and higher concurrency
cfg.Storage.IOMaxCombineLimit = 128 // 1MB
cfg.Storage.IOMaxConcurrency = 256
case profile.OLTP:
// OLTP benefits from concurrency but keep chunks standard
cfg.Storage.IOMaxConcurrency = 128
}

// Optionally set io_method based on OS and profile
// For now keep default "worker"
// If Linux and profile is DW or OLTP, consider io_uring
// but we need to be careful about container environments
// cfg.Storage.IOMethod = "worker"

return cfg, nil
}

type ioWorkerCalculation struct {
Value int
InitialValue int
HDDAdjusted bool
MinimumApplied bool
LogicalCPUCapApplied bool
}

func calculateIOWorkers(totalCPU int, workload profile.Profile, diskType string) ioWorkerCalculation {
calculation := ioWorkerCalculation{
InitialValue: int(math.Ceil(float64(totalCPU) * ioWorkerFactor(workload, diskType))),
HDDAdjusted: diskType == "HDD",
}
calculation.Value = calculation.InitialValue
if calculation.Value < 2 {
calculation.Value = 2
calculation.MinimumApplied = true
}
if calculation.Value > totalCPU {
calculation.Value = totalCPU
calculation.LogicalCPUCapApplied = true
}
return calculation
}

func ioWorkerFactor(workload profile.Profile, diskType string) float64 {
var factor float64
switch workload {
case profile.Desktop:
factor = 0.1
case profile.Web:
Expand All @@ -31,42 +80,12 @@ func computeAIO(in *input.Input, cfg *category.ExportCfg) (*category.ExportCfg,

// Adjust factor based on disk type (optional)
// HDD may benefit from more workers, SSD from fewer
switch in.DiskType {
switch diskType {
case "HDD":
factor += 0.1
case "SSD", "SAN":
// keep factor as is
}

// Calculate io_workers
workers := int(math.Ceil(float64(in.TotalCPU) * factor))
// Ensure at least 2 workers, but keep default 3 as minimum
if workers < 2 {
workers = 2
}
// Max workers? Not needed, but can limit to total CPU
if workers > in.TotalCPU {
workers = in.TotalCPU
}
cfg.Storage.IOWorkers = workers

// Tune io_max_combine_limit and io_max_concurrency based on profile
// Values assume 8KB pages for limits (16 = 128KB, 128 = 1MB)
switch in.Profile {
case profile.DW:
// Data Warehouse benefits from larger I/O chunks and higher concurrency
cfg.Storage.IOMaxCombineLimit = 128 // 1MB
cfg.Storage.IOMaxConcurrency = 256
case profile.OLTP:
// OLTP benefits from concurrency but keep chunks standard
cfg.Storage.IOMaxConcurrency = 128
}

// Optionally set io_method based on OS and profile
// For now keep default "worker"
// If Linux and profile is DW or OLTP, consider io_uring
// but we need to be careful about container environments
// cfg.Storage.IOMethod = "worker"

return cfg, nil
}
return factor
}
97 changes: 86 additions & 11 deletions pkg/rules/compute.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,118 @@ package rules

import (
"fmt"
"reflect"
"strings"

"github.com/pgconfig/api/pkg/category"
"github.com/pgconfig/api/pkg/input"
)

type rule func(*input.Input, *category.ExportCfg) (*category.ExportCfg, error)

var allRules = []rule{
computeArch,
computeOS,
computeProfile,
computeStorage,
computeAIO,
type ruleID string

const (
ruleArchitecture ruleID = "architecture"
ruleOperatingSystem ruleID = "operating system"
ruleProfile ruleID = "profile"
ruleStorage ruleID = "storage"
ruleAsynchronousIO ruleID = "asynchronous I/O"
rulePostgreSQLVersion ruleID = "PostgreSQL Version"
)

type namedRule struct {
id ruleID
apply rule
}

type ruleAdjustment struct {
rule ruleID
before string
after string
}

type parameterSnapshot struct {
raw any
formatted string
}

var allRules = []namedRule{
{id: ruleArchitecture, apply: computeArch},
{id: ruleOperatingSystem, apply: computeOS},
{id: ruleProfile, apply: computeProfile},
{id: ruleStorage, apply: computeStorage},
{id: ruleAsynchronousIO, apply: computeAIO},

// computeVersion can remove values deppending on the version
// to be sure that it will not break other rules, leave it at
// the end.
computeVersion,
{id: rulePostgreSQLVersion, apply: computeVersion},
}

// Compute evaluate all parameters
func Compute(in input.Input) (*category.ExportCfg, error) {
out, _, err := computeWithAdjustments(in)
return out, err
}

// computeWithAdjustments is the single calculation pipeline shared by legacy
// consumers and the provenance-aware tuning operation.
func computeWithAdjustments(in input.Input) (*category.ExportCfg, map[string][]ruleAdjustment, error) {
var (
out *category.ExportCfg
err error
)

out = category.NewExportCfg(in)
adjustments := make(map[string][]ruleAdjustment)

for _, rule := range allRules {
for _, currentRule := range allRules {
before := parameterValues(out, in.PostgresVersion)

out, err = rule(&in, out)
out, err = currentRule.apply(&in, out)

if err != nil {
return nil, fmt.Errorf("could not process rule: %w", err)
return nil, nil, fmt.Errorf("could not process rule: %w", err)
}
after := parameterValues(out, in.PostgresVersion)
for name, afterValue := range after {
beforeValue, existed := before[name]
if existed && !reflect.DeepEqual(beforeValue.raw, afterValue.raw) {
adjustments[name] = append(adjustments[name], ruleAdjustment{
rule: currentRule.id, before: beforeValue.formatted, after: afterValue.formatted,
})
}
}
}

return out, adjustments, nil
}

func parameterValues(cfg *category.ExportCfg, postgresVersion float32) map[string]parameterSnapshot {
formattedValues := make(map[string]string)
for _, group := range cfg.ToSlice(postgresVersion, false, "") {
for _, parameter := range group.Parameters {
formattedValues[parameter.Name] = parameter.Value
}
}

return out, nil
values := make(map[string]parameterSnapshot)
categories := reflect.ValueOf(cfg).Elem()
for categoryIndex := 0; categoryIndex < categories.NumField(); categoryIndex++ {
categoryValue := categories.Field(categoryIndex)
if categoryValue.IsNil() {
continue
}
parameters := categoryValue.Elem()
parameterType := parameters.Type()
for parameterIndex := 0; parameterIndex < parameters.NumField(); parameterIndex++ {
name := strings.Split(parameterType.Field(parameterIndex).Tag.Get("json"), ",")[0]
values[name] = parameterSnapshot{
raw: parameters.Field(parameterIndex).Interface(),
formatted: formattedValues[name],
}
}
}
return values
}
Loading
Loading