Skip to content

Commit 9cc0efe

Browse files
committed
feat: move dashboard log data processing to web worker
1 parent 0c2471a commit 9cc0efe

File tree

8 files changed

+492
-875
lines changed

8 files changed

+492
-875
lines changed

dashboard/lib/analytics.worker.ts

Lines changed: 309 additions & 35 deletions
Large diffs are not rendered by default.

dashboard/lib/components/activity.tsx

Lines changed: 45 additions & 250 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
import { Chart as ChartJS, BarElement, LinearScale, CategoryScale, TimeScale, Tooltip, Legend, ChartData } from "chart.js";
44
import { useEffect, useMemo, useState, useRef, memo } from "react";
55
import { Bar } from "react-chartjs-2";
6-
import { NginxLog } from "@/lib/types";
76
import 'chartjs-adapter-date-fns';
8-
import { getDateRange, Period, periodStart } from "@/lib/period";
7+
import { Period } from "@/lib/period";
98

109
ChartJS.register(
1110
BarElement,
@@ -16,149 +15,6 @@ ChartJS.register(
1615
Legend
1716
);
1817

19-
function getDayId(date: Date) {
20-
if (!(date instanceof Date)) {
21-
throw new Error("Invalid date object");
22-
}
23-
24-
return new Date(date).setHours(0, 0, 0, 0); // Set minutes, seconds, and milliseconds to zero
25-
}
26-
27-
function getHourId(date: Date) {
28-
if (!(date instanceof Date)) {
29-
throw new Error("Invalid date object");
30-
}
31-
32-
return new Date(date).setMinutes(0, 0, 0); // Set minutes, seconds, and milliseconds to zero
33-
}
34-
35-
function get6HourId(date: Date) {
36-
if (!(date instanceof Date)) {
37-
throw new Error("Invalid date object");
38-
}
39-
const ms6h = 6 * 60 * 60 * 1000;
40-
return Math.floor(date.getTime() / ms6h) * ms6h;
41-
}
42-
43-
function get5MinuteId(date: Date) {
44-
if (!(date instanceof Date)) {
45-
throw new Error("Invalid date object");
46-
}
47-
48-
const msPer5Min = 5 * 60 * 1000; // 5 minutes in milliseconds
49-
return (new Date(Math.round(date.getTime() / msPer5Min) * msPer5Min)).getTime();
50-
}
51-
52-
function getMinuteId(date: Date) {
53-
if (!(date instanceof Date)) {
54-
throw new Error("Invalid date object");
55-
}
56-
57-
return new Date(date).setSeconds(0, 0); // Changed to use setSeconds instead
58-
}
59-
60-
const getStepSize = (period: Period, data: NginxLog[]) => {
61-
switch (period) {
62-
case '24 hours':
63-
return 300000; // 5 minutes
64-
case 'week':
65-
return 3600000; // 1 hour
66-
case 'month':
67-
return 21600000; // 6 hours
68-
case '6 months':
69-
return 86400000; // 1 day
70-
case 'all time':
71-
const range = getDateRange(data);
72-
if (!range) {
73-
return 8.64e+7; // day
74-
}
75-
76-
const diff = range.end - range.start;
77-
if (diff <= 86400000) {
78-
return 300000; // 5 minutes
79-
} else if (diff <= 604800000) {
80-
return 3600000; // hour
81-
} else {
82-
return 8.64e+7; // day
83-
}
84-
default:
85-
return 8.64e+7; // day
86-
}
87-
}
88-
89-
const incrementDate = (date: Date, period: Period) => {
90-
switch (period) {
91-
case '24 hours':
92-
return new Date(date.setMinutes(date.getMinutes() + 5));
93-
case 'week':
94-
return new Date(date.setHours(date.getHours() + 1));
95-
case 'month':
96-
return new Date(date.setHours(date.getHours() + 6));
97-
case '6 months':
98-
case 'all time':
99-
default:
100-
return new Date(date.setDate(date.getDate() + 1));
101-
}
102-
}
103-
104-
const getTimeIdGetter = (period: Period, data: NginxLog[]) => {
105-
switch (period) {
106-
case '24 hours':
107-
return get5MinuteId
108-
case 'week':
109-
return getHourId
110-
case 'month':
111-
return get6HourId
112-
case '6 months':
113-
return getDayId
114-
case 'all time':
115-
const range = getDateRange(data);
116-
if (!range) {
117-
return getDayId;
118-
}
119-
120-
const diff = range.end - range.start;
121-
if (diff <= 86400000) {
122-
return get5MinuteId;
123-
} else if (diff <= 604800000) {
124-
return getHourId;
125-
} else {
126-
return getDayId;
127-
}
128-
default:
129-
return getDayId
130-
}
131-
}
132-
133-
const getTimeUnit = (period: Period, data: NginxLog[]) => {
134-
switch (period) {
135-
case '24 hours':
136-
return 'minute'
137-
case 'week':
138-
return 'hour'
139-
case 'month':
140-
return 'hour'
141-
case '6 months':
142-
return 'day'
143-
case 'all time':
144-
const range = getDateRange(data);
145-
if (!range) {
146-
return 'day';
147-
}
148-
149-
const diff = range.end - range.start;
150-
if (diff <= 86400000) {
151-
return 'minute';
152-
} else if (diff <= 604800000) {
153-
return 'hour';
154-
} else {
155-
return 'day';
156-
}
157-
default:
158-
return 'day'
159-
}
160-
}
161-
16218
const getSuccessRateLevel = (successRate: number | null) => {
16319
if (successRate === null) {
16420
return null
@@ -204,7 +60,21 @@ function calculateDisplayRates(rates: { timestamp: number, value: number | null
20460
return sampled;
20561
}
20662

207-
function Activity({ data, period }: { data: NginxLog[], period: Period }) {
63+
function Activity({
64+
period,
65+
activityBuckets,
66+
activitySuccessRates,
67+
activityPeriodLabels,
68+
activityStepSize,
69+
activityTimeUnit,
70+
}: {
71+
period: Period;
72+
activityBuckets: { timestamp: number; requests: number; users: number }[];
73+
activitySuccessRates: { timestamp: number; successRate: number | null }[];
74+
activityPeriodLabels: { start: string; end: string };
75+
activityStepSize: number;
76+
activityTimeUnit: 'minute' | 'hour' | 'day';
77+
}) {
20878
const [containerWidth, setContainerWidth] = useState<number>(0);
20979
const containerRef = useRef<HTMLDivElement>(null);
21080

@@ -239,64 +109,15 @@ function Activity({ data, period }: { data: NginxLog[], period: Period }) {
239109
};
240110
}, []); // Remove containerWidth from dependencies to prevent circular updates
241111

242-
// Single pass over data to compute chart data, success rates, and period labels
243-
const { plotData, plotOptions, successRates, periodLabels } = useMemo(() => {
244-
const chartPoints: { [id: string]: { requests: number; users: Set<string> } } = {};
245-
const ratePoints: { [id: string]: { success: number, total: number } } = {};
246-
247-
const start = periodStart(period);
248-
const getTimeId = getTimeIdGetter(period, data);
249-
250-
let currentDate: Date;
251-
let end: Date;
252-
if (start === null) {
253-
const range = getDateRange(data);
254-
if (!range) {
255-
return { plotData: null, plotOptions: null, successRates: [], periodLabels: { start: '', end: '' } };
256-
}
257-
currentDate = new Date(range.start);
258-
end = new Date(range.end);
259-
} else {
260-
end = new Date();
261-
currentDate = new Date(start);
262-
}
263-
264-
while (currentDate <= end) {
265-
const timeId = getTimeId(currentDate);
266-
chartPoints[timeId] = { requests: 0, users: new Set() };
267-
ratePoints[timeId] = { success: 0, total: 0 };
268-
currentDate = incrementDate(currentDate, period);
269-
}
270-
271-
// Single loop over data for both chart points and success rates
272-
for (const row of data) {
273-
if (!row.timestamp) continue;
274-
275-
const timeId = getTimeId(row.timestamp);
276-
const userId = `${row.ipAddress}::${row.userAgent}`;
277-
278-
if (chartPoints[timeId]) {
279-
chartPoints[timeId].requests++;
280-
chartPoints[timeId].users.add(userId);
281-
} else {
282-
chartPoints[timeId] = { requests: 1, users: new Set([userId]) };
283-
}
284-
285-
if (row.status) {
286-
if (!ratePoints[timeId]) {
287-
ratePoints[timeId] = { success: 0, total: 0 };
288-
}
289-
if (row.status >= 200 && row.status <= 399) {
290-
ratePoints[timeId].success++;
291-
}
292-
ratePoints[timeId].total++;
293-
}
112+
const { plotData, plotOptions, successRates } = useMemo(() => {
113+
if (activityBuckets.length === 0) {
114+
return { plotData: null, plotOptions: null, successRates: [] };
294115
}
295116

296-
const values = Object.entries(chartPoints).map(([x, y]) => ({
297-
x: new Date(parseInt(x)),
298-
requests: y.requests - y.users.size,
299-
users: y.users.size
117+
const values = activityBuckets.map(b => ({
118+
x: new Date(b.timestamp),
119+
requests: b.requests,
120+
users: b.users,
300121
}));
301122

302123
const plotData: ChartData<"bar"> = {
@@ -320,31 +141,34 @@ function Activity({ data, period }: { data: NginxLog[], period: Period }) {
320141
]
321142
};
322143

144+
// For the x-axis max, we need the current time bucketed to the same granularity
145+
let nowId: number;
146+
switch (activityTimeUnit) {
147+
case 'minute': nowId = Math.round(Date.now() / 300000) * 300000; break;
148+
case 'hour': nowId = new Date().setMinutes(0, 0, 0); break;
149+
default: nowId = new Date().setHours(0, 0, 0, 0); break;
150+
}
151+
323152
const plotOptions: object = {
324153
scales: {
325154
x: {
326155
type: 'time',
327156
display: false,
328-
grid: {
329-
display: false
330-
},
331-
max: period === 'all time' ? undefined : getTimeId(new Date())
157+
grid: { display: false },
158+
time: { unit: activityTimeUnit, stepSize: activityStepSize },
159+
max: period === 'all time' ? undefined : nowId,
332160
},
333161
y: {
334162
display: false,
335-
title: {
336-
text: 'Requests'
337-
},
163+
title: { text: 'Requests' },
338164
min: 0,
339-
stacked: true
165+
stacked: true,
340166
}
341167
},
342168
maintainAspectRatio: false,
343169
responsive: true,
344170
plugins: {
345-
legend: {
346-
display: false
347-
},
171+
legend: { display: false },
348172
tooltip: {
349173
enabled: true,
350174
callbacks: {
@@ -365,42 +189,13 @@ function Activity({ data, period }: { data: NginxLog[], period: Period }) {
365189
}
366190
};
367191

368-
const successRates = Object.entries(ratePoints)
369-
.sort(([t1], [t2]) => Number(t2) - Number(t1))
370-
.map(([timeId, value]) => ({
371-
timestamp: Number(timeId),
372-
value: value.total ? value.success / value.total : null
373-
}))
374-
.reverse();
375-
376-
let periodLabels = { start: '', end: '' };
377-
switch (period) {
378-
case '24 hours':
379-
periodLabels = { start: '24 hours ago', end: 'Now' };
380-
break;
381-
case 'week':
382-
periodLabels = { start: 'One week ago', end: 'Now' };
383-
break;
384-
case 'month':
385-
periodLabels = { start: 'One month ago', end: 'Now' };
386-
break;
387-
case '6 months':
388-
periodLabels = { start: 'Six months ago', end: 'Now' };
389-
break;
390-
default: {
391-
const range = getDateRange(data);
392-
if (range) {
393-
periodLabels = {
394-
start: new Date(range.start).toLocaleDateString(),
395-
end: new Date(range.end).toLocaleDateString()
396-
};
397-
}
398-
break;
399-
}
400-
}
192+
const successRates = activitySuccessRates.map(r => ({
193+
timestamp: r.timestamp,
194+
value: r.successRate,
195+
}));
401196

402-
return { plotData, plotOptions, successRates, periodLabels };
403-
}, [data, period]);
197+
return { plotData, plotOptions, successRates };
198+
}, [activityBuckets, activitySuccessRates, activityStepSize, activityTimeUnit, period]);
404199

405200
// Only recalculate display rates when successRates or container width changes
406201
const displayRates = useMemo(
@@ -446,8 +241,8 @@ function Activity({ data, period }: { data: NginxLog[], period: Period }) {
446241

447242
<div className="pb-0">
448243
<div className="flex justify-between mt-2 mb-1 overflow-hidden text-xs text-[var(--text-muted3)] mx-1">
449-
<div>{periodLabels.start}</div>
450-
<div>{periodLabels.end}</div>
244+
<div>{activityPeriodLabels.start}</div>
245+
<div>{activityPeriodLabels.end}</div>
451246
</div>
452247
</div>
453248
</div>

0 commit comments

Comments
 (0)