Stream car usage aggregation

This commit is contained in:
seo
2026-06-26 01:34:24 +09:00
parent ec8adbd56d
commit 2e9c4eaf4a
2 changed files with 117 additions and 88 deletions
+116 -88
View File
@@ -204,24 +204,10 @@ function monthly_data_usage(PDO $pdo): array
$monthStart = $now->modify('first day of this month')->setTime(0, 0, 0);
$nextMonthStart = $monthStart->modify('first day of next month');
$stmt = $pdo->prepare(""
. "SELECT ts, source, cmd, sent_bytes, received_bytes, total_bytes, tcp_ok, tcp_error "
. "FROM car_data_usage "
. "WHERE id='car' AND ts >= :month_start AND ts < :next_month_start "
. "ORDER BY ts ASC"
);
$stmt->execute([
':month_start' => $monthStart->format('Y-m-d H:i:s'),
':next_month_start' => $nextMonthStart->format('Y-m-d H:i:s'),
]);
$usageRows = $stmt->fetchAll();
$usage = calibrated_usage_rows($usageRows);
$todayStart = $now->setTime(0, 0, 0);
$tomorrowStart = $todayStart->modify('+1 day');
$todayUsage = calibrated_usage_rows(array_values(array_filter($usageRows, function (array $row) use ($todayStart, $tomorrowStart): bool {
$ts = strtotime((string)($row['ts'] ?? ''));
return $ts !== false && $ts >= $todayStart->getTimestamp() && $ts < $tomorrowStart->getTimestamp();
})));
$usage = calibrated_usage_range($pdo, $monthStart, $nextMonthStart);
$todayUsage = calibrated_usage_range($pdo, $todayStart, $tomorrowStart);
$sentBytes = $usage['sent_bytes'];
$receivedBytes = $usage['received_bytes'];
@@ -339,51 +325,57 @@ function floor_krw_10(float $value): int
return (int)floor(max(0.0, $value) / 10) * 10;
}
function calibrated_usage_rows(array $rows): array
function usage_success_averages(PDO $pdo, DateTimeInterface $start, DateTimeInterface $end): array
{
$sentBytes = 0;
$receivedBytes = 0;
$totalBytes = 0;
$successfulTotalBytes = 0;
$successfulCount = 0;
$successByCmd = [];
$firstUsageTs = null;
$lastUsageTs = null;
$avgByCmd = [];
foreach ($rows as $row) {
$stmt = $pdo->prepare(""
. "SELECT cmd, SUM(total_bytes) AS total_bytes, COUNT(*) AS sample_count "
. "FROM car_data_usage "
. "WHERE id='car' AND ts >= :start_ts AND ts < :end_ts "
. "AND tcp_ok = 1 AND total_bytes > 0 "
. "GROUP BY cmd"
);
$stmt->execute([
':start_ts' => $start->format('Y-m-d H:i:s'),
':end_ts' => $end->format('Y-m-d H:i:s'),
]);
while ($row = $stmt->fetch()) {
$cmd = (string)($row['cmd'] ?? '');
$sent = (int)($row['sent_bytes'] ?? 0);
$received = (int)($row['received_bytes'] ?? 0);
$total = (int)($row['total_bytes'] ?? 0);
$tcpOk = (int)($row['tcp_ok'] ?? 0) === 1;
$sentBytes += $sent;
$receivedBytes += $received;
$totalBytes += $total;
if ($firstUsageTs === null && !empty($row['ts'])) {
$firstUsageTs = (string)$row['ts'];
}
if (!empty($row['ts'])) {
$lastUsageTs = (string)$row['ts'];
$count = (int)($row['sample_count'] ?? 0);
if ($count <= 0) {
continue;
}
if ($tcpOk && $total > 0) {
$successfulTotalBytes += $total;
$successfulCount++;
if (!isset($successByCmd[$cmd])) {
$successByCmd[$cmd] = ['total' => 0, 'count' => 0];
}
$successByCmd[$cmd]['total'] += $total;
$successByCmd[$cmd]['count']++;
}
$successfulTotalBytes += $total;
$successfulCount += $count;
$avgByCmd[$cmd] = (int)round($total / $count);
}
$globalAverage = $successfulCount > 0 ? (int)round($successfulTotalBytes / $successfulCount) : 0;
$avgByCmd = [];
foreach ($successByCmd as $cmd => $stat) {
$avgByCmd[$cmd] = $stat['count'] > 0 ? (int)round($stat['total'] / $stat['count']) : $globalAverage;
}
return [
'avg_by_cmd' => $avgByCmd,
'global_average' => $globalAverage,
'success_count' => $successfulCount,
];
}
function calibrated_usage_range(PDO $pdo, DateTimeInterface $start, DateTimeInterface $end): array
{
$averages = usage_success_averages($pdo, $start, $end);
$avgByCmd = $averages['avg_by_cmd'];
$globalAverage = $averages['global_average'];
$sentBytes = 0;
$receivedBytes = 0;
$totalBytes = 0;
$firstUsageTs = null;
$lastUsageTs = null;
$adjustedTotalBytes = 0;
$adjustedFailureBytes = 0;
@@ -391,47 +383,83 @@ function calibrated_usage_rows(array $rows): array
$connectFailureCount = 0;
$receiveGapExcludedCount = 0;
$lastSuccessfulTs = null;
$sampleCount = 0;
foreach ($rows as $row) {
$cmd = (string)($row['cmd'] ?? '');
$sent = (int)($row['sent_bytes'] ?? 0);
$total = (int)($row['total_bytes'] ?? 0);
$tcpOk = (int)($row['tcp_ok'] ?? 0) === 1;
$rowTs = strtotime((string)($row['ts'] ?? ''));
$stmt = null;
$previousBuffered = $pdo->getAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY);
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
if ($tcpOk) {
$adjustedTotalBytes += $total;
if ($rowTs !== false) {
$lastSuccessfulTs = $rowTs;
try {
$stmt = $pdo->prepare(""
. "SELECT ts, cmd, sent_bytes, received_bytes, total_bytes, tcp_ok "
. "FROM car_data_usage "
. "WHERE id='car' AND ts >= :start_ts AND ts < :end_ts "
. "ORDER BY ts ASC"
);
$stmt->execute([
':start_ts' => $start->format('Y-m-d H:i:s'),
':end_ts' => $end->format('Y-m-d H:i:s'),
]);
while ($row = $stmt->fetch()) {
$sampleCount++;
$cmd = (string)($row['cmd'] ?? '');
$sent = (int)($row['sent_bytes'] ?? 0);
$received = (int)($row['received_bytes'] ?? 0);
$total = (int)($row['total_bytes'] ?? 0);
$tcpOk = (int)($row['tcp_ok'] ?? 0) === 1;
$rowTs = strtotime((string)($row['ts'] ?? ''));
$sentBytes += $sent;
$receivedBytes += $received;
$totalBytes += $total;
if ($firstUsageTs === null && !empty($row['ts'])) {
$firstUsageTs = (string)$row['ts'];
}
continue;
if (!empty($row['ts'])) {
$lastUsageTs = (string)$row['ts'];
}
if ($tcpOk) {
$adjustedTotalBytes += $total;
if ($rowTs !== false) {
$lastSuccessfulTs = $rowTs;
}
continue;
}
if ($sent <= 0) {
// TCP 연결 자체가 실패한 건은 모뎀까지 명령이 전달되지 않은 것으로 보고 과금 추정에서 제외한다.
$connectFailureCount++;
continue;
}
$connectedFailureCount++;
if (
$lastSuccessfulTs === null ||
$rowTs === false ||
($rowTs - $lastSuccessfulTs) > RECEIVE_GAP_LIMIT_SEC
) {
$receiveGapExcludedCount++;
continue;
}
$estimatedBytes = $avgByCmd[$cmd] ?? $globalAverage;
if ($estimatedBytes <= 0) {
$estimatedBytes = max($sent, $total);
}
$adjustedTotalBytes += $estimatedBytes;
$adjustedFailureBytes += $estimatedBytes;
}
if ($sent <= 0) {
// TCP 연결 자체가 실패한 건은 모뎀까지 명령이 전달되지 않은 것으로 보고 과금 추정에서 제외한다.
$connectFailureCount++;
continue;
} finally {
if ($stmt instanceof PDOStatement) {
$stmt->closeCursor();
}
$connectedFailureCount++;
if (
$lastSuccessfulTs === null ||
$rowTs === false ||
($rowTs - $lastSuccessfulTs) > RECEIVE_GAP_LIMIT_SEC
) {
$receiveGapExcludedCount++;
continue;
}
$estimatedBytes = $avgByCmd[$cmd] ?? $globalAverage;
if ($estimatedBytes <= 0) {
$estimatedBytes = max($sent, $total);
}
$adjustedTotalBytes += $estimatedBytes;
$adjustedFailureBytes += $estimatedBytes;
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $previousBuffered);
}
return [
@@ -443,8 +471,8 @@ function calibrated_usage_rows(array $rows): array
'connected_failure_count' => $connectedFailureCount,
'connect_failure_count' => $connectFailureCount,
'receive_gap_excluded_count' => $receiveGapExcludedCount,
'sample_count' => count($rows),
'success_count' => $successfulCount,
'sample_count' => $sampleCount,
'success_count' => $averages['success_count'],
'success_average_bytes' => $globalAverage,
'first_usage_ts' => $firstUsageTs,
'last_usage_ts' => $lastUsageTs,