Skip to content

Commit 5727d2a

Browse files
committed
Cleaned up the API in Analytics to different namespaces
and documented the Analytics mechanism
1 parent 70f6257 commit 5727d2a

21 files changed

+275
-174
lines changed

dev.md

+41-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,47 @@ it creates new accounts based on initial setup specified by the schema.
242242

243243
### Analytics
244244

245-
<!-- TODO -->
245+
The Analytics module consists of two parts: Single and Top.
246+
247+
### Single metrics
248+
249+
`Analytics\Single` computes single-value metrics.
250+
251+
The `Analytics\Single\Query` interface abstracts different metric types
252+
parameterized by a generic parameter `P`.
253+
Use `CachedValue::loop` to spawn a refresh loop that fetches the latest metric value.
254+
If `P` is `Player`, use `PlayerInfoUpdater::registerListener()`
255+
to automatically spawn refresh loops for online players.
256+
257+
### Top metrics
258+
259+
`Analytics\Top` reports server-wide top metrics.
260+
261+
Due to the label-oriented mechanism,
262+
it is not possible to efficiently fetch the top accounts directly
263+
because the SQL database cannot be indexed by a specific label.
264+
To allow efficient top metric queries,
265+
the metric is first computed for each grouping label value
266+
(usually the player UUID) and cached in the `capital_analytics_top_cache` table.
267+
268+
A top metric query is defined by the following:
269+
270+
- The aggregator to use.
271+
This also defines whether the query operates on accounts or transactions.
272+
Currently all aggregators are accounts-only or transactions-only,
273+
but there will be aggregators on transactions for each account in the future.
274+
- The label selector that filters rows.
275+
For example, if the aggregator is about number of transactions of each player,
276+
the label selector filters away non-player accounts
277+
(it is not a transaction label selector).
278+
- The grouping label name, where its values will be used to group rows.
279+
For queries on top players, this is `AccountLabels::PLAYER_UUID`.
280+
281+
These three values uniquely identify a top query for computation cache.
282+
These values are md5-hashed into the `capital_analytics_top_cache.query` column,
283+
which are reused on multiple servers.
284+
The computation takes place in batches, updating a subset of label values each time.
285+
Call `Analytics\Top\Mod::runRefreshLoop()` to start a refreshing loop.
246286

247287
### Transfer
248288

phpstan-baseline.neon

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ parameters:
33
-
44
message: "#^While loop condition is always true\\.$#"
55
count: 1
6-
path: src/SOFe/Capital/Analytics/ConfigTop.php
6+
path: src/SOFe/Capital/Analytics/Top/Mod.php
77

88
-
99
message: "#^Method SOFe\\\\Capital\\\\Config\\\\Raw\\:\\:loadConfig\\(\\) should return T of SOFe\\\\Capital\\\\Config\\\\ConfigInterface but returns object\\.$#"

src/SOFe/Capital/Analytics/CachedSingleValue.php

-16
This file was deleted.

src/SOFe/Capital/Analytics/Config.php

+7-35
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ final class Config implements Singleton, FromContext, ConfigInterface {
2828
use SingletonArgs, SingletonTrait, ConfigTrait;
2929

3030
/**
31-
* @param array<string, PlayerInfosManager> $singleQueries
31+
* @param array<string, Single\PlayerInfoUpdater> $singleQueries
3232
* @param array<string, PlayerInfoCommand> $infoCommands
33-
* @param list<ConfigTop> $topQueries
33+
* @param list<Top\Config> $topQueries
3434
*/
3535
public function __construct(
3636
public array $singleQueries,
@@ -102,7 +102,7 @@ public static function parse(Parser $config, Context $di, Raw $raw) : Generator
102102

103103
foreach ($topPlayersConfig->getKeys() as $key) {
104104
$queryConfig = $topPlayersConfig->enter($key, null);
105-
$topQueries[] = self::parseTopPlayerQuery($queryConfig, $schema, $key);
105+
$topQueries[] = Top\Config::parse($queryConfig, $schema, $key);
106106
}
107107

108108
return new self(
@@ -112,7 +112,7 @@ public static function parse(Parser $config, Context $di, Raw $raw) : Generator
112112
);
113113
}
114114

115-
private static function parseSingleQuery(Parser $infoConfig, Schema\Schema $schema, string $infoName) : PlayerInfosManager {
115+
private static function parseSingleQuery(Parser $infoConfig, Schema\Schema $schema, string $infoName) : Single\PlayerInfoUpdater {
116116
$type = $infoConfig->expectString("of", "account", <<<'EOT'
117117
The data source of this info.
118118
If set to "account", the info is calculated from statistics of some of the player's accounts.
@@ -128,7 +128,7 @@ private static function parseSingleQuery(Parser $infoConfig, Schema\Schema $sche
128128

129129
$metric = AccountQueryMetric::parseConfig($infoConfig, "metric");
130130

131-
$query = new AccountSingleQuery($metric, fn(Player $player) => $infoSchema->getSelector($player));
131+
$query = new Single\AccountQuery($metric, fn(Player $player) => $infoSchema->getSelector($player));
132132
} elseif ($type === "transaction") {
133133
$selectorConfig = $infoConfig->enter("selector", "Filter transactions by labels");
134134
$labels = [];
@@ -139,7 +139,7 @@ private static function parseSingleQuery(Parser $infoConfig, Schema\Schema $sche
139139

140140
$metric = TransactionQueryMetric::parseConfig($infoConfig, "metric");
141141

142-
$query = new TransactionSingleQuery($metric, fn(Player $player) => $labels->transform(new PlayerInfo($player)));
142+
$query = new Single\TransactionQuery($metric, fn(Player $player) => $labels->transform(new PlayerInfo($player)));
143143
} else {
144144
throw new AssertionError("unreachable code");
145145
}
@@ -149,7 +149,7 @@ private static function parseSingleQuery(Parser $infoConfig, Schema\Schema $sche
149149
This will only affect displays and will not affect transactions.
150150
EOT) * 20.);
151151

152-
return new PlayerInfosManager($infoName, new CachedSingleQuery($query, $updateFrequencyTicks));
152+
return new Single\PlayerInfoUpdater($infoName, new Single\Cached($query, $updateFrequencyTicks));
153153
}
154154

155155
private static function parseInfoCommand(Parser $config, string $cmdName) : PlayerInfoCommand {
@@ -164,32 +164,4 @@ private static function parseInfoCommand(Parser $config, string $cmdName) : Play
164164

165165
return new PlayerInfoCommand($command, $template);
166166
}
167-
168-
private static function parseTopPlayerQuery(Parser $infoConfig, Schema\Schema $schema, string $cmdName) : ConfigTop {
169-
$cmdConfig = $infoConfig->enter("command", "The command that displays the information.");
170-
$command = DynamicCommand::parse($cmdConfig, "analytics.top", $cmdName, "Displays the richest player", false);
171-
172-
$queryArgs = TopQueryArgs::parse($infoConfig, $schema);
173-
174-
$refreshConfig = $infoConfig->enter("refresh", <<<'EOT'
175-
Refresh settings for the top query.
176-
These settings depend on how many active accounts you have in the database
177-
as well as how powerful the CPU of your database server is.
178-
Try increasing the frequencies and reducing batch size if the database server is lagging.
179-
EOT);
180-
$refreshArgs = TopRefreshArgs::parse($refreshConfig);
181-
182-
$paginationConfig = $infoConfig->enter("pagination", <<<'EOT'
183-
Pagination settings for the top query.
184-
EOT);
185-
$paginationArgs = TopPaginationArgs::parse($paginationConfig);
186-
187-
return new ConfigTop(
188-
command: $command,
189-
queryArgs: $queryArgs,
190-
refreshArgs: $refreshArgs,
191-
paginationArgs: $paginationArgs,
192-
messages: TopMessages::parse($infoConfig->enter("messages", "Configures the displayed messages")),
193-
);
194-
}
195167
}

src/SOFe/Capital/Analytics/Mod.php

+1-15
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,22 @@
44

55
namespace SOFe\Capital\Analytics;
66

7-
use SOFe\AwaitStd\AwaitStd;
8-
use SOFe\Capital\Database\Database;
97
use SOFe\Capital\Di\FromContext;
108
use SOFe\Capital\Di\Singleton;
119
use SOFe\Capital\Di\SingletonArgs;
1210
use SOFe\Capital\Di\SingletonTrait;
1311
use SOFe\Capital\Plugin\MainClass;
14-
use SOFe\InfoAPI\Info;
1512

1613
final class Mod implements Singleton, FromContext {
1714
use SingletonArgs, SingletonTrait;
1815

1916
public const API_VERSION = "0.1.0";
2017

21-
public static function fromSingletonArgs(Config $config, MainClass $plugin, AwaitStd $std, Database $db, DatabaseUtils $dbu) : self {
22-
Info::registerByReflection("capital.analytics.top", PaginationInfo::class);
23-
TopResultEntryInfo::initCommon();
24-
25-
foreach ($config->singleQueries as $manager) {
26-
$manager->register($plugin, $std, $db);
27-
}
28-
18+
public static function fromSingletonArgs(Config $config, MainClass $plugin, Single\Mod $_single, Top\Mod $_top) : self {
2919
foreach ($config->infoCommands as $cmd) {
3020
$cmd->register($plugin);
3121
}
3222

33-
foreach ($config->topQueries as $query) {
34-
$query->register($plugin, $std, $dbu);
35-
}
36-
3723
return new self;
3824
}
3925
}

src/SOFe/Capital/Analytics/AccountSingleQuery.php src/SOFe/Capital/Analytics/Single/AccountQuery.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace SOFe\Capital\Analytics;
5+
namespace SOFe\Capital\Analytics\Single;
66

77
use Closure;
88
use Generator;
@@ -12,9 +12,9 @@
1212

1313
/**
1414
* @template P
15-
* @implements SingleQuery<P>
15+
* @implements Query<P>
1616
*/
17-
final class AccountSingleQuery implements SingleQuery {
17+
final class AccountQuery implements Query {
1818
/**
1919
* @param Closure(P): LabelSelector $labelSelector
2020
*/

src/SOFe/Capital/Analytics/CachedSingleQuery.php src/SOFe/Capital/Analytics/Single/Cached.php

+5-4
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@
22

33
declare(strict_types=1);
44

5-
namespace SOFe\Capital\Analytics;
5+
namespace SOFe\Capital\Analytics\Single;
66

77
use function min;
88

99
/**
10+
* The configuration for a SingleQuery whose result is cached locally and refreshd periodically.
1011
* @template P
1112
*/
12-
final class CachedSingleQuery {
13+
final class Cached {
1314
/**
14-
* @param SingleQuery<P> $query
15+
* @param Query<P> $query
1516
*/
1617
public function __construct(
17-
public SingleQuery $query,
18+
public Query $query,
1819
public int $updateFrequencyTicks,
1920
) {
2021
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SOFe\Capital\Analytics\Single;
6+
7+
use Closure;
8+
use Generator;
9+
use SOFe\AwaitStd\AwaitStd;
10+
use SOFe\Capital\Database\Database;
11+
use SOFe\InfoAPI\NumberInfo;
12+
13+
final class CachedValue {
14+
public function __construct(public ?float $value) {
15+
}
16+
17+
public function asInfo() : ?NumberInfo {
18+
return $this->value !== null ? new NumberInfo($this->value) : null;
19+
}
20+
21+
/**
22+
* @template P
23+
* @param Closure(): bool $continue Whether to continue the loop.
24+
* @param Cached<P> $cache The cached query.
25+
* @param P $p
26+
* @return VoidPromise
27+
*/
28+
public function loop(Closure $continue, AwaitStd $std, Cached $cache, $p, Database $db) : Generator {
29+
while ($continue()) {
30+
$this->value = yield from $cache->query->fetch($p, $db);
31+
32+
yield from $std->sleep($cache->updateFrequencyTicks);
33+
}
34+
}
35+
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SOFe\Capital\Analytics\Single;
6+
7+
use SOFe\AwaitStd\AwaitStd;
8+
use SOFe\Capital\Analytics;
9+
use SOFe\Capital\Database\Database;
10+
use SOFe\Capital\Di\FromContext;
11+
use SOFe\Capital\Di\Singleton;
12+
use SOFe\Capital\Di\SingletonArgs;
13+
use SOFe\Capital\Di\SingletonTrait;
14+
use SOFe\Capital\Plugin\MainClass;
15+
16+
final class Mod implements Singleton, FromContext {
17+
use SingletonArgs, SingletonTrait;
18+
19+
public const API_VERSION = "0.1.0";
20+
21+
public static function fromSingletonArgs(Analytics\Config $config, MainClass $plugin, AwaitStd $std, Database $db) : self {
22+
foreach ($config->singleQueries as $manager) {
23+
$manager->register($plugin, $std, $db);
24+
}
25+
26+
return new self;
27+
}
28+
}

0 commit comments

Comments
 (0)