} */ public function series(Server $server, int $windowSeconds): array { $windowSeconds = max(300, $windowSeconds); // Bucket width is at least the 1/min sample interval, so a bucket holds ~one sample and the // line stays continuous instead of zig-zagging between half-empty buckets. $width = max(60, (int) floor($windowSeconds / 120)); $buckets = (int) ceil($windowSeconds / $width); $now = time(); $from = $now - $windowSeconds; $rows = MetricSample::query() ->where('server_id', $server->id) ->where('sampled_at', '>=', now()->subSeconds($windowSeconds)) ->orderBy('sampled_at') ->get(['cpu', 'mem', 'disk', 'load', 'sampled_at']); $sum = []; foreach ($rows as $r) { $idx = (int) floor(($r->sampled_at->getTimestamp() - $from) / $width); if ($idx < 0) { continue; } if ($idx >= $buckets) { $idx = $buckets - 1; } $sum[$idx] ??= ['cpu' => 0, 'mem' => 0, 'disk' => 0, 'load' => 0.0, 'n' => 0]; $sum[$idx]['cpu'] += (int) $r->cpu; $sum[$idx]['mem'] += (int) $r->mem; $sum[$idx]['disk'] += (int) $r->disk; $sum[$idx]['load'] += (float) $r->load; $sum[$idx]['n']++; } ksort($sum); $points = []; foreach ($sum as $i => $b) { $n = $b['n']; $points[] = [ 't' => (int) round($from + ($i + 0.5) * $width), 'cpu' => (int) round($b['cpu'] / $n), 'mem' => (int) round($b['mem'] / $n), 'disk' => (int) round($b['disk'] / $n), 'load' => round($b['load'] / $n, 2), ]; } return ['window' => $windowSeconds, 'from' => $from, 'now' => $now, 'bucket' => $width, 'points' => $points]; } }