Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 42 additions & 16 deletions src/telemetry/telemetry.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ export class TelemetryService {
return (name: string) => (alias ? `${alias}.${name}` : name);
}

private static activeLineups(interval: string) {
const recent = `${TelemetryService.nativeMatch("m")}
AND m.effective_at >= now() - interval '${interval}'`;

return `SELECT m.lineup_1_id FROM public.matches m WHERE ${recent}
UNION
SELECT m.lineup_2_id FROM public.matches m WHERE ${recent}`;
}

constructor(
private readonly logger: Logger,
private readonly redisManagerService: RedisManagerService,
Expand Down Expand Up @@ -752,10 +761,28 @@ export class TelemetryService {
return row?.value;
}

// Telemetry is the least important thing a panel does, so it is never allowed
// to hold a connection open. SET LOCAL scopes the timeout to this transaction
// rather than leaking onto a pooled connection the rest of the app reuses. A
// timeout aborts the whole collection and send() skips the hour.
private static readonly CollectTimeoutMs = 15_000;

private async collectQuery<T>(sql: string): Promise<T> {
return await this.postgres.transaction(async (client) => {
await client.query(
`SET LOCAL statement_timeout = ${TelemetryService.CollectTimeoutMs}`,
);

const result = await client.query(sql);

return result.rows as T;
});
}

private async getSettings(): Promise<Map<string, string>> {
const rows = await this.postgres.query<
Array<{ name: string; value: string }>
>(`SELECT name, value FROM public.settings`);
const rows = await this.collectQuery<Array<{ name: string; value: string }>>(
`SELECT name, value FROM public.settings`,
);

return new Map(rows.map(({ name, value }) => [name, value]));
}
Expand All @@ -770,9 +797,7 @@ export class TelemetryService {
}

private async getMatchesByType(): Promise<Record<string, number>> {
const rows = await this.postgres.query<
Array<{ type: string; count: string }>
>(
const rows = await this.collectQuery<Array<{ type: string; count: string }>>(
`SELECT o.type, count(*) AS count
FROM public.matches m
JOIN public.match_options o ON o.id = m.match_options_id
Expand All @@ -784,7 +809,7 @@ export class TelemetryService {
}

private async getMatchesBySource(): Promise<Record<string, number>> {
const rows = await this.postgres.query<
const rows = await this.collectQuery<
Array<{ source: string; count: string }>
>(
`SELECT source, count(*) AS count
Expand Down Expand Up @@ -814,7 +839,7 @@ export class TelemetryService {
}

private async getCounts(): Promise<TelemetryCounts> {
const [row] = await this.postgres.query<Array<TelemetryCounts>>(
const [row] = await this.collectQuery<Array<TelemetryCounts>>(
`SELECT
(SELECT min(created_at) FROM public.matches) AS installed_at,

Expand Down Expand Up @@ -883,20 +908,21 @@ export class TelemetryService {

(SELECT count(*) FROM public.players) AS players_registered,
(SELECT count(*) FROM public.teams) AS teams_total,
-- An OR across both lineup columns cannot use an index and forces a
-- join over every lineup row. Feeding the two sides in separately lets
-- this ride the (match_lineup_id, steam_id) unique index instead.
(SELECT count(DISTINCT p.steam_id)
FROM public.match_lineup_players p
JOIN public.matches m
ON m.lineup_1_id = p.match_lineup_id OR m.lineup_2_id = p.match_lineup_id
WHERE p.steam_id IS NOT NULL
AND ${TelemetryService.nativeMatch("m")}
AND m.effective_at >= now() - interval '7 days') AS players_active_7d,
AND p.match_lineup_id IN (
${TelemetryService.activeLineups("7 days")}
)) AS players_active_7d,
(SELECT count(DISTINCT p.steam_id)
FROM public.match_lineup_players p
JOIN public.matches m
ON m.lineup_1_id = p.match_lineup_id OR m.lineup_2_id = p.match_lineup_id
WHERE p.steam_id IS NOT NULL
AND ${TelemetryService.nativeMatch("m")}
AND m.effective_at >= now() - interval '30 days') AS players_active_30d,
AND p.match_lineup_id IN (
${TelemetryService.activeLineups("30 days")}
)) AS players_active_30d,

(SELECT count(*) FROM public.tournaments) AS tournaments,
(SELECT count(*) FROM public.league_seasons) AS league_seasons,
Expand Down
18 changes: 10 additions & 8 deletions test/telemetry-perf.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Logger } from "@nestjs/common";

const report = (line: string) => process.stdout.write(`PERF ${line}\n`);
import { PostgresService } from "./../src/postgres/postgres.service";
import { bootMigratedDb, SqlTestDb } from "./utils/sql-test-db";
import { TelemetryService } from "./../src/telemetry/telemetry.service";
Expand Down Expand Up @@ -44,7 +46,7 @@ benchmark("telemetry perf", () => {

const MATCHES = 20000;

console.log(`seeding ${MATCHES} matches...`);
report(`seeding ${MATCHES} matches...`);
const started = Date.now();

await postgres.query(
Expand Down Expand Up @@ -102,15 +104,15 @@ benchmark("telemetry perf", () => {
FROM matches m`,
);

console.log(`seeded in ${Date.now() - started}ms`);
report(`seeded in ${Date.now() - started}ms`);

const counts = await postgres.query<Array<Record<string, string>>>(
`SELECT
(SELECT count(*) FROM matches) AS matches,
(SELECT count(*) FROM match_lineup_players) AS lineup_players,
(SELECT count(*) FROM match_maps) AS maps`,
);
console.log("row counts:", counts[0]);
report(`row counts: ${JSON.stringify(counts[0])}`);

await postgres.query("ANALYZE");
}, 900_000);
Expand All @@ -123,7 +125,7 @@ benchmark("telemetry perf", () => {
for (let i = 0; i < 3; i++) {
const started = Date.now();
await service.collect();
console.log(`collect() run ${i + 1}: ${Date.now() - started}ms`);
report(`collect() run ${i + 1}: ${Date.now() - started}ms`);
}
}, 300_000);

Expand All @@ -138,12 +140,12 @@ benchmark("telemetry perf", () => {

const started = Date.now();
await postgres.query(sql);
console.log(`OR-join active players: ${Date.now() - started}ms`);
report(`OR-join active players: ${Date.now() - started}ms`);

const plan = await postgres.query<Array<Record<string, string>>>(
`EXPLAIN (ANALYZE, BUFFERS) ${sql}`,
);
console.log(plan.map((r) => r["QUERY PLAN"]).join("\n"));
report(plan.map((r) => r["QUERY PLAN"]).join("\n"));
}, 300_000);

it("times an IN-rewrite of the same thing", async () => {
Expand All @@ -160,11 +162,11 @@ benchmark("telemetry perf", () => {

const started = Date.now();
await postgres.query(sql);
console.log(`IN-rewrite active players: ${Date.now() - started}ms`);
report(`IN-rewrite active players: ${Date.now() - started}ms`);

const plan = await postgres.query<Array<Record<string, string>>>(
`EXPLAIN (ANALYZE, BUFFERS) ${sql}`,
);
console.log(plan.map((r) => r["QUERY PLAN"]).join("\n"));
report(plan.map((r) => r["QUERY PLAN"]).join("\n"));
}, 300_000);
});
Loading