blob: 57329a770a9a81635c3f31e018a231e1b6e1dbb3 [file] [log] [blame]
// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package benchmarks
import (
"context"
"fmt"
"runtime"
"testing"
"time"
"github.com/klauspost/compress/zstd"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/luci/common/clock/testclock"
gerritpb "go.chromium.org/luci/common/proto/gerrit"
"go.chromium.org/luci/gae/impl/memory"
"go.chromium.org/luci/gae/service/datastore"
cfgpb "go.chromium.org/luci/cv/api/config/v2"
"go.chromium.org/luci/cv/internal/changelist"
"go.chromium.org/luci/cv/internal/common"
"go.chromium.org/luci/cv/internal/gerrit"
gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
"go.chromium.org/luci/cv/internal/gerrit/trigger"
"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
)
var epoch = testclock.TestRecentTimeUTC
// clidOffset emulates realistic datastore-generated CLIDs.
const clidOffset = 4683683202072576
func benchmarkRAMper1KObjects(b *testing.B, objectKind string, work func()) (before, after runtime.MemStats) {
runtime.GC()
runtime.ReadMemStats(&before)
b.ReportAllocs()
work()
runtime.ReadMemStats(&after)
obj1 := float64(after.HeapAlloc-before.HeapAlloc) / 1024 / 1024 / float64(b.N)
b.ReportMetric(obj1*1000, "MB-heap/1K-"+objectKind)
return
}
// BenchmarkLoadCLsUsage1K estimates how much RAM keeping 1K CL objects will take.
func BenchmarkLoadCLsUsage1K(b *testing.B) {
if b.N > 100000 {
b.Skip("Specify lower `-benchtime=XX`")
}
ctx := putCLs(b.N)
cls := initCLObjects(b.N)
benchmarkRAMper1KObjects(b, "CL", func() {
if err := datastore.Get(ctx, cls); err != nil {
panic(err)
}
})
}
// BenchmarkPMStatePCLRAMUsage1K estimates how much RAM keeping 1K PM.PState.PCL
// objects will take.
func BenchmarkPMStatePCLRAMUsage1K(b *testing.B) {
if b.N > 100000 {
b.Skip("Specify lower `-benchtime=XX`")
}
cis := makeCIs(b.N)
cls := make([]*changelist.CL, b.N)
for i, ci := range cis {
cls[i] = makeCL(ci)
}
out := make([]*prjpb.PCL, b.N)
benchmarkRAMper1KObjects(b, "prjpb.PCL", func() {
for i, cl := range cls {
out[i] = makePCL(cl)
}
})
bs, _ := proto.Marshal(&prjpb.PState{Pcls: out})
v := float64(len(bs)) / 1024 / 1024 / float64(b.N)
b.ReportMetric(v*1000, "MB-pb/1K-"+"prjpb.PCL")
var z zstd.Encoder
compressed := z.EncodeAll(bs, nil)
v = float64(len(compressed)) / 1024 / 1024 / float64(b.N)
b.ReportMetric(v*1000, "MB-pb-zstd/1K-"+"prjpb.PCL")
}
func putCLs(N int) context.Context {
ctx := memory.Use(context.Background())
cis := makeCIs(N)
for _, ci := range cis {
cl := makeCL(ci)
if err := datastore.Put(ctx, cl); err != nil {
panic(err)
}
}
return ctx
}
func initCLObjects(N int) []*changelist.CL {
out := make([]*changelist.CL, N)
for i := range out {
out[i] = &changelist.CL{ID: common.CLID(i + 1 + clidOffset)}
}
return out
}
func makePCL(cl *changelist.CL) *prjpb.PCL {
// Copy deps for realism.
deps := make([]*changelist.Dep, len(cl.Snapshot.GetDeps()))
copy(deps, cl.Snapshot.GetDeps())
tr := trigger.Find(cl.Snapshot.GetGerrit().GetInfo(), &cfgpb.ConfigGroup{})
// Copy trigger email string for realism of allocations.
tr.Email = tr.Email + " "
return &prjpb.PCL{
Clid: int64(cl.ID),
Eversion: int64(cl.EVersion),
Status: prjpb.PCL_OK,
Submitted: false,
ConfigGroupIndexes: []int32{0},
Deps: deps,
Trigger: tr,
}
}
func makeCL(ci *gerritpb.ChangeInfo) *changelist.CL {
now := epoch.Add(time.Hour * 1000)
mps, ps, err := gerrit.EquivalentPatchsetRange(ci)
if err != nil {
panic(err)
}
num := clidOffset + ci.GetNumber()
// The deps aren't supposed to make sense, only to resemble what actual
// CL stores.
return &changelist.CL{
ID: common.CLID(num),
ExternalID: changelist.MustGobID("host", ci.GetNumber()),
Snapshot: &changelist.Snapshot{
Kind: &changelist.Snapshot_Gerrit{Gerrit: &changelist.Gerrit{
Info: ci,
Files: []string{"a.cpp"},
Host: "host",
GitDeps: []*changelist.GerritGitDep{
{Change: num - 1, Immediate: true},
{Change: num - 2, Immediate: false},
},
SoftDeps: []*changelist.GerritSoftDep{
{Change: num + 1, Host: "host-2"},
},
}},
Deps: []*changelist.Dep{
{Clid: num - 2, Kind: changelist.DepKind_SOFT},
{Clid: num - 1, Kind: changelist.DepKind_HARD},
{Clid: num + 1, Kind: changelist.DepKind_SOFT}, // num:host-2
},
ExternalUpdateTime: ci.GetUpdated(),
LuciProject: "test",
MinEquivalentPatchset: int32(mps),
Patchset: int32(ps),
},
ApplicableConfig: &changelist.ApplicableConfig{
Projects: []*changelist.ApplicableConfig_Project{
{Name: "test", ConfigGroupIds: []string{"main"}},
},
},
DependentMeta: &changelist.DependentMeta{
ByProject: map[string]*changelist.DependentMeta_Meta{
"test-2": {
NoAccess: true,
UpdateTime: timestamppb.New(now),
},
},
},
EVersion: 1,
UpdateTime: now,
IncompleteRuns: common.MakeRunIDs("test/run-001"),
}
}
func makeCIs(N int) []*gerritpb.ChangeInfo {
cis := make([]*gerritpb.ChangeInfo, N)
for i := range cis {
cis[i] = makeCI(i)
changelist.RemoveUnusedGerritInfo(cis[i])
}
return cis
}
func makeCI(i int) *gerritpb.ChangeInfo {
di := time.Duration(i)
u1 := fmt.Sprintf("user-%d", 2*i+1)
u2 := fmt.Sprintf("user-%d", 2*i+2)
return gf.CI(
i+1,
gf.Owner("owner-3"),
gf.PS(5),
gf.AllRevs(),
gf.CQ(+1, epoch.Add(di*time.Minute), u1),
gf.Vote("Code-Review", +2, epoch.Add(di*time.Minute), u2),
gf.Messages(
&gerritpb.ChangeMessageInfo{
Author: gf.U(u1),
Date: timestamppb.New(epoch.Add(di * time.Minute)),
Message: "this is message",
Id: "sha-something-1",
},
&gerritpb.ChangeMessageInfo{
Author: gf.U(u2),
Date: timestamppb.New(epoch.Add(di * time.Minute)),
Message: "this is a better message",
Id: "sha-something-2",
},
),
)
}