From 702db21ed19904f8d5824852e850f87711aa8af4 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Tue, 8 Sep 2026 14:23:16 -0400 Subject: [PATCH 1/4] RuleGroup: add unmarshal and streaming benchmark Baseline benchmark go1.26.6 test ./api/prometheus/v1 -bench RuleGroup -benchmem -run ^$ -count=6 goos: darwin goarch: arm64 pkg: github.com/prometheus/client_golang/api/prometheus/v1 cpu: Apple M4 Pro BenchmarkRuleGroup/streaming-14 1059 945867 ns/op 1324909 B/op 27396 allocs/op BenchmarkRuleGroup/streaming-14 1258 946632 ns/op 1324947 B/op 27396 allocs/op BenchmarkRuleGroup/streaming-14 1298 936014 ns/op 1324919 B/op 27396 allocs/op BenchmarkRuleGroup/streaming-14 1275 942051 ns/op 1324939 B/op 27396 allocs/op BenchmarkRuleGroup/streaming-14 1280 949613 ns/op 1324919 B/op 27396 allocs/op BenchmarkRuleGroup/streaming-14 1272 943165 ns/op 1324928 B/op 27396 allocs/op BenchmarkRuleGroup/unmarshal-14 1358 884021 ns/op 1021988 B/op 27314 allocs/op BenchmarkRuleGroup/unmarshal-14 1347 885628 ns/op 1021997 B/op 27314 allocs/op BenchmarkRuleGroup/unmarshal-14 1353 892039 ns/op 1021991 B/op 27314 allocs/op BenchmarkRuleGroup/unmarshal-14 1330 886184 ns/op 1021980 B/op 27314 allocs/op BenchmarkRuleGroup/unmarshal-14 1359 884447 ns/op 1021998 B/op 27314 allocs/op BenchmarkRuleGroup/unmarshal-14 1350 890479 ns/op 1022020 B/op 27314 allocs/op PASS ok github.com/prometheus/client_golang/api/prometheus/v1 15.595s Signed-off-by: Jordan Liggitt --- api/prometheus/v1/api_bench_test.go | 74 +++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/api/prometheus/v1/api_bench_test.go b/api/prometheus/v1/api_bench_test.go index 97f502e56..2794ffbc0 100644 --- a/api/prometheus/v1/api_bench_test.go +++ b/api/prometheus/v1/api_bench_test.go @@ -13,8 +13,10 @@ package v1 import ( + "bytes" "encoding/json" "strconv" + "strings" "testing" "time" @@ -197,3 +199,75 @@ func BenchmarkSamplesJsonSerialization(b *testing.B) { }) } } + +func BenchmarkRuleGroup(b *testing.B) { + alertingRuleJSON, err := json.Marshal(struct { + Type RuleType `json:"type"` + AlertingRule `json:""` + }{ + Type: RuleTypeAlerting, + AlertingRule: AlertingRule{ + Name: "HighRequestLatency", + Query: "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5", + Duration: 600, + Labels: model.LabelSet{"severity": "page"}, + Annotations: model.LabelSet{"summary": "High request latency"}, + Alerts: []*Alert{{ + ActiveAt: time.Now().UTC(), + Annotations: model.LabelSet{"summary": "High request latency"}, + Labels: model.LabelSet{"alertname": "HighRequestLatency", "severity": "page"}, + State: AlertStateFiring, + Value: "1e+00", + }}, + Health: RuleHealthGood, + LastError: "Unknown", + EvaluationTime: 1, + LastEvaluation: time.Now().Round(time.Millisecond).UTC(), + State: "state", + }, + }) + if err != nil { + b.Fatal(err) + } + b.Log("alerting:", string(alertingRuleJSON)) + + recordingRuleJSON, err := json.Marshal(struct { + Type RuleType `json:"type"` + RecordingRule `json:""` + }{ + Type: RuleTypeRecording, + RecordingRule: RecordingRule{ + Name: "job:http_inprogress_requests:sum", + Query: "sum(http_inprogress_requests) by (job)", + Labels: model.LabelSet{"severity": "page"}, + Health: RuleHealthGood, + LastError: "Unknown", + EvaluationTime: 1, + LastEvaluation: time.Now().Round(time.Millisecond).UTC(), + }, + }) + if err != nil { + b.Fatal(err) + } + b.Log("recording:", string(recordingRuleJSON)) + + data := []byte(`{ +"name":"myname","file":"myfile","interval":0.0000005,"rules":[` + + string(alertingRuleJSON) + strings.Repeat(","+string(alertingRuleJSON), 100) + strings.Repeat(","+string(recordingRuleJSON), 100) + + `]}`) + + b.Run("streaming", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if err := jsoniter.NewDecoder(bytes.NewReader(data)).Decode(&RuleGroup{}); err != nil { + b.Fatal(err) + } + } + }) + b.Run("unmarshal", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if err := jsoniter.Unmarshal(data, &RuleGroup{}); err != nil { + b.Fatal(err) + } + } + }) +} From 15f21fcc84d4eb2f86005c9f55f8fd9ad2678176 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Tue, 8 Sep 2026 14:48:48 -0400 Subject: [PATCH 2/4] RuleGroup: Use stdlib json to unmarshal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmark delta on go1.26.6 goos: darwin goarch: arm64 pkg: github.com/prometheus/client_golang/api/prometheus/v1 cpu: Apple M4 Pro │ 1-jsoniter.txt │ 2-gojson.txt │ │ sec/op │ sec/op vs base │ RuleGroup/streaming-14 944.5µ ± 1% 2083.1µ ± 1% +120.54% (p=0.002 n=6) RuleGroup/unmarshal-14 885.9µ ± 1% 2143.6µ ± 2% +141.97% (p=0.002 n=6) geomean 914.7µ 2.113m +131.01% │ 1-jsoniter.txt │ 2-gojson.txt │ │ B/op │ B/op vs base │ RuleGroup/streaming-14 1293.9Ki ± 0% 962.7Ki ± 0% -25.60% (p=0.002 n=6) RuleGroup/unmarshal-14 998.0Ki ± 0% 708.2Ki ± 0% -29.05% (p=0.002 n=6) geomean 1.110Mi 825.7Ki -27.34% │ 1-jsoniter.txt │ 2-gojson.txt │ │ allocs/op │ allocs/op vs base │ RuleGroup/streaming-14 27.40k ± 0% 11.92k ± 0% -56.49% (p=0.002 n=6) RuleGroup/unmarshal-14 27.31k ± 0% 11.91k ± 0% -56.41% (p=0.002 n=6) geomean 27.35k 11.91k -56.45% Signed-off-by: Jordan Liggitt --- api/prometheus/v1/api.go | 25 +++++++++++++------------ api/prometheus/v1/api_bench_test.go | 4 ++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/api/prometheus/v1/api.go b/api/prometheus/v1/api.go index 62f2ed7dc..5ec5bbb57 100644 --- a/api/prometheus/v1/api.go +++ b/api/prometheus/v1/api.go @@ -17,6 +17,7 @@ package v1 import ( "context" + gojson "encoding/json" "errors" "fmt" "math" @@ -745,13 +746,13 @@ type Stat struct { func (rg *RuleGroup) UnmarshalJSON(b []byte) error { v := struct { - Name string `json:"name"` - File string `json:"file"` - Interval float64 `json:"interval"` - Rules []json.RawMessage `json:"rules"` + Name string `json:"name"` + File string `json:"file"` + Interval float64 `json:"interval"` + Rules []gojson.RawMessage `json:"rules"` }{} - if err := json.Unmarshal(b, &v); err != nil { + if err := gojson.Unmarshal(b, &v); err != nil { return err } @@ -761,12 +762,12 @@ func (rg *RuleGroup) UnmarshalJSON(b []byte) error { for _, rule := range v.Rules { alertingRule := AlertingRule{} - if err := json.Unmarshal(rule, &alertingRule); err == nil { + if err := gojson.Unmarshal(rule, &alertingRule); err == nil { rg.Rules = append(rg.Rules, alertingRule) continue } recordingRule := RecordingRule{} - if err := json.Unmarshal(rule, &recordingRule); err == nil { + if err := gojson.Unmarshal(rule, &recordingRule); err == nil { rg.Rules = append(rg.Rules, recordingRule) continue } @@ -780,7 +781,7 @@ func (r *AlertingRule) UnmarshalJSON(b []byte) error { v := struct { Type string `json:"type"` }{} - if err := json.Unmarshal(b, &v); err != nil { + if err := gojson.Unmarshal(b, &v); err != nil { return err } if v.Type == "" { @@ -803,7 +804,7 @@ func (r *AlertingRule) UnmarshalJSON(b []byte) error { LastEvaluation time.Time `json:"lastEvaluation"` State string `json:"state"` }{} - if err := json.Unmarshal(b, &rule); err != nil { + if err := gojson.Unmarshal(b, &rule); err != nil { return err } r.Health = rule.Health @@ -825,7 +826,7 @@ func (r *RecordingRule) UnmarshalJSON(b []byte) error { v := struct { Type string `json:"type"` }{} - if err := json.Unmarshal(b, &v); err != nil { + if err := gojson.Unmarshal(b, &v); err != nil { return err } if v.Type == "" { @@ -844,7 +845,7 @@ func (r *RecordingRule) UnmarshalJSON(b []byte) error { EvaluationTime float64 `json:"evaluationTime"` LastEvaluation time.Time `json:"lastEvaluation"` }{} - if err := json.Unmarshal(b, &rule); err != nil { + if err := gojson.Unmarshal(b, &rule); err != nil { return err } r.Health = rule.Health @@ -1296,7 +1297,7 @@ func (h *httpAPI) Rules(ctx context.Context, matches []string) (RulesResult, err } var res RulesResult - err = json.Unmarshal(body, &res) + err = gojson.Unmarshal(body, &res) return res, err } diff --git a/api/prometheus/v1/api_bench_test.go b/api/prometheus/v1/api_bench_test.go index 2794ffbc0..1fe56fea3 100644 --- a/api/prometheus/v1/api_bench_test.go +++ b/api/prometheus/v1/api_bench_test.go @@ -258,14 +258,14 @@ func BenchmarkRuleGroup(b *testing.B) { b.Run("streaming", func(b *testing.B) { for i := 0; i < b.N; i++ { - if err := jsoniter.NewDecoder(bytes.NewReader(data)).Decode(&RuleGroup{}); err != nil { + if err := json.NewDecoder(bytes.NewReader(data)).Decode(&RuleGroup{}); err != nil { b.Fatal(err) } } }) b.Run("unmarshal", func(b *testing.B) { for i := 0; i < b.N; i++ { - if err := jsoniter.Unmarshal(data, &RuleGroup{}); err != nil { + if err := json.Unmarshal(data, &RuleGroup{}); err != nil { b.Fatal(err) } } From 93eab07290712428b9525e131da2120c3ac78653 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Tue, 8 Sep 2026 14:50:06 -0400 Subject: [PATCH 3/4] RuleGroup: avoid repeatedly unmarshaling type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit goos: darwin goarch: arm64 pkg: github.com/prometheus/client_golang/api/prometheus/v1 cpu: Apple M4 Pro │ 2-gojson.txt │ 3-typefield.txt │ │ sec/op │ sec/op vs base │ RuleGroup/streaming-14 2.083m ± 1% 1.606m ± 0% -22.93% (p=0.002 n=6) RuleGroup/unmarshal-14 2.144m ± 2% 1.604m ± 3% -25.18% (p=0.002 n=6) geomean 2.113m 1.605m -24.06% │ 2-gojson.txt │ 3-typefield.txt │ │ B/op │ B/op vs base │ RuleGroup/streaming-14 962.7Ki ± 0% 832.7Ki ± 0% -13.50% (p=0.002 n=6) RuleGroup/unmarshal-14 708.2Ki ± 0% 578.3Ki ± 0% -18.33% (p=0.002 n=6) geomean 825.7Ki 693.9Ki -15.95% │ 2-gojson.txt │ 3-typefield.txt │ │ allocs/op │ allocs/op vs base │ RuleGroup/streaming-14 11.920k ± 0% 9.614k ± 0% -19.35% (p=0.002 n=6) RuleGroup/unmarshal-14 11.907k ± 0% 9.601k ± 0% -19.37% (p=0.002 n=6) geomean 11.91k 9.607k -19.36% Signed-off-by: Jordan Liggitt --- api/prometheus/v1/api.go | 64 ++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/api/prometheus/v1/api.go b/api/prometheus/v1/api.go index 5ec5bbb57..2d0eaf847 100644 --- a/api/prometheus/v1/api.go +++ b/api/prometheus/v1/api.go @@ -761,36 +761,57 @@ func (rg *RuleGroup) UnmarshalJSON(b []byte) error { rg.Interval = v.Interval for _, rule := range v.Rules { - alertingRule := AlertingRule{} - if err := gojson.Unmarshal(rule, &alertingRule); err == nil { - rg.Rules = append(rg.Rules, alertingRule) - continue + ruleType, err := unmarshalRuleType(rule) + if err != nil { + return err } - recordingRule := RecordingRule{} - if err := gojson.Unmarshal(rule, &recordingRule); err == nil { + switch ruleType { + case RuleTypeAlerting: + alertingRule := AlertingRule{} + if err := alertingRule.unmarshalTypeCheckedJSON(rule); err != nil { + return err + } + rg.Rules = append(rg.Rules, alertingRule) + case RuleTypeRecording: + recordingRule := RecordingRule{} + if err := recordingRule.unmarshalTypeCheckedJSON(rule); err != nil { + return err + } rg.Rules = append(rg.Rules, recordingRule) - continue + default: + return errors.New("failed to decode JSON into an alerting or recording rule") } - return errors.New("failed to decode JSON into an alerting or recording rule") } return nil } -func (r *AlertingRule) UnmarshalJSON(b []byte) error { +func unmarshalRuleType(b []byte) (RuleType, error) { v := struct { Type string `json:"type"` }{} if err := gojson.Unmarshal(b, &v); err != nil { - return err + return RuleType(""), err } if v.Type == "" { - return errors.New("type field not present in rule") + return RuleType(""), errors.New("type field not present in rule") } - if v.Type != string(RuleTypeAlerting) { - return fmt.Errorf("expected rule of type %s but got %s", string(RuleTypeAlerting), v.Type) + return RuleType(v.Type), nil +} + +func (r *AlertingRule) UnmarshalJSON(b []byte) error { + ruleType, err := unmarshalRuleType(b) + if err != nil { + return err + } + if ruleType != RuleTypeAlerting { + return fmt.Errorf("expected rule of type %s but got %s", string(RuleTypeAlerting), ruleType) } + return r.unmarshalTypeCheckedJSON(b) +} +// unmarshalTypeCheckedJSON unmarshals json with the type field already verified to be RuleTypeAlerting +func (r *AlertingRule) unmarshalTypeCheckedJSON(b []byte) error { rule := struct { Name string `json:"name"` Query string `json:"query"` @@ -823,19 +844,18 @@ func (r *AlertingRule) UnmarshalJSON(b []byte) error { } func (r *RecordingRule) UnmarshalJSON(b []byte) error { - v := struct { - Type string `json:"type"` - }{} - if err := gojson.Unmarshal(b, &v); err != nil { + ruleType, err := unmarshalRuleType(b) + if err != nil { return err } - if v.Type == "" { - return errors.New("type field not present in rule") - } - if v.Type != string(RuleTypeRecording) { - return fmt.Errorf("expected rule of type %s but got %s", string(RuleTypeRecording), v.Type) + if ruleType != RuleTypeRecording { + return fmt.Errorf("expected rule of type %s but got %s", string(RuleTypeRecording), ruleType) } + return r.unmarshalTypeCheckedJSON(b) +} +// unmarshalTypeCheckedJSON unmarshals json with the type field already verified to be RuleTypeRecording +func (r *RecordingRule) unmarshalTypeCheckedJSON(b []byte) error { rule := struct { Name string `json:"name"` Query string `json:"query"` From ce3781c48f71b9b93dcfc4910024d6466b65efe9 Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Tue, 8 Sep 2026 14:53:30 -0400 Subject: [PATCH 4/4] RuleGroup: optimize internal decode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit goos: darwin goarch: arm64 pkg: github.com/prometheus/client_golang/api/prometheus/v1 cpu: Apple M4 Pro │ 3-typefield.txt │ 4-structcopy.txt │ │ sec/op │ sec/op vs base │ RuleGroup/streaming-14 1.606m ± 0% 1.552m ± 0% -3.35% (p=0.002 n=6) RuleGroup/unmarshal-14 1.604m ± 3% 1.522m ± 1% -5.13% (p=0.002 n=6) geomean 1.605m 1.537m -4.25% │ 3-typefield.txt │ 4-structcopy.txt │ │ B/op │ B/op vs base │ RuleGroup/streaming-14 832.7Ki ± 0% 832.7Ki ± 0% ~ (p=0.418 n=6) RuleGroup/unmarshal-14 578.3Ki ± 0% 578.3Ki ± 0% ~ (p=0.197 n=6) geomean 693.9Ki 693.9Ki -0.00% │ 3-typefield.txt │ 4-structcopy.txt │ │ allocs/op │ allocs/op vs base │ RuleGroup/streaming-14 9.614k ± 0% 9.614k ± 0% ~ (p=1.000 n=6) ¹ RuleGroup/unmarshal-14 9.601k ± 0% 9.601k ± 0% ~ (p=1.000 n=6) ¹ geomean 9.607k 9.607k +0.00% Signed-off-by: Jordan Liggitt --- api/prometheus/v1/api.go | 56 +++++----------------------------------- 1 file changed, 6 insertions(+), 50 deletions(-) diff --git a/api/prometheus/v1/api.go b/api/prometheus/v1/api.go index 2d0eaf847..d5e0980cf 100644 --- a/api/prometheus/v1/api.go +++ b/api/prometheus/v1/api.go @@ -810,37 +810,11 @@ func (r *AlertingRule) UnmarshalJSON(b []byte) error { return r.unmarshalTypeCheckedJSON(b) } +type alertingRuleInternal AlertingRule + // unmarshalTypeCheckedJSON unmarshals json with the type field already verified to be RuleTypeAlerting func (r *AlertingRule) unmarshalTypeCheckedJSON(b []byte) error { - rule := struct { - Name string `json:"name"` - Query string `json:"query"` - Duration float64 `json:"duration"` - Labels model.LabelSet `json:"labels"` - Annotations model.LabelSet `json:"annotations"` - Alerts []*Alert `json:"alerts"` - Health RuleHealth `json:"health"` - LastError string `json:"lastError,omitempty"` - EvaluationTime float64 `json:"evaluationTime"` - LastEvaluation time.Time `json:"lastEvaluation"` - State string `json:"state"` - }{} - if err := gojson.Unmarshal(b, &rule); err != nil { - return err - } - r.Health = rule.Health - r.Annotations = rule.Annotations - r.Name = rule.Name - r.Query = rule.Query - r.Alerts = rule.Alerts - r.Duration = rule.Duration - r.Labels = rule.Labels - r.LastError = rule.LastError - r.EvaluationTime = rule.EvaluationTime - r.LastEvaluation = rule.LastEvaluation - r.State = rule.State - - return nil + return gojson.Unmarshal(b, (*alertingRuleInternal)(r)) } func (r *RecordingRule) UnmarshalJSON(b []byte) error { @@ -854,29 +828,11 @@ func (r *RecordingRule) UnmarshalJSON(b []byte) error { return r.unmarshalTypeCheckedJSON(b) } +type recordingRuleInternal RecordingRule + // unmarshalTypeCheckedJSON unmarshals json with the type field already verified to be RuleTypeRecording func (r *RecordingRule) unmarshalTypeCheckedJSON(b []byte) error { - rule := struct { - Name string `json:"name"` - Query string `json:"query"` - Labels model.LabelSet `json:"labels,omitempty"` - Health RuleHealth `json:"health"` - LastError string `json:"lastError,omitempty"` - EvaluationTime float64 `json:"evaluationTime"` - LastEvaluation time.Time `json:"lastEvaluation"` - }{} - if err := gojson.Unmarshal(b, &rule); err != nil { - return err - } - r.Health = rule.Health - r.Labels = rule.Labels - r.Name = rule.Name - r.LastError = rule.LastError - r.Query = rule.Query - r.EvaluationTime = rule.EvaluationTime - r.LastEvaluation = rule.LastEvaluation - - return nil + return gojson.Unmarshal(b, (*recordingRuleInternal)(r)) } func (qr *queryResult) UnmarshalJSON(b []byte) error {