Skip to content

Commit

Permalink
Add timers to data access layer methods (#56)
Browse files Browse the repository at this point in the history
* add timers

* move code over

* typo
  • Loading branch information
owens1127 authored Jun 23, 2024
1 parent 999cdd0 commit 2ac5222
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 136 deletions.
85 changes: 49 additions & 36 deletions src/data-access-layer/history.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { InstanceForPlayer } from "../schema/components/InstanceForPlayer"
import { postgres } from "../services/postgres"
import { activityHistoryQueryTimer } from "../services/prometheus/metrics"
import { withTimer } from "../services/prometheus/util"

export const getActivities = async (
membershipId: bigint | string,
Expand All @@ -13,42 +15,53 @@ export const getActivities = async (
cursor?: Date
}
) => {
return await postgres.queryRows<InstanceForPlayer>(
`SELECT
instance_id::text AS "instanceId",
hash::text AS "hash",
activity_id AS "activityId",
version_id AS "versionId",
activity.completed AS "completed",
player_count AS "playerCount",
score AS "score",
fresh AS "fresh",
flawless AS "flawless",
date_started AS "dateStarted",
date_completed AS "dateCompleted",
duration AS "duration",
platform_type AS "platformType",
date_completed < COALESCE(day_one_end, TIMESTAMP 'epoch') AS "isDayOne",
date_completed < COALESCE(contest_end, TIMESTAMP 'epoch') AS "isContest",
date_completed < COALESCE(week_one_end, TIMESTAMP 'epoch') AS "isWeekOne",
JSONB_BUILD_OBJECT(
'completed', activity_player.completed,
'sherpas', activity_player.sherpas,
'isFirstClear', activity_player.is_first_clear,
'timePlayedSeconds', activity_player.time_played_seconds
) as player
FROM activity_player
INNER JOIN activity USING (instance_id)
INNER JOIN activity_hash USING (hash)
INNER JOIN activity_definition ON activity_definition.id = activity_hash.activity_id
WHERE membership_id = $1::bigint
AND date_completed < $2
${cutoff ? "AND date_completed > $4" : ""}
ORDER BY date_completed DESC
LIMIT $3;`,
return await withTimer(
activityHistoryQueryTimer,
{
params: cutoff ? [membershipId, cursor, count, cutoff] : [membershipId, cursor, count],
fetchCount: count
}
count: count,
cursor: String(cursor.getTime() !== 0),
cutoff: String(!!cutoff)
},
() =>
postgres.queryRows<InstanceForPlayer>(
`SELECT
instance_id::text AS "instanceId",
hash::text AS "hash",
activity_id AS "activityId",
version_id AS "versionId",
activity.completed AS "completed",
player_count AS "playerCount",
score AS "score",
fresh AS "fresh",
flawless AS "flawless",
date_started AS "dateStarted",
date_completed AS "dateCompleted",
duration AS "duration",
platform_type AS "platformType",
date_completed < COALESCE(day_one_end, TIMESTAMP 'epoch') AS "isDayOne",
date_completed < COALESCE(contest_end, TIMESTAMP 'epoch') AS "isContest",
date_completed < COALESCE(week_one_end, TIMESTAMP 'epoch') AS "isWeekOne",
JSONB_BUILD_OBJECT(
'completed', activity_player.completed,
'sherpas', activity_player.sherpas,
'isFirstClear', activity_player.is_first_clear,
'timePlayedSeconds', activity_player.time_played_seconds
) as player
FROM activity_player
INNER JOIN activity USING (instance_id)
INNER JOIN activity_hash USING (hash)
INNER JOIN activity_definition ON activity_definition.id = activity_hash.activity_id
WHERE membership_id = $1::bigint
AND date_completed < $2
${cutoff ? "AND date_completed > $4" : ""}
ORDER BY date_completed DESC
LIMIT $3;`,
{
params: cutoff
? [membershipId, cursor, count, cutoff]
: [membershipId, cursor, count],
fetchCount: count
}
)
)
}
221 changes: 122 additions & 99 deletions src/data-access-layer/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
WorldFirstEntry
} from "../schema/components/PlayerProfile"
import { postgres } from "../services/postgres"
import { playerProfileQueryTimer } from "../services/prometheus/metrics"
import { withTimer } from "../services/prometheus/util"

export const getPlayer = async (membershipId: bigint | string) => {
return await postgres.queryRow<PlayerInfo>(
Expand All @@ -25,115 +27,136 @@ export const getPlayer = async (membershipId: bigint | string) => {
)
}
export const getPlayerActivityStats = async (membershipId: bigint | string) => {
return await postgres.queryRows<PlayerProfileActivityStats>(
`SELECT
activity_definition.id AS "activityId",
COALESCE(player_stats.fresh_clears, 0) AS "freshClears",
COALESCE(player_stats.clears, 0) AS "clears",
COALESCE(player_stats.sherpas, 0) AS "sherpas",
COALESCE(player_stats.trios, 0) AS "trios",
COALESCE(player_stats.duos, 0) AS "duos",
COALESCE(player_stats.solos, 0) AS "solos",
CASE WHEN fastest_instance_id IS NOT NULL
THEN JSONB_BUILD_OBJECT(
'instanceId', fastest.instance_id::text,
'hash', fastest.hash::text,
'activityId', fastest_ah.activity_id,
'versionId', fastest_ah.version_id,
'completed', fastest.completed,
'playerCount', fastest.player_count,
'score', fastest.score,
'fresh', fastest.fresh,
'flawless', fastest.flawless,
'dateStarted', fastest.date_started,
'dateCompleted', fastest.date_completed,
'duration', fastest.duration,
'platformType', fastest.platform_type,
'isDayOne', date_completed < COALESCE(day_one_end, TIMESTAMP 'epoch'),
'isContest', date_completed < COALESCE(contest_end, TIMESTAMP 'epoch'),
'isWeekOne', date_completed < COALESCE(week_one_end, TIMESTAMP 'epoch')
)
ELSE NULL
END as "fastestInstance"
FROM activity_definition
LEFT JOIN player_stats ON activity_definition.id = player_stats.activity_id
AND player_stats.membership_id = $1::bigint
LEFT JOIN activity fastest ON player_stats.fastest_instance_id = fastest.instance_id
LEFT JOIN activity_hash fastest_ah ON fastest.hash = fastest_ah.hash
ORDER BY activity_definition.id`,
return await withTimer(
playerProfileQueryTimer,
{
params: [membershipId],
fetchCount: 100
}
method: "getPlayerActivityStats"
},
() =>
postgres.queryRows<PlayerProfileActivityStats>(
`SELECT
activity_definition.id AS "activityId",
COALESCE(player_stats.fresh_clears, 0) AS "freshClears",
COALESCE(player_stats.clears, 0) AS "clears",
COALESCE(player_stats.sherpas, 0) AS "sherpas",
COALESCE(player_stats.trios, 0) AS "trios",
COALESCE(player_stats.duos, 0) AS "duos",
COALESCE(player_stats.solos, 0) AS "solos",
CASE WHEN fastest_instance_id IS NOT NULL
THEN JSONB_BUILD_OBJECT(
'instanceId', fastest.instance_id::text,
'hash', fastest.hash::text,
'activityId', fastest_ah.activity_id,
'versionId', fastest_ah.version_id,
'completed', fastest.completed,
'playerCount', fastest.player_count,
'score', fastest.score,
'fresh', fastest.fresh,
'flawless', fastest.flawless,
'dateStarted', fastest.date_started,
'dateCompleted', fastest.date_completed,
'duration', fastest.duration,
'platformType', fastest.platform_type,
'isDayOne', date_completed < COALESCE(day_one_end, TIMESTAMP 'epoch'),
'isContest', date_completed < COALESCE(contest_end, TIMESTAMP 'epoch'),
'isWeekOne', date_completed < COALESCE(week_one_end, TIMESTAMP 'epoch')
)
ELSE NULL
END as "fastestInstance"
FROM activity_definition
LEFT JOIN player_stats ON activity_definition.id = player_stats.activity_id
AND player_stats.membership_id = $1::bigint
LEFT JOIN activity fastest ON player_stats.fastest_instance_id = fastest.instance_id
LEFT JOIN activity_hash fastest_ah ON fastest.hash = fastest_ah.hash
ORDER BY activity_definition.id`,
{
params: [membershipId],
fetchCount: 100
}
)
)
}

export const getPlayerGlobalStats = async (membershipId: bigint | string) => {
return await postgres.queryRow<PlayerProfileGlobalStats>(
`SELECT
JSONB_BUILD_OBJECT(
'value', clears,
'rank', clears_rank
) AS "clears",
JSONB_BUILD_OBJECT(
'value', fresh_clears,
'rank', fresh_clears_rank
) AS "freshClears",
JSONB_BUILD_OBJECT(
'value', sherpas,
'rank', sherpas_rank
) AS "sherpas",
CASE WHEN speed IS NOT NULL THEN JSONB_BUILD_OBJECT(
'value', speed,
'rank', speed_rank
) ELSE NULL END AS "sumOfBest"
FROM individual_global_leaderboard
WHERE membership_id = $1::bigint`,
return await withTimer(
playerProfileQueryTimer,
{
params: [membershipId]
}
method: "getPlayerGlobalStats"
},
() =>
postgres.queryRow<PlayerProfileGlobalStats>(
`SELECT
JSONB_BUILD_OBJECT(
'value', clears,
'rank', clears_rank
) AS "clears",
JSONB_BUILD_OBJECT(
'value', fresh_clears,
'rank', fresh_clears_rank
) AS "freshClears",
JSONB_BUILD_OBJECT(
'value', sherpas,
'rank', sherpas_rank
) AS "sherpas",
CASE WHEN speed IS NOT NULL THEN JSONB_BUILD_OBJECT(
'value', speed,
'rank', speed_rank
) ELSE NULL END AS "sumOfBest"
FROM individual_global_leaderboard
WHERE membership_id = $1::bigint`,
{
params: [membershipId]
}
)
)
}

export const getWorldFirstEntries = async (membershipId: bigint | string) => {
return await postgres.queryRows<
| WorldFirstEntry
| {
activityId: bigint
rank: null
instanceId: null
timeAfterLaunch: null
isDayOne: boolean
isContest: boolean
isWeekOne: boolean
isChallengeMode: boolean
}
>(
`
SELECT
activity_definition.id AS "activityId",
rank,
instance_id::text AS "instanceId",
time_after_launch AS "timeAfterLaunch",
(CASE WHEN instance_id IS NOT NULL THEN date_completed < COALESCE(day_one_end, TIMESTAMP 'epoch') ELSE false END) AS "isDayOne",
(CASE WHEN instance_id IS NOT NULL THEN date_completed < COALESCE(contest_end, TIMESTAMP 'epoch') ELSE false END) AS "isContest",
(CASE WHEN instance_id IS NOT NULL THEN date_completed < COALESCE(week_one_end, TIMESTAMP 'epoch') ELSE false END) AS "isWeekOne",
COALESCE(is_challenge_mode, false) AS "isChallengeMode"
FROM activity_definition
LEFT JOIN LATERAL (
SELECT instance_id, time_after_launch, date_completed, rank, is_challenge_mode
FROM world_first_contest_leaderboard
WHERE activity_id = activity_definition.id
AND membership_ids @> $1::jsonb
AND rank <= 500
ORDER BY rank ASC
LIMIT 1
) AS "__inner__" ON true
WHERE is_raid = true
ORDER BY activity_definition.id ASC;`,
return await withTimer(
playerProfileQueryTimer,
{
params: [`${[membershipId]}`],
fetchCount: 100
}
method: "getWorldFirstEntries"
},
() =>
postgres.queryRows<
| WorldFirstEntry
| {
activityId: bigint
rank: null
instanceId: null
timeAfterLaunch: null
isDayOne: boolean
isContest: boolean
isWeekOne: boolean
isChallengeMode: boolean
}
>(
`
SELECT
activity_definition.id AS "activityId",
rank,
instance_id::text AS "instanceId",
time_after_launch AS "timeAfterLaunch",
(CASE WHEN instance_id IS NOT NULL THEN date_completed < COALESCE(day_one_end, TIMESTAMP 'epoch') ELSE false END) AS "isDayOne",
(CASE WHEN instance_id IS NOT NULL THEN date_completed < COALESCE(contest_end, TIMESTAMP 'epoch') ELSE false END) AS "isContest",
(CASE WHEN instance_id IS NOT NULL THEN date_completed < COALESCE(week_one_end, TIMESTAMP 'epoch') ELSE false END) AS "isWeekOne",
COALESCE(is_challenge_mode, false) AS "isChallengeMode"
FROM activity_definition
LEFT JOIN LATERAL (
SELECT instance_id, time_after_launch, date_completed, rank, is_challenge_mode
FROM world_first_contest_leaderboard
WHERE activity_id = activity_definition.id
AND membership_ids @> $1::jsonb
AND rank <= 500
ORDER BY rank ASC
LIMIT 1
) AS "__inner__" ON true
WHERE is_raid = true
ORDER BY activity_definition.id ASC;`,
{
params: [`${[membershipId]}`],
fetchCount: 100
}
)
)
}
1 change: 1 addition & 0 deletions src/routes/player/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Results are ordered by a combination of the number of raid completions and last
membershipType: membershipType === -1 ? undefined : membershipType,
global
})

return RaidHubRoute.ok({
params: { count, query: searchTerm },
results
Expand Down
15 changes: 14 additions & 1 deletion src/services/prometheus/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ export const httpRequestTimer = new Histogram({
name: "incoming_api_request_duration_ms",
help: "Duration of HTTP requests in ms",
labelNames: ["path", "status_code"],
// buckets for response time from 0.1ms to 10s
buckets: [0.1, 1, 5, 15, 50, 100, 200, 300, 400, 500, 1000, 2000, 5000, 10000]
})

export const activityHistoryQueryTimer = new Histogram({
name: "activity_history_query_duration_ms",
help: "Duration of activity history queries in ms",
labelNames: ["count", "cutoff", "cursor"],
buckets: [0.1, 0.5, 1, 5, 10, 50, 100, 250, 500, 1000, 5000, 10000]
})

export const playerProfileQueryTimer = new Histogram({
name: "player_profile_query_duration_ms",
help: "Duration of player profile queries in ms",
labelNames: ["method"],
buckets: [0.1, 0.5, 1, 5, 10, 50, 100, 250, 500, 1000, 5000, 10000]
})
12 changes: 12 additions & 0 deletions src/services/prometheus/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Histogram } from "prom-client"

export const withTimer = async <K extends string, T>(
metric: Histogram<K>,
labels: Partial<Record<K, string | number>>,
fn: () => Promise<T>
) => {
const start = Date.now()
return await fn().finally(() => {
metric.observe(labels, Date.now() - start)
})
}

0 comments on commit 2ac5222

Please sign in to comment.