@@ -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,166 @@ 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 )
3238
-
3239
3244
q .mutex .RLock ()
3240
3245
defer q .mutex .RUnlock ()
3241
3246
3242
- for _ ,s := range q .workspaceAgentStats {
3243
- if s .CreatedAt .Before (arg .StartTime )|| s .CreatedAt .Equal (arg .EndTime )|| s .CreatedAt .After (arg .EndTime ) {
3247
+ /*
3248
+ WITH
3249
+ */
3250
+
3251
+ /*
3252
+ insights AS (
3253
+ SELECT
3254
+ user_id,
3255
+ -- See motivation in GetTemplateInsights for LEAST(SUM(n), 30).
3256
+ LEAST(SUM(usage_mins), 30) AS usage_mins,
3257
+ LEAST(SUM(ssh_mins), 30) AS ssh_mins,
3258
+ LEAST(SUM(sftp_mins), 30) AS sftp_mins,
3259
+ LEAST(SUM(reconnecting_pty_mins), 30) AS reconnecting_pty_mins,
3260
+ LEAST(SUM(vscode_mins), 30) AS vscode_mins,
3261
+ LEAST(SUM(jetbrains_mins), 30) AS jetbrains_mins
3262
+ FROM
3263
+ template_usage_stats
3264
+ WHERE
3265
+ start_time >= @start_time::timestamptz
3266
+ AND end_time <= @end_time::timestamptz
3267
+ AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
3268
+ GROUP BY
3269
+ start_time, user_id
3270
+ ),
3271
+ */
3272
+
3273
+ type insightsGroupBy struct {
3274
+ StartTime time.Time
3275
+ UserID uuid.UUID
3276
+ }
3277
+ type insightsRow struct {
3278
+ insightsGroupBy
3279
+ UsageMins int16
3280
+ SSHMins int16
3281
+ SFTPMins int16
3282
+ ReconnectingPTYMins int16
3283
+ VSCodeMins int16
3284
+ JetBrainsMins int16
3285
+ }
3286
+ insights := make (map [insightsGroupBy ]insightsRow )
3287
+ for _ ,stat := range q .templateUsageStats {
3288
+ if stat .StartTime .Before (arg .StartTime )|| stat .EndTime .After (arg .EndTime ) {
3244
3289
continue
3245
3290
}
3246
- if len (arg .TemplateIDs )> 0 && ! slices .Contains (arg .TemplateIDs ,s .TemplateID ) {
3291
+ if len (arg .TemplateIDs )> 0 && ! slices .Contains (arg .TemplateIDs ,stat .TemplateID ) {
3247
3292
continue
3248
3293
}
3249
- if s .ConnectionCount == 0 {
3250
- continue
3294
+ key := insightsGroupBy {
3295
+ StartTime :stat .StartTime ,
3296
+ UserID :stat .UserID ,
3297
+ }
3298
+ row ,ok := insights [key ]
3299
+ if ! ok {
3300
+ row = insightsRow {
3301
+ insightsGroupBy :key ,
3302
+ }
3251
3303
}
3304
+ row .UsageMins = least (row .UsageMins + stat .UsageMins ,30 )
3305
+ row .SSHMins = least (row .SSHMins + stat .SshMins ,30 )
3306
+ row .SFTPMins = least (row .SFTPMins + stat .SftpMins ,30 )
3307
+ row .ReconnectingPTYMins = least (row .ReconnectingPTYMins + stat .ReconnectingPtyMins ,30 )
3308
+ row .VSCodeMins = least (row .VSCodeMins + stat .VscodeMins ,30 )
3309
+ row .JetBrainsMins = least (row .JetBrainsMins + stat .JetbrainsMins ,30 )
3310
+ insights [key ]= row
3311
+ }
3252
3312
3253
- templateIDSet [s .TemplateID ]= struct {}{}
3254
- if appUsageIntervalsByUser [s .UserID ]== nil {
3255
- appUsageIntervalsByUser [s .UserID ]= make (map [time.Time ]* database.GetTemplateInsightsRow )
3313
+ /*
3314
+ templates AS (
3315
+ SELECT
3316
+ array_agg(DISTINCT template_id) AS template_ids,
3317
+ array_agg(DISTINCT template_id) FILTER (WHERE ssh_mins > 0) AS ssh_template_ids,
3318
+ array_agg(DISTINCT template_id) FILTER (WHERE sftp_mins > 0) AS sftp_template_ids,
3319
+ array_agg(DISTINCT template_id) FILTER (WHERE reconnecting_pty_mins > 0) AS reconnecting_pty_template_ids,
3320
+ array_agg(DISTINCT template_id) FILTER (WHERE vscode_mins > 0) AS vscode_template_ids,
3321
+ array_agg(DISTINCT template_id) FILTER (WHERE jetbrains_mins > 0) AS jetbrains_template_ids
3322
+ FROM
3323
+ template_usage_stats
3324
+ WHERE
3325
+ start_time >= @start_time::timestamptz
3326
+ AND end_time <= @end_time::timestamptz
3327
+ AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
3328
+ )
3329
+ */
3330
+
3331
+ type templateRow struct {
3332
+ TemplateIDs []uuid.UUID
3333
+ SSHTemplateIDs []uuid.UUID
3334
+ SFTPTemplateIDs []uuid.UUID
3335
+ ReconnectingPTYIDs []uuid.UUID
3336
+ VSCodeTemplateIDs []uuid.UUID
3337
+ JetBrainsTemplateIDs []uuid.UUID
3338
+ }
3339
+ templates := templateRow {}
3340
+ for _ ,stat := range q .templateUsageStats {
3341
+ if stat .StartTime .Before (arg .StartTime )|| stat .EndTime .After (arg .EndTime ) {
3342
+ continue
3256
3343
}
3257
- t := s .CreatedAt .Truncate (time .Minute )
3258
- if _ ,ok := appUsageIntervalsByUser [s.UserID ][t ];! ok {
3259
- appUsageIntervalsByUser [s.UserID ][t ]= & database.GetTemplateInsightsRow {}
3344
+ if len (arg .TemplateIDs )> 0 && ! slices .Contains (arg .TemplateIDs ,stat .TemplateID ) {
3345
+ continue
3260
3346
}
3261
-
3262
- if s . SessionCountJetBrains > 0 {
3263
- appUsageIntervalsByUser [s. UserID ][ t ]. UsageJetbrainsSeconds = 60
3347
+ templates . TemplateIDs = append ( templates . TemplateIDs , stat . TemplateID )
3348
+ if stat . SshMins > 0 {
3349
+ templates . SSHTemplateIDs = append ( templates . SSHTemplateIDs , stat . TemplateID )
3264
3350
}
3265
- if s . SessionCountVSCode > 0 {
3266
- appUsageIntervalsByUser [s. UserID ][ t ]. UsageVscodeSeconds = 60
3351
+ if stat . SftpMins > 0 {
3352
+ templates . SFTPTemplateIDs = append ( templates . SFTPTemplateIDs , stat . TemplateID )
3267
3353
}
3268
- if s . SessionCountReconnectingPTY > 0 {
3269
- appUsageIntervalsByUser [s. UserID ][ t ]. UsageReconnectingPtySeconds = 60
3354
+ if stat . ReconnectingPtyMins > 0 {
3355
+ templates . ReconnectingPTYIDs = append ( templates . ReconnectingPTYIDs , stat . TemplateID )
3270
3356
}
3271
- if s .SessionCountSSH > 0 {
3272
- appUsageIntervalsByUser [s.UserID ][t ].UsageSshSeconds = 60
3357
+ if stat .VscodeMins > 0 {
3358
+ templates .VSCodeTemplateIDs = append (templates .VSCodeTemplateIDs ,stat .TemplateID )
3359
+ }
3360
+ if stat .JetbrainsMins > 0 {
3361
+ templates .JetBrainsTemplateIDs = append (templates .JetBrainsTemplateIDs ,stat .TemplateID )
3273
3362
}
3274
3363
}
3275
3364
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
- }
3365
+ /*
3366
+ SELECT
3367
+ COALESCE((SELECT template_ids FROM templates), '{}')::uuid[] AS template_ids, -- Includes app usage.
3368
+ COALESCE((SELECT ssh_template_ids FROM templates), '{}')::uuid[] AS ssh_template_ids,
3369
+ COALESCE((SELECT sftp_template_ids FROM templates), '{}')::uuid[] AS sftp_template_ids,
3370
+ COALESCE((SELECT reconnecting_pty_template_ids FROM templates), '{}')::uuid[] AS reconnecting_pty_template_ids,
3371
+ COALESCE((SELECT vscode_template_ids FROM templates), '{}')::uuid[] AS vscode_template_ids,
3372
+ COALESCE((SELECT jetbrains_template_ids FROM templates), '{}')::uuid[] AS jetbrains_template_ids,
3373
+ COALESCE(COUNT(DISTINCT user_id), 0)::bigint AS active_users, -- Includes app usage.
3374
+ COALESCE(SUM(usage_mins) * 60, 0)::bigint AS usage_total_seconds, -- Includes app usage.
3375
+ COALESCE(SUM(ssh_mins) * 60, 0)::bigint AS usage_ssh_seconds,
3376
+ COALESCE(SUM(sftp_mins) * 60, 0)::bigint AS usage_sftp_seconds,
3377
+ COALESCE(SUM(reconnecting_pty_mins) * 60, 0)::bigint AS usage_reconnecting_pty_seconds,
3378
+ COALESCE(SUM(vscode_mins) * 60, 0)::bigint AS usage_vscode_seconds,
3379
+ COALESCE(SUM(jetbrains_mins) * 60, 0)::bigint AS usage_jetbrains_seconds
3380
+ FROM
3381
+ insights;
3382
+ */
3287
3383
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
3384
+ var row database.GetTemplateInsightsRow
3385
+ row .TemplateIDs = uniqueSortedUUIDs (templates .TemplateIDs )
3386
+ row .SshTemplateIds = uniqueSortedUUIDs (templates .SSHTemplateIDs )
3387
+ row .SftpTemplateIds = uniqueSortedUUIDs (templates .SFTPTemplateIDs )
3388
+ row .ReconnectingPtyTemplateIds = uniqueSortedUUIDs (templates .ReconnectingPTYIDs )
3389
+ row .VscodeTemplateIds = uniqueSortedUUIDs (templates .VSCodeTemplateIDs )
3390
+ row .JetbrainsTemplateIds = uniqueSortedUUIDs (templates .JetBrainsTemplateIDs )
3391
+ activeUserIDs := make (map [uuid.UUID ]struct {})
3392
+ for _ ,insight := range insights {
3393
+ activeUserIDs [insight .UserID ]= struct {}{}
3394
+ row .UsageTotalSeconds += int64 (insight .UsageMins )* 60
3395
+ row .UsageSshSeconds += int64 (insight .SSHMins )* 60
3396
+ row .UsageSftpSeconds += int64 (insight .SFTPMins )* 60
3397
+ row .UsageReconnectingPtySeconds += int64 (insight .ReconnectingPTYMins )* 60
3398
+ row .UsageVscodeSeconds += int64 (insight .VSCodeMins )* 60
3399
+ row .UsageJetbrainsSeconds += int64 (insight .JetBrainsMins )* 60
3400
+ }
3401
+ row .ActiveUsers = int64 (len (activeUserIDs ))
3402
+
3403
+ return row ,nil
3301
3404
}
3302
3405
3303
3406
func (q * FakeQuerier )GetTemplateInsightsByInterval (ctx context.Context ,arg database.GetTemplateInsightsByIntervalParams ) ([]database.GetTemplateInsightsByIntervalRow ,error ) {