@@ -799,6 +799,92 @@ func least[T constraints.Ordered](a, b T) T {
799
799
return b
800
800
}
801
801
802
+ func (q * FakeQuerier ) getLatestWorkspaceAppByTemplateIDUserIDSlugNoLock (ctx context.Context , templateID , userID uuid.UUID , slug string ) (database.WorkspaceApp , error ) {
803
+ /*
804
+ SELECT
805
+ app.display_name,
806
+ app.icon,
807
+ app.slug
808
+ FROM
809
+ workspace_apps AS app
810
+ JOIN
811
+ workspace_agents AS agent
812
+ ON
813
+ agent.id = app.agent_id
814
+ JOIN
815
+ workspace_resources AS resource
816
+ ON
817
+ resource.id = agent.resource_id
818
+ JOIN
819
+ workspace_builds AS build
820
+ ON
821
+ build.job_id = resource.job_id
822
+ JOIN
823
+ workspaces AS workspace
824
+ ON
825
+ workspace.id = build.workspace_id
826
+ WHERE
827
+ -- Requires lateral join.
828
+ app.slug = app_usage.key
829
+ AND workspace.owner_id = tus.user_id
830
+ AND workspace.template_id = tus.template_id
831
+ ORDER BY
832
+ app.created_at DESC
833
+ LIMIT 1
834
+ */
835
+
836
+ var workspaces []database.Workspace
837
+ for _ , w := range q .workspaces {
838
+ if w .TemplateID != templateID || w .OwnerID != userID {
839
+ continue
840
+ }
841
+ workspaces = append (workspaces , w )
842
+ }
843
+ slices .SortFunc (workspaces , func (a , b database.Workspace ) int {
844
+ if a .CreatedAt .Before (b .CreatedAt ) {
845
+ return 1
846
+ } else if a .CreatedAt .Equal (b .CreatedAt ) {
847
+ return 0
848
+ }
849
+ return - 1
850
+ })
851
+
852
+ for _ , workspace := range workspaces {
853
+
854
+ build , err := q .getLatestWorkspaceBuildByWorkspaceIDNoLock (ctx , workspace .ID )
855
+ if err != nil {
856
+ continue
857
+ }
858
+
859
+ resources , err := q .getWorkspaceResourcesByJobIDNoLock (ctx , build .JobID )
860
+ if err != nil {
861
+ continue
862
+ }
863
+ var resourceIDs []uuid.UUID
864
+ for _ , resource := range resources {
865
+ resourceIDs = append (resourceIDs , resource .ID )
866
+ }
867
+
868
+ agents , err := q .getWorkspaceAgentsByResourceIDsNoLock (ctx , resourceIDs )
869
+ if err != nil {
870
+ continue
871
+ }
872
+
873
+ for _ , agent := range agents {
874
+ app , err := q .getWorkspaceAppByAgentIDAndSlugNoLock (ctx , database.GetWorkspaceAppByAgentIDAndSlugParams {
875
+ AgentID : agent .ID ,
876
+ Slug : slug ,
877
+ })
878
+ if err != nil {
879
+ continue
880
+ }
881
+ return app , nil
882
+ }
883
+ }
884
+
885
+ return database.WorkspaceApp {}, sql .ErrNoRows
886
+ }
887
+
802
888
func (* FakeQuerier ) AcquireLock (_ context.Context , _ int64 ) error {
803
889
return xerrors .New ("AcquireLock must only be called within a transaction" )
804
890
}
@@ -2892,119 +2978,214 @@ func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.G
2892
2978
q .mutex .RLock ()
2893
2979
defer q .mutex .RUnlock ()
2894
2980
2895
- type appKey struct {
2896
- AccessMethod string
2897
- SlugOrPort string
2898
- Slug string
2899
- DisplayName string
2900
- Icon string
2981
+ /*
2982
+ WITH
2983
+ */
2984
+
2985
+ /*
2986
+ app_insights AS (
2987
+ SELECT
2988
+ tus.user_id,
2989
+ array_agg(DISTINCT tus.template_id)::uuid[] AS template_ids,
2990
+ app_usage.key::text AS app_name,
2991
+ COALESCE(wa.display_name, '') AS display_name,
2992
+ COALESCE(wa.icon, '') AS icon,
2993
+ (wa.slug IS NOT NULL)::boolean AS is_app,
2994
+ LEAST(SUM(app_usage.value::int), 30) AS app_usage_mins
2995
+ FROM
2996
+ template_usage_stats AS tus, jsonb_each(app_usage_mins) AS app_usage
2997
+ LEFT JOIN LATERAL (
2998
+ -- Fetch the latest app info for each app based on slug and template.
2999
+ SELECT
3000
+ app.display_name,
3001
+ app.icon,
3002
+ app.slug
3003
+ FROM
3004
+ workspace_apps AS app
3005
+ JOIN
3006
+ workspace_agents AS agent
3007
+ ON
3008
+ agent.id = app.agent_id
3009
+ JOIN
3010
+ workspace_resources AS resource
3011
+ ON
3012
+ resource.id = agent.resource_id
3013
+ JOIN
3014
+ workspace_builds AS build
3015
+ ON
3016
+ build.job_id = resource.job_id
3017
+ JOIN
3018
+ workspaces AS workspace
3019
+ ON
3020
+ workspace.id = build.workspace_id
3021
+ WHERE
3022
+ -- Requires lateral join.
3023
+ app.slug = app_usage.key
3024
+ AND workspace.owner_id = tus.user_id
3025
+ AND workspace.template_id = tus.template_id
3026
+ ORDER BY
3027
+ app.created_at DESC
3028
+ LIMIT 1
3029
+ ) AS wa
3030
+ ON
3031
+ true
3032
+ WHERE
3033
+ tus.start_time >= @start_time::timestamptz
3034
+ AND tus.end_time <= @end_time::timestamptz
3035
+ AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN tus.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
3036
+ GROUP BY
3037
+ tus.start_time, tus.user_id, app_usage.key::text, wa.display_name, wa.icon, wa.slug
3038
+ ),
3039
+ */
3040
+
3041
+ type appInsightsGroupBy struct {
3042
+ StartTime time.Time
3043
+ UserID uuid.UUID
3044
+ AppName string
3045
+ DisplayName string
3046
+ Icon string
3047
+ IsApp bool
2901
3048
}
2902
- type uniqueKey struct {
2903
- TemplateID uuid.UUID
2904
- UserID uuid.UUID
2905
- AgentID uuid.UUID
2906
- AppKey appKey
3049
+ type appInsightsRow struct {
3050
+ appInsightsGroupBy
3051
+ TemplateIDs []uuid.UUID
3052
+ AppUsageMins int64
2907
3053
}
2908
-
2909
- appUsageIntervalsByUserAgentApp := make (map [uniqueKey ]map [time.Time ]int64 )
2910
- for _ , s := range q .workspaceAppStats {
2911
- // (was.session_started_at >= ts.from_ AND was.session_started_at < ts.to_)
2912
- // OR (was.session_ended_at > ts.from_ AND was.session_ended_at < ts.to_)
2913
- // OR (was.session_started_at < ts.from_ AND was.session_ended_at >= ts.to_)
2914
- if ! (((s .SessionStartedAt .After (arg .StartTime ) || s .SessionStartedAt .Equal (arg .StartTime )) && s .SessionStartedAt .Before (arg .EndTime )) ||
2915
- (s .SessionEndedAt .After (arg .StartTime ) && s .SessionEndedAt .Before (arg .EndTime )) ||
2916
- (s .SessionStartedAt .Before (arg .StartTime ) && (s .SessionEndedAt .After (arg .EndTime ) || s .SessionEndedAt .Equal (arg .EndTime )))) {
3054
+ appInsightRows := make (map [appInsightsGroupBy ]appInsightsRow )
3055
+ // FROM
3056
+ for _ , stat := range q .templateUsageStats {
3057
+ // WHERE
3058
+ if stat .StartTime .Before (arg .StartTime ) || stat .StartTime .After (arg .EndTime ) {
2917
3059
continue
2918
3060
}
2919
-
2920
- w , err := q .getWorkspaceByIDNoLock (ctx , s .WorkspaceID )
2921
- if err != nil {
2922
- return nil , err
2923
- }
2924
-
2925
- if len (arg .TemplateIDs ) > 0 && ! slices .Contains (arg .TemplateIDs , w .TemplateID ) {
3061
+ if len (arg .TemplateIDs ) > 0 && ! slices .Contains (arg .TemplateIDs , stat .TemplateID ) {
2926
3062
continue
2927
3063
}
2928
3064
2929
- app , _ := q . getWorkspaceAppByAgentIDAndSlugNoLock ( ctx , database. GetWorkspaceAppByAgentIDAndSlugParams {
2930
- AgentID : s . AgentID ,
2931
- Slug : s . SlugOrPort ,
2932
- } )
3065
+ // json_each
3066
+ for appName , appUsage := range stat . AppUsageMins {
3067
+ // LEFT JOIN LATERAL
3068
+ app , _ := q . getLatestWorkspaceAppByTemplateIDUserIDSlugNoLock ( ctx , stat . TemplateID , stat . UserID , appName )
2933
3069
2934
- key := uniqueKey {
2935
- TemplateID : w .TemplateID ,
2936
- UserID : s .UserID ,
2937
- AgentID : s .AgentID ,
2938
- AppKey : appKey {
2939
- AccessMethod : s .AccessMethod ,
2940
- SlugOrPort : s .SlugOrPort ,
2941
- Slug : app .Slug ,
2942
- DisplayName : app .DisplayName ,
2943
- Icon : app .Icon ,
2944
- },
2945
- }
2946
- if appUsageIntervalsByUserAgentApp [key ] == nil {
2947
- appUsageIntervalsByUserAgentApp [key ] = make (map [time.Time ]int64 )
3070
+ // SELECT
3071
+ key := appInsightsGroupBy {
3072
+ StartTime : stat .StartTime ,
3073
+ UserID : stat .UserID ,
3074
+ AppName : appName ,
3075
+ DisplayName : app .DisplayName ,
3076
+ Icon : app .Icon ,
3077
+ IsApp : app .Slug != "" ,
3078
+ }
3079
+ row , ok := appInsightRows [key ]
3080
+ if ! ok {
3081
+ row = appInsightsRow {
3082
+ appInsightsGroupBy : key ,
3083
+ }
3084
+ }
3085
+ row .TemplateIDs = append (row .TemplateIDs , stat .TemplateID )
3086
+ row .AppUsageMins = least (row .AppUsageMins + appUsage , 30 )
3087
+ appInsightRows [key ] = row
2948
3088
}
3089
+ }
2949
3090
2950
- t := s .SessionStartedAt .Truncate (5 * time .Minute )
2951
- if t .Before (arg .StartTime ) {
2952
- t = arg .StartTime
2953
- }
2954
- for t .Before (s .SessionEndedAt ) && t .Before (arg .EndTime ) {
2955
- appUsageIntervalsByUserAgentApp [key ][t ] = 60 // 1 minute.
2956
- t = t .Add (1 * time .Minute )
2957
- }
3091
+ /*
3092
+ templates AS (
3093
+ SELECT
3094
+ app_name,
3095
+ display_name,
3096
+ icon,
3097
+ is_app,
3098
+ array_agg(DISTINCT template_id)::uuid[] AS template_ids
3099
+ FROM
3100
+ app_insights, unnest(template_ids) AS template_id
3101
+ GROUP BY
3102
+ app_name, display_name, icon, is_app
3103
+ )
3104
+ */
3105
+
3106
+ type appGroupBy struct {
3107
+ AppName string
3108
+ DisplayName string
3109
+ Icon string
3110
+ IsApp bool
3111
+ }
3112
+ type templateRow struct {
3113
+ appGroupBy
3114
+ TemplateIDs []uuid.UUID
2958
3115
}
2959
3116
2960
- appUsageTemplateIDs := make (map [appKey ]map [uuid.UUID ]struct {})
2961
- appUsageUserIDs := make (map [appKey ]map [uuid.UUID ]struct {})
2962
- appUsage := make (map [appKey ]int64 )
2963
- for uniqueKey , usage := range appUsageIntervalsByUserAgentApp {
2964
- for _ , seconds := range usage {
2965
- if appUsageTemplateIDs [uniqueKey .AppKey ] == nil {
2966
- appUsageTemplateIDs [uniqueKey .AppKey ] = make (map [uuid.UUID ]struct {})
2967
- }
2968
- appUsageTemplateIDs [uniqueKey.AppKey ][uniqueKey.TemplateID ] = struct {}{}
2969
- if appUsageUserIDs [uniqueKey .AppKey ] == nil {
2970
- appUsageUserIDs [uniqueKey .AppKey ] = make (map [uuid.UUID ]struct {})
3117
+ templateRows := make (map [appGroupBy ]templateRow )
3118
+ for _ , row := range appInsightRows {
3119
+ key := appGroupBy {
3120
+ AppName : row .AppName ,
3121
+ DisplayName : row .DisplayName ,
3122
+ Icon : row .Icon ,
3123
+ IsApp : row .IsApp ,
3124
+ }
3125
+ row , ok := templateRows [key ]
3126
+ if ! ok {
3127
+ row = templateRow {
3128
+ appGroupBy : key ,
2971
3129
}
2972
- appUsageUserIDs [uniqueKey.AppKey ][uniqueKey.UserID ] = struct {}{}
2973
- appUsage [uniqueKey .AppKey ] += seconds
2974
3130
}
3131
+ row .TemplateIDs = append (row .TemplateIDs , row .TemplateIDs ... )
3132
+ templateRows [key ] = row
2975
3133
}
2976
3134
2977
- var rows []database.GetTemplateAppInsightsRow
2978
- for appKey , usage := range appUsage {
2979
- templateIDs := make ([]uuid.UUID , 0 , len (appUsageTemplateIDs [appKey ]))
2980
- for templateID := range appUsageTemplateIDs [appKey ] {
2981
- templateIDs = append (templateIDs , templateID )
2982
- }
2983
- slices .SortFunc (templateIDs , func (a , b uuid.UUID ) int {
2984
- return slice .Ascending (a .String (), b .String ())
2985
- })
2986
- activeUserIDs := make ([]uuid.UUID , 0 , len (appUsageUserIDs [appKey ]))
2987
- for userID := range appUsageUserIDs [appKey ] {
2988
- activeUserIDs = append (activeUserIDs , userID )
3135
+ /*
3136
+ SELECT
3137
+ t.template_ids,
3138
+ array_agg(DISTINCT ai.user_id)::uuid[] AS active_user_ids,
3139
+ ai.app_name AS slug_or_port,
3140
+ ai.display_name,
3141
+ ai.icon,
3142
+ ai.is_app,
3143
+ (SUM(ai.app_usage_mins) * 60)::bigint AS usage_seconds
3144
+ FROM
3145
+ app_insights AS ai
3146
+ JOIN
3147
+ templates AS t
3148
+ ON
3149
+ ai.app_name = t.app_name
3150
+ AND ai.display_name = t.display_name
3151
+ AND ai.icon = t.icon
3152
+ AND ai.is_app = t.is_app
3153
+ GROUP BY
3154
+ t.template_ids, ai.app_name, ai.display_name, ai.icon, ai.is_app;
3155
+ */
3156
+
3157
+ type templateAppInsightsRow struct {
3158
+ TemplateIDs []uuid.UUID
3159
+ ActiveUserIDs []uuid.UUID
3160
+ UsageSeconds int64
3161
+ }
3162
+ groupedRows := make (map [appGroupBy ]templateAppInsightsRow )
3163
+ for _ , aiRow := range appInsightRows {
3164
+ key := appGroupBy {
3165
+ AppName : aiRow .AppName ,
3166
+ DisplayName : aiRow .DisplayName ,
3167
+ Icon : aiRow .Icon ,
3168
+ IsApp : aiRow .IsApp ,
2989
3169
}
2990
- slices .SortFunc (activeUserIDs , func (a , b uuid.UUID ) int {
2991
- return slice .Ascending (a .String (), b .String ())
2992
- })
3170
+ row := groupedRows [key ]
3171
+ row .TemplateIDs = append (row .TemplateIDs , aiRow .TemplateIDs ... )
3172
+ row .ActiveUserIDs = append (row .ActiveUserIDs , aiRow .UserID )
3173
+ row .UsageSeconds += aiRow .AppUsageMins * 60
3174
+ }
2993
3175
3176
+ var rows []database.GetTemplateAppInsightsRow
3177
+ for k , gr := range groupedRows {
2994
3178
rows = append (rows , database.GetTemplateAppInsightsRow {
2995
- TemplateIDs : templateIDs ,
2996
- ActiveUserIDs : activeUserIDs ,
2997
- AccessMethod : appKey .AccessMethod ,
2998
- SlugOrPort : appKey .SlugOrPort ,
2999
- DisplayName : appKey .DisplayName ,
3000
- Icon : appKey .Icon ,
3001
- IsApp : appKey .Slug != "" ,
3002
- UsageSeconds : usage ,
3179
+ TemplateIDs : uniqueSortedUUIDs (gr .TemplateIDs ),
3180
+ ActiveUsers : int64 (len (uniqueSortedUUIDs (gr .ActiveUserIDs ))),
3181
+ DisplayName : k .DisplayName ,
3182
+ SlugOrPort : k .AppName ,
3183
+ UsageSeconds : gr .UsageSeconds ,
3003
3184
})
3004
3185
}
3005
3186
3006
3187
// NOTE(mafredri): Add sorting if we decide on how to handle PostgreSQL collations.
3007
- // ORDER BY access_method, slug_or_port, display_name, icon, is_app
3188
+ // ORDER BY slug_or_port, display_name, icon, is_app
3008
3189
return rows , nil
3009
3190
}
3010
3191
0 commit comments