mirror of
https://github.com/coder/coder.git
synced 2026-09-01 14:53:15 +08:00
62f4afbb60
`convertWorkspaceBuild` rebuilt seven maps from the caller's global slices on every call and rescanned the provisioner daemon rows to filter by job ID. `convertWorkspaceBuilds` calls it once per build with identical slices, so map construction cost `O(builds x rows)` where `O(rows)` suffices — quadratic in the number of workspaces on `GET /api/v2/workspaces`. The maps move into a `workspaceBuildIndex` built once per batch, keyed exactly as before and now including daemons by job ID. `convertWorkspaceBuild` takes the index instead of eight slices. Its parent already hoisted `workspaceByID`, `jobByID`, and `templateVersionByID` out of the same loop; this makes the rest consistent. Agents are sorted while the index is built, so a resource read by several builds is sorted once rather than once per build. Same comparator over the same rows, so the order is unchanged; `TestConvertWorkspaceBuildsAgentOrder` covers it. `BenchmarkConvertWorkspaceBuilds`, 5 resources x 2 agents x 4 apps per build: | builds | ns/op | B/op | allocs/op | | --- | --- | --- | --- | | 1 | 48.3k -> 48.1k | 121k -> 125k | 342 -> 359 | | 25 | 12.7M -> 1.34M | 38.0MB -> 3.2MB | 69,621 -> 8,347 | | 100 | 186M -> 5.67M | 596MB -> 13.0MB | 1,024,941 -> 33,080 | Single-build conversion is a wash (one extra struct allocation); the quadratic term is gone. Addresses the map-allocation half of PLAT-386 / #27205. Bounding the page size is separate (#28040) and does not remove this cost: at 100 workspaces per page it is still 100 passes over every resource, agent, app, script, log source, status, and daemon row in the page. --- Created with Coder Agents on behalf of @jscottmiller.
300 lines
9.1 KiB
Go
300 lines
9.1 KiB
Go
package coderd
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/tailnet"
|
|
)
|
|
|
|
// buildFixture holds one batch of rows shaped like the output of
|
|
// workspaceBuildsData: a set of builds plus the flat resource, agent, app,
|
|
// script, log source, status, and daemon rows for all of them.
|
|
type buildFixture struct {
|
|
builds []database.WorkspaceBuild
|
|
workspaces []database.Workspace
|
|
jobs []database.GetProvisionerJobsByIDsWithQueuePositionRow
|
|
templateVersions []database.TemplateVersion
|
|
resources []database.WorkspaceResource
|
|
metadata []database.WorkspaceResourceMetadatum
|
|
agents []database.WorkspaceAgent
|
|
apps []database.WorkspaceApp
|
|
statuses []database.WorkspaceAppStatus
|
|
scripts []database.GetWorkspaceAgentScriptsByAgentIDsRow
|
|
logSources []database.WorkspaceAgentLogSource
|
|
daemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
|
|
}
|
|
|
|
func newBuildFixture(builds, resourcesPerBuild int, agentsPerResource int32, appsPerAgent int) buildFixture {
|
|
now := time.Date(2026, 8, 11, 0, 0, 0, 0, time.UTC)
|
|
f := buildFixture{}
|
|
for b := range builds {
|
|
workspaceID := uuid.New()
|
|
jobID := uuid.New()
|
|
templateVersionID := uuid.New()
|
|
|
|
f.workspaces = append(f.workspaces, database.Workspace{
|
|
ID: workspaceID,
|
|
OwnerID: uuid.New(),
|
|
OwnerUsername: fmt.Sprintf("owner-%d", b),
|
|
Name: fmt.Sprintf("workspace-%d", b),
|
|
})
|
|
f.jobs = append(f.jobs, database.GetProvisionerJobsByIDsWithQueuePositionRow{
|
|
ProvisionerJob: database.ProvisionerJob{
|
|
ID: jobID,
|
|
CreatedAt: now,
|
|
StartedAt: sql.NullTime{Time: now, Valid: true},
|
|
},
|
|
})
|
|
f.templateVersions = append(f.templateVersions, database.TemplateVersion{
|
|
ID: templateVersionID,
|
|
Name: fmt.Sprintf("version-%d", b),
|
|
})
|
|
f.builds = append(f.builds, database.WorkspaceBuild{
|
|
ID: uuid.New(),
|
|
WorkspaceID: workspaceID,
|
|
JobID: jobID,
|
|
TemplateVersionID: templateVersionID,
|
|
BuildNumber: 1,
|
|
Transition: database.WorkspaceTransitionStart,
|
|
})
|
|
f.daemons = append(f.daemons, database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{
|
|
JobID: jobID,
|
|
ProvisionerDaemon: database.ProvisionerDaemon{
|
|
ID: uuid.New(),
|
|
Name: fmt.Sprintf("daemon-%d", b),
|
|
LastSeenAt: sql.NullTime{Time: now, Valid: true},
|
|
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
|
|
},
|
|
})
|
|
|
|
for r := range resourcesPerBuild {
|
|
resourceID := uuid.New()
|
|
f.resources = append(f.resources, database.WorkspaceResource{
|
|
ID: resourceID,
|
|
JobID: jobID,
|
|
Transition: database.WorkspaceTransitionStart,
|
|
Type: "example",
|
|
Name: fmt.Sprintf("resource-%d-%d", b, r),
|
|
})
|
|
f.metadata = append(f.metadata, database.WorkspaceResourceMetadatum{
|
|
WorkspaceResourceID: resourceID,
|
|
Key: "key",
|
|
Value: sql.NullString{String: "value", Valid: true},
|
|
})
|
|
|
|
for a := range agentsPerResource {
|
|
agentID := uuid.New()
|
|
f.agents = append(f.agents, database.WorkspaceAgent{
|
|
ID: agentID,
|
|
ResourceID: resourceID,
|
|
Name: fmt.Sprintf("agent-%d-%d-%d", b, r, a),
|
|
DisplayOrder: agentsPerResource - a,
|
|
APIKeyScope: database.AgentKeyScopeEnumAll,
|
|
})
|
|
f.scripts = append(f.scripts, database.GetWorkspaceAgentScriptsByAgentIDsRow{
|
|
WorkspaceAgentID: agentID,
|
|
Script: "echo hello",
|
|
})
|
|
f.logSources = append(f.logSources, database.WorkspaceAgentLogSource{
|
|
WorkspaceAgentID: agentID,
|
|
DisplayName: "startup",
|
|
})
|
|
|
|
for p := range appsPerAgent {
|
|
appID := uuid.New()
|
|
f.apps = append(f.apps, database.WorkspaceApp{
|
|
ID: appID,
|
|
AgentID: agentID,
|
|
Slug: fmt.Sprintf("app-%d-%d-%d-%d", b, r, a, p),
|
|
DisplayName: "App",
|
|
Health: database.WorkspaceAppHealthHealthy,
|
|
SharingLevel: database.AppSharingLevelOwner,
|
|
OpenIn: database.WorkspaceAppOpenInSlimWindow,
|
|
})
|
|
f.statuses = append(f.statuses, database.WorkspaceAppStatus{
|
|
ID: uuid.New(),
|
|
AgentID: agentID,
|
|
AppID: appID,
|
|
State: database.WorkspaceAppStatusStateComplete,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return f
|
|
}
|
|
|
|
func newConvertAPI() *API {
|
|
api := &API{
|
|
Options: &Options{
|
|
AgentInactiveDisconnectTimeout: time.Minute,
|
|
DeploymentValues: &codersdk.DeploymentValues{},
|
|
},
|
|
}
|
|
coordinator := tailnet.NewCoordinator(slog.Logger{})
|
|
api.TailnetCoordinator.Store(&coordinator)
|
|
return api
|
|
}
|
|
|
|
func (f buildFixture) convert(api *API) ([]codersdk.WorkspaceBuild, error) {
|
|
return api.convertWorkspaceBuilds(
|
|
f.builds,
|
|
f.workspaces,
|
|
f.jobs,
|
|
f.resources,
|
|
f.metadata,
|
|
f.agents,
|
|
f.apps,
|
|
f.statuses,
|
|
f.scripts,
|
|
f.logSources,
|
|
f.templateVersions,
|
|
f.daemons,
|
|
)
|
|
}
|
|
|
|
// TestConvertWorkspaceBuildsAgentOrder covers the ordering that the shared
|
|
// index is responsible for: agents come back sorted by display order. The
|
|
// fixture assigns display order in reverse of agent name, so the expected
|
|
// result is names in descending order.
|
|
func TestConvertWorkspaceBuildsAgentOrder(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
api := newConvertAPI()
|
|
fixture := newBuildFixture(3, 2, 3, 1)
|
|
|
|
builds, err := fixture.convert(api)
|
|
require.NoError(t, err)
|
|
require.Len(t, builds, 3)
|
|
|
|
for _, build := range builds {
|
|
require.NotEmpty(t, build.Resources)
|
|
for _, resource := range build.Resources {
|
|
require.Len(t, resource.Agents, 3)
|
|
for i := 1; i < len(resource.Agents); i++ {
|
|
require.Greater(t, resource.Agents[i-1].Name, resource.Agents[i].Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestConvertWorkspaceBuildsAgentNameTiebreak covers the second half of the
|
|
// agent comparison: agents sharing a display order are ordered by name.
|
|
func TestConvertWorkspaceBuildsAgentNameTiebreak(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
api := newConvertAPI()
|
|
fixture := newBuildFixture(1, 1, 3, 1)
|
|
for i := range fixture.agents {
|
|
fixture.agents[i].DisplayOrder = 0
|
|
}
|
|
|
|
builds, err := fixture.convert(api)
|
|
require.NoError(t, err)
|
|
require.Len(t, builds, 1)
|
|
require.Len(t, builds[0].Resources, 1)
|
|
|
|
agents := builds[0].Resources[0].Agents
|
|
require.Len(t, agents, 3)
|
|
for i := 1; i < len(agents); i++ {
|
|
require.Less(t, agents[i-1].Name, agents[i].Name)
|
|
}
|
|
}
|
|
|
|
// TestConvertWorkspaceBuildsRowsPerBuild asserts that a build receives only the
|
|
// rows keyed to its own job and agents. The fixture encodes the build index in
|
|
// app slugs, so apps read from another build are detectable by slug.
|
|
func TestConvertWorkspaceBuildsRowsPerBuild(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
buildCount = 3
|
|
resourcesPerJob = 2
|
|
agentsPerRes = 2
|
|
appsPerAgent = 2
|
|
)
|
|
|
|
api := newConvertAPI()
|
|
fixture := newBuildFixture(buildCount, resourcesPerJob, agentsPerRes, appsPerAgent)
|
|
|
|
builds, err := fixture.convert(api)
|
|
require.NoError(t, err)
|
|
require.Len(t, builds, buildCount)
|
|
|
|
for b, build := range builds {
|
|
require.Len(t, build.Resources, resourcesPerJob)
|
|
|
|
for _, resource := range build.Resources {
|
|
require.Equal(t, fixture.jobs[b].ProvisionerJob.ID, resource.JobID)
|
|
require.Len(t, resource.Metadata, 1)
|
|
require.Len(t, resource.Agents, agentsPerRes)
|
|
|
|
for _, agent := range resource.Agents {
|
|
require.Len(t, agent.Scripts, 1)
|
|
require.Len(t, agent.LogSources, 1)
|
|
require.Len(t, agent.Apps, appsPerAgent)
|
|
|
|
for _, app := range agent.Apps {
|
|
require.True(t, strings.HasPrefix(app.Slug, fmt.Sprintf("app-%d-", b)), app.Slug)
|
|
require.Len(t, app.Statuses, 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestConvertWorkspaceBuildsMatchedProvisioners asserts that eligible daemons
|
|
// are matched to the build whose job they were fetched for. The last build in
|
|
// the batch has no daemon rows, so it reports zero counts while the others
|
|
// report one.
|
|
func TestConvertWorkspaceBuildsMatchedProvisioners(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
api := newConvertAPI()
|
|
fixture := newBuildFixture(3, 1, 1, 1)
|
|
fixture.daemons = fixture.daemons[:len(fixture.daemons)-1]
|
|
|
|
builds, err := fixture.convert(api)
|
|
require.NoError(t, err)
|
|
require.Len(t, builds, 3)
|
|
|
|
for _, build := range builds[:2] {
|
|
require.NotNil(t, build.MatchedProvisioners)
|
|
require.Equal(t, 1, build.MatchedProvisioners.Count)
|
|
require.Equal(t, 1, build.MatchedProvisioners.Available)
|
|
require.True(t, build.MatchedProvisioners.MostRecentlySeen.Valid)
|
|
}
|
|
|
|
last := builds[2]
|
|
require.NotNil(t, last.MatchedProvisioners)
|
|
require.Equal(t, 0, last.MatchedProvisioners.Count)
|
|
require.Equal(t, 0, last.MatchedProvisioners.Available)
|
|
require.False(t, last.MatchedProvisioners.MostRecentlySeen.Valid)
|
|
}
|
|
|
|
func BenchmarkConvertWorkspaceBuilds(b *testing.B) {
|
|
for _, builds := range []int{1, 25, 100} {
|
|
b.Run(fmt.Sprintf("Builds%d", builds), func(b *testing.B) {
|
|
api := newConvertAPI()
|
|
fixture := newBuildFixture(builds, 5, 2, 4)
|
|
|
|
b.ReportAllocs()
|
|
for b.Loop() {
|
|
if _, err := fixture.convert(api); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|