@@ -16,6 +16,7 @@ import (
16
16
17
17
"github.com/google/uuid"
18
18
"github.com/lib/pq"
19
+ "golang.org/x/exp/constraints"
19
20
"golang.org/x/exp/maps"
20
21
"golang.org/x/exp/slices"
21
22
"golang.org/x/xerrors"
@@ -791,6 +792,13 @@ func tagsSubset(m1, m2 map[string]string) bool {
791
792
// default tags when no tag is specified for a provisioner or job
792
793
var tagsUntagged = provisionersdk .MutateTags (uuid .Nil ,nil )
793
794
795
+ func least [T constraints.Ordered ](a ,b T )T {
796
+ if a < b {
797
+ return a
798
+ }
799
+ return b
800
+ }
801
+
794
802
func (* FakeQuerier )AcquireLock (_ context.Context ,_ int64 )error {
795
803
return xerrors .New ("AcquireLock must only be called within a transaction" )
796
804
}
@@ -3233,71 +3241,163 @@ func (q *FakeQuerier) GetTemplateInsights(_ context.Context, arg database.GetTem
3233
3241
return database.GetTemplateInsightsRow {},err
3234
3242
}
3235
3243
3236
- templateIDSet := make (map [uuid.UUID ]struct {})
3237
- appUsageIntervalsByUser := make (map [uuid.UUID ]map [time.Time ]* database.GetTemplateInsightsRow )
3244
+ /*
3245
+ WITH
3246
+ */
3238
3247
3239
- q .mutex .RLock ()
3240
- defer q .mutex .RUnlock ()
3248
+ /*
3249
+ insights AS (
3250
+ SELECT
3251
+ user_id,
3252
+ -- See motivation in GetTemplateInsights for LEAST(SUM(n), 30).
3253
+ LEAST(SUM(usage_mins), 30) AS usage_mins,
3254
+ LEAST(SUM(ssh_mins), 30) AS ssh_mins,
3255
+ LEAST(SUM(sftp_mins), 30) AS sftp_mins,
3256
+ LEAST(SUM(reconnecting_pty_mins), 30) AS reconnecting_pty_mins,
3257
+ LEAST(SUM(vscode_mins), 30) AS vscode_mins,
3258
+ LEAST(SUM(jetbrains_mins), 30) AS jetbrains_mins
3259
+ FROM
3260
+ template_usage_stats
3261
+ WHERE
3262
+ start_time >= @start_time::timestamptz
3263
+ AND end_time <= @end_time::timestamptz
3264
+ AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
3265
+ GROUP BY
3266
+ start_time, user_id
3267
+ ),
3268
+ */
3241
3269
3242
- for _ ,s := range q .workspaceAgentStats {
3243
- if s .CreatedAt .Before (arg .StartTime )|| s .CreatedAt .Equal (arg .EndTime )|| s .CreatedAt .After (arg .EndTime ) {
3270
+ type insightsGroupBy struct {
3271
+ StartTime time.Time
3272
+ UserID uuid.UUID
3273
+ }
3274
+ type insightsRow struct {
3275
+ insightsGroupBy
3276
+ UsageMins int16
3277
+ SSHMins int16
3278
+ SFTPMins int16
3279
+ ReconnectingPTYMins int16
3280
+ VSCodeMins int16
3281
+ JetBrainsMins int16
3282
+ }
3283
+ insights := make (map [insightsGroupBy ]insightsRow )
3284
+ for _ ,stat := range q .templateUsageStats {
3285
+ if stat .StartTime .Before (arg .StartTime )|| stat .EndTime .After (arg .EndTime ) {
3244
3286
continue
3245
3287
}
3246
- if len (arg .TemplateIDs )> 0 && ! slices .Contains (arg .TemplateIDs ,s .TemplateID ) {
3288
+ if len (arg .TemplateIDs )> 0 && ! slices .Contains (arg .TemplateIDs ,stat .TemplateID ) {
3247
3289
continue
3248
3290
}
3249
- if s .ConnectionCount == 0 {
3250
- continue
3291
+ key := insightsGroupBy {
3292
+ StartTime :stat .StartTime ,
3293
+ UserID :stat .UserID ,
3251
3294
}
3295
+ row ,ok := insights [key ]
3296
+ if ! ok {
3297
+ row = insightsRow {
3298
+ insightsGroupBy :key ,
3299
+ }
3300
+ }
3301
+ row .UsageMins = least (row .UsageMins + stat .UsageMins ,30 )
3302
+ row .SSHMins = least (row .SSHMins + stat .SshMins ,30 )
3303
+ row .SFTPMins = least (row .SFTPMins + stat .SftpMins ,30 )
3304
+ row .ReconnectingPTYMins = least (row .ReconnectingPTYMins + stat .ReconnectingPtyMins ,30 )
3305
+ row .VSCodeMins = least (row .VSCodeMins + stat .VscodeMins ,30 )
3306
+ row .JetBrainsMins = least (row .JetBrainsMins + stat .JetbrainsMins ,30 )
3307
+ insights [key ]= row
3308
+ }
3252
3309
3253
- templateIDSet [s .TemplateID ]= struct {}{}
3254
- if appUsageIntervalsByUser [s .UserID ]== nil {
3255
- appUsageIntervalsByUser [s .UserID ]= make (map [time.Time ]* database.GetTemplateInsightsRow )
3310
+ /*
3311
+ templates AS (
3312
+ SELECT
3313
+ array_agg(DISTINCT template_id) AS template_ids,
3314
+ array_agg(DISTINCT template_id) FILTER (WHERE ssh_mins > 0) AS ssh_template_ids,
3315
+ array_agg(DISTINCT template_id) FILTER (WHERE sftp_mins > 0) AS sftp_template_ids,
3316
+ array_agg(DISTINCT template_id) FILTER (WHERE reconnecting_pty_mins > 0) AS reconnecting_pty_template_ids,
3317
+ array_agg(DISTINCT template_id) FILTER (WHERE vscode_mins > 0) AS vscode_template_ids,
3318
+ array_agg(DISTINCT template_id) FILTER (WHERE jetbrains_mins > 0) AS jetbrains_template_ids
3319
+ FROM
3320
+ template_usage_stats
3321
+ WHERE
3322
+ start_time >= @start_time::timestamptz
3323
+ AND end_time <= @end_time::timestamptz
3324
+ AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
3325
+ )
3326
+ */
3327
+
3328
+ type templateRow struct {
3329
+ TemplateIDs []uuid.UUID
3330
+ SSHTemplateIDs []uuid.UUID
3331
+ SFTPTemplateIDs []uuid.UUID
3332
+ ReconnectingPTYIDs []uuid.UUID
3333
+ VSCodeTemplateIDs []uuid.UUID
3334
+ JetBrainsTemplateIDs []uuid.UUID
3335
+ }
3336
+ templates := templateRow {}
3337
+ for _ ,stat := range q .templateUsageStats {
3338
+ if stat .StartTime .Before (arg .StartTime )|| stat .EndTime .After (arg .EndTime ) {
3339
+ continue
3256
3340
}
3257
- t := s .CreatedAt .Truncate (time .Minute )
3258
- if _ ,ok := appUsageIntervalsByUser [s.UserID ][t ];! ok {
3259
- appUsageIntervalsByUser [s.UserID ][t ]= & database.GetTemplateInsightsRow {}
3341
+ if len (arg .TemplateIDs )> 0 && ! slices .Contains (arg .TemplateIDs ,stat .TemplateID ) {
3342
+ continue
3260
3343
}
3261
-
3262
- if s . SessionCountJetBrains > 0 {
3263
- appUsageIntervalsByUser [s. UserID ][ t ]. UsageJetbrainsSeconds = 60
3344
+ templates . TemplateIDs = append ( templates . TemplateIDs , stat . TemplateID )
3345
+ if stat . SshMins > 0 {
3346
+ templates . SSHTemplateIDs = append ( templates . SSHTemplateIDs , stat . TemplateID )
3264
3347
}
3265
- if s . SessionCountVSCode > 0 {
3266
- appUsageIntervalsByUser [s. UserID ][ t ]. UsageVscodeSeconds = 60
3348
+ if stat . SftpMins > 0 {
3349
+ templates . SFTPTemplateIDs = append ( templates . SFTPTemplateIDs , stat . TemplateID )
3267
3350
}
3268
- if s . SessionCountReconnectingPTY > 0 {
3269
- appUsageIntervalsByUser [s. UserID ][ t ]. UsageReconnectingPtySeconds = 60
3351
+ if stat . ReconnectingPtyMins > 0 {
3352
+ templates . ReconnectingPTYIDs = append ( templates . ReconnectingPTYIDs , stat . TemplateID )
3270
3353
}
3271
- if s .SessionCountSSH > 0 {
3272
- appUsageIntervalsByUser [s.UserID ][t ].UsageSshSeconds = 60
3354
+ if stat .VscodeMins > 0 {
3355
+ templates .VSCodeTemplateIDs = append (templates .VSCodeTemplateIDs ,stat .TemplateID )
3356
+ }
3357
+ if stat .JetbrainsMins > 0 {
3358
+ templates .JetBrainsTemplateIDs = append (templates .JetBrainsTemplateIDs ,stat .TemplateID )
3273
3359
}
3274
3360
}
3275
3361
3276
- templateIDs := make ([]uuid.UUID ,0 ,len (templateIDSet ))
3277
- for templateID := range templateIDSet {
3278
- templateIDs = append (templateIDs ,templateID )
3279
- }
3280
- slices .SortFunc (templateIDs ,func (a ,b uuid.UUID )int {
3281
- return slice .Ascending (a .String (),b .String ())
3282
- })
3283
- activeUserIDs := make ([]uuid.UUID ,0 ,len (appUsageIntervalsByUser ))
3284
- for userID := range appUsageIntervalsByUser {
3285
- activeUserIDs = append (activeUserIDs ,userID )
3286
- }
3362
+ /*
3363
+ SELECT
3364
+ COALESCE((SELECT template_ids FROM templates), '{}')::uuid[] AS template_ids, -- Includes app usage.
3365
+ COALESCE((SELECT ssh_template_ids FROM templates), '{}')::uuid[] AS ssh_template_ids,
3366
+ COALESCE((SELECT sftp_template_ids FROM templates), '{}')::uuid[] AS sftp_template_ids,
3367
+ COALESCE((SELECT reconnecting_pty_template_ids FROM templates), '{}')::uuid[] AS reconnecting_pty_template_ids,
3368
+ COALESCE((SELECT vscode_template_ids FROM templates), '{}')::uuid[] AS vscode_template_ids,
3369
+ COALESCE((SELECT jetbrains_template_ids FROM templates), '{}')::uuid[] AS jetbrains_template_ids,
3370
+ COALESCE(COUNT(DISTINCT user_id), 0)::bigint AS active_users, -- Includes app usage.
3371
+ COALESCE(SUM(usage_mins) * 60, 0)::bigint AS usage_total_seconds, -- Includes app usage.
3372
+ COALESCE(SUM(ssh_mins) * 60, 0)::bigint AS usage_ssh_seconds,
3373
+ COALESCE(SUM(sftp_mins) * 60, 0)::bigint AS usage_sftp_seconds,
3374
+ COALESCE(SUM(reconnecting_pty_mins) * 60, 0)::bigint AS usage_reconnecting_pty_seconds,
3375
+ COALESCE(SUM(vscode_mins) * 60, 0)::bigint AS usage_vscode_seconds,
3376
+ COALESCE(SUM(jetbrains_mins) * 60, 0)::bigint AS usage_jetbrains_seconds
3377
+ FROM
3378
+ insights;
3379
+ */
3287
3380
3288
- result := database.GetTemplateInsightsRow {
3289
- TemplateIDs :templateIDs ,
3290
- ActiveUsers :int64 (len (activeUserIDs )),
3291
- }
3292
- for _ ,intervals := range appUsageIntervalsByUser {
3293
- for _ ,interval := range intervals {
3294
- result .UsageJetbrainsSeconds += interval .UsageJetbrainsSeconds
3295
- result .UsageVscodeSeconds += interval .UsageVscodeSeconds
3296
- result .UsageReconnectingPtySeconds += interval .UsageReconnectingPtySeconds
3297
- result .UsageSshSeconds += interval .UsageSshSeconds
3298
- }
3299
- }
3300
- return result ,nil
3381
+ var row database.GetTemplateInsightsRow
3382
+ row .TemplateIDs = uniqueSortedUUIDs (templates .TemplateIDs )
3383
+ row .SshTemplateIds = uniqueSortedUUIDs (templates .SSHTemplateIDs )
3384
+ row .SftpTemplateIds = uniqueSortedUUIDs (templates .SFTPTemplateIDs )
3385
+ row .ReconnectingPtyTemplateIds = uniqueSortedUUIDs (templates .ReconnectingPTYIDs )
3386
+ row .VscodeTemplateIds = uniqueSortedUUIDs (templates .VSCodeTemplateIDs )
3387
+ row .JetbrainsTemplateIds = uniqueSortedUUIDs (templates .JetBrainsTemplateIDs )
3388
+ activeUserIDs := make (map [uuid.UUID ]struct {})
3389
+ for _ ,insight := range insights {
3390
+ activeUserIDs [insight .UserID ]= struct {}{}
3391
+ row .UsageTotalSeconds += int64 (insight .UsageMins )* 60
3392
+ row .UsageSshSeconds += int64 (insight .SSHMins )* 60
3393
+ row .UsageSftpSeconds += int64 (insight .SFTPMins )* 60
3394
+ row .UsageReconnectingPtySeconds += int64 (insight .ReconnectingPTYMins )* 60
3395
+ row .UsageVscodeSeconds += int64 (insight .VSCodeMins )* 60
3396
+ row .UsageJetbrainsSeconds += int64 (insight .JetBrainsMins )* 60
3397
+ }
3398
+ row .ActiveUsers = int64 (len (activeUserIDs ))
3399
+
3400
+ return row ,nil
3301
3401
}
3302
3402
3303
3403
func (q * FakeQuerier )GetTemplateInsightsByInterval (ctx context.Context ,arg database.GetTemplateInsightsByIntervalParams ) ([]database.GetTemplateInsightsByIntervalRow ,error ) {