speed up vm trace pages
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-02-09 14:19:24 +11:00
parent c4097ca608
commit 59b16db04f
12 changed files with 702 additions and 208 deletions

View File

@@ -605,6 +605,7 @@ CREATE TABLE IF NOT EXISTS vm_hourly_stats (
}
_, _ = execLog(ctx, dbConn, `CREATE INDEX IF NOT EXISTS vm_hourly_stats_vmuuid_time_idx ON vm_hourly_stats ("VmUuid","SnapshotTime")`)
_, _ = execLog(ctx, dbConn, `CREATE INDEX IF NOT EXISTS vm_hourly_stats_vmid_time_idx ON vm_hourly_stats ("VmId","SnapshotTime")`)
_, _ = execLog(ctx, dbConn, `CREATE INDEX IF NOT EXISTS vm_hourly_stats_name_time_idx ON vm_hourly_stats (lower("Name"),"SnapshotTime")`)
_, _ = execLog(ctx, dbConn, `CREATE INDEX IF NOT EXISTS vm_hourly_stats_snapshottime_idx ON vm_hourly_stats ("SnapshotTime")`)
return nil
}
@@ -627,6 +628,8 @@ CREATE TABLE IF NOT EXISTS vm_lifecycle_cache (
return err
}
_, _ = execLog(ctx, dbConn, `CREATE INDEX IF NOT EXISTS vm_lifecycle_cache_vmuuid_idx ON vm_lifecycle_cache ("VmUuid")`)
_, _ = execLog(ctx, dbConn, `CREATE INDEX IF NOT EXISTS vm_lifecycle_cache_vmid_idx ON vm_lifecycle_cache ("VmId")`)
_, _ = execLog(ctx, dbConn, `CREATE INDEX IF NOT EXISTS vm_lifecycle_cache_name_idx ON vm_lifecycle_cache (lower("Name"))`)
return nil
}
@@ -947,6 +950,9 @@ CREATE TABLE IF NOT EXISTS vm_daily_rollup (
}
_, _ = execLog(ctx, dbConn, `CREATE INDEX IF NOT EXISTS vm_daily_rollup_date_idx ON vm_daily_rollup ("Date")`)
_, _ = execLog(ctx, dbConn, `CREATE INDEX IF NOT EXISTS vm_daily_rollup_vcenter_date_idx ON vm_daily_rollup ("Vcenter","Date")`)
_, _ = execLog(ctx, dbConn, `CREATE INDEX IF NOT EXISTS vm_daily_rollup_vmid_date_idx ON vm_daily_rollup ("VmId","Date")`)
_, _ = execLog(ctx, dbConn, `CREATE INDEX IF NOT EXISTS vm_daily_rollup_vmuuid_date_idx ON vm_daily_rollup ("VmUuid","Date")`)
_, _ = execLog(ctx, dbConn, `CREATE INDEX IF NOT EXISTS vm_daily_rollup_name_date_idx ON vm_daily_rollup (lower("Name"),"Date")`)
return nil
}
@@ -1666,6 +1672,22 @@ type VmLifecycle struct {
DeletionTime int64
}
func vmLookupPredicate(vmID, vmUUID, name string) (string, []interface{}, bool) {
vmID = strings.TrimSpace(vmID)
vmUUID = strings.TrimSpace(vmUUID)
name = strings.TrimSpace(name)
switch {
case vmID != "":
return `"VmId" = ?`, []interface{}{vmID}, true
case vmUUID != "":
return `"VmUuid" = ?`, []interface{}{vmUUID}, true
case name != "":
return `lower("Name") = ?`, []interface{}{strings.ToLower(name)}, true
default:
return "", nil, false
}
}
// FetchVmTrace returns combined hourly snapshot records for a VM (by id/uuid/name) ordered by snapshot time.
// It prefers the shared vm_hourly_stats history table and falls back to per-snapshot tables.
func FetchVmTrace(ctx context.Context, dbConn *sqlx.DB, vmID, vmUUID, name string) ([]VmTraceRow, error) {
@@ -1681,24 +1703,142 @@ func FetchVmTrace(ctx context.Context, dbConn *sqlx.DB, vmID, vmUUID, name strin
return fetchVmTraceFromSnapshotTables(ctx, dbConn, vmID, vmUUID, name)
}
// FetchVmTraceDaily returns one row per day for a VM, preferring vm_daily_rollup cache.
func FetchVmTraceDaily(ctx context.Context, dbConn *sqlx.DB, vmID, vmUUID, name string) ([]VmTraceRow, error) {
if TableExists(ctx, dbConn, "vm_daily_rollup") {
if err := EnsureVmDailyRollup(ctx, dbConn); err != nil {
slog.Warn("failed to ensure vm_daily_rollup indexes", "error", err)
}
rows, err := fetchVmTraceDailyFromRollup(ctx, dbConn, vmID, vmUUID, name)
if err != nil {
slog.Warn("vm daily trace cache query failed; falling back to daily summary tables", "error", err)
} else if len(rows) > 0 {
slog.Debug("vm daily trace loaded from daily rollup cache", "row_count", len(rows))
return rows, nil
}
}
return fetchVmTraceDailyFromSummaryTables(ctx, dbConn, vmID, vmUUID, name)
}
func fetchVmTraceDailyFromRollup(ctx context.Context, dbConn *sqlx.DB, vmID, vmUUID, name string) ([]VmTraceRow, error) {
matchWhere, args, ok := vmLookupPredicate(vmID, vmUUID, name)
if !ok {
return nil, nil
}
query := fmt.Sprintf(`
SELECT "Date" AS "SnapshotTime",
COALESCE("Name",'') AS "Name",
COALESCE("Vcenter",'') AS "Vcenter",
COALESCE("VmId",'') AS "VmId",
COALESCE("VmUuid",'') AS "VmUuid",
COALESCE("LastResourcePool",'') AS "ResourcePool",
CAST(CASE
WHEN COALESCE("SamplesPresent",0) > 0 THEN ROUND(1.0 * COALESCE("SumVcpu",0) / "SamplesPresent")
ELSE COALESCE("LastVcpuCount",0)
END AS BIGINT) AS "VcpuCount",
CAST(CASE
WHEN COALESCE("SamplesPresent",0) > 0 THEN ROUND(1.0 * COALESCE("SumRam",0) / "SamplesPresent")
ELSE COALESCE("LastRamGB",0)
END AS BIGINT) AS "RamGB",
COALESCE("LastProvisionedDisk",0) AS "ProvisionedDisk",
COALESCE("CreationTime",0) AS "CreationTime",
COALESCE("DeletionTime",0) AS "DeletionTime"
FROM vm_daily_rollup
WHERE %s
ORDER BY "Date"
`, matchWhere)
query = dbConn.Rebind(query)
rows := make([]VmTraceRow, 0, 128)
if err := selectLog(ctx, dbConn, &rows, query, args...); err != nil {
return nil, err
}
return rows, nil
}
func fetchVmTraceDailyFromSummaryTables(ctx context.Context, dbConn *sqlx.DB, vmID, vmUUID, name string) ([]VmTraceRow, error) {
matchWhere, args, ok := vmLookupPredicate(vmID, vmUUID, name)
if !ok {
return nil, nil
}
if !TableExists(ctx, dbConn, "snapshot_registry") {
return nil, nil
}
var tables []struct {
TableName string `db:"table_name"`
SnapshotTime int64 `db:"snapshot_time"`
}
if err := selectLog(ctx, dbConn, &tables, `
SELECT table_name, snapshot_time
FROM snapshot_registry
WHERE snapshot_type = 'daily'
ORDER BY snapshot_time
`); err != nil {
return nil, err
}
if len(tables) == 0 {
return nil, nil
}
rows := make([]VmTraceRow, 0, len(tables))
for _, t := range tables {
if err := ValidateTableName(t.TableName); err != nil {
continue
}
query := fmt.Sprintf(`
SELECT %d AS "SnapshotTime",
COALESCE("Name",'') AS "Name",
COALESCE("Vcenter",'') AS "Vcenter",
COALESCE("VmId",'') AS "VmId",
COALESCE("VmUuid",'') AS "VmUuid",
COALESCE("ResourcePool",'') AS "ResourcePool",
CAST(COALESCE("AvgVcpuCount","VcpuCount",0) AS BIGINT) AS "VcpuCount",
CAST(COALESCE("AvgRamGB","RamGB",0) AS BIGINT) AS "RamGB",
COALESCE("AvgProvisionedDisk","ProvisionedDisk",0) AS "ProvisionedDisk",
COALESCE("CreationTime",0) AS "CreationTime",
COALESCE("DeletionTime",0) AS "DeletionTime"
FROM %s
WHERE %s
`, t.SnapshotTime, t.TableName, matchWhere)
query = dbConn.Rebind(query)
var tmp []VmTraceRow
if err := selectLog(ctx, dbConn, &tmp, query, args...); err != nil {
continue
}
rows = append(rows, tmp...)
}
sort.Slice(rows, func(i, j int) bool {
return rows[i].SnapshotTime < rows[j].SnapshotTime
})
return rows, nil
}
func fetchVmTraceFromHourlyCache(ctx context.Context, dbConn *sqlx.DB, vmID, vmUUID, name string) ([]VmTraceRow, error) {
query := `
matchWhere, args, ok := vmLookupPredicate(vmID, vmUUID, name)
if !ok {
return nil, nil
}
query := fmt.Sprintf(`
SELECT "SnapshotTime","Name","Vcenter","VmId","VmUuid","ResourcePool","VcpuCount","RamGB","ProvisionedDisk",
COALESCE("CreationTime",0) AS "CreationTime",
COALESCE("DeletionTime",0) AS "DeletionTime"
FROM vm_hourly_stats
WHERE ("VmId" = ? OR "VmUuid" = ? OR lower("Name") = lower(?))
WHERE %s
ORDER BY "SnapshotTime"
`
`, matchWhere)
query = dbConn.Rebind(query)
var rows []VmTraceRow
if err := selectLog(ctx, dbConn, &rows, query, vmID, vmUUID, name); err != nil {
if err := selectLog(ctx, dbConn, &rows, query, args...); err != nil {
return nil, err
}
return rows, nil
}
func fetchVmTraceFromSnapshotTables(ctx context.Context, dbConn *sqlx.DB, vmID, vmUUID, name string) ([]VmTraceRow, error) {
matchWhere, args, ok := vmLookupPredicate(vmID, vmUUID, name)
if !ok {
return nil, nil
}
var tables []struct {
TableName string `db:"table_name"`
SnapshotTime int64 `db:"snapshot_time"`
@@ -1716,7 +1856,6 @@ ORDER BY snapshot_time
}
rows := make([]VmTraceRow, 0, len(tables))
driver := strings.ToLower(dbConn.DriverName())
slog.Debug("vm trace scanning tables", "table_count", len(tables), "vm_id", vmID, "vm_uuid", vmUUID, "name", name)
@@ -1731,14 +1870,9 @@ SELECT %d AS "SnapshotTime",
COALESCE("CreationTime",0) AS "CreationTime",
COALESCE("DeletionTime",0) AS "DeletionTime"
FROM %s
WHERE ("VmId" = ? OR "VmUuid" = ? OR lower("Name") = lower(?))
`, t.SnapshotTime, t.TableName)
args := []interface{}{vmID, vmUUID, name}
if driver != "sqlite" {
query = strings.Replace(query, "?", "$1", 1)
query = strings.Replace(query, "?", "$2", 1)
query = strings.Replace(query, "?", "$3", 1)
}
WHERE %s
`, t.SnapshotTime, t.TableName, matchWhere)
query = dbConn.Rebind(query)
var tmp []VmTraceRow
if err := selectLog(ctx, dbConn, &tmp, query, args...); err != nil {
slog.Warn("vm trace query failed for table", "table", t.TableName, "error", err)
@@ -1778,6 +1912,10 @@ func FetchVmLifecycle(ctx context.Context, dbConn *sqlx.DB, vmID, vmUUID, name s
}
func fetchVmLifecycleFromHourlyCache(ctx context.Context, dbConn *sqlx.DB, vmID, vmUUID, name string) (VmLifecycle, bool, error) {
matchWhere, args, ok := vmLookupPredicate(vmID, vmUUID, name)
if !ok {
return VmLifecycle{}, false, nil
}
var row struct {
Rows int64 `db:"rows"`
Creation sql.NullInt64 `db:"creation_time"`
@@ -1793,10 +1931,10 @@ SELECT
MAX("SnapshotTime") AS last_seen,
MIN(NULLIF("DeletionTime",0)) AS deletion_time
FROM vm_hourly_stats
WHERE ("VmId" = ? OR "VmUuid" = ? OR lower("Name") = lower(?))
WHERE ` + matchWhere + `
`
query = dbConn.Rebind(query)
if err := getLog(ctx, dbConn, &row, query, vmID, vmUUID, name); err != nil {
if err := getLog(ctx, dbConn, &row, query, args...); err != nil {
return VmLifecycle{}, false, err
}
if row.Rows == 0 {
@@ -1820,6 +1958,10 @@ WHERE ("VmId" = ? OR "VmUuid" = ? OR lower("Name") = lower(?))
}
func fetchVmLifecycleFromLifecycleCache(ctx context.Context, dbConn *sqlx.DB, vmID, vmUUID, name string) (VmLifecycle, bool, error) {
matchWhere, args, ok := vmLookupPredicate(vmID, vmUUID, name)
if !ok {
return VmLifecycle{}, false, nil
}
var row struct {
Rows int64 `db:"rows"`
FirstSeen sql.NullInt64 `db:"first_seen"`
@@ -1833,10 +1975,10 @@ SELECT
MAX(NULLIF("LastSeen",0)) AS last_seen,
MIN(NULLIF("DeletedAt",0)) AS deletion_time
FROM vm_lifecycle_cache
WHERE ("VmId" = ? OR "VmUuid" = ? OR lower("Name") = lower(?))
WHERE ` + matchWhere + `
`
query = dbConn.Rebind(query)
if err := getLog(ctx, dbConn, &row, query, vmID, vmUUID, name); err != nil {
if err := getLog(ctx, dbConn, &row, query, args...); err != nil {
return VmLifecycle{}, false, err
}
if row.Rows == 0 {
@@ -1884,6 +2026,10 @@ func mergeVmLifecycle(base, overlay VmLifecycle) VmLifecycle {
func fetchVmLifecycleFromSnapshotTables(ctx context.Context, dbConn *sqlx.DB, vmID, vmUUID, name string) (VmLifecycle, error) {
var lifecycle VmLifecycle
matchWhere, args, ok := vmLookupPredicate(vmID, vmUUID, name)
if !ok {
return lifecycle, nil
}
var tables []struct {
TableName string `db:"table_name"`
SnapshotTime int64 `db:"snapshot_time"`
@@ -1896,7 +2042,6 @@ ORDER BY snapshot_time
`); err != nil {
return lifecycle, err
}
driver := strings.ToLower(dbConn.DriverName())
minCreation := int64(0)
consecutiveMissing := 0
@@ -1907,14 +2052,9 @@ ORDER BY snapshot_time
query := fmt.Sprintf(`
SELECT MIN(NULLIF("CreationTime",0)) AS min_creation, COUNT(1) AS cnt
FROM %s
WHERE ("VmId" = ? OR "VmUuid" = ? OR lower("Name") = lower(?))
`, t.TableName)
args := []interface{}{vmID, vmUUID, name}
if driver != "sqlite" {
query = strings.Replace(query, "?", "$1", 1)
query = strings.Replace(query, "?", "$2", 1)
query = strings.Replace(query, "?", "$3", 1)
}
WHERE %s
`, t.TableName, matchWhere)
query = dbConn.Rebind(query)
var probe struct {
MinCreation sql.NullInt64 `db:"min_creation"`
Cnt int64 `db:"cnt"`