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
26 changes: 25 additions & 1 deletion svc/api/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1430,6 +1430,17 @@ You can use the API without a key, but registering for a key allows increased ra
type: "integer",
},
},
{
name: "role",
in: "query",
description:
"Benchmark against cores or supports only, estimated from farm within each team. Can be combined with bracket. Omit for all roles",
required: false,
schema: {
type: "string",
enum: ["core", "support"],
},
},
],
responses: {
200: {
Expand Down Expand Up @@ -1457,7 +1468,20 @@ You can use the API without a key, but registering for a key allows increased ra
.json({ error: "bracket must be an integer between 1 and 8" });
}
}
const result = await getHeroBenchmarks(req.query.hero_id, bracket);
let role: string | null = null;
if (req.query.role !== undefined) {
role = String(req.query.role);
if (role !== "core" && role !== "support") {
return res
.status(400)
.json({ error: "role must be core or support" });
}
}
const result = await getHeroBenchmarks(
req.query.hero_id,
bracket,
role,
);
return res.json(result);
},
},
Expand Down
32 changes: 31 additions & 1 deletion svc/counts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,23 @@ runReliableQueue("counts", 2, async (job: CountsJob, metadata) => {
const turbo = isTurbo(match);
// 1 to 8 based on the average rank of the match, as in updateHeroCounts
const rank = !turbo && avg ? Math.floor(avg / 10) : null;
// Rank each team by gold_per_min and call the top 3 cores, the rest
// supports. The exact 1-5 position needs parsed data, but this split is
// right for 99.4% of players using api data alone (see #2980).
const roles = new Map<number, string>();
if (!turbo) {
[true, false].forEach((radiant) => {
const team = match.players.filter((p) => isRadiant(p) === radiant);
if (team.length === 5) {
team
.slice()
.sort((a, b) => (b.gold_per_min ?? 0) - (a.gold_per_min ?? 0))
.forEach((p, i) =>
roles.set(p.player_slot, i < 3 ? "core" : "support"),
);
}
});
}
if (
match.match_id % 100 < Number(config.BENCHMARKS_SAMPLE_PERCENT) &&
(isSignificant(match) || turbo)
Expand Down Expand Up @@ -244,10 +261,23 @@ runReliableQueue("counts", 2, async (job: CountsJob, metadata) => {
2,
);
redis.expireat(rkey, expiretime);
// Filtered sets keep both slots so every key has the same
// shape, with an empty segment for the filter not in use.
const role = roles.get(p.player_slot);
if (role) {
const roleKey = `${rkey}::${role}`;
redis.zadd(roleKey, metric, match.match_id);
redis.expireat(roleKey, expiretime);
}
if (rank) {
const rankKey = `${rkey}:${rank}`;
const rankKey = `${rkey}:${rank}:`;
redis.zadd(rankKey, metric, match.match_id);
redis.expireat(rankKey, expiretime);
if (role) {
const rankRoleKey = `${rkey}:${rank}:${role}`;
redis.zadd(rankRoleKey, metric, match.match_id);
redis.expireat(rankRoleKey, expiretime);
}
}
}
});
Expand Down
17 changes: 14 additions & 3 deletions svc/util/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,22 @@ export async function getHeroItemPopularity(heroId: string) {
late_game_items: lateGameItems,
};
}
export async function getHeroBenchmarks(heroId: string, bracket?: number | null) {
export async function getHeroBenchmarks(
heroId: string,
bracket?: number | null,
role?: string | null,
) {
const ret: AnyDict = {};
const arr = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99];
// Bracketed sets add the 1-8 rank bracket after the global/turbo slot
const suffix = bracket ? `:${bracket}` : "";
// Key slots are benchmarks:<epoch>:<metric>:<hero>:<turbo>:<bracket>:<role>
// Filtered sets always carry both the bracket and role slots, empty for the
// ones not in use, and are only written for non-turbo matches:
// ...:<hero>::5: ...:<hero>:::core ...:<hero>::5:core

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's an extra colon in this comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's right, but only because the base key already ends in the empty turbo slot, which the comment didn't show. spelled the slots out in de4a1c1 so it reads without that context:

// Key slots are benchmarks:<epoch>:<metric>:<hero>:<turbo>:<bracket>:<role>

// The unfiltered key keeps no suffix at all so existing ones still resolve.
let suffix = "";
if (bracket || role) {
suffix = `:${bracket ?? ""}:${role ?? ""}`;
}
const items: [metric: string, percentile: number][] = [];
Object.keys(benchmarks).forEach((metric) => {
arr.forEach((percentile) => {
Expand Down
Loading