useOverviewHistory.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import { useEffect, useMemo, useState } from 'react';
  2. import { HttpUtil, TimeFormatter } from '@/utils';
  3. import type { Status } from '@/models/status';
  4. const OVERVIEW_WINDOW = 72;
  5. const SEED_BUCKET_SECONDS = 2;
  6. const SERIES_KEYS = ['cpu', 'mem', 'swap', 'diskUsage', 'netUp', 'netDown', 'tcpCount', 'udpCount'] as const;
  7. export type OverviewSeriesKey = (typeof SERIES_KEYS)[number];
  8. export interface OverviewHistory {
  9. series: Record<OverviewSeriesKey, number[]>;
  10. labels: string[];
  11. }
  12. interface HistoryPoint {
  13. t: number;
  14. v: number;
  15. }
  16. interface HistoryWindow {
  17. series: Record<OverviewSeriesKey, number[]>;
  18. times: number[];
  19. }
  20. function emptySeries(): Record<OverviewSeriesKey, number[]> {
  21. return Object.fromEntries(SERIES_KEYS.map((key) => [key, [] as number[]])) as Record<OverviewSeriesKey, number[]>;
  22. }
  23. function emptyWindow(): HistoryWindow {
  24. return { series: emptySeries(), times: [] };
  25. }
  26. function sampleOf(status: Status): Record<OverviewSeriesKey, number> {
  27. return {
  28. cpu: status.cpu.percent,
  29. mem: status.mem.percent,
  30. swap: status.swap.percent,
  31. diskUsage: status.disk.percent,
  32. netUp: status.netIO.up,
  33. netDown: status.netIO.down,
  34. tcpCount: status.tcpCount,
  35. udpCount: status.udpCount,
  36. };
  37. }
  38. function tailWindow<T>(values: T[]): T[] {
  39. return values.slice(-OVERVIEW_WINDOW);
  40. }
  41. export function mean(values: number[]): number {
  42. if (values.length === 0) return 0;
  43. let total = 0;
  44. for (const v of values) total += v;
  45. return total / values.length;
  46. }
  47. export function peak(values: number[]): number {
  48. let max = 0;
  49. for (const v of values) if (v > max) max = v;
  50. return max;
  51. }
  52. /* the seed bucket must be in the backend's allowedHistoryBuckets whitelist;
  53. 2s is the smallest and matches the status poll cadence */
  54. export function useOverviewHistory(status: Status, hasData: boolean): OverviewHistory {
  55. const [trend, setTrend] = useState<HistoryWindow>(emptyWindow);
  56. useEffect(() => {
  57. let cancelled = false;
  58. const seed = async () => {
  59. const responses = new Map<OverviewSeriesKey, HistoryPoint[]>();
  60. await Promise.all(
  61. SERIES_KEYS.map(async (key) => {
  62. const msg = await HttpUtil.get<HistoryPoint[]>(
  63. `/panel/api/server/history/${key}/${SEED_BUCKET_SECONDS}`,
  64. undefined,
  65. { silent: true },
  66. );
  67. if (msg?.success && Array.isArray(msg.obj)) responses.set(key, msg.obj);
  68. }),
  69. );
  70. if (cancelled || responses.size === 0) return;
  71. let axis: HistoryPoint[] = [];
  72. for (const points of responses.values()) {
  73. if (points.length > axis.length) axis = points;
  74. }
  75. axis = tailWindow(axis);
  76. if (axis.length === 0) return;
  77. const seedTimes = axis.map((p) => Number(p.t) || 0);
  78. const seedSeries = emptySeries();
  79. for (const key of SERIES_KEYS) {
  80. const byTs = new Map<number, number>();
  81. for (const p of responses.get(key) ?? []) byTs.set(Number(p.t) || 0, Number(p.v) || 0);
  82. seedSeries[key] = seedTimes.map((ts) => byTs.get(ts) ?? 0);
  83. }
  84. setTrend((prev) => {
  85. const merged = emptyWindow();
  86. merged.times = tailWindow(seedTimes.concat(prev.times));
  87. for (const key of SERIES_KEYS) {
  88. merged.series[key] = tailWindow(seedSeries[key].concat(prev.series[key]));
  89. }
  90. return merged;
  91. });
  92. };
  93. seed().catch(() => undefined);
  94. return () => {
  95. cancelled = true;
  96. };
  97. }, []);
  98. useEffect(() => {
  99. if (!hasData) return;
  100. setTrend((prev) => {
  101. const point = sampleOf(status);
  102. const next = emptyWindow();
  103. next.times = tailWindow(prev.times.concat(Math.floor(Date.now() / 1000)));
  104. for (const key of SERIES_KEYS) {
  105. next.series[key] = tailWindow(prev.series[key].concat(point[key]));
  106. }
  107. return next;
  108. });
  109. }, [status, hasData]);
  110. const labels = useMemo(() => trend.times.map(TimeFormatter.formatClock), [trend.times]);
  111. return useMemo(() => ({ series: trend.series, labels }), [trend.series, labels]);
  112. }